@mrrlin-dev/mcp 0.4.95 → 0.4.97

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.
Files changed (2) hide show
  1. package/dist/bin.cjs +1834 -256
  2. package/package.json +1 -1
package/dist/bin.cjs CHANGED
@@ -32,6 +32,7 @@ var __toESM = (mod2, isNodeMode, target) => (target = mod2 != null ? __create(__
32
32
  isNodeMode || !mod2 || !mod2.__esModule ? __defProp(target, "default", { value: mod2, enumerable: true }) : target,
33
33
  mod2
34
34
  ));
35
+ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: true }), mod2);
35
36
 
36
37
  // ../../node_modules/.pnpm/@iarna+toml@2.2.5/node_modules/@iarna/toml/lib/parser.js
37
38
  var require_parser = __commonJS({
@@ -21655,6 +21656,11 @@ var init_install_public_mcp = __esm({
21655
21656
  });
21656
21657
 
21657
21658
  // src/bin.ts
21659
+ var bin_exports = {};
21660
+ __export(bin_exports, {
21661
+ augmentProcessPathWithCommonDirs: () => augmentProcessPathWithCommonDirs
21662
+ });
21663
+ module.exports = __toCommonJS(bin_exports);
21658
21664
  var import_node_path42 = __toESM(require("node:path"), 1);
21659
21665
  var import_node_child_process22 = require("node:child_process");
21660
21666
  var import_node_fs25 = require("node:fs");
@@ -37372,6 +37378,7 @@ var openApiOperationIds = {
37372
37378
  promoteSpecReadyCas: "promoteSpecReadyCas",
37373
37379
  recordSpecConsensusReview: "recordSpecConsensusReview",
37374
37380
  getSpecConsensusReview: "getSpecConsensusReview",
37381
+ finalizeSpecReviewCas: "finalizeSpecReviewCas",
37375
37382
  onboardingKickoffTransition: "onboardingKickoffTransition",
37376
37383
  ingestTelemetry: "ingestTelemetry",
37377
37384
  ingestOperatorTelemetry: "ingestOperatorTelemetry",
@@ -37642,6 +37649,63 @@ var PERSONA_ROLE_MAP = {
37642
37649
  "spec-author": "spec"
37643
37650
  };
37644
37651
  var UNNAMED_PERSONAS = ["plan-reviewer", "scope-analyst"];
37652
+ var mrrlinSpecOwnershipSnapshotSchema = external_exports.discriminatedUnion("kind", [
37653
+ external_exports.object({
37654
+ kind: external_exports.literal("task"),
37655
+ taskId: external_exports.string().min(1),
37656
+ domains: external_exports.array(external_exports.string()).default(["code"])
37657
+ }),
37658
+ external_exports.object({
37659
+ kind: external_exports.literal("plan"),
37660
+ planId: external_exports.string().min(1),
37661
+ domains: external_exports.array(external_exports.string()).default(["code"])
37662
+ }),
37663
+ external_exports.object({
37664
+ kind: external_exports.literal("project"),
37665
+ domains: external_exports.array(external_exports.string()).default(["code"])
37666
+ })
37667
+ ]);
37668
+ var SPEC_CONSENSUS_OUTCOMES = [
37669
+ "CONVERGED",
37670
+ "approved",
37671
+ "CONSENSUS_BLOCKED",
37672
+ "CONSENSUS_BLOCKED_CLARIFICATION",
37673
+ "SKIPPED"
37674
+ ];
37675
+ var specConsensusOutcomeSchema = external_exports.enum(SPEC_CONSENSUS_OUTCOMES);
37676
+ var mrrlinFinalizeSpecReviewCasBodySchema = external_exports.object({
37677
+ expectedContentHash: external_exports.string().min(1),
37678
+ reviewHash: external_exports.string().min(1).optional(),
37679
+ ownership: mrrlinSpecOwnershipSnapshotSchema,
37680
+ review: external_exports.object({
37681
+ outcome: specConsensusOutcomeSchema,
37682
+ rounds: external_exports.number().int().nonnegative().default(0),
37683
+ suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.nullish(),
37684
+ history: mrrlinSpecConsensusHistorySchema.nullish(),
37685
+ highlights: mrrlinSpecConsensusHighlightsSchema.nullish()
37686
+ }),
37687
+ specReady: external_exports.boolean().default(false)
37688
+ }).refine((data) => {
37689
+ if (data.specReady && (data.review.outcome === "CONSENSUS_BLOCKED" || data.review.outcome === "CONSENSUS_BLOCKED_CLARIFICATION")) {
37690
+ return false;
37691
+ }
37692
+ return true;
37693
+ }, {
37694
+ message: "specReady cannot be true when review outcome is blocked",
37695
+ path: ["specReady"]
37696
+ });
37697
+ var mrrlinFinalizeSpecReviewCasResponseSchema = external_exports.object({
37698
+ data: external_exports.discriminatedUnion("status", [
37699
+ external_exports.object({
37700
+ status: external_exports.literal("applied"),
37701
+ promoted: external_exports.boolean()
37702
+ }),
37703
+ external_exports.object({
37704
+ status: external_exports.literal("superseded"),
37705
+ reason: external_exports.string()
37706
+ })
37707
+ ])
37708
+ });
37645
37709
 
37646
37710
  // ../../packages/schemas/dist/asyncJob.js
37647
37711
  var ASYNC_JOB_STATUSES = ["queued", "running", "done", "failed", "superseded"];
@@ -37713,7 +37777,7 @@ var mrrlinAsyncJobBeginAttemptSchema = external_exports.object({
37713
37777
  });
37714
37778
  var mrrlinAsyncJobTerminalizeSchema = external_exports.object({
37715
37779
  leaseId: external_exports.string().min(1),
37716
- status: external_exports.enum(["done", "failed"]),
37780
+ status: external_exports.enum(["done", "failed", "superseded"]),
37717
37781
  resultJson: external_exports.string().nullable().optional(),
37718
37782
  error: external_exports.string().nullable().optional(),
37719
37783
  failureReason: mrrlinAsyncJobFailureReasonSchema.nullable().optional(),
@@ -38016,13 +38080,36 @@ var DEFAULT_INTEGRATION_ROUTES = {
38016
38080
  operations: {
38017
38081
  dispatch: "slack_post_message"
38018
38082
  }
38083
+ },
38084
+ publish: {
38085
+ capability: "publish",
38086
+ artifactKind: "page",
38087
+ operations: {
38088
+ preview: "publish_preview",
38089
+ release: "publish_release",
38090
+ rollback: "publish_rollback",
38091
+ observe: "publish_observe"
38092
+ }
38093
+ },
38094
+ "local-preview": {
38095
+ capability: "publish",
38096
+ artifactKind: "page",
38097
+ operations: {
38098
+ preview: "publish_preview",
38099
+ release: "publish_release",
38100
+ rollback: "publish_rollback",
38101
+ observe: "publish_observe"
38102
+ }
38019
38103
  }
38020
38104
  };
38021
38105
  var SERVER_ALIASES = {
38022
38106
  "github-mcp": "github",
38023
38107
  gh: "github",
38024
38108
  "github-server": "github",
38025
- "slack-mcp": "slack"
38109
+ "slack-mcp": "slack",
38110
+ "static-publisher": "publish",
38111
+ "local-publisher": "publish",
38112
+ preview: "local-preview"
38026
38113
  };
38027
38114
  function getDefaultCapabilityRouteForServer(serverId) {
38028
38115
  const normalizedId = (SERVER_ALIASES[serverId.toLowerCase()] ?? serverId).toLowerCase();
@@ -38052,10 +38139,14 @@ var mrrlinTaskEventAutonomyLevelSchema = external_exports.enum([
38052
38139
  ]);
38053
38140
  var mrrlinAutoDeploySchema = external_exports.enum(["off", "dev", "prod"]);
38054
38141
  var mrrlinTaskDomainsSchema = external_exports.array(external_exports.string()).min(1);
38055
- function domainScopedReviewHash(rawHash, domain2) {
38056
- const d = Array.isArray(domain2) ? domain2.map(String) : [String(domain2 || "code")];
38142
+ function normalizeDomainArray(domains) {
38143
+ const d = Array.isArray(domains) ? domains.map(String) : [String(domains || "code")];
38057
38144
  const normalized = d.map((v) => v.trim().toLowerCase()).filter(Boolean);
38058
38145
  normalized.sort();
38146
+ return normalized.length === 0 ? ["code"] : Array.from(new Set(normalized));
38147
+ }
38148
+ function domainScopedReviewHash(rawHash, domain2) {
38149
+ const normalized = normalizeDomainArray(domain2);
38059
38150
  return normalized.length === 1 && normalized[0] === "code" ? rawHash : `${rawHash}@${normalized.join(",")}`;
38060
38151
  }
38061
38152
  var mrrlinDeclaredOutcomeShapeSchema = external_exports.object({
@@ -38355,7 +38446,8 @@ var mrrlinTaskSchema = external_exports.object({
38355
38446
  // ADR 0020. OUTPUT-optional (not defaulted-required) so hardcoded task fixtures/mocks across the
38356
38447
  // codebase need no churn; the DB column is NOT NULL default 'code', so real rows always carry it,
38357
38448
  // and resolveHeads() treats an absent domain as `code`. Persisted create/update use their own forms.
38358
- domains: mrrlinTaskDomainsSchema.optional(),
38449
+ // Preprocesses null to undefined so tasks with null domains deserialize gracefully without widening MrrlinTask['domains'] to null.
38450
+ domains: external_exports.preprocess((val) => val === null ? void 0 : val, mrrlinTaskDomainsSchema.optional()),
38359
38451
  autonomyLevel: mrrlinAutonomyLevelSchema,
38360
38452
  autonomyReason: external_exports.string().min(1).max(240),
38361
38453
  blockedBy: external_exports.array(mrrlinTaskIdSchema),
@@ -38470,14 +38562,17 @@ var mrrlinWikiPageSchema = external_exports.object({
38470
38562
  createdAt: external_exports.string().datetime().nullable().default(null),
38471
38563
  folder: external_exports.string().min(1),
38472
38564
  id: mrrlinWikiPageIdSchema,
38473
- markdownBody: external_exports.string().min(1),
38565
+ markdownBody: external_exports.string().default(""),
38474
38566
  path: mrrlinWikiPagePathSchema,
38475
38567
  preview: external_exports.string(),
38476
38568
  specReady: external_exports.boolean().default(false),
38477
38569
  taskId: mrrlinTaskIdSchema.nullable().default(null),
38478
- title: external_exports.string().min(1),
38570
+ title: external_exports.string().default("Untitled"),
38479
38571
  updatedAt: external_exports.string().datetime().nullable().default(null)
38480
38572
  });
38573
+ var mrrlinWikiPageSummarySchema = mrrlinWikiPageSchema.omit({
38574
+ markdownBody: true
38575
+ });
38481
38576
  var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
38482
38577
  ready: external_exports.boolean()
38483
38578
  });
@@ -38628,6 +38723,7 @@ var mrrlinTaskCreateShapeSchema = external_exports.object({
38628
38723
  // "caller omitted this" apart from "caller explicitly chose off" -- omitted falls through to
38629
38724
  // the preview-only constant "dev"; the tasks column itself still defaults to 'off' at the
38630
38725
  // SQL level for any direct-insert path that bypasses createTask entirely.
38726
+ assignee: mrrlinTaskAssigneeSchema.optional(),
38631
38727
  autoDeploy: mrrlinAutoDeploySchema.optional(),
38632
38728
  domains: mrrlinTaskDomainsSchema.optional(),
38633
38729
  autonomyLevel: mrrlinAutonomyLevelSchema.default("human_review"),
@@ -38772,6 +38868,7 @@ var mrrlinTaskListFiltersSchema = external_exports.object({
38772
38868
  category: taxonomyCategorySchema.optional(),
38773
38869
  dueDate: external_exports.string().date().optional(),
38774
38870
  impact: mrrlinTaskImpactSchema.optional(),
38871
+ limit: external_exports.coerce.number().int().positive().optional(),
38775
38872
  q: external_exports.string().trim().min(1).optional(),
38776
38873
  status: mrrlinTaskStatusSchema.optional(),
38777
38874
  subcategory: taxonomySubcategorySchema.optional(),
@@ -38837,11 +38934,14 @@ var mrrlinWikiPageUpdateSchema = external_exports.object({
38837
38934
  taskId: mrrlinTaskIdSchema.nullable().optional(),
38838
38935
  title: external_exports.string().min(1).optional()
38839
38936
  }).refine((value) => Object.keys(value).length > 0, { message: "WikiPageUpdate requires at least one field" });
38937
+ var mrrlinWikiPageViewSchema = external_exports.enum(["summary"]);
38840
38938
  var mrrlinWikiPageListFiltersSchema = external_exports.object({
38841
38939
  folder: external_exports.string().min(1).optional(),
38842
38940
  id: mrrlinWikiPageIdSchema.optional(),
38941
+ limit: external_exports.coerce.number().int().positive().optional(),
38843
38942
  q: external_exports.string().min(1).optional(),
38844
- taskId: mrrlinTaskIdSchema.optional()
38943
+ taskId: mrrlinTaskIdSchema.optional(),
38944
+ view: mrrlinWikiPageViewSchema.optional()
38845
38945
  });
38846
38946
  var mrrlinWikiPageSearchQuerySchema = external_exports.object({
38847
38947
  folder: external_exports.string().min(1).optional(),
@@ -38858,10 +38958,10 @@ var mrrlinWikiPageSearchResultSchema = external_exports.object({
38858
38958
  memoryClass: external_exports.enum(["normative", "historical"]),
38859
38959
  path: external_exports.string().min(1),
38860
38960
  precedence: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3)]).nullable(),
38861
- preview: external_exports.string(),
38961
+ preview: external_exports.string().default(""),
38862
38962
  rank: external_exports.number(),
38863
- snippet: external_exports.string(),
38864
- title: external_exports.string().min(1),
38963
+ snippet: external_exports.string().default(""),
38964
+ title: external_exports.string().default(""),
38865
38965
  updatedAt: external_exports.string().datetime().nullable()
38866
38966
  });
38867
38967
  var wikiKnowledgeChunkSchema = external_exports.object({
@@ -39037,6 +39137,9 @@ var wikiPageResponseSchema = external_exports.object({
39037
39137
  var wikiPageListResponseSchema = external_exports.object({
39038
39138
  data: external_exports.array(mrrlinWikiPageSchema)
39039
39139
  });
39140
+ var wikiPageSummaryListResponseSchema = external_exports.object({
39141
+ data: external_exports.array(mrrlinWikiPageSummarySchema)
39142
+ });
39040
39143
  var wikiPageSearchResponseSchema = external_exports.object({
39041
39144
  data: external_exports.array(mrrlinWikiPageSearchResultSchema)
39042
39145
  });
@@ -39508,6 +39611,7 @@ var mrrlinSchedulerParkedReasonSchema = external_exports.enum([
39508
39611
  "running",
39509
39612
  "awaiting_handoff",
39510
39613
  "awaiting_clarification",
39614
+ "consensus_blocked",
39511
39615
  "spec_awaiting_approval",
39512
39616
  "blocked_by_active",
39513
39617
  // Derived, not stored (design.md, "Parking is derived"): an open `question` inbox item with
@@ -39541,7 +39645,8 @@ var mrrlinInFlightSchedulerEntrySchema = external_exports.discriminatedUnion("ki
39541
39645
  toolName: external_exports.enum(["draft_task_spec", "run_spec_consensus"]),
39542
39646
  // Additive: same resolution as the `run` variant above. null when `taskId` is null (a job
39543
39647
  // genuinely untied to a visible task) or the task could not be found.
39544
- taskTitle: external_exports.string().nullable()
39648
+ taskTitle: external_exports.string().nullable(),
39649
+ specPageId: external_exports.string().nullable().optional()
39545
39650
  })
39546
39651
  ]);
39547
39652
  var mrrlinIneligibleTaskSchema = external_exports.object({
@@ -39735,12 +39840,35 @@ var mrrlinInboxItemDecideSchema = external_exports.object({
39735
39840
  });
39736
39841
  }
39737
39842
  });
39843
+ var mrrlinInboxItemSummarySchema = external_exports.object({
39844
+ answer: external_exports.string().nullable(),
39845
+ createdAt: external_exports.string().datetime(),
39846
+ createdBy: external_exports.string().min(1),
39847
+ decidedAt: external_exports.string().datetime().nullable(),
39848
+ decidedBy: external_exports.string().min(1).nullable(),
39849
+ dedupKey: external_exports.string().nullable(),
39850
+ directorSessionId: external_exports.string().min(1).nullable(),
39851
+ groupId: external_exports.string().nullable().default(null),
39852
+ id: mrrlinInboxItemIdSchema,
39853
+ kind: mrrlinInboxItemKindSchema,
39854
+ origin: external_exports.string().nullable().optional(),
39855
+ planId: mrrlinPlanIdSchema.nullable().default(null),
39856
+ projectSlug: mrrlinProjectSlugSchema,
39857
+ readAt: external_exports.string().datetime().nullable(),
39858
+ reason: external_exports.string().nullable().optional(),
39859
+ status: mrrlinInboxItemStatusSchema,
39860
+ taskId: mrrlinTaskIdSchema.nullable(),
39861
+ taskTitle: external_exports.string().nullable()
39862
+ });
39863
+ var mrrlinInboxItemViewSchema = external_exports.enum(["summary"]);
39738
39864
  var mrrlinInboxItemListFiltersSchema = external_exports.object({
39739
39865
  groupId: external_exports.string().optional(),
39740
39866
  kind: mrrlinInboxItemKindSchema.optional(),
39867
+ limit: external_exports.coerce.number().int().positive().optional(),
39741
39868
  readState: external_exports.enum(["read", "unread"]).optional(),
39742
39869
  status: mrrlinInboxItemStatusSchema.optional(),
39743
- taskId: mrrlinTaskIdSchema.optional()
39870
+ taskId: mrrlinTaskIdSchema.optional(),
39871
+ view: mrrlinInboxItemViewSchema.optional()
39744
39872
  });
39745
39873
  var mrrlinIntentVerdictSchema = external_exports.object({
39746
39874
  verdict: external_exports.enum(["CLEAR", "UNCLEAR"]),
@@ -39784,6 +39912,7 @@ var inboxItemsMarkReadResponseSchema = external_exports.object({
39784
39912
  });
39785
39913
  var inboxItemResponseSchema = external_exports.object({ data: mrrlinInboxItemSchema });
39786
39914
  var inboxItemListResponseSchema = external_exports.object({ data: external_exports.array(mrrlinInboxItemSchema) });
39915
+ var inboxItemSummaryListResponseSchema = external_exports.object({ data: external_exports.array(mrrlinInboxItemSummarySchema) });
39787
39916
  var capabilityServerIdSchema = external_exports.string().trim().min(1).max(200);
39788
39917
  var capabilityNameSchema = external_exports.string().trim().min(1).max(200);
39789
39918
  var capabilityOperationNameSchema = external_exports.string().min(1).max(100).regex(/^[a-z][a-z0-9_]*$/, "an operation name must be lower_snake_case, starting with a letter");
@@ -39984,11 +40113,12 @@ function buildTaskListPath(projectSlug, filters, view) {
39984
40113
  category: filters.category,
39985
40114
  dueDate: filters.dueDate,
39986
40115
  impact: filters.impact,
40116
+ limit: filters.limit === void 0 ? void 0 : String(filters.limit),
39987
40117
  q: filters.q,
39988
40118
  status: filters.status,
39989
40119
  subcategory: filters.subcategory,
39990
40120
  type: filters.type,
39991
- view
40121
+ view: view ?? filters.view
39992
40122
  });
39993
40123
  }
39994
40124
  var MrrlinApiError = class extends Error {
@@ -40253,28 +40383,35 @@ function createMrrlinClient(config2) {
40253
40383
  // this one implementation signature; MrrlinClient's declared overloads keep callers precise.
40254
40384
  async listTasks(projectSlug, filters = {}, options) {
40255
40385
  const path29 = buildTaskListPath(projectSlug, filters, options?.view);
40256
- const body = await request(path29);
40386
+ const body = await request(path29, options?.signal ? { signal: options.signal } : void 0);
40257
40387
  if (options?.view === "summary") {
40258
40388
  return taskSummaryListResponseSchema.parse(body).data;
40259
40389
  }
40260
40390
  return taskListResponseSchema.parse(body).data;
40261
40391
  },
40262
- async listTaskSummaries(projectSlug, filters = {}) {
40392
+ async listTaskSummaries(projectSlug, filters = {}, options) {
40263
40393
  const path29 = buildTaskListPath(projectSlug, filters, "summary");
40264
- const body = await request(path29);
40394
+ const body = await request(path29, options?.signal ? { signal: options.signal } : void 0);
40265
40395
  return taskSummaryListResponseSchema.parse(body).data;
40266
40396
  },
40267
40397
  async listTaxonomy(projectSlug) {
40268
40398
  const body = await request(`${projectPath(projectSlug)}/taxonomy`);
40269
40399
  return taxonomyListResponseSchema.parse(body).data;
40270
40400
  },
40271
- async listWikiPages(projectSlug, filters = {}) {
40401
+ async listWikiPages(projectSlug, filters = {}, options) {
40402
+ const view = options?.view ?? filters.view;
40272
40403
  const path29 = appendQuery(`${projectPath(projectSlug)}/wiki`, {
40273
40404
  folder: filters.folder,
40405
+ id: filters.id,
40406
+ limit: filters.limit === void 0 ? void 0 : String(filters.limit),
40274
40407
  q: filters.q,
40275
- taskId: filters.taskId
40408
+ taskId: filters.taskId,
40409
+ view
40276
40410
  });
40277
- const body = await request(path29);
40411
+ const body = await request(path29, options?.signal ? { signal: options.signal } : void 0);
40412
+ if (view === "summary") {
40413
+ return wikiPageSummaryListResponseSchema.parse(body).data;
40414
+ }
40278
40415
  return wikiPageListResponseSchema.parse(body).data;
40279
40416
  },
40280
40417
  async searchWikiPages(projectSlug, query) {
@@ -40561,15 +40698,21 @@ function createMrrlinClient(config2) {
40561
40698
  });
40562
40699
  return capabilityUseResponseSchema.parse(body).data;
40563
40700
  },
40564
- async listInboxItems(projectSlug, filters = {}) {
40701
+ async listInboxItems(projectSlug, filters = {}, options) {
40702
+ const view = options?.view ?? filters.view;
40565
40703
  const url2 = appendQuery(`${projectPath(projectSlug)}/inbox`, {
40566
40704
  groupId: filters.groupId,
40567
40705
  kind: filters.kind,
40706
+ limit: filters.limit === void 0 ? void 0 : String(filters.limit),
40568
40707
  readState: filters.readState,
40569
40708
  status: filters.status,
40570
- taskId: filters.taskId
40709
+ taskId: filters.taskId,
40710
+ view
40571
40711
  });
40572
- const body = await request(url2);
40712
+ const body = await request(url2, options?.signal ? { signal: options.signal } : void 0);
40713
+ if (view === "summary") {
40714
+ return inboxItemSummaryListResponseSchema.parse(body).data;
40715
+ }
40573
40716
  return inboxItemListResponseSchema.parse(body).data;
40574
40717
  },
40575
40718
  async createInboxItem(projectSlug, input) {
@@ -40952,6 +41095,14 @@ function createMrrlinClient(config2) {
40952
41095
  const body = await request(url2);
40953
41096
  return specConsensusReviewMaybeResponseSchema.parse(body).data;
40954
41097
  },
41098
+ async finalizeSpecReviewCas(projectSlug, pageId, input) {
41099
+ const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}/consensus-finalize`, {
41100
+ body: JSON.stringify(input),
41101
+ headers: { "content-type": "application/json" },
41102
+ method: "POST"
41103
+ });
41104
+ return mrrlinFinalizeSpecReviewCasResponseSchema.parse(body).data;
41105
+ },
40955
41106
  async issueAgentCredential(projectSlug, input) {
40956
41107
  const body = await request(`${projectPath(projectSlug)}/agent-credentials`, {
40957
41108
  body: JSON.stringify(input),
@@ -41154,19 +41305,19 @@ function searchKnowledgeIndex(index, query, limit = 5) {
41154
41305
  if (queryTokens.length === 0)
41155
41306
  return [];
41156
41307
  const documents = index.map((chunk) => tokenize(`${chunk.heading_path.join(" ")} ${chunk.chunk_text}`));
41157
- const averageLength = documents.reduce((sum, tokens) => sum + tokens.length, 0) / Math.max(documents.length, 1);
41308
+ const averageLength = documents.reduce((sum, tokens2) => sum + tokens2.length, 0) / Math.max(documents.length, 1);
41158
41309
  const documentFrequency = /* @__PURE__ */ new Map();
41159
- for (const tokens of documents) {
41160
- for (const token of new Set(tokens)) {
41310
+ for (const tokens2 of documents) {
41311
+ for (const token of new Set(tokens2)) {
41161
41312
  documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
41162
41313
  }
41163
41314
  }
41164
41315
  const k1 = 1.2;
41165
41316
  const b = 0.75;
41166
41317
  const scored = index.map((chunk, chunkIndex) => {
41167
- const tokens = documents[chunkIndex] ?? [];
41318
+ const tokens2 = documents[chunkIndex] ?? [];
41168
41319
  const frequencies = /* @__PURE__ */ new Map();
41169
- for (const token of tokens) {
41320
+ for (const token of tokens2) {
41170
41321
  frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
41171
41322
  }
41172
41323
  const score = queryTokens.reduce((sum, token) => {
@@ -41175,7 +41326,7 @@ function searchKnowledgeIndex(index, query, limit = 5) {
41175
41326
  return sum;
41176
41327
  const df = documentFrequency.get(token) ?? 0;
41177
41328
  const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
41178
- const denominator = tf + k1 * (1 - b + b * (tokens.length / Math.max(averageLength, 1)));
41329
+ const denominator = tf + k1 * (1 - b + b * (tokens2.length / Math.max(averageLength, 1)));
41179
41330
  return sum + idf * (tf * (k1 + 1) / denominator);
41180
41331
  }, 0);
41181
41332
  return { ...chunk, score };
@@ -41723,18 +41874,87 @@ function resolveProducerPersona(domain2) {
41723
41874
  }
41724
41875
 
41725
41876
  // src/consensus/acceptance-criteria.ts
41726
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
41727
- var CHECKLIST_ITEM_RE = /^\s*[-*]\s+\[[ xX]\]\s+(.+\S)\s*$/;
41728
- function normalizeHeading(title) {
41729
- return title.trim().toLowerCase().replace(/\s+/g, " ");
41877
+ var HEADING_RE = /^[ ]{0,3}(#{1,6})\s+(.+?)\s*$/;
41878
+ var TOP_LEVEL_CHECKLIST_RE = /^[ ]{0,1}[-*]\s+\[[ xX]\]\s+(.+\S)\s*$/;
41879
+ var TOP_LEVEL_ITEM_RE = /^[ ]{0,1}(?:\d+[.)]|[-*])\s+(.+?\S)\s*$/;
41880
+ var INDENTED_CODE_RE = /^[ ]{4,}\S/;
41881
+ function createCommonMarkFenceScanner() {
41882
+ let active = null;
41883
+ return {
41884
+ isFenced(line) {
41885
+ const trimmed = line.trimStart();
41886
+ const indent = line.length - trimmed.length;
41887
+ if (indent <= 3) {
41888
+ if (!active) {
41889
+ const m = /^(`{3,}|~{3,})/.exec(trimmed);
41890
+ if (m && m[1]) {
41891
+ const fenceStr = m[1];
41892
+ const char = fenceStr[0];
41893
+ const length = fenceStr.length;
41894
+ const remainder = trimmed.slice(length);
41895
+ if (char === "~" || !remainder.includes("`")) {
41896
+ active = { char, length };
41897
+ return true;
41898
+ }
41899
+ }
41900
+ } else {
41901
+ const m = /^(`{3,}|~{3,})\s*$/.exec(trimmed);
41902
+ if (m && m[1]) {
41903
+ const fenceStr = m[1];
41904
+ const char = fenceStr[0];
41905
+ const length = fenceStr.length;
41906
+ if (char === active.char && length >= active.length) {
41907
+ active = null;
41908
+ return true;
41909
+ }
41910
+ }
41911
+ }
41912
+ }
41913
+ return active !== null;
41914
+ },
41915
+ isInCodeBlock() {
41916
+ return active !== null;
41917
+ }
41918
+ };
41919
+ }
41920
+ var ACCEPTANCE_CRITERIA_ALIASES = /* @__PURE__ */ new Set([
41921
+ "acceptance criteria",
41922
+ "acceptance criteria (checklist)",
41923
+ "acceptance criteria (dod)",
41924
+ "acceptance criteria (definition of done)",
41925
+ "criteria for acceptance",
41926
+ "success criteria",
41927
+ "definition of done",
41928
+ "dod",
41929
+ "dod (definition of done)",
41930
+ "\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u043F\u0440\u0438\u0439\u043D\u044F\u0442\u043D\u043E\u0441\u0442\u0456",
41931
+ "\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u043F\u0440\u0438\u0439\u043E\u043C\u043A\u0438",
41932
+ "\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u043F\u0440\u0438\u0439\u043C\u0430\u043D\u043D\u044F",
41933
+ "\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u0443\u0441\u043F\u0456\u0445\u0443",
41934
+ "\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0456",
41935
+ "kryteria akceptacji",
41936
+ "kryteria sukcesu",
41937
+ "akzeptanzkriterien",
41938
+ "erfolgskriterien"
41939
+ ]);
41940
+ function isAcceptanceCriteriaHeading(title) {
41941
+ const strippedNumber = title.trim().toLowerCase().replace(/^\d+[.)]\s*/, "");
41942
+ const cleaned = strippedNumber.replace(/[:\-?.]+$/, "").replace(/\s+/g, " ").trim();
41943
+ if (ACCEPTANCE_CRITERIA_ALIASES.has(cleaned)) return true;
41944
+ if (cleaned.startsWith("acceptance criteria") || cleaned.startsWith("\u043A\u0440\u0438\u0442\u0435\u0440\u0456\u0457 \u043F\u0440\u0438\u0439\u043D\u044F\u0442\u043D\u043E\u0441\u0442\u0456")) return true;
41945
+ return false;
41730
41946
  }
41731
41947
  function parseAcceptanceCriteria(markdown) {
41732
41948
  const lines2 = markdown.split(/\r?\n/);
41949
+ const fenceScanner = createCommonMarkFenceScanner();
41733
41950
  let sectionStart = -1;
41734
41951
  let sectionLevel = 0;
41735
41952
  for (let i = 0; i < lines2.length; i += 1) {
41736
- const m = HEADING_RE.exec((lines2[i] ?? "").trim());
41737
- if (m && normalizeHeading(m[2] ?? "") === "acceptance criteria") {
41953
+ const rawLine = lines2[i] ?? "";
41954
+ const isFenced = fenceScanner.isFenced(rawLine);
41955
+ if (isFenced) continue;
41956
+ const m = HEADING_RE.exec(rawLine);
41957
+ if (m && isAcceptanceCriteriaHeading(m[2] ?? "")) {
41738
41958
  sectionStart = i + 1;
41739
41959
  sectionLevel = (m[1] ?? "").length;
41740
41960
  break;
@@ -41743,14 +41963,65 @@ function parseAcceptanceCriteria(markdown) {
41743
41963
  if (sectionStart === -1) return null;
41744
41964
  const items = [];
41745
41965
  for (let i = sectionStart; i < lines2.length; i += 1) {
41746
- const line = lines2[i] ?? "";
41747
- const heading = HEADING_RE.exec(line.trim());
41966
+ const rawLine = lines2[i] ?? "";
41967
+ const isFenced = fenceScanner.isFenced(rawLine);
41968
+ if (isFenced) continue;
41969
+ if (INDENTED_CODE_RE.test(rawLine)) continue;
41970
+ const heading = HEADING_RE.exec(rawLine);
41748
41971
  if (heading && (heading[1] ?? "").length <= sectionLevel) break;
41749
- const item = CHECKLIST_ITEM_RE.exec(line);
41972
+ const item = TOP_LEVEL_CHECKLIST_RE.exec(rawLine);
41750
41973
  if (item && item[1]) items.push(item[1].trim());
41751
41974
  }
41752
41975
  return items.length > 0 ? { items } : null;
41753
41976
  }
41977
+ function normalizeAcceptanceCriteria(markdown) {
41978
+ const isCrlf = markdown.includes("\r\n");
41979
+ const newline = isCrlf ? "\r\n" : "\n";
41980
+ const lines2 = markdown.split(/\r?\n/);
41981
+ const fenceScanner = createCommonMarkFenceScanner();
41982
+ let sectionStart = -1;
41983
+ let sectionLevel = 0;
41984
+ let headingIndex = -1;
41985
+ for (let i = 0; i < lines2.length; i += 1) {
41986
+ const rawLine = lines2[i] ?? "";
41987
+ const isFenced = fenceScanner.isFenced(rawLine);
41988
+ if (isFenced) continue;
41989
+ const m = HEADING_RE.exec(rawLine);
41990
+ if (m && isAcceptanceCriteriaHeading(m[2] ?? "")) {
41991
+ headingIndex = i;
41992
+ sectionStart = i + 1;
41993
+ sectionLevel = (m[1] ?? "").length;
41994
+ break;
41995
+ }
41996
+ }
41997
+ if (sectionStart === -1) return markdown;
41998
+ let changed = false;
41999
+ const newLines = [...lines2];
42000
+ const hashes = "#".repeat(sectionLevel || 2);
42001
+ if (newLines[headingIndex]?.trim() !== `${hashes} Acceptance criteria`) {
42002
+ newLines[headingIndex] = `${hashes} Acceptance criteria`;
42003
+ changed = true;
42004
+ }
42005
+ const bodyFenceScanner = createCommonMarkFenceScanner();
42006
+ for (let i = 0; i < sectionStart; i += 1) {
42007
+ bodyFenceScanner.isFenced(lines2[i] ?? "");
42008
+ }
42009
+ for (let i = sectionStart; i < newLines.length; i += 1) {
42010
+ const rawLine = newLines[i] ?? "";
42011
+ const isFenced = bodyFenceScanner.isFenced(rawLine);
42012
+ if (isFenced) continue;
42013
+ if (INDENTED_CODE_RE.test(rawLine)) continue;
42014
+ const heading = HEADING_RE.exec(rawLine);
42015
+ if (heading && (heading[1] ?? "").length <= sectionLevel) break;
42016
+ if (TOP_LEVEL_CHECKLIST_RE.test(rawLine)) continue;
42017
+ const match = TOP_LEVEL_ITEM_RE.exec(rawLine);
42018
+ if (match && match[1]) {
42019
+ newLines[i] = `- [ ] ${match[1]}`;
42020
+ changed = true;
42021
+ }
42022
+ }
42023
+ return changed ? newLines.join(newline) : markdown;
42024
+ }
41754
42025
  function resolveSpecBudget(impact) {
41755
42026
  switch (impact?.toLowerCase()) {
41756
42027
  case "high":
@@ -41822,12 +42093,21 @@ function applyClarificationCap(issues, impact) {
41822
42093
  };
41823
42094
  }
41824
42095
  async function runSpecGate(_target, deps, opts) {
41825
- if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
42096
+ const spec = await deps.readSpec();
42097
+ let currentHash = spec.contentHash;
42098
+ let currentMarkdown = spec.markdownBody;
42099
+ if (!await deps.consensusEnabled()) {
42100
+ return {
42101
+ outcome: "SKIPPED",
42102
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown }
42103
+ };
42104
+ }
41826
42105
  const intent = await deps.readIntent();
41827
42106
  const legacyExempt = intent.specApproved === "skipped_legacy";
41828
42107
  if (!legacyExempt && intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
41829
42108
  return {
41830
42109
  outcome: "CONSENSUS_BLOCKED",
42110
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
41831
42111
  operatorClarifications: [
41832
42112
  {
41833
42113
  kind: "missing_requirement",
@@ -41845,10 +42125,17 @@ async function runSpecGate(_target, deps, opts) {
41845
42125
  const baseHeads = resolveHeads(intent.domains, intent.impact);
41846
42126
  const extraHeads = opts?.extraHeads ?? [];
41847
42127
  const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
41848
- const spec = await deps.readSpec();
41849
- let currentHash = spec.contentHash;
41850
- let currentMarkdown = spec.markdownBody;
41851
42128
  let conflicted = false;
42129
+ if (parseAcceptanceCriteria(currentMarkdown) === null) {
42130
+ const normalized = normalizeAcceptanceCriteria(currentMarkdown);
42131
+ if (parseAcceptanceCriteria(normalized) !== null) {
42132
+ const cas = await deps.casUpdateSpec(normalized, currentHash);
42133
+ if (cas.ok) {
42134
+ currentHash = cas.contentHash;
42135
+ currentMarkdown = normalized;
42136
+ }
42137
+ }
42138
+ }
41852
42139
  if (parseAcceptanceCriteria(currentMarkdown) === null) {
41853
42140
  const bootstrapIssue = {
41854
42141
  id: "acceptance-criteria-bootstrap",
@@ -41861,6 +42148,7 @@ async function runSpecGate(_target, deps, opts) {
41861
42148
  `);
41862
42149
  return {
41863
42150
  outcome: "CONSENSUS_BLOCKED",
42151
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
41864
42152
  diagnostics: [
41865
42153
  {
41866
42154
  kind: "acceptance_criteria_unauthorable",
@@ -41881,8 +42169,11 @@ async function runSpecGate(_target, deps, opts) {
41881
42169
  }
41882
42170
  const cas = await deps.casUpdateSpec(revised.text, currentHash);
41883
42171
  if (!cas.ok) {
41884
- await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
41885
- return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
42172
+ await deps.writeEvidence?.({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
42173
+ return {
42174
+ outcome: "SPEC_CHANGED_DURING_REVIEW",
42175
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown }
42176
+ };
41886
42177
  }
41887
42178
  currentHash = cas.contentHash;
41888
42179
  currentMarkdown = revised.text;
@@ -41918,8 +42209,11 @@ async function runSpecGate(_target, deps, opts) {
41918
42209
  // error and must propagate so it is not silently masked as a conflict.
41919
42210
  }).catch((e) => conflicted ? null : Promise.reject(e));
41920
42211
  if (conflicted || result === null) {
41921
- await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
41922
- return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
42212
+ await deps.writeEvidence?.({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
42213
+ return {
42214
+ outcome: "SPEC_CHANGED_DURING_REVIEW",
42215
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown }
42216
+ };
41923
42217
  }
41924
42218
  if (result.outcome === "CONVERGED" && intent.taskId !== null && !validateSpecIdentity(currentMarkdown, intent.taskId)) {
41925
42219
  process.stderr.write(
@@ -41928,6 +42222,7 @@ async function runSpecGate(_target, deps, opts) {
41928
42222
  );
41929
42223
  return {
41930
42224
  outcome: "CONSENSUS_BLOCKED",
42225
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
41931
42226
  diagnostics: [
41932
42227
  {
41933
42228
  kind: "spec_identity_mismatch",
@@ -41951,19 +42246,50 @@ async function runSpecGate(_target, deps, opts) {
41951
42246
  keptClarifications = capped.kept;
41952
42247
  droppedByCap = capped.dropped;
41953
42248
  }
41954
- await deps.writeEvidence({
41955
- specPageId: spec.pageId,
41956
- outcome: result.outcome,
41957
- rounds: result.rounds,
41958
- history: result.history,
41959
- contentHash: currentHash,
41960
- ...droppedByCap.length > 0 ? { droppedByCap } : {}
41961
- });
42249
+ const ownership = intent.taskId !== null ? { kind: "task", taskId: intent.taskId, domains: intent.domains ?? ["code"] } : intent.planId != null ? { kind: "plan", planId: intent.planId, domains: intent.domains ?? ["code"] } : { kind: "project", domains: intent.domains ?? ["code"] };
42250
+ if (deps.finalizeSpecReviewCas) {
42251
+ const shouldFinalizeCas = result.outcome === "CONVERGED" || result.outcome === "CONSENSUS_BLOCKED" && keptClarifications.length === 0;
42252
+ if (shouldFinalizeCas) {
42253
+ const finalizeRes = await deps.finalizeSpecReviewCas({
42254
+ expectedContentHash: currentHash,
42255
+ ownership,
42256
+ review: {
42257
+ outcome: result.outcome,
42258
+ rounds: result.rounds,
42259
+ history: result.history,
42260
+ ...droppedByCap.length > 0 ? { suppressionEvidence: { droppedByCap } } : {}
42261
+ },
42262
+ specReady: result.outcome === "CONVERGED"
42263
+ });
42264
+ if (finalizeRes.status === "superseded") {
42265
+ return {
42266
+ outcome: "SUPERSEDED",
42267
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
42268
+ evaluatedOwnership: ownership
42269
+ };
42270
+ }
42271
+ }
42272
+ } else {
42273
+ await deps.writeEvidence?.({
42274
+ specPageId: spec.pageId,
42275
+ outcome: result.outcome,
42276
+ rounds: result.rounds,
42277
+ history: result.history,
42278
+ contentHash: currentHash,
42279
+ ...droppedByCap.length > 0 ? { droppedByCap } : {}
42280
+ });
42281
+ if (result.outcome === "CONVERGED") {
42282
+ await deps.promoteSpecReady?.(currentHash);
42283
+ }
42284
+ }
41962
42285
  if (result.outcome === "CONVERGED") {
41963
- await deps.promoteSpecReady(currentHash);
41964
42286
  return {
41965
42287
  outcome: "CONVERGED",
42288
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
42289
+ evaluatedOwnership: ownership,
41966
42290
  headOutcomes: result.history.at(-1)?.headOutcomes ?? [],
42291
+ rounds: result.rounds,
42292
+ history: result.history,
41967
42293
  arbiterInfo: result.arbiterInfo,
41968
42294
  panelComposition: result.panelComposition,
41969
42295
  sufficiencyVerdict: result.sufficiencyVerdict
@@ -41972,6 +42298,10 @@ async function runSpecGate(_target, deps, opts) {
41972
42298
  if (result.outcome === "CONSENSUS_BLOCKED") {
41973
42299
  return {
41974
42300
  outcome: "CONSENSUS_BLOCKED",
42301
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
42302
+ evaluatedOwnership: ownership,
42303
+ rounds: result.rounds,
42304
+ history: result.history,
41975
42305
  ...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
41976
42306
  ...droppedByCap.length > 0 ? { droppedByCap } : {},
41977
42307
  ...result.escalation ? { escalation: result.escalation } : {},
@@ -41983,6 +42313,8 @@ async function runSpecGate(_target, deps, opts) {
41983
42313
  }
41984
42314
  return {
41985
42315
  outcome: result.outcome,
42316
+ evaluatedSnapshot: { contentHash: currentHash, markdownBody: currentMarkdown },
42317
+ evaluatedOwnership: ownership,
41986
42318
  arbiterInfo: result.arbiterInfo,
41987
42319
  panelComposition: result.panelComposition,
41988
42320
  sufficiencyVerdict: result.sufficiencyVerdict
@@ -42401,6 +42733,8 @@ var CodexServerManager = class {
42401
42733
  rpcClient.respond(id, { decision: "accept" });
42402
42734
  } else if (method === "mcpServer/elicitation/request") {
42403
42735
  rpcClient.respond(id, { action: "accept" });
42736
+ } else if (method.includes("approval") || method.includes("Approval") || method.includes("permission") || method.includes("elicitation")) {
42737
+ rpcClient.respond(id, { action: "accept", decision: "accept", approved: true });
42404
42738
  }
42405
42739
  });
42406
42740
  const notificationHandlers = /* @__PURE__ */ new Set();
@@ -43198,6 +43532,9 @@ var ClaudeProcess = class {
43198
43532
  if (options.mcpConfig) {
43199
43533
  args.push("--mcp-config", options.mcpConfig);
43200
43534
  }
43535
+ if (options.appendSystemPrompt && options.appendSystemPrompt.trim().length > 0) {
43536
+ args.push("--append-system-prompt", options.appendSystemPrompt.trim());
43537
+ }
43201
43538
  args.push("--permission-mode", options.permissionMode);
43202
43539
  if (options.disallowedTools && options.disallowedTools.length > 0) {
43203
43540
  args.push("--disallowedTools", options.disallowedTools.join(","));
@@ -43473,7 +43810,8 @@ var ClaudeAgentHandle = class {
43473
43810
  disallowedTools,
43474
43811
  model: params.model,
43475
43812
  cwd: params.cwd,
43476
- mcpConfig: params.mcpConfig
43813
+ mcpConfig: params.mcpConfig,
43814
+ appendSystemPrompt: params.baseInstructions
43477
43815
  });
43478
43816
  this.processes.set(child.sessionId, child);
43479
43817
  this.mappers.set(child.sessionId, new ClaudeBridgeEventMapper());
@@ -43487,7 +43825,8 @@ var ClaudeAgentHandle = class {
43487
43825
  disallowedTools,
43488
43826
  model: params.model,
43489
43827
  cwd: params.cwd,
43490
- mcpConfig: params.mcpConfig
43828
+ mcpConfig: params.mcpConfig,
43829
+ appendSystemPrompt: params.baseInstructions
43491
43830
  });
43492
43831
  this.processes.set(threadId, child);
43493
43832
  this.mappers.set(threadId, new ClaudeBridgeEventMapper());
@@ -43631,8 +43970,14 @@ var AgyProcess = class _AgyProcess {
43631
43970
  args.push("--mode", "accept-edits");
43632
43971
  break;
43633
43972
  }
43634
- if (options.model) {
43635
- args.push("--model", options.model);
43973
+ let modelToUse = options.model;
43974
+ if (modelToUse === "gemini-3.1-pro-high" || modelToUse === "gemini-3.7-flash-high") {
43975
+ modelToUse = "gemini-3.8-flash-high";
43976
+ } else if (modelToUse === "gemini-3.1-pro-low" || modelToUse === "gemini-3.7-flash-low") {
43977
+ modelToUse = "gemini-3.8-flash-low";
43978
+ }
43979
+ if (modelToUse) {
43980
+ args.push("--model", modelToUse);
43636
43981
  }
43637
43982
  if (mode.kind === "resume") {
43638
43983
  args.push("--conversation", mode.resumeId);
@@ -43861,7 +44206,7 @@ function agyModelCatalog() {
43861
44206
  id: entry.id,
43862
44207
  displayName: entry.displayName,
43863
44208
  description: entry.description,
43864
- isDefault: entry.id === "gemini-3.1-pro-high"
44209
+ isDefault: entry.id === "gemini-3.8-flash-high"
43865
44210
  }));
43866
44211
  }
43867
44212
  var MODEL_ID_REGEX2 = /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,63}$/;
@@ -43886,7 +44231,7 @@ function parseAgyModelsOutput(output3) {
43886
44231
  id,
43887
44232
  displayName,
43888
44233
  description,
43889
- isDefault: id === "gemini-3.1-pro-high"
44234
+ isDefault: id === "gemini-3.8-flash-high"
43890
44235
  });
43891
44236
  }
43892
44237
  return models;
@@ -43955,6 +44300,7 @@ function permissionModeFor2(sandbox, approvalPolicy) {
43955
44300
  var AgyAgentHandle = class {
43956
44301
  processes = /* @__PURE__ */ new Map();
43957
44302
  mappers = /* @__PURE__ */ new Map();
44303
+ baseInstructions = /* @__PURE__ */ new Map();
43958
44304
  async startThread(params) {
43959
44305
  const { permissionMode } = permissionModeFor2(params.sandbox, params.approvalPolicy);
43960
44306
  const child = new AgyProcess({
@@ -43965,6 +44311,9 @@ var AgyAgentHandle = class {
43965
44311
  });
43966
44312
  this.processes.set(child.sessionId, child);
43967
44313
  this.mappers.set(child.sessionId, new AgyBridgeEventMapper());
44314
+ if (params.baseInstructions && params.baseInstructions.trim().length > 0) {
44315
+ this.baseInstructions.set(child.sessionId, params.baseInstructions.trim());
44316
+ }
43968
44317
  return { threadId: child.sessionId };
43969
44318
  }
43970
44319
  async resumeThread(threadId, params) {
@@ -43982,12 +44331,26 @@ var AgyAgentHandle = class {
43982
44331
  });
43983
44332
  this.processes.set(threadId, child);
43984
44333
  this.mappers.set(threadId, new AgyBridgeEventMapper());
44334
+ if (params.baseInstructions && params.baseInstructions.trim().length > 0) {
44335
+ this.baseInstructions.set(threadId, params.baseInstructions.trim());
44336
+ }
43985
44337
  return { threadId: child.sessionId };
43986
44338
  }
43987
44339
  async startTurn(threadId, input) {
43988
44340
  const child = this.requireProcess(threadId);
43989
44341
  const text = input.filter((block) => block.type === "text").map((block) => block.text).join("\n");
43990
- child.sendTurn(text);
44342
+ const inst = this.baseInstructions.get(threadId);
44343
+ if (inst) {
44344
+ const composed = `[SYSTEM INSTRUCTIONS - MRRLIN DIRECTOR]
44345
+ ${inst}
44346
+
44347
+ ---
44348
+ [OPERATOR REQUEST]
44349
+ ${text}`;
44350
+ child.sendTurn(composed);
44351
+ } else {
44352
+ child.sendTurn(text);
44353
+ }
43991
44354
  return { turnId: void 0 };
43992
44355
  }
43993
44356
  async interruptTurn(threadId) {
@@ -43997,6 +44360,7 @@ var AgyAgentHandle = class {
43997
44360
  });
43998
44361
  this.processes.delete(threadId);
43999
44362
  this.mappers.delete(threadId);
44363
+ this.baseInstructions.delete(threadId);
44000
44364
  }
44001
44365
  }
44002
44366
  onEvent(threadId, handler) {
@@ -44385,7 +44749,18 @@ function runAgyExec(input, deps) {
44385
44749
  prompt = trimmedInstructions + "\n\n" + prompt;
44386
44750
  }
44387
44751
  if (input.model) {
44388
- args.push("--model", input.model);
44752
+ let targetModel = input.model;
44753
+ if (targetModel === "gemini-3.1-pro-high" || targetModel === "gemini-3.7-flash-high") {
44754
+ targetModel = "gemini-3.8-flash-high";
44755
+ } else if (targetModel === "gemini-3.1-pro-low" || targetModel === "gemini-3.7-flash-low") {
44756
+ targetModel = "gemini-3.8-flash-low";
44757
+ }
44758
+ const known = AGY_MODEL_CATALOG.find((m) => m.id === targetModel || m.displayName.toLowerCase() === targetModel.toLowerCase());
44759
+ if (known && !known.id.startsWith("gemini-3.1")) {
44760
+ args.push("--model", known.id);
44761
+ } else {
44762
+ args.push("--model", "gemini-3.8-flash-high");
44763
+ }
44389
44764
  }
44390
44765
  args.push(`--print=${prompt}`);
44391
44766
  return new Promise((resolve) => {
@@ -44468,7 +44843,9 @@ function extractOneShotResult2(stdout) {
44468
44843
 
44469
44844
  // ../../packages/agent-client/dist/run-agent-exec.js
44470
44845
  function runAgentExec(input, deps, persistedProvider) {
44471
- const provider = resolveProvider(process.env, persistedProvider);
44846
+ const explicitProvider = persistedProvider ?? process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase();
44847
+ const envToUse = explicitProvider ? { ...process.env, MRRLIN_AGENT_PROVIDER: explicitProvider } : process.env;
44848
+ const provider = resolveProvider(envToUse, explicitProvider);
44472
44849
  if (provider.id === "codex") {
44473
44850
  return runCodexExec(input, deps);
44474
44851
  }
@@ -44484,7 +44861,7 @@ function runAgentExec(input, deps, persistedProvider) {
44484
44861
  // ../../packages/agent-client/dist/resolved-seat.js
44485
44862
  var CODEX_DEFAULT_MODEL_CONSTANT = "gpt-5.6-terra";
44486
44863
  var CLAUDE_DEFAULT_MODEL_CONSTANT = "sonnet";
44487
- var AGY_DEFAULT_MODEL_CONSTANT = "gemini-3.1-pro-high";
44864
+ var AGY_DEFAULT_MODEL_CONSTANT = "gemini-3.8-flash-high";
44488
44865
  var CODEX_DEFAULT_MODEL_ENV = "MRRLIN_CODEX_DEFAULT_MODEL";
44489
44866
  var CODEX_STRONG_MODEL_ENV = "MRRLIN_CODEX_STRONG_MODEL";
44490
44867
  var CLAUDE_DEFAULT_MODEL_ENV = "MRRLIN_CLAUDE_DEFAULT_MODEL";
@@ -44635,23 +45012,32 @@ async function resolveGatedTaskId(client, projectSlug, page) {
44635
45012
  function domainScopedReviewHash2(rawHash, domain2) {
44636
45013
  return domainScopedReviewHash(rawHash, domain2);
44637
45014
  }
44638
- async function resolveSpecDomain(client, projectSlug, page) {
45015
+ async function resolveSpecDomains(client, projectSlug, page) {
44639
45016
  try {
44640
45017
  const taskId = await resolveGatedTaskId(client, projectSlug, page);
44641
45018
  if (taskId !== null) {
44642
45019
  const task = await client.getTask(projectSlug, taskId);
44643
- return normalizeDomain(task?.domains ?? task?.domain);
45020
+ return normalizeDomainArray(task?.domains ?? task?.domain);
44644
45021
  }
44645
45022
  const plans = await client.listPlans(projectSlug);
44646
45023
  const plan = plans.find((p) => p.specWikiPageId === page.id);
44647
- const planDomains = plan?.domains ?? plan?.domain;
44648
- if (planDomains != null) return normalizeDomain(planDomains);
45024
+ if (plan) {
45025
+ const rawPlanDomains = plan?.domains ?? plan?.domain;
45026
+ const substantivePlanDomains = (Array.isArray(rawPlanDomains) ? rawPlanDomains : rawPlanDomains != null ? [rawPlanDomains] : []).map(String).map((d) => d.trim().toLowerCase()).filter(Boolean);
45027
+ if (substantivePlanDomains.length > 0) {
45028
+ return normalizeDomainArray(substantivePlanDomains);
45029
+ }
45030
+ }
44649
45031
  const project = await client.getProject(projectSlug);
44650
- return normalizeDomain(project?.defaultDomains ?? project?.defaultDomain);
45032
+ return normalizeDomainArray(project?.defaultDomains ?? project?.defaultDomain);
44651
45033
  } catch {
44652
- return "code";
45034
+ return ["code"];
44653
45035
  }
44654
45036
  }
45037
+ async function resolveSpecDomain(client, projectSlug, page) {
45038
+ const domains = await resolveSpecDomains(client, projectSlug, page);
45039
+ return normalizeDomain(domains);
45040
+ }
44655
45041
 
44656
45042
  // src/consensus/findings.ts
44657
45043
  function shouldCurateFindings(history) {
@@ -45035,7 +45421,13 @@ function gateCallRecorder(args) {
45035
45421
  }
45036
45422
  function buildSpecGateDeps(args) {
45037
45423
  const { client, projectSlug, specPageId, codexExecutable, extraHeadCount = 0, seat, stateDir } = args;
45038
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "spec", provider: seat?.id ?? null });
45424
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45425
+ const recordCost = gateCallRecorder({
45426
+ stateDir,
45427
+ projectSlug,
45428
+ gate: "spec",
45429
+ provider: consensusProvider ?? seat?.id ?? null
45430
+ });
45039
45431
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45040
45432
  const maxHeadCalls = 2 * rounds * (MAX_DOMAIN_HEADS + extraHeadCount + 2);
45041
45433
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
@@ -45049,8 +45441,24 @@ function buildSpecGateDeps(args) {
45049
45441
  }
45050
45442
  const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
45051
45443
  if (taskId === null) {
45444
+ const plans = await client.listPlans(projectSlug);
45445
+ const plan = plans.find((p) => p.specWikiPageId === specPageId);
45446
+ if (plan) {
45447
+ const project2 = await client.getProject(projectSlug);
45448
+ const rawPlanDomains = plan?.domains ?? plan?.domain;
45449
+ const substantivePlanDomains = (Array.isArray(rawPlanDomains) ? rawPlanDomains : rawPlanDomains != null ? [rawPlanDomains] : []).map(String).map((d) => d.trim().toLowerCase()).filter(Boolean);
45450
+ const planDomains = substantivePlanDomains.length > 0 ? substantivePlanDomains : project2?.defaultDomains ?? (project2?.defaultDomain ? [project2.defaultDomain] : void 0);
45451
+ return {
45452
+ taskId: null,
45453
+ planId: plan.id,
45454
+ seed: null,
45455
+ title: plan.title ?? plan.name ?? null,
45456
+ domains: planDomains,
45457
+ notes: null
45458
+ };
45459
+ }
45052
45460
  const project = await client.getProject(projectSlug);
45053
- return { taskId: null, seed: null, title: null, domains: project.defaultDomains, notes: null };
45461
+ return { taskId: null, planId: null, seed: null, title: null, domains: project.defaultDomains, notes: null };
45054
45462
  }
45055
45463
  const task = await client.getTask(projectSlug, taskId);
45056
45464
  if (!task) {
@@ -45067,10 +45475,7 @@ function buildSpecGateDeps(args) {
45067
45475
  if (answered && answered.answer) {
45068
45476
  resolvedSeed = "(No operator statement was recorded when this task was created. Derived from the operator's answer to the gate's requirements question:)\n\n" + answered.answer;
45069
45477
  } else {
45070
- const parsed = notes ? parseAcceptanceCriteria(notes) : null;
45071
- if (parsed !== null && parsed.items.length > 0) {
45072
- 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");
45073
- }
45478
+ resolvedSeed = null;
45074
45479
  }
45075
45480
  }
45076
45481
  const adjudicated = selectAdjudicated(taskInboxItems);
@@ -45132,7 +45537,9 @@ ${artifact}`,
45132
45537
  timeoutMs,
45133
45538
  sandbox: "read-only",
45134
45539
  codexExecutable
45135
- }
45540
+ },
45541
+ void 0,
45542
+ consensusProvider
45136
45543
  );
45137
45544
  recordCost("head", personaName, null, startedAt, result);
45138
45545
  return result;
@@ -45149,8 +45556,10 @@ ${artifact}`,
45149
45556
  codexExecutable,
45150
45557
  // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45151
45558
  // default model.
45152
- model: seat?.model ?? seat?.models?.strong
45153
- }
45559
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45560
+ },
45561
+ void 0,
45562
+ consensusProvider
45154
45563
  );
45155
45564
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45156
45565
  return result;
@@ -45173,7 +45582,9 @@ ${artifact}`,
45173
45582
  timeoutMs,
45174
45583
  sandbox: "read-only",
45175
45584
  codexExecutable
45176
- }
45585
+ },
45586
+ void 0,
45587
+ consensusProvider
45177
45588
  );
45178
45589
  recordCost("reviser", null, null, startedAt, result);
45179
45590
  return result;
@@ -45206,6 +45617,28 @@ ${artifact}`,
45206
45617
  return false;
45207
45618
  }
45208
45619
  },
45620
+ async finalizeSpecReviewCas(input) {
45621
+ const history = input.review.history;
45622
+ const highlights = await curateFindings(Array.isArray(history) ? history : [], {
45623
+ runCodexExec: async (execInput) => {
45624
+ const startedAt = Date.now();
45625
+ const result = await runAgentExec(execInput, void 0, consensusProvider);
45626
+ recordCost("findings", "findings-writer", null, startedAt, result);
45627
+ return result;
45628
+ },
45629
+ loadPersona,
45630
+ cwd: sterileCwd(),
45631
+ timeoutMs,
45632
+ codexExecutable
45633
+ });
45634
+ return client.finalizeSpecReviewCas(projectSlug, specPageId, {
45635
+ ...input,
45636
+ review: {
45637
+ ...input.review,
45638
+ ...highlights ? { highlights } : {}
45639
+ }
45640
+ });
45641
+ },
45209
45642
  // Record the consensus-review outcome keyed by content hash via the D2b
45210
45643
  // endpoint. `record` is typed `unknown` (it crosses the SpecGateDeps
45211
45644
  // boundary) so we narrow it defensively:
@@ -45228,7 +45661,7 @@ ${artifact}`,
45228
45661
  // call the cost record silently omits.
45229
45662
  runCodexExec: async (input) => {
45230
45663
  const startedAt = Date.now();
45231
- const result = await runAgentExec(input);
45664
+ const result = await runAgentExec(input, void 0, consensusProvider);
45232
45665
  recordCost("findings", "findings-writer", null, startedAt, result);
45233
45666
  return result;
45234
45667
  },
@@ -45257,7 +45690,13 @@ ${artifact}`,
45257
45690
  }
45258
45691
  function buildCodeGateDeps(args) {
45259
45692
  const { client, projectSlug, baseRef, cwd, codexExecutable, extraHeads, seat, stateDir } = args;
45260
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "code", provider: seat?.id ?? null });
45693
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45694
+ const recordCost = gateCallRecorder({
45695
+ stateDir,
45696
+ projectSlug,
45697
+ gate: "code",
45698
+ provider: consensusProvider ?? seat?.id ?? null
45699
+ });
45261
45700
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45262
45701
  const heads = ["code-reviewer", ...extraHeads ?? []];
45263
45702
  const maxHeadCalls = 2 * rounds * (heads.length + 2);
@@ -45285,60 +45724,72 @@ function buildCodeGateDeps(args) {
45285
45724
  async runHead(personaName, artifact, dismissed) {
45286
45725
  const persona = loadPersona(personaName);
45287
45726
  const startedAt = Date.now();
45288
- const result = await runAgentExec({
45289
- prompt: `Review this code diff.
45727
+ const result = await runAgentExec(
45728
+ {
45729
+ prompt: `Review this code diff.
45290
45730
 
45291
45731
  ${dismissed}
45292
45732
 
45293
45733
  --- DIFF ---
45294
45734
  ${artifact}`,
45295
- developerInstructions: persona,
45296
- // Read-only review of a diff that is entirely in the prompt — sterile, per sterile-cwd.ts.
45297
- // This factory is the ONE that stays mixed: runReviser below keeps the real `cwd` because
45298
- // it edits the branch, and the git helpers keep it because they are not provider calls.
45299
- cwd: sterileCwd(),
45300
- timeoutMs: headTimeoutMs,
45301
- sandbox: "read-only",
45302
- codexExecutable
45303
- });
45735
+ developerInstructions: persona,
45736
+ // Read-only review of a diff that is entirely in the prompt — sterile, per sterile-cwd.ts.
45737
+ // This factory is the ONE that stays mixed: runReviser below keeps the real `cwd` because
45738
+ // it edits the branch, and the git helpers keep it because they are not provider calls.
45739
+ cwd: sterileCwd(),
45740
+ timeoutMs: headTimeoutMs,
45741
+ sandbox: "read-only",
45742
+ codexExecutable
45743
+ },
45744
+ void 0,
45745
+ consensusProvider
45746
+ );
45304
45747
  recordCost("head", personaName, null, startedAt, result);
45305
45748
  return result;
45306
45749
  },
45307
45750
  async runArbiter(prompt) {
45308
45751
  const startedAt = Date.now();
45309
- const result = await runAgentExec({
45310
- prompt,
45311
- developerInstructions: loadPersona("arbiter"),
45312
- cwd: sterileCwd(),
45313
- timeoutMs: headTimeoutMs,
45314
- sandbox: "read-only",
45315
- codexExecutable,
45316
- // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45317
- // default model.
45318
- model: seat?.model ?? seat?.models?.strong
45319
- });
45752
+ const result = await runAgentExec(
45753
+ {
45754
+ prompt,
45755
+ developerInstructions: loadPersona("arbiter"),
45756
+ cwd: sterileCwd(),
45757
+ timeoutMs: headTimeoutMs,
45758
+ sandbox: "read-only",
45759
+ codexExecutable,
45760
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45761
+ // default model.
45762
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45763
+ },
45764
+ void 0,
45765
+ consensusProvider
45766
+ );
45320
45767
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45321
45768
  return result;
45322
45769
  },
45323
45770
  async runReviser(artifact, accepted) {
45324
45771
  const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
45325
45772
  const startedAt = Date.now();
45326
- const result = await runAgentExec({
45327
- prompt: `Accepted issues to fix:
45773
+ const result = await runAgentExec(
45774
+ {
45775
+ prompt: `Accepted issues to fix:
45328
45776
  ${acceptedList}
45329
45777
 
45330
45778
  --- DIFF ---
45331
45779
  ${artifact}`,
45332
- developerInstructions: "You are fixing code review issues. Edit ONLY files already changed in this branch's diff; make the minimal change to resolve each accepted issue. Do not refactor unrelated code. Do not edit files outside the diff.",
45333
- // The real repository root, NOT sterileCwd(): this is the one gate call that runs
45334
- // `workspace-write` and edits the branch, so a sterile cwd would point the sandbox at an
45335
- // empty directory and the revision would silently apply to nothing — the gate would keep
45336
- // reporting rounds while fixing no code at all.
45337
- cwd,
45338
- timeoutMs: reviserTimeoutMs,
45339
- sandbox: "workspace-write",
45340
- codexExecutable
45341
- });
45780
+ developerInstructions: "You are fixing code review issues. Edit ONLY files already changed in this branch's diff; make the minimal change to resolve each accepted issue. Do not refactor unrelated code. Do not edit files outside the diff.",
45781
+ // The real repository root, NOT sterileCwd(): this is the one gate call that runs
45782
+ // `workspace-write` and edits the branch, so a sterile cwd would point the sandbox at an
45783
+ // empty directory and the revision would silently apply to nothing — the gate would keep
45784
+ // reporting rounds while fixing no code at all.
45785
+ cwd,
45786
+ timeoutMs: reviserTimeoutMs,
45787
+ sandbox: "workspace-write",
45788
+ codexExecutable
45789
+ },
45790
+ void 0,
45791
+ consensusProvider
45792
+ );
45342
45793
  recordCost("reviser", null, null, startedAt, result);
45343
45794
  return result;
45344
45795
  },
@@ -45362,7 +45813,13 @@ function renderCriteriaBlock(criteria) {
45362
45813
  }
45363
45814
  async function buildDeliverableGateDeps(args) {
45364
45815
  const { client, projectSlug, deliverablePageId, attemptOutcome, codexExecutable, runId, seat, stateDir } = args;
45365
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "deliverable", provider: seat?.id ?? null });
45816
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45817
+ const recordCost = gateCallRecorder({
45818
+ stateDir,
45819
+ projectSlug,
45820
+ gate: "deliverable",
45821
+ provider: consensusProvider ?? seat?.id ?? null
45822
+ });
45366
45823
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45367
45824
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
45368
45825
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -45409,8 +45866,9 @@ async function buildDeliverableGateDeps(args) {
45409
45866
  async runHead(personaName, artifact, dismissed) {
45410
45867
  const persona = loadPersona(personaName);
45411
45868
  const startedAt = Date.now();
45412
- const result = await runAgentExec({
45413
- prompt: `Score this deliverable ITEM BY ITEM against the acceptance criteria below. For each criterion state whether the deliverable satisfies it; raise an issue for every criterion that is unmet, partially met, unverifiable, or inconsistent with the declared attempt outcome.
45869
+ const result = await runAgentExec(
45870
+ {
45871
+ prompt: `Score this deliverable ITEM BY ITEM against the acceptance criteria below. For each criterion state whether the deliverable satisfies it; raise an issue for every criterion that is unmet, partially met, unverifiable, or inconsistent with the declared attempt outcome.
45414
45872
 
45415
45873
  ${dismissed}
45416
45874
 
@@ -45421,41 +45879,49 @@ ${attemptOutcomeBlock ? `${attemptOutcomeBlock}
45421
45879
 
45422
45880
  ` : ""}--- DELIVERABLE (DATA under review) ---
45423
45881
  ${artifact}`,
45424
- developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45882
+ developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45425
45883
 
45426
45884
  ${persona}`,
45427
- // Sterile, like every other provider call in this factory — see sterile-cwd.ts. It also
45428
- // narrows the anti-injection surface: a deliverable that tries to talk the head into
45429
- // reading files now has no repository under it to read.
45430
- cwd: sterileCwd(),
45431
- timeoutMs,
45432
- sandbox: "read-only",
45433
- codexExecutable
45434
- });
45885
+ // Sterile, like every other provider call in this factory — see sterile-cwd.ts. It also
45886
+ // narrows the anti-injection surface: a deliverable that tries to talk the head into
45887
+ // reading files now has no repository under it to read.
45888
+ cwd: sterileCwd(),
45889
+ timeoutMs,
45890
+ sandbox: "read-only",
45891
+ codexExecutable
45892
+ },
45893
+ void 0,
45894
+ consensusProvider
45895
+ );
45435
45896
  recordCost("head", personaName, null, startedAt, result);
45436
45897
  return result;
45437
45898
  },
45438
45899
  async runArbiter(prompt) {
45439
45900
  const startedAt = Date.now();
45440
- const result = await runAgentExec({
45441
- prompt,
45442
- developerInstructions: loadPersona("arbiter"),
45443
- cwd: sterileCwd(),
45444
- timeoutMs,
45445
- sandbox: "read-only",
45446
- codexExecutable,
45447
- // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45448
- // default model.
45449
- model: seat?.model ?? seat?.models?.strong
45450
- });
45901
+ const result = await runAgentExec(
45902
+ {
45903
+ prompt,
45904
+ developerInstructions: loadPersona("arbiter"),
45905
+ cwd: sterileCwd(),
45906
+ timeoutMs,
45907
+ sandbox: "read-only",
45908
+ codexExecutable,
45909
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45910
+ // default model.
45911
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45912
+ },
45913
+ void 0,
45914
+ consensusProvider
45915
+ );
45451
45916
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45452
45917
  return result;
45453
45918
  },
45454
45919
  async runReviser(artifact, accepted) {
45455
45920
  const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
45456
45921
  const startedAt = Date.now();
45457
- const result = await runAgentExec({
45458
- prompt: `Accepted issues to fix (each maps to an unmet acceptance criterion):
45922
+ const result = await runAgentExec(
45923
+ {
45924
+ prompt: `Accepted issues to fix (each maps to an unmet acceptance criterion):
45459
45925
  ${acceptedList}
45460
45926
 
45461
45927
  --- ACCEPTANCE CRITERIA ---
@@ -45463,14 +45929,17 @@ ${criteriaBlock}
45463
45929
 
45464
45930
  --- DELIVERABLE ---
45465
45931
  ${artifact}`,
45466
- developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45932
+ developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45467
45933
 
45468
45934
  You revise a deliverable to address the accepted issues so it satisfies the acceptance criteria and stays consistent with the declared outcome${attemptOutcome ? ` (${attemptOutcome})` : ""}. Return ONLY the full revised markdown, no commentary. Do not invent facts; keep the deliverable's format and do not change the declared outcome.`,
45469
- cwd: sterileCwd(),
45470
- timeoutMs,
45471
- sandbox: "read-only",
45472
- codexExecutable
45473
- });
45935
+ cwd: sterileCwd(),
45936
+ timeoutMs,
45937
+ sandbox: "read-only",
45938
+ codexExecutable
45939
+ },
45940
+ void 0,
45941
+ consensusProvider
45942
+ );
45474
45943
  recordCost("reviser", null, null, startedAt, result);
45475
45944
  return result;
45476
45945
  },
@@ -45501,7 +45970,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
45501
45970
  const rounds2 = typeof r["rounds"] === "number" ? r["rounds"] : 0;
45502
45971
  const history = r["history"];
45503
45972
  const highlights = await curateFindings(Array.isArray(history) ? history : [], {
45504
- runCodexExec: runAgentExec,
45973
+ runCodexExec: (execInput) => runAgentExec(execInput, void 0, consensusProvider),
45505
45974
  loadPersona,
45506
45975
  cwd: sterileCwd(),
45507
45976
  timeoutMs,
@@ -49891,22 +50360,22 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
49891
50360
  }
49892
50361
  const nonInteractiveFlow = !provider.redirectUrl;
49893
50362
  if (authorizationCode !== void 0 || nonInteractiveFlow) {
49894
- const tokens2 = await fetchToken(provider, authorizationServerUrl, {
50363
+ const tokens3 = await fetchToken(provider, authorizationServerUrl, {
49895
50364
  metadata,
49896
50365
  resource,
49897
50366
  authorizationCode,
49898
50367
  fetchFn
49899
50368
  });
49900
- await provider.saveTokens(tokens2);
50369
+ await provider.saveTokens(tokens3);
49901
50370
  return "AUTHORIZED";
49902
50371
  }
49903
- const tokens = await provider.tokens();
49904
- if (tokens?.refresh_token) {
50372
+ const tokens2 = await provider.tokens();
50373
+ if (tokens2?.refresh_token) {
49905
50374
  try {
49906
50375
  const newTokens = await refreshAuthorization(authorizationServerUrl, {
49907
50376
  metadata,
49908
50377
  clientInformation,
49909
- refreshToken: tokens.refresh_token,
50378
+ refreshToken: tokens2.refresh_token,
49910
50379
  resource,
49911
50380
  addClientAuthentication: provider.addClientAuthentication,
49912
50381
  fetchFn
@@ -50208,7 +50677,7 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
50208
50677
  grant_type: "refresh_token",
50209
50678
  refresh_token: refreshToken
50210
50679
  });
50211
- const tokens = await executeTokenRequest(authorizationServerUrl, {
50680
+ const tokens2 = await executeTokenRequest(authorizationServerUrl, {
50212
50681
  metadata,
50213
50682
  tokenRequestParams,
50214
50683
  clientInformation,
@@ -50216,7 +50685,7 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
50216
50685
  resource,
50217
50686
  fetchFn
50218
50687
  });
50219
- return { refresh_token: refreshToken, ...tokens };
50688
+ return { refresh_token: refreshToken, ...tokens2 };
50220
50689
  }
50221
50690
  async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
50222
50691
  const scope = provider.clientMetadata.scope;
@@ -50521,9 +50990,9 @@ var StreamableHTTPClientTransport = class {
50521
50990
  async _commonHeaders() {
50522
50991
  const headers = {};
50523
50992
  if (this._authProvider) {
50524
- const tokens = await this._authProvider.tokens();
50525
- if (tokens) {
50526
- headers["Authorization"] = `Bearer ${tokens.access_token}`;
50993
+ const tokens2 = await this._authProvider.tokens();
50994
+ if (tokens2) {
50995
+ headers["Authorization"] = `Bearer ${tokens2.access_token}`;
50527
50996
  }
50528
50997
  }
50529
50998
  if (this._sessionId) {
@@ -52284,13 +52753,87 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52284
52753
  suppressedAsAdjudicated.push({ rawQuestion: clarification.rawQuestion, coveredByAnswer: covering.answer });
52285
52754
  return false;
52286
52755
  });
52756
+ let planId = null;
52757
+ if (!taskId) {
52758
+ const plans = await ctx.client.listPlans(input.projectSlug);
52759
+ planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
52760
+ }
52761
+ const evaluatedContentHash = result.evaluatedSnapshot?.contentHash ?? page?.contentHash;
52762
+ const isSeedAbsence = operatorClarifications.some(
52763
+ (c) => c.rawQuestion === SEED_ABSENCE_QUESTION || c.rawQuestion.includes(SEED_ABSENCE_MARKER)
52764
+ );
52765
+ if (!isSeedAbsence && operatorClarifications.length > 0 && evaluatedContentHash) {
52766
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
52767
+ id: input.pageId,
52768
+ taskId
52769
+ });
52770
+ const suppressionEvidence = (result.droppedByCap?.length ?? 0) > 0 || suppressedAsAdjudicated.length > 0 ? {
52771
+ ...result.droppedByCap?.length ? { droppedByCap: result.droppedByCap } : {},
52772
+ ...suppressedAsAdjudicated.length ? { suppressedAsAdjudicated } : {}
52773
+ } : void 0;
52774
+ let ownership;
52775
+ if (result.evaluatedOwnership) {
52776
+ ownership = result.evaluatedOwnership;
52777
+ } else if (taskId !== null) {
52778
+ const task = await ctx.client.getTask(input.projectSlug, taskId).catch(() => null);
52779
+ ownership = { kind: "task", taskId, domains: task?.domains ?? [domain2] };
52780
+ } else if (planId !== null) {
52781
+ const plans = await ctx.client.listPlans(input.projectSlug).catch(() => []);
52782
+ const plan = plans.find((p) => p.id === planId);
52783
+ ownership = { kind: "plan", planId, domains: plan?.domains ?? ["code"] };
52784
+ } else {
52785
+ ownership = { kind: "project", domains: ["code"] };
52786
+ }
52787
+ if (ctx.client.finalizeSpecReviewCas) {
52788
+ const finalizeRes = await ctx.client.finalizeSpecReviewCas(input.projectSlug, input.pageId, {
52789
+ expectedContentHash: evaluatedContentHash,
52790
+ ownership,
52791
+ review: {
52792
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
52793
+ rounds: result.rounds !== void 0 ? result.rounds : result.history?.length ? result.history.length : 0,
52794
+ ...result.history !== void 0 ? { history: result.history } : {},
52795
+ ...result.highlights !== void 0 ? { highlights: Array.isArray(result.highlights) ? { findings: result.highlights } : result.highlights } : {},
52796
+ ...suppressionEvidence ? { suppressionEvidence } : {}
52797
+ },
52798
+ specReady: false
52799
+ });
52800
+ if (finalizeRes.status === "superseded") {
52801
+ return {
52802
+ kind: "superseded",
52803
+ finalReviewedHash: evaluatedContentHash,
52804
+ error: "Spec content changed during review",
52805
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52806
+ };
52807
+ }
52808
+ if (finalizeRes.status !== "applied") {
52809
+ return {
52810
+ kind: "failed",
52811
+ failureReason: "consensus_blocked",
52812
+ error: finalizeRes.reason ?? "cas_rejected",
52813
+ finalReviewedHash: evaluatedContentHash,
52814
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52815
+ };
52816
+ }
52817
+ } else if (ctx.client.recordSpecConsensusReview) {
52818
+ try {
52819
+ await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
52820
+ contentHash: domainScopedReviewHash2(evaluatedContentHash, domain2),
52821
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
52822
+ rounds: result.rounds !== void 0 ? result.rounds : result.history?.length ? result.history.length : 0,
52823
+ ...result.history !== void 0 ? { history: result.history } : {},
52824
+ ...result.highlights !== void 0 ? { highlights: Array.isArray(result.highlights) ? { findings: result.highlights } : result.highlights } : {},
52825
+ ...suppressionEvidence ? { suppressionEvidence } : {}
52826
+ });
52827
+ } catch (error52) {
52828
+ process.stderr.write(
52829
+ `[mrrlin-mcp consensus] legacy clarification write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
52830
+ `
52831
+ );
52832
+ }
52833
+ }
52834
+ }
52287
52835
  let filedQuestionCount = 0;
52288
52836
  if (toFile.length > 0) {
52289
- let planId = null;
52290
- if (!taskId) {
52291
- const plans = await ctx.client.listPlans(input.projectSlug);
52292
- planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
52293
- }
52294
52837
  if (taskId) {
52295
52838
  const scopeId = taskId;
52296
52839
  const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto4.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
@@ -52307,7 +52850,11 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52307
52850
  questionOptions,
52308
52851
  ...groupId ? { groupId } : {}
52309
52852
  });
52310
- } catch {
52853
+ } catch (err) {
52854
+ const isConflict = err?.status === 409 || err?.statusCode === 409 || err?.code === "23505" || String(err?.message ?? "").includes("already exists") || String(err?.message ?? "").includes("duplicate key");
52855
+ if (!isConflict) {
52856
+ throw err;
52857
+ }
52311
52858
  }
52312
52859
  }
52313
52860
  filedQuestionCount = toFile.length;
@@ -52327,7 +52874,11 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52327
52874
  questionOptions,
52328
52875
  ...groupId ? { groupId } : {}
52329
52876
  });
52330
- } catch {
52877
+ } catch (err) {
52878
+ const isConflict = err?.status === 409 || err?.statusCode === 409 || err?.code === "23505" || String(err?.message ?? "").includes("already exists") || String(err?.message ?? "").includes("duplicate key");
52879
+ if (!isConflict) {
52880
+ throw err;
52881
+ }
52331
52882
  }
52332
52883
  }
52333
52884
  filedQuestionCount = toFile.length;
@@ -52356,40 +52907,15 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52356
52907
  } catch {
52357
52908
  }
52358
52909
  }
52359
- if (operatorClarifications.length > 0) {
52360
- try {
52361
- if (page) {
52362
- const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
52363
- id: page.id,
52364
- taskId: page.taskId ?? null
52365
- });
52366
- const suppressionEvidence = (result.droppedByCap?.length ?? 0) > 0 || suppressedAsAdjudicated.length > 0 ? {
52367
- ...result.droppedByCap?.length ? { droppedByCap: result.droppedByCap } : {},
52368
- ...suppressedAsAdjudicated.length ? { suppressedAsAdjudicated } : {}
52369
- } : void 0;
52370
- await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
52371
- contentHash: domainScopedReviewHash2(page.contentHash, domain2),
52372
- outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
52373
- rounds: 0,
52374
- ...suppressionEvidence ? { suppressionEvidence } : {}
52375
- });
52376
- }
52377
- } catch (error52) {
52378
- process.stderr.write(
52379
- `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
52380
- `
52381
- );
52382
- }
52383
- }
52384
52910
  let error51;
52385
52911
  if (filedQuestionCount > 0) {
52386
52912
  error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
52387
52913
  } else if (diagnostics.length > 0 && operatorClarifications.length === 0) {
52388
52914
  const diagnosticKinds = new Set(diagnostics.map((d) => d.kind));
52389
52915
  if (diagnosticKinds.has("acceptance_criteria_unauthorable")) {
52390
- error51 = "Consensus blocked: the spec has no machine-usable acceptance criteria and the reviser could not add them automatically. A human needs to write the `## Acceptance criteria` section directly, not an inbox answer.";
52916
+ error51 = "Consensus paused: the spec needs a clear `## Acceptance criteria` checklist (`- [ ]`) before review can complete. Please add or update the checklist in the spec.";
52391
52917
  } else {
52392
- error51 = "Consensus blocked: the spec content does not match its linked task. This requires fixing the spec/task relationship directly, not an inbox answer.";
52918
+ error51 = "Consensus paused: the spec content does not match its linked task. Please update the spec or task relationship directly, not an inbox answer.";
52393
52919
  }
52394
52920
  } else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
52395
52921
  error51 = `The reviewers raised ${suppressedAsAdjudicated.length} clarification(s), but each was a reworded repeat of one already answered \u2014 nothing new to answer.`;
@@ -52401,11 +52927,13 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52401
52927
  if (result.escalation) {
52402
52928
  error51 += ` (reason: ${result.escalation.diagnosis})`;
52403
52929
  }
52930
+ const isRelationshipDiagnostic = isSeedAbsence || result.diagnostics?.some((d) => d.kind === "spec_identity_mismatch") || result.escalation?.diagnosis === "spec_identity_mismatch";
52404
52931
  return {
52405
52932
  kind: "failed",
52406
52933
  failureReason: "consensus_blocked",
52407
52934
  error: error51,
52408
52935
  filedQuestionCount,
52936
+ finalReviewedHash: isRelationshipDiagnostic ? null : result.evaluatedSnapshot?.contentHash ?? null,
52409
52937
  ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52410
52938
  };
52411
52939
  }
@@ -52419,18 +52947,30 @@ async function retireGateQuestionsOnConverge(ctx, input, resolvedTaskId) {
52419
52947
  const contentHash = page?.contentHash ?? null;
52420
52948
  const inboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId: resolvedTaskId });
52421
52949
  for (const item of inboxItems) {
52422
- if (item.origin !== "gate" || item.status !== "pending" || item.kind !== "question" || item.taskId != null && item.taskId !== resolvedTaskId) {
52950
+ if (item.status !== "pending") {
52951
+ continue;
52952
+ }
52953
+ if (item.taskId != null && item.taskId !== resolvedTaskId) {
52954
+ continue;
52955
+ }
52956
+ const isGateQuestion = (item.origin === "gate" || item.origin === "gate_block") && item.kind === "question";
52957
+ const specRefPattern = new RegExp(
52958
+ `Specs/${input.pageId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![0-9a-zA-Z_-])`
52959
+ );
52960
+ const isSpecHandoff = item.kind === "handoff" && item.id.startsWith(`II-SPEC-STOPPED-${input.pageId}-`) && specRefPattern.test(item.body);
52961
+ if (!isGateQuestion && !isSpecHandoff) {
52423
52962
  continue;
52424
52963
  }
52425
52964
  try {
52965
+ const verdict = isGateQuestion ? "rejected" : "acknowledged";
52426
52966
  await ctx.client.decideInboxItem(input.projectSlug, item.id, {
52427
- verdict: "rejected",
52967
+ verdict,
52428
52968
  decidedBy: "gate",
52429
52969
  reason: `Superseded by converged spec Specs/${input.pageId} (contentHash: ${contentHash ?? "unknown"})`
52430
52970
  });
52431
52971
  } catch (itemError) {
52432
52972
  process.stderr.write(
52433
- `[mrrlin-mcp consensus] gate-question retirement failed for ${input.projectSlug}/${input.pageId} item ${item.id}: ${itemError instanceof Error ? itemError.message : String(itemError)}
52973
+ `[mrrlin-mcp consensus] gate retirement failed for ${input.projectSlug}/${input.pageId} item ${item.id}: ${itemError instanceof Error ? itemError.message : String(itemError)}
52434
52974
  `
52435
52975
  );
52436
52976
  }
@@ -52520,6 +53060,13 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
52520
53060
  return { kind: "retry", failureReason: "provider_unavailable", error: "Consensus unavailable (provider/quota).", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
52521
53061
  case "SPEC_CHANGED_DURING_REVIEW":
52522
53062
  return { kind: "retry", failureReason: "spec_changed", error: "Spec changed during review.", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
53063
+ case "SUPERSEDED":
53064
+ return {
53065
+ kind: "superseded",
53066
+ reason: "Spec changed or ownership unlinked during review",
53067
+ error: "Spec changed or ownership unlinked during review",
53068
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
53069
+ };
52523
53070
  default:
52524
53071
  return {
52525
53072
  kind: "failed",
@@ -52530,6 +53077,14 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
52530
53077
  }
52531
53078
  }
52532
53079
  function onComplete(outcome, input) {
53080
+ if (outcome.kind === "superseded") {
53081
+ return {
53082
+ kind: "info",
53083
+ suppressInbox: true,
53084
+ body: outcome.reason ?? "Superseded",
53085
+ ...outcome.taskId ? { taskId: outcome.taskId } : {}
53086
+ };
53087
+ }
52533
53088
  const taskId = "taskId" in outcome ? outcome.taskId : void 0;
52534
53089
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
52535
53090
  if (outcome.kind === "done") {
@@ -52579,10 +53134,12 @@ ${findings.map((f) => `\u2022 ${f}`).join("\n")}` : "";
52579
53134
  };
52580
53135
  }
52581
53136
  const handoffTaskRef = taskRef ? ` (${taskRef})` : "";
53137
+ const revisionSuffix = "finalReviewedHash" in outcome && outcome.finalReviewedHash ? outcome.finalReviewedHash.slice(0, 8) : "init";
52582
53138
  return {
52583
53139
  kind: "handoff",
53140
+ id: `II-SPEC-STOPPED-${input.pageId}-${revisionSuffix}`,
52584
53141
  ...taskId ? { taskId } : {},
52585
- body: `The spec review stopped for Specs/${input.pageId}${handoffTaskRef}. ${outcome.error} Review the item, consult the Director if needed, then continue in Codex or handle it manually.`
53142
+ body: `The spec review stopped for Specs/${input.pageId}${handoffTaskRef}. ${outcome.error} Review the spec or consult the Director to update it.`
52586
53143
  };
52587
53144
  }
52588
53145
  var consensusDescriptor = {
@@ -52761,6 +53318,14 @@ async function run2(ctx, input) {
52761
53318
  };
52762
53319
  }
52763
53320
  function onComplete2(outcome, input) {
53321
+ if (outcome.kind === "superseded") {
53322
+ return {
53323
+ kind: "info",
53324
+ suppressInbox: true,
53325
+ body: outcome.reason ?? outcome.error ?? "Superseded",
53326
+ ...outcome.taskId ? { taskId: outcome.taskId } : {}
53327
+ };
53328
+ }
52764
53329
  const taskId = outcome.kind === "failed" || outcome.kind === "done" ? outcome.taskId : void 0;
52765
53330
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
52766
53331
  if (outcome.kind === "done") {
@@ -52818,7 +53383,7 @@ function defaultSleep(ms) {
52818
53383
  return new Promise((resolve) => setTimeout(resolve, ms));
52819
53384
  }
52820
53385
  function enforceSpecOutputContract(markdown, taskId, title) {
52821
- let body = markdown.trim();
53386
+ let body = normalizeAcceptanceCriteria(markdown.trim());
52822
53387
  const headingRe = new RegExp(`^#\\s+${taskId}\\b`, "m");
52823
53388
  if (!headingRe.test(body)) {
52824
53389
  body = `# ${taskId} \u2014 ${title}
@@ -53275,6 +53840,14 @@ async function runDraftSpec(ctx, input, deps) {
53275
53840
  };
53276
53841
  }
53277
53842
  function onComplete3(outcome, input) {
53843
+ if (outcome.kind === "superseded") {
53844
+ return {
53845
+ kind: "info",
53846
+ suppressInbox: true,
53847
+ body: outcome.reason ?? outcome.error ?? "Superseded",
53848
+ taskId: input.taskId
53849
+ };
53850
+ }
53278
53851
  if (outcome.kind === "done") {
53279
53852
  let parsed = {};
53280
53853
  try {
@@ -54167,6 +54740,9 @@ async function runReportEvidence(ctx, input) {
54167
54740
  return { kind: "done", resultJson: JSON.stringify(artifact), finalReviewedHash: null };
54168
54741
  }
54169
54742
  function onComplete5(outcome, input) {
54743
+ if (outcome.kind === "superseded") {
54744
+ return { kind: "info", body: `Evidence assembly superseded for report ${input.reportId}.`, suppressInbox: true };
54745
+ }
54170
54746
  if (outcome.kind === "done") {
54171
54747
  return { kind: "info", body: `Evidence assembled for report ${input.reportId}.`, suppressInbox: true };
54172
54748
  }
@@ -55350,7 +55926,7 @@ var mcpToolDescriptions = {
55350
55926
  [mcpToolNames.appendTaskEvent]: "Append an operator note to a task's activity timeline. Also use it as the task-linked receipt path for material immediate work that has a parent task; summarize scope, outcome/no-result, evidence or artifacts, blockers, and next action in the note body.",
55351
55927
  [mcpToolNames.createWikiPage]: "Create a markdown Wiki page.",
55352
55928
  [mcpToolNames.deleteTaskDependency]: "Delete a blocked-by relationship.",
55353
- [mcpToolNames.createTask]: "Create a new task in a project. Defaults to status='backlog' with a blank assignee; pass status='todo' explicitly to authorize immediate autopilot work. 'backlog' and 'todo' are the only creatable statuses. 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. When creating tasks for work that produces a published web page, author the work as a [code_change] task and a [page] task (with domain: 'content') linked by blocked_by. Set autoDeploy according to the publish policy: 'prod' if only meaningful once live (robots.txt, sitemap), 'dev' to stop at preview (landing page, layout), or 'off' if the repo is not the operator's own.",
55929
+ [mcpToolNames.createTask]: "Create a new task in a project. Defaults to status='todo' to authorize immediate autopilot work; pass status='backlog' only if explicitly parking the task for later. 'backlog' and 'todo' are the only creatable statuses. 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. When creating tasks for work that produces a published web page, author the work as a [code_change] task and a [page] task (with domain: 'content') linked by blocked_by. Set autoDeploy according to the publish policy: 'prod' if only meaningful once live (robots.txt, sitemap), 'dev' to stop at preview (landing page, layout), or 'off' if the repo is not the operator's own.",
55354
55930
  [mcpToolNames.getProject]: "Fetch a single project summary by slug.",
55355
55931
  [mcpToolNames.getProjectSnapshot]: "Fetch the full project snapshot: GitHub binding, tasks, plans, and wiki pages.",
55356
55932
  [mcpToolNames.getTask]: "Fetch a single task by project slug and task id.",
@@ -55591,20 +56167,18 @@ async function getAgentProviderForPanel(client, opts = {}) {
55591
56167
  }
55592
56168
  function createMrrlinTools(options) {
55593
56169
  const client = createMrrlinClient(withAuth(options));
55594
- let boundSlug = options.activeProjectSlug ?? (process.env.MRRLIN_PROJECT_SLUG?.trim() || void 0);
55595
- let sessionResolved = false;
55596
56170
  async function resolveBoundSlug() {
55597
- if (boundSlug !== void 0) return boundSlug;
55598
- if (sessionResolved) return boundSlug;
56171
+ if (options.activeProjectSlug !== void 0) return options.activeProjectSlug;
56172
+ const envSlug = process.env.MRRLIN_PROJECT_SLUG?.trim();
56173
+ if (envSlug) return envSlug;
55599
56174
  try {
55600
56175
  const settings = await client.getOperatorSettings();
55601
56176
  if (settings?.activeProjectSlug) {
55602
- boundSlug = settings.activeProjectSlug;
56177
+ return settings.activeProjectSlug;
55603
56178
  }
55604
56179
  } catch {
55605
56180
  }
55606
- sessionResolved = true;
55607
- return boundSlug;
56181
+ return void 0;
55608
56182
  }
55609
56183
  async function checkTenantBoundary(targetSlug, resourceName) {
55610
56184
  const active = await resolveBoundSlug();
@@ -55621,6 +56195,22 @@ function createMrrlinTools(options) {
55621
56195
  );
55622
56196
  }
55623
56197
  }
56198
+ async function autoRetirePendingScopeQuestions(c, projectSlug, resolutionContext) {
56199
+ try {
56200
+ const pendingQuestions = await c.listInboxItems(projectSlug, { status: "pending", kind: "question" });
56201
+ for (const item of pendingQuestions) {
56202
+ if (!item.taskId) {
56203
+ await c.decideInboxItem(projectSlug, item.id, {
56204
+ decidedBy: "director",
56205
+ verdict: "acknowledged",
56206
+ answer: `Addressed in materialized plan/tasks (${resolutionContext})`
56207
+ }).catch(() => {
56208
+ });
56209
+ }
56210
+ }
56211
+ } catch {
56212
+ }
56213
+ }
55624
56214
  return {
55625
56215
  [mcpToolNames.archivePlan]: makeTool(
55626
56216
  client,
@@ -55639,7 +56229,9 @@ function createMrrlinTools(options) {
55639
56229
  "Unable to create plan.",
55640
56230
  async (c, { projectSlug, ...input }) => {
55641
56231
  await checkTenantBoundary(projectSlug, "plan");
55642
- return await c.createPlan(projectSlug, input);
56232
+ const plan = await c.createPlan(projectSlug, input);
56233
+ await autoRetirePendingScopeQuestions(c, projectSlug, `Plan ${plan.id}: ${plan.name}`);
56234
+ return plan;
55643
56235
  }
55644
56236
  ),
55645
56237
  [mcpToolNames.createTaskDependency]: makeTool(
@@ -55734,6 +56326,13 @@ function createMrrlinTools(options) {
55734
56326
  ...input,
55735
56327
  folder: input.folder?.trim().toLowerCase() === "specs" ? "Specs" : input.folder
55736
56328
  };
56329
+ if (pageInput.folder === "Specs") {
56330
+ if (!parseAcceptanceCriteria(pageInput.markdownBody)) {
56331
+ throw new Error(
56332
+ "Error: Specifications in folder 'Specs' MUST include an '## Acceptance criteria' section with at least one top-level markdown checklist item ('- [ ] <condition>'). Please re-author the page body with verifiable checklist criteria."
56333
+ );
56334
+ }
56335
+ }
55737
56336
  return await deliverBuiltIn(
55738
56337
  c,
55739
56338
  projectSlug,
@@ -55760,7 +56359,10 @@ function createMrrlinTools(options) {
55760
56359
  "Unable to create task.",
55761
56360
  async (c, { projectSlug, ...input }) => {
55762
56361
  await checkTenantBoundary(projectSlug, "task");
55763
- return await c.createTask(projectSlug, input);
56362
+ const status = input.status ?? "todo";
56363
+ const task = await c.createTask(projectSlug, { ...input, status });
56364
+ await autoRetirePendingScopeQuestions(c, projectSlug, `Task ${task.id}: ${task.title}`);
56365
+ return task;
55764
56366
  }
55765
56367
  ),
55766
56368
  [mcpToolNames.getProject]: makeTool(
@@ -55979,6 +56581,20 @@ function createMrrlinTools(options) {
55979
56581
  ...patch,
55980
56582
  ...patch.folder !== void 0 ? { folder: patch.folder.trim().toLowerCase() === "specs" ? "Specs" : patch.folder } : {}
55981
56583
  };
56584
+ let targetFolder = patchInput.folder;
56585
+ let targetBody = patch.markdownBody;
56586
+ if ((targetFolder === void 0 || targetBody === void 0) && (targetFolder === "Specs" || targetBody !== void 0)) {
56587
+ const existing = await c.getWikiPage(projectSlug, pageId);
56588
+ if (targetFolder === void 0) targetFolder = existing.folder;
56589
+ if (targetBody === void 0) targetBody = existing.markdownBody;
56590
+ }
56591
+ if (targetFolder === "Specs" && targetBody !== void 0) {
56592
+ if (!parseAcceptanceCriteria(targetBody)) {
56593
+ throw new Error(
56594
+ "Error: Specifications in folder 'Specs' MUST include an '## Acceptance criteria' section with at least one top-level markdown checklist item ('- [ ] <condition>'). Please re-author the page body with verifiable checklist criteria."
56595
+ );
56596
+ }
56597
+ }
55982
56598
  return await deliverBuiltIn(
55983
56599
  c,
55984
56600
  projectSlug,
@@ -58077,7 +58693,719 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
58077
58693
  }
58078
58694
 
58079
58695
  // src/_generated/version.ts
58080
- var PKG_VERSION = "0.4.95";
58696
+ var PKG_VERSION = "0.4.97";
58697
+
58698
+ // src/director-closure.ts
58699
+ var CLOSURE_SNAPSHOT_TIMEOUT_MS = 2e3;
58700
+ var CLOSURE_MAX_SNAPSHOT_PAGES = 500;
58701
+ var CLOSURE_MAX_SNAPSHOT_TASKS = 2e3;
58702
+ var CLOSURE_MAX_SNAPSHOT_INBOX = 500;
58703
+ var CLOSURE_MIN_PROSE_CHARS = 25;
58704
+ var CLOSURE_MIN_PROSE_WORDS = 4;
58705
+ var ID_RE = /\b(?:GT|WK)-\d+\b/g;
58706
+ function diffSnapshots(before, after) {
58707
+ const receipts = [];
58708
+ const diffMap = (kind, prev, next) => {
58709
+ for (const [id, entry] of next) {
58710
+ const old = prev.get(id);
58711
+ if (!old) {
58712
+ receipts.push({ kind, action: "created", id, title: entry.title });
58713
+ } else if (entry.updatedAt !== null && entry.updatedAt !== old.updatedAt) {
58714
+ receipts.push({ kind, action: "updated", id, title: entry.title });
58715
+ }
58716
+ }
58717
+ };
58718
+ diffMap("task", before.tasks, after.tasks);
58719
+ if (before.pages && after.pages) diffMap("page", before.pages, after.pages);
58720
+ if (before.inbox && after.inbox) {
58721
+ for (const id of after.inbox) {
58722
+ if (!before.inbox.has(id)) receipts.push({ kind: "inbox", action: "created", id, title: "" });
58723
+ }
58724
+ }
58725
+ return receipts;
58726
+ }
58727
+ function finalAssistantText(events) {
58728
+ let lastToolResult = -1;
58729
+ events.forEach((event, index) => {
58730
+ if (event.type === "tool_result") lastToolResult = index;
58731
+ });
58732
+ return events.slice(lastToolResult + 1).filter((event) => event.type === "assistant" && typeof event.content === "string").map((event) => event.content).join("");
58733
+ }
58734
+ var MUTATION_TOOLS = /* @__PURE__ */ new Set([
58735
+ "create_task",
58736
+ "update_task",
58737
+ "create_wiki_page",
58738
+ "update_wiki_page",
58739
+ "create_inbox_item"
58740
+ ]);
58741
+ function extractStructuredResultId(content) {
58742
+ if (!content) return null;
58743
+ let parsed = content;
58744
+ if (typeof content === "string") {
58745
+ try {
58746
+ parsed = JSON.parse(content);
58747
+ } catch {
58748
+ const trimmed = content.trim();
58749
+ if (/^(?:GT|WK)-\d+$/.test(trimmed) || /^II-[0-9a-zA-Z-]+$/.test(trimmed)) {
58750
+ return trimmed;
58751
+ }
58752
+ return null;
58753
+ }
58754
+ }
58755
+ if (Array.isArray(parsed)) {
58756
+ for (const item of parsed) {
58757
+ const id = extractStructuredResultId(item);
58758
+ if (id) return id;
58759
+ }
58760
+ return null;
58761
+ }
58762
+ if (parsed && typeof parsed === "object") {
58763
+ const obj = parsed;
58764
+ if (typeof obj.text === "string") {
58765
+ const id = extractStructuredResultId(obj.text);
58766
+ if (id) return id;
58767
+ }
58768
+ if (obj.structuredContent && typeof obj.structuredContent === "object") {
58769
+ const sc = obj.structuredContent;
58770
+ if (sc.data && typeof sc.data === "object" && typeof sc.data.id === "string") {
58771
+ return sc.data.id;
58772
+ }
58773
+ if (typeof sc.id === "string") return sc.id;
58774
+ }
58775
+ if (obj.data && typeof obj.data === "object" && typeof obj.data.id === "string") {
58776
+ return obj.data.id;
58777
+ }
58778
+ if (typeof obj.id === "string") {
58779
+ return obj.id;
58780
+ }
58781
+ if (Array.isArray(obj.content)) {
58782
+ for (const item of obj.content) {
58783
+ const id = extractStructuredResultId(item);
58784
+ if (id) return id;
58785
+ }
58786
+ }
58787
+ }
58788
+ return null;
58789
+ }
58790
+ function isErrorResult(event) {
58791
+ if (event.is_error === true || event.is_error === "true") return true;
58792
+ if (event.tool && typeof event.tool === "object") {
58793
+ if (typeof event.tool.exitCode === "number" && event.tool.exitCode !== 0) return true;
58794
+ if (typeof event.tool.status === "string" && event.tool.status === "error") return true;
58795
+ }
58796
+ const checkError = (val) => {
58797
+ if (!val) return false;
58798
+ if (Array.isArray(val)) {
58799
+ return val.some(checkError);
58800
+ }
58801
+ if (typeof val === "object") {
58802
+ const obj = val;
58803
+ if (obj.isError === true || obj.isError === "true" || obj.error !== void 0) return true;
58804
+ if (Array.isArray(obj.content) && obj.content.some(checkError)) return true;
58805
+ if (typeof obj.text === "string") {
58806
+ try {
58807
+ return checkError(JSON.parse(obj.text));
58808
+ } catch {
58809
+ }
58810
+ }
58811
+ } else if (typeof val === "string") {
58812
+ try {
58813
+ return checkError(JSON.parse(val));
58814
+ } catch {
58815
+ }
58816
+ }
58817
+ return false;
58818
+ };
58819
+ return checkError(event.content);
58820
+ }
58821
+ function turnReferences(events) {
58822
+ const mutatedIds = /* @__PURE__ */ new Set();
58823
+ const mutatedInboxIds = /* @__PURE__ */ new Set();
58824
+ const textIds = /* @__PURE__ */ new Set();
58825
+ let hasToolEvents = false;
58826
+ const keyedPending = /* @__PURE__ */ new Map();
58827
+ const unkeyedCalls = [];
58828
+ for (const event of events) {
58829
+ if (event.type === "tool_use") {
58830
+ hasToolEvents = true;
58831
+ const rawName = event.tool?.name ?? event.name ?? "";
58832
+ const toolName = rawName.replace(/^.*__/, "");
58833
+ const isMutation = MUTATION_TOOLS.has(toolName);
58834
+ const inputObj = event.tool?.input && typeof event.tool.input === "object" ? event.tool.input : event.input && typeof event.input === "object" ? event.input : {};
58835
+ const rawTargetId = typeof inputObj.taskId === "string" ? inputObj.taskId : typeof inputObj.pageId === "string" ? inputObj.pageId : typeof inputObj.id === "string" ? inputObj.id : null;
58836
+ let targetId = null;
58837
+ if (rawTargetId) {
58838
+ if ((toolName === "create_task" || toolName === "update_task") && /^GT-\d+$/.test(rawTargetId)) {
58839
+ targetId = rawTargetId;
58840
+ } else if ((toolName === "create_wiki_page" || toolName === "update_wiki_page") && /^WK-\d+$/.test(rawTargetId)) {
58841
+ targetId = rawTargetId;
58842
+ }
58843
+ }
58844
+ const callId = event.tool?.id ?? event.tool_use_id ?? event.id;
58845
+ if (callId && typeof callId === "string") {
58846
+ if (isMutation) {
58847
+ keyedPending.set(callId, { toolName, targetId });
58848
+ }
58849
+ } else {
58850
+ if (isMutation) {
58851
+ unkeyedCalls.push({ isMutation: true, toolName, targetId });
58852
+ } else {
58853
+ unkeyedCalls.push({ isMutation: false });
58854
+ }
58855
+ }
58856
+ } else if (event.type === "tool_result") {
58857
+ hasToolEvents = true;
58858
+ const callId = event.tool?.id ?? event.tool_use_id ?? event.id;
58859
+ let mutation;
58860
+ if (callId && typeof callId === "string") {
58861
+ mutation = keyedPending.get(callId);
58862
+ keyedPending.delete(callId);
58863
+ } else if (unkeyedCalls.length > 0) {
58864
+ const unkeyed = unkeyedCalls.shift();
58865
+ if (unkeyed && unkeyed.isMutation) {
58866
+ mutation = { toolName: unkeyed.toolName, targetId: unkeyed.targetId };
58867
+ }
58868
+ }
58869
+ if (mutation && !isErrorResult(event)) {
58870
+ const returnedId = extractStructuredResultId(event.content);
58871
+ if (mutation.toolName === "create_inbox_item") {
58872
+ if (returnedId && /^II-[0-9a-zA-Z-]+$/.test(returnedId)) {
58873
+ mutatedInboxIds.add(returnedId);
58874
+ }
58875
+ } else {
58876
+ if (mutation.targetId) {
58877
+ mutatedIds.add(mutation.targetId);
58878
+ }
58879
+ if (returnedId) {
58880
+ if ((mutation.toolName === "create_task" || mutation.toolName === "update_task") && /^GT-\d+$/.test(returnedId)) {
58881
+ mutatedIds.add(returnedId);
58882
+ } else if ((mutation.toolName === "create_wiki_page" || mutation.toolName === "update_wiki_page") && /^WK-\d+$/.test(returnedId)) {
58883
+ mutatedIds.add(returnedId);
58884
+ }
58885
+ }
58886
+ }
58887
+ }
58888
+ } else if (event.type === "assistant" && typeof event.content === "string") {
58889
+ for (const id of event.content.match(ID_RE) ?? []) textIds.add(id);
58890
+ }
58891
+ }
58892
+ return { mutatedIds, mutatedInboxIds, textIds, hasToolEvents };
58893
+ }
58894
+ function attributeReceipts(receipts, refs) {
58895
+ if (refs.hasToolEvents) {
58896
+ const board2 = receipts.filter((r) => r.kind !== "inbox" && refs.mutatedIds.has(r.id));
58897
+ const inbox = receipts.filter((r) => r.kind === "inbox" && refs.mutatedInboxIds.has(r.id));
58898
+ return [...board2, ...inbox];
58899
+ }
58900
+ const board = receipts.filter((r) => r.kind !== "inbox" && refs.textIds.has(r.id));
58901
+ return [...board];
58902
+ }
58903
+ function closurePresent(finalText, receipts) {
58904
+ const mentioned = new Set(finalText.match(ID_RE) ?? []);
58905
+ if (!receipts.every((receipt) => receipt.kind === "inbox" || mentioned.has(receipt.id))) return false;
58906
+ if (receipts.some((receipt) => receipt.kind === "inbox") && !/(?<![\p{L}\p{N}])(?:inbox|інбокс|вхідні)(?![\p{L}\p{N}])/iu.test(finalText)) return false;
58907
+ const nonIdText = finalText.replace(ID_RE, "");
58908
+ const letters = nonIdText.match(new RegExp("\\p{L}", "gu")) ?? [];
58909
+ if (letters.length < CLOSURE_MIN_PROSE_CHARS) return false;
58910
+ const words = nonIdText.match(new RegExp("\\p{L}{2,}", "gu")) ?? [];
58911
+ const uniqueWords = new Set(words.map((w) => w.toLowerCase()));
58912
+ if (uniqueWords.size < CLOSURE_MIN_PROSE_WORDS) return false;
58913
+ return true;
58914
+ }
58915
+ function describe3(receipt, lang = "en") {
58916
+ const title = receipt.title.trim();
58917
+ const noun = lang === "uk" ? receipt.kind === "task" ? "\u0437\u0430\u0434\u0430\u0447\u0430" : "\u0441\u043F\u0435\u0446\u0438\u0444\u0456\u043A\u0430\u0446\u0456\u044F" : receipt.kind === "task" ? "task" : "page";
58918
+ return title ? `${noun} ${receipt.id} "${title}"` : `${noun} ${receipt.id}`;
58919
+ }
58920
+ function renderClosure(receipts, lang = "en") {
58921
+ const board = receipts.filter((r) => r.kind !== "inbox");
58922
+ const created = board.filter((r) => r.action === "created");
58923
+ const updated = board.filter((r) => r.action === "updated");
58924
+ const inboxCount = receipts.filter((r) => r.kind === "inbox").length;
58925
+ const parts = [];
58926
+ if (lang === "uk") {
58927
+ if (created.length) parts.push(`\u041F\u0456\u0434 \u0447\u0430\u0441 \u0446\u044C\u043E\u0433\u043E \u0445\u043E\u0434\u0443 \u0432 \u043F\u0440\u043E\u0454\u043A\u0442\u0456 \u0434\u043E\u0434\u0430\u043D\u043E: ${created.map((r) => describe3(r, "uk")).join("; ")}.`);
58928
+ if (updated.length) parts.push(`\u041E\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${updated.map((r) => describe3(r, "uk")).join("; ")}.`);
58929
+ if (inboxCount) parts.push(`\u041D\u043E\u0432\u0456 \u0435\u043B\u0435\u043C\u0435\u043D\u0442\u0438 Inbox: ${inboxCount}.`);
58930
+ if (created.some((r) => r.kind === "task")) parts.push("\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043A\u0440\u043E\u043A: \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0430 \u0434\u043E\u0448\u0446\u0456 \u0437\u0430\u0434\u0430\u0447.");
58931
+ return `
58932
+
58933
+ **\u041F\u0456\u0434\u0441\u0443\u043C\u043E\u043A:** ${parts.join(" ")}`;
58934
+ }
58935
+ if (created.length) parts.push(`During this turn the project gained: ${created.map((r) => describe3(r, "en")).join("; ")}.`);
58936
+ if (updated.length) parts.push(`Updated: ${updated.map((r) => describe3(r, "en")).join("; ")}.`);
58937
+ if (inboxCount) parts.push(`New Inbox items: ${inboxCount}.`);
58938
+ if (created.some((r) => r.kind === "task")) parts.push("Next: the task is on the task board.");
58939
+ return `
58940
+
58941
+ **Summary:** ${parts.join(" ")}`;
58942
+ }
58943
+ function closureFor(input) {
58944
+ const receipts = attributeReceipts(diffSnapshots(input.before, input.after), turnReferences(input.events));
58945
+ if (receipts.length === 0) return null;
58946
+ if (closurePresent(finalAssistantText(input.events), receipts)) return null;
58947
+ const isUk = Boolean(input.operatorText && /[Ѐ-ӿ]/.test(input.operatorText));
58948
+ return renderClosure(receipts, isUk ? "uk" : "en");
58949
+ }
58950
+
58951
+ // src/clarification-ledger.ts
58952
+ var LEDGER_MAX_ITEMS = 20;
58953
+ var LEDGER_MAX_QUESTIONS_PER_TURN = 5;
58954
+ var LEDGER_MAX_EXTRACTED_QUESTIONS = 50;
58955
+ var LEDGER_REASK_JACCARD = 0.6;
58956
+ var LEDGER_MAX_QUESTION_CHARS = 300;
58957
+ function emptyLedger() {
58958
+ return { turn: 0, items: [] };
58959
+ }
58960
+ var NEGATION_RE = /\b(?:no\s+default|not\s+(?:\w+\s+)*(?:answered|assumed|resolved|decided|selected|chosen)|unanswered|unresolved|undecided|remains?\s+(?:\w+\s+)*(?:open|unanswered|unresolved|undecided)|still\s+open|without\s+(?:a\s+)?default|no\s+assumption|has\s+not\s+(?:been\s+)?(?:answered|assumed|resolved|decided|selected|chosen)|is\s+not\s+(?:answered|assumed|resolved|decided|selected|chosen)|was\s+not\s+(?:answered|assumed|resolved|decided|selected|chosen)|cannot\s+be\s+(?:answered|assumed|resolved|decided)|(?:default|assumed|assuming|assumption|decided|resolved|answered|choice|decision|option|selection)\s+(?:is\s+|was\s+|are\s+|were\s+|will\s+be\s+|to\s+|:\s*|\s*=\s*)?(?:not|never|no))\b|не\s+(?:\w+\s+)*(?:вирішено|відповіли|прийнято|обрано|визначено|узгоджено|припускаю)|без\s+(?:відповіді|замовчування)|залишається\s+відкритим|невирішено|(?:дефолт|за\s+замовчуванням|вирішено|відповіли|прийнято|припускаємо|припущення|вибір|рішення|варіант)\s*(?::\s*)?(?:не|ні|ніколи)/iu;
58961
+ var DEFERRED_DECISION_RE = /\b(?:will\s+(?:be\s+)?(?:decided|determined|resolved|chosen|selected|answered|settled|clarified|discussed|picked)|to\s+be\s+(?:decided|determined|resolved|chosen|selected|answered|settled|clarified|discussed|picked)|pending\s+(?:decision|resolution|clarification|answer|choice)|tbd|to\s+be\s+determined|deferred|postponed|will\s+(?:decide|determine|choose|resolve|answer)|decide\s+later|determine\s+later|deciding\s+later|at\s+a\s+later\s+time|tomorrow|next\s+(?:sprint|week|turn)|depends\s+on|contingent\s+on|conditional\s+on|subject\s+to|if\s+and\s+when)\b|буде\s+(?:вирішено|визначено|обрано|узгоджено|встановлено|з'ясовано)|вирішимо\s+пізніше|визначимо\s+пізніше|відкладено|залежить\s+від|потребує\s+уточнення|ще\s+не\s+(?:вирішено|визначено|обрано)/iu;
58962
+ var RESOLUTION_MARKER_RE = /\b(?:answered|assumed|assuming|assumption|blocker|default|resolved|decided)\b|відпові|прийнят|приймаю|беру|за замовчуванням|блокер|припуска|вирішено|узгоджено/iu;
58963
+ var LEDGER_STOP_WORDS = /* @__PURE__ */ new Set([
58964
+ "which",
58965
+ "what",
58966
+ "where",
58967
+ "when",
58968
+ "who",
58969
+ "whom",
58970
+ "how",
58971
+ "why",
58972
+ "should",
58973
+ "would",
58974
+ "could",
58975
+ "will",
58976
+ "can",
58977
+ "may",
58978
+ "might",
58979
+ "must",
58980
+ "need",
58981
+ "needed",
58982
+ "use",
58983
+ "used",
58984
+ "have",
58985
+ "been",
58986
+ "there",
58987
+ "this",
58988
+ "that",
58989
+ "question",
58990
+ "questions",
58991
+ "item",
58992
+ "items",
58993
+ "task",
58994
+ "tasks",
58995
+ "about",
58996
+ "\u044F\u043A\u0456",
58997
+ "\u044F\u043A\u0438\u0439",
58998
+ "\u044F\u043A\u0430",
58999
+ "\u044F\u043A\u0435",
59000
+ "\u044F\u043A\u0438\u0445",
59001
+ "\u044F\u043A\u043E\u043C\u0443",
59002
+ "\u044F\u043A\u0456\u0439",
59003
+ "\u0445\u0442\u043E",
59004
+ "\u043A\u043E\u0433\u043E",
59005
+ "\u043A\u043E\u043C\u0443",
59006
+ "\u043A\u0438\u043C",
59007
+ "\u0449\u043E",
59008
+ "\u0447\u043E\u0433\u043E",
59009
+ "\u0447\u043E\u043C\u0443",
59010
+ "\u0447\u0438\u043C",
59011
+ "\u0434\u0435",
59012
+ "\u043A\u0443\u0434\u0438",
59013
+ "\u0437\u0432\u0456\u0434\u043A\u0438",
59014
+ "\u043A\u043E\u043B\u0438",
59015
+ "\u0447\u043E\u043C\u0443",
59016
+ "\u043D\u0430\u0432\u0456\u0449\u043E",
59017
+ "\u044F\u043A",
59018
+ "\u0447\u0438",
59019
+ "\u0430\u0431\u043E",
59020
+ "\u0434\u043B\u044F",
59021
+ "\u043F\u0440\u043E",
59022
+ "\u043F\u0440\u0438",
59023
+ "\u043D\u0430\u0434",
59024
+ "\u043F\u0456\u0434",
59025
+ "\u0432\u0456\u0434",
59026
+ "\u0434\u043E",
59027
+ "\u043F\u0438\u0442\u0430\u043D\u043D\u044F",
59028
+ "\u0437\u0430\u0434\u0430\u0447\u0430",
59029
+ "\u0437\u0430\u0434\u0430\u0447\u0456",
59030
+ "\u043F\u043E\u0442\u0440\u0456\u0431\u043D\u043E",
59031
+ "\u043F\u043E\u0442\u0440\u0456\u0431\u043D\u0456",
59032
+ "\u0442\u0440\u0435\u0431\u0430"
59033
+ ]);
59034
+ function tokens(text) {
59035
+ return new Set(
59036
+ text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 3)
59037
+ );
59038
+ }
59039
+ function tokenMatches(a, b) {
59040
+ if (a === b) return true;
59041
+ if (a.length >= 4 && b.length >= 4 && (a.startsWith(b.slice(0, 4)) || b.startsWith(a.slice(0, 4)))) {
59042
+ return true;
59043
+ }
59044
+ return false;
59045
+ }
59046
+ function isSameQuestion(a, b) {
59047
+ const ta = tokens(a);
59048
+ const tb = tokens(b);
59049
+ if (ta.size === 0 || tb.size === 0) return false;
59050
+ let shared = 0;
59051
+ for (const token of ta) if (tb.has(token)) shared += 1;
59052
+ return shared / (ta.size + tb.size - shared) >= LEDGER_REASK_JACCARD;
59053
+ }
59054
+ function isAffirmativeResolutionForQuestion(question, text, otherQuestions) {
59055
+ if (!text || !RESOLUTION_MARKER_RE.test(text)) return false;
59056
+ if (NEGATION_RE.test(text)) return false;
59057
+ if (DEFERRED_DECISION_RE.test(text)) return false;
59058
+ const substantiveQuestionToks = new Set(
59059
+ Array.from(tokens(question)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3)
59060
+ );
59061
+ if (otherQuestions && otherQuestions.length > 0) {
59062
+ const textToks2 = tokens(text);
59063
+ for (const otherQ of otherQuestions) {
59064
+ if (otherQ === question || isSameQuestion(otherQ, question)) continue;
59065
+ const otherToks = new Set(
59066
+ Array.from(tokens(otherQ)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3)
59067
+ );
59068
+ let hasSharedDomain = false;
59069
+ for (const t of substantiveQuestionToks) {
59070
+ for (const ot of otherToks) {
59071
+ if (tokenMatches(t, ot)) {
59072
+ hasSharedDomain = true;
59073
+ break;
59074
+ }
59075
+ }
59076
+ if (hasSharedDomain) break;
59077
+ }
59078
+ if (hasSharedDomain) {
59079
+ const otherDistinguishingToks = [];
59080
+ for (const ot of otherToks) {
59081
+ let inThis = false;
59082
+ for (const t of substantiveQuestionToks) {
59083
+ if (tokenMatches(t, ot)) {
59084
+ inThis = true;
59085
+ break;
59086
+ }
59087
+ }
59088
+ if (!inThis) otherDistinguishingToks.push(ot);
59089
+ }
59090
+ const thisDistinguishingToks = [];
59091
+ for (const t of substantiveQuestionToks) {
59092
+ let inOther = false;
59093
+ for (const ot of otherToks) {
59094
+ if (tokenMatches(t, ot)) {
59095
+ inOther = true;
59096
+ break;
59097
+ }
59098
+ }
59099
+ if (!inOther) thisDistinguishingToks.push(t);
59100
+ }
59101
+ if (otherDistinguishingToks.length > 0 && thisDistinguishingToks.length > 0) {
59102
+ let textMatchesOtherDistinguishing = false;
59103
+ for (const ot of otherDistinguishingToks) {
59104
+ for (const lt of textToks2) {
59105
+ if (tokenMatches(ot, lt)) {
59106
+ textMatchesOtherDistinguishing = true;
59107
+ break;
59108
+ }
59109
+ }
59110
+ if (textMatchesOtherDistinguishing) break;
59111
+ }
59112
+ let textMatchesThisDistinguishing = false;
59113
+ for (const t of thisDistinguishingToks) {
59114
+ for (const lt of textToks2) {
59115
+ if (tokenMatches(t, lt)) {
59116
+ textMatchesThisDistinguishing = true;
59117
+ break;
59118
+ }
59119
+ }
59120
+ if (textMatchesThisDistinguishing) break;
59121
+ }
59122
+ if (textMatchesOtherDistinguishing && !textMatchesThisDistinguishing) {
59123
+ return false;
59124
+ }
59125
+ }
59126
+ }
59127
+ }
59128
+ }
59129
+ if (!isTopicConsistentWithQuestion(question, text)) {
59130
+ return false;
59131
+ }
59132
+ const textToks = tokens(text);
59133
+ let shared = 0;
59134
+ for (const it of substantiveQuestionToks) {
59135
+ for (const lt of textToks) {
59136
+ if (tokenMatches(it, lt)) {
59137
+ shared += 1;
59138
+ break;
59139
+ }
59140
+ }
59141
+ }
59142
+ return shared >= 1 && (shared >= 2 || substantiveQuestionToks.size <= 2 || shared / substantiveQuestionToks.size >= 0.25);
59143
+ }
59144
+ function isTopicConsistentWithQuestion(question, text) {
59145
+ const substantiveQuestionToks = Array.from(tokens(question)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59146
+ const genericQualifiers = /* @__PURE__ */ new Set([
59147
+ "the",
59148
+ "a",
59149
+ "an",
59150
+ "this",
59151
+ "that",
59152
+ "our",
59153
+ "its",
59154
+ "their",
59155
+ "your",
59156
+ "\u0446\u0435\u0439",
59157
+ "\u0446\u044F",
59158
+ "\u0446\u0435",
59159
+ "\u043D\u0430\u0448",
59160
+ "\u043D\u0430\u0448\u0430",
59161
+ "\u0439\u043E\u0433\u043E",
59162
+ "\u0457\u0457",
59163
+ "is",
59164
+ "as",
59165
+ "by",
59166
+ "take",
59167
+ "taking",
59168
+ "taken",
59169
+ "set",
59170
+ "use",
59171
+ "default",
59172
+ "assumed",
59173
+ "assuming",
59174
+ "decision",
59175
+ "outcome",
59176
+ "choice",
59177
+ "\u0434\u0435\u0444\u043E\u043B\u0442",
59178
+ "\u0437\u0430\u043C\u043E\u0432\u0447\u0443\u0432\u0430\u043D\u043D\u044F",
59179
+ "\u0432\u0438\u0431\u0456\u0440",
59180
+ "\u0431\u0435\u0440\u0443",
59181
+ "\u0432\u0432\u0430\u0436\u0430\u044E",
59182
+ "\u043F\u0440\u0438\u0439\u043D\u044F\u0442\u043E",
59183
+ "\u043E\u0431\u0440\u0430\u043D\u043E"
59184
+ ]);
59185
+ const beforeTopicMatch = text.match(/([a-zA-Z\u0400-\u04FF_-]+)\s+(?:default|дефолт|за\s+замовчуванням|is|are|was|were|will\s+be|є|це|буде|будуть|обрано|було|=|:|—|-)\b/i);
59186
+ if (beforeTopicMatch && beforeTopicMatch[1]) {
59187
+ const qualifier = beforeTopicMatch[1].toLowerCase();
59188
+ if (!genericQualifiers.has(qualifier)) {
59189
+ let qualifierMatches = false;
59190
+ for (const it of substantiveQuestionToks) {
59191
+ if (tokenMatches(it, qualifier)) {
59192
+ qualifierMatches = true;
59193
+ break;
59194
+ }
59195
+ }
59196
+ if (!qualifierMatches) {
59197
+ return false;
59198
+ }
59199
+ }
59200
+ }
59201
+ const resolutionKeywords = /* @__PURE__ */ new Set([
59202
+ "answered",
59203
+ "assumed",
59204
+ "assuming",
59205
+ "default",
59206
+ "blocker",
59207
+ "decision",
59208
+ "outcome",
59209
+ "choice",
59210
+ "\u0432\u0456\u0434\u043F\u043E\u0432\u0456",
59211
+ "\u043F\u0440\u0438\u0439\u043D\u044F\u0442",
59212
+ "\u043F\u0440\u0438\u0439\u043C\u0430\u044E",
59213
+ "\u0431\u0435\u0440\u0443",
59214
+ "\u0437\u0430\u043C\u043E\u0432\u0447\u0443\u0432\u0430\u043D\u043D\u044F\u043C",
59215
+ "\u0431\u043B\u043E\u043A\u0435\u0440",
59216
+ "\u043F\u0440\u0438\u043F\u0443\u0441\u043A\u0430",
59217
+ "\u0432\u0438\u0440\u0456\u0448\u0435\u043D\u043E",
59218
+ "\u0443\u0437\u0433\u043E\u0434\u0436\u0435\u043D\u043E"
59219
+ ]);
59220
+ const textToks = Array.from(tokens(text)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59221
+ const contentToks = textToks.filter((t) => !resolutionKeywords.has(t) && !genericQualifiers.has(t));
59222
+ for (const ct of contentToks) {
59223
+ const topicMatch = new RegExp(`\\b${ct}\\s+(?:default|\u0434\u0435\u0444\u043E\u043B\u0442|is|are|was|were|will\\s+be|\u0454|\u0446\u0435|\u0431\u0443\u0434\u0435|\u0431\u0443\u0434\u0443\u0442\u044C|=|:|\u2014|-)\\b`, "i").test(text);
59224
+ if (topicMatch) {
59225
+ let matchesQ = false;
59226
+ for (const qt of substantiveQuestionToks) {
59227
+ if (tokenMatches(qt, ct)) {
59228
+ matchesQ = true;
59229
+ break;
59230
+ }
59231
+ }
59232
+ if (!matchesQ) {
59233
+ return false;
59234
+ }
59235
+ }
59236
+ }
59237
+ return true;
59238
+ }
59239
+ function isDirectInlineResolutionForQuestion(question, afterText) {
59240
+ if (!afterText) return false;
59241
+ const isDirect = /^\s*(?:[-—–:]|\()\s*(?:answered|assumed|assuming|default|blocker|відпові|прийнят|приймаю|беру|за\s+замовчуванням|блокер|припуска|вирішено|узгоджено)/i.test(afterText);
59242
+ if (!isDirect) return false;
59243
+ if (NEGATION_RE.test(afterText)) return false;
59244
+ if (DEFERRED_DECISION_RE.test(afterText)) return false;
59245
+ return isTopicConsistentWithQuestion(question, afterText);
59246
+ }
59247
+ function extractQuestions(text) {
59248
+ const withoutCode = text.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
59249
+ const found = [];
59250
+ for (const rawLine of withoutCode.split(/\n+/)) {
59251
+ const line = rawLine.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").replace(/(\*\*|__|[*_~"«»“”])/g, "").trim();
59252
+ if (!line) continue;
59253
+ for (const match of line.matchAll(/[^.!?]*\?(?=\s|$)/g)) {
59254
+ const sentence = match[0].trim();
59255
+ const after = line.slice((match.index ?? 0) + match[0].length).split("?")[0] ?? "";
59256
+ const hasInlineMarker = /^\s*(?:[-—–:]|\()\s*(?:answered|assumed|assuming|default|blocker|відпові|прийнят|приймаю|беру|за\s+замовчуванням|блокер|припуска|вирішено|узгоджено)/i.test(after);
59257
+ if (hasInlineMarker) {
59258
+ if (isDirectInlineResolutionForQuestion(sentence, after)) continue;
59259
+ } else {
59260
+ if (isAffirmativeResolutionForQuestion(sentence, after)) continue;
59261
+ }
59262
+ if (/https?:\/\/\S*$/.test(sentence)) continue;
59263
+ if (sentence.replace(/[^\p{L}]/gu, "").length < 6) continue;
59264
+ const question = sentence.slice(0, LEDGER_MAX_QUESTION_CHARS);
59265
+ if (found.some((seen) => isSameQuestion(seen, question))) continue;
59266
+ found.push(question);
59267
+ if (found.length >= LEDGER_MAX_EXTRACTED_QUESTIONS) return found;
59268
+ }
59269
+ }
59270
+ return found;
59271
+ }
59272
+ function presentLedger(ledger) {
59273
+ const turn = ledger.turn + 1;
59274
+ const candidates = ledger.items.map((item, index) => ({ item, index })).filter(({ item }) => item.status === "open" || item.status === "reasked" || item.status === "addressed");
59275
+ const shownIndexes = new Set(candidates.slice(-LEDGER_MAX_ITEMS).map(({ index }) => index));
59276
+ const items = ledger.items.map((item, index) => shownIndexes.has(index) ? { ...item, presentedTurn: turn } : item);
59277
+ const shown = items.filter((_, index) => shownIndexes.has(index)).map(({ question, status, askedTurn }) => ({ question, status, askedTurn }));
59278
+ return { ledger: { turn, items }, shown };
59279
+ }
59280
+ function isQuestionExplicitlyResolved(item, text, otherOpenQuestions) {
59281
+ if (!text || !RESOLUTION_MARKER_RE.test(text)) return false;
59282
+ const withoutCode = text.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
59283
+ for (const rawLine of withoutCode.split(/\n+/)) {
59284
+ const line = rawLine.trim();
59285
+ if (!line) continue;
59286
+ if (line.includes(item.question)) {
59287
+ const qIndex = line.indexOf(item.question);
59288
+ const after = line.slice(qIndex + item.question.length).trim();
59289
+ const isDirectAttachment = /^[:—–\->]\s*|[^\w\s]\s*(?:assumed|default|дефолт|outcome|decision|обрано|прийнято)\b/i.test(after);
59290
+ if (isDirectAttachment && isDirectInlineResolutionForQuestion(item.question, after)) {
59291
+ const hasOtherTokens = otherOpenQuestions?.some((oq) => {
59292
+ if (oq === item.question || isSameQuestion(oq, item.question)) return false;
59293
+ const otherToks = Array.from(tokens(oq)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59294
+ const thisToks = Array.from(tokens(item.question)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59295
+ const otherUnique = otherToks.filter((ot) => !thisToks.some((tt) => tokenMatches(ot, tt)));
59296
+ const afterToks = tokens(after);
59297
+ return otherUnique.length > 0 && otherUnique.some((ot) => Array.from(afterToks).some((at) => tokenMatches(ot, at)));
59298
+ });
59299
+ if (!hasOtherTokens) {
59300
+ return true;
59301
+ }
59302
+ }
59303
+ }
59304
+ }
59305
+ const segments = withoutCode.split(/\n+|(?<=[.!?])\s+/).map((l) => l.trim()).filter(Boolean);
59306
+ for (const segment of segments) {
59307
+ if (!RESOLUTION_MARKER_RE.test(segment)) continue;
59308
+ if (NEGATION_RE.test(segment)) continue;
59309
+ if (DEFERRED_DECISION_RE.test(segment)) continue;
59310
+ if (segment.includes(item.question) || isSameQuestion(segment, item.question)) {
59311
+ const hasOtherTokens = otherOpenQuestions?.some((oq) => {
59312
+ if (oq === item.question || isSameQuestion(oq, item.question)) return false;
59313
+ const otherToks = Array.from(tokens(oq)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59314
+ const thisToks = Array.from(tokens(item.question)).filter((t) => !LEDGER_STOP_WORDS.has(t) && t.length >= 3);
59315
+ const otherUnique = otherToks.filter((ot) => !thisToks.some((tt) => tokenMatches(ot, tt)));
59316
+ const segToks = tokens(segment);
59317
+ return otherUnique.length > 0 && otherUnique.some((ot) => Array.from(segToks).some((st) => tokenMatches(ot, st)));
59318
+ });
59319
+ if (!hasOtherTokens) {
59320
+ return true;
59321
+ }
59322
+ }
59323
+ if (isAffirmativeResolutionForQuestion(item.question, segment, otherOpenQuestions)) {
59324
+ return true;
59325
+ }
59326
+ }
59327
+ return false;
59328
+ }
59329
+ function recordTurn(ledger, finalText) {
59330
+ const turn = ledger.turn;
59331
+ const newQuestions = extractQuestions(finalText);
59332
+ const unmatched = [...newQuestions];
59333
+ const reasked = [];
59334
+ const openQuestions = ledger.items.filter((it) => it.status === "open" || it.status === "reasked").map((it) => it.question);
59335
+ const items = ledger.items.map((item) => {
59336
+ if (item.status === "archived") return item;
59337
+ const matchIndex = unmatched.findIndex((question) => isSameQuestion(question, item.question));
59338
+ if (matchIndex >= 0) {
59339
+ unmatched.splice(matchIndex, 1);
59340
+ if (item.askedTurn === turn) return item;
59341
+ const again = { ...item, status: "reasked", reasked: item.reasked + 1 };
59342
+ delete again.addressedTurn;
59343
+ reasked.push(again);
59344
+ return again;
59345
+ }
59346
+ if (item.status === "addressed") {
59347
+ return item.addressedTurn !== void 0 && item.addressedTurn < turn ? { ...item, status: "archived" } : item;
59348
+ }
59349
+ const otherQuestions = openQuestions.filter((q) => q !== item.question);
59350
+ if (isQuestionExplicitlyResolved(item, finalText, otherQuestions)) {
59351
+ return { ...item, status: "addressed", addressedTurn: turn };
59352
+ }
59353
+ return item;
59354
+ });
59355
+ const overflow = [];
59356
+ let unresolved = items.filter((item) => item.status === "open" || item.status === "reasked").length;
59357
+ let recordedNew = 0;
59358
+ for (const question of unmatched) {
59359
+ if (recordedNew >= LEDGER_MAX_QUESTIONS_PER_TURN) {
59360
+ overflow.push(question);
59361
+ continue;
59362
+ }
59363
+ if (unresolved >= LEDGER_MAX_ITEMS) {
59364
+ overflow.push(question);
59365
+ continue;
59366
+ }
59367
+ items.push({ question, askedTurn: turn, status: "open", reasked: 0 });
59368
+ unresolved += 1;
59369
+ recordedNew += 1;
59370
+ }
59371
+ return { ...capLedger({ turn, items }), reasked, overflow };
59372
+ }
59373
+ var DROP_ORDER = ["archived", "addressed"];
59374
+ function capLedger(ledger) {
59375
+ const items = [...ledger.items];
59376
+ const dropped = [];
59377
+ for (const status of DROP_ORDER) {
59378
+ while (items.length > LEDGER_MAX_ITEMS) {
59379
+ const index = items.findIndex((item) => item.status === status);
59380
+ if (index < 0) break;
59381
+ dropped.push(...items.splice(index, 1));
59382
+ }
59383
+ }
59384
+ return { ledger: { turn: ledger.turn, items }, dropped };
59385
+ }
59386
+ function parseLedger(value) {
59387
+ if (!value || typeof value !== "object") return void 0;
59388
+ const raw = value;
59389
+ if (typeof raw.turn !== "number" || !Array.isArray(raw.items)) return void 0;
59390
+ if (raw.items.length > LEDGER_MAX_ITEMS) return void 0;
59391
+ const statuses = /* @__PURE__ */ new Set(["open", "reasked", "addressed", "archived"]);
59392
+ const items = [];
59393
+ for (const entry of raw.items) {
59394
+ if (!entry || typeof entry !== "object") continue;
59395
+ const item = entry;
59396
+ if (typeof item.question !== "string" || typeof item.askedTurn !== "number") continue;
59397
+ if (typeof item.status !== "string" || !statuses.has(item.status)) continue;
59398
+ items.push({
59399
+ question: item.question.slice(0, LEDGER_MAX_QUESTION_CHARS),
59400
+ askedTurn: item.askedTurn,
59401
+ status: item.status,
59402
+ reasked: typeof item.reasked === "number" ? item.reasked : 0,
59403
+ ...typeof item.presentedTurn === "number" ? { presentedTurn: item.presentedTurn } : {},
59404
+ ...typeof item.addressedTurn === "number" ? { addressedTurn: item.addressedTurn } : {}
59405
+ });
59406
+ }
59407
+ return { turn: raw.turn, items };
59408
+ }
58081
59409
 
58082
59410
  // src/api-base-url-migrate.ts
58083
59411
  var import_node_fs13 = __toESM(require("node:fs"), 1);
@@ -61546,6 +62874,22 @@ async function driveRun(deps, projectSlug, runId) {
61546
62874
  }
61547
62875
  }
61548
62876
  }
62877
+ const needsBrowser = claimed.prerequisites?.some((p) => p.mcp === "mrrlin-browser") || task.domains?.includes("browser");
62878
+ if (needsBrowser) {
62879
+ const checkBusy = deps.checkBrowserBusy ?? checkBrowserBusy;
62880
+ const busy = checkBusy();
62881
+ if (busy.busy) {
62882
+ await failRun(
62883
+ deps,
62884
+ projectSlug,
62885
+ task,
62886
+ runId,
62887
+ leaseId,
62888
+ `browser_busy: run ${runId} needs the browser, but it is currently in use by another task (holder pid: ${busy.holderPid}). Please retry once it finishes.`
62889
+ );
62890
+ return;
62891
+ }
62892
+ }
61549
62893
  const blockerTasks = task.blockedBy.length ? await Promise.all(task.blockedBy.map((blockerTaskId) => deps.client.getTask(projectSlug, blockerTaskId))) : [];
61550
62894
  const missingBlockers = task.blockedBy.filter((_, index) => !blockerTasks[index]);
61551
62895
  if (missingBlockers.length > 0) {
@@ -61724,12 +63068,20 @@ async function driveRun(deps, projectSlug, runId) {
61724
63068
  turnCount: 0,
61725
63069
  usage: null
61726
63070
  };
61727
- const recordTurn = (outcome) => {
63071
+ const recordTurn2 = (outcome) => {
61728
63072
  runTelemetry.promptBytes += outcome.telemetry.promptBytes;
61729
63073
  runTelemetry.turnCount += 1;
61730
63074
  runTelemetry.usage = addTurnTokenUsage(runTelemetry.usage, outcome.telemetry.usage);
61731
63075
  return outcome;
61732
63076
  };
63077
+ const isContextBudgetExhausted = (usage) => {
63078
+ if (!usage) return false;
63079
+ const contextTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
63080
+ if (contextTokens <= 0) return false;
63081
+ const maxTokens = 2e5;
63082
+ const remainingTokens = maxTokens - contextTokens;
63083
+ return remainingTokens / maxTokens < 0.03 || remainingTokens < 4e3;
63084
+ };
61733
63085
  const effortDecision = deriveEffort({
61734
63086
  impact: task.impact,
61735
63087
  type: task.type,
@@ -61952,7 +63304,7 @@ ${renderTaskStateSection(taskState)}`;
61952
63304
  );
61953
63305
  return;
61954
63306
  }
61955
- const primed = recordTurn(
63307
+ const primed = recordTurn2(
61956
63308
  await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming", effortDecision?.level)
61957
63309
  );
61958
63310
  if (!primed.ok) {
@@ -62016,7 +63368,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62016
63368
  );
62017
63369
  return;
62018
63370
  }
62019
- const outcome = recordTurn(
63371
+ const outcome = recordTurn2(
62020
63372
  await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work", effortDecision?.level)
62021
63373
  );
62022
63374
  if (!outcome.ok) {
@@ -62024,6 +63376,26 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62024
63376
  await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
62025
63377
  return;
62026
63378
  }
63379
+ if (isContextBudgetExhausted(outcome.telemetry.usage)) {
63380
+ console.warn(
63381
+ JSON.stringify({
63382
+ event: "context_budget_gate_triggered",
63383
+ projectSlug,
63384
+ runId,
63385
+ taskId: task.id,
63386
+ usage: outcome.telemetry.usage
63387
+ })
63388
+ );
63389
+ await failRun(
63390
+ deps,
63391
+ projectSlug,
63392
+ task,
63393
+ runId,
63394
+ leaseId,
63395
+ `context_budget_exhausted: context window is near capacity (<3% remaining). Execution run halted safely to preserve progress and prevent lost output.`
63396
+ );
63397
+ return;
63398
+ }
62027
63399
  const prReadyStatuses = useExecutor ? CODE_PR_READY_TASK_STATUSES : COMPLETED_TASK_STATUSES;
62028
63400
  let readiness = await assertRunCompletionReady(deps, projectSlug, task.id, runId, prReadyStatuses);
62029
63401
  if (!readiness.ok) {
@@ -62042,7 +63414,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62042
63414
  })
62043
63415
  );
62044
63416
  const completionPrompt = buildCompletionTurnPrompt(task.id, readiness.task.status);
62045
- const completionOutcome = recordTurn(
63417
+ const completionOutcome = recordTurn2(
62046
63418
  await driveTurn(
62047
63419
  deps,
62048
63420
  codex,
@@ -63207,12 +64579,21 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
63207
64579
  } finally {
63208
64580
  clearInterval(touchTimer);
63209
64581
  }
64582
+ if (outcome.kind === "superseded") {
64583
+ await deps.client.terminalizeAsyncJob(projectSlug, jobId, {
64584
+ leaseId,
64585
+ status: "superseded",
64586
+ error: outcome.reason ?? outcome.error ?? "Superseded"
64587
+ }).catch(() => {
64588
+ });
64589
+ return;
64590
+ }
63210
64591
  const terminalStatus = outcome.kind === "done" ? "done" : "failed";
63211
64592
  await deps.client.terminalizeAsyncJob(projectSlug, jobId, {
63212
64593
  leaseId,
63213
64594
  status: terminalStatus,
63214
64595
  resultJson: outcome.kind === "done" ? outcome.resultJson : null,
63215
- finalReviewedHash: outcome.kind === "done" ? outcome.finalReviewedHash : null,
64596
+ finalReviewedHash: outcome.kind === "done" ? outcome.finalReviewedHash : outcome.kind === "failed" ? outcome.finalReviewedHash ?? null : null,
63216
64597
  error: outcome.kind === "done" ? null : outcome.error,
63217
64598
  failureReason: outcome.kind === "done" ? null : outcome.failureReason
63218
64599
  }).catch(() => {
@@ -63599,7 +64980,9 @@ function loadThreadIndex(stateDir, ttlMs, now) {
63599
64980
  lastUsedAt,
63600
64981
  model: typeof entry.model === "string" ? entry.model : null,
63601
64982
  reasoningEffort: typeof entry.reasoningEffort === "string" ? entry.reasoningEffort : null,
63602
- provider: typeof entry.provider === "string" ? entry.provider : null
64983
+ provider: typeof entry.provider === "string" ? entry.provider : null,
64984
+ systemInstructionHash: typeof entry.systemInstructionHash === "string" ? entry.systemInstructionHash : null,
64985
+ ...parseLedger(entry.clarificationLedger) ? { clarificationLedger: parseLedger(entry.clarificationLedger) } : {}
63603
64986
  });
63604
64987
  }
63605
64988
  return result;
@@ -68399,6 +69782,25 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68399
69782
  "You are the Mrrlin Director assistant.",
68400
69783
  "You can call MCP tools to read and write project state.",
68401
69784
  "Prefer calling tools over guessing. Be concise.",
69785
+ // O-DIRECTOR-HARD-GATES: a compact behaviour contract replaced ~30 accumulated mandates, several of
69786
+ // which were fitted to single benchmark personas. Closure and localization are enforced in code
69787
+ // (director-closure.ts, linkifyMrrlinIds.ts); the clarification ledger arrives in the turn context.
69788
+ "Behaviour contract (three rules):",
69789
+ '- DEFAULT-FIRST: never open an open-ended question round. When the intent-confirmation contract below says you must ask before creating a task, ask only about what would change the architecture or the deliverable \u2014 never about anything the operator already stated in this conversation \u2014 and once the operator has replied, take a reasonable default for whatever is still open and say so plainly ("unless you object, I take X") instead of asking again. Never ask permission to create tasks or specs \u2014 creating them is your job.',
69790
+ "- USER-FACING CLOSURE: end every turn with plain text in the operator's language. If the turn created or changed anything, name each item (ID with a one-line scope), where the result goes, and what happens next; if it changed nothing, say plainly what you need from the operator or what happens next. Never invent an ID or a write to have something to report. A UI card or a bare list of IDs never replaces that text.",
69791
+ "- QUESTION LIFECYCLE: every question you asked must end in exactly one state \u2014 answered by the operator, assumed (you state the default you took), or blocker (you state what it blocks). The open-question list in the turn context is authoritative: resolve each item explicitly and never drop one silently.",
69792
+ "Core guards:",
69793
+ "- Never claim a deliverable, file, test result, or running execution exists unless a tool actually produced it. Never claim autopilot is running unless an execution run is active; for a human_review task say it waits in To Do for your approval.",
69794
+ "- Mention a GT- or WK- id only if a tool returned it in this conversation (server-authoritative receipts).",
69795
+ "- Reply in the operator's language for the whole turn (questions, task scopes, summaries).",
69796
+ "- Keep chat short and human. No code blocks, DDL, terminal output, local filesystem paths, internal job/inbox UUIDs or alert markers in chat \u2014 put long content into a wiki page and link it.",
69797
+ "- Spec pages in folder Specs must contain `## Acceptance criteria` followed by `- [ ]` items.",
69798
+ "- AGNOSTIC DELIVERY MANDATE: Mrrlin is an Agnostic Delivery Platform decoupled from mandatory GitHub. Delivery is routed strictly by artifact kind: (1) document -> wiki (specs, policies, legal compliance playbooks, project documentation); (2) page -> publish (web pages, landing pages, interactive HTML previews); (3) file_set -> artifacts (file uploads, bundles, build zips); (4) message -> send (outbound webhooks, Slack, notifications); (5) code_change -> repo_host (git / GitHub ONLY when modifying source repository code). NEVER stall or refuse non-code tasks (such as marketing landing pages, GDPR checklists, or legal compliance playbooks) by demanding a Git repository. Non-code tasks deliver directly via page (Publish) or document (Wiki).",
69799
+ "- 3-TIER COMMERCIAL SCOPE GOVERNANCE: Treat tasks by domain governance tier. (1) Tier 1 Core Tech Delivery (backend, frontend, SRE, databases): decompose autonomously into repository code changes (when a repo is bound) or code artifacts. (2) Tier 2 Planning & Content (landing pages, SEO, marketing): create actionable drafts; when details such as a phone number, brand name or copy are missing, do not loop on questions \u2014 create the task, record `## Placeholders: [NEED_INPUT: <item>]` in the Wiki spec, and set `autonomyLevel: 'human_review'`. (3) Tier 3 Guardrailed Advisory (high risk: privacy/GDPR, security/SOC 2, billing, production database migrations, infrastructure): always set `autonomyLevel: 'human_review'`, put the domain's standard safety controls into the spec, and state that irreversible actions need human sign-off.",
69800
+ "- INBOX QUESTION RETIREMENT MANDATE: If clarifying questions were banked or exist as pending items in Inbox from a previous turn (e.g. via `create_inbox_item(kind: 'question')`), and the operator has now answered them in chat in Turn 2+, you MUST call `decide_inbox_item(id, verdict: 'acknowledged', note: ...)` (or `verdict: 'rejected'` if moot) to retire the pending questions BEFORE creating tasks or plans on the board. Never leave questions you received answers to lingering as unanswered items in Inbox.",
69801
+ "- CHAT CLARIFICATIONS ARE IN CHAT ONLY: During synchronous interactive Director chat, ask clarifying questions directly in your chat response. DO NOT call `create_inbox_item(kind: 'question')` for routine conversational clarifications in Turn 1 \u2014 Inbox questions are reserved for autonomous background workers or asynchronous gate pauses that occur while the operator is away.",
69802
+ "- AUTOPILOT PRESENCE FENCE TRANSPARENCY: Autopilot requires operator web app presence to trigger execution ticks (`operator_absent`). When the operator asks about autopilot starting, remind them that keeping the web tab open allows the scheduler to advance the queue automatically.",
69803
+ "",
68402
69804
  "Autonomy level is NOT a reason to skip clarification. `auto` means only that no human gate is",
68403
69805
  "required before the result LANDS (merge/deploy); it does not authorize building on unvalidated",
68404
69806
  "assumptions. Judge clarification on requirement clarity, independently of autonomy_level.",
@@ -68492,17 +69894,22 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68492
69894
  "turn with one sentence restating your understanding, then at most 3 scope-level questions, and",
68493
69895
  "do NOT create the task yet. Create it after the operator replies.",
68494
69896
  "",
68495
- "Once the operator replies and clarifies or confirms the scope (or if scope was already clear),",
68496
- "you MUST materialize the work onto the board: create the plan (if needed) and call `create_task`",
68497
- "in the same turn so actionable task cards appear on `/tasks` immediately. Never leave the board",
68498
- "empty with 0 tasks after agreeing on scope. Never claim tasks are created, queued, or ready unless",
68499
- "`create_task` was actually invoked and returned the task ID.",
68500
- "",
68501
69897
  "Set operatorSeedRequest to the operator's OWN WORDS, verbatim. Select every message in this",
68502
69898
  "conversation that forms part of the request \u2014 not only the last one \u2014 and concatenate them in",
68503
69899
  "order, separated by a blank line. Do NOT paraphrase, summarise, or rewrite any of it, and do",
68504
69900
  "NOT add words of your own.",
68505
69901
  "",
69902
+ "Once the operator replies and clarifies or confirms the scope (or if scope was already clear),",
69903
+ "you MUST create the tasks: create the plan (if needed) and call `create_task` in the same turn so",
69904
+ "actionable task cards appear on the Tasks board immediately. Never leave the board empty with 0",
69905
+ "tasks after agreeing on scope. Never claim tasks are created, queued, or ready unless `create_task`",
69906
+ "was actually invoked and returned the task ID. Set `status: 'todo'` on newly created actionable",
69907
+ "tasks unless explicitly asked for backlog, so they appear in active Kanban columns immediately.",
69908
+ "When referring to created tasks or where to view them, use natural, human-friendly language with a markdown link",
69909
+ "to the Tasks page (e.g. 'on the [Tasks page](/tasks)', '[Tasks board](/tasks)', or in Ukrainian '\u043D\u0430 [\u0441\u0442\u043E\u0440\u0456\u043D\u0446\u0456 \u0437\u0430\u0434\u0430\u0447](/tasks)').",
69910
+ "Never output raw technical URL fragments like '\u043D\u0430 /tasks' or 'on /tasks', and never use developer database",
69911
+ "jargon like 'materialized tasks' or '\u043C\u0430\u0442\u0435\u0440\u0456\u0430\u043B\u0456\u0437\u0443\u0432\u0430\u0432 \u0437\u0430\u0434\u0430\u0447\u0456' \u2014 say 'created' / '\u0441\u0442\u0432\u043E\u0440\u0438\u0432' or 'added' / '\u0434\u043E\u0434\u0430\u0432' instead.",
69912
+ "",
68506
69913
  "Set resolvedIntent to the accepted task intent in your own concise words only after the outcome",
68507
69914
  "is resolved enough to create the task. Use it to capture the actionable understanding that came",
68508
69915
  "from the conversation and, when needed, retrieved wiki context. Do NOT use resolvedIntent as a",
@@ -68541,6 +69948,9 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68541
69948
  " SHALL sentence stating the externally visible behavior.",
68542
69949
  "- Give every requirement at least one `#### Scenario: <name>` with `- WHEN ...` / `- THEN ...`",
68543
69950
  " bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
69951
+ "- MUST include `## Acceptance criteria`: a markdown checklist (`- [ ] ...`) of concrete, verifiable",
69952
+ " conditions \u2014 this is the machine-usable yardstick the consensus review scores against. NEVER format criteria as numbered lists (`1. ...`) or plain bullets.",
69953
+ "- MUST include `## Verification`: the exact tests, commands, or checks that prove each criterion holds.",
68544
69954
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
68545
69955
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
68546
69956
  '- 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 `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. When the review agrees, tell the operator "the reviewers agreed on the spec" and proceed to create_execution_run. When it does not, tell them the reviewers could not agree and that there is a decision question in the inbox. Never say "consensus", "converged", or "CONVERGED" to the operator; those are internal words.',
@@ -68660,15 +70070,20 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68660
70070
  `Product-surface contract \u2014 never answer "I don't know where that is" about Mrrlin's own UI:`,
68661
70071
  "- You are talking to an operator inside this product. When they ask where something is, name",
68662
70072
  " the route and the control by the label actually printed on it, then let them click it.",
68663
- "- Connecting a GitHub repository lives on `/settings`, under the `GitHub integration` heading.",
68664
- " That SECTION is always on the page \u2014 it sits below Overview, Scheduler and Intent Judge, so",
68665
- " an operator who cannot find it has most likely not scrolled that far. What the section shows",
68666
- " depends on how far along they already are, so do NOT promise a specific card before you know:",
68667
- ' nothing connected yet \u2192 a card titled "Connect a GitHub repository" with a `Connect GitHub`',
68668
- " button that installs the GitHub App; App installed but no repo chosen \u2192 a repo picker to bind",
68669
- " one; repo already bound \u2192 the connected repo and its deploy workflows. Describe the section",
68670
- " and let them read which of the three they are looking at, or ask them. Binding a repo is",
68671
- " optional: a project without one still works, it just has no branch/PR/deploy controls on tasks.",
70073
+ "- Autopilot view lives on `/autopilot`. It displays autonomous execution runs, active background",
70074
+ " jobs, and tasks currently in handoff. To inspect why tasks are in handoff or diagnose autonomous",
70075
+ ' progress, use the canonical typed tools: `list_inbox_items` (filtering by kind="handoff"),',
70076
+ " `list_tasks` (filtering by status), and `list_execution_runs`.",
70077
+ "- Tasks and board backlog live on `/tasks` (and individual task details on `/items/{taskId}`).",
70078
+ " Always use `list_tasks`, `get_task`, and `list_task_events` to inspect them.",
70079
+ "- Project knowledge, deliverable reports, and specifications live under `/wiki` and `/deliverables/*`.",
70080
+ " These are stored as project wiki pages \u2014 read them directly via `get_wiki_page` or search with",
70081
+ " `search_wiki_pages`/`list_wiki_pages`. Never open the browser for `/deliverables/*` or `/wiki/*`.",
70082
+ "- Workspace settings, scheduler configurations, and integrations live on `/settings`.",
70083
+ "- Universal Tool Availability: All canonical typed tools (`list_tasks`, `get_task`, `list_inbox_items`,",
70084
+ " `list_execution_runs`, `get_wiki_page`, `search_wiki_pages`, `list_wiki_pages`) are authoritative",
70085
+ " and fully available across ALL routes and session tabs. Never tell an operator that a typed tool",
70086
+ " is missing or unavailable because of the current page or route.",
68672
70087
  "- If a surface is genuinely not in this list, say what you do know and check with a typed tool",
68673
70088
  ' rather than guessing a route \u2014 a wrong path costs the operator more than an honest "let me',
68674
70089
  ' check". Never send them to the browser to hunt for a page in their own app.',
@@ -68722,22 +70137,12 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68722
70137
  "- During a live chat turn, when a SPECIFIC task is ambiguous and the ambiguity affects",
68723
70138
  " the work, ASK the operator a simple, concrete question inline in your reply (default to",
68724
70139
  " asking rather than silently assuming). Low-impact ambiguity may still be recorded as an",
68725
- " assumption on the spec page.",
68726
- "- For any such task-affecting question, ALSO bank it durably before your final reply:",
68727
- ' `create_inbox_item` with `kind="question"`, `taskId` set, `directorSessionId` from the',
68728
- " Context JSON, `body` = the exact question, and `questionOptions` = 2-3 concrete answer",
68729
- " options. Each option MUST include a short `label`, a concise `description`, and a",
68730
- " self-contained durable `answer`; you MAY mark at most one option as `recommended` when",
68731
- " you explicitly prefer a path. The operator always has a custom-answer path in the UI, so",
68732
- " your options should guide, not trap. (The server dedups identical open questions; do not",
68733
- " supply a dedup key.)",
68734
- "- When the operator answers in chat, write the answer into the Specs/{taskId}",
68735
- ' Clarifications section, then `decide_inbox_item` the question with `verdict="acknowledged"`',
68736
- " and `answer` = the resolved answer.",
68737
- "- Project-level ambiguity (scope or direction not yet tied to a specific task) MAY be banked",
68738
- " the same way but with `taskId` OMITTED. There is no spec page yet, so incorporate the answer",
68739
- " into your current scoping/conversation. Reserve taskless questions for genuine project-level",
68740
- " scope \u2014 task-affecting questions still set `taskId`.",
70140
+ " assumption on the spec page. NEVER ask clarifying questions after Turn 1 when the operator",
70141
+ " has provided core project scope; adopt standard conventions for secondary edge cases and create tasks.",
70142
+ '- In synchronous live Director chat (Turn 1 scoping), ask your questions directly in your chat reply without creating Inbox items. Only bank questions in Inbox (`create_inbox_item` with `kind="question"`, `taskId` set, `directorSessionId` from Context JSON, `questionOptions` = 2-3 concrete answer options) when an autonomous background task pauses, when a task requires operator decision while the operator is away, or when explicitly requested. Each banked option MUST include a short `label`, a concise `description`, and a self-contained durable `answer`; you MAY mark at most one option as `recommended` when you explicitly prefer a path.',
70143
+ '- When the operator answers in chat, write the answer into the Specs/{taskId} Clarifications section, and if an Inbox question was previously open, `decide_inbox_item` with `verdict="acknowledged"` and `answer` = the resolved answer.',
70144
+ "- Project-level ambiguity (scope or direction not yet tied to a specific task) in live chat stays in chat; for genuine asynchronous pauses it MAY be banked with `taskId` OMITTED. Reserve taskless Inbox questions for genuine project-level scope.",
70145
+ "- Retiring answered questions: When the operator answers a previously banked question (task-specific or taskless) in chat, you MUST immediately call `decide_inbox_item` with `verdict=\"acknowledged\"` and note/answer BEFORE calling `create_task`. Do NOT leave answered questions in status 'pending' in Inbox.",
68741
70146
  "- Binding taskless answers to a task: WHEN you create a task out of project-level scoping,",
68742
70147
  " call `list_inbox_items` (kind=question), fold any relevant `acknowledged` TASKLESS answer",
68743
70148
  " into the new task's Specs/{id} Clarifications, and `reject` (with a reason) any taskless",
@@ -69065,7 +70470,8 @@ function createBridgeRequestListener(opts) {
69065
70470
  }
69066
70471
  const nonce = import_node_crypto15.default.randomBytes(16).toString("base64url");
69067
70472
  res.writeHead(200, { "content-type": "application/json" });
69068
- res.end(JSON.stringify({ ok: true, wsUrl: `ws://${host}:${port}/ws`, token, nonce }));
70473
+ const publicHost = host === "0.0.0.0" ? "127.0.0.1" : host;
70474
+ res.end(JSON.stringify({ ok: true, wsUrl: `ws://${publicHost}:${port}/ws`, token, nonce }));
69069
70475
  return;
69070
70476
  }
69071
70477
  res.writeHead(404).end();
@@ -69970,8 +71376,10 @@ function createBridgeMessageHandler(deps) {
69970
71376
  const desiredEffort = normalizeReasoningEffort(msg.reasoningEffort);
69971
71377
  const resolvedProviderId = turnSeat.id;
69972
71378
  let startedFreshThread = false;
71379
+ let desiredInstructionHash = null;
69973
71380
  try {
69974
71381
  const startParams = deps.buildStartParams(msg, desiredModel);
71382
+ desiredInstructionHash = import_node_crypto15.default.createHash("sha256").update(startParams.baseInstructions ?? "").digest("hex");
69975
71383
  if (!thread) {
69976
71384
  const newThread = {
69977
71385
  threadId: "",
@@ -69982,7 +71390,8 @@ function createBridgeMessageHandler(deps) {
69982
71390
  reasoningEffort: desiredEffort,
69983
71391
  replay: null,
69984
71392
  supersededReplay: null,
69985
- provider: resolvedProviderId
71393
+ provider: resolvedProviderId,
71394
+ systemInstructionHash: null
69986
71395
  };
69987
71396
  const started = await codexClient.startThread(startParams);
69988
71397
  newThread.threadId = started.threadId;
@@ -69993,17 +71402,27 @@ function createBridgeMessageHandler(deps) {
69993
71402
  } else {
69994
71403
  const existingThread = thread;
69995
71404
  const providerMismatch = existingThread.provider !== null && existingThread.provider !== resolvedProviderId;
71405
+ const instructionMismatch = (resolvedProviderId === "agy" || resolvedProviderId === "claude") && (!existingThread.systemInstructionHash || existingThread.systemInstructionHash !== desiredInstructionHash);
69996
71406
  let resumed = true;
69997
71407
  let result;
69998
- if (!existingThread.threadId || providerMismatch) {
71408
+ if (!existingThread.threadId || providerMismatch || instructionMismatch) {
71409
+ if (existingThread.threadId) {
71410
+ await codexClient.interruptTurn(existingThread.threadId).catch(() => {
71411
+ });
71412
+ }
69999
71413
  resumed = false;
70000
71414
  result = await codexClient.startThread(startParams);
71415
+ existingThread.systemInstructionHash = null;
70001
71416
  } else {
70002
71417
  try {
70003
- result = await codexClient.resumeThread(existingThread.threadId, startParams);
71418
+ result = await codexClient.resumeThread(existingThread.threadId, {
71419
+ ...startParams,
71420
+ alreadySeeded: true
71421
+ });
70004
71422
  } catch {
70005
71423
  resumed = false;
70006
71424
  result = await codexClient.startThread(startParams);
71425
+ existingThread.systemInstructionHash = null;
70007
71426
  }
70008
71427
  }
70009
71428
  existingThread.threadId = result.threadId;
@@ -70137,10 +71556,13 @@ function createBridgeMessageHandler(deps) {
70137
71556
  deps.releaseCodex(turnSeat.id);
70138
71557
  return;
70139
71558
  }
71559
+ const presentedLedger = presentLedger(thread.clarificationLedger ?? emptyLedger());
71560
+ thread.clarificationLedger = presentedLedger.ledger;
70140
71561
  const contextPrefix = renderContextPrefix({
70141
71562
  ...msg.context,
70142
71563
  project: projectContext,
70143
71564
  answeredQuestions,
71565
+ openQuestions: presentedLedger.shown.length ? presentedLedger.shown : void 0,
70144
71566
  directorSessionId
70145
71567
  });
70146
71568
  const sendInTurn = (msg2) => {
@@ -70195,7 +71617,30 @@ function createBridgeMessageHandler(deps) {
70195
71617
  let exhaustedLimitSignal = null;
70196
71618
  let turnTokenUsage = null;
70197
71619
  let promptBytes = 0;
71620
+ let contextBudgetGateTriggered = false;
70198
71621
  const deferredResults = [];
71622
+ const closureEvents = [];
71623
+ let closureBefore = null;
71624
+ const takeClosureSnapshot = async () => {
71625
+ const snapshot = deps.snapshotProjectObjects;
71626
+ if (!snapshot || !contextProjectSlug) return null;
71627
+ const controller = new AbortController();
71628
+ let timer;
71629
+ try {
71630
+ timer = setTimeout(() => {
71631
+ controller.abort(new Error("snapshot timed out"));
71632
+ }, CLOSURE_SNAPSHOT_TIMEOUT_MS);
71633
+ return await snapshot(contextProjectSlug, { signal: controller.signal });
71634
+ } catch (error51) {
71635
+ process.stderr.write(
71636
+ `[mrrlin-mcp director-bridge] closure_snapshot_failed for ${contextProjectSlug}: ${error51 instanceof Error ? error51.message : String(error51)}
71637
+ `
71638
+ );
71639
+ return null;
71640
+ } finally {
71641
+ if (timer) clearTimeout(timer);
71642
+ }
71643
+ };
70199
71644
  let pokeActivity = () => {
70200
71645
  };
70201
71646
  const forward = (event) => {
@@ -70207,11 +71652,24 @@ function createBridgeMessageHandler(deps) {
70207
71652
  if (event.type === "token_usage") {
70208
71653
  const usage = event.usage;
70209
71654
  turnTokenUsage = addTurnTokenUsage(turnTokenUsage, usage);
71655
+ if (turnTokenUsage) {
71656
+ const totalTokens = (turnTokenUsage.inputTokens ?? 0) + (turnTokenUsage.outputTokens ?? 0);
71657
+ if (totalTokens > 0) {
71658
+ const maxTokens = 2e5;
71659
+ const remainingTokens = maxTokens - totalTokens;
71660
+ if (remainingTokens / maxTokens < 0.03 || remainingTokens < 4e3) {
71661
+ contextBudgetGateTriggered = true;
71662
+ }
71663
+ }
71664
+ }
70210
71665
  return;
70211
71666
  }
70212
71667
  if (event.type === "turn_completed" || event.type === "rate_limits") {
70213
71668
  return;
70214
71669
  }
71670
+ if (event.type === "assistant" || event.type === "tool_result" || event.type === "tool_use") {
71671
+ closureEvents.push(event);
71672
+ }
70215
71673
  outputEvents += 1;
70216
71674
  sendInTurn({ type: "event", directorSessionId, event });
70217
71675
  };
@@ -70220,7 +71678,21 @@ function createBridgeMessageHandler(deps) {
70220
71678
  pokeActivity();
70221
71679
  });
70222
71680
  const currentTurnBrowserToolIds = /* @__PURE__ */ new Set();
71681
+ let hadErrorOnTurn = false;
71682
+ let confirmedProductiveEvent = false;
70223
71683
  const unsubEvent = codexClient.onEvent(threadId, (event) => {
71684
+ if (event.type === "error") {
71685
+ hadErrorOnTurn = true;
71686
+ } else if (!confirmedProductiveEvent && !hadErrorOnTurn) {
71687
+ const isProductive = event.type === "assistant" && typeof event.content === "string" && event.content.trim().length > 0 || event.type === "tool_use" || event.type === "tool_result" || event.type === "result" && event.subtype === "success";
71688
+ if (isProductive) {
71689
+ confirmedProductiveEvent = true;
71690
+ if (desiredInstructionHash && thread.systemInstructionHash !== desiredInstructionHash) {
71691
+ thread.systemInstructionHash = desiredInstructionHash;
71692
+ deps.persistThreads();
71693
+ }
71694
+ }
71695
+ }
70224
71696
  if (event.type === "tool_use") {
70225
71697
  toolBusy.start(event.tool.id);
70226
71698
  if (event.tool.name.startsWith("mcp__mrrlin-browser__browser_")) {
@@ -70354,6 +71826,7 @@ ${effectiveText}` : effectiveText;
70354
71826
  turnInput.push({ type: "image", url: imageDataUri });
70355
71827
  }
70356
71828
  sendInTurn({ type: "turn-executing", directorSessionId, turnId: currentReplayBuf.turnId });
71829
+ closureBefore = await takeClosureSnapshot();
70357
71830
  await codexClient.startTurn(thread.threadId, turnInput, thread.reasoningEffort ?? void 0);
70358
71831
  await turnFinished;
70359
71832
  })();
@@ -70371,7 +71844,47 @@ ${effectiveText}` : effectiveText;
70371
71844
  error: zeroOutputLimit.message
70372
71845
  });
70373
71846
  }
70374
- const telemetry = turnTokenUsage !== null ? { promptBytes, usage: turnTokenUsage } : void 0;
71847
+ if (!hadErrorOnTurn && outputEvents > 0) {
71848
+ const recorded = recordTurn(thread.clarificationLedger ?? emptyLedger(), finalAssistantText(closureEvents));
71849
+ thread.clarificationLedger = recorded.ledger;
71850
+ for (const item of recorded.reasked) {
71851
+ process.stderr.write(
71852
+ `[mrrlin-mcp director-bridge] clarification_reasked (x${item.reasked}) for ${directorSessionId}: ${item.question}
71853
+ `
71854
+ );
71855
+ }
71856
+ for (const question of recorded.overflow) {
71857
+ process.stderr.write(
71858
+ `[mrrlin-mcp director-bridge] clarification_overflow for ${directorSessionId}: ${question}
71859
+ `
71860
+ );
71861
+ }
71862
+ for (const item of recorded.dropped) {
71863
+ process.stderr.write(
71864
+ `[mrrlin-mcp director-bridge] clarification_dropped (${item.status}) for ${directorSessionId}: ${item.question}
71865
+ `
71866
+ );
71867
+ }
71868
+ deps.persistThreads();
71869
+ }
71870
+ if (closureBefore && !hadErrorOnTurn && outputEvents > 0) {
71871
+ const closureAfter = await takeClosureSnapshot();
71872
+ const closureText = closureAfter ? closureFor({ before: closureBefore, after: closureAfter, events: closureEvents, operatorText: msg.message ?? "" }) : null;
71873
+ if (closureText) {
71874
+ sendInTurn({ type: "event", directorSessionId, event: { type: "assistant", content: closureText } });
71875
+ }
71876
+ }
71877
+ if (contextBudgetGateTriggered) {
71878
+ sendInTurn({
71879
+ type: "event",
71880
+ directorSessionId,
71881
+ event: {
71882
+ type: "assistant",
71883
+ content: "\n\n\u26A0\uFE0F **Context Budget Gate**: Model context window is near capacity (<3% remaining). Further tool dispatch disabled to preserve conversational integrity and results."
71884
+ }
71885
+ });
71886
+ }
71887
+ const telemetry = turnTokenUsage !== null ? { promptBytes, usage: turnTokenUsage, contextBudgetGateTriggered } : void 0;
70375
71888
  for (const event of deferredResults) {
70376
71889
  sendInTurn({ type: "event", directorSessionId, event, telemetry });
70377
71890
  }
@@ -70655,7 +72168,7 @@ async function startDirectorBridge() {
70655
72168
  await tokenRefresher.start();
70656
72169
  process.stderr.write("[mrrlin-mcp director-bridge] agent credential active; operator token auto-refresh enabled.\n");
70657
72170
  } else {
70658
- const apiToken = envToken ?? readOperatorToken() ?? void 0;
72171
+ let apiToken = envToken ?? readOperatorToken() ?? void 0;
70659
72172
  if (envToken) {
70660
72173
  try {
70661
72174
  persistOperatorToken(envToken);
@@ -70858,7 +72371,9 @@ async function startDirectorBridge() {
70858
72371
  reasoningEffort: normalizeReasoningEffort(entry.reasoningEffort),
70859
72372
  replay: null,
70860
72373
  supersededReplay: null,
70861
- provider: entry.provider
72374
+ provider: entry.provider,
72375
+ systemInstructionHash: entry.systemInstructionHash ?? null,
72376
+ ...entry.clarificationLedger ? { clarificationLedger: entry.clarificationLedger } : {}
70862
72377
  });
70863
72378
  }
70864
72379
  const persistThreads = () => {
@@ -70870,7 +72385,9 @@ async function startDirectorBridge() {
70870
72385
  lastUsedAt: thread.lastUsedAt,
70871
72386
  model: thread.model,
70872
72387
  reasoningEffort: thread.reasoningEffort,
70873
- provider: thread.provider
72388
+ provider: thread.provider,
72389
+ systemInstructionHash: thread.systemInstructionHash ?? null,
72390
+ ...thread.clarificationLedger ? { clarificationLedger: thread.clarificationLedger } : {}
70874
72391
  }
70875
72392
  ])
70876
72393
  );
@@ -71037,6 +72554,35 @@ async function startDirectorBridge() {
71037
72554
  }
71038
72555
  },
71039
72556
  listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
72557
+ snapshotProjectObjects: async (projectSlug, opts) => {
72558
+ const signal = opts?.signal;
72559
+ if (signal?.aborted) throw new Error("snapshot aborted");
72560
+ const tasks = await client.listTasks(
72561
+ projectSlug,
72562
+ { archived: "all", limit: CLOSURE_MAX_SNAPSHOT_TASKS + 1 },
72563
+ { view: "summary", signal }
72564
+ );
72565
+ if (tasks.length > CLOSURE_MAX_SNAPSHOT_TASKS) {
72566
+ throw new Error(`${tasks.length} tasks exceed the closure snapshot cap (${CLOSURE_MAX_SNAPSHOT_TASKS})`);
72567
+ }
72568
+ const [pages, inbox] = await Promise.all([
72569
+ client.listWikiPages(
72570
+ projectSlug,
72571
+ { folder: "Specs", limit: CLOSURE_MAX_SNAPSHOT_PAGES + 1 },
72572
+ { view: "summary", signal }
72573
+ ),
72574
+ client.listInboxItems(
72575
+ projectSlug,
72576
+ { status: "pending", limit: CLOSURE_MAX_SNAPSHOT_INBOX + 1 },
72577
+ { view: "summary", signal }
72578
+ )
72579
+ ]);
72580
+ return {
72581
+ inbox: inbox.length > CLOSURE_MAX_SNAPSHOT_INBOX ? null : new Set(inbox.map((item) => item.id)),
72582
+ tasks: new Map(tasks.map((task) => [task.id, { title: task.title, updatedAt: task.updatedAt ?? null }])),
72583
+ pages: pages.length > CLOSURE_MAX_SNAPSHOT_PAGES ? null : new Map(pages.map((page) => [page.id, { title: page.title, updatedAt: page.updatedAt }]))
72584
+ };
72585
+ },
71040
72586
  getProject: (projectSlug) => client.getProject(projectSlug),
71041
72587
  // External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
71042
72588
  // escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
@@ -71571,10 +73117,17 @@ function renderContextPrefix(context) {
71571
73117
  if (directorSessionId) payload.directorSessionId = directorSessionId;
71572
73118
  if (contextMessages?.length) payload.contextMessages = contextMessages;
71573
73119
  if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
73120
+ const openQuestions = context?.openQuestions?.length ? context.openQuestions : void 0;
73121
+ if (openQuestions) payload.openQuestions = openQuestions;
71574
73122
  const keys = Object.keys(payload);
71575
73123
  if (keys.length > 0) {
71576
73124
  parts.push(`Context (JSON): ${JSON.stringify(payload)}`);
71577
73125
  }
73126
+ if (openQuestions) {
73127
+ parts.push(
73128
+ "[OPEN QUESTIONS] `openQuestions` lists questions you asked earlier in this chat. For each one, state in this reply whether it is answered (by the operator), assumed (say the default you took) or a blocker (say what it blocks). Do not ask it again unless the answer would change the architecture or the deliverable."
73129
+ );
73130
+ }
71578
73131
  return parts.join("\n\n");
71579
73132
  }
71580
73133
 
@@ -72299,7 +73852,9 @@ function jsonSchemaProperty(schema) {
72299
73852
  property.enum = jsonSchema.enum;
72300
73853
  }
72301
73854
  if (typeof jsonSchema.pattern === "string") {
72302
- property.pattern = jsonSchema.pattern;
73855
+ if (!/\(\?<?[!=]/.test(jsonSchema.pattern)) {
73856
+ property.pattern = jsonSchema.pattern;
73857
+ }
72303
73858
  }
72304
73859
  return property;
72305
73860
  }
@@ -72909,7 +74464,8 @@ function installService(deps) {
72909
74464
  return { ok: false };
72910
74465
  }
72911
74466
  const claudeBin = deps.which("claude");
72912
- const resolvedBins = [...bins, claudeBin].filter((b) => b !== null);
74467
+ const agyBin = deps.which("agy");
74468
+ const resolvedBins = [...bins, claudeBin, agyBin].filter((b) => b !== null);
72913
74469
  const isWin = import_node_os18.default.platform() === "win32";
72914
74470
  const systemDirs = isWin ? (deps.env.PATH || "").split(import_node_path39.default.delimiter).filter(Boolean) : SYSTEM_PATH_DIRS;
72915
74471
  const pathEnv = Array.from(
@@ -73184,6 +74740,24 @@ Missing required flag(s): ${missing.join(
73184
74740
  }
73185
74741
 
73186
74742
  // src/bin.ts
74743
+ function augmentProcessPathWithCommonDirs() {
74744
+ const isWin = process.platform === "win32";
74745
+ if (isWin) return;
74746
+ const home = process.env.HOME || (0, import_node_os20.homedir)();
74747
+ const candidates = [
74748
+ import_node_path43.default.join(home, ".local", "bin"),
74749
+ import_node_path43.default.join(home, ".cargo", "bin"),
74750
+ "/usr/local/bin",
74751
+ "/opt/homebrew/bin"
74752
+ ];
74753
+ const existing = (process.env.PATH || "").split(import_node_path43.default.delimiter).filter(Boolean);
74754
+ const existingSet = new Set(existing);
74755
+ const toAdd = candidates.filter((d) => !existingSet.has(d) && (0, import_node_fs25.existsSync)(d));
74756
+ if (toAdd.length > 0) {
74757
+ process.env.PATH = [...existing, ...toAdd].join(import_node_path43.default.delimiter);
74758
+ }
74759
+ }
74760
+ augmentProcessPathWithCommonDirs();
73187
74761
  function pm2Runner(args) {
73188
74762
  const r = (0, import_node_child_process22.spawnSync)("pm2", args, { encoding: "utf8" });
73189
74763
  process.stderr.write(r.stdout ?? "");
@@ -73690,6 +75264,10 @@ main().catch((error51) => {
73690
75264
  `);
73691
75265
  process.exit(1);
73692
75266
  });
75267
+ // Annotate the CommonJS export names for ESM import in node:
75268
+ 0 && (module.exports = {
75269
+ augmentProcessPathWithCommonDirs
75270
+ });
73693
75271
  /*! Bundled license information:
73694
75272
 
73695
75273
  content-type/index.js: