@mrrlin-dev/mcp 0.4.95 → 0.4.98

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 +2071 -424
  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({
@@ -1646,20 +1647,20 @@ var require_parse_async = __commonJS({
1646
1647
  const index = 0;
1647
1648
  const blocksize = opts.blocksize || 40960;
1648
1649
  const parser = new TOMLParser();
1649
- return new Promise((resolve, reject) => {
1650
- setImmediate(parseAsyncNext, index, blocksize, resolve, reject);
1650
+ return new Promise((resolve2, reject) => {
1651
+ setImmediate(parseAsyncNext, index, blocksize, resolve2, reject);
1651
1652
  });
1652
- function parseAsyncNext(index2, blocksize2, resolve, reject) {
1653
+ function parseAsyncNext(index2, blocksize2, resolve2, reject) {
1653
1654
  if (index2 >= str.length) {
1654
1655
  try {
1655
- return resolve(parser.finish());
1656
+ return resolve2(parser.finish());
1656
1657
  } catch (err) {
1657
1658
  return reject(prettyError(err, str));
1658
1659
  }
1659
1660
  }
1660
1661
  try {
1661
1662
  parser.parse(str.slice(index2, index2 + blocksize2));
1662
- setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2, resolve, reject);
1663
+ setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2, resolve2, reject);
1663
1664
  } catch (err) {
1664
1665
  reject(prettyError(err, str));
1665
1666
  }
@@ -1685,7 +1686,7 @@ var require_parse_stream = __commonJS({
1685
1686
  function parseReadable(stm) {
1686
1687
  const parser = new TOMLParser();
1687
1688
  stm.setEncoding("utf8");
1688
- return new Promise((resolve, reject) => {
1689
+ return new Promise((resolve2, reject) => {
1689
1690
  let readable;
1690
1691
  let ended = false;
1691
1692
  let errored = false;
@@ -1693,7 +1694,7 @@ var require_parse_stream = __commonJS({
1693
1694
  ended = true;
1694
1695
  if (readable) return;
1695
1696
  try {
1696
- resolve(parser.finish());
1697
+ resolve2(parser.finish());
1697
1698
  } catch (err) {
1698
1699
  reject(err);
1699
1700
  }
@@ -4979,7 +4980,7 @@ var require_compile = __commonJS({
4979
4980
  const schOrFunc = root.refs[ref];
4980
4981
  if (schOrFunc)
4981
4982
  return schOrFunc;
4982
- let _sch = resolve.call(this, root, ref);
4983
+ let _sch = resolve2.call(this, root, ref);
4983
4984
  if (_sch === void 0) {
4984
4985
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
4985
4986
  const { schemaId } = this.opts;
@@ -5006,7 +5007,7 @@ var require_compile = __commonJS({
5006
5007
  function sameSchemaEnv(s1, s2) {
5007
5008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
5008
5009
  }
5009
- function resolve(root, ref) {
5010
+ function resolve2(root, ref) {
5010
5011
  let sch;
5011
5012
  while (typeof (sch = this.refs[ref]) == "string")
5012
5013
  ref = sch;
@@ -5836,7 +5837,7 @@ var require_fast_uri = __commonJS({
5836
5837
  }
5837
5838
  return uri;
5838
5839
  }
5839
- function resolve(baseURI, relativeURI, options) {
5840
+ function resolve2(baseURI, relativeURI, options) {
5840
5841
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
5841
5842
  const {
5842
5843
  parsed: baseParsed,
@@ -6205,7 +6206,7 @@ var require_fast_uri = __commonJS({
6205
6206
  var fastUri = {
6206
6207
  SCHEMES,
6207
6208
  normalize: normalize2,
6208
- resolve,
6209
+ resolve: resolve2,
6209
6210
  resolveComponent,
6210
6211
  equal,
6211
6212
  serialize,
@@ -9392,12 +9393,12 @@ var require_isexe = __commonJS({
9392
9393
  if (typeof Promise !== "function") {
9393
9394
  throw new TypeError("callback not provided");
9394
9395
  }
9395
- return new Promise(function(resolve, reject) {
9396
+ return new Promise(function(resolve2, reject) {
9396
9397
  isexe(path29, options || {}, function(er, is) {
9397
9398
  if (er) {
9398
9399
  reject(er);
9399
9400
  } else {
9400
- resolve(is);
9401
+ resolve2(is);
9401
9402
  }
9402
9403
  });
9403
9404
  });
@@ -9463,27 +9464,27 @@ var require_which = __commonJS({
9463
9464
  opt = {};
9464
9465
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
9465
9466
  const found = [];
9466
- const step = (i) => new Promise((resolve, reject) => {
9467
+ const step = (i) => new Promise((resolve2, reject) => {
9467
9468
  if (i === pathEnv.length)
9468
- return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
9469
+ return opt.all && found.length ? resolve2(found) : reject(getNotFoundError(cmd));
9469
9470
  const ppRaw = pathEnv[i];
9470
9471
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
9471
9472
  const pCmd = path29.join(pathPart, cmd);
9472
9473
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
9473
- resolve(subStep(p, i, 0));
9474
+ resolve2(subStep(p, i, 0));
9474
9475
  });
9475
- const subStep = (p, i, ii) => new Promise((resolve, reject) => {
9476
+ const subStep = (p, i, ii) => new Promise((resolve2, reject) => {
9476
9477
  if (ii === pathExt.length)
9477
- return resolve(step(i + 1));
9478
+ return resolve2(step(i + 1));
9478
9479
  const ext = pathExt[ii];
9479
9480
  isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
9480
9481
  if (!er && is) {
9481
9482
  if (opt.all)
9482
9483
  found.push(p + ext);
9483
9484
  else
9484
- return resolve(p + ext);
9485
+ return resolve2(p + ext);
9485
9486
  }
9486
- return resolve(subStep(p, i, ii + 1));
9487
+ return resolve2(subStep(p, i, ii + 1));
9487
9488
  });
9488
9489
  });
9489
9490
  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
@@ -14016,7 +14017,7 @@ var require_websocket = __commonJS({
14016
14017
  var http2 = require("http");
14017
14018
  var net = require("net");
14018
14019
  var tls = require("tls");
14019
- var { randomBytes: randomBytes5, createHash: createHash5 } = require("crypto");
14020
+ var { randomBytes: randomBytes5, createHash: createHash6 } = require("crypto");
14020
14021
  var { Duplex, Readable } = require("stream");
14021
14022
  var { URL: URL2 } = require("url");
14022
14023
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -14684,7 +14685,7 @@ var require_websocket = __commonJS({
14684
14685
  abortHandshake(websocket, socket, "Invalid Upgrade header");
14685
14686
  return;
14686
14687
  }
14687
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
14688
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
14688
14689
  if (res.headers["sec-websocket-accept"] !== digest) {
14689
14690
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
14690
14691
  return;
@@ -15053,7 +15054,7 @@ var require_websocket_server = __commonJS({
15053
15054
  var EventEmitter3 = require("events");
15054
15055
  var http2 = require("http");
15055
15056
  var { Duplex } = require("stream");
15056
- var { createHash: createHash5 } = require("crypto");
15057
+ var { createHash: createHash6 } = require("crypto");
15057
15058
  var extension2 = require_extension();
15058
15059
  var PerMessageDeflate2 = require_permessage_deflate();
15059
15060
  var subprotocol2 = require_subprotocol();
@@ -15360,7 +15361,7 @@ var require_websocket_server = __commonJS({
15360
15361
  );
15361
15362
  }
15362
15363
  if (this._state > RUNNING) return abortHandshake(socket, 503);
15363
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
15364
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
15364
15365
  const headers = [
15365
15366
  "HTTP/1.1 101 Switching Protocols",
15366
15367
  "Upgrade: websocket",
@@ -16946,12 +16947,12 @@ var require_adapter = __commonJS({
16946
16947
  return newFs;
16947
16948
  }
16948
16949
  function toPromise(method) {
16949
- return (...args) => new Promise((resolve, reject) => {
16950
+ return (...args) => new Promise((resolve2, reject) => {
16950
16951
  args.push((err, result) => {
16951
16952
  if (err) {
16952
16953
  reject(err);
16953
16954
  } else {
16954
- resolve(result);
16955
+ resolve2(result);
16955
16956
  }
16956
16957
  });
16957
16958
  method(...args);
@@ -21405,10 +21406,10 @@ var require_browser = __commonJS({
21405
21406
  text = canvas;
21406
21407
  canvas = void 0;
21407
21408
  }
21408
- return new Promise(function(resolve, reject) {
21409
+ return new Promise(function(resolve2, reject) {
21409
21410
  try {
21410
21411
  const data = QRCode2.create(text, opts);
21411
- resolve(renderFunc(data, canvas, opts));
21412
+ resolve2(renderFunc(data, canvas, opts));
21412
21413
  } catch (e) {
21413
21414
  reject(e);
21414
21415
  }
@@ -21489,11 +21490,11 @@ var require_server = __commonJS({
21489
21490
  }
21490
21491
  function render(renderFunc, text, params) {
21491
21492
  if (!params.cb) {
21492
- return new Promise(function(resolve, reject) {
21493
+ return new Promise(function(resolve2, reject) {
21493
21494
  try {
21494
21495
  const data = QRCode2.create(text, params.opts);
21495
21496
  return renderFunc(data, params.opts, function(err, data2) {
21496
- return err ? reject(err) : resolve(data2);
21497
+ return err ? reject(err) : resolve2(data2);
21497
21498
  });
21498
21499
  } catch (e) {
21499
21500
  reject(e);
@@ -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 };
@@ -41342,7 +41493,7 @@ function createBoundedSemaphore(slots) {
41342
41493
  let available = slots;
41343
41494
  const queue = [];
41344
41495
  return {
41345
- acquire: () => available > 0 ? (available -= 1, Promise.resolve()) : new Promise((resolve) => queue.push(resolve)),
41496
+ acquire: () => available > 0 ? (available -= 1, Promise.resolve()) : new Promise((resolve2) => queue.push(resolve2)),
41346
41497
  release: () => {
41347
41498
  const next = queue.shift();
41348
41499
  if (next) next();
@@ -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
@@ -42192,22 +42524,22 @@ var CodexJsonRpcClient = class {
42192
42524
  const payload = { jsonrpc: "2.0", id, method, params };
42193
42525
  const line = `${JSON.stringify(payload)}
42194
42526
  `;
42195
- const resultPromise = new Promise((resolve, reject) => {
42527
+ const resultPromise = new Promise((resolve2, reject) => {
42196
42528
  const onResponse = (response) => {
42197
42529
  if (response.error) {
42198
42530
  reject(new Error(response.error.message ?? "Codex JSON-RPC error"));
42199
42531
  return;
42200
42532
  }
42201
- resolve(response.result);
42533
+ resolve2(response.result);
42202
42534
  };
42203
42535
  this.emitter.once(`response:${id}`, onResponse);
42204
42536
  });
42205
- await new Promise((resolve, reject) => {
42537
+ await new Promise((resolve2, reject) => {
42206
42538
  this.writeStream.write(line, (error51) => {
42207
42539
  if (error51)
42208
42540
  reject(error51);
42209
42541
  else
42210
- resolve();
42542
+ resolve2();
42211
42543
  });
42212
42544
  });
42213
42545
  return await resultPromise;
@@ -42370,11 +42702,11 @@ var CodexServerManager = class {
42370
42702
  env: config2?.env ?? process.env,
42371
42703
  shell: true
42372
42704
  });
42373
- await new Promise((resolve, reject) => {
42705
+ await new Promise((resolve2, reject) => {
42374
42706
  child.on("error", (err) => {
42375
42707
  reject(new Error(`Failed to spawn codex app-server: ${err.message}`));
42376
42708
  });
42377
- child.on("spawn", () => resolve());
42709
+ child.on("spawn", () => resolve2());
42378
42710
  });
42379
42711
  if (!child.stdin || !child.stdout) {
42380
42712
  child.kill();
@@ -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();
@@ -43151,7 +43485,7 @@ var ClaudeProcess = class {
43151
43485
  cwd: options.cwd?.trim() || void 0,
43152
43486
  // Inherit the operator's env so the CLI picks up their own ~/.claude.json tool registry —
43153
43487
  // unless an explicit mcpConfig was provided in options.
43154
- env: process.env,
43488
+ env: options.env ?? process.env,
43155
43489
  stdio: ["pipe", "pipe", "pipe"]
43156
43490
  });
43157
43491
  this.child.stdout.setEncoding("utf8");
@@ -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(","));
@@ -43243,15 +43580,15 @@ var ClaudeProcess = class {
43243
43580
  return Promise.resolve();
43244
43581
  }
43245
43582
  if (this.disposed) {
43246
- return new Promise((resolve) => {
43583
+ return new Promise((resolve2) => {
43247
43584
  const off = this.onExit(() => {
43248
43585
  off();
43249
- resolve();
43586
+ resolve2();
43250
43587
  });
43251
43588
  });
43252
43589
  }
43253
43590
  this.disposed = true;
43254
- return new Promise((resolve) => {
43591
+ return new Promise((resolve2) => {
43255
43592
  const forceKill = setTimeout(() => {
43256
43593
  if (!this.exited)
43257
43594
  this.child.kill("SIGKILL");
@@ -43260,7 +43597,7 @@ var ClaudeProcess = class {
43260
43597
  const off = this.onExit(() => {
43261
43598
  clearTimeout(forceKill);
43262
43599
  off();
43263
- resolve();
43600
+ resolve2();
43264
43601
  });
43265
43602
  this.child.kill("SIGTERM");
43266
43603
  });
@@ -43460,11 +43797,16 @@ function permissionModeFor(sandbox) {
43460
43797
  return { permissionMode: "bypassPermissions" };
43461
43798
  }
43462
43799
  var ClaudeAgentHandle = class {
43800
+ childEnv;
43463
43801
  // One resident child + one mapper per thread — mirrors ClaudeProcess's own per-thread residency
43464
43802
  // and ClaudeBridgeEventMapper's documented "fresh instance per thread" contract (its tool-use
43465
43803
  // dedup sets must not leak across threads).
43466
43804
  processes = /* @__PURE__ */ new Map();
43467
43805
  mappers = /* @__PURE__ */ new Map();
43806
+ /** Exact complete environment passed unchanged to every process this handle spawns. */
43807
+ constructor(childEnv) {
43808
+ this.childEnv = childEnv;
43809
+ }
43468
43810
  async startThread(params) {
43469
43811
  const { permissionMode, disallowedTools } = permissionModeFor(params.sandbox);
43470
43812
  const child = new ClaudeProcess({
@@ -43473,7 +43815,9 @@ var ClaudeAgentHandle = class {
43473
43815
  disallowedTools,
43474
43816
  model: params.model,
43475
43817
  cwd: params.cwd,
43476
- mcpConfig: params.mcpConfig
43818
+ mcpConfig: params.mcpConfig,
43819
+ appendSystemPrompt: params.baseInstructions,
43820
+ ...this.childEnv === void 0 ? {} : { env: this.childEnv }
43477
43821
  });
43478
43822
  this.processes.set(child.sessionId, child);
43479
43823
  this.mappers.set(child.sessionId, new ClaudeBridgeEventMapper());
@@ -43487,7 +43831,9 @@ var ClaudeAgentHandle = class {
43487
43831
  disallowedTools,
43488
43832
  model: params.model,
43489
43833
  cwd: params.cwd,
43490
- mcpConfig: params.mcpConfig
43834
+ mcpConfig: params.mcpConfig,
43835
+ appendSystemPrompt: params.baseInstructions,
43836
+ ...this.childEnv === void 0 ? {} : { env: this.childEnv }
43491
43837
  });
43492
43838
  this.processes.set(threadId, child);
43493
43839
  this.mappers.set(threadId, new ClaudeBridgeEventMapper());
@@ -43631,8 +43977,14 @@ var AgyProcess = class _AgyProcess {
43631
43977
  args.push("--mode", "accept-edits");
43632
43978
  break;
43633
43979
  }
43634
- if (options.model) {
43635
- args.push("--model", options.model);
43980
+ let modelToUse = options.model;
43981
+ if (modelToUse === "gemini-3.1-pro-high" || modelToUse === "gemini-3.7-flash-high") {
43982
+ modelToUse = "gemini-3.8-flash-high";
43983
+ } else if (modelToUse === "gemini-3.1-pro-low" || modelToUse === "gemini-3.7-flash-low") {
43984
+ modelToUse = "gemini-3.8-flash-low";
43985
+ }
43986
+ if (modelToUse) {
43987
+ args.push("--model", modelToUse);
43636
43988
  }
43637
43989
  if (mode.kind === "resume") {
43638
43990
  args.push("--conversation", mode.resumeId);
@@ -43641,7 +43993,7 @@ var AgyProcess = class _AgyProcess {
43641
43993
  const cwd = options.cwd?.trim() || void 0;
43642
43994
  this.child = (0, import_node_child_process4.spawn)(executable, args, {
43643
43995
  cwd,
43644
- env: process.env,
43996
+ env: options.env ?? process.env,
43645
43997
  stdio: ["pipe", "pipe", "pipe"]
43646
43998
  });
43647
43999
  activeProcesses.add(this.child);
@@ -43861,7 +44213,7 @@ function agyModelCatalog() {
43861
44213
  id: entry.id,
43862
44214
  displayName: entry.displayName,
43863
44215
  description: entry.description,
43864
- isDefault: entry.id === "gemini-3.1-pro-high"
44216
+ isDefault: entry.id === "gemini-3.8-flash-high"
43865
44217
  }));
43866
44218
  }
43867
44219
  var MODEL_ID_REGEX2 = /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,63}$/;
@@ -43886,7 +44238,7 @@ function parseAgyModelsOutput(output3) {
43886
44238
  id,
43887
44239
  displayName,
43888
44240
  description,
43889
- isDefault: id === "gemini-3.1-pro-high"
44241
+ isDefault: id === "gemini-3.8-flash-high"
43890
44242
  });
43891
44243
  }
43892
44244
  return models;
@@ -43953,18 +44305,28 @@ function permissionModeFor2(sandbox, approvalPolicy) {
43953
44305
  return { permissionMode: "acceptEdits" };
43954
44306
  }
43955
44307
  var AgyAgentHandle = class {
44308
+ childEnv;
43956
44309
  processes = /* @__PURE__ */ new Map();
43957
44310
  mappers = /* @__PURE__ */ new Map();
44311
+ baseInstructions = /* @__PURE__ */ new Map();
44312
+ /** Exact complete environment passed unchanged to every process this handle spawns. */
44313
+ constructor(childEnv) {
44314
+ this.childEnv = childEnv;
44315
+ }
43958
44316
  async startThread(params) {
43959
44317
  const { permissionMode } = permissionModeFor2(params.sandbox, params.approvalPolicy);
43960
44318
  const child = new AgyProcess({
43961
44319
  mode: { kind: "new" },
43962
44320
  permissionMode,
43963
44321
  model: params.model,
43964
- cwd: params.cwd
44322
+ cwd: params.cwd,
44323
+ ...this.childEnv === void 0 ? {} : { env: this.childEnv }
43965
44324
  });
43966
44325
  this.processes.set(child.sessionId, child);
43967
44326
  this.mappers.set(child.sessionId, new AgyBridgeEventMapper());
44327
+ if (params.baseInstructions && params.baseInstructions.trim().length > 0) {
44328
+ this.baseInstructions.set(child.sessionId, params.baseInstructions.trim());
44329
+ }
43968
44330
  return { threadId: child.sessionId };
43969
44331
  }
43970
44332
  async resumeThread(threadId, params) {
@@ -43978,16 +44340,31 @@ var AgyAgentHandle = class {
43978
44340
  mode: { kind: "resume", resumeId: threadId },
43979
44341
  permissionMode,
43980
44342
  model: params.model,
43981
- cwd: params.cwd
44343
+ cwd: params.cwd,
44344
+ ...this.childEnv === void 0 ? {} : { env: this.childEnv }
43982
44345
  });
43983
44346
  this.processes.set(threadId, child);
43984
44347
  this.mappers.set(threadId, new AgyBridgeEventMapper());
44348
+ if (params.baseInstructions && params.baseInstructions.trim().length > 0) {
44349
+ this.baseInstructions.set(threadId, params.baseInstructions.trim());
44350
+ }
43985
44351
  return { threadId: child.sessionId };
43986
44352
  }
43987
44353
  async startTurn(threadId, input) {
43988
44354
  const child = this.requireProcess(threadId);
43989
44355
  const text = input.filter((block) => block.type === "text").map((block) => block.text).join("\n");
43990
- child.sendTurn(text);
44356
+ const inst = this.baseInstructions.get(threadId);
44357
+ if (inst) {
44358
+ const composed = `[SYSTEM INSTRUCTIONS - MRRLIN DIRECTOR]
44359
+ ${inst}
44360
+
44361
+ ---
44362
+ [OPERATOR REQUEST]
44363
+ ${text}`;
44364
+ child.sendTurn(composed);
44365
+ } else {
44366
+ child.sendTurn(text);
44367
+ }
43991
44368
  return { turnId: void 0 };
43992
44369
  }
43993
44370
  async interruptTurn(threadId) {
@@ -43997,6 +44374,7 @@ var AgyAgentHandle = class {
43997
44374
  });
43998
44375
  this.processes.delete(threadId);
43999
44376
  this.mappers.delete(threadId);
44377
+ this.baseInstructions.delete(threadId);
44000
44378
  }
44001
44379
  }
44002
44380
  onEvent(threadId, handler) {
@@ -44019,6 +44397,14 @@ var AgyAgentHandle = class {
44019
44397
  async readRateLimits() {
44020
44398
  return null;
44021
44399
  }
44400
+ disposeAll() {
44401
+ for (const child of this.processes.values())
44402
+ void child.dispose().catch(() => {
44403
+ });
44404
+ this.processes.clear();
44405
+ this.mappers.clear();
44406
+ this.baseInstructions.clear();
44407
+ }
44022
44408
  requireProcess(threadId) {
44023
44409
  const child = this.processes.get(threadId);
44024
44410
  if (!child)
@@ -44191,7 +44577,7 @@ ${input.prompt}` : input.prompt;
44191
44577
  args.push("-m", input.model);
44192
44578
  }
44193
44579
  args.push(fullPrompt);
44194
- return new Promise((resolve) => {
44580
+ return new Promise((resolve2) => {
44195
44581
  let settled = false;
44196
44582
  const timerRef = { id: void 0 };
44197
44583
  const finish = (result) => {
@@ -44200,7 +44586,7 @@ ${input.prompt}` : input.prompt;
44200
44586
  settled = true;
44201
44587
  if (timerRef.id)
44202
44588
  clearTimeout(timerRef.id);
44203
- resolve(result);
44589
+ resolve2(result);
44204
44590
  };
44205
44591
  const options = {
44206
44592
  stdio: ["ignore", "pipe", "pipe"],
@@ -44278,7 +44664,7 @@ function runClaudeExec(input, deps) {
44278
44664
  args.push("--model", input.model);
44279
44665
  }
44280
44666
  args.push(input.prompt);
44281
- return new Promise((resolve) => {
44667
+ return new Promise((resolve2) => {
44282
44668
  let settled = false;
44283
44669
  const timerRef = { id: void 0 };
44284
44670
  const finish = (result) => {
@@ -44287,7 +44673,7 @@ function runClaudeExec(input, deps) {
44287
44673
  settled = true;
44288
44674
  if (timerRef.id)
44289
44675
  clearTimeout(timerRef.id);
44290
- resolve(result);
44676
+ resolve2(result);
44291
44677
  };
44292
44678
  const options = {
44293
44679
  stdio: ["ignore", "pipe", "pipe"],
@@ -44385,10 +44771,21 @@ function runAgyExec(input, deps) {
44385
44771
  prompt = trimmedInstructions + "\n\n" + prompt;
44386
44772
  }
44387
44773
  if (input.model) {
44388
- args.push("--model", input.model);
44774
+ let targetModel = input.model;
44775
+ if (targetModel === "gemini-3.1-pro-high" || targetModel === "gemini-3.7-flash-high") {
44776
+ targetModel = "gemini-3.8-flash-high";
44777
+ } else if (targetModel === "gemini-3.1-pro-low" || targetModel === "gemini-3.7-flash-low") {
44778
+ targetModel = "gemini-3.8-flash-low";
44779
+ }
44780
+ const known = AGY_MODEL_CATALOG.find((m) => m.id === targetModel || m.displayName.toLowerCase() === targetModel.toLowerCase());
44781
+ if (known && !known.id.startsWith("gemini-3.1")) {
44782
+ args.push("--model", known.id);
44783
+ } else {
44784
+ args.push("--model", "gemini-3.8-flash-high");
44785
+ }
44389
44786
  }
44390
44787
  args.push(`--print=${prompt}`);
44391
- return new Promise((resolve) => {
44788
+ return new Promise((resolve2) => {
44392
44789
  let settled = false;
44393
44790
  const timerRef = { id: void 0 };
44394
44791
  const finish = (result) => {
@@ -44397,7 +44794,7 @@ function runAgyExec(input, deps) {
44397
44794
  settled = true;
44398
44795
  if (timerRef.id)
44399
44796
  clearTimeout(timerRef.id);
44400
- resolve(result);
44797
+ resolve2(result);
44401
44798
  };
44402
44799
  const options = {
44403
44800
  stdio: ["ignore", "pipe", "pipe"],
@@ -44468,7 +44865,9 @@ function extractOneShotResult2(stdout) {
44468
44865
 
44469
44866
  // ../../packages/agent-client/dist/run-agent-exec.js
44470
44867
  function runAgentExec(input, deps, persistedProvider) {
44471
- const provider = resolveProvider(process.env, persistedProvider);
44868
+ const explicitProvider = persistedProvider ?? process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase();
44869
+ const envToUse = explicitProvider ? { ...process.env, MRRLIN_AGENT_PROVIDER: explicitProvider } : process.env;
44870
+ const provider = resolveProvider(envToUse, explicitProvider);
44472
44871
  if (provider.id === "codex") {
44473
44872
  return runCodexExec(input, deps);
44474
44873
  }
@@ -44484,7 +44883,7 @@ function runAgentExec(input, deps, persistedProvider) {
44484
44883
  // ../../packages/agent-client/dist/resolved-seat.js
44485
44884
  var CODEX_DEFAULT_MODEL_CONSTANT = "gpt-5.6-terra";
44486
44885
  var CLAUDE_DEFAULT_MODEL_CONSTANT = "sonnet";
44487
- var AGY_DEFAULT_MODEL_CONSTANT = "gemini-3.1-pro-high";
44886
+ var AGY_DEFAULT_MODEL_CONSTANT = "gemini-3.8-flash-high";
44488
44887
  var CODEX_DEFAULT_MODEL_ENV = "MRRLIN_CODEX_DEFAULT_MODEL";
44489
44888
  var CODEX_STRONG_MODEL_ENV = "MRRLIN_CODEX_STRONG_MODEL";
44490
44889
  var CLAUDE_DEFAULT_MODEL_ENV = "MRRLIN_CLAUDE_DEFAULT_MODEL";
@@ -44635,23 +45034,32 @@ async function resolveGatedTaskId(client, projectSlug, page) {
44635
45034
  function domainScopedReviewHash2(rawHash, domain2) {
44636
45035
  return domainScopedReviewHash(rawHash, domain2);
44637
45036
  }
44638
- async function resolveSpecDomain(client, projectSlug, page) {
45037
+ async function resolveSpecDomains(client, projectSlug, page) {
44639
45038
  try {
44640
45039
  const taskId = await resolveGatedTaskId(client, projectSlug, page);
44641
45040
  if (taskId !== null) {
44642
45041
  const task = await client.getTask(projectSlug, taskId);
44643
- return normalizeDomain(task?.domains ?? task?.domain);
45042
+ return normalizeDomainArray(task?.domains ?? task?.domain);
44644
45043
  }
44645
45044
  const plans = await client.listPlans(projectSlug);
44646
45045
  const plan = plans.find((p) => p.specWikiPageId === page.id);
44647
- const planDomains = plan?.domains ?? plan?.domain;
44648
- if (planDomains != null) return normalizeDomain(planDomains);
45046
+ if (plan) {
45047
+ const rawPlanDomains = plan?.domains ?? plan?.domain;
45048
+ const substantivePlanDomains = (Array.isArray(rawPlanDomains) ? rawPlanDomains : rawPlanDomains != null ? [rawPlanDomains] : []).map(String).map((d) => d.trim().toLowerCase()).filter(Boolean);
45049
+ if (substantivePlanDomains.length > 0) {
45050
+ return normalizeDomainArray(substantivePlanDomains);
45051
+ }
45052
+ }
44649
45053
  const project = await client.getProject(projectSlug);
44650
- return normalizeDomain(project?.defaultDomains ?? project?.defaultDomain);
45054
+ return normalizeDomainArray(project?.defaultDomains ?? project?.defaultDomain);
44651
45055
  } catch {
44652
- return "code";
45056
+ return ["code"];
44653
45057
  }
44654
45058
  }
45059
+ async function resolveSpecDomain(client, projectSlug, page) {
45060
+ const domains = await resolveSpecDomains(client, projectSlug, page);
45061
+ return normalizeDomain(domains);
45062
+ }
44655
45063
 
44656
45064
  // src/consensus/findings.ts
44657
45065
  function shouldCurateFindings(history) {
@@ -44726,14 +45134,14 @@ var DIFF_MAX_BYTES = 2e5;
44726
45134
  var FASTGATE_OUTPUT_MAX_BYTES = 16e3;
44727
45135
  var GIT_TIMEOUT_MS = 6e4;
44728
45136
  var FASTGATE_TIMEOUT_MS = 3e5;
44729
- var defaultRunCmd = (cmd, args, opts) => new Promise((resolve) => {
45137
+ var defaultRunCmd = (cmd, args, opts) => new Promise((resolve2) => {
44730
45138
  let settled = false;
44731
45139
  const timerRef = { id: void 0 };
44732
45140
  const finish = (result) => {
44733
45141
  if (settled) return;
44734
45142
  settled = true;
44735
45143
  if (timerRef.id) clearTimeout(timerRef.id);
44736
- resolve(result);
45144
+ resolve2(result);
44737
45145
  };
44738
45146
  let child;
44739
45147
  try {
@@ -45035,7 +45443,13 @@ function gateCallRecorder(args) {
45035
45443
  }
45036
45444
  function buildSpecGateDeps(args) {
45037
45445
  const { client, projectSlug, specPageId, codexExecutable, extraHeadCount = 0, seat, stateDir } = args;
45038
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "spec", provider: seat?.id ?? null });
45446
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45447
+ const recordCost = gateCallRecorder({
45448
+ stateDir,
45449
+ projectSlug,
45450
+ gate: "spec",
45451
+ provider: consensusProvider ?? seat?.id ?? null
45452
+ });
45039
45453
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45040
45454
  const maxHeadCalls = 2 * rounds * (MAX_DOMAIN_HEADS + extraHeadCount + 2);
45041
45455
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
@@ -45049,8 +45463,24 @@ function buildSpecGateDeps(args) {
45049
45463
  }
45050
45464
  const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
45051
45465
  if (taskId === null) {
45466
+ const plans = await client.listPlans(projectSlug);
45467
+ const plan = plans.find((p) => p.specWikiPageId === specPageId);
45468
+ if (plan) {
45469
+ const project2 = await client.getProject(projectSlug);
45470
+ const rawPlanDomains = plan?.domains ?? plan?.domain;
45471
+ const substantivePlanDomains = (Array.isArray(rawPlanDomains) ? rawPlanDomains : rawPlanDomains != null ? [rawPlanDomains] : []).map(String).map((d) => d.trim().toLowerCase()).filter(Boolean);
45472
+ const planDomains = substantivePlanDomains.length > 0 ? substantivePlanDomains : project2?.defaultDomains ?? (project2?.defaultDomain ? [project2.defaultDomain] : void 0);
45473
+ return {
45474
+ taskId: null,
45475
+ planId: plan.id,
45476
+ seed: null,
45477
+ title: plan.title ?? plan.name ?? null,
45478
+ domains: planDomains,
45479
+ notes: null
45480
+ };
45481
+ }
45052
45482
  const project = await client.getProject(projectSlug);
45053
- return { taskId: null, seed: null, title: null, domains: project.defaultDomains, notes: null };
45483
+ return { taskId: null, planId: null, seed: null, title: null, domains: project.defaultDomains, notes: null };
45054
45484
  }
45055
45485
  const task = await client.getTask(projectSlug, taskId);
45056
45486
  if (!task) {
@@ -45067,10 +45497,7 @@ function buildSpecGateDeps(args) {
45067
45497
  if (answered && answered.answer) {
45068
45498
  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
45499
  } 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
- }
45500
+ resolvedSeed = null;
45074
45501
  }
45075
45502
  }
45076
45503
  const adjudicated = selectAdjudicated(taskInboxItems);
@@ -45132,7 +45559,9 @@ ${artifact}`,
45132
45559
  timeoutMs,
45133
45560
  sandbox: "read-only",
45134
45561
  codexExecutable
45135
- }
45562
+ },
45563
+ void 0,
45564
+ consensusProvider
45136
45565
  );
45137
45566
  recordCost("head", personaName, null, startedAt, result);
45138
45567
  return result;
@@ -45149,8 +45578,10 @@ ${artifact}`,
45149
45578
  codexExecutable,
45150
45579
  // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45151
45580
  // default model.
45152
- model: seat?.model ?? seat?.models?.strong
45153
- }
45581
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45582
+ },
45583
+ void 0,
45584
+ consensusProvider
45154
45585
  );
45155
45586
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45156
45587
  return result;
@@ -45173,7 +45604,9 @@ ${artifact}`,
45173
45604
  timeoutMs,
45174
45605
  sandbox: "read-only",
45175
45606
  codexExecutable
45176
- }
45607
+ },
45608
+ void 0,
45609
+ consensusProvider
45177
45610
  );
45178
45611
  recordCost("reviser", null, null, startedAt, result);
45179
45612
  return result;
@@ -45206,6 +45639,28 @@ ${artifact}`,
45206
45639
  return false;
45207
45640
  }
45208
45641
  },
45642
+ async finalizeSpecReviewCas(input) {
45643
+ const history = input.review.history;
45644
+ const highlights = await curateFindings(Array.isArray(history) ? history : [], {
45645
+ runCodexExec: async (execInput) => {
45646
+ const startedAt = Date.now();
45647
+ const result = await runAgentExec(execInput, void 0, consensusProvider);
45648
+ recordCost("findings", "findings-writer", null, startedAt, result);
45649
+ return result;
45650
+ },
45651
+ loadPersona,
45652
+ cwd: sterileCwd(),
45653
+ timeoutMs,
45654
+ codexExecutable
45655
+ });
45656
+ return client.finalizeSpecReviewCas(projectSlug, specPageId, {
45657
+ ...input,
45658
+ review: {
45659
+ ...input.review,
45660
+ ...highlights ? { highlights } : {}
45661
+ }
45662
+ });
45663
+ },
45209
45664
  // Record the consensus-review outcome keyed by content hash via the D2b
45210
45665
  // endpoint. `record` is typed `unknown` (it crosses the SpecGateDeps
45211
45666
  // boundary) so we narrow it defensively:
@@ -45228,7 +45683,7 @@ ${artifact}`,
45228
45683
  // call the cost record silently omits.
45229
45684
  runCodexExec: async (input) => {
45230
45685
  const startedAt = Date.now();
45231
- const result = await runAgentExec(input);
45686
+ const result = await runAgentExec(input, void 0, consensusProvider);
45232
45687
  recordCost("findings", "findings-writer", null, startedAt, result);
45233
45688
  return result;
45234
45689
  },
@@ -45257,7 +45712,13 @@ ${artifact}`,
45257
45712
  }
45258
45713
  function buildCodeGateDeps(args) {
45259
45714
  const { client, projectSlug, baseRef, cwd, codexExecutable, extraHeads, seat, stateDir } = args;
45260
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "code", provider: seat?.id ?? null });
45715
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45716
+ const recordCost = gateCallRecorder({
45717
+ stateDir,
45718
+ projectSlug,
45719
+ gate: "code",
45720
+ provider: consensusProvider ?? seat?.id ?? null
45721
+ });
45261
45722
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45262
45723
  const heads = ["code-reviewer", ...extraHeads ?? []];
45263
45724
  const maxHeadCalls = 2 * rounds * (heads.length + 2);
@@ -45285,60 +45746,72 @@ function buildCodeGateDeps(args) {
45285
45746
  async runHead(personaName, artifact, dismissed) {
45286
45747
  const persona = loadPersona(personaName);
45287
45748
  const startedAt = Date.now();
45288
- const result = await runAgentExec({
45289
- prompt: `Review this code diff.
45749
+ const result = await runAgentExec(
45750
+ {
45751
+ prompt: `Review this code diff.
45290
45752
 
45291
45753
  ${dismissed}
45292
45754
 
45293
45755
  --- DIFF ---
45294
45756
  ${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
- });
45757
+ developerInstructions: persona,
45758
+ // Read-only review of a diff that is entirely in the prompt — sterile, per sterile-cwd.ts.
45759
+ // This factory is the ONE that stays mixed: runReviser below keeps the real `cwd` because
45760
+ // it edits the branch, and the git helpers keep it because they are not provider calls.
45761
+ cwd: sterileCwd(),
45762
+ timeoutMs: headTimeoutMs,
45763
+ sandbox: "read-only",
45764
+ codexExecutable
45765
+ },
45766
+ void 0,
45767
+ consensusProvider
45768
+ );
45304
45769
  recordCost("head", personaName, null, startedAt, result);
45305
45770
  return result;
45306
45771
  },
45307
45772
  async runArbiter(prompt) {
45308
45773
  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
- });
45774
+ const result = await runAgentExec(
45775
+ {
45776
+ prompt,
45777
+ developerInstructions: loadPersona("arbiter"),
45778
+ cwd: sterileCwd(),
45779
+ timeoutMs: headTimeoutMs,
45780
+ sandbox: "read-only",
45781
+ codexExecutable,
45782
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45783
+ // default model.
45784
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45785
+ },
45786
+ void 0,
45787
+ consensusProvider
45788
+ );
45320
45789
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45321
45790
  return result;
45322
45791
  },
45323
45792
  async runReviser(artifact, accepted) {
45324
45793
  const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
45325
45794
  const startedAt = Date.now();
45326
- const result = await runAgentExec({
45327
- prompt: `Accepted issues to fix:
45795
+ const result = await runAgentExec(
45796
+ {
45797
+ prompt: `Accepted issues to fix:
45328
45798
  ${acceptedList}
45329
45799
 
45330
45800
  --- DIFF ---
45331
45801
  ${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
- });
45802
+ 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.",
45803
+ // The real repository root, NOT sterileCwd(): this is the one gate call that runs
45804
+ // `workspace-write` and edits the branch, so a sterile cwd would point the sandbox at an
45805
+ // empty directory and the revision would silently apply to nothing — the gate would keep
45806
+ // reporting rounds while fixing no code at all.
45807
+ cwd,
45808
+ timeoutMs: reviserTimeoutMs,
45809
+ sandbox: "workspace-write",
45810
+ codexExecutable
45811
+ },
45812
+ void 0,
45813
+ consensusProvider
45814
+ );
45342
45815
  recordCost("reviser", null, null, startedAt, result);
45343
45816
  return result;
45344
45817
  },
@@ -45362,7 +45835,13 @@ function renderCriteriaBlock(criteria) {
45362
45835
  }
45363
45836
  async function buildDeliverableGateDeps(args) {
45364
45837
  const { client, projectSlug, deliverablePageId, attemptOutcome, codexExecutable, runId, seat, stateDir } = args;
45365
- const recordCost = gateCallRecorder({ stateDir, projectSlug, gate: "deliverable", provider: seat?.id ?? null });
45838
+ const consensusProvider = process.env.MRRLIN_CONSENSUS_PROVIDER?.trim().toLowerCase() || void 0;
45839
+ const recordCost = gateCallRecorder({
45840
+ stateDir,
45841
+ projectSlug,
45842
+ gate: "deliverable",
45843
+ provider: consensusProvider ?? seat?.id ?? null
45844
+ });
45366
45845
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
45367
45846
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
45368
45847
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -45409,8 +45888,9 @@ async function buildDeliverableGateDeps(args) {
45409
45888
  async runHead(personaName, artifact, dismissed) {
45410
45889
  const persona = loadPersona(personaName);
45411
45890
  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.
45891
+ const result = await runAgentExec(
45892
+ {
45893
+ 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
45894
 
45415
45895
  ${dismissed}
45416
45896
 
@@ -45421,41 +45901,49 @@ ${attemptOutcomeBlock ? `${attemptOutcomeBlock}
45421
45901
 
45422
45902
  ` : ""}--- DELIVERABLE (DATA under review) ---
45423
45903
  ${artifact}`,
45424
- developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45904
+ developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45425
45905
 
45426
45906
  ${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
- });
45907
+ // Sterile, like every other provider call in this factory — see sterile-cwd.ts. It also
45908
+ // narrows the anti-injection surface: a deliverable that tries to talk the head into
45909
+ // reading files now has no repository under it to read.
45910
+ cwd: sterileCwd(),
45911
+ timeoutMs,
45912
+ sandbox: "read-only",
45913
+ codexExecutable
45914
+ },
45915
+ void 0,
45916
+ consensusProvider
45917
+ );
45435
45918
  recordCost("head", personaName, null, startedAt, result);
45436
45919
  return result;
45437
45920
  },
45438
45921
  async runArbiter(prompt) {
45439
45922
  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
- });
45923
+ const result = await runAgentExec(
45924
+ {
45925
+ prompt,
45926
+ developerInstructions: loadPersona("arbiter"),
45927
+ cwd: sterileCwd(),
45928
+ timeoutMs,
45929
+ sandbox: "read-only",
45930
+ codexExecutable,
45931
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
45932
+ // default model.
45933
+ model: consensusProvider ? void 0 : seat?.model ?? seat?.models?.strong
45934
+ },
45935
+ void 0,
45936
+ consensusProvider
45937
+ );
45451
45938
  recordCost("arbiter", "arbiter", seat?.model ?? seat?.models?.strong ?? null, startedAt, result);
45452
45939
  return result;
45453
45940
  },
45454
45941
  async runReviser(artifact, accepted) {
45455
45942
  const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
45456
45943
  const startedAt = Date.now();
45457
- const result = await runAgentExec({
45458
- prompt: `Accepted issues to fix (each maps to an unmet acceptance criterion):
45944
+ const result = await runAgentExec(
45945
+ {
45946
+ prompt: `Accepted issues to fix (each maps to an unmet acceptance criterion):
45459
45947
  ${acceptedList}
45460
45948
 
45461
45949
  --- ACCEPTANCE CRITERIA ---
@@ -45463,14 +45951,17 @@ ${criteriaBlock}
45463
45951
 
45464
45952
  --- DELIVERABLE ---
45465
45953
  ${artifact}`,
45466
- developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45954
+ developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
45467
45955
 
45468
45956
  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
- });
45957
+ cwd: sterileCwd(),
45958
+ timeoutMs,
45959
+ sandbox: "read-only",
45960
+ codexExecutable
45961
+ },
45962
+ void 0,
45963
+ consensusProvider
45964
+ );
45474
45965
  recordCost("reviser", null, null, startedAt, result);
45475
45966
  return result;
45476
45967
  },
@@ -45501,7 +45992,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
45501
45992
  const rounds2 = typeof r["rounds"] === "number" ? r["rounds"] : 0;
45502
45993
  const history = r["history"];
45503
45994
  const highlights = await curateFindings(Array.isArray(history) ? history : [], {
45504
- runCodexExec: runAgentExec,
45995
+ runCodexExec: (execInput) => runAgentExec(execInput, void 0, consensusProvider),
45505
45996
  loadPersona,
45506
45997
  cwd: sterileCwd(),
45507
45998
  timeoutMs,
@@ -45568,14 +46059,14 @@ function errMessage3(err) {
45568
46059
  return err instanceof Error ? err.message : String(err);
45569
46060
  }
45570
46061
  function runExternalAgentsStatus(bin, args, timeoutMs, spawn6) {
45571
- return new Promise((resolve) => {
46062
+ return new Promise((resolve2) => {
45572
46063
  let settled = false;
45573
46064
  const timerRef = { id: void 0 };
45574
46065
  const finish = (r) => {
45575
46066
  if (settled) return;
45576
46067
  settled = true;
45577
46068
  if (timerRef.id) clearTimeout(timerRef.id);
45578
- resolve(r);
46069
+ resolve2(r);
45579
46070
  };
45580
46071
  const options = { stdio: ["ignore", "pipe", "pipe"], env: process.env, shell: false };
45581
46072
  let child;
@@ -45655,14 +46146,14 @@ function errMessage4(err) {
45655
46146
  return err instanceof Error ? err.message : String(err);
45656
46147
  }
45657
46148
  function runExternalAgents(bin, args, label, timeoutMs, spawn6) {
45658
- return new Promise((resolve) => {
46149
+ return new Promise((resolve2) => {
45659
46150
  let settled = false;
45660
46151
  const timerRef = { id: void 0 };
45661
46152
  const finish = (r) => {
45662
46153
  if (settled) return;
45663
46154
  settled = true;
45664
46155
  if (timerRef.id) clearTimeout(timerRef.id);
45665
- resolve(r);
46156
+ resolve2(r);
45666
46157
  };
45667
46158
  const options = {
45668
46159
  stdio: ["ignore", "pipe", "pipe"],
@@ -48161,7 +48652,7 @@ var Protocol = class {
48161
48652
  return;
48162
48653
  }
48163
48654
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
48164
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
48655
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
48165
48656
  options?.signal?.throwIfAborted();
48166
48657
  }
48167
48658
  } catch (error51) {
@@ -48178,7 +48669,7 @@ var Protocol = class {
48178
48669
  */
48179
48670
  request(request, resultSchema, options) {
48180
48671
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
48181
- return new Promise((resolve, reject) => {
48672
+ return new Promise((resolve2, reject) => {
48182
48673
  const earlyReject = (error51) => {
48183
48674
  reject(error51);
48184
48675
  };
@@ -48256,7 +48747,7 @@ var Protocol = class {
48256
48747
  if (!parseResult.success) {
48257
48748
  reject(parseResult.error);
48258
48749
  } else {
48259
- resolve(parseResult.data);
48750
+ resolve2(parseResult.data);
48260
48751
  }
48261
48752
  } catch (error51) {
48262
48753
  reject(error51);
@@ -48517,12 +49008,12 @@ var Protocol = class {
48517
49008
  }
48518
49009
  } catch {
48519
49010
  }
48520
- return new Promise((resolve, reject) => {
49011
+ return new Promise((resolve2, reject) => {
48521
49012
  if (signal.aborted) {
48522
49013
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
48523
49014
  return;
48524
49015
  }
48525
- const timeoutId = setTimeout(resolve, interval);
49016
+ const timeoutId = setTimeout(resolve2, interval);
48526
49017
  signal.addEventListener("abort", () => {
48527
49018
  clearTimeout(timeoutId);
48528
49019
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -49891,22 +50382,22 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
49891
50382
  }
49892
50383
  const nonInteractiveFlow = !provider.redirectUrl;
49893
50384
  if (authorizationCode !== void 0 || nonInteractiveFlow) {
49894
- const tokens2 = await fetchToken(provider, authorizationServerUrl, {
50385
+ const tokens3 = await fetchToken(provider, authorizationServerUrl, {
49895
50386
  metadata,
49896
50387
  resource,
49897
50388
  authorizationCode,
49898
50389
  fetchFn
49899
50390
  });
49900
- await provider.saveTokens(tokens2);
50391
+ await provider.saveTokens(tokens3);
49901
50392
  return "AUTHORIZED";
49902
50393
  }
49903
- const tokens = await provider.tokens();
49904
- if (tokens?.refresh_token) {
50394
+ const tokens2 = await provider.tokens();
50395
+ if (tokens2?.refresh_token) {
49905
50396
  try {
49906
50397
  const newTokens = await refreshAuthorization(authorizationServerUrl, {
49907
50398
  metadata,
49908
50399
  clientInformation,
49909
- refreshToken: tokens.refresh_token,
50400
+ refreshToken: tokens2.refresh_token,
49910
50401
  resource,
49911
50402
  addClientAuthentication: provider.addClientAuthentication,
49912
50403
  fetchFn
@@ -50208,7 +50699,7 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
50208
50699
  grant_type: "refresh_token",
50209
50700
  refresh_token: refreshToken
50210
50701
  });
50211
- const tokens = await executeTokenRequest(authorizationServerUrl, {
50702
+ const tokens2 = await executeTokenRequest(authorizationServerUrl, {
50212
50703
  metadata,
50213
50704
  tokenRequestParams,
50214
50705
  clientInformation,
@@ -50216,7 +50707,7 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
50216
50707
  resource,
50217
50708
  fetchFn
50218
50709
  });
50219
- return { refresh_token: refreshToken, ...tokens };
50710
+ return { refresh_token: refreshToken, ...tokens2 };
50220
50711
  }
50221
50712
  async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
50222
50713
  const scope = provider.clientMetadata.scope;
@@ -50521,9 +51012,9 @@ var StreamableHTTPClientTransport = class {
50521
51012
  async _commonHeaders() {
50522
51013
  const headers = {};
50523
51014
  if (this._authProvider) {
50524
- const tokens = await this._authProvider.tokens();
50525
- if (tokens) {
50526
- headers["Authorization"] = `Bearer ${tokens.access_token}`;
51015
+ const tokens2 = await this._authProvider.tokens();
51016
+ if (tokens2) {
51017
+ headers["Authorization"] = `Bearer ${tokens2.access_token}`;
50527
51018
  }
50528
51019
  }
50529
51020
  if (this._sessionId) {
@@ -50958,7 +51449,7 @@ var StdioClientTransport = class {
50958
51449
  if (this._process) {
50959
51450
  throw new Error("StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.");
50960
51451
  }
50961
- return new Promise((resolve, reject) => {
51452
+ return new Promise((resolve2, reject) => {
50962
51453
  this._process = (0, import_cross_spawn.default)(this._serverParams.command, this._serverParams.args ?? [], {
50963
51454
  // merge default env with server env because mcp server needs some env vars
50964
51455
  env: {
@@ -50975,7 +51466,7 @@ var StdioClientTransport = class {
50975
51466
  this.onerror?.(error51);
50976
51467
  });
50977
51468
  this._process.on("spawn", () => {
50978
- resolve();
51469
+ resolve2();
50979
51470
  });
50980
51471
  this._process.on("close", (_code) => {
50981
51472
  this._process = void 0;
@@ -51040,22 +51531,22 @@ var StdioClientTransport = class {
51040
51531
  if (this._process) {
51041
51532
  const processToClose = this._process;
51042
51533
  this._process = void 0;
51043
- const closePromise = new Promise((resolve) => {
51534
+ const closePromise = new Promise((resolve2) => {
51044
51535
  processToClose.once("close", () => {
51045
- resolve();
51536
+ resolve2();
51046
51537
  });
51047
51538
  });
51048
51539
  try {
51049
51540
  processToClose.stdin?.end();
51050
51541
  } catch {
51051
51542
  }
51052
- await Promise.race([closePromise, new Promise((resolve) => setTimeout(resolve, 2e3).unref())]);
51543
+ await Promise.race([closePromise, new Promise((resolve2) => setTimeout(resolve2, 2e3).unref())]);
51053
51544
  if (processToClose.exitCode === null) {
51054
51545
  try {
51055
51546
  processToClose.kill("SIGTERM");
51056
51547
  } catch {
51057
51548
  }
51058
- await Promise.race([closePromise, new Promise((resolve) => setTimeout(resolve, 2e3).unref())]);
51549
+ await Promise.race([closePromise, new Promise((resolve2) => setTimeout(resolve2, 2e3).unref())]);
51059
51550
  }
51060
51551
  if (processToClose.exitCode === null) {
51061
51552
  try {
@@ -51067,15 +51558,15 @@ var StdioClientTransport = class {
51067
51558
  this._readBuffer.clear();
51068
51559
  }
51069
51560
  send(message) {
51070
- return new Promise((resolve) => {
51561
+ return new Promise((resolve2) => {
51071
51562
  if (!this._process?.stdin) {
51072
51563
  throw new Error("Not connected");
51073
51564
  }
51074
51565
  const json2 = serializeMessage(message);
51075
51566
  if (this._process.stdin.write(json2)) {
51076
- resolve();
51567
+ resolve2();
51077
51568
  } else {
51078
- this._process.stdin.once("drain", resolve);
51569
+ this._process.stdin.once("drain", resolve2);
51079
51570
  }
51080
51571
  });
51081
51572
  }
@@ -52284,13 +52775,87 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52284
52775
  suppressedAsAdjudicated.push({ rawQuestion: clarification.rawQuestion, coveredByAnswer: covering.answer });
52285
52776
  return false;
52286
52777
  });
52778
+ let planId = null;
52779
+ if (!taskId) {
52780
+ const plans = await ctx.client.listPlans(input.projectSlug);
52781
+ planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
52782
+ }
52783
+ const evaluatedContentHash = result.evaluatedSnapshot?.contentHash ?? page?.contentHash;
52784
+ const isSeedAbsence = operatorClarifications.some(
52785
+ (c) => c.rawQuestion === SEED_ABSENCE_QUESTION || c.rawQuestion.includes(SEED_ABSENCE_MARKER)
52786
+ );
52787
+ if (!isSeedAbsence && operatorClarifications.length > 0 && evaluatedContentHash) {
52788
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
52789
+ id: input.pageId,
52790
+ taskId
52791
+ });
52792
+ const suppressionEvidence = (result.droppedByCap?.length ?? 0) > 0 || suppressedAsAdjudicated.length > 0 ? {
52793
+ ...result.droppedByCap?.length ? { droppedByCap: result.droppedByCap } : {},
52794
+ ...suppressedAsAdjudicated.length ? { suppressedAsAdjudicated } : {}
52795
+ } : void 0;
52796
+ let ownership;
52797
+ if (result.evaluatedOwnership) {
52798
+ ownership = result.evaluatedOwnership;
52799
+ } else if (taskId !== null) {
52800
+ const task = await ctx.client.getTask(input.projectSlug, taskId).catch(() => null);
52801
+ ownership = { kind: "task", taskId, domains: task?.domains ?? [domain2] };
52802
+ } else if (planId !== null) {
52803
+ const plans = await ctx.client.listPlans(input.projectSlug).catch(() => []);
52804
+ const plan = plans.find((p) => p.id === planId);
52805
+ ownership = { kind: "plan", planId, domains: plan?.domains ?? ["code"] };
52806
+ } else {
52807
+ ownership = { kind: "project", domains: ["code"] };
52808
+ }
52809
+ if (ctx.client.finalizeSpecReviewCas) {
52810
+ const finalizeRes = await ctx.client.finalizeSpecReviewCas(input.projectSlug, input.pageId, {
52811
+ expectedContentHash: evaluatedContentHash,
52812
+ ownership,
52813
+ review: {
52814
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
52815
+ rounds: result.rounds !== void 0 ? result.rounds : result.history?.length ? result.history.length : 0,
52816
+ ...result.history !== void 0 ? { history: result.history } : {},
52817
+ ...result.highlights !== void 0 ? { highlights: Array.isArray(result.highlights) ? { findings: result.highlights } : result.highlights } : {},
52818
+ ...suppressionEvidence ? { suppressionEvidence } : {}
52819
+ },
52820
+ specReady: false
52821
+ });
52822
+ if (finalizeRes.status === "superseded") {
52823
+ return {
52824
+ kind: "superseded",
52825
+ finalReviewedHash: evaluatedContentHash,
52826
+ error: "Spec content changed during review",
52827
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52828
+ };
52829
+ }
52830
+ if (finalizeRes.status !== "applied") {
52831
+ return {
52832
+ kind: "failed",
52833
+ failureReason: "consensus_blocked",
52834
+ error: finalizeRes.reason ?? "cas_rejected",
52835
+ finalReviewedHash: evaluatedContentHash,
52836
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52837
+ };
52838
+ }
52839
+ } else if (ctx.client.recordSpecConsensusReview) {
52840
+ try {
52841
+ await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
52842
+ contentHash: domainScopedReviewHash2(evaluatedContentHash, domain2),
52843
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
52844
+ rounds: result.rounds !== void 0 ? result.rounds : result.history?.length ? result.history.length : 0,
52845
+ ...result.history !== void 0 ? { history: result.history } : {},
52846
+ ...result.highlights !== void 0 ? { highlights: Array.isArray(result.highlights) ? { findings: result.highlights } : result.highlights } : {},
52847
+ ...suppressionEvidence ? { suppressionEvidence } : {}
52848
+ });
52849
+ } catch (error52) {
52850
+ process.stderr.write(
52851
+ `[mrrlin-mcp consensus] legacy clarification write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
52852
+ `
52853
+ );
52854
+ }
52855
+ }
52856
+ }
52287
52857
  let filedQuestionCount = 0;
52288
52858
  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
52859
  if (taskId) {
52295
52860
  const scopeId = taskId;
52296
52861
  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 +52872,11 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52307
52872
  questionOptions,
52308
52873
  ...groupId ? { groupId } : {}
52309
52874
  });
52310
- } catch {
52875
+ } catch (err) {
52876
+ const isConflict = err?.status === 409 || err?.statusCode === 409 || err?.code === "23505" || String(err?.message ?? "").includes("already exists") || String(err?.message ?? "").includes("duplicate key");
52877
+ if (!isConflict) {
52878
+ throw err;
52879
+ }
52311
52880
  }
52312
52881
  }
52313
52882
  filedQuestionCount = toFile.length;
@@ -52327,7 +52896,11 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52327
52896
  questionOptions,
52328
52897
  ...groupId ? { groupId } : {}
52329
52898
  });
52330
- } catch {
52899
+ } catch (err) {
52900
+ const isConflict = err?.status === 409 || err?.statusCode === 409 || err?.code === "23505" || String(err?.message ?? "").includes("already exists") || String(err?.message ?? "").includes("duplicate key");
52901
+ if (!isConflict) {
52902
+ throw err;
52903
+ }
52331
52904
  }
52332
52905
  }
52333
52906
  filedQuestionCount = toFile.length;
@@ -52356,40 +52929,15 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52356
52929
  } catch {
52357
52930
  }
52358
52931
  }
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
52932
  let error51;
52385
52933
  if (filedQuestionCount > 0) {
52386
52934
  error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
52387
52935
  } else if (diagnostics.length > 0 && operatorClarifications.length === 0) {
52388
52936
  const diagnosticKinds = new Set(diagnostics.map((d) => d.kind));
52389
52937
  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.";
52938
+ 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
52939
  } 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.";
52940
+ 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
52941
  }
52394
52942
  } else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
52395
52943
  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 +52949,13 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
52401
52949
  if (result.escalation) {
52402
52950
  error51 += ` (reason: ${result.escalation.diagnosis})`;
52403
52951
  }
52952
+ const isRelationshipDiagnostic = isSeedAbsence || result.diagnostics?.some((d) => d.kind === "spec_identity_mismatch") || result.escalation?.diagnosis === "spec_identity_mismatch";
52404
52953
  return {
52405
52954
  kind: "failed",
52406
52955
  failureReason: "consensus_blocked",
52407
52956
  error: error51,
52408
52957
  filedQuestionCount,
52958
+ finalReviewedHash: isRelationshipDiagnostic ? null : result.evaluatedSnapshot?.contentHash ?? null,
52409
52959
  ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
52410
52960
  };
52411
52961
  }
@@ -52419,18 +52969,30 @@ async function retireGateQuestionsOnConverge(ctx, input, resolvedTaskId) {
52419
52969
  const contentHash = page?.contentHash ?? null;
52420
52970
  const inboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId: resolvedTaskId });
52421
52971
  for (const item of inboxItems) {
52422
- if (item.origin !== "gate" || item.status !== "pending" || item.kind !== "question" || item.taskId != null && item.taskId !== resolvedTaskId) {
52972
+ if (item.status !== "pending") {
52973
+ continue;
52974
+ }
52975
+ if (item.taskId != null && item.taskId !== resolvedTaskId) {
52976
+ continue;
52977
+ }
52978
+ const isGateQuestion = (item.origin === "gate" || item.origin === "gate_block") && item.kind === "question";
52979
+ const specRefPattern = new RegExp(
52980
+ `Specs/${input.pageId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![0-9a-zA-Z_-])`
52981
+ );
52982
+ const isSpecHandoff = item.kind === "handoff" && item.id.startsWith(`II-SPEC-STOPPED-${input.pageId}-`) && specRefPattern.test(item.body);
52983
+ if (!isGateQuestion && !isSpecHandoff) {
52423
52984
  continue;
52424
52985
  }
52425
52986
  try {
52987
+ const verdict = isGateQuestion ? "rejected" : "acknowledged";
52426
52988
  await ctx.client.decideInboxItem(input.projectSlug, item.id, {
52427
- verdict: "rejected",
52989
+ verdict,
52428
52990
  decidedBy: "gate",
52429
52991
  reason: `Superseded by converged spec Specs/${input.pageId} (contentHash: ${contentHash ?? "unknown"})`
52430
52992
  });
52431
52993
  } catch (itemError) {
52432
52994
  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)}
52995
+ `[mrrlin-mcp consensus] gate retirement failed for ${input.projectSlug}/${input.pageId} item ${item.id}: ${itemError instanceof Error ? itemError.message : String(itemError)}
52434
52996
  `
52435
52997
  );
52436
52998
  }
@@ -52520,6 +53082,13 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
52520
53082
  return { kind: "retry", failureReason: "provider_unavailable", error: "Consensus unavailable (provider/quota).", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
52521
53083
  case "SPEC_CHANGED_DURING_REVIEW":
52522
53084
  return { kind: "retry", failureReason: "spec_changed", error: "Spec changed during review.", ...resolvedTaskId ? { taskId: resolvedTaskId } : {} };
53085
+ case "SUPERSEDED":
53086
+ return {
53087
+ kind: "superseded",
53088
+ reason: "Spec changed or ownership unlinked during review",
53089
+ error: "Spec changed or ownership unlinked during review",
53090
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
53091
+ };
52523
53092
  default:
52524
53093
  return {
52525
53094
  kind: "failed",
@@ -52530,6 +53099,14 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
52530
53099
  }
52531
53100
  }
52532
53101
  function onComplete(outcome, input) {
53102
+ if (outcome.kind === "superseded") {
53103
+ return {
53104
+ kind: "info",
53105
+ suppressInbox: true,
53106
+ body: outcome.reason ?? "Superseded",
53107
+ ...outcome.taskId ? { taskId: outcome.taskId } : {}
53108
+ };
53109
+ }
52533
53110
  const taskId = "taskId" in outcome ? outcome.taskId : void 0;
52534
53111
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
52535
53112
  if (outcome.kind === "done") {
@@ -52579,10 +53156,12 @@ ${findings.map((f) => `\u2022 ${f}`).join("\n")}` : "";
52579
53156
  };
52580
53157
  }
52581
53158
  const handoffTaskRef = taskRef ? ` (${taskRef})` : "";
53159
+ const revisionSuffix = "finalReviewedHash" in outcome && outcome.finalReviewedHash ? outcome.finalReviewedHash.slice(0, 8) : "init";
52582
53160
  return {
52583
53161
  kind: "handoff",
53162
+ id: `II-SPEC-STOPPED-${input.pageId}-${revisionSuffix}`,
52584
53163
  ...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.`
53164
+ body: `The spec review stopped for Specs/${input.pageId}${handoffTaskRef}. ${outcome.error} Review the spec or consult the Director to update it.`
52586
53165
  };
52587
53166
  }
52588
53167
  var consensusDescriptor = {
@@ -52761,6 +53340,14 @@ async function run2(ctx, input) {
52761
53340
  };
52762
53341
  }
52763
53342
  function onComplete2(outcome, input) {
53343
+ if (outcome.kind === "superseded") {
53344
+ return {
53345
+ kind: "info",
53346
+ suppressInbox: true,
53347
+ body: outcome.reason ?? outcome.error ?? "Superseded",
53348
+ ...outcome.taskId ? { taskId: outcome.taskId } : {}
53349
+ };
53350
+ }
52764
53351
  const taskId = outcome.kind === "failed" || outcome.kind === "done" ? outcome.taskId : void 0;
52765
53352
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
52766
53353
  if (outcome.kind === "done") {
@@ -52815,10 +53402,10 @@ var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
52815
53402
  var DRAFT_SPEC_PERSONA = "spec-author";
52816
53403
  var PAGE_ID_MAX_ATTEMPTS = 5;
52817
53404
  function defaultSleep(ms) {
52818
- return new Promise((resolve) => setTimeout(resolve, ms));
53405
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
52819
53406
  }
52820
53407
  function enforceSpecOutputContract(markdown, taskId, title) {
52821
- let body = markdown.trim();
53408
+ let body = normalizeAcceptanceCriteria(markdown.trim());
52822
53409
  const headingRe = new RegExp(`^#\\s+${taskId}\\b`, "m");
52823
53410
  if (!headingRe.test(body)) {
52824
53411
  body = `# ${taskId} \u2014 ${title}
@@ -53275,6 +53862,14 @@ async function runDraftSpec(ctx, input, deps) {
53275
53862
  };
53276
53863
  }
53277
53864
  function onComplete3(outcome, input) {
53865
+ if (outcome.kind === "superseded") {
53866
+ return {
53867
+ kind: "info",
53868
+ suppressInbox: true,
53869
+ body: outcome.reason ?? outcome.error ?? "Superseded",
53870
+ taskId: input.taskId
53871
+ };
53872
+ }
53278
53873
  if (outcome.kind === "done") {
53279
53874
  let parsed = {};
53280
53875
  try {
@@ -54167,6 +54762,9 @@ async function runReportEvidence(ctx, input) {
54167
54762
  return { kind: "done", resultJson: JSON.stringify(artifact), finalReviewedHash: null };
54168
54763
  }
54169
54764
  function onComplete5(outcome, input) {
54765
+ if (outcome.kind === "superseded") {
54766
+ return { kind: "info", body: `Evidence assembly superseded for report ${input.reportId}.`, suppressInbox: true };
54767
+ }
54170
54768
  if (outcome.kind === "done") {
54171
54769
  return { kind: "info", body: `Evidence assembled for report ${input.reportId}.`, suppressInbox: true };
54172
54770
  }
@@ -55350,7 +55948,7 @@ var mcpToolDescriptions = {
55350
55948
  [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
55949
  [mcpToolNames.createWikiPage]: "Create a markdown Wiki page.",
55352
55950
  [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.",
55951
+ [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
55952
  [mcpToolNames.getProject]: "Fetch a single project summary by slug.",
55355
55953
  [mcpToolNames.getProjectSnapshot]: "Fetch the full project snapshot: GitHub binding, tasks, plans, and wiki pages.",
55356
55954
  [mcpToolNames.getTask]: "Fetch a single task by project slug and task id.",
@@ -55591,20 +56189,18 @@ async function getAgentProviderForPanel(client, opts = {}) {
55591
56189
  }
55592
56190
  function createMrrlinTools(options) {
55593
56191
  const client = createMrrlinClient(withAuth(options));
55594
- let boundSlug = options.activeProjectSlug ?? (process.env.MRRLIN_PROJECT_SLUG?.trim() || void 0);
55595
- let sessionResolved = false;
55596
56192
  async function resolveBoundSlug() {
55597
- if (boundSlug !== void 0) return boundSlug;
55598
- if (sessionResolved) return boundSlug;
56193
+ if (options.activeProjectSlug !== void 0) return options.activeProjectSlug;
56194
+ const envSlug = process.env.MRRLIN_PROJECT_SLUG?.trim();
56195
+ if (envSlug) return envSlug;
55599
56196
  try {
55600
56197
  const settings = await client.getOperatorSettings();
55601
56198
  if (settings?.activeProjectSlug) {
55602
- boundSlug = settings.activeProjectSlug;
56199
+ return settings.activeProjectSlug;
55603
56200
  }
55604
56201
  } catch {
55605
56202
  }
55606
- sessionResolved = true;
55607
- return boundSlug;
56203
+ return void 0;
55608
56204
  }
55609
56205
  async function checkTenantBoundary(targetSlug, resourceName) {
55610
56206
  const active = await resolveBoundSlug();
@@ -55734,6 +56330,13 @@ function createMrrlinTools(options) {
55734
56330
  ...input,
55735
56331
  folder: input.folder?.trim().toLowerCase() === "specs" ? "Specs" : input.folder
55736
56332
  };
56333
+ if (pageInput.folder === "Specs") {
56334
+ if (!parseAcceptanceCriteria(pageInput.markdownBody)) {
56335
+ throw new Error(
56336
+ "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."
56337
+ );
56338
+ }
56339
+ }
55737
56340
  return await deliverBuiltIn(
55738
56341
  c,
55739
56342
  projectSlug,
@@ -55760,7 +56363,8 @@ function createMrrlinTools(options) {
55760
56363
  "Unable to create task.",
55761
56364
  async (c, { projectSlug, ...input }) => {
55762
56365
  await checkTenantBoundary(projectSlug, "task");
55763
- return await c.createTask(projectSlug, input);
56366
+ const status = input.status ?? "todo";
56367
+ return await c.createTask(projectSlug, { ...input, status });
55764
56368
  }
55765
56369
  ),
55766
56370
  [mcpToolNames.getProject]: makeTool(
@@ -55979,6 +56583,20 @@ function createMrrlinTools(options) {
55979
56583
  ...patch,
55980
56584
  ...patch.folder !== void 0 ? { folder: patch.folder.trim().toLowerCase() === "specs" ? "Specs" : patch.folder } : {}
55981
56585
  };
56586
+ let targetFolder = patchInput.folder;
56587
+ let targetBody = patch.markdownBody;
56588
+ if ((targetFolder === void 0 || targetBody === void 0) && (targetFolder === "Specs" || targetBody !== void 0)) {
56589
+ const existing = await c.getWikiPage(projectSlug, pageId);
56590
+ if (targetFolder === void 0) targetFolder = existing.folder;
56591
+ if (targetBody === void 0) targetBody = existing.markdownBody;
56592
+ }
56593
+ if (targetFolder === "Specs" && targetBody !== void 0) {
56594
+ if (!parseAcceptanceCriteria(targetBody)) {
56595
+ throw new Error(
56596
+ "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."
56597
+ );
56598
+ }
56599
+ }
55982
56600
  return await deliverBuiltIn(
55983
56601
  c,
55984
56602
  projectSlug,
@@ -56948,7 +57566,7 @@ var import_node_http = __toESM(require("node:http"), 1);
56948
57566
  var import_node_os17 = __toESM(require("node:os"), 1);
56949
57567
  var import_node_path38 = __toESM(require("node:path"), 1);
56950
57568
  var import_node_child_process21 = require("node:child_process");
56951
- var import_node_crypto15 = __toESM(require("node:crypto"), 1);
57569
+ var import_node_crypto16 = __toESM(require("node:crypto"), 1);
56952
57570
 
56953
57571
  // src/turn-replay-buffer.ts
56954
57572
  var DEFAULT_MAX_EVENTS = 2e3;
@@ -57467,7 +58085,7 @@ function createTurnLock() {
57467
58085
  } else {
57468
58086
  autonomousTotalActive += 1;
57469
58087
  }
57470
- return new Promise((resolve, reject) => {
58088
+ return new Promise((resolve2, reject) => {
57471
58089
  const start = () => {
57472
58090
  let bodyPromise;
57473
58091
  try {
@@ -57475,7 +58093,7 @@ function createTurnLock() {
57475
58093
  } catch (e) {
57476
58094
  bodyPromise = Promise.reject(e);
57477
58095
  }
57478
- bodyPromise.then((v) => resolve(v)).catch((e) => reject(e)).finally(() => {
58096
+ bodyPromise.then((v) => resolve2(v)).catch((e) => reject(e)).finally(() => {
57479
58097
  if (lane === "chat") {
57480
58098
  chatTotalActive -= 1;
57481
58099
  } else {
@@ -58077,7 +58695,717 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
58077
58695
  }
58078
58696
 
58079
58697
  // src/_generated/version.ts
58080
- var PKG_VERSION = "0.4.95";
58698
+ var PKG_VERSION = "0.4.98";
58699
+
58700
+ // src/director-closure.ts
58701
+ var CLOSURE_SNAPSHOT_TIMEOUT_MS = 2e3;
58702
+ var CLOSURE_MAX_SNAPSHOT_PAGES = 500;
58703
+ var CLOSURE_MAX_SNAPSHOT_TASKS = 2e3;
58704
+ var CLOSURE_MAX_SNAPSHOT_INBOX = 500;
58705
+ var CLOSURE_MIN_PROSE_CHARS = 25;
58706
+ var CLOSURE_MIN_PROSE_WORDS = 4;
58707
+ var ID_RE = /\b(?:GT|WK)-\d+\b/g;
58708
+ function diffSnapshots(before, after) {
58709
+ const receipts = [];
58710
+ const diffMap = (kind, prev, next) => {
58711
+ for (const [id, entry] of next) {
58712
+ const old = prev.get(id);
58713
+ if (!old) {
58714
+ receipts.push({ kind, action: "created", id, title: entry.title });
58715
+ } else if (entry.updatedAt !== null && entry.updatedAt !== old.updatedAt) {
58716
+ receipts.push({ kind, action: "updated", id, title: entry.title });
58717
+ }
58718
+ }
58719
+ };
58720
+ diffMap("task", before.tasks, after.tasks);
58721
+ if (before.pages && after.pages) diffMap("page", before.pages, after.pages);
58722
+ if (before.inbox && after.inbox) {
58723
+ for (const id of after.inbox) {
58724
+ if (!before.inbox.has(id)) receipts.push({ kind: "inbox", action: "created", id, title: "" });
58725
+ }
58726
+ }
58727
+ return receipts;
58728
+ }
58729
+ function finalAssistantText(events) {
58730
+ let lastToolResult = -1;
58731
+ events.forEach((event, index) => {
58732
+ if (event.type === "tool_result") lastToolResult = index;
58733
+ });
58734
+ return events.slice(lastToolResult + 1).filter((event) => event.type === "assistant" && typeof event.content === "string").map((event) => event.content).join("");
58735
+ }
58736
+ var MUTATION_TOOLS = /* @__PURE__ */ new Set([
58737
+ "create_task",
58738
+ "update_task",
58739
+ "create_wiki_page",
58740
+ "update_wiki_page",
58741
+ "create_inbox_item"
58742
+ ]);
58743
+ function extractStructuredResultId(content) {
58744
+ if (!content) return null;
58745
+ let parsed = content;
58746
+ if (typeof content === "string") {
58747
+ try {
58748
+ parsed = JSON.parse(content);
58749
+ } catch {
58750
+ const trimmed = content.trim();
58751
+ if (/^(?:GT|WK)-\d+$/.test(trimmed) || /^II-[0-9a-zA-Z-]+$/.test(trimmed)) {
58752
+ return trimmed;
58753
+ }
58754
+ return null;
58755
+ }
58756
+ }
58757
+ if (Array.isArray(parsed)) {
58758
+ for (const item of parsed) {
58759
+ const id = extractStructuredResultId(item);
58760
+ if (id) return id;
58761
+ }
58762
+ return null;
58763
+ }
58764
+ if (parsed && typeof parsed === "object") {
58765
+ const obj = parsed;
58766
+ if (typeof obj.text === "string") {
58767
+ const id = extractStructuredResultId(obj.text);
58768
+ if (id) return id;
58769
+ }
58770
+ if (obj.structuredContent && typeof obj.structuredContent === "object") {
58771
+ const sc = obj.structuredContent;
58772
+ if (sc.data && typeof sc.data === "object" && typeof sc.data.id === "string") {
58773
+ return sc.data.id;
58774
+ }
58775
+ if (typeof sc.id === "string") return sc.id;
58776
+ }
58777
+ if (obj.data && typeof obj.data === "object" && typeof obj.data.id === "string") {
58778
+ return obj.data.id;
58779
+ }
58780
+ if (typeof obj.id === "string") {
58781
+ return obj.id;
58782
+ }
58783
+ if (Array.isArray(obj.content)) {
58784
+ for (const item of obj.content) {
58785
+ const id = extractStructuredResultId(item);
58786
+ if (id) return id;
58787
+ }
58788
+ }
58789
+ }
58790
+ return null;
58791
+ }
58792
+ function isErrorResult(event) {
58793
+ if (event.is_error === true || event.is_error === "true") return true;
58794
+ if (event.tool && typeof event.tool === "object") {
58795
+ if (typeof event.tool.exitCode === "number" && event.tool.exitCode !== 0) return true;
58796
+ if (typeof event.tool.status === "string" && event.tool.status === "error") return true;
58797
+ }
58798
+ const checkError = (val) => {
58799
+ if (!val) return false;
58800
+ if (Array.isArray(val)) {
58801
+ return val.some(checkError);
58802
+ }
58803
+ if (typeof val === "object") {
58804
+ const obj = val;
58805
+ if (obj.isError === true || obj.isError === "true" || obj.error !== void 0) return true;
58806
+ if (Array.isArray(obj.content) && obj.content.some(checkError)) return true;
58807
+ if (typeof obj.text === "string") {
58808
+ try {
58809
+ return checkError(JSON.parse(obj.text));
58810
+ } catch {
58811
+ }
58812
+ }
58813
+ } else if (typeof val === "string") {
58814
+ try {
58815
+ return checkError(JSON.parse(val));
58816
+ } catch {
58817
+ }
58818
+ }
58819
+ return false;
58820
+ };
58821
+ return checkError(event.content);
58822
+ }
58823
+ function turnReferences(events) {
58824
+ const mutatedIds = /* @__PURE__ */ new Set();
58825
+ const mutatedInboxIds = /* @__PURE__ */ new Set();
58826
+ const textIds = /* @__PURE__ */ new Set();
58827
+ let hasToolEvents = false;
58828
+ const keyedPending = /* @__PURE__ */ new Map();
58829
+ const unkeyedCalls = [];
58830
+ for (const event of events) {
58831
+ if (event.type === "tool_use") {
58832
+ hasToolEvents = true;
58833
+ const rawName = event.tool?.name ?? event.name ?? "";
58834
+ const toolName = rawName.replace(/^.*__/, "");
58835
+ const isMutation = MUTATION_TOOLS.has(toolName);
58836
+ const inputObj = event.tool?.input && typeof event.tool.input === "object" ? event.tool.input : event.input && typeof event.input === "object" ? event.input : {};
58837
+ const rawTargetId = typeof inputObj.taskId === "string" ? inputObj.taskId : typeof inputObj.pageId === "string" ? inputObj.pageId : typeof inputObj.id === "string" ? inputObj.id : null;
58838
+ let targetId = null;
58839
+ if (rawTargetId) {
58840
+ if ((toolName === "create_task" || toolName === "update_task") && /^GT-\d+$/.test(rawTargetId)) {
58841
+ targetId = rawTargetId;
58842
+ } else if ((toolName === "create_wiki_page" || toolName === "update_wiki_page") && /^WK-\d+$/.test(rawTargetId)) {
58843
+ targetId = rawTargetId;
58844
+ }
58845
+ }
58846
+ const callId = event.tool?.id ?? event.tool_use_id ?? event.id;
58847
+ if (callId && typeof callId === "string") {
58848
+ if (isMutation) {
58849
+ keyedPending.set(callId, { toolName, targetId });
58850
+ }
58851
+ } else {
58852
+ if (isMutation) {
58853
+ unkeyedCalls.push({ isMutation: true, toolName, targetId });
58854
+ } else {
58855
+ unkeyedCalls.push({ isMutation: false });
58856
+ }
58857
+ }
58858
+ } else if (event.type === "tool_result") {
58859
+ hasToolEvents = true;
58860
+ const callId = event.tool?.id ?? event.tool_use_id ?? event.id;
58861
+ let mutation;
58862
+ if (callId && typeof callId === "string") {
58863
+ mutation = keyedPending.get(callId);
58864
+ keyedPending.delete(callId);
58865
+ } else if (unkeyedCalls.length > 0) {
58866
+ const unkeyed = unkeyedCalls.shift();
58867
+ if (unkeyed && unkeyed.isMutation) {
58868
+ mutation = { toolName: unkeyed.toolName, targetId: unkeyed.targetId };
58869
+ }
58870
+ }
58871
+ if (mutation && !isErrorResult(event)) {
58872
+ const returnedId = extractStructuredResultId(event.content);
58873
+ if (mutation.toolName === "create_inbox_item") {
58874
+ if (returnedId && /^II-[0-9a-zA-Z-]+$/.test(returnedId)) {
58875
+ mutatedInboxIds.add(returnedId);
58876
+ }
58877
+ } else {
58878
+ if (mutation.targetId) {
58879
+ mutatedIds.add(mutation.targetId);
58880
+ }
58881
+ if (returnedId) {
58882
+ if ((mutation.toolName === "create_task" || mutation.toolName === "update_task") && /^GT-\d+$/.test(returnedId)) {
58883
+ mutatedIds.add(returnedId);
58884
+ } else if ((mutation.toolName === "create_wiki_page" || mutation.toolName === "update_wiki_page") && /^WK-\d+$/.test(returnedId)) {
58885
+ mutatedIds.add(returnedId);
58886
+ }
58887
+ }
58888
+ }
58889
+ }
58890
+ } else if (event.type === "assistant" && typeof event.content === "string") {
58891
+ for (const id of event.content.match(ID_RE) ?? []) textIds.add(id);
58892
+ }
58893
+ }
58894
+ return { mutatedIds, mutatedInboxIds, textIds, hasToolEvents };
58895
+ }
58896
+ function attributeReceipts(receipts, refs) {
58897
+ if (refs.hasToolEvents) {
58898
+ const board2 = receipts.filter((r) => r.kind !== "inbox" && refs.mutatedIds.has(r.id));
58899
+ const inbox = receipts.filter((r) => r.kind === "inbox" && refs.mutatedInboxIds.has(r.id));
58900
+ return [...board2, ...inbox];
58901
+ }
58902
+ const board = receipts.filter((r) => r.kind !== "inbox" && refs.textIds.has(r.id));
58903
+ return [...board];
58904
+ }
58905
+ function closurePresent(finalText, receipts) {
58906
+ const mentioned = new Set(finalText.match(ID_RE) ?? []);
58907
+ if (!receipts.every((receipt) => receipt.kind === "inbox" || mentioned.has(receipt.id))) return false;
58908
+ if (receipts.some((receipt) => receipt.kind === "inbox") && !/(?<![\p{L}\p{N}])(?:inbox|інбокс|вхідні)(?![\p{L}\p{N}])/iu.test(finalText)) return false;
58909
+ const nonIdText = finalText.replace(ID_RE, "");
58910
+ const letters = nonIdText.match(new RegExp("\\p{L}", "gu")) ?? [];
58911
+ if (letters.length < CLOSURE_MIN_PROSE_CHARS) return false;
58912
+ const words = nonIdText.match(new RegExp("\\p{L}{2,}", "gu")) ?? [];
58913
+ const uniqueWords = new Set(words.map((w) => w.toLowerCase()));
58914
+ if (uniqueWords.size < CLOSURE_MIN_PROSE_WORDS) return false;
58915
+ return true;
58916
+ }
58917
+ function describe3(receipt, lang = "en") {
58918
+ const title = receipt.title.trim();
58919
+ 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";
58920
+ return title ? `${noun} ${receipt.id} "${title}"` : `${noun} ${receipt.id}`;
58921
+ }
58922
+ function renderClosure(receipts, lang = "en") {
58923
+ const board = receipts.filter((r) => r.kind !== "inbox");
58924
+ const created = board.filter((r) => r.action === "created");
58925
+ const updated = board.filter((r) => r.action === "updated");
58926
+ const inboxCount = receipts.filter((r) => r.kind === "inbox").length;
58927
+ const parts = [];
58928
+ if (lang === "uk") {
58929
+ 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("; ")}.`);
58930
+ if (updated.length) parts.push(`\u041E\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${updated.map((r) => describe3(r, "uk")).join("; ")}.`);
58931
+ if (inboxCount) parts.push(`\u041D\u043E\u0432\u0456 \u0435\u043B\u0435\u043C\u0435\u043D\u0442\u0438 Inbox: ${inboxCount}.`);
58932
+ 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.");
58933
+ return `
58934
+
58935
+ **\u041F\u0456\u0434\u0441\u0443\u043C\u043E\u043A:** ${parts.join(" ")}`;
58936
+ }
58937
+ if (created.length) parts.push(`During this turn the project gained: ${created.map((r) => describe3(r, "en")).join("; ")}.`);
58938
+ if (updated.length) parts.push(`Updated: ${updated.map((r) => describe3(r, "en")).join("; ")}.`);
58939
+ if (inboxCount) parts.push(`New Inbox items: ${inboxCount}.`);
58940
+ if (created.some((r) => r.kind === "task")) parts.push("Next: the task is on the task board.");
58941
+ return `
58942
+
58943
+ **Summary:** ${parts.join(" ")}`;
58944
+ }
58945
+ function closureFor(input) {
58946
+ const receipts = attributeReceipts(diffSnapshots(input.before, input.after), turnReferences(input.events));
58947
+ if (receipts.length === 0) return null;
58948
+ if (closurePresent(finalAssistantText(input.events), receipts)) return null;
58949
+ const isUk = Boolean(input.operatorText && /[Ѐ-ӿ]/.test(input.operatorText));
58950
+ return renderClosure(receipts, isUk ? "uk" : "en");
58951
+ }
58952
+
58953
+ // src/clarification-ledger.ts
58954
+ var LEDGER_MAX_ITEMS = 20;
58955
+ var LEDGER_MAX_QUESTIONS_PER_TURN = 5;
58956
+ var LEDGER_MAX_EXTRACTED_QUESTIONS = 50;
58957
+ var LEDGER_MAX_QUESTION_CHARS = 300;
58958
+ function emptyLedger() {
58959
+ return { turn: 0, items: [] };
58960
+ }
58961
+ 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;
58962
+ 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;
58963
+ var RESOLUTION_MARKER_RE = /\b(?:answered|assumed|assuming|assumption|blocker|default|resolved|decided)\b|відпові|прийнят|приймаю|беру|за замовчуванням|блокер|припуска|вирішено|узгоджено/iu;
58964
+ var LEDGER_STOP_WORDS = /* @__PURE__ */ new Set([
58965
+ "which",
58966
+ "what",
58967
+ "where",
58968
+ "when",
58969
+ "who",
58970
+ "whom",
58971
+ "how",
58972
+ "why",
58973
+ "should",
58974
+ "would",
58975
+ "could",
58976
+ "will",
58977
+ "can",
58978
+ "may",
58979
+ "might",
58980
+ "must",
58981
+ "need",
58982
+ "needed",
58983
+ "use",
58984
+ "used",
58985
+ "have",
58986
+ "been",
58987
+ "there",
58988
+ "this",
58989
+ "that",
58990
+ "question",
58991
+ "questions",
58992
+ "item",
58993
+ "items",
58994
+ "task",
58995
+ "tasks",
58996
+ "about",
58997
+ "\u044F\u043A\u0456",
58998
+ "\u044F\u043A\u0438\u0439",
58999
+ "\u044F\u043A\u0430",
59000
+ "\u044F\u043A\u0435",
59001
+ "\u044F\u043A\u0438\u0445",
59002
+ "\u044F\u043A\u043E\u043C\u0443",
59003
+ "\u044F\u043A\u0456\u0439",
59004
+ "\u0445\u0442\u043E",
59005
+ "\u043A\u043E\u0433\u043E",
59006
+ "\u043A\u043E\u043C\u0443",
59007
+ "\u043A\u0438\u043C",
59008
+ "\u0449\u043E",
59009
+ "\u0447\u043E\u0433\u043E",
59010
+ "\u0447\u043E\u043C\u0443",
59011
+ "\u0447\u0438\u043C",
59012
+ "\u0434\u0435",
59013
+ "\u043A\u0443\u0434\u0438",
59014
+ "\u0437\u0432\u0456\u0434\u043A\u0438",
59015
+ "\u043A\u043E\u043B\u0438",
59016
+ "\u0447\u043E\u043C\u0443",
59017
+ "\u043D\u0430\u0432\u0456\u0449\u043E",
59018
+ "\u044F\u043A",
59019
+ "\u0447\u0438",
59020
+ "\u0430\u0431\u043E",
59021
+ "\u0434\u043B\u044F",
59022
+ "\u043F\u0440\u043E",
59023
+ "\u043F\u0440\u0438",
59024
+ "\u043D\u0430\u0434",
59025
+ "\u043F\u0456\u0434",
59026
+ "\u0432\u0456\u0434",
59027
+ "\u0434\u043E",
59028
+ "\u043F\u0438\u0442\u0430\u043D\u043D\u044F",
59029
+ "\u0437\u0430\u0434\u0430\u0447\u0430",
59030
+ "\u0437\u0430\u0434\u0430\u0447\u0456",
59031
+ "\u043F\u043E\u0442\u0440\u0456\u0431\u043D\u043E",
59032
+ "\u043F\u043E\u0442\u0440\u0456\u0431\u043D\u0456",
59033
+ "\u0442\u0440\u0435\u0431\u0430"
59034
+ ]);
59035
+ function tokens(text) {
59036
+ return new Set(
59037
+ text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 3)
59038
+ );
59039
+ }
59040
+ function tokenMatches(a, b) {
59041
+ if (a === b) return true;
59042
+ if (a.length >= 4 && b.length >= 4 && (a.startsWith(b.slice(0, 4)) || b.startsWith(a.slice(0, 4)))) {
59043
+ return true;
59044
+ }
59045
+ return false;
59046
+ }
59047
+ function normalizeQuestionIdentity(value) {
59048
+ return value.normalize("NFKC").toLowerCase().replace(/\s+/gu, " ").trim();
59049
+ }
59050
+ function isSameQuestion(a, b) {
59051
+ const left = normalizeQuestionIdentity(a);
59052
+ return /[\p{L}\p{N}]/u.test(left) && left === normalizeQuestionIdentity(b);
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);
@@ -58556,7 +59884,7 @@ async function writeCrashSentinel(stateDir, sentinel) {
58556
59884
  }
58557
59885
  function withTimeout(fn, ms = DEFAULT_OPERATION_TIMEOUT_MS) {
58558
59886
  const controller = new AbortController();
58559
- return new Promise((resolve, reject) => {
59887
+ return new Promise((resolve2, reject) => {
58560
59888
  const timer = setTimeout(() => {
58561
59889
  controller.abort();
58562
59890
  const timeoutError = new Error(`operation timed out after ${ms}ms`);
@@ -58566,7 +59894,7 @@ function withTimeout(fn, ms = DEFAULT_OPERATION_TIMEOUT_MS) {
58566
59894
  fn(controller.signal).then(
58567
59895
  (value) => {
58568
59896
  clearTimeout(timer);
58569
- resolve(value);
59897
+ resolve2(value);
58570
59898
  },
58571
59899
  (error51) => {
58572
59900
  clearTimeout(timer);
@@ -59512,7 +60840,7 @@ function runExternalAgentsCli(args, opts) {
59512
60840
  }
59513
60841
  const useStdin = opts.stdinValue !== void 0;
59514
60842
  const stdioConfig = useStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
59515
- return new Promise((resolve) => {
60843
+ return new Promise((resolve2) => {
59516
60844
  let stdout = "";
59517
60845
  let stderr = "";
59518
60846
  let settled = false;
@@ -59520,7 +60848,7 @@ function runExternalAgentsCli(args, opts) {
59520
60848
  const done = (code) => {
59521
60849
  if (!settled) {
59522
60850
  settled = true;
59523
- resolve({ stdout, stderr, code, timedOut, source, triedPath });
60851
+ resolve2({ stdout, stderr, code, timedOut, source, triedPath });
59524
60852
  }
59525
60853
  };
59526
60854
  let spawned;
@@ -59688,7 +61016,7 @@ function startProviderReadinessLoop(deps) {
59688
61016
 
59689
61017
  // src/execution-consumer.ts
59690
61018
  var import_node_child_process19 = require("node:child_process");
59691
- var import_node_crypto13 = __toESM(require("node:crypto"), 1);
61019
+ var import_node_crypto14 = __toESM(require("node:crypto"), 1);
59692
61020
  var import_node_path31 = __toESM(require("node:path"), 1);
59693
61021
  var import_node_util2 = require("node:util");
59694
61022
 
@@ -60544,6 +61872,7 @@ async function awaitTurnWithDeadline(opts) {
60544
61872
 
60545
61873
  // src/worktree.ts
60546
61874
  var import_node_child_process18 = require("node:child_process");
61875
+ var import_node_crypto13 = require("node:crypto");
60547
61876
  var import_node_fs17 = require("node:fs");
60548
61877
  var import_node_path30 = require("node:path");
60549
61878
 
@@ -60571,8 +61900,43 @@ function worktreeBranch(runId) {
60571
61900
  return `mrrlin/run-${runId}`;
60572
61901
  }
60573
61902
  var defaultGitExec = (args, env) => (0, import_node_child_process18.execFileSync)("git", args, { encoding: "utf8", timeout: 3e4, env });
60574
- function git(checkout, args) {
60575
- return (0, import_node_child_process18.execFileSync)("git", ["-C", checkout, ...args], {
61903
+ function disabledGitHooksPath(locksDir, scope) {
61904
+ const scopeKey = (0, import_node_crypto13.createHash)("sha256").update(scope).digest("hex").slice(0, 32);
61905
+ return (0, import_node_path30.resolve)(locksDir, "..", "disabled-git-hooks", scopeKey);
61906
+ }
61907
+ function prepareDisabledGitHooksDir(locksDir, scope) {
61908
+ const hooksDir = disabledGitHooksPath(locksDir, scope);
61909
+ const hooksRoot = (0, import_node_path30.resolve)(hooksDir, "..");
61910
+ (0, import_node_fs17.mkdirSync)(hooksRoot, { recursive: true, mode: 448 });
61911
+ (0, import_node_fs17.mkdirSync)(hooksDir, { recursive: true, mode: 320 });
61912
+ if ((0, import_node_fs17.readdirSync)(hooksDir).length > 0) {
61913
+ throw new Error(`disabled_git_hooks_dir_not_empty: ${hooksDir}`);
61914
+ }
61915
+ try {
61916
+ (0, import_node_fs17.chmodSync)(hooksDir, 320);
61917
+ } catch {
61918
+ }
61919
+ return hooksDir;
61920
+ }
61921
+ function removeDisabledGitHooksDir(locksDir, scope) {
61922
+ try {
61923
+ (0, import_node_fs17.rmdirSync)(disabledGitHooksPath(locksDir, scope));
61924
+ } catch {
61925
+ }
61926
+ }
61927
+ function withDisabledGitHooksEnv(env, hooksDir) {
61928
+ const rawCount = env.GIT_CONFIG_COUNT;
61929
+ const parsedCount = rawCount === void 0 ? 0 : Number(rawCount);
61930
+ const count2 = Number.isSafeInteger(parsedCount) && parsedCount >= 0 ? parsedCount : 0;
61931
+ return {
61932
+ ...env,
61933
+ GIT_CONFIG_COUNT: String(count2 + 1),
61934
+ [`GIT_CONFIG_KEY_${count2}`]: "core.hooksPath",
61935
+ [`GIT_CONFIG_VALUE_${count2}`]: hooksDir
61936
+ };
61937
+ }
61938
+ function git(checkout, args, hooksDir) {
61939
+ return (0, import_node_child_process18.execFileSync)("git", ["-C", checkout, "-c", `core.hooksPath=${hooksDir}`, ...args], {
60576
61940
  encoding: "utf8",
60577
61941
  timeout: 3e4,
60578
61942
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -60580,20 +61944,21 @@ function git(checkout, args) {
60580
61944
  }
60581
61945
  async function createRunWorktree(opts) {
60582
61946
  const worktreePath = (0, import_node_path30.join)(opts.worktreeRoot, opts.slug, opts.runId);
61947
+ const hooksDir = prepareDisabledGitHooksDir(opts.locksDir, opts.runId);
60583
61948
  await withCheckoutLock(opts.locksDir, opts.slug, async () => {
60584
61949
  (0, import_node_fs17.mkdirSync)(opts.worktreeRoot, { recursive: true });
60585
61950
  try {
60586
- git(opts.checkout, ["fetch", "origin", opts.defaultBranch]);
61951
+ git(opts.checkout, ["fetch", "origin", opts.defaultBranch], hooksDir);
60587
61952
  if (!(0, import_node_fs17.existsSync)(worktreePath)) {
60588
61953
  git(opts.checkout, [
60589
61954
  "worktree",
60590
61955
  "add",
61956
+ "--no-track",
60591
61957
  "-b",
60592
61958
  worktreeBranch(opts.runId),
60593
61959
  worktreePath,
60594
61960
  `origin/${opts.defaultBranch}`
60595
- ]);
60596
- git(worktreePath, ["config", "core.hooksPath", "/dev/null"]);
61961
+ ], hooksDir);
60597
61962
  }
60598
61963
  } catch (error51) {
60599
61964
  const detail = error51 instanceof Error ? error51.message : String(error51);
@@ -60605,12 +61970,14 @@ async function createRunWorktree(opts) {
60605
61970
  return worktreePath;
60606
61971
  }
60607
61972
  async function removeRunWorktree(opts) {
61973
+ const hooksDir = prepareDisabledGitHooksDir(opts.locksDir, (0, import_node_path30.basename)(opts.worktreePath));
60608
61974
  await withCheckoutLock(opts.locksDir, opts.slug, async () => {
60609
61975
  try {
60610
- git(opts.checkout, ["worktree", "remove", "--force", opts.worktreePath]);
61976
+ git(opts.checkout, ["worktree", "remove", "--force", opts.worktreePath], hooksDir);
60611
61977
  } catch {
60612
61978
  }
60613
- git(opts.checkout, ["worktree", "prune"]);
61979
+ git(opts.checkout, ["worktree", "prune"], hooksDir);
61980
+ removeDisabledGitHooksDir(opts.locksDir, (0, import_node_path30.basename)(opts.worktreePath));
60614
61981
  });
60615
61982
  }
60616
61983
  async function pushRunBranch(opts, execGit = defaultGitExec) {
@@ -60633,6 +62000,7 @@ async function hasCommitsToPush(opts, execGit = defaultGitExec) {
60633
62000
  }
60634
62001
  async function doPushRunBranch(opts, execGit) {
60635
62002
  const branch = worktreeBranch(opts.runId);
62003
+ const hooksDir = prepareDisabledGitHooksDir(opts.locksDir, opts.runId);
60636
62004
  const pushEnv = {
60637
62005
  PATH: process.env.PATH ?? "",
60638
62006
  ...process.env.HOME !== void 0 ? { HOME: process.env.HOME } : {},
@@ -60678,7 +62046,7 @@ async function doPushRunBranch(opts, execGit) {
60678
62046
  "-C",
60679
62047
  opts.worktreePath,
60680
62048
  "-c",
60681
- "core.hooksPath=/dev/null",
62049
+ `core.hooksPath=${hooksDir}`,
60682
62050
  "push",
60683
62051
  "--no-verify",
60684
62052
  "origin",
@@ -60690,6 +62058,7 @@ async function doPushRunBranch(opts, execGit) {
60690
62058
  }
60691
62059
  async function sweepOrphanWorktrees(opts) {
60692
62060
  const reaped = [];
62061
+ const hooksDir = prepareDisabledGitHooksDir(opts.locksDir, `sweep:${opts.slug}`);
60693
62062
  const ownedPrefix = (() => {
60694
62063
  try {
60695
62064
  return (import_node_fs17.realpathSync.native ?? import_node_fs17.realpathSync)(opts.worktreeRoot);
@@ -60697,32 +62066,37 @@ async function sweepOrphanWorktrees(opts) {
60697
62066
  return opts.worktreeRoot;
60698
62067
  }
60699
62068
  })() + import_node_path30.sep + opts.slug + import_node_path30.sep;
60700
- await withCheckoutLock(opts.locksDir, opts.slug, async () => {
60701
- const listing = git(opts.checkout, ["worktree", "list", "--porcelain"]);
60702
- for (const block of listing.split("\n\n")) {
60703
- const m = /^worktree (.+)$/m.exec(block);
60704
- const wt = m?.[1]?.trim();
60705
- if (!wt) continue;
60706
- const canonicalWt = (() => {
60707
- try {
60708
- return (import_node_fs17.realpathSync.native ?? import_node_fs17.realpathSync)(wt);
60709
- } catch {
60710
- return wt;
60711
- }
60712
- })();
60713
- const normWt = (0, import_node_path30.normalize)(canonicalWt);
60714
- const normPrefix = (0, import_node_path30.normalize)(ownedPrefix);
60715
- const isMatch = process.platform === "win32" ? normWt.toLowerCase().startsWith(normPrefix.toLowerCase()) : normWt.startsWith(normPrefix);
60716
- if (isMatch && opts.isReapable(wt)) {
60717
- try {
60718
- git(opts.checkout, ["worktree", "remove", "--force", wt]);
60719
- reaped.push(wt);
60720
- } catch {
62069
+ try {
62070
+ await withCheckoutLock(opts.locksDir, opts.slug, async () => {
62071
+ const listing = git(opts.checkout, ["worktree", "list", "--porcelain"], hooksDir);
62072
+ for (const block of listing.split("\n\n")) {
62073
+ const m = /^worktree (.+)$/m.exec(block);
62074
+ const wt = m?.[1]?.trim();
62075
+ if (!wt) continue;
62076
+ const canonicalWt = (() => {
62077
+ try {
62078
+ return (import_node_fs17.realpathSync.native ?? import_node_fs17.realpathSync)(wt);
62079
+ } catch {
62080
+ return wt;
62081
+ }
62082
+ })();
62083
+ const normWt = (0, import_node_path30.normalize)(canonicalWt);
62084
+ const normPrefix = (0, import_node_path30.normalize)(ownedPrefix);
62085
+ const isMatch = process.platform === "win32" ? normWt.toLowerCase().startsWith(normPrefix.toLowerCase()) : normWt.startsWith(normPrefix);
62086
+ if (isMatch && opts.isReapable(wt)) {
62087
+ try {
62088
+ git(opts.checkout, ["worktree", "remove", "--force", wt], hooksDir);
62089
+ removeDisabledGitHooksDir(opts.locksDir, (0, import_node_path30.basename)(wt));
62090
+ reaped.push(wt);
62091
+ } catch {
62092
+ }
60721
62093
  }
60722
62094
  }
60723
- }
60724
- git(opts.checkout, ["worktree", "prune"]);
60725
- });
62095
+ git(opts.checkout, ["worktree", "prune"], hooksDir);
62096
+ });
62097
+ } finally {
62098
+ removeDisabledGitHooksDir(opts.locksDir, `sweep:${opts.slug}`);
62099
+ }
60726
62100
  return reaped;
60727
62101
  }
60728
62102
 
@@ -60916,7 +62290,7 @@ function readPositiveIntEnv(name, fallback) {
60916
62290
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
60917
62291
  }
60918
62292
  function delayMs(ms) {
60919
- return new Promise((resolve) => setTimeout(resolve, ms));
62293
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
60920
62294
  }
60921
62295
  function executionBundleBudget() {
60922
62296
  if ((process.env.MRRLIN_RETRIEVAL_BUDGET_DISABLE ?? "").trim() === "1") return void 0;
@@ -61244,7 +62618,7 @@ function isBridgeGeneratedArtifact(artifact) {
61244
62618
  return typeof payload === "object" && payload !== null && "event" in payload;
61245
62619
  }
61246
62620
  function newArtifactId() {
61247
- return `EA-${import_node_crypto13.default.randomUUID()}`;
62621
+ return `EA-${import_node_crypto14.default.randomUUID()}`;
61248
62622
  }
61249
62623
  var BLAST_RADIUS_FLOOR_PATTERNS = [/^supabase\/migrations\//, /^packages\/db\/src\/schema\.ts$/];
61250
62624
  function isBlastRadiusFloorPath(path29) {
@@ -61413,8 +62787,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
61413
62787
  return deps.run("exec", async () => {
61414
62788
  const pending = [];
61415
62789
  let resolveTurnEnded = null;
61416
- const turnEnded = new Promise((resolve) => {
61417
- resolveTurnEnded = resolve;
62790
+ const turnEnded = new Promise((resolve2) => {
62791
+ resolveTurnEnded = resolve2;
61418
62792
  });
61419
62793
  let acceptedTurnId = null;
61420
62794
  const unsubActivity = codex.onActivity(threadId, () => {
@@ -61495,7 +62869,7 @@ async function driveRun(deps, projectSlug, runId) {
61495
62869
  if (!claimed) return;
61496
62870
  if (!claimed.leaseId) {
61497
62871
  await deps.client.createInboxItem(projectSlug, {
61498
- id: `II-${import_node_crypto13.default.randomUUID()}`,
62872
+ id: `II-${import_node_crypto14.default.randomUUID()}`,
61499
62873
  kind: "handoff",
61500
62874
  taskId: claimed.taskId,
61501
62875
  body: `Execution run ${runId} was claimed but returned no leaseId. This indicates DB/API corruption and requires operator attention.`
@@ -61546,6 +62920,22 @@ async function driveRun(deps, projectSlug, runId) {
61546
62920
  }
61547
62921
  }
61548
62922
  }
62923
+ const needsBrowser = claimed.prerequisites?.some((p) => p.mcp === "mrrlin-browser") || task.domains?.includes("browser");
62924
+ if (needsBrowser) {
62925
+ const checkBusy = deps.checkBrowserBusy ?? checkBrowserBusy;
62926
+ const busy = checkBusy();
62927
+ if (busy.busy) {
62928
+ await failRun(
62929
+ deps,
62930
+ projectSlug,
62931
+ task,
62932
+ runId,
62933
+ leaseId,
62934
+ `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.`
62935
+ );
62936
+ return;
62937
+ }
62938
+ }
61549
62939
  const blockerTasks = task.blockedBy.length ? await Promise.all(task.blockedBy.map((blockerTaskId) => deps.client.getTask(projectSlug, blockerTaskId))) : [];
61550
62940
  const missingBlockers = task.blockedBy.filter((_, index) => !blockerTasks[index]);
61551
62941
  if (missingBlockers.length > 0) {
@@ -61724,12 +63114,20 @@ async function driveRun(deps, projectSlug, runId) {
61724
63114
  turnCount: 0,
61725
63115
  usage: null
61726
63116
  };
61727
- const recordTurn = (outcome) => {
63117
+ const recordTurn2 = (outcome) => {
61728
63118
  runTelemetry.promptBytes += outcome.telemetry.promptBytes;
61729
63119
  runTelemetry.turnCount += 1;
61730
63120
  runTelemetry.usage = addTurnTokenUsage(runTelemetry.usage, outcome.telemetry.usage);
61731
63121
  return outcome;
61732
63122
  };
63123
+ const isContextBudgetExhausted = (usage) => {
63124
+ if (!usage) return false;
63125
+ const contextTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
63126
+ if (contextTokens <= 0) return false;
63127
+ const maxTokens = 2e5;
63128
+ const remainingTokens = maxTokens - contextTokens;
63129
+ return remainingTokens / maxTokens < 0.03 || remainingTokens < 4e3;
63130
+ };
61733
63131
  const effortDecision = deriveEffort({
61734
63132
  impact: task.impact,
61735
63133
  type: task.type,
@@ -61952,7 +63350,7 @@ ${renderTaskStateSection(taskState)}`;
61952
63350
  );
61953
63351
  return;
61954
63352
  }
61955
- const primed = recordTurn(
63353
+ const primed = recordTurn2(
61956
63354
  await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming", effortDecision?.level)
61957
63355
  );
61958
63356
  if (!primed.ok) {
@@ -62016,7 +63414,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62016
63414
  );
62017
63415
  return;
62018
63416
  }
62019
- const outcome = recordTurn(
63417
+ const outcome = recordTurn2(
62020
63418
  await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work", effortDecision?.level)
62021
63419
  );
62022
63420
  if (!outcome.ok) {
@@ -62024,6 +63422,26 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62024
63422
  await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
62025
63423
  return;
62026
63424
  }
63425
+ if (isContextBudgetExhausted(outcome.telemetry.usage)) {
63426
+ console.warn(
63427
+ JSON.stringify({
63428
+ event: "context_budget_gate_triggered",
63429
+ projectSlug,
63430
+ runId,
63431
+ taskId: task.id,
63432
+ usage: outcome.telemetry.usage
63433
+ })
63434
+ );
63435
+ await failRun(
63436
+ deps,
63437
+ projectSlug,
63438
+ task,
63439
+ runId,
63440
+ leaseId,
63441
+ `context_budget_exhausted: context window is near capacity (<3% remaining). Execution run halted safely to preserve progress and prevent lost output.`
63442
+ );
63443
+ return;
63444
+ }
62027
63445
  const prReadyStatuses = useExecutor ? CODE_PR_READY_TASK_STATUSES : COMPLETED_TASK_STATUSES;
62028
63446
  let readiness = await assertRunCompletionReady(deps, projectSlug, task.id, runId, prReadyStatuses);
62029
63447
  if (!readiness.ok) {
@@ -62042,7 +63460,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
62042
63460
  })
62043
63461
  );
62044
63462
  const completionPrompt = buildCompletionTurnPrompt(task.id, readiness.task.status);
62045
- const completionOutcome = recordTurn(
63463
+ const completionOutcome = recordTurn2(
62046
63464
  await driveTurn(
62047
63465
  deps,
62048
63466
  codex,
@@ -62902,7 +64320,7 @@ async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason
62902
64320
  }
62903
64321
  try {
62904
64322
  await deps.client.createInboxItem(projectSlug, {
62905
- id: `II-${import_node_crypto13.default.randomUUID()}`,
64323
+ id: `II-${import_node_crypto14.default.randomUUID()}`,
62906
64324
  kind: "handoff",
62907
64325
  taskId: task.id,
62908
64326
  body: `Task ${task.id} (${task.title}) still needs operator follow-up: ${reason}. Mrrlin could not move the task back to Handoff needed automatically.`
@@ -62937,7 +64355,7 @@ async function notifyOperatorHandoff(deps, projectSlug, task, runId, reason) {
62937
64355
  }
62938
64356
  try {
62939
64357
  await deps.client.createInboxItem(projectSlug, {
62940
- id: `II-${import_node_crypto13.default.randomUUID()}`,
64358
+ id: `II-${import_node_crypto14.default.randomUUID()}`,
62941
64359
  kind: "handoff",
62942
64360
  taskId: task.id,
62943
64361
  body: `Execution run ${runId} for task ${task.id} (${task.title}) needs operator attention: ${reason}`
@@ -63098,7 +64516,7 @@ async function deployGateHandoff(deps, projectSlug, task, runId, reason, leaseId
63098
64516
  }).catch(() => {
63099
64517
  });
63100
64518
  await deps.client.createInboxItem(projectSlug, {
63101
- id: `II-${import_node_crypto13.default.randomUUID()}`,
64519
+ id: `II-${import_node_crypto14.default.randomUUID()}`,
63102
64520
  kind: "handoff",
63103
64521
  taskId: task.id,
63104
64522
  body: `Deploy gate for run ${runId} (task ${task.id} \u2014 ${task.title}) needs operator attention: ${reason}`
@@ -63117,7 +64535,7 @@ function maxLeaseReclaims() {
63117
64535
  }
63118
64536
 
63119
64537
  // src/async-job-consumer.ts
63120
- var import_node_crypto14 = __toESM(require("node:crypto"), 1);
64538
+ var import_node_crypto15 = __toESM(require("node:crypto"), 1);
63121
64539
  var ASYNC_JOB_MAX_ATTEMPTS = 2;
63122
64540
  function ASYNC_UNOWNED_TTL_DAYS() {
63123
64541
  const raw = Number(process.env.MRRLIN_ASYNC_UNOWNED_TTL_DAYS);
@@ -63207,12 +64625,21 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
63207
64625
  } finally {
63208
64626
  clearInterval(touchTimer);
63209
64627
  }
64628
+ if (outcome.kind === "superseded") {
64629
+ await deps.client.terminalizeAsyncJob(projectSlug, jobId, {
64630
+ leaseId,
64631
+ status: "superseded",
64632
+ error: outcome.reason ?? outcome.error ?? "Superseded"
64633
+ }).catch(() => {
64634
+ });
64635
+ return;
64636
+ }
63210
64637
  const terminalStatus = outcome.kind === "done" ? "done" : "failed";
63211
64638
  await deps.client.terminalizeAsyncJob(projectSlug, jobId, {
63212
64639
  leaseId,
63213
64640
  status: terminalStatus,
63214
64641
  resultJson: outcome.kind === "done" ? outcome.resultJson : null,
63215
- finalReviewedHash: outcome.kind === "done" ? outcome.finalReviewedHash : null,
64642
+ finalReviewedHash: outcome.kind === "done" ? outcome.finalReviewedHash : outcome.kind === "failed" ? outcome.finalReviewedHash ?? null : null,
63216
64643
  error: outcome.kind === "done" ? null : outcome.error,
63217
64644
  failureReason: outcome.kind === "done" ? null : outcome.failureReason
63218
64645
  }).catch(() => {
@@ -63233,7 +64660,7 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
63233
64660
  const delivery = descriptor2.onComplete(outcome, input);
63234
64661
  if (!delivery.suppressInbox) {
63235
64662
  await deps.client.createInboxItem(projectSlug, {
63236
- id: delivery.id ?? `II-${import_node_crypto14.default.randomUUID()}`,
64663
+ id: delivery.id ?? `II-${import_node_crypto15.default.randomUUID()}`,
63237
64664
  kind: delivery.kind,
63238
64665
  body: delivery.body,
63239
64666
  ...delivery.taskId ? { taskId: delivery.taskId } : {},
@@ -63251,13 +64678,13 @@ async function deriveRequestedHash(client, toolName, input) {
63251
64678
  if (input.requestedHash) return input.requestedHash;
63252
64679
  if (toolName === "run_spec_consensus" && input.pageId) {
63253
64680
  const page = await client.getWikiPage(input.projectSlug, input.pageId);
63254
- return page.contentHash ?? import_node_crypto14.default.createHash("sha256").update(page.markdownBody).digest("hex");
64681
+ return page.contentHash ?? import_node_crypto15.default.createHash("sha256").update(page.markdownBody).digest("hex");
63255
64682
  }
63256
64683
  if (toolName === "draft_task_spec" && input.taskId) {
63257
64684
  const task = await client.getTask(input.projectSlug, input.taskId);
63258
- return task.updatedAt ?? import_node_crypto14.default.createHash("sha256").update(JSON.stringify(task)).digest("hex");
64685
+ return task.updatedAt ?? import_node_crypto15.default.createHash("sha256").update(JSON.stringify(task)).digest("hex");
63259
64686
  }
63260
- return import_node_crypto14.default.createHash("sha256").update(JSON.stringify(input)).digest("hex");
64687
+ return import_node_crypto15.default.createHash("sha256").update(JSON.stringify(input)).digest("hex");
63261
64688
  }
63262
64689
  async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJob(deps, slug, jobId)) {
63263
64690
  const projects = await deps.client.listProjects();
@@ -63599,7 +65026,9 @@ function loadThreadIndex(stateDir, ttlMs, now) {
63599
65026
  lastUsedAt,
63600
65027
  model: typeof entry.model === "string" ? entry.model : null,
63601
65028
  reasoningEffort: typeof entry.reasoningEffort === "string" ? entry.reasoningEffort : null,
63602
- provider: typeof entry.provider === "string" ? entry.provider : null
65029
+ provider: typeof entry.provider === "string" ? entry.provider : null,
65030
+ systemInstructionHash: typeof entry.systemInstructionHash === "string" ? entry.systemInstructionHash : null,
65031
+ ...parseLedger(entry.clarificationLedger) ? { clarificationLedger: parseLedger(entry.clarificationLedger) } : {}
63603
65032
  });
63604
65033
  }
63605
65034
  return result;
@@ -67359,7 +68788,7 @@ function saveAgentPairing(record2) {
67359
68788
  }
67360
68789
  }
67361
68790
  function runPhoneTurn(handler, directorSessionId, text, emit) {
67362
- return new Promise((resolve) => {
68791
+ return new Promise((resolve2) => {
67363
68792
  const emitter = new import_node_events3.EventEmitter();
67364
68793
  const fakeSocket = {
67365
68794
  on: (event, cb) => {
@@ -67374,7 +68803,7 @@ function runPhoneTurn(handler, directorSessionId, text, emit) {
67374
68803
  settled = true;
67375
68804
  if (timer) clearTimeout(timer);
67376
68805
  emitter.removeAllListeners();
67377
- resolve();
68806
+ resolve2();
67378
68807
  };
67379
68808
  const send = (msg) => {
67380
68809
  if (msg.type === "error") {
@@ -67534,13 +68963,19 @@ async function renderPairingQrSvg(payload) {
67534
68963
  // src/executor-server.ts
67535
68964
  var import_node_child_process20 = require("node:child_process");
67536
68965
  function defaultReadinessRunner(provider) {
67537
- const command = provider === "claude" ? "claude" : "codex";
68966
+ if (provider !== "codex" && provider !== "claude" && provider !== "agy") {
68967
+ throw new Error(`unsupported executor provider: ${String(provider)}`);
68968
+ }
68969
+ const command = provider === "claude" ? "claude" : provider === "agy" ? "agy" : "codex";
67538
68970
  return (args) => {
67539
68971
  const r = (0, import_node_child_process20.spawnSync)(command, args, { encoding: "utf8", timeout: 8e3, shell: true });
67540
68972
  return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
67541
68973
  };
67542
68974
  }
67543
68975
  async function startExecutorServer(opts) {
68976
+ if (opts.provider !== "codex" && opts.provider !== "claude" && opts.provider !== "agy") {
68977
+ throw new Error(`unsupported executor provider: ${String(opts.provider)}`);
68978
+ }
67544
68979
  const verify = opts.verifyReadiness ?? verifyProviderReadiness;
67545
68980
  const runner = opts.readinessRunner ?? defaultReadinessRunner(opts.provider);
67546
68981
  const readiness = await verify(opts.provider, runner);
@@ -67553,8 +68988,37 @@ async function startExecutorServer(opts) {
67553
68988
  error51.details = readiness.details;
67554
68989
  throw error51;
67555
68990
  }
68991
+ const baseEnv = { ...opts.env ?? process.env };
68992
+ const childEnv = withDisabledGitHooksEnv(baseEnv, opts.disabledGitHooksDir);
68993
+ if (opts.provider === "codex") {
68994
+ const manager = new CodexServerManager();
68995
+ try {
68996
+ const client = await manager.acquire({
68997
+ cwd: opts.worktree,
68998
+ approvalPolicy: "never",
68999
+ env: childEnv,
69000
+ ...opts.spawn ? { spawn: opts.spawn } : {}
69001
+ });
69002
+ let disposed = false;
69003
+ const dispose = () => {
69004
+ if (disposed) return;
69005
+ disposed = true;
69006
+ try {
69007
+ manager.shutdown();
69008
+ } catch {
69009
+ }
69010
+ };
69011
+ return { handle: createCodexAgentHandle(client), dispose };
69012
+ } catch (error51) {
69013
+ try {
69014
+ manager.shutdown();
69015
+ } catch {
69016
+ }
69017
+ throw error51;
69018
+ }
69019
+ }
67556
69020
  if (opts.provider === "claude") {
67557
- const handle = new ClaudeAgentHandle();
69021
+ const handle = new ClaudeAgentHandle(childEnv);
67558
69022
  let disposed = false;
67559
69023
  const dispose = () => {
67560
69024
  if (disposed) return;
@@ -67563,30 +69027,17 @@ async function startExecutorServer(opts) {
67563
69027
  };
67564
69028
  return { handle, dispose };
67565
69029
  }
67566
- const manager = new CodexServerManager();
67567
- try {
67568
- const client = await manager.acquire({
67569
- cwd: opts.worktree,
67570
- approvalPolicy: "never",
67571
- ...opts.spawn ? { spawn: opts.spawn } : {}
67572
- });
69030
+ if (opts.provider === "agy") {
69031
+ const handle = new AgyAgentHandle(childEnv);
67573
69032
  let disposed = false;
67574
69033
  const dispose = () => {
67575
69034
  if (disposed) return;
67576
69035
  disposed = true;
67577
- try {
67578
- manager.shutdown();
67579
- } catch {
67580
- }
69036
+ handle.disposeAll();
67581
69037
  };
67582
- return { handle: createCodexAgentHandle(client), dispose };
67583
- } catch (error51) {
67584
- try {
67585
- manager.shutdown();
67586
- } catch {
67587
- }
67588
- throw error51;
69038
+ return { handle, dispose };
67589
69039
  }
69040
+ throw new Error(`unsupported executor provider: ${String(opts.provider)}`);
67590
69041
  }
67591
69042
 
67592
69043
  // src/self-review.ts
@@ -68375,9 +69826,9 @@ function readCodexFallbackModels() {
68375
69826
  return [...models.values()];
68376
69827
  }
68377
69828
  function selfUpdateInboxItemId(offeredVersion) {
68378
- if (!offeredVersion) return `II-MCPUPD-${(0, import_node_crypto15.randomUUID)()}`;
69829
+ if (!offeredVersion) return `II-MCPUPD-${(0, import_node_crypto16.randomUUID)()}`;
68379
69830
  if (/^[A-Za-z0-9._-]+$/.test(offeredVersion)) return `II-MCPUPD-${offeredVersion}`;
68380
- const hash3 = import_node_crypto15.default.createHash("sha256").update(offeredVersion).digest("hex").slice(0, 10);
69831
+ const hash3 = import_node_crypto16.default.createHash("sha256").update(offeredVersion).digest("hex").slice(0, 10);
68381
69832
  return `II-MCPUPD-${offeredVersion.replace(/[^A-Za-z0-9._-]/g, "-")}-${hash3}`;
68382
69833
  }
68383
69834
  function neutralCheckoutCwd(stateDir) {
@@ -68399,6 +69850,25 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68399
69850
  "You are the Mrrlin Director assistant.",
68400
69851
  "You can call MCP tools to read and write project state.",
68401
69852
  "Prefer calling tools over guessing. Be concise.",
69853
+ // O-DIRECTOR-HARD-GATES: a compact behaviour contract replaced ~30 accumulated mandates, several of
69854
+ // which were fitted to single benchmark personas. Closure and localization are enforced in code
69855
+ // (director-closure.ts, linkifyMrrlinIds.ts); the clarification ledger arrives in the turn context.
69856
+ "Behaviour contract (three rules):",
69857
+ '- 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.',
69858
+ "- 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.",
69859
+ "- 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.",
69860
+ "Core guards:",
69861
+ "- 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.",
69862
+ "- Mention a GT- or WK- id only if a tool returned it in this conversation (server-authoritative receipts).",
69863
+ "- Reply in the operator's language for the whole turn (questions, task scopes, summaries).",
69864
+ "- 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.",
69865
+ "- Spec pages in folder Specs must contain `## Acceptance criteria` followed by `- [ ]` items.",
69866
+ "- 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).",
69867
+ "- 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.",
69868
+ "- INBOX QUESTION RETIREMENT MANDATE \u2014 Retiring answered questions: Before creating a task/plan, retire only pending Inbox questions the operator actually answered. Call `decide_inbox_item(itemId: <exact id>, decidedBy: 'director', verdict: 'acknowledged', answer: <actual answer>)`. Use `verdict: 'rejected', reason: ...` only if moot. Never close an unrelated item.",
69869
+ "- 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.",
69870
+ "- 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.",
69871
+ "",
68402
69872
  "Autonomy level is NOT a reason to skip clarification. `auto` means only that no human gate is",
68403
69873
  "required before the result LANDS (merge/deploy); it does not authorize building on unvalidated",
68404
69874
  "assumptions. Judge clarification on requirement clarity, independently of autonomy_level.",
@@ -68492,17 +69962,22 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68492
69962
  "turn with one sentence restating your understanding, then at most 3 scope-level questions, and",
68493
69963
  "do NOT create the task yet. Create it after the operator replies.",
68494
69964
  "",
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
69965
  "Set operatorSeedRequest to the operator's OWN WORDS, verbatim. Select every message in this",
68502
69966
  "conversation that forms part of the request \u2014 not only the last one \u2014 and concatenate them in",
68503
69967
  "order, separated by a blank line. Do NOT paraphrase, summarise, or rewrite any of it, and do",
68504
69968
  "NOT add words of your own.",
68505
69969
  "",
69970
+ "Once the operator replies and clarifies or confirms the scope (or if scope was already clear),",
69971
+ "you MUST create the tasks: create the plan (if needed) and call `create_task` in the same turn so",
69972
+ "actionable task cards appear on the Tasks board immediately. Never leave the board empty with 0",
69973
+ "tasks after agreeing on scope. Never claim tasks are created, queued, or ready unless `create_task`",
69974
+ "was actually invoked and returned the task ID. Set `status: 'todo'` on newly created actionable",
69975
+ "tasks unless explicitly asked for backlog, so they appear in active Kanban columns immediately.",
69976
+ "When referring to created tasks or where to view them, use natural, human-friendly language with a markdown link",
69977
+ "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)').",
69978
+ "Never output raw technical URL fragments like '\u043D\u0430 /tasks' or 'on /tasks', and never use developer database",
69979
+ "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.",
69980
+ "",
68506
69981
  "Set resolvedIntent to the accepted task intent in your own concise words only after the outcome",
68507
69982
  "is resolved enough to create the task. Use it to capture the actionable understanding that came",
68508
69983
  "from the conversation and, when needed, retrieved wiki context. Do NOT use resolvedIntent as a",
@@ -68541,6 +70016,9 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68541
70016
  " SHALL sentence stating the externally visible behavior.",
68542
70017
  "- Give every requirement at least one `#### Scenario: <name>` with `- WHEN ...` / `- THEN ...`",
68543
70018
  " bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
70019
+ "- MUST include `## Acceptance criteria`: a markdown checklist (`- [ ] ...`) of concrete, verifiable",
70020
+ " conditions \u2014 this is the machine-usable yardstick the consensus review scores against. NEVER format criteria as numbered lists (`1. ...`) or plain bullets.",
70021
+ "- MUST include `## Verification`: the exact tests, commands, or checks that prove each criterion holds.",
68544
70022
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
68545
70023
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
68546
70024
  '- 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 +70138,20 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68660
70138
  `Product-surface contract \u2014 never answer "I don't know where that is" about Mrrlin's own UI:`,
68661
70139
  "- You are talking to an operator inside this product. When they ask where something is, name",
68662
70140
  " 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.",
70141
+ "- Autopilot view lives on `/autopilot`. It displays autonomous execution runs, active background",
70142
+ " jobs, and tasks currently in handoff. To inspect why tasks are in handoff or diagnose autonomous",
70143
+ ' progress, use the canonical typed tools: `list_inbox_items` (filtering by kind="handoff"),',
70144
+ " `list_tasks` (filtering by status), and `list_execution_runs`.",
70145
+ "- Tasks and board backlog live on `/tasks` (and individual task details on `/items/{taskId}`).",
70146
+ " Always use `list_tasks`, `get_task`, and `list_task_events` to inspect them.",
70147
+ "- Project knowledge, deliverable reports, and specifications live under `/wiki` and `/deliverables/*`.",
70148
+ " These are stored as project wiki pages \u2014 read them directly via `get_wiki_page` or search with",
70149
+ " `search_wiki_pages`/`list_wiki_pages`. Never open the browser for `/deliverables/*` or `/wiki/*`.",
70150
+ "- Workspace settings, scheduler configurations, and integrations live on `/settings`.",
70151
+ "- Universal Tool Availability: All canonical typed tools (`list_tasks`, `get_task`, `list_inbox_items`,",
70152
+ " `list_execution_runs`, `get_wiki_page`, `search_wiki_pages`, `list_wiki_pages`) are authoritative",
70153
+ " and fully available across ALL routes and session tabs. Never tell an operator that a typed tool",
70154
+ " is missing or unavailable because of the current page or route.",
68672
70155
  "- If a surface is genuinely not in this list, say what you do know and check with a typed tool",
68673
70156
  ' rather than guessing a route \u2014 a wrong path costs the operator more than an honest "let me',
68674
70157
  ' check". Never send them to the browser to hunt for a page in their own app.',
@@ -68722,22 +70205,12 @@ function buildThreadStartParams(msg, config2, desiredModel) {
68722
70205
  "- During a live chat turn, when a SPECIFIC task is ambiguous and the ambiguity affects",
68723
70206
  " the work, ASK the operator a simple, concrete question inline in your reply (default to",
68724
70207
  " 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`.",
70208
+ " assumption on the spec page. NEVER ask clarifying questions after Turn 1 when the operator",
70209
+ " has provided core project scope; adopt standard conventions for secondary edge cases and create tasks.",
70210
+ '- 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.',
70211
+ '- 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.',
70212
+ "- 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.",
70213
+ "- When the operator answers a banked question, immediately call `decide_inbox_item` with its exact `itemId`, `decidedBy='director'`, `verdict='acknowledged'`, and the actual `answer` before any `create_task`. Never close another pending question.",
68741
70214
  "- Binding taskless answers to a task: WHEN you create a task out of project-level scoping,",
68742
70215
  " call `list_inbox_items` (kind=question), fold any relevant `acknowledged` TASKLESS answer",
68743
70216
  " into the new task's Specs/{id} Clarifications, and `reject` (with a reason) any taskless",
@@ -68843,8 +70316,8 @@ async function runCollectedAgentTurn(handle, prompt, cwd, baseInstructions) {
68843
70316
  let acceptedTurnId;
68844
70317
  let turnError = null;
68845
70318
  let resolveTurnEnded = null;
68846
- const turnEnded = new Promise((resolve) => {
68847
- resolveTurnEnded = resolve;
70319
+ const turnEnded = new Promise((resolve2) => {
70320
+ resolveTurnEnded = resolve2;
68848
70321
  });
68849
70322
  const unsub = handle.onEvent(threadId, (event) => {
68850
70323
  if (event.type === "assistant") chunks.push(event.content);
@@ -69063,9 +70536,10 @@ function createBridgeRequestListener(opts) {
69063
70536
  res.end(JSON.stringify({ ok: false, error: "Origin not allowed." }));
69064
70537
  return;
69065
70538
  }
69066
- const nonce = import_node_crypto15.default.randomBytes(16).toString("base64url");
70539
+ const nonce = import_node_crypto16.default.randomBytes(16).toString("base64url");
69067
70540
  res.writeHead(200, { "content-type": "application/json" });
69068
- res.end(JSON.stringify({ ok: true, wsUrl: `ws://${host}:${port}/ws`, token, nonce }));
70541
+ const publicHost = host === "0.0.0.0" ? "127.0.0.1" : host;
70542
+ res.end(JSON.stringify({ ok: true, wsUrl: `ws://${publicHost}:${port}/ws`, token, nonce }));
69069
70543
  return;
69070
70544
  }
69071
70545
  res.writeHead(404).end();
@@ -69366,7 +70840,7 @@ function createBridgeMessageHandler(deps) {
69366
70840
  }
69367
70841
  }
69368
70842
  socket.on("message", async (data) => {
69369
- const spanId = import_node_crypto15.default.randomUUID();
70843
+ const spanId = import_node_crypto16.default.randomUUID();
69370
70844
  if (logCtx) {
69371
70845
  logCtx.span.id = spanId;
69372
70846
  logCtx.span.at = Date.now();
@@ -69970,8 +71444,10 @@ function createBridgeMessageHandler(deps) {
69970
71444
  const desiredEffort = normalizeReasoningEffort(msg.reasoningEffort);
69971
71445
  const resolvedProviderId = turnSeat.id;
69972
71446
  let startedFreshThread = false;
71447
+ let desiredInstructionHash = null;
69973
71448
  try {
69974
71449
  const startParams = deps.buildStartParams(msg, desiredModel);
71450
+ desiredInstructionHash = import_node_crypto16.default.createHash("sha256").update(startParams.baseInstructions ?? "").digest("hex");
69975
71451
  if (!thread) {
69976
71452
  const newThread = {
69977
71453
  threadId: "",
@@ -69982,7 +71458,8 @@ function createBridgeMessageHandler(deps) {
69982
71458
  reasoningEffort: desiredEffort,
69983
71459
  replay: null,
69984
71460
  supersededReplay: null,
69985
- provider: resolvedProviderId
71461
+ provider: resolvedProviderId,
71462
+ systemInstructionHash: null
69986
71463
  };
69987
71464
  const started = await codexClient.startThread(startParams);
69988
71465
  newThread.threadId = started.threadId;
@@ -69993,17 +71470,27 @@ function createBridgeMessageHandler(deps) {
69993
71470
  } else {
69994
71471
  const existingThread = thread;
69995
71472
  const providerMismatch = existingThread.provider !== null && existingThread.provider !== resolvedProviderId;
71473
+ const instructionMismatch = (resolvedProviderId === "agy" || resolvedProviderId === "claude") && (!existingThread.systemInstructionHash || existingThread.systemInstructionHash !== desiredInstructionHash);
69996
71474
  let resumed = true;
69997
71475
  let result;
69998
- if (!existingThread.threadId || providerMismatch) {
71476
+ if (!existingThread.threadId || providerMismatch || instructionMismatch) {
71477
+ if (existingThread.threadId) {
71478
+ await codexClient.interruptTurn(existingThread.threadId).catch(() => {
71479
+ });
71480
+ }
69999
71481
  resumed = false;
70000
71482
  result = await codexClient.startThread(startParams);
71483
+ existingThread.systemInstructionHash = null;
70001
71484
  } else {
70002
71485
  try {
70003
- result = await codexClient.resumeThread(existingThread.threadId, startParams);
71486
+ result = await codexClient.resumeThread(existingThread.threadId, {
71487
+ ...startParams,
71488
+ alreadySeeded: true
71489
+ });
70004
71490
  } catch {
70005
71491
  resumed = false;
70006
71492
  result = await codexClient.startThread(startParams);
71493
+ existingThread.systemInstructionHash = null;
70007
71494
  }
70008
71495
  }
70009
71496
  existingThread.threadId = result.threadId;
@@ -70037,7 +71524,7 @@ function createBridgeMessageHandler(deps) {
70037
71524
  thread.inFlight = true;
70038
71525
  pendingTurnSessions.delete(directorSessionId);
70039
71526
  thread.replay = createReplayBuffer({
70040
- turnId: (0, import_node_crypto15.randomUUID)(),
71527
+ turnId: (0, import_node_crypto16.randomUUID)(),
70041
71528
  spanId,
70042
71529
  ...deps.retentionMs !== void 0 ? { retentionMs: deps.retentionMs } : {},
70043
71530
  ...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
@@ -70137,10 +71624,13 @@ function createBridgeMessageHandler(deps) {
70137
71624
  deps.releaseCodex(turnSeat.id);
70138
71625
  return;
70139
71626
  }
71627
+ const presentedLedger = presentLedger(thread.clarificationLedger ?? emptyLedger());
71628
+ thread.clarificationLedger = presentedLedger.ledger;
70140
71629
  const contextPrefix = renderContextPrefix({
70141
71630
  ...msg.context,
70142
71631
  project: projectContext,
70143
71632
  answeredQuestions,
71633
+ openQuestions: presentedLedger.shown.length ? presentedLedger.shown : void 0,
70144
71634
  directorSessionId
70145
71635
  });
70146
71636
  const sendInTurn = (msg2) => {
@@ -70187,15 +71677,38 @@ function createBridgeMessageHandler(deps) {
70187
71677
  }
70188
71678
  let resolveTurn = null;
70189
71679
  let rejectTurn = null;
70190
- const turnFinished = new Promise((resolve, reject) => {
70191
- resolveTurn = resolve;
71680
+ const turnFinished = new Promise((resolve2, reject) => {
71681
+ resolveTurn = resolve2;
70192
71682
  rejectTurn = (error51) => reject(error51);
70193
71683
  });
70194
71684
  let outputEvents = 0;
70195
71685
  let exhaustedLimitSignal = null;
70196
71686
  let turnTokenUsage = null;
70197
71687
  let promptBytes = 0;
71688
+ let contextBudgetGateTriggered = false;
70198
71689
  const deferredResults = [];
71690
+ const closureEvents = [];
71691
+ let closureBefore = null;
71692
+ const takeClosureSnapshot = async () => {
71693
+ const snapshot = deps.snapshotProjectObjects;
71694
+ if (!snapshot || !contextProjectSlug) return null;
71695
+ const controller = new AbortController();
71696
+ let timer;
71697
+ try {
71698
+ timer = setTimeout(() => {
71699
+ controller.abort(new Error("snapshot timed out"));
71700
+ }, CLOSURE_SNAPSHOT_TIMEOUT_MS);
71701
+ return await snapshot(contextProjectSlug, { signal: controller.signal });
71702
+ } catch (error51) {
71703
+ process.stderr.write(
71704
+ `[mrrlin-mcp director-bridge] closure_snapshot_failed for ${contextProjectSlug}: ${error51 instanceof Error ? error51.message : String(error51)}
71705
+ `
71706
+ );
71707
+ return null;
71708
+ } finally {
71709
+ if (timer) clearTimeout(timer);
71710
+ }
71711
+ };
70199
71712
  let pokeActivity = () => {
70200
71713
  };
70201
71714
  const forward = (event) => {
@@ -70207,11 +71720,24 @@ function createBridgeMessageHandler(deps) {
70207
71720
  if (event.type === "token_usage") {
70208
71721
  const usage = event.usage;
70209
71722
  turnTokenUsage = addTurnTokenUsage(turnTokenUsage, usage);
71723
+ if (turnTokenUsage) {
71724
+ const totalTokens = (turnTokenUsage.inputTokens ?? 0) + (turnTokenUsage.outputTokens ?? 0);
71725
+ if (totalTokens > 0) {
71726
+ const maxTokens = 2e5;
71727
+ const remainingTokens = maxTokens - totalTokens;
71728
+ if (remainingTokens / maxTokens < 0.03 || remainingTokens < 4e3) {
71729
+ contextBudgetGateTriggered = true;
71730
+ }
71731
+ }
71732
+ }
70210
71733
  return;
70211
71734
  }
70212
71735
  if (event.type === "turn_completed" || event.type === "rate_limits") {
70213
71736
  return;
70214
71737
  }
71738
+ if (event.type === "assistant" || event.type === "tool_result" || event.type === "tool_use") {
71739
+ closureEvents.push(event);
71740
+ }
70215
71741
  outputEvents += 1;
70216
71742
  sendInTurn({ type: "event", directorSessionId, event });
70217
71743
  };
@@ -70220,7 +71746,21 @@ function createBridgeMessageHandler(deps) {
70220
71746
  pokeActivity();
70221
71747
  });
70222
71748
  const currentTurnBrowserToolIds = /* @__PURE__ */ new Set();
71749
+ let hadErrorOnTurn = false;
71750
+ let confirmedProductiveEvent = false;
70223
71751
  const unsubEvent = codexClient.onEvent(threadId, (event) => {
71752
+ if (event.type === "error") {
71753
+ hadErrorOnTurn = true;
71754
+ } else if (!confirmedProductiveEvent && !hadErrorOnTurn) {
71755
+ 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";
71756
+ if (isProductive) {
71757
+ confirmedProductiveEvent = true;
71758
+ if (desiredInstructionHash && thread.systemInstructionHash !== desiredInstructionHash) {
71759
+ thread.systemInstructionHash = desiredInstructionHash;
71760
+ deps.persistThreads();
71761
+ }
71762
+ }
71763
+ }
70224
71764
  if (event.type === "tool_use") {
70225
71765
  toolBusy.start(event.tool.id);
70226
71766
  if (event.tool.name.startsWith("mcp__mrrlin-browser__browser_")) {
@@ -70354,6 +71894,7 @@ ${effectiveText}` : effectiveText;
70354
71894
  turnInput.push({ type: "image", url: imageDataUri });
70355
71895
  }
70356
71896
  sendInTurn({ type: "turn-executing", directorSessionId, turnId: currentReplayBuf.turnId });
71897
+ closureBefore = await takeClosureSnapshot();
70357
71898
  await codexClient.startTurn(thread.threadId, turnInput, thread.reasoningEffort ?? void 0);
70358
71899
  await turnFinished;
70359
71900
  })();
@@ -70371,7 +71912,47 @@ ${effectiveText}` : effectiveText;
70371
71912
  error: zeroOutputLimit.message
70372
71913
  });
70373
71914
  }
70374
- const telemetry = turnTokenUsage !== null ? { promptBytes, usage: turnTokenUsage } : void 0;
71915
+ if (!hadErrorOnTurn && outputEvents > 0) {
71916
+ const recorded = recordTurn(thread.clarificationLedger ?? emptyLedger(), finalAssistantText(closureEvents));
71917
+ thread.clarificationLedger = recorded.ledger;
71918
+ for (const item of recorded.reasked) {
71919
+ process.stderr.write(
71920
+ `[mrrlin-mcp director-bridge] clarification_reasked (x${item.reasked}) for ${directorSessionId}: ${item.question}
71921
+ `
71922
+ );
71923
+ }
71924
+ for (const question of recorded.overflow) {
71925
+ process.stderr.write(
71926
+ `[mrrlin-mcp director-bridge] clarification_overflow for ${directorSessionId}: ${question}
71927
+ `
71928
+ );
71929
+ }
71930
+ for (const item of recorded.dropped) {
71931
+ process.stderr.write(
71932
+ `[mrrlin-mcp director-bridge] clarification_dropped (${item.status}) for ${directorSessionId}: ${item.question}
71933
+ `
71934
+ );
71935
+ }
71936
+ deps.persistThreads();
71937
+ }
71938
+ if (closureBefore && !hadErrorOnTurn && outputEvents > 0) {
71939
+ const closureAfter = await takeClosureSnapshot();
71940
+ const closureText = closureAfter ? closureFor({ before: closureBefore, after: closureAfter, events: closureEvents, operatorText: msg.message ?? "" }) : null;
71941
+ if (closureText) {
71942
+ sendInTurn({ type: "event", directorSessionId, event: { type: "assistant", content: closureText } });
71943
+ }
71944
+ }
71945
+ if (contextBudgetGateTriggered) {
71946
+ sendInTurn({
71947
+ type: "event",
71948
+ directorSessionId,
71949
+ event: {
71950
+ type: "assistant",
71951
+ 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."
71952
+ }
71953
+ });
71954
+ }
71955
+ const telemetry = turnTokenUsage !== null ? { promptBytes, usage: turnTokenUsage, contextBudgetGateTriggered } : void 0;
70375
71956
  for (const event of deferredResults) {
70376
71957
  sendInTurn({ type: "event", directorSessionId, event, telemetry });
70377
71958
  }
@@ -70655,7 +72236,7 @@ async function startDirectorBridge() {
70655
72236
  await tokenRefresher.start();
70656
72237
  process.stderr.write("[mrrlin-mcp director-bridge] agent credential active; operator token auto-refresh enabled.\n");
70657
72238
  } else {
70658
- const apiToken = envToken ?? readOperatorToken() ?? void 0;
72239
+ let apiToken = envToken ?? readOperatorToken() ?? void 0;
70659
72240
  if (envToken) {
70660
72241
  try {
70661
72242
  persistOperatorToken(envToken);
@@ -70726,6 +72307,7 @@ async function startDirectorBridge() {
70726
72307
  acquireExecutorAgent: async (worktreePath, providerId) => {
70727
72308
  const executor = await startExecutorServer({
70728
72309
  worktree: worktreePath,
72310
+ disabledGitHooksDir: prepareDisabledGitHooksDir(import_node_path38.default.join(config2.stateDir, "locks"), import_node_path38.default.basename(worktreePath)),
70729
72311
  provider: providerId
70730
72312
  });
70731
72313
  return { handle: executor.handle, dispose: executor.dispose };
@@ -70764,7 +72346,7 @@ async function startDirectorBridge() {
70764
72346
  };
70765
72347
  const sessionSinks = createSessionSinkRegistry();
70766
72348
  const tombstones = /* @__PURE__ */ new Map();
70767
- const bridgeInstanceId = (0, import_node_crypto15.randomUUID)();
72349
+ const bridgeInstanceId = (0, import_node_crypto16.randomUUID)();
70768
72350
  const asyncConsumerDeps = {
70769
72351
  client,
70770
72352
  run: wrappedTurnLock.run,
@@ -70858,7 +72440,9 @@ async function startDirectorBridge() {
70858
72440
  reasoningEffort: normalizeReasoningEffort(entry.reasoningEffort),
70859
72441
  replay: null,
70860
72442
  supersededReplay: null,
70861
- provider: entry.provider
72443
+ provider: entry.provider,
72444
+ systemInstructionHash: entry.systemInstructionHash ?? null,
72445
+ ...entry.clarificationLedger ? { clarificationLedger: entry.clarificationLedger } : {}
70862
72446
  });
70863
72447
  }
70864
72448
  const persistThreads = () => {
@@ -70870,7 +72454,9 @@ async function startDirectorBridge() {
70870
72454
  lastUsedAt: thread.lastUsedAt,
70871
72455
  model: thread.model,
70872
72456
  reasoningEffort: thread.reasoningEffort,
70873
- provider: thread.provider
72457
+ provider: thread.provider,
72458
+ systemInstructionHash: thread.systemInstructionHash ?? null,
72459
+ ...thread.clarificationLedger ? { clarificationLedger: thread.clarificationLedger } : {}
70874
72460
  }
70875
72461
  ])
70876
72462
  );
@@ -71037,6 +72623,35 @@ async function startDirectorBridge() {
71037
72623
  }
71038
72624
  },
71039
72625
  listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
72626
+ snapshotProjectObjects: async (projectSlug, opts) => {
72627
+ const signal = opts?.signal;
72628
+ if (signal?.aborted) throw new Error("snapshot aborted");
72629
+ const tasks = await client.listTasks(
72630
+ projectSlug,
72631
+ { archived: "all", limit: CLOSURE_MAX_SNAPSHOT_TASKS + 1 },
72632
+ { view: "summary", signal }
72633
+ );
72634
+ if (tasks.length > CLOSURE_MAX_SNAPSHOT_TASKS) {
72635
+ throw new Error(`${tasks.length} tasks exceed the closure snapshot cap (${CLOSURE_MAX_SNAPSHOT_TASKS})`);
72636
+ }
72637
+ const [pages, inbox] = await Promise.all([
72638
+ client.listWikiPages(
72639
+ projectSlug,
72640
+ { folder: "Specs", limit: CLOSURE_MAX_SNAPSHOT_PAGES + 1 },
72641
+ { view: "summary", signal }
72642
+ ),
72643
+ client.listInboxItems(
72644
+ projectSlug,
72645
+ { status: "pending", limit: CLOSURE_MAX_SNAPSHOT_INBOX + 1 },
72646
+ { view: "summary", signal }
72647
+ )
72648
+ ]);
72649
+ return {
72650
+ inbox: inbox.length > CLOSURE_MAX_SNAPSHOT_INBOX ? null : new Set(inbox.map((item) => item.id)),
72651
+ tasks: new Map(tasks.map((task) => [task.id, { title: task.title, updatedAt: task.updatedAt ?? null }])),
72652
+ pages: pages.length > CLOSURE_MAX_SNAPSHOT_PAGES ? null : new Map(pages.map((page) => [page.id, { title: page.title, updatedAt: page.updatedAt }]))
72653
+ };
72654
+ },
71040
72655
  getProject: (projectSlug) => client.getProject(projectSlug),
71041
72656
  // External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
71042
72657
  // escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
@@ -71058,7 +72673,7 @@ async function startDirectorBridge() {
71058
72673
  const bridgeLogger = createBridgeLogger(config2.stateDir, process.env);
71059
72674
  let bridgeBinShaShort = "unknown00000";
71060
72675
  try {
71061
- bridgeBinShaShort = import_node_crypto15.default.createHash("sha256").update(import_node_fs23.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
72676
+ bridgeBinShaShort = import_node_crypto16.default.createHash("sha256").update(import_node_fs23.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
71062
72677
  } catch {
71063
72678
  }
71064
72679
  attachBridgeWss(server, {
@@ -71149,9 +72764,9 @@ async function startDirectorBridge() {
71149
72764
  res.writeHead(400).end();
71150
72765
  });
71151
72766
  });
71152
- await new Promise((resolve, reject) => {
72767
+ await new Promise((resolve2, reject) => {
71153
72768
  dispatchServer.on("error", reject);
71154
- dispatchServer.listen(workerPort(), "127.0.0.1", () => resolve());
72769
+ dispatchServer.listen(workerPort(), "127.0.0.1", () => resolve2());
71155
72770
  });
71156
72771
  const bridgeHealth = new BridgeHealthTracker({
71157
72772
  stateDir: config2.stateDir,
@@ -71342,7 +72957,7 @@ async function startDirectorBridge() {
71342
72957
  )
71343
72958
  });
71344
72959
  }
71345
- await new Promise((resolve, reject) => {
72960
+ await new Promise((resolve2, reject) => {
71346
72961
  server.on("error", (err) => {
71347
72962
  if (err.code === "EADDRINUSE") {
71348
72963
  const conflictingPids = scanForOtherBridges();
@@ -71358,7 +72973,7 @@ async function startDirectorBridge() {
71358
72973
  }
71359
72974
  reject(err);
71360
72975
  });
71361
- server.listen(config2.port, config2.host, () => resolve());
72976
+ server.listen(config2.port, config2.host, () => resolve2());
71362
72977
  });
71363
72978
  const orphans = scanForOtherBridges();
71364
72979
  if (orphans.length > 0) {
@@ -71499,10 +73114,10 @@ function timingSafeTokenEqual(provided, expected) {
71499
73114
  const a = Buffer.from(provided);
71500
73115
  const b = Buffer.from(expected);
71501
73116
  if (a.length !== b.length) return false;
71502
- return import_node_crypto15.default.timingSafeEqual(a, b);
73117
+ return import_node_crypto16.default.timingSafeEqual(a, b);
71503
73118
  }
71504
73119
  function readDispatchBody(req) {
71505
- return new Promise((resolve, reject) => {
73120
+ return new Promise((resolve2, reject) => {
71506
73121
  const chunks = [];
71507
73122
  let size = 0;
71508
73123
  req.on("data", (chunk) => {
@@ -71514,7 +73129,7 @@ function readDispatchBody(req) {
71514
73129
  }
71515
73130
  chunks.push(chunk);
71516
73131
  });
71517
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
73132
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
71518
73133
  req.on("error", reject);
71519
73134
  });
71520
73135
  }
@@ -71528,7 +73143,7 @@ function readOrCreateBridgeToken(stateDir) {
71528
73143
  } catch {
71529
73144
  }
71530
73145
  import_node_fs23.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
71531
- const token = import_node_crypto15.default.randomBytes(32).toString("base64url");
73146
+ const token = import_node_crypto16.default.randomBytes(32).toString("base64url");
71532
73147
  import_node_fs23.default.writeFileSync(tokenPath, `${token}
71533
73148
  `, { mode: 384 });
71534
73149
  return token;
@@ -71571,10 +73186,17 @@ function renderContextPrefix(context) {
71571
73186
  if (directorSessionId) payload.directorSessionId = directorSessionId;
71572
73187
  if (contextMessages?.length) payload.contextMessages = contextMessages;
71573
73188
  if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
73189
+ const openQuestions = context?.openQuestions?.length ? context.openQuestions : void 0;
73190
+ if (openQuestions) payload.openQuestions = openQuestions;
71574
73191
  const keys = Object.keys(payload);
71575
73192
  if (keys.length > 0) {
71576
73193
  parts.push(`Context (JSON): ${JSON.stringify(payload)}`);
71577
73194
  }
73195
+ if (openQuestions) {
73196
+ parts.push(
73197
+ "[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."
73198
+ );
73199
+ }
71578
73200
  return parts.join("\n\n");
71579
73201
  }
71580
73202
 
@@ -72219,12 +73841,12 @@ var StdioServerTransport = class {
72219
73841
  this.onclose?.();
72220
73842
  }
72221
73843
  send(message) {
72222
- return new Promise((resolve) => {
73844
+ return new Promise((resolve2) => {
72223
73845
  const json2 = serializeMessage(message);
72224
73846
  if (this._stdout.write(json2)) {
72225
- resolve();
73847
+ resolve2();
72226
73848
  } else {
72227
- this._stdout.once("drain", resolve);
73849
+ this._stdout.once("drain", resolve2);
72228
73850
  }
72229
73851
  });
72230
73852
  }
@@ -72299,7 +73921,9 @@ function jsonSchemaProperty(schema) {
72299
73921
  property.enum = jsonSchema.enum;
72300
73922
  }
72301
73923
  if (typeof jsonSchema.pattern === "string") {
72302
- property.pattern = jsonSchema.pattern;
73924
+ if (!/\(\?<?[!=]/.test(jsonSchema.pattern)) {
73925
+ property.pattern = jsonSchema.pattern;
73926
+ }
72303
73927
  }
72304
73928
  return property;
72305
73929
  }
@@ -72909,7 +74533,8 @@ function installService(deps) {
72909
74533
  return { ok: false };
72910
74534
  }
72911
74535
  const claudeBin = deps.which("claude");
72912
- const resolvedBins = [...bins, claudeBin].filter((b) => b !== null);
74536
+ const agyBin = deps.which("agy");
74537
+ const resolvedBins = [...bins, claudeBin, agyBin].filter((b) => b !== null);
72913
74538
  const isWin = import_node_os18.default.platform() === "win32";
72914
74539
  const systemDirs = isWin ? (deps.env.PATH || "").split(import_node_path39.default.delimiter).filter(Boolean) : SYSTEM_PATH_DIRS;
72915
74540
  const pathEnv = Array.from(
@@ -73184,6 +74809,24 @@ Missing required flag(s): ${missing.join(
73184
74809
  }
73185
74810
 
73186
74811
  // src/bin.ts
74812
+ function augmentProcessPathWithCommonDirs() {
74813
+ const isWin = process.platform === "win32";
74814
+ if (isWin) return;
74815
+ const home = process.env.HOME || (0, import_node_os20.homedir)();
74816
+ const candidates = [
74817
+ import_node_path43.default.join(home, ".local", "bin"),
74818
+ import_node_path43.default.join(home, ".cargo", "bin"),
74819
+ "/usr/local/bin",
74820
+ "/opt/homebrew/bin"
74821
+ ];
74822
+ const existing = (process.env.PATH || "").split(import_node_path43.default.delimiter).filter(Boolean);
74823
+ const existingSet = new Set(existing);
74824
+ const toAdd = candidates.filter((d) => !existingSet.has(d) && (0, import_node_fs25.existsSync)(d));
74825
+ if (toAdd.length > 0) {
74826
+ process.env.PATH = [...existing, ...toAdd].join(import_node_path43.default.delimiter);
74827
+ }
74828
+ }
74829
+ augmentProcessPathWithCommonDirs();
73187
74830
  function pm2Runner(args) {
73188
74831
  const r = (0, import_node_child_process22.spawnSync)("pm2", args, { encoding: "utf8" });
73189
74832
  process.stderr.write(r.stdout ?? "");
@@ -73690,6 +75333,10 @@ main().catch((error51) => {
73690
75333
  `);
73691
75334
  process.exit(1);
73692
75335
  });
75336
+ // Annotate the CommonJS export names for ESM import in node:
75337
+ 0 && (module.exports = {
75338
+ augmentProcessPathWithCommonDirs
75339
+ });
73693
75340
  /*! Bundled license information:
73694
75341
 
73695
75342
  content-type/index.js: