@coreplane/switchboard 1.236.0 → 1.238.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/assets/deploy/cloudflare-resident/worker.ts +210 -74
  2. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +36 -10
  3. package/dist/assets/package-lock.json +3 -3
  4. package/dist/assets/package.json +1 -1
  5. package/dist/assets/source.json +3 -3
  6. package/dist/assets/src/core/authz/authorize.ts +25 -6
  7. package/dist/assets/src/core/authz/policy.ts +9 -0
  8. package/dist/assets/src/core/authz/types.ts +15 -1
  9. package/dist/assets/src/core/coordinator/driver.ts +5 -1
  10. package/dist/assets/src/core/runEvents.ts +10 -6
  11. package/dist/assets/src/core/ship/coordinator.ts +23 -1
  12. package/dist/assets/src/execution/residentDepsStore.ts +14 -2
  13. package/dist/assets/src/execution/residentDiskBudget.ts +45 -11
  14. package/dist/assets/src/execution/residentHead.ts +34 -8
  15. package/dist/assets/src/execution/residentRebind.ts +88 -31
  16. package/dist/assets/src/execution/sandboxLifecycle.ts +33 -0
  17. package/dist/assets/src/execution/snapshotRetention.ts +62 -0
  18. package/dist/assets/src/mcp/registry.ts +9 -1
  19. package/dist/assets/web/dist/.vite/manifest.json +408 -408
  20. package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
  21. package/dist/assets/web/dist/assets/{CostsPage-BfJsol9y.js → CostsPage-DKk9OjB0.js} +1 -1
  22. package/dist/assets/web/dist/assets/{DeliveryPage-CvInfu7y.js → DeliveryPage-Kz6js49i.js} +1 -1
  23. package/dist/assets/web/dist/assets/{NotFoundPage-d37EP8hS.js → NotFoundPage-BXLrQQzy.js} +1 -1
  24. package/dist/assets/web/dist/assets/{ResidentDetailPage-DQSD0SZD.js → ResidentDetailPage-CrfCd8fx.js} +1 -1
  25. package/dist/assets/web/dist/assets/ResidentsIndexPage-BdW0k2h3.js +1 -0
  26. package/dist/assets/web/dist/assets/{RunFoldRow-Bngn4GjF.js → RunFoldRow-CbTd9SiS.js} +1 -1
  27. package/dist/assets/web/dist/assets/{RunRoutePage-a-tMrirB.js → RunRoutePage-br0H-8Pz.js} +3 -3
  28. package/dist/assets/web/dist/assets/RunsIndexPage-Bnzb15zH.js +1 -0
  29. package/dist/assets/web/dist/assets/{RunsTabs-CyCdOhJ2.js → RunsTabs-XpCqeWu4.js} +1 -1
  30. package/dist/assets/web/dist/assets/{ScheduledPage-ByCmyrDs.js → ScheduledPage-NLhjzHAz.js} +1 -1
  31. package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +1 -0
  32. package/dist/assets/web/dist/assets/{StatusDot-D-BPSPpw.js → StatusDot-DkXALvKf.js} +1 -1
  33. package/dist/assets/web/dist/assets/{Tooltip-DPDx5tXT.js → Tooltip-DtLIyi5P.js} +1 -1
  34. package/dist/assets/web/dist/assets/{UnitRoutePage-0eiekTHQ.js → UnitRoutePage-DGXukEAH.js} +1 -1
  35. package/dist/assets/web/dist/assets/{angular-html-Ni1Xil2G.js → angular-html-DYtV9uIN.js} +1 -1
  36. package/dist/assets/web/dist/assets/{angular-ts-oCgo6p05.js → angular-ts-DF2P25Zp.js} +1 -1
  37. package/dist/assets/web/dist/assets/{apl-CiWYl0gh.js → apl-Ff96wxko.js} +1 -1
  38. package/dist/assets/web/dist/assets/{astro-B-VeKZ_J.js → astro-LbBnZna-.js} +1 -1
  39. package/dist/assets/web/dist/assets/{blade-BIWEISKd.js → blade-DltuxeBW.js} +1 -1
  40. package/dist/assets/web/dist/assets/{c-DHxA5Byp.js → c-DBg7yuPx.js} +1 -1
  41. package/dist/assets/web/dist/assets/{chapel-D4qGLw-Q.js → chapel-C83hUAds.js} +1 -1
  42. package/dist/assets/web/dist/assets/{cobol-BznX5Q-u.js → cobol-DB0yV_2D.js} +1 -1
  43. package/dist/assets/web/dist/assets/{coffee-CVr8Idlj.js → coffee-Bpih6Kzl.js} +1 -1
  44. package/dist/assets/web/dist/assets/{cpp-wrk6NfW4.js → cpp-CU00ZeRn.js} +1 -1
  45. package/dist/assets/web/dist/assets/{crystal-CGYWMeH1.js → crystal-f8ImJkux.js} +1 -1
  46. package/dist/assets/web/dist/assets/{css-BgGytMsy.js → css-4VrC4ISo.js} +1 -1
  47. package/dist/assets/web/dist/assets/{dist-DSkVB16z.js → dist-DtA7JiH5.js} +2 -2
  48. package/dist/assets/web/dist/assets/{durationTone-DYogEPEk.js → durationTone-CYMux25J.js} +1 -1
  49. package/dist/assets/web/dist/assets/{edge-CdR6AU9v.js → edge-BwFDzfS_.js} +1 -1
  50. package/dist/assets/web/dist/assets/{elixir-CQRb3PZx.js → elixir-wwIYIpJm.js} +1 -1
  51. package/dist/assets/web/dist/assets/{elm-CpuA6W8B.js → elm-DyDuM3v3.js} +1 -1
  52. package/dist/assets/web/dist/assets/{erb-CvlrblqH.js → erb-DFJBEVrh.js} +1 -1
  53. package/dist/assets/web/dist/assets/{git-rebase-COnovCVc.js → git-rebase-CuDv-44X.js} +1 -1
  54. package/dist/assets/web/dist/assets/{glimmer-js-C-DS4g_J.js → glimmer-js-ZFnrm4J3.js} +1 -1
  55. package/dist/assets/web/dist/assets/{glimmer-ts-DTkorWRI.js → glimmer-ts-XY4Yqshp.js} +1 -1
  56. package/dist/assets/web/dist/assets/{glsl-CH51-JW7.js → glsl-xnR8efx0.js} +1 -1
  57. package/dist/assets/web/dist/assets/{graphql-BO-XymNv.js → graphql-D-gO5tnP.js} +1 -1
  58. package/dist/assets/web/dist/assets/{hack-C-N0oicf.js → hack-CSxbW4ax.js} +1 -1
  59. package/dist/assets/web/dist/assets/{haml-BfmdtWoP.js → haml-D4ar42QT.js} +1 -1
  60. package/dist/assets/web/dist/assets/{handlebars-BqhO_te-.js → handlebars-CMLbDE7o.js} +1 -1
  61. package/dist/assets/web/dist/assets/{html-DUEZlTKV.js → html-CVekU4Fs.js} +1 -1
  62. package/dist/assets/web/dist/assets/{html-derivative-DOUJTS8A.js → html-derivative-St3HHw_N.js} +1 -1
  63. package/dist/assets/web/dist/assets/{http-j7UH0Cqv.js → http-Mon0DWHS.js} +1 -1
  64. package/dist/assets/web/dist/assets/{hurl-D6ZaBLJk.js → hurl-Cbl5IvBs.js} +1 -1
  65. package/dist/assets/web/dist/assets/{indexRow-BjXBzI5Z.js → indexRow-FLJu5mR9.js} +1 -1
  66. package/dist/assets/web/dist/assets/{java-dugWx3wi.js → java-Bprck1l-.js} +1 -1
  67. package/dist/assets/web/dist/assets/{javascript-CK4GC4JO.js → javascript-C73G1-UR.js} +1 -1
  68. package/dist/assets/web/dist/assets/{jinja-DrLKIyKG.js → jinja-C4DuRxYJ.js} +1 -1
  69. package/dist/assets/web/dist/assets/{jison-CS1Th-TO.js → jison-B2U03GEC.js} +1 -1
  70. package/dist/assets/web/dist/assets/{json-CkBqdVys.js → json-CvTZUDzx.js} +1 -1
  71. package/dist/assets/web/dist/assets/{jsx-CCxdg7n3.js → jsx-DMWKCzwo.js} +1 -1
  72. package/dist/assets/web/dist/assets/{julia-BalKW2hA.js → julia-Czn7bV_3.js} +1 -1
  73. package/dist/assets/web/dist/assets/{just-BKw1G40D.js → just-BBxpNfvC.js} +1 -1
  74. package/dist/assets/web/dist/assets/{latex-Dfad6ZN-.js → latex-Bw9yg5mr.js} +1 -1
  75. package/dist/assets/web/dist/assets/{liquid-f_CMGBvd.js → liquid-BRCjaydE.js} +1 -1
  76. package/dist/assets/web/dist/assets/{lua-BcMK_uKM.js → lua-DUu2KIC0.js} +1 -1
  77. package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +1 -0
  78. package/dist/assets/web/dist/assets/{main-DR8iKpNt.js → main-Dht0in4v.js} +2 -2
  79. package/dist/assets/web/dist/assets/{marko-B0oNLx2i.js → marko-CZJZIlSQ.js} +1 -1
  80. package/dist/assets/web/dist/assets/{mdc-Bc_MmIyq.js → mdc-BmClq6fd.js} +1 -1
  81. package/dist/assets/web/dist/assets/{nginx-Cmxv8RIt.js → nginx-Bmt3xSEm.js} +1 -1
  82. package/dist/assets/web/dist/assets/{nim-BtgRfaY0.js → nim-Da-i0utK.js} +1 -1
  83. package/dist/assets/web/dist/assets/{org-Dl9EzlHh.js → org-BvAre2qP.js} +1 -1
  84. package/dist/assets/web/dist/assets/{perl-BmF7x7BB.js → perl-v9WwFFtB.js} +1 -1
  85. package/dist/assets/web/dist/assets/{php-rH6CVptQ.js → php-DTU4Zyze.js} +1 -1
  86. package/dist/assets/web/dist/assets/{pug-CIN5Ccck.js → pug-DhTwEqTO.js} +1 -1
  87. package/dist/assets/web/dist/assets/{qml-BpLS3RFr.js → qml-BUD9AC_c.js} +1 -1
  88. package/dist/assets/web/dist/assets/{r-BUMF3B-H.js → r-CAnu56n7.js} +1 -1
  89. package/dist/assets/web/dist/assets/{razor-B7sTBPdT.js → razor-Dn0P4gAq.js} +1 -1
  90. package/dist/assets/web/dist/assets/{regexp-h2SvwrsJ.js → regexp-CZyBRaVW.js} +1 -1
  91. package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
  92. package/dist/assets/web/dist/assets/{rst-DLOWEsD2.js → rst-Ckyd5UsY.js} +1 -1
  93. package/dist/assets/web/dist/assets/{ruby-Cl_-I4k3.js → ruby-BBQ62fsx.js} +1 -1
  94. package/dist/assets/web/dist/assets/{sas-Cm4M-BvV.js → sas-CW35hMxG.js} +1 -1
  95. package/dist/assets/web/dist/assets/{scss-CEFCEQUF.js → scss-CpV8pF4J.js} +1 -1
  96. package/dist/assets/web/dist/assets/{shellscript-CE6zb5eS.js → shellscript-CRJvsmE1.js} +1 -1
  97. package/dist/assets/web/dist/assets/{shellsession-B4fDVzrM.js → shellsession-CyriUZxf.js} +1 -1
  98. package/dist/assets/web/dist/assets/{soy-DwXCMpjE.js → soy-D5QzYUeg.js} +1 -1
  99. package/dist/assets/web/dist/assets/{sql-DrnAKnyD.js → sql-CUBf_Mse.js} +1 -1
  100. package/dist/assets/web/dist/assets/{stata-BzlwPWD6.js → stata-B-4n4ITz.js} +1 -1
  101. package/dist/assets/web/dist/assets/{surrealql-CeUieZXd.js → surrealql-RuWZBLtr.js} +1 -1
  102. package/dist/assets/web/dist/assets/{svelte-BERIpIKR.js → svelte-CO5YzCsK.js} +1 -1
  103. package/dist/assets/web/dist/assets/{templ-6FSuUDwg.js → templ-DbkWVaBF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{tex-SWCyMwNX.js → tex-IuxoEcId.js} +1 -1
  105. package/dist/assets/web/dist/assets/{ts-tags-VWj7gWgY.js → ts-tags-g6dGQPjd.js} +1 -1
  106. package/dist/assets/web/dist/assets/{tsx-B6dzfL8L.js → tsx-WnXwiyH2.js} +1 -1
  107. package/dist/assets/web/dist/assets/{twig-BDXsr482.js → twig-Lx1G7PnY.js} +1 -1
  108. package/dist/assets/web/dist/assets/{typescript-s100Dt8y.js → typescript-BQd7biLF.js} +1 -1
  109. package/dist/assets/web/dist/assets/{typst-WfyEAUdt.js → typst-D2lXPHK7.js} +1 -1
  110. package/dist/assets/web/dist/assets/{vue-CkXOOJ-6.js → vue-4wVQD9y9.js} +1 -1
  111. package/dist/assets/web/dist/assets/{vue-html-CEpzdcau.js → vue-html-DZPaRte4.js} +1 -1
  112. package/dist/assets/web/dist/assets/{vue-vine-D9dsfiFA.js → vue-vine-DbmZCmu-.js} +1 -1
  113. package/dist/assets/web/dist/assets/{xml-iBDofbJq.js → xml-DV16szyo.js} +1 -1
  114. package/dist/assets/web/dist/assets/{xsl-CqbJF4hD.js → xsl-eo5-1sk0.js} +1 -1
  115. package/dist/assets/web/dist/assets/{yaml-D9i6OxYe.js → yaml-BXdX8HZf.js} +1 -1
  116. package/dist/cli.js +1143 -542
  117. package/package.json +1 -1
  118. package/dist/assets/web/dist/assets/AppShell-DZdz_sl7.js +0 -1
  119. package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +0 -1
  120. package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +0 -1
  121. package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +0 -1
  122. package/dist/assets/web/dist/assets/main-B77vAZVg.css +0 -1
  123. package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +0 -1
package/dist/cli.js CHANGED
@@ -697,7 +697,7 @@ function validatePolicy(rules) {
697
697
  }
698
698
  }
699
699
  }
700
- var grant, MEMBER_OF, IS_SELF, OWNER_OF, ALL_CHANNELS, POLICY, ACTOR_KINDS, CONDITION_KINDS, REQUIRED_ATTRIBUTE, PLACEHOLDER, PolicyError;
700
+ var grant, MEMBER_OF, IS_SELF, ACTS_AS_PERSON, OWNER_OF, ALL_CHANNELS, POLICY, ACTOR_KINDS, CONDITION_KINDS, REQUIRED_ATTRIBUTE, PLACEHOLDER, PolicyError;
701
701
  var init_policy = __esm({
702
702
  "../../src/core/authz/policy.ts"() {
703
703
  "use strict";
@@ -705,6 +705,7 @@ var init_policy = __esm({
705
705
  grant = (g) => ({ kind: "has-grant", grant: g });
706
706
  MEMBER_OF = { kind: "member-of" };
707
707
  IS_SELF = { kind: "is-self" };
708
+ ACTS_AS_PERSON = { kind: "acts-as-person" };
708
709
  OWNER_OF = { kind: "owner-of" };
709
710
  ALL_CHANNELS = { kind: "all-channels" };
710
711
  POLICY = [
@@ -799,6 +800,13 @@ var init_policy = __esm({
799
800
  // ── mcp (external MCP servers live in the three config tiers) ────────────
800
801
  { action: "mcp:read", resource: "command", when: [grant("mcp:read")] },
801
802
  { action: "mcp:write", resource: "command", when: [grant("mcp:write")] },
803
+ // A person always has their own tier to write (`mcp add|connect|remove`
804
+ // for themselves), as `config set me` already is theirs: the row admits an
805
+ // actor whose `self` names a chat identity — every chat user (who holds
806
+ // `mcp:write` by baseline anyway) and a dashboard session linked to its
807
+ // person (record 0042) — never an unlinked browser session or a credential.
808
+ // Which tier the write reaches stays the handler's question below.
809
+ { action: "mcp:write", resource: "command", actorKinds: ["user"], when: [ACTS_AS_PERSON] },
802
810
  // A CHANNEL's servers: the channel-config right for a person; a credential
803
811
  // an admin minted with `mcp:write` manages any tier it can name.
804
812
  { action: "mcp:write", resource: "config-scope", resourceKind: "channel", when: [grant("config:write")] },
@@ -864,6 +872,7 @@ var init_policy = __esm({
864
872
  "has-grant",
865
873
  "member-of",
866
874
  "is-self",
875
+ "acts-as-person",
867
876
  "owner-of",
868
877
  "all-channels"
869
878
  ];
@@ -918,13 +927,20 @@ function principalOf(actor) {
918
927
  while (current.onBehalfOf) current = current.onBehalfOf;
919
928
  return current;
920
929
  }
930
+ function selfIdsOf(actor) {
931
+ const principal = principalOf(actor);
932
+ return principal.self && principal.self.length > 0 ? principal.self : [principal.id];
933
+ }
934
+ function actsAsPerson(selfIds) {
935
+ return selfIds.some((id) => id.startsWith("slack:"));
936
+ }
921
937
  function deny(reason2) {
922
938
  return { allow: false, reason: reason2 };
923
939
  }
924
940
  function isKnownActorKind(kind) {
925
941
  return ACTOR_KINDS.includes(kind);
926
942
  }
927
- function evaluateCondition(condition, grants, selfId, attributes) {
943
+ function evaluateCondition(condition, grants, selfIds, attributes) {
928
944
  switch (condition.kind) {
929
945
  case "has-grant": {
930
946
  const grant2 = resolveGrant(condition.grant, attributes);
@@ -933,7 +949,9 @@ function evaluateCondition(condition, grants, selfId, attributes) {
933
949
  case "member-of":
934
950
  return attributes.channelId !== void 0 && holds(grants.channels, attributes.channelId) || attributes.channelVisibility === "public";
935
951
  case "is-self":
936
- return attributes.userId !== void 0 && attributes.userId === selfId;
952
+ return attributes.userId !== void 0 && selfIds.includes(attributes.userId);
953
+ case "acts-as-person":
954
+ return actsAsPerson(selfIds);
937
955
  case "owner-of":
938
956
  return attributes.repo !== void 0 && holds(grants.repos, attributes.repo);
939
957
  case "all-channels":
@@ -954,10 +972,10 @@ function authorizeWith(rules, actor, action, resource) {
954
972
  );
955
973
  if (forOrigin.length === 0) return deny("origin-visibility");
956
974
  const grants = effectiveGrants(actor);
957
- const selfId = principalOf(actor).id;
975
+ const selfIds = selfIdsOf(actor);
958
976
  let reason2;
959
977
  for (const rule of forOrigin) {
960
- const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfId, attributes));
978
+ const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes));
961
979
  if (!failed) return ALLOW;
962
980
  reason2 ??= FAILURE_REASON[failed.kind];
963
981
  }
@@ -975,6 +993,7 @@ var init_authorize = __esm({
975
993
  ALLOW = Object.freeze({ allow: true });
976
994
  FAILURE_REASON = {
977
995
  "has-grant": "missing-grant",
996
+ "acts-as-person": "not-person",
978
997
  "member-of": "not-member",
979
998
  "is-self": "not-self",
980
999
  "owner-of": "not-owner",
@@ -2540,6 +2559,9 @@ function unitSlug(unit) {
2540
2559
  function unitBranch(planId, slug) {
2541
2560
  return `plan/${planId}/${slug}`;
2542
2561
  }
2562
+ function isUnitBranch(ref) {
2563
+ return /^plan\/[^/]+\/[^/]+$/.test(ref);
2564
+ }
2543
2565
  function parsePlanBranch(branch2) {
2544
2566
  const m = PLAN_BRANCH.exec(branch2);
2545
2567
  return m ? { planId: m[1], unitSlug: m[2] } : void 0;
@@ -3247,7 +3269,7 @@ function isOutcome(v) {
3247
3269
  function isMcpServerEntry(v) {
3248
3270
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
3249
3271
  const e = v;
3250
- return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.headersEnv === void 0 || isHeadersEnv(e.headersEnv)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt));
3272
+ return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.headersEnv === void 0 || isHeadersEnv(e.headersEnv)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt)) && (e.promotedFrom === void 0 || isStr(e.promotedFrom, 260));
3251
3273
  }
3252
3274
  function isSealedCredential(v) {
3253
3275
  if (!v || typeof v !== "object") return false;
@@ -3273,7 +3295,8 @@ function serverView(scopeKey, name, entry, opts) {
3273
3295
  state,
3274
3296
  source: opts.source,
3275
3297
  ...entry.addedBy ? { addedBy: entry.addedBy } : {},
3276
- ...entry.addedAt !== void 0 ? { addedAt: entry.addedAt } : {}
3298
+ ...entry.addedAt !== void 0 ? { addedAt: entry.addedAt } : {},
3299
+ ...entry.promotedFrom ? { promotedFrom: entry.promotedFrom } : {}
3277
3300
  };
3278
3301
  }
3279
3302
  function safeUrl(raw) {
@@ -4134,6 +4157,15 @@ var init_config2 = __esm({
4134
4157
  * caller's right to read each channel is decided; `config show --channel`
4135
4158
  * is the read that carries the values.
4136
4159
  */
4160
+ /** Every channel and every user with a tier of its own — a static block or a
4161
+ * runtime override — sorted: what `mcp list --all` walks (record 0042). */
4162
+ mcpTierIds() {
4163
+ const ids = (stat, runtime) => [.../* @__PURE__ */ new Set([...Object.keys(stat ?? {}), ...Object.keys(runtime)])].sort();
4164
+ return {
4165
+ channels: ids(this.config.channels, this.overrides.channels),
4166
+ users: ids(this.config.users, this.overrides.users)
4167
+ };
4168
+ }
4137
4169
  channelsWithScope() {
4138
4170
  const ids = /* @__PURE__ */ new Set([...Object.keys(this.config.channels ?? {}), ...Object.keys(this.overrides.channels)]);
4139
4171
  const rows = [];
@@ -10044,10 +10076,15 @@ function wakePause(ms, signal2) {
10044
10076
  });
10045
10077
  }
10046
10078
  function reboundOf(value) {
10079
+ const move = returnedOf(value);
10080
+ return move !== void 0 && move.pr !== void 0 ? { from: move.from, to: move.to, pr: move.pr } : void 0;
10081
+ }
10082
+ function returnedOf(value) {
10047
10083
  if (typeof value !== "object" || value === null) return void 0;
10048
10084
  const v = value;
10049
- if (typeof v.from !== "string" || typeof v.to !== "string" || typeof v.pr !== "number") return void 0;
10050
- if (!v.from || !v.to || !Number.isSafeInteger(v.pr) || v.pr <= 0) return void 0;
10085
+ if (typeof v.from !== "string" || typeof v.to !== "string" || !v.from || !v.to) return void 0;
10086
+ if (v.pr === void 0) return { from: v.from, to: v.to };
10087
+ if (typeof v.pr !== "number" || !Number.isSafeInteger(v.pr) || v.pr <= 0) return void 0;
10051
10088
  return { from: v.from, to: v.to, pr: v.pr };
10052
10089
  }
10053
10090
  function rebindRefusedOf(value) {
@@ -10288,7 +10325,7 @@ var init_resident = __esm({
10288
10325
  const trace = sanitizeGraftedSteps(data.trace);
10289
10326
  const rebound = reboundOf(data.rebound);
10290
10327
  const rebindRefused = rebindRefusedOf(data.rebindRefused);
10291
- const returned = reboundOf(data.returned);
10328
+ const returned = returnedOf(data.returned);
10292
10329
  this.lastBinding = {
10293
10330
  ref: data.ref,
10294
10331
  sha: data.sha,
@@ -10858,7 +10895,7 @@ function rebindLabel(binding) {
10858
10895
  if (binding.rebound) return ` \xB7 rebound to ${binding.rebound.to} (this thread's PR #${binding.rebound.pr})`;
10859
10896
  if (binding.returned) {
10860
10897
  const r = binding.returned;
10861
- return ` \xB7 returned to ${r.to} (the branch of this thread's PR #${r.pr} is gone)`;
10898
+ return r.pr !== void 0 ? ` \xB7 returned to ${r.to} (the branch of this thread's PR #${r.pr} is gone)` : ` \xB7 returned to ${r.to} (this thread's branch ${r.from} is gone)`;
10862
10899
  }
10863
10900
  if (binding.rebindRefused) {
10864
10901
  const r = binding.rebindRefused;
@@ -15232,6 +15269,7 @@ var init_commandRegistry = __esm({
15232
15269
  commandId: cmd.id,
15233
15270
  callerKind: caller.kind,
15234
15271
  callerId: caller.id,
15272
+ ...caller.actor.asUser ? { asUser: caller.actor.asUser.id } : {},
15235
15273
  effect: cmd.effect,
15236
15274
  outcome: res.ok ? "ok" : res.error,
15237
15275
  ...reason2 === void 0 ? {} : { reason: reason2 },
@@ -15416,8 +15454,14 @@ function assertMayEditChannel(caller, channel) {
15416
15454
  if (!mayEditChannel(caller, channel))
15417
15455
  throw new CommandError("unauthorized", "Channel config changes are restricted.");
15418
15456
  }
15419
- function assertMeWritable(caller) {
15420
- if (caller.kind === "access") throw new CommandError("unauthorized", ME_ON_ACCESS_MESSAGE);
15457
+ function meIdOf(caller) {
15458
+ if (caller.kind !== "access") return caller.id;
15459
+ return caller.actor.self?.find((id) => id.startsWith("slack:"));
15460
+ }
15461
+ function meIdOrRefuse(caller) {
15462
+ const id = meIdOf(caller);
15463
+ if (id === void 0) throw new CommandError("unauthorized", ME_ON_ACCESS_MESSAGE);
15464
+ return id;
15421
15465
  }
15422
15466
  async function mayReadChannel(caller, channel, deps) {
15423
15467
  const origin = caller.origin?.channelId;
@@ -15477,7 +15521,7 @@ var init_config4 = __esm({
15477
15521
  const channel = targetChannel(caller, options.channel);
15478
15522
  await assertMayReadChannel(caller, channel, deps);
15479
15523
  const description = {
15480
- ...await deps.config.describeConfig(channel, caller.id),
15524
+ ...await deps.config.describeConfig(channel, meIdOf(caller) ?? caller.id),
15481
15525
  channelConfigRestricted: !mayEditChannel(caller, channel)
15482
15526
  };
15483
15527
  return description;
@@ -15570,8 +15614,7 @@ var init_config4 = __esm({
15570
15614
  assertMayEditChannel(caller, channel);
15571
15615
  effective = await deps.config.setChannelOverride(channel, patch);
15572
15616
  } else {
15573
- assertMeWritable(caller);
15574
- effective = await deps.config.setUserOverride(caller.id, patch);
15617
+ effective = await deps.config.setUserOverride(meIdOrRefuse(caller), patch);
15575
15618
  }
15576
15619
  return { scope: args.scope, effective: summarizeScope(effective) };
15577
15620
  }
@@ -15590,8 +15633,7 @@ var init_config4 = __esm({
15590
15633
  assertMayEditChannel(caller, channel);
15591
15634
  await deps.config.clearChannelOverride(channel);
15592
15635
  } else {
15593
- assertMeWritable(caller);
15594
- await deps.config.clearUserOverride(caller.id);
15636
+ await deps.config.clearUserOverride(meIdOrRefuse(caller));
15595
15637
  }
15596
15638
  return { scope: args.scope, cleared: true };
15597
15639
  }
@@ -15633,7 +15675,7 @@ ${quote(current ?? "")}`;
15633
15675
  handler: async ({ args, options, caller, deps }) => {
15634
15676
  const channel = args.scope === "channel" ? targetChannel(caller, options.channel) : void 0;
15635
15677
  if (args.text === void 0 && channel !== void 0) await assertMayReadChannel(caller, channel, deps);
15636
- const scopes = await deps.config.scopes(channel ?? caller.origin?.channelId ?? "", caller.id);
15678
+ const scopes = await deps.config.scopes(channel ?? caller.origin?.channelId ?? "", meIdOf(caller) ?? caller.id);
15637
15679
  const current = (args.scope === "channel" ? scopes.channel : scopes.user).instructions?.trim();
15638
15680
  if (args.text === void 0)
15639
15681
  return { scope: args.scope, action: "show", ...current ? { instructions: current } : {} };
@@ -15650,8 +15692,7 @@ ${quote(current ?? "")}`;
15650
15692
  assertMayEditChannel(caller, channel);
15651
15693
  effective = await deps.config.setChannelOverride(channel, patch);
15652
15694
  } else {
15653
- assertMeWritable(caller);
15654
- effective = await deps.config.setUserOverride(caller.id, patch);
15695
+ effective = await deps.config.setUserOverride(meIdOrRefuse(caller), patch);
15655
15696
  }
15656
15697
  if (text.length === 0) {
15657
15698
  const fromStatic = effective.instructions?.trim();
@@ -15782,7 +15823,25 @@ function anyOf(alternatives) {
15782
15823
  if (flat.length === 1) return flat[0];
15783
15824
  return { kind: "or", of: flat };
15784
15825
  }
15785
- function compileCondition(condition, grants, selfId) {
15826
+ function allOf(parts) {
15827
+ const flat = [];
15828
+ for (const p of parts) {
15829
+ if (p.kind === "none") return NONE;
15830
+ if (p.kind === "all") continue;
15831
+ if (p.kind === "and") flat.push(...p.of);
15832
+ else flat.push(p);
15833
+ }
15834
+ if (flat.length === 0) return ALL;
15835
+ if (flat.length === 1) return flat[0];
15836
+ return { kind: "and", of: flat };
15837
+ }
15838
+ function selfPredicate(selfIds) {
15839
+ return anyOf(selfIds.map((userId) => ({ kind: "user-is", userId })));
15840
+ }
15841
+ function ownedBy(actor) {
15842
+ return selfPredicate(selfIdsOf(actor));
15843
+ }
15844
+ function compileCondition(condition, grants, selfIds) {
15786
15845
  switch (condition.kind) {
15787
15846
  case "has-grant": {
15788
15847
  if (grants.actions === "all") return ALL;
@@ -15793,7 +15852,9 @@ function compileCondition(condition, grants, selfId) {
15793
15852
  if (grants.channels === "all") return ALL;
15794
15853
  return anyOf([grants.channels.size === 0 ? NONE : { kind: "channels-in", channelIds: grants.channels }, PUBLIC]);
15795
15854
  case "is-self":
15796
- return { kind: "user-is", userId: selfId };
15855
+ return selfPredicate(selfIds);
15856
+ case "acts-as-person":
15857
+ return actsAsPerson(selfIds) ? ALL : NONE;
15797
15858
  case "owner-of":
15798
15859
  if (grants.repos === "all") return ALL;
15799
15860
  return grants.repos.size === 0 ? NONE : { kind: "repos-in", repos: grants.repos };
@@ -15802,17 +15863,9 @@ function compileCondition(condition, grants, selfId) {
15802
15863
  }
15803
15864
  return NONE;
15804
15865
  }
15805
- function compileRule(rule, grants, selfId) {
15866
+ function compileRule(rule, grants, selfIds) {
15806
15867
  if (rule.originVisibility) return NONE;
15807
- const parts = [];
15808
- for (const condition of rule.when) {
15809
- const part = compileCondition(condition, grants, selfId);
15810
- if (part.kind === "none") return NONE;
15811
- if (part.kind !== "all") parts.push(part);
15812
- }
15813
- if (parts.length === 0) return ALL;
15814
- if (parts.length === 1) return parts[0];
15815
- return { kind: "and", of: parts };
15868
+ return allOf(rule.when.map((condition) => compileCondition(condition, grants, selfIds)));
15816
15869
  }
15817
15870
  function predicateWith(rules, actor, action, resourceType, kind) {
15818
15871
  if (RESOURCE_KINDS[resourceType] && kind === void 0) {
@@ -15822,12 +15875,12 @@ function predicateWith(rules, actor, action, resourceType, kind) {
15822
15875
  if (!target) throw new TypeError(`authz predicate: no target for ${resourceType}${kind ? `/${kind}` : ""}`);
15823
15876
  if (!isKnownActorKind(actor.kind)) return NONE;
15824
15877
  const grants = effectiveGrants(actor);
15825
- const selfId = principalOf(actor).id;
15878
+ const selfIds = selfIdsOf(actor);
15826
15879
  const alternatives = [];
15827
15880
  for (const rule of rules) {
15828
15881
  if (rule.action !== action || ruleTarget(rule) !== target) continue;
15829
15882
  if (rule.actorKinds && !rule.actorKinds.includes(actor.kind)) continue;
15830
- alternatives.push(compileRule(rule, grants, selfId));
15883
+ alternatives.push(compileRule(rule, grants, selfIds));
15831
15884
  }
15832
15885
  return anyOf(alternatives);
15833
15886
  }
@@ -18938,6 +18991,25 @@ var init_service = __esm({
18938
18991
  for (const r of resolved) out.push(await this.view(r));
18939
18992
  return out;
18940
18993
  }
18994
+ /** Every tier there is — the org, every channel, every user (static and runtime) — for an
18995
+ * actor with org rights; anyone else is refused (record 0042: a non-admin never sees another
18996
+ * person's tier). Each row carries `addedByName` when the lookup answers. */
18997
+ async listAll(actor) {
18998
+ if (!actor.orgAdmin)
18999
+ throw new McpServiceError(
19000
+ "unauthorized",
19001
+ "listing every tier's MCP servers takes admin rights (repo-management rights). `mcp list` shows the org's, this channel's and your own."
19002
+ );
19003
+ const { channels, users } = this.opts.config.mcpTierIds();
19004
+ const targets = [
19005
+ { kind: "org" },
19006
+ ...channels.map((id) => ({ kind: "channel", id })),
19007
+ ...users.map((id) => ({ kind: "user", id }))
19008
+ ];
19009
+ const out = [];
19010
+ for (const target of targets) for (const r of this.tierEntries(target)) out.push(await this.view(r));
19011
+ return out;
19012
+ }
18941
19013
  async add(actor, target, input) {
18942
19014
  const scopeKey = mcpScopeKey(target.kind, target.id);
18943
19015
  try {
@@ -19052,6 +19124,55 @@ var init_service = __esm({
19052
19124
  return { ...view, probe: { ok: false, error: (err2 instanceof Error ? err2.message : String(err2)).slice(0, 300) } };
19053
19125
  }
19054
19126
  }
19127
+ /** `mcp promote <name> --from <user>` (record 0042): copy a person's runtime entry — `url`,
19128
+ * `auth`, `agents` (widened only when the admin passes some) — into the org tier under the
19129
+ * same name, `addedBy` the admin and `promotedFrom` the person, and for bearer/oauth mint a
19130
+ * fresh ORG connect ticket exactly as `mcp add --scope org` would. The person's sealed
19131
+ * credential is never read, unsealed or written: it is theirs, and the org's calls must not
19132
+ * run as them. The personal entry stays (shadowed by the org tier at run time). */
19133
+ async promote(actor, name, from, opts = {}) {
19134
+ const org = this.target(actor, "org", void 0);
19135
+ const source2 = this.tierEntries({ kind: "user", id: from }).find((r) => r.name === name);
19136
+ if (!source2)
19137
+ throw new McpServiceError(
19138
+ "not_found",
19139
+ `no MCP server named "${name}" in the user scope of ${from} (see \`mcp list --all\`)`
19140
+ );
19141
+ if (source2.source === "config")
19142
+ throw new McpServiceError(
19143
+ "conflict",
19144
+ `"${name}" is pinned in config.yaml under users.${from} \u2014 move it to defaults.mcpServers there`
19145
+ );
19146
+ const runtime = this.opts.config.runtimeScope("org");
19147
+ if (runtime.mcpServers?.[name] || this.opts.config.isStaticMcpServer("org", void 0, name))
19148
+ throw new McpServiceError(
19149
+ "conflict",
19150
+ `an org MCP server named "${name}" already exists \u2014 remove it first or pick another name`
19151
+ );
19152
+ if (Object.keys(runtime.mcpServers ?? {}).length >= MCP_SERVERS_PER_SCOPE_MAX)
19153
+ throw new McpServiceError("invalid_input", `the org scope already has ${MCP_SERVERS_PER_SCOPE_MAX} servers`);
19154
+ const agents = this.checkAgents("org", opts.agents ?? source2.entry.agents);
19155
+ if (source2.entry.auth !== "none") this.requireCredentialSupport(source2.entry.auth);
19156
+ const entry = {
19157
+ url: source2.entry.url,
19158
+ agents,
19159
+ auth: source2.entry.auth,
19160
+ addedBy: actor.id,
19161
+ addedAt: this.now(),
19162
+ promotedFrom: from
19163
+ };
19164
+ await this.writeServers(org, { ...runtime.mcpServers, [name]: entry });
19165
+ const view = serverView("org", name, entry, { hasCredential: false, source: "runtime" });
19166
+ if (entry.auth === "none") return { server: view, promotedFrom: from };
19167
+ const ticket = await this.mintTicket(mcpCredentialKey("org", name), actor);
19168
+ return {
19169
+ server: view,
19170
+ promotedFrom: from,
19171
+ connectUrl: this.connectUrl(ticket),
19172
+ expiresAt: ticket.expiresAt,
19173
+ expiresInMinutes: TICKET_MINUTES
19174
+ };
19175
+ }
19055
19176
  // ---- the connect page ----------------------------------------------------------
19056
19177
  /** GET: binding an unbound ticket is a compare-and-swap on its state; when
19057
19178
  * another opener won the race the decision is re-planned against the
@@ -19421,7 +19542,7 @@ var init_service = __esm({
19421
19542
  return `${(this.opts.publicBaseUrl ?? "").replace(/\/+$/, "")}/mcp/connect/${ticket.nonce}`;
19422
19543
  }
19423
19544
  async mintTicket(serverId, actor) {
19424
- const email = this.opts.resolveEmail ? await this.opts.resolveEmail(actor.id).catch(() => void 0) : void 0;
19545
+ const email = actor.email ?? (this.opts.resolveEmail ? await this.opts.resolveEmail(actor.id).catch(() => void 0) : void 0);
19425
19546
  const ticket = newTicket({
19426
19547
  nonce: this.nonce(),
19427
19548
  serverId,
@@ -19478,7 +19599,9 @@ var init_service = __esm({
19478
19599
  }
19479
19600
  async view(r) {
19480
19601
  const hasCredential = r.entry.auth !== "none" && !r.entry.tokenEnv ? await this.viaSecrets(() => this.opts.secrets.getCredential(mcpCredentialKey(r.scopeKey, r.name))) !== null : false;
19481
- return serverView(r.scopeKey, r.name, r.entry, { hasCredential, source: r.source });
19602
+ const view = serverView(r.scopeKey, r.name, r.entry, { hasCredential, source: r.source });
19603
+ const name = view.addedBy && this.opts.resolveName ? await this.opts.resolveName(view.addedBy).catch(() => void 0) : void 0;
19604
+ return name ? { ...view, addedByName: name } : view;
19482
19605
  }
19483
19606
  async specFor(r) {
19484
19607
  const id = mcpCredentialKey(r.scopeKey, r.name);
@@ -19576,7 +19699,11 @@ async function serviceOf(deps) {
19576
19699
  }
19577
19700
  function actorOf(caller, channel) {
19578
19701
  return {
19579
- id: caller.id,
19702
+ // The person, for a dashboard session linked to one (record 0042): its
19703
+ // own tier, `addedBy` and the connect ticket's email are the person's.
19704
+ id: meIdOf(caller) ?? caller.id,
19705
+ // A dashboard session's own email binds the tickets it mints, linked or not.
19706
+ ...caller.email ? { email: caller.email } : {},
19580
19707
  orgAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "org" }).allow,
19581
19708
  channelAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "channel", id: channel ?? "" }).allow
19582
19709
  };
@@ -19585,7 +19712,7 @@ function channelOf2(caller, option) {
19585
19712
  return option ?? caller.origin?.channelId;
19586
19713
  }
19587
19714
  async function writeTarget(svc, caller, actor, word, channel) {
19588
- if ((word ?? "me") === "me" && caller.kind === "access")
19715
+ if ((word ?? "me") === "me" && meIdOf(caller) === void 0)
19589
19716
  throw new CommandError("unauthorized", MCP_ME_ON_ACCESS_MESSAGE);
19590
19717
  return via(() => svc.target(actor, word ?? "me", channel));
19591
19718
  }
@@ -19627,7 +19754,10 @@ function renderServerLine(r) {
19627
19754
  const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (bot env)" : "\u23F3 awaiting credential";
19628
19755
  const agents = Array.isArray(r.agents) ? r.agents.join(", ") : "";
19629
19756
  const pinned = r.source === "config" ? " \xB7 pinned in config.yaml" : "";
19630
- return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}`;
19757
+ const by = typeof r.addedBy === "string" ? ` \xB7 added by ${typeof r.addedByName === "string" ? r.addedByName : r.addedBy}` : "";
19758
+ const from = typeof r.promotedFrom === "string" ? ` \xB7 promoted from ${r.promotedFrom}` : "";
19759
+ const owner = r.scope === "user" && typeof r.scopeKey === "string" ? ` \xB7 tier of ${r.scopeKey.slice("user:".length)}` : "";
19760
+ return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}${by}${from}${owner}`;
19631
19761
  }
19632
19762
  function renderList(output) {
19633
19763
  const o = output;
@@ -19667,13 +19797,14 @@ function renderShow(output) {
19667
19797
  function registerMcpCommands(registry) {
19668
19798
  for (const cmd of MCP_COMMANDS) registry.register(cmd);
19669
19799
  }
19670
- var defineCommand10, serverName, serverUrl, scopeOption, channelOption2, nameArg, MCP_ME_ON_ACCESS_MESSAGE, mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
19800
+ var defineCommand10, serverName, serverUrl, scopeOption, channelOption2, nameArg, USER_ID_PATTERN, MCP_ME_ON_ACCESS_MESSAGE, mcpList, mcpPromote, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
19671
19801
  var init_mcp = __esm({
19672
19802
  "../../src/core/commands/mcp.ts"() {
19673
19803
  "use strict";
19674
19804
  init_config3();
19675
19805
  init_service();
19676
19806
  init_authorize();
19807
+ init_config4();
19677
19808
  init_commandRegistry();
19678
19809
  defineCommand10 = commandDefiner();
19679
19810
  serverName = z13.string().refine((s) => MCP_SERVER_NAME_PATTERN.test(s), "expected a slug: lowercase letters, digits, dashes (\u2264 32 chars)");
@@ -19683,22 +19814,53 @@ var init_mcp = __esm({
19683
19814
  );
19684
19815
  channelOption2 = z13.string().optional().describe("target channel for `--scope channel` and for `list` (default: the channel you are speaking in)");
19685
19816
  nameArg = { name: "name", schema: serverName, describe: "the server's name (see `mcp list`)" };
19817
+ USER_ID_PATTERN = /^[a-z]+:[A-Za-z0-9_.@-]{1,200}$/;
19686
19818
  MCP_ME_ON_ACCESS_MESSAGE = "Personal MCP servers are added in chat (`mcp add <name> --url \u2026`): your runs are requested as your chat user, not as this browser session, so a server added here for yourself would reach no run. Pass --scope org or --scope channel.";
19687
19819
  mcpList = defineCommand10({
19688
19820
  id: "mcp.list",
19689
19821
  enabledWhen: (caps) => caps.mcp,
19690
- options: z13.object({ channel: channelOption2 }),
19822
+ options: z13.object({
19823
+ channel: channelOption2,
19824
+ all: flag.optional().describe("every tier there is \u2014 the org's, every channel's, every user's (admins)")
19825
+ }),
19691
19826
  action: "mcp:read",
19692
19827
  effect: "read",
19693
- describe: "External MCP servers your runs in this channel can use \u2014 org-wide, this channel's, and your own \u2014 with state and agents; never a credential.",
19828
+ describe: "External MCP servers your runs in this channel can use \u2014 org-wide, this channel's, and your own \u2014 with state and agents; never a credential. `--all` (admins): every tier.",
19694
19829
  render: renderList,
19695
19830
  handler: async ({ options, caller, deps }) => {
19696
19831
  const svc = await serviceOf(deps);
19697
19832
  const channel = channelOf2(caller, options.channel);
19698
- const servers = await via(() => svc.list(actorOf(caller, channel), channel));
19833
+ const actor = actorOf(caller, channel);
19834
+ const servers = await via(() => options.all ? svc.listAll(actor) : svc.list(actor, channel));
19699
19835
  return { servers };
19700
19836
  }
19701
19837
  });
19838
+ mcpPromote = defineCommand10({
19839
+ id: "mcp.promote",
19840
+ enabledWhen: (caps) => caps.mcp,
19841
+ args: [nameArg],
19842
+ options: z13.object({
19843
+ from: z13.string().refine((s) => USER_ID_PATTERN.test(s), "expected a platform-namespaced user id (slack:U\u2026)").describe("whose personal server to promote (`slack:U\u2026`, as `mcp list --all` shows it)"),
19844
+ agents: z13.string().transform(
19845
+ (s) => s.split(",").map((a) => a.trim()).filter(Boolean)
19846
+ ).optional().describe("widen the agents (default: the personal entry's); an org server may name coding/review/ship")
19847
+ }),
19848
+ action: "mcp:write",
19849
+ // Org-only by definition (there is no `me` alternative to point at), so the gate is the
19850
+ // table's, at the door: the same `mcp:write` on `config-scope { org }` that `mcp add --scope org` asks.
19851
+ resource: () => ({ type: "config-scope", kind: "org" }),
19852
+ effect: "write",
19853
+ describe: "Re-issue a person's MCP server in the org tier (admins): the same name, URL and auth, added by you; a bearer/oauth server gets a fresh org connect link for you to complete \u2014 the person's credential is never copied.",
19854
+ render: renderAdd,
19855
+ settle: (output, { deps }) => settleConnect(deps, output),
19856
+ handler: async ({ args, options, caller, deps }) => {
19857
+ const svc = await serviceOf(deps);
19858
+ const actor = actorOf(caller, channelOf2(caller, void 0));
19859
+ return await via(
19860
+ () => svc.promote(actor, args.name, options.from, options.agents ? { agents: options.agents } : {})
19861
+ );
19862
+ }
19863
+ });
19702
19864
  mcpAdd = defineCommand10({
19703
19865
  id: "mcp.add",
19704
19866
  enabledWhen: (caps) => caps.mcp,
@@ -19796,7 +19958,7 @@ var init_mcp = __esm({
19796
19958
  return await via(() => svc.remove(actor, target, args.name));
19797
19959
  }
19798
19960
  });
19799
- MCP_COMMANDS = [mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove];
19961
+ MCP_COMMANDS = [mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, mcpPromote];
19800
19962
  }
19801
19963
  });
19802
19964
 
@@ -22669,14 +22831,16 @@ var init_runs = __esm({
22669
22831
  sinceMs: z18.coerce.number().int().nonnegative().optional().describe("only runs started at or after this epoch ms"),
22670
22832
  limit: positiveInt3.max(RUN_LIST_MAX_LIMIT).optional().describe(`page size (max ${RUN_LIST_MAX_LIMIT})`),
22671
22833
  before: z18.coerce.number().int().nonnegative().optional().describe("page cursor: runs finished before this epoch ms"),
22672
- beforeId: runId2.optional().describe("page cursor tie-breaker: the last id of the previous page")
22834
+ beforeId: runId2.optional().describe("page cursor tie-breaker: the last id of the previous page"),
22835
+ mine: flag.optional().describe("only the runs you requested \u2014 for a dashboard session, the Slack user its email names (record 0042)")
22673
22836
  }),
22674
22837
  action: "runs:read",
22675
22838
  effect: "read",
22676
22839
  describe: "List runs (live and persisted, newest first) \u2014 metadata only, never message text.",
22677
22840
  handler: async ({ options, caller, deps }) => {
22678
- const visibleTo = predicateFor(caller.actor, "runs:read", "run");
22679
- const { thread, parent, ...rest } = options;
22841
+ const { thread, parent, mine, ...rest } = options;
22842
+ const readable = predicateFor(caller.actor, "runs:read", "run");
22843
+ const visibleTo = mine ? allOf([readable, ownedBy(caller.actor)]) : readable;
22680
22844
  return asJson3(
22681
22845
  await (await deps.runs()).listRuns({
22682
22846
  ...rest,
@@ -25768,7 +25932,13 @@ async function fetchPullRequestFacts(pr) {
25768
25932
  ...typeof data.title === "string" && data.title ? { title: data.title } : {},
25769
25933
  ..."auto_merge" in data ? { autoMergeEnabled: data.auto_merge !== null } : {},
25770
25934
  ..."mergeable" in data ? { mergeable: data.mergeable === null ? null : data.mergeable === true } : {},
25771
- ...typeof data.mergeable_state === "string" && data.mergeable_state ? { mergeableState: data.mergeable_state } : {}
25935
+ ...typeof data.mergeable_state === "string" && data.mergeable_state ? { mergeableState: data.mergeable_state } : {},
25936
+ // The merge commit rides only a merged row: on an open pull request GitHub
25937
+ // reports a test-merge sha under `merge_commit_sha`.
25938
+ ...typeof data.merged_at === "string" && data.merged_at ? {
25939
+ mergedAt: data.merged_at,
25940
+ ...typeof data.merge_commit_sha === "string" && /^[0-9a-f]{40}$/.test(data.merge_commit_sha) ? { mergeCommitSha: data.merge_commit_sha } : {}
25941
+ } : {}
25772
25942
  };
25773
25943
  }
25774
25944
  async function mergePullRequest(pr, opts) {
@@ -26121,7 +26291,12 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
26121
26291
  } else {
26122
26292
  out.prUnpostable = { number: inherited.number, reason: head.reason };
26123
26293
  if (inherited.source === "record" && head.reason === "closed" && head.sha !== void 0) {
26124
- out.closedRecordPr = { number: inherited.number, headSha: head.sha, merged: head.merged };
26294
+ out.closedRecordPr = {
26295
+ number: inherited.number,
26296
+ headSha: head.sha,
26297
+ ...head.ref !== void 0 ? { headRef: head.ref } : {},
26298
+ merged: head.merged
26299
+ };
26125
26300
  }
26126
26301
  }
26127
26302
  }
@@ -26134,11 +26309,21 @@ function ownPrOf(ctx) {
26134
26309
  }
26135
26310
  function recordPrOf(ctx) {
26136
26311
  if (ctx.prFromRecord && ctx.pr !== void 0 && ctx.headSha !== void 0) {
26137
- return { number: ctx.pr, headSha: ctx.headSha, state: "open" };
26312
+ return {
26313
+ number: ctx.pr,
26314
+ headSha: ctx.headSha,
26315
+ ...ctx.refFromPr && ctx.ref !== void 0 ? { headBranch: ctx.ref } : {},
26316
+ state: "open"
26317
+ };
26138
26318
  }
26139
26319
  if (ctx.closedRecordPr !== void 0) {
26140
- const { number, headSha, merged } = ctx.closedRecordPr;
26141
- return { number, headSha, state: merged ? "merged" : "closed" };
26320
+ const { number, headSha, headRef, merged } = ctx.closedRecordPr;
26321
+ return {
26322
+ number,
26323
+ headSha,
26324
+ ...headRef !== void 0 ? { headBranch: headRef } : {},
26325
+ state: merged ? "merged" : "closed"
26326
+ };
26142
26327
  }
26143
26328
  return void 0;
26144
26329
  }
@@ -26150,8 +26335,14 @@ function inheritedPr(thread, records, repo) {
26150
26335
  }
26151
26336
  async function openPrHeadSha(pr) {
26152
26337
  const head = await prHead(pr).catch(() => void 0);
26153
- if (head?.state === "closed")
26154
- return { reason: "closed", ...head.sha ? { sha: head.sha } : {}, merged: head.merged };
26338
+ if (head?.state === "closed") {
26339
+ return {
26340
+ reason: "closed",
26341
+ ...head.sha ? { sha: head.sha } : {},
26342
+ ...head.ref ? { ref: head.ref } : {},
26343
+ merged: head.merged
26344
+ };
26345
+ }
26155
26346
  if (head?.state === "open" && head.sha) {
26156
26347
  return {
26157
26348
  sha: head.sha,
@@ -26999,7 +27190,7 @@ var init_types5 = __esm({
26999
27190
 
27000
27191
  // ../../src/core/dispatch/commandRun.ts
27001
27192
  function isInlineRunCommand(id) {
27002
- return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove)$/.test(id);
27193
+ return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove|promote)$/.test(id);
27003
27194
  }
27004
27195
  async function runChatCommand(deps, msg, io, parsed, ending, trace, opts = {}) {
27005
27196
  const commands = deps.commands;
@@ -32512,6 +32703,14 @@ async function attachRound(deps, ctx) {
32512
32703
  return attached;
32513
32704
  });
32514
32705
  }
32706
+ async function reattachWorkspace2(deps, ctx) {
32707
+ try {
32708
+ return { kind: "attached", round: await attachRound(deps, ctx) };
32709
+ } catch (err2) {
32710
+ if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
32711
+ throw err2;
32712
+ }
32713
+ }
32515
32714
  async function attachWorkspace(deps, ctx) {
32516
32715
  const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach } = ctx;
32517
32716
  let round2;
@@ -32908,7 +33107,7 @@ function openThroughSeam(harness, deps, run2) {
32908
33107
  }
32909
33108
  return harness.open(deps, run2);
32910
33109
  }
32911
- var isRecord3, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError;
33110
+ var isRecord3, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, RELAUNCH_CEILING, HarnessContainerReplacedError;
32912
33111
  var init_contract4 = __esm({
32913
33112
  "../../src/core/harness/contract.ts"() {
32914
33113
  "use strict";
@@ -32939,6 +33138,114 @@ var init_contract4 = __esm({
32939
33138
  expected;
32940
33139
  found;
32941
33140
  };
33141
+ RELAUNCH_CEILING = 2;
33142
+ HarnessContainerReplacedError = class extends HarnessInterruptedError {
33143
+ constructor(message3, said2, was, now, record2) {
33144
+ super(message3, "container replaced under the run", "container_replaced");
33145
+ this.said = said2;
33146
+ this.was = was;
33147
+ this.now = now;
33148
+ this.record = record2;
33149
+ this.name = "HarnessContainerReplacedError";
33150
+ }
33151
+ said;
33152
+ was;
33153
+ now;
33154
+ record;
33155
+ };
33156
+ }
33157
+ });
33158
+
33159
+ // ../../src/core/dispatch/relaunch.ts
33160
+ async function prepareRelaunch(deps, ctx) {
33161
+ const { replaced, facts: facts2, harness } = ctx;
33162
+ const refuse3 = (why, reason2, refusal = "container_replaced") => ({
33163
+ kind: "refused",
33164
+ interruption: new RelaunchRefusedError(why, reason2, refusal)
33165
+ });
33166
+ if (harness.history !== "authored-session")
33167
+ return refuse3(
33168
+ `the ${harness.name} harness keeps its own store, which went with the container, so nothing here can rebuild its process; the run restarts from its request`,
33169
+ "container replaced under the run; the harness keeps its own store; restarting from the request"
33170
+ );
33171
+ if (facts2 === void 0)
33172
+ return refuse3(
33173
+ "the row carries no harness facts to count a relaunch on; the run restarts from its request",
33174
+ "container replaced under the run before its process left facts; restarting from the request"
33175
+ );
33176
+ const relaunches = facts2.relaunches;
33177
+ if (relaunches >= RELAUNCH_CEILING)
33178
+ return refuse3(
33179
+ `the container was replaced under the run again and the relaunch ceiling is ${RELAUNCH_CEILING} (${relaunches} relaunches already), so pi is not started a ${ordinal(relaunches + 2)} time; the run restarts from its request`,
33180
+ `relaunch ceiling: ${relaunches} relaunches already; restarting from the request`
33181
+ );
33182
+ let round2;
33183
+ if (ctx.binding !== void 0) {
33184
+ const reattached = await reattachWorkspace2(deps, {
33185
+ threadKey: ctx.threadKey,
33186
+ agent: ctx.agent,
33187
+ profile: ctx.profile,
33188
+ repoCtx: ctx.repoCtx,
33189
+ root: ctx.root,
33190
+ clock: ctx.clock,
33191
+ reattach: ctx.binding
33192
+ });
33193
+ if (reattached.kind === "reattach_refused")
33194
+ return refuse3(
33195
+ `the run's workspace could not be re-attached in the replacement container (${reattached.why}); the run restarts from its request as a new run in this thread`,
33196
+ "workspace lost with the replaced container; restarting from the request",
33197
+ "workspace_lost"
33198
+ );
33199
+ round2 = reattached.round;
33200
+ }
33201
+ const counted = { ...facts2, relaunches: relaunches + 1 };
33202
+ let written = counted;
33203
+ let bearer;
33204
+ if (deps.runBearers) {
33205
+ const rotated = deps.runBearers.rotate(ctx.runId, (secretHash) => {
33206
+ written = { ...counted, bearerHash: secretHash };
33207
+ ctx.saveFacts(written);
33208
+ });
33209
+ if (!rotated.ok)
33210
+ return refuse3(
33211
+ `the run's bearer could not be rotated for the relaunch (${rotated.reason}); the run restarts from its request`,
33212
+ `the run's bearer could not be rotated (${rotated.reason}); restarting from the request`
33213
+ );
33214
+ bearer = rotated.token;
33215
+ } else {
33216
+ ctx.saveFacts(written);
33217
+ }
33218
+ const { deadline, ...record2 } = replaced.record;
33219
+ return {
33220
+ kind: "relaunch",
33221
+ resume: {
33222
+ ...record2,
33223
+ remainingMs: Math.max(0, deadline - ctx.clock()),
33224
+ facts: written,
33225
+ relaunch: {
33226
+ ...replaced.was !== void 0 ? { from: replaced.was } : {},
33227
+ ...replaced.now !== void 0 ? { to: replaced.now } : {}
33228
+ }
33229
+ },
33230
+ ...bearer !== void 0 ? { bearer } : {},
33231
+ ...round2 !== void 0 ? { round: round2 } : {}
33232
+ };
33233
+ }
33234
+ function ordinal(n2) {
33235
+ return n2 === 2 ? "second" : n2 === 3 ? "third" : n2 === 4 ? "fourth" : `${n2}th`;
33236
+ }
33237
+ var RelaunchRefusedError;
33238
+ var init_relaunch = __esm({
33239
+ "../../src/core/dispatch/relaunch.ts"() {
33240
+ "use strict";
33241
+ init_contract4();
33242
+ init_provision();
33243
+ RelaunchRefusedError = class extends HarnessInterruptedError {
33244
+ constructor(message3, reason2, refusal) {
33245
+ super(message3, reason2, refusal);
33246
+ this.name = "RelaunchRefusedError";
33247
+ }
33248
+ };
32942
33249
  }
32943
33250
  });
32944
33251
 
@@ -32980,7 +33287,7 @@ function startScript(start3) {
32980
33287
  `mkfifo -m 600 ${shellQuote(paths.fifo)}`,
32981
33288
  keepLog ? `: >> ${shellQuote(paths.log)}` : `: > ${shellQuote(paths.log)}`,
32982
33289
  keepLog ? `: >> ${shellQuote(paths.errLog)}` : `: > ${shellQuote(paths.errLog)}`,
32983
- `setsid -f sh -c ${shellQuote(inner)}`,
33290
+ `setsid -f sh -c ${shellQuote(inner)} ${DETACHED_STDIO}`,
32984
33291
  `sleep 0.3`,
32985
33292
  `cat ${shellQuote(paths.pidFile)}`
32986
33293
  ].join(" && ");
@@ -33087,7 +33394,7 @@ function stdoutOf(operation, out) {
33087
33394
  const stdout = cut >= 0 ? out.slice(0, cut) : out;
33088
33395
  return stdout === "(no output)" ? "" : stdout;
33089
33396
  }
33090
- var PORT_ARG, HARNESS_PORT_ENV, HarnessContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, COMMAND_WORD, FREE_PORT_JS, IDENTITY_WORD, METHOD_WORD, PATH_WORD, HEADER_NAME, OP_TIMEOUT_MS, CURL_MAX_TIME_S, secretHeaderEnv, STDERR_MARK, RUNTIME_WORD, HarnessContainerRuntimeReplacedError, ExecHarnessContainer;
33397
+ var PORT_ARG, HARNESS_PORT_ENV, HarnessContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, COMMAND_WORD, FREE_PORT_JS, DETACHED_STDIO, IDENTITY_WORD, METHOD_WORD, PATH_WORD, HEADER_NAME, OP_TIMEOUT_MS, CURL_MAX_TIME_S, secretHeaderEnv, STDERR_MARK, RUNTIME_WORD, HarnessContainerRuntimeReplacedError, ExecHarnessContainer;
33091
33398
  var init_container = __esm({
33092
33399
  "../../src/core/harness/container.ts"() {
33093
33400
  "use strict";
@@ -33109,6 +33416,7 @@ var init_container = __esm({
33109
33416
  LOG_READ_BYTES = 48 * 1024;
33110
33417
  COMMAND_WORD = /^[A-Za-z0-9_./-]+$/;
33111
33418
  FREE_PORT_JS = 'const s=require("net").createServer();s.listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})';
33419
+ DETACHED_STDIO = "</dev/null >/dev/null 2>&1";
33112
33420
  IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
33113
33421
  METHOD_WORD = /^[A-Z]+$/;
33114
33422
  PATH_WORD = /^\/\S*$/;
@@ -34251,12 +34559,32 @@ var init_mirror = __esm({
34251
34559
  seedEchoPending = false;
34252
34560
  mirroredTail;
34253
34561
  iteration = 0;
34562
+ /** The rows written since the mirror started, and the compaction entries
34563
+ * among them by position — kept whether or not the ledger is wired, so
34564
+ * the harness holds this generation's copy of the record it wrote
34565
+ * (`written`), what a relaunch under a living bot rebuilds from. Bounded
34566
+ * by the run as the ledger's transcript and pi's own session file are — one
34567
+ * row per turn under the run's turn guard, the same bytes both already
34568
+ * hold — and never trimmed: a rebuilt session renders every row, with each
34569
+ * compaction where it sat, so a row dropped here would be a row the record
34570
+ * clause lost. */
34571
+ rows = [];
34572
+ compactionRows = [];
34254
34573
  /** The highest ledger inbox seq folded in so far (run-history item 40). */
34255
34574
  inboxConsumedSeq = 0;
34256
34575
  /** Whether a report is owed: the mirror is wired and something happened. */
34257
34576
  get wired() {
34258
34577
  return this.deps.onStep !== void 0;
34259
34578
  }
34579
+ /** The transcript rows this mirror has written since it started (the rows
34580
+ * after the seed or the resumed transcript it was constructed on), and the
34581
+ * compaction entries among them, each `before` counted from the first of
34582
+ * these rows: what a relaunch under a living bot rebuilds from
34583
+ * (harness-pi item 8), this generation's copy of the ledger's rows. A row
34584
+ * met again on a re-attach (`alreadyHeld`) was the caller's and is not here. */
34585
+ get written() {
34586
+ return { messages: [...this.rows], compactions: [...this.compactionRows] };
34587
+ }
34260
34588
  /** The row (the run's own index) the calls under way ride in — the last
34261
34589
  * assistant turn the ledger holds — for the `tool_call` stamp (run-history
34262
34590
  * item 53); undefined before any assistant turn, or when the transcript's
@@ -34277,6 +34605,19 @@ var init_mirror = __esm({
34277
34605
  expectSeedEcho() {
34278
34606
  this.seedEchoPending = true;
34279
34607
  }
34608
+ /** The settlement turn a rebuilt process's session starts on (harness-pi
34609
+ * item 8) — one tool result per call in flight at the death: the relay's
34610
+ * answer, the still-running note or the restart note — primed as the user
34611
+ * content pending for the next step, the write-through's own convention
34612
+ * (the results since the last assistant turn are the one user turn the next
34613
+ * step carries). The ledger's next rows are then the session's,
34614
+ * `[user(settlements + the continue echo), assistant]` from the seed index,
34615
+ * so a rebuild from the ledger at the next death hands the model a
34616
+ * `tool_result` for every `tool_use` and the two records agree on what the
34617
+ * model saw. Never on a re-attach, where pi's own log carries the results. */
34618
+ prime(parts) {
34619
+ this.pendingUser.push(...parts);
34620
+ }
34280
34621
  /** The row about to be written, against the transcript's tail — compared
34281
34622
  * once, with the first row the mirror meets: equal means the ledger holds
34282
34623
  * it already (read again, harness-pi item 8), together with the results
@@ -34295,7 +34636,6 @@ var init_mirror = __esm({
34295
34636
  * there — so the harness can move the row's offset past it. A result or a
34296
34637
  * steer's text waits in memory for the next step, and the answer is no. */
34297
34638
  async onMessage(message3, turn) {
34298
- if (!this.deps.onStep) return false;
34299
34639
  const chat = chatMessageOf(message3);
34300
34640
  if (!chat) return false;
34301
34641
  if (chat.content.length === 0) return false;
@@ -34307,7 +34647,7 @@ var init_mirror = __esm({
34307
34647
  this.pendingUser.push(...chat.content);
34308
34648
  return false;
34309
34649
  }
34310
- if (this.alreadyHeld({ turn: chat })) return true;
34650
+ if (this.alreadyHeld({ turn: chat })) return this.wired;
34311
34651
  const turns = [];
34312
34652
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
34313
34653
  this.pendingUser = [];
@@ -34324,6 +34664,8 @@ var init_mirror = __esm({
34324
34664
  };
34325
34665
  this.idx += turns.length;
34326
34666
  this.lastAssistantIdx = this.idx - 1;
34667
+ this.rows.push(...turns);
34668
+ if (!this.deps.onStep) return false;
34327
34669
  await this.deps.onStep(report2);
34328
34670
  return true;
34329
34671
  }
@@ -34333,8 +34675,7 @@ var init_mirror = __esm({
34333
34675
  * the summary where pi wrote it and the index moves past it. Answers as
34334
34676
  * `onMessage` does: the ledger holds everything up to the entry. */
34335
34677
  async onCompaction(entry, turn) {
34336
- if (!this.deps.onStep) return false;
34337
- if (this.alreadyHeld({ compaction: entry })) return true;
34678
+ if (this.alreadyHeld({ compaction: entry })) return this.wired;
34338
34679
  const turns = [];
34339
34680
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
34340
34681
  this.pendingUser = [];
@@ -34349,6 +34690,9 @@ var init_mirror = __esm({
34349
34690
  inboxConsumedSeq: this.inboxConsumedSeq
34350
34691
  };
34351
34692
  this.idx += turns.length + 1;
34693
+ this.rows.push(...turns);
34694
+ this.compactionRows.push({ before: this.rows.length, entry });
34695
+ if (!this.deps.onStep) return false;
34352
34696
  await this.deps.onStep(report2);
34353
34697
  return true;
34354
34698
  }
@@ -34507,6 +34851,426 @@ var init_process = __esm({
34507
34851
  }
34508
34852
  });
34509
34853
 
34854
+ // ../../src/execution/tracingExecutor.ts
34855
+ var TracingExecutor;
34856
+ var init_tracingExecutor = __esm({
34857
+ "../../src/execution/tracingExecutor.ts"() {
34858
+ "use strict";
34859
+ TracingExecutor = class {
34860
+ constructor(inner, span, backend) {
34861
+ this.inner = inner;
34862
+ this.span = span;
34863
+ this.backend = backend;
34864
+ const innerRelease = inner.release?.bind(inner);
34865
+ if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
34866
+ const innerMoveTo = inner.moveTo?.bind(inner);
34867
+ if (innerMoveTo) this.moveTo = (sha) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { span: s }));
34868
+ const innerReadBytes = inner.readBytes?.bind(inner);
34869
+ if (innerReadBytes)
34870
+ this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
34871
+ }
34872
+ inner;
34873
+ span;
34874
+ backend;
34875
+ release;
34876
+ moveTo;
34877
+ readBytes;
34878
+ /** Each op under its own `exec.*` span, handed to the inner executor as
34879
+ * `opts.span` so its HTTP calls become `http.client` children (item 21). */
34880
+ timed(name, fn, extra = {}) {
34881
+ return this.span.span(name, (s) => fn(s), {
34882
+ attrs: {
34883
+ ...this.backend !== void 0 ? { backend: this.backend } : {},
34884
+ ...extra.timeoutMs !== void 0 ? { timeoutMs: extra.timeoutMs } : {}
34885
+ }
34886
+ });
34887
+ }
34888
+ exec(command2, opts) {
34889
+ return this.timed("exec.exec", (s) => this.inner.exec(command2, { ...opts, span: s }), {
34890
+ timeoutMs: opts?.timeoutMs
34891
+ });
34892
+ }
34893
+ readFile(path) {
34894
+ return this.timed("exec.read_file", (s) => this.inner.readFile(path, { span: s }));
34895
+ }
34896
+ writeFile(path, content) {
34897
+ return this.timed("exec.write_file", (s) => this.inner.writeFile(path, content, { span: s }));
34898
+ }
34899
+ };
34900
+ }
34901
+ });
34902
+
34903
+ // ../../src/core/chatMessage.ts
34904
+ function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
34905
+ const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
34906
+ \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
34907
+ if (typeof content === "string") return capText(content);
34908
+ return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
34909
+ }
34910
+ var MAX_TOOL_RESULT_CHARS;
34911
+ var init_chatMessage = __esm({
34912
+ "../../src/core/chatMessage.ts"() {
34913
+ "use strict";
34914
+ MAX_TOOL_RESULT_CHARS = 12e4;
34915
+ }
34916
+ });
34917
+
34918
+ // ../../src/core/harness/pi/toolRules.ts
34919
+ import { isAbsolute as isAbsolute5, relative, resolve as resolve10 } from "node:path";
34920
+ function reachFor(identity2) {
34921
+ if (identity2 === "write") return CODING_REACH;
34922
+ if (identity2 === "read") return READ_REACH;
34923
+ return NONE_REACH;
34924
+ }
34925
+ function judgeToolCall(tool, input, ctx) {
34926
+ const bundle = PI_TOOL_BUNDLES[tool];
34927
+ if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
34928
+ if (!reachFor(ctx.identity).has(bundle)) {
34929
+ return outside(
34930
+ ctx.identity === "none" ? `${tool} is the \`${bundle}\` bundle: a run without a workspace (identity none) has none of pi's own tools` : `${tool} is the \`${bundle}\` bundle, outside the ${ctx.identity} identity's reach`
34931
+ );
34932
+ }
34933
+ const args = isRecord6(input) ? input : {};
34934
+ if (tool === "bash") return judgeBash(args.command, ctx);
34935
+ if (bundle === "files" || bundle === "write-files") return judgePath(args.path, ctx);
34936
+ return allowed;
34937
+ }
34938
+ function judgeBash(command2, ctx) {
34939
+ if (typeof command2 !== "string") return refused3("malformed \u2014 bash without a string command");
34940
+ if (CREDENTIAL_FILE.test(command2)) return refused3("credential \u2014 reads the executor's credential store");
34941
+ if (ENV_DUMP.test(command2)) return refused3("credential \u2014 dumps the process environment");
34942
+ if (CREDENTIAL_VAR.test(command2)) return refused3("credential \u2014 expands a credential variable");
34943
+ if (MERGE_OR_APPROVE.test(command2)) {
34944
+ return refused3("merge/approve \u2014 a coding run never merges or approves a pull request");
34945
+ }
34946
+ if (ctx.identity !== "write") {
34947
+ if (ANY_PUSH.test(command2)) return refused3("read-only \u2014 a read-identity run never pushes");
34948
+ if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
34949
+ return refused3("read-only \u2014 a read-identity run never writes to GitHub");
34950
+ }
34951
+ return allowed;
34952
+ }
34953
+ for (const match of command2.matchAll(GIT_PUSH)) {
34954
+ const verdict = judgePush(match[1], ctx);
34955
+ if (verdict.verdict !== "allowed") return verdict;
34956
+ }
34957
+ return allowed;
34958
+ }
34959
+ function judgePush(tail, ctx) {
34960
+ const words = tail.split(/\s+/).filter((w) => w.length > 0 && !w.startsWith("-"));
34961
+ const [remote, refspec] = words;
34962
+ if (remote !== void 0 && remote !== "origin") {
34963
+ return refused3(`repo:use \u2014 push to remote \`${remote}\`, not the run's repository (origin)`);
34964
+ }
34965
+ if (refspec === void 0) return allowed;
34966
+ const destination = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
34967
+ if (ctx.branch !== void 0) {
34968
+ const branch3 = destination === "HEAD" ? ctx.branch : destination.replace(/^refs\/heads\//, "");
34969
+ if (branch3 !== ctx.branch) return refused3(`repo:use \u2014 push to \`${branch3}\`, not the run's branch ${ctx.branch}`);
34970
+ return allowed;
34971
+ }
34972
+ const branch2 = destination.replace(/^refs\/heads\//, "");
34973
+ if (branch2 !== "HEAD" && (ctx.protectedBranches ?? []).includes(branch2)) {
34974
+ return refused3(
34975
+ `repo:use \u2014 push to \`${branch2}\`, the branch this run's pull request targets; push your own branch`
34976
+ );
34977
+ }
34978
+ return allowed;
34979
+ }
34980
+ function judgePath(path, ctx) {
34981
+ if (path === void 0) return allowed;
34982
+ if (typeof path !== "string") return refused3("malformed \u2014 a path tool without a string path");
34983
+ if (CREDENTIAL_FILE.test(path)) return refused3("credential \u2014 reads the executor's credential store");
34984
+ const rel = relative(ctx.checkout, resolve10(ctx.checkout, path));
34985
+ if (rel.startsWith("..") || isAbsolute5(rel)) return refused3(`path \u2014 \`${path}\` resolves outside the checkout`);
34986
+ return allowed;
34987
+ }
34988
+ var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
34989
+ var init_toolRules = __esm({
34990
+ "../../src/core/harness/pi/toolRules.ts"() {
34991
+ "use strict";
34992
+ PI_TOOL_BUNDLES = {
34993
+ bash: "shell",
34994
+ read: "files",
34995
+ grep: "files",
34996
+ find: "files",
34997
+ ls: "files",
34998
+ write: "write-files",
34999
+ edit: "write-files",
35000
+ submit_pr_description: "pr",
35001
+ submit_verdict: "verdict"
35002
+ };
35003
+ CODING_REACH = /* @__PURE__ */ new Set([
35004
+ "shell",
35005
+ "files",
35006
+ "write-files",
35007
+ "web",
35008
+ "github-read",
35009
+ "github-write",
35010
+ "pr"
35011
+ ]);
35012
+ READ_REACH = /* @__PURE__ */ new Set(["shell", "files", "web", "search", "github-read", "verdict"]);
35013
+ NONE_REACH = /* @__PURE__ */ new Set();
35014
+ allowed = { verdict: "allowed" };
35015
+ refused3 = (reason2) => ({ verdict: "refused", reason: reason2 });
35016
+ outside = (reason2) => ({ verdict: "outside-profile", reason: reason2 });
35017
+ isRecord6 = (v) => typeof v === "object" && v !== null;
35018
+ CREDENTIAL_FILE = /(^|[\s/'"`])(\.git\/)?(github-credentials|\.git-credentials)($|[\s'"`|;&])/;
35019
+ ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
35020
+ CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
35021
+ MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
35022
+ GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
35023
+ GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}([^;&|]*)`, "g");
35024
+ ANY_PUSH = new RegExp(GIT_THEN_PUSH);
35025
+ GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
35026
+ WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
35027
+ GH_API_WRITE = new RegExp(
35028
+ `\\bgh\\s+api\\b(?=[^;&|]*(?:\\s(?:-X|--method)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-f|-F|--field|--raw-field|--input)[\\s=]))`
35029
+ );
35030
+ CURL_GITHUB_WRITE = new RegExp(
35031
+ `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
35032
+ );
35033
+ }
35034
+ });
35035
+
35036
+ // ../../src/core/harness/pi/relay.ts
35037
+ function stillRunningNote(tool) {
35038
+ return `This ${tool} call is still running in the bot: the process that made it was relaunched while the call was in flight, and the call keeps running there rather than being re-run \u2014 do not run it again; its result is kept on the relay under the same call id.`;
35039
+ }
35040
+ async function relayToolCall(harness, calls, ask, opts = {}) {
35041
+ const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
35042
+ const window = new AbortController();
35043
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
35044
+ const progress = await Promise.race([
35045
+ answer.then((a) => ({ done: true, answer: a })),
35046
+ sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
35047
+ ]);
35048
+ window.abort();
35049
+ return progress;
35050
+ }
35051
+ function relayedToolDefinitions(harness) {
35052
+ return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
35053
+ }
35054
+ function authorizeToolCall(harness, ask) {
35055
+ harness.gateSaw(ask.toolCallId);
35056
+ const blocked = harness.toolsBlocked();
35057
+ const refuse3 = (reason2) => {
35058
+ harness.emit({
35059
+ type: "run_note",
35060
+ kind: "tool_refused",
35061
+ summary: redactAndCap(`${ask.tool} refused: ${reason2}`, 300)
35062
+ });
35063
+ return { allow: false, reason: reason2 };
35064
+ };
35065
+ if (blocked !== void 0) return refuse3(blocked);
35066
+ if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
35067
+ const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
35068
+ if (verdict.verdict === "allowed") return { allow: true };
35069
+ return refuse3(verdict.reason);
35070
+ }
35071
+ async function runRelayedTool(harness, ask, opts = {}) {
35072
+ const tool = harness.tools.find((t) => t.name === ask.tool);
35073
+ if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
35074
+ const span = harness.toolSpan(ask.toolCallId);
35075
+ const base = harness.toolContext;
35076
+ const readConversation = base.conversation;
35077
+ const ctx = {
35078
+ ...span ? {
35079
+ ...base,
35080
+ span,
35081
+ executor: new TracingExecutor(base.executor, span, harness.backend),
35082
+ publish: (e) => harness.emit(withSpanId(e, span.id)),
35083
+ ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
35084
+ } : { ...base, publish: (e) => harness.emit(e) },
35085
+ ...opts.signal && !base.signal ? { signal: opts.signal } : {},
35086
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {},
35087
+ // After the spreads, so the call's own id can never be shadowed by one on the base context.
35088
+ callId: ask.toolCallId
35089
+ };
35090
+ try {
35091
+ const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
35092
+ const output = await tool.run(input, ctx);
35093
+ return { content: piContentOf(capToolResultContent(output)), isError: false };
35094
+ } catch (err2) {
35095
+ return errorAnswerOf(err2);
35096
+ }
35097
+ }
35098
+ function errorAnswerOf(err2) {
35099
+ const message3 = err2 instanceof Error ? err2.message : String(err2);
35100
+ return { content: piContentOf(capToolResultContent(`Error: ${message3}`)), isError: true };
35101
+ }
35102
+ function piContentOf(content) {
35103
+ if (typeof content === "string") return [{ type: "text", text: content }];
35104
+ return content.map(
35105
+ (p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "image" ? { type: "image", data: p.data, mimeType: p.mediaType } : { type: "text", text: `[document ${p.name ?? "document"} (${p.mediaType})]` }
35106
+ );
35107
+ }
35108
+ function withSpanId(e, spanId) {
35109
+ switch (e.type) {
35110
+ case "tool_call":
35111
+ case "tool_result":
35112
+ case "run_note":
35113
+ case "assistant":
35114
+ case "skill_use":
35115
+ return { ...e, spanId };
35116
+ default:
35117
+ return e;
35118
+ }
35119
+ }
35120
+ var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
35121
+ var init_relay = __esm({
35122
+ "../../src/core/harness/pi/relay.ts"() {
35123
+ "use strict";
35124
+ init_tracingExecutor();
35125
+ init_chatMessage();
35126
+ init_awaitChildren();
35127
+ init_runEvents();
35128
+ init_toolRules();
35129
+ HarnessRegistry = class {
35130
+ live = /* @__PURE__ */ new Map();
35131
+ /** The run drives a pi from here, its relayed calls kept beside it; the
35132
+ * returned function forgets it and ends whatever call still runs. */
35133
+ register(harness) {
35134
+ return this.hold({ harness, calls: new RelayedCalls() });
35135
+ }
35136
+ /** A process relaunched under the same run — in a replacement container,
35137
+ * with a rotated bearer (harness-pi item 8) — takes the run's registration
35138
+ * over, and the run's relayed calls stay: a call in flight keeps running
35139
+ * under the same end signal, and an ask by its id through the new
35140
+ * registration joins it and reads its answer. The earlier registration's
35141
+ * forget is a no-op from here; the returned one forgets the run and ends
35142
+ * the calls. Refuses by name a run that is not registered. */
35143
+ replace(harness) {
35144
+ const previous = this.live.get(harness.runId);
35145
+ if (!previous) throw new Error(`run ${harness.runId} is not registered on the relay: nothing to replace`);
35146
+ return this.hold({ harness, calls: previous.calls });
35147
+ }
35148
+ /** The run forgotten whichever registration holds it, its calls ended: the
35149
+ * run loop's, for a run whose process was found gone with its container
35150
+ * and is not relaunched — the harness left the registration standing so a
35151
+ * relaunch could take it over (`replace`) with the calls kept, and a
35152
+ * refusal has to end them. A run not registered is nothing to forget. */
35153
+ forget(runId3) {
35154
+ const entry = this.live.get(runId3);
35155
+ if (!entry) return;
35156
+ this.live.delete(runId3);
35157
+ entry.calls.end();
35158
+ }
35159
+ hold(entry) {
35160
+ this.live.set(entry.harness.runId, entry);
35161
+ return () => {
35162
+ if (this.live.get(entry.harness.runId) !== entry) return;
35163
+ this.live.delete(entry.harness.runId);
35164
+ entry.calls.end();
35165
+ };
35166
+ }
35167
+ get(runId3) {
35168
+ return this.live.get(runId3)?.harness;
35169
+ }
35170
+ /** The run's relayed calls, for as long as it is registered. */
35171
+ calls(runId3) {
35172
+ return this.live.get(runId3)?.calls;
35173
+ }
35174
+ size() {
35175
+ return this.live.size;
35176
+ }
35177
+ };
35178
+ RELAY_POLL_WINDOW_MS = 3e4;
35179
+ RelayedCalls = class {
35180
+ calls = /* @__PURE__ */ new Map();
35181
+ /** The ids whose call is still running: joined by `join`, forgotten as each answers. */
35182
+ running = /* @__PURE__ */ new Set();
35183
+ /** The ids answered from the record (`settle`), never run here. */
35184
+ settledIds = /* @__PURE__ */ new Set();
35185
+ ending = new AbortController();
35186
+ get size() {
35187
+ return this.calls.size;
35188
+ }
35189
+ /** Aborted when the run ends: the signal every call here runs under. */
35190
+ get signal() {
35191
+ return this.ending.signal;
35192
+ }
35193
+ /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
35194
+ join(callId, start3) {
35195
+ let answer = this.calls.get(callId);
35196
+ if (!answer) {
35197
+ answer = start3();
35198
+ this.calls.set(callId, answer);
35199
+ this.running.add(callId);
35200
+ const done = () => void this.running.delete(callId);
35201
+ answer.then(done, done);
35202
+ }
35203
+ return answer;
35204
+ }
35205
+ /** The ids of the calls still running, in the order they were started. */
35206
+ inFlight() {
35207
+ return [...this.running];
35208
+ }
35209
+ /** A call asked here whose answer already landed: the answer, or nothing
35210
+ * for a call never asked, one still running (`awaitInFlight` reads those)
35211
+ * or one settled from the record (`settle`: the record's answer, not the
35212
+ * bot's). What a relaunch reads for a call the record names beside the
35213
+ * calls it awaits (harness-pi item 8): an answer that landed after the
35214
+ * process died and before it could ask again is carried into the rebuilt
35215
+ * session too, never lost to a restart note. A `start` that rejected is
35216
+ * the error answer `runRelayedTool` would have given. */
35217
+ async answered(callId) {
35218
+ const answer = this.calls.get(callId);
35219
+ if (!answer || this.running.has(callId) || this.settledIds.has(callId)) return void 0;
35220
+ return answer.then(
35221
+ (a) => a,
35222
+ (err2) => errorAnswerOf(err2)
35223
+ );
35224
+ }
35225
+ /** Every call still running, awaited together up to one window — the relay
35226
+ * window a request waits, by default — for a relaunch that rebuilds the
35227
+ * process's session (harness-pi item 8): a call that answers inside it is
35228
+ * `done` with its answer, the result the rebuilt session carries; one still
35229
+ * running after it is not, keeps running here for an ask by the same id,
35230
+ * and the rebuilt session carries the still-running note in its place
35231
+ * (`stillRunningNote`). A call whose `start` rejected is `done` with an
35232
+ * error answer, as `runRelayedTool` answers its own throws, so one
35233
+ * straggler's exception never loses the others' answers. Nothing is ended
35234
+ * or re-run. Answers at once with nothing in flight. */
35235
+ async awaitInFlight(opts = {}) {
35236
+ const ids = this.inFlight();
35237
+ if (ids.length === 0) return [];
35238
+ const window = new AbortController();
35239
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
35240
+ const closed = sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({
35241
+ done: false
35242
+ }));
35243
+ const progress = await Promise.all(
35244
+ ids.map(async (callId) => {
35245
+ const answered = this.calls.get(callId).then(
35246
+ (answer) => ({ done: true, answer }),
35247
+ (err2) => ({ done: true, answer: errorAnswerOf(err2) })
35248
+ );
35249
+ return { callId, ...await Promise.race([answered, closed]) };
35250
+ })
35251
+ );
35252
+ window.abort();
35253
+ return progress;
35254
+ }
35255
+ /** The call's answer is known before it is asked (harness-pi item 8): the
35256
+ * record's settlement for a call in flight when the previous generation
35257
+ * died, whose extension asks again with the same id. Every ask reads it and
35258
+ * the tool never runs; a call already joined keeps the answer it has. */
35259
+ settle(callId, answer) {
35260
+ if (this.calls.has(callId)) return;
35261
+ this.calls.set(callId, Promise.resolve(answer));
35262
+ this.settledIds.add(callId);
35263
+ }
35264
+ end() {
35265
+ this.ending.abort();
35266
+ this.calls.clear();
35267
+ this.running.clear();
35268
+ this.settledIds.clear();
35269
+ }
35270
+ };
35271
+ }
35272
+ });
35273
+
34510
35274
  // ../../src/core/harness/pi/transport.ts
34511
35275
  var NEWLINE, PiRpcTransport;
34512
35276
  var init_transport = __esm({
@@ -34642,16 +35406,25 @@ function promptOf(seed) {
34642
35406
  function settlementText(s) {
34643
35407
  return s.action === "synthetic" ? s.text : `The bot restarted while this ${s.toolUse.name} call was in flight; its result was lost \u2014 re-check its effects before re-running it.`;
34644
35408
  }
34645
- function settlementResults(settlements) {
35409
+ function settlementResults(settlements, settled = /* @__PURE__ */ new Map()) {
34646
35410
  if (settlements.length === 0) return void 0;
34647
35411
  return {
34648
35412
  role: "user",
34649
- content: settlements.map((s) => ({
34650
- type: "tool_result",
34651
- toolUseId: s.toolUse.id,
34652
- content: settlementText(s),
34653
- isError: true
34654
- }))
35413
+ content: settlements.map((s) => {
35414
+ const relay = settled.get(s.toolUse.id);
35415
+ return {
35416
+ type: "tool_result",
35417
+ toolUseId: s.toolUse.id,
35418
+ content: relay?.text ?? settlementText(s),
35419
+ ...relay?.isError ?? true ? { isError: true } : {}
35420
+ };
35421
+ })
35422
+ };
35423
+ }
35424
+ function relaySettlementOf(answer) {
35425
+ return {
35426
+ text: answer.content.map((c) => c.type === "text" ? c.text : "[image]").join("\n"),
35427
+ isError: answer.isError
34655
35428
  };
34656
35429
  }
34657
35430
  function ledgerTailOf(resume) {
@@ -34782,12 +35555,36 @@ async function runPiHarnessOpen(deps, run2) {
34782
35555
  await deps.sleep(seenTick);
34783
35556
  }
34784
35557
  };
34785
- const forget = deps.registry.register(live);
35558
+ const relaunch = run2.resume?.relaunch;
35559
+ const forget = relaunch !== void 0 && deps.registry.get(run2.runId) !== void 0 ? deps.registry.replace(live) : deps.registry.register(live);
34786
35560
  const calls = deps.registry.calls(run2.runId);
34787
35561
  for (const s of run2.resume?.settlements ?? []) calls?.settle(s.toolUse.id, settlementAnswer(s));
34788
35562
  let pid;
34789
35563
  let transport;
34790
35564
  let facts2;
35565
+ const recordBase = {
35566
+ messages: run2.resume?.messages ?? run2.messages,
35567
+ compactions: run2.resume?.compactions ?? []
35568
+ };
35569
+ const relaySettled = /* @__PURE__ */ new Map();
35570
+ let stillRunning = 0;
35571
+ const recordNow = () => {
35572
+ const written = mirror.written;
35573
+ const messages = [...recordBase.messages, ...written.messages];
35574
+ const last = messages.at(-1);
35575
+ const calls2 = last?.role === "assistant" ? last.content.filter((p) => p.type === "tool_use") : [];
35576
+ return {
35577
+ messages,
35578
+ compactions: [
35579
+ ...recordBase.compactions,
35580
+ ...written.compactions.map((c) => ({ ...c, before: recordBase.messages.length + c.before }))
35581
+ ],
35582
+ settlements: calls2.map((toolUse) => ({ toolUse, action: "synthetic", text: replacedCallNote(toolUse.name) })),
35583
+ turn: bridge.turns,
35584
+ inboxConsumedSeq: mirror.inboxConsumedSeq,
35585
+ deadline
35586
+ };
35587
+ };
34791
35588
  let mirrored = 0;
34792
35589
  const save = () => {
34793
35590
  if (facts2) {
@@ -34814,8 +35611,8 @@ async function runPiHarnessOpen(deps, run2) {
34814
35611
  hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
34815
35612
  );
34816
35613
  save();
34817
- forget();
34818
35614
  if (replaced !== void 0) return;
35615
+ forget();
34819
35616
  if (pid !== void 0) await container.kill(pid).catch(() => {
34820
35617
  });
34821
35618
  if (paths !== void 0) await container.remove(paths).catch(() => {
@@ -34827,7 +35624,7 @@ async function runPiHarnessOpen(deps, run2) {
34827
35624
  let elsewhere;
34828
35625
  const here = await container.identity();
34829
35626
  const honoured = (hash) => hash !== void 0 && (deps.bearers === void 0 || deps.bearers.adopt(run2.runId, hash));
34830
- if (recorded !== void 0) {
35627
+ if (recorded !== void 0 && relaunch === void 0) {
34831
35628
  const located2 = await locatePi(recorded, container, here);
34832
35629
  if (located2 === "another-container") {
34833
35630
  elsewhere = `pi is elsewhere: the row's pi (pid ${recorded.pid}) ran in container ${recorded.container}, not the one this run was handed (${here}), so it was neither probed nor ended here`;
@@ -34862,7 +35659,7 @@ async function runPiHarnessOpen(deps, run2) {
34862
35659
  );
34863
35660
  } else {
34864
35661
  paths = piRunPathsAt(await container.makeRoot(piRunPaths(run2.runId).dir));
34865
- if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0)
35662
+ if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0 && relaunch === void 0)
34866
35663
  await container.remove(piRunPathsAt(recorded.root)).catch(() => {
34867
35664
  });
34868
35665
  const spec = {
@@ -34878,12 +35675,33 @@ async function runPiHarnessOpen(deps, run2) {
34878
35675
  };
34879
35676
  let session;
34880
35677
  if (run2.resume) {
34881
- const settled2 = settlementResults(run2.resume.settlements);
35678
+ const wanted = new Map(run2.resume.settlements.map((s) => [s.toolUse.id, s.toolUse.name]));
35679
+ const running = calls?.inFlight().filter((id) => wanted.has(id)) ?? [];
35680
+ if (running.length > 0)
35681
+ run2.onProgress?.(
35682
+ `waiting up to ${Math.round(RELAY_POLL_WINDOW_MS / 1e3)} s for ${running.length} relayed call(s) still running in the bot before pi restarts`
35683
+ );
35684
+ for (const p of await calls?.awaitInFlight({ sleep: (ms) => deps.sleep(ms) }) ?? []) {
35685
+ const tool = wanted.get(p.callId);
35686
+ if (tool === void 0) continue;
35687
+ relaySettled.set(
35688
+ p.callId,
35689
+ p.done ? relaySettlementOf(p.answer) : { text: stillRunningNote(tool), isError: false }
35690
+ );
35691
+ if (!p.done) stillRunning++;
35692
+ }
35693
+ for (const [callId] of wanted) {
35694
+ if (relaySettled.has(callId)) continue;
35695
+ const answer2 = await calls?.answered(callId);
35696
+ if (answer2 !== void 0) relaySettled.set(callId, relaySettlementOf(answer2));
35697
+ }
35698
+ const settled2 = settlementResults(run2.resume.settlements, relaySettled);
34882
35699
  session = {
34883
35700
  stem: "resumed",
34884
35701
  messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages,
34885
35702
  compactions: run2.resume.compactions ?? []
34886
35703
  };
35704
+ if (settled2) mirror.prime(settled2.content);
34887
35705
  } else {
34888
35706
  const earlier2 = splitSeed(run2.messages).session;
34889
35707
  if (earlier2.length > 0) session = { stem: "seed", messages: earlier2, compactions: [] };
@@ -34908,7 +35726,20 @@ async function runPiHarnessOpen(deps, run2) {
34908
35726
  )
34909
35727
  );
34910
35728
  }
34911
- if (run2.resume) {
35729
+ if (run2.resume && relaunch !== void 0) {
35730
+ const inFlight = run2.resume.settlements.length;
35731
+ const answered = relaySettled.size - stillRunning;
35732
+ const lost = inFlight - relaySettled.size;
35733
+ const settledHow = inFlight === 0 ? "nothing was in flight" : `${inFlight} call(s) were in flight: ` + [
35734
+ ...answered > 0 ? [`${answered} answered on the relay`] : [],
35735
+ ...stillRunning > 0 ? [`${stillRunning} still running there`] : [],
35736
+ ...lost > 0 ? [`${lost} lost with the container, each answered with a restart note`] : []
35737
+ ].join(", ");
35738
+ note(
35739
+ "resumed",
35740
+ `relaunched after the container was replaced (${relaunch.from ?? "unknown"} \u2192 ${relaunch.to ?? "unknown"}): the row's pi (pid ${recorded?.pid ?? "unknown"}) went with the old container and was neither probed nor ended here; pi restarted in the container the run holds on the mirrored transcript \u2014 ${settledHow}; ${Math.round(remainingMs / 6e4)} min of budget left`
35741
+ );
35742
+ } else if (run2.resume) {
34912
35743
  const lost = run2.resume.settlements.length;
34913
35744
  const how = elsewhere !== void 0 ? `${elsewhere}, and pi restarted` : ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
34914
35745
  note(
@@ -35066,7 +35897,8 @@ ${stagedLine}` : prompt;
35066
35897
  return new PiContainerReplacedError(
35067
35898
  said2.message,
35068
35899
  facts2?.container,
35069
- await container.identity().catch(() => void 0)
35900
+ await container.identity().catch(() => void 0),
35901
+ recordNow()
35070
35902
  );
35071
35903
  };
35072
35904
  check();
@@ -35336,6 +36168,7 @@ var init_harness = __esm({
35336
36168
  init_mirror();
35337
36169
  init_process();
35338
36170
  init_protocol();
36171
+ init_relay();
35339
36172
  init_transport();
35340
36173
  FINALE_TIMEOUT_MS = 3 * 6e4;
35341
36174
  CALL_SEEN_WAIT_MS = 3e3;
@@ -35355,21 +36188,17 @@ var init_harness = __esm({
35355
36188
  this.name = "GateBypassed";
35356
36189
  }
35357
36190
  };
35358
- PiContainerReplacedError = class extends HarnessInterruptedError {
35359
- constructor(said2, was, now) {
36191
+ PiContainerReplacedError = class extends HarnessContainerReplacedError {
36192
+ constructor(said2, was, now, record2) {
35360
36193
  super(
35361
- `the container running pi was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)}); the run restarts from its request`,
35362
- "container replaced under the run; restarting from the request",
35363
- "container_replaced"
36194
+ `the container running pi was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)})`,
36195
+ said2,
36196
+ was,
36197
+ now,
36198
+ record2
35364
36199
  );
35365
- this.said = said2;
35366
- this.was = was;
35367
- this.now = now;
35368
36200
  this.name = "PiContainerReplacedError";
35369
36201
  }
35370
- said;
35371
- was;
35372
- now;
35373
36202
  };
35374
36203
  POLICY_REFUSAL_REPLY = "the model refused this request under its usage policy \u2014 rephrase it and the thread continues";
35375
36204
  ModelPolicyRefusedError = class extends Error {
@@ -35834,7 +36663,8 @@ async function runCodingPrPostStep(input) {
35834
36663
  const base = prDescription ? await resolveBaseRefLazy(candidates, repo, input.fetchRepoInfo) : resolveBaseRef(candidates, void 0);
35835
36664
  const pushedBranch = branch2 !== void 0 && branch2 !== base && pushed;
35836
36665
  const ownPr = target.ownPr;
35837
- const pushedPastOwnPr = ownPr !== void 0 && pushed && headSha !== void 0 && !sameCommit(headSha, ownPr.headSha) ? headSha : void 0;
36666
+ const onOwnPrBranch = ownPr?.headBranch !== void 0 && branch2 === ownPr.headBranch;
36667
+ const pushedPastOwnPr = ownPr !== void 0 && onOwnPrBranch && pushed && headSha !== void 0 && !sameCommit(headSha, ownPr.headSha) ? headSha : void 0;
35838
36668
  if (prDescription && ownPr !== void 0 && ownPr.state !== "open" && pushedPastOwnPr !== void 0 && branch2 !== void 0) {
35839
36669
  const url = `https://github.com/${repo}/pull/${ownPr.number}`;
35840
36670
  console.log(
@@ -35848,7 +36678,7 @@ async function runCodingPrPostStep(input) {
35848
36678
  });
35849
36679
  return `\u26A0\uFE0F A PR description was submitted and \`${branch2}\`${branchNote} was pushed to \`${pushedPastOwnPr.slice(0, 7)}\`, but this thread's pull request ${url} is ${ownPr.state} and that branch is its head \u2014 the new commits need a new pull request, so nothing was edited: compare & open manually: ${compareUrl}`;
35850
36680
  }
35851
- if (prDescription && ownPr !== void 0 && (branch2 === base || ownPr.state !== "open")) {
36681
+ if (prDescription && ownPr !== void 0 && (branch2 === base || ownPr.state !== "open" && onOwnPrBranch)) {
35852
36682
  const { number, headSha: prHead2, state } = ownPr;
35853
36683
  const url = `https://github.com/${repo}/pull/${number}`;
35854
36684
  const standing = state === "open" ? "" : ` (${state})`;
@@ -36199,7 +37029,6 @@ async function runLoop(deps, ctx) {
36199
37029
  mcpForRun,
36200
37030
  run: run2,
36201
37031
  registry,
36202
- round: round2,
36203
37032
  admitted,
36204
37033
  ledgerRun,
36205
37034
  resume,
@@ -36226,7 +37055,9 @@ async function runLoop(deps, ctx) {
36226
37055
  seed
36227
37056
  } = ctx;
36228
37057
  const agent = budgetedAgent(ctx.agent, profile);
36229
- const { executor, binding } = round2.selection;
37058
+ const { round: round2 } = ctx;
37059
+ let { executor } = round2.selection;
37060
+ const { binding } = round2.selection;
36230
37061
  const coordinatorBase = (async () => {
36231
37062
  if (coordinator === void 0) return void 0;
36232
37063
  if (coordinator.base !== void 0 || repoCtx.baseRef !== void 0) return coordinator.base;
@@ -36420,6 +37251,12 @@ async function runLoop(deps, ctx) {
36420
37251
  const postedBefore = resume ? reviewPostedBefore(resume.events) : void 0;
36421
37252
  if (postedBefore) reviewHead = postedBefore.head;
36422
37253
  const facts2 = resume ? harnessFactsOf(resume.row.state.harness) : void 0;
37254
+ let lastFacts = facts2;
37255
+ const saveFacts = (h) => {
37256
+ lastFacts = h;
37257
+ ledgerRun?.setState({ harness: h });
37258
+ };
37259
+ let workspaceBinding = workspaceBindingFor(round2.selection, profile.machine);
36423
37260
  try {
36424
37261
  if (finish2) {
36425
37262
  onEvent({
@@ -36490,11 +37327,21 @@ async function runLoop(deps, ctx) {
36490
37327
  )
36491
37328
  )
36492
37329
  ];
36493
- harnessSession = await openThroughSeam(
37330
+ let bearer = ctx.bearer;
37331
+ let harnessResume = reentry ? {
37332
+ messages: reentry.messages,
37333
+ compactions: reentry.compactions,
37334
+ settlements: reentry.settlements,
37335
+ remainingMs: reentry.remainingMs,
37336
+ turn: reentry.turn,
37337
+ inboxConsumedSeq: reentry.inboxConsumedSeq,
37338
+ ...facts2 ? { facts: facts2 } : {}
37339
+ } : void 0;
37340
+ const openRun = () => openThroughSeam(
36494
37341
  harnessDeps.harness,
36495
37342
  {
36496
37343
  container: harnessDeps.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine),
36497
- bearer: ctx.bearer,
37344
+ bearer,
36498
37345
  harnessUrl,
36499
37346
  registry: harnessDeps.registry,
36500
37347
  ...deps.runBearers ? { bearers: deps.runBearers } : {},
@@ -36525,24 +37372,55 @@ async function runLoop(deps, ctx) {
36525
37372
  ...stageFollowUps ? { stageFollowUps } : {},
36526
37373
  onEvent,
36527
37374
  onProgress,
36528
- ...ledgerRun ? {
36529
- onStep: ledgerRun.step.bind(ledgerRun),
36530
- logIndexOf: ledgerRun.logIndexOf.bind(ledgerRun),
36531
- saveFacts: (h) => ledgerRun.setState({ harness: h })
36532
- } : {},
36533
- ...reentry ? {
36534
- resume: {
36535
- messages: reentry.messages,
36536
- compactions: reentry.compactions,
36537
- settlements: reentry.settlements,
36538
- remainingMs: reentry.remainingMs,
36539
- turn: reentry.turn,
36540
- inboxConsumedSeq: reentry.inboxConsumedSeq,
36541
- ...facts2 ? { facts: facts2 } : {}
36542
- }
36543
- } : {}
37375
+ ...ledgerRun ? { onStep: ledgerRun.step.bind(ledgerRun), logIndexOf: ledgerRun.logIndexOf.bind(ledgerRun) } : {},
37376
+ saveFacts,
37377
+ ...harnessResume ? { resume: harnessResume } : {}
36544
37378
  }
36545
37379
  );
37380
+ for (; ; ) {
37381
+ try {
37382
+ harnessSession = await openRun();
37383
+ break;
37384
+ } catch (err2) {
37385
+ if (!(err2 instanceof HarnessContainerReplacedError)) throw err2;
37386
+ let decision;
37387
+ try {
37388
+ decision = await prepareRelaunch(deps, {
37389
+ runId: run2.id,
37390
+ threadKey: msg.threadKey,
37391
+ agent: ctx.agent,
37392
+ profile,
37393
+ repoCtx,
37394
+ root,
37395
+ clock: clock4,
37396
+ harness: harnessDeps.harness,
37397
+ replaced: err2,
37398
+ facts: lastFacts,
37399
+ binding: workspaceBinding,
37400
+ saveFacts
37401
+ });
37402
+ } catch (failed) {
37403
+ harnessDeps.registry.forget(run2.id);
37404
+ throw failed;
37405
+ }
37406
+ if (decision.kind === "refused") {
37407
+ harnessDeps.registry.forget(run2.id);
37408
+ onEvent({ type: "run_note", kind: "sandbox_restarted", summary: decision.interruption.message });
37409
+ throw decision.interruption;
37410
+ }
37411
+ if (decision.round !== void 0) {
37412
+ executor = decision.round.selection.executor;
37413
+ toolContext.executor = executor;
37414
+ const rebound = workspaceBindingFor(decision.round.selection, profile.machine);
37415
+ if (rebound !== void 0) {
37416
+ workspaceBinding = rebound;
37417
+ ledgerRun?.setState({ binding: rebound });
37418
+ }
37419
+ }
37420
+ if (decision.bearer !== void 0) bearer = decision.bearer;
37421
+ harnessResume = decision.resume;
37422
+ }
37423
+ }
36546
37424
  answer = harnessSession.answer;
36547
37425
  }
36548
37426
  if (isPrReview && repoCtx.repo && repoCtx.pr !== void 0 && run2.control.requested !== "hard" && postedBefore === void 0) {
@@ -36851,7 +37729,9 @@ var init_runLoop = __esm({
36851
37729
  init_provider();
36852
37730
  init_toolsets();
36853
37731
  init_contract4();
37732
+ init_relaunch();
36854
37733
  init_botHostContainer();
37734
+ init_factory();
36855
37735
  init_container();
36856
37736
  init_harness();
36857
37737
  init_windDown();
@@ -37004,7 +37884,7 @@ async function shipPreflight(input) {
37004
37884
  `\u{1F6AB} Nothing to ship: give ship a task (\`agent:ship in ${repo}: <task>\`), or name an open ship PR by URL to resume its review loop.`
37005
37885
  );
37006
37886
  }
37007
- const ref = repoCtx.ref && !repoCtx.refFromPr && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
37887
+ const ref = repoCtx.ref && !repoCtx.refFromPr && !isUnitBranch(repoCtx.ref) && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
37008
37888
  return {
37009
37889
  ok: true,
37010
37890
  entry: { repo, base: resolveBaseRef([ref], info?.defaultBranch) }
@@ -37137,7 +38017,7 @@ function planWhere(p, units, mergedBefore) {
37137
38017
  async function handOffToCoordinator(deps, input) {
37138
38018
  const log = deps.log ?? console.log;
37139
38019
  const planned = await plan(deps, input);
37140
- if (!planned.ok) return refused3(planned.reply);
38020
+ if (!planned.ok) return refused4(planned.reply);
37141
38021
  const p = planned.planned;
37142
38022
  const planOf = () => ({
37143
38023
  id: p.planId,
@@ -37167,15 +38047,15 @@ async function handOffToCoordinator(deps, input) {
37167
38047
  const status3 = await deps.status(latest.id);
37168
38048
  const where = `plan \`${p.planId}\``;
37169
38049
  if (status3.kind === "unanswered")
37170
- return refused3(
38050
+ return refused4(
37171
38051
  `\u26A0\uFE0F The plan runner could not tell whether \`${latest.id}\` still runs: ${status3.reason}. Nothing ran; re-issue the request to try again.`
37172
38052
  );
37173
38053
  if (status3.kind === "status" && RUNNING.has(status3.status))
37174
- return refused3(
38054
+ return refused4(
37175
38055
  `\u{1F6AB} A runner for ${where} is still running (\`${latest.id}\`, status: ${status3.status}): wait for it to end \u2014 or terminate it in the Workflows dashboard \u2014 before re-issuing.`
37176
38056
  );
37177
38057
  if (status3.kind === "status" && !ENDED.has(status3.status))
37178
- return refused3(
38058
+ return refused4(
37179
38059
  `\u{1F6AB} A runner for ${where} (\`${latest.id}\`) is in a state the bot does not read as ended (${status3.status}) \u2014 a person decides.`
37180
38060
  );
37181
38061
  const merged = /* @__PURE__ */ new Set();
@@ -37187,7 +38067,7 @@ async function handOffToCoordinator(deps, input) {
37187
38067
  }
37188
38068
  const remaining = p.selected.filter((u) => !merged.has(u));
37189
38069
  if (remaining.length === 0)
37190
- return refused3(
38070
+ return refused4(
37191
38071
  `\u{1F6AB} Every unit of ${where} this request names is merged already (${p.selected.join(", ")}) \u2014 nothing left to run.`
37192
38072
  );
37193
38073
  const mergedBefore = p.selected.filter((u) => merged.has(u));
@@ -37222,16 +38102,16 @@ async function start(deps, input, instance, units, where, write) {
37222
38102
  const put = write === "replace" ? await deps.instances.replace(instance) : await deps.instances.put(instance);
37223
38103
  if (!put.ok) {
37224
38104
  if (put.reason === "unavailable")
37225
- return refused3(
38105
+ return refused4(
37226
38106
  "\u26A0\uFE0F The plan runner needs run history on the state Worker (`runHistory.worker`): the instance record could not be written, so nothing ran."
37227
38107
  );
37228
- return refused3(
38108
+ return refused4(
37229
38109
  `\u{1F6AB} A runner for \`${instance.id}\` was just recorded by another request \u2014 re-issue in a minute if it did not start.`
37230
38110
  );
37231
38111
  }
37232
38112
  const rows = await deps.instances.putUnits(units);
37233
38113
  if (!rows.ok)
37234
- return refused3(
38114
+ return refused4(
37235
38115
  "\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
37236
38116
  );
37237
38117
  let answer;
@@ -37248,26 +38128,26 @@ async function start(deps, input, instance, units, where, write) {
37248
38128
  }
37249
38129
  case "duplicate": {
37250
38130
  const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
37251
- return refused3(
38131
+ return refused4(
37252
38132
  `\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
37253
38133
  );
37254
38134
  }
37255
38135
  case "failed":
37256
38136
  case "unanswered":
37257
38137
  log(`[ship] ${input.msg.threadKey}: the plan runner ${instance.id} could not be started \u2014 ${answer.reason}`);
37258
- return refused3(
38138
+ return refused4(
37259
38139
  `\u26A0\uFE0F The plan runner could not be started: ${answer.reason}. Nothing ran; re-issue the request to try again.`
37260
38140
  );
37261
38141
  }
37262
38142
  }
37263
- var refused3, describe6, RUNNING, ENDED;
38143
+ var refused4, describe6, RUNNING, ENDED;
37264
38144
  var init_handOff = __esm({
37265
38145
  "../../src/core/coordinator/handOff.ts"() {
37266
38146
  "use strict";
37267
38147
  init_contract();
37268
38148
  init_preflight();
37269
38149
  init_coordinator();
37270
- refused3 = (reply) => ({ status: "aborted", reply });
38150
+ refused4 = (reply) => ({ status: "aborted", reply });
37271
38151
  describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
37272
38152
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
37273
38153
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
@@ -37867,12 +38747,12 @@ function continuable(run2) {
37867
38747
  return run2.finished && run2.session !== void 0 && run2.agent !== void 0;
37868
38748
  }
37869
38749
  function stickyAgentOf(runs) {
37870
- const newest = runs[0];
38750
+ const newest = runs.find(addressed);
37871
38751
  if (newest === void 0 || !continuable(newest)) return void 0;
37872
38752
  return newest.agent;
37873
38753
  }
37874
38754
  function threadRouteOf(runs, agent) {
37875
- const newest = runs[0];
38755
+ const newest = runs.find(addressed);
37876
38756
  if (newest === void 0 || !continuable(newest) || newest.agent !== agent || newest.route === void 0)
37877
38757
  return void 0;
37878
38758
  const { preset, reason: reason2, model } = newest.route;
@@ -37896,11 +38776,12 @@ function threadPrOf(runs) {
37896
38776
  if (run2 === void 0) return void 0;
37897
38777
  return { repo: run2.repo, number: run2.pr.number, at: run2.finishedAt ?? run2.startedAt };
37898
38778
  }
37899
- var THREAD_READ_LIMIT;
38779
+ var THREAD_READ_LIMIT, addressed;
37900
38780
  var init_thread = __esm({
37901
38781
  "../../src/core/dispatch/thread.ts"() {
37902
38782
  "use strict";
37903
38783
  THREAD_READ_LIMIT = 8;
38784
+ addressed = (run2) => run2.parentInstanceId === void 0;
37904
38785
  }
37905
38786
  });
37906
38787
 
@@ -39988,391 +40869,6 @@ var init_piHarness = __esm({
39988
40869
  }
39989
40870
  });
39990
40871
 
39991
- // ../../src/execution/tracingExecutor.ts
39992
- var TracingExecutor;
39993
- var init_tracingExecutor = __esm({
39994
- "../../src/execution/tracingExecutor.ts"() {
39995
- "use strict";
39996
- TracingExecutor = class {
39997
- constructor(inner, span, backend) {
39998
- this.inner = inner;
39999
- this.span = span;
40000
- this.backend = backend;
40001
- const innerRelease = inner.release?.bind(inner);
40002
- if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
40003
- const innerMoveTo = inner.moveTo?.bind(inner);
40004
- if (innerMoveTo) this.moveTo = (sha) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { span: s }));
40005
- const innerReadBytes = inner.readBytes?.bind(inner);
40006
- if (innerReadBytes)
40007
- this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
40008
- }
40009
- inner;
40010
- span;
40011
- backend;
40012
- release;
40013
- moveTo;
40014
- readBytes;
40015
- /** Each op under its own `exec.*` span, handed to the inner executor as
40016
- * `opts.span` so its HTTP calls become `http.client` children (item 21). */
40017
- timed(name, fn, extra = {}) {
40018
- return this.span.span(name, (s) => fn(s), {
40019
- attrs: {
40020
- ...this.backend !== void 0 ? { backend: this.backend } : {},
40021
- ...extra.timeoutMs !== void 0 ? { timeoutMs: extra.timeoutMs } : {}
40022
- }
40023
- });
40024
- }
40025
- exec(command2, opts) {
40026
- return this.timed("exec.exec", (s) => this.inner.exec(command2, { ...opts, span: s }), {
40027
- timeoutMs: opts?.timeoutMs
40028
- });
40029
- }
40030
- readFile(path) {
40031
- return this.timed("exec.read_file", (s) => this.inner.readFile(path, { span: s }));
40032
- }
40033
- writeFile(path, content) {
40034
- return this.timed("exec.write_file", (s) => this.inner.writeFile(path, content, { span: s }));
40035
- }
40036
- };
40037
- }
40038
- });
40039
-
40040
- // ../../src/core/chatMessage.ts
40041
- function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
40042
- const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
40043
- \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
40044
- if (typeof content === "string") return capText(content);
40045
- return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
40046
- }
40047
- var MAX_TOOL_RESULT_CHARS;
40048
- var init_chatMessage = __esm({
40049
- "../../src/core/chatMessage.ts"() {
40050
- "use strict";
40051
- MAX_TOOL_RESULT_CHARS = 12e4;
40052
- }
40053
- });
40054
-
40055
- // ../../src/core/harness/pi/toolRules.ts
40056
- import { isAbsolute as isAbsolute5, relative, resolve as resolve10 } from "node:path";
40057
- function reachFor(identity2) {
40058
- if (identity2 === "write") return CODING_REACH;
40059
- if (identity2 === "read") return READ_REACH;
40060
- return NONE_REACH;
40061
- }
40062
- function judgeToolCall(tool, input, ctx) {
40063
- const bundle = PI_TOOL_BUNDLES[tool];
40064
- if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
40065
- if (!reachFor(ctx.identity).has(bundle)) {
40066
- return outside(
40067
- ctx.identity === "none" ? `${tool} is the \`${bundle}\` bundle: a run without a workspace (identity none) has none of pi's own tools` : `${tool} is the \`${bundle}\` bundle, outside the ${ctx.identity} identity's reach`
40068
- );
40069
- }
40070
- const args = isRecord6(input) ? input : {};
40071
- if (tool === "bash") return judgeBash(args.command, ctx);
40072
- if (bundle === "files" || bundle === "write-files") return judgePath(args.path, ctx);
40073
- return allowed;
40074
- }
40075
- function judgeBash(command2, ctx) {
40076
- if (typeof command2 !== "string") return refused4("malformed \u2014 bash without a string command");
40077
- if (CREDENTIAL_FILE.test(command2)) return refused4("credential \u2014 reads the executor's credential store");
40078
- if (ENV_DUMP.test(command2)) return refused4("credential \u2014 dumps the process environment");
40079
- if (CREDENTIAL_VAR.test(command2)) return refused4("credential \u2014 expands a credential variable");
40080
- if (MERGE_OR_APPROVE.test(command2)) {
40081
- return refused4("merge/approve \u2014 a coding run never merges or approves a pull request");
40082
- }
40083
- if (ctx.identity !== "write") {
40084
- if (ANY_PUSH.test(command2)) return refused4("read-only \u2014 a read-identity run never pushes");
40085
- if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
40086
- return refused4("read-only \u2014 a read-identity run never writes to GitHub");
40087
- }
40088
- return allowed;
40089
- }
40090
- for (const match of command2.matchAll(GIT_PUSH)) {
40091
- const verdict = judgePush(match[1], ctx);
40092
- if (verdict.verdict !== "allowed") return verdict;
40093
- }
40094
- return allowed;
40095
- }
40096
- function judgePush(tail, ctx) {
40097
- const words = tail.split(/\s+/).filter((w) => w.length > 0 && !w.startsWith("-"));
40098
- const [remote, refspec] = words;
40099
- if (remote !== void 0 && remote !== "origin") {
40100
- return refused4(`repo:use \u2014 push to remote \`${remote}\`, not the run's repository (origin)`);
40101
- }
40102
- if (refspec === void 0) return allowed;
40103
- const destination = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
40104
- if (ctx.branch !== void 0) {
40105
- const branch3 = destination === "HEAD" ? ctx.branch : destination.replace(/^refs\/heads\//, "");
40106
- if (branch3 !== ctx.branch) return refused4(`repo:use \u2014 push to \`${branch3}\`, not the run's branch ${ctx.branch}`);
40107
- return allowed;
40108
- }
40109
- const branch2 = destination.replace(/^refs\/heads\//, "");
40110
- if (branch2 !== "HEAD" && (ctx.protectedBranches ?? []).includes(branch2)) {
40111
- return refused4(
40112
- `repo:use \u2014 push to \`${branch2}\`, the branch this run's pull request targets; push your own branch`
40113
- );
40114
- }
40115
- return allowed;
40116
- }
40117
- function judgePath(path, ctx) {
40118
- if (path === void 0) return allowed;
40119
- if (typeof path !== "string") return refused4("malformed \u2014 a path tool without a string path");
40120
- if (CREDENTIAL_FILE.test(path)) return refused4("credential \u2014 reads the executor's credential store");
40121
- const rel = relative(ctx.checkout, resolve10(ctx.checkout, path));
40122
- if (rel.startsWith("..") || isAbsolute5(rel)) return refused4(`path \u2014 \`${path}\` resolves outside the checkout`);
40123
- return allowed;
40124
- }
40125
- var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused4, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
40126
- var init_toolRules = __esm({
40127
- "../../src/core/harness/pi/toolRules.ts"() {
40128
- "use strict";
40129
- PI_TOOL_BUNDLES = {
40130
- bash: "shell",
40131
- read: "files",
40132
- grep: "files",
40133
- find: "files",
40134
- ls: "files",
40135
- write: "write-files",
40136
- edit: "write-files",
40137
- submit_pr_description: "pr",
40138
- submit_verdict: "verdict"
40139
- };
40140
- CODING_REACH = /* @__PURE__ */ new Set([
40141
- "shell",
40142
- "files",
40143
- "write-files",
40144
- "web",
40145
- "github-read",
40146
- "github-write",
40147
- "pr"
40148
- ]);
40149
- READ_REACH = /* @__PURE__ */ new Set(["shell", "files", "web", "search", "github-read", "verdict"]);
40150
- NONE_REACH = /* @__PURE__ */ new Set();
40151
- allowed = { verdict: "allowed" };
40152
- refused4 = (reason2) => ({ verdict: "refused", reason: reason2 });
40153
- outside = (reason2) => ({ verdict: "outside-profile", reason: reason2 });
40154
- isRecord6 = (v) => typeof v === "object" && v !== null;
40155
- CREDENTIAL_FILE = /(^|[\s/'"`])(\.git\/)?(github-credentials|\.git-credentials)($|[\s'"`|;&])/;
40156
- ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
40157
- CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
40158
- MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
40159
- GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
40160
- GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}([^;&|]*)`, "g");
40161
- ANY_PUSH = new RegExp(GIT_THEN_PUSH);
40162
- GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
40163
- WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
40164
- GH_API_WRITE = new RegExp(
40165
- `\\bgh\\s+api\\b(?=[^;&|]*(?:\\s(?:-X|--method)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-f|-F|--field|--raw-field|--input)[\\s=]))`
40166
- );
40167
- CURL_GITHUB_WRITE = new RegExp(
40168
- `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
40169
- );
40170
- }
40171
- });
40172
-
40173
- // ../../src/core/harness/pi/relay.ts
40174
- async function relayToolCall(harness, calls, ask, opts = {}) {
40175
- const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
40176
- const window = new AbortController();
40177
- const sleep2 = opts.sleep ?? sleepUnlessAborted;
40178
- const progress = await Promise.race([
40179
- answer.then((a) => ({ done: true, answer: a })),
40180
- sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
40181
- ]);
40182
- window.abort();
40183
- return progress;
40184
- }
40185
- function relayedToolDefinitions(harness) {
40186
- return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
40187
- }
40188
- function authorizeToolCall(harness, ask) {
40189
- harness.gateSaw(ask.toolCallId);
40190
- const blocked = harness.toolsBlocked();
40191
- const refuse3 = (reason2) => {
40192
- harness.emit({
40193
- type: "run_note",
40194
- kind: "tool_refused",
40195
- summary: redactAndCap(`${ask.tool} refused: ${reason2}`, 300)
40196
- });
40197
- return { allow: false, reason: reason2 };
40198
- };
40199
- if (blocked !== void 0) return refuse3(blocked);
40200
- if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
40201
- const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
40202
- if (verdict.verdict === "allowed") return { allow: true };
40203
- return refuse3(verdict.reason);
40204
- }
40205
- async function runRelayedTool(harness, ask, opts = {}) {
40206
- const tool = harness.tools.find((t) => t.name === ask.tool);
40207
- if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
40208
- const span = harness.toolSpan(ask.toolCallId);
40209
- const base = harness.toolContext;
40210
- const readConversation = base.conversation;
40211
- const ctx = {
40212
- ...span ? {
40213
- ...base,
40214
- span,
40215
- executor: new TracingExecutor(base.executor, span, harness.backend),
40216
- publish: (e) => harness.emit(withSpanId(e, span.id)),
40217
- ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
40218
- } : { ...base, publish: (e) => harness.emit(e) },
40219
- ...opts.signal && !base.signal ? { signal: opts.signal } : {},
40220
- ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {},
40221
- // After the spreads, so the call's own id can never be shadowed by one on the base context.
40222
- callId: ask.toolCallId
40223
- };
40224
- try {
40225
- const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
40226
- const output = await tool.run(input, ctx);
40227
- return { content: piContentOf(capToolResultContent(output)), isError: false };
40228
- } catch (err2) {
40229
- return errorAnswerOf(err2);
40230
- }
40231
- }
40232
- function errorAnswerOf(err2) {
40233
- const message3 = err2 instanceof Error ? err2.message : String(err2);
40234
- return { content: piContentOf(capToolResultContent(`Error: ${message3}`)), isError: true };
40235
- }
40236
- function piContentOf(content) {
40237
- if (typeof content === "string") return [{ type: "text", text: content }];
40238
- return content.map(
40239
- (p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "image" ? { type: "image", data: p.data, mimeType: p.mediaType } : { type: "text", text: `[document ${p.name ?? "document"} (${p.mediaType})]` }
40240
- );
40241
- }
40242
- function withSpanId(e, spanId) {
40243
- switch (e.type) {
40244
- case "tool_call":
40245
- case "tool_result":
40246
- case "run_note":
40247
- case "assistant":
40248
- case "skill_use":
40249
- return { ...e, spanId };
40250
- default:
40251
- return e;
40252
- }
40253
- }
40254
- var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
40255
- var init_relay = __esm({
40256
- "../../src/core/harness/pi/relay.ts"() {
40257
- "use strict";
40258
- init_tracingExecutor();
40259
- init_chatMessage();
40260
- init_awaitChildren();
40261
- init_runEvents();
40262
- init_toolRules();
40263
- HarnessRegistry = class {
40264
- live = /* @__PURE__ */ new Map();
40265
- /** The run drives a pi from here, its relayed calls kept beside it; the
40266
- * returned function forgets it and ends whatever call still runs. */
40267
- register(harness) {
40268
- return this.hold({ harness, calls: new RelayedCalls() });
40269
- }
40270
- /** A process relaunched under the same run — in a replacement container,
40271
- * with a rotated bearer (harness-pi item 8) — takes the run's registration
40272
- * over, and the run's relayed calls stay: a call in flight keeps running
40273
- * under the same end signal, and an ask by its id through the new
40274
- * registration joins it and reads its answer. The earlier registration's
40275
- * forget is a no-op from here; the returned one forgets the run and ends
40276
- * the calls. Refuses by name a run that is not registered. */
40277
- replace(harness) {
40278
- const previous = this.live.get(harness.runId);
40279
- if (!previous) throw new Error(`run ${harness.runId} is not registered on the relay: nothing to replace`);
40280
- return this.hold({ harness, calls: previous.calls });
40281
- }
40282
- hold(entry) {
40283
- this.live.set(entry.harness.runId, entry);
40284
- return () => {
40285
- if (this.live.get(entry.harness.runId) !== entry) return;
40286
- this.live.delete(entry.harness.runId);
40287
- entry.calls.end();
40288
- };
40289
- }
40290
- get(runId3) {
40291
- return this.live.get(runId3)?.harness;
40292
- }
40293
- /** The run's relayed calls, for as long as it is registered. */
40294
- calls(runId3) {
40295
- return this.live.get(runId3)?.calls;
40296
- }
40297
- size() {
40298
- return this.live.size;
40299
- }
40300
- };
40301
- RELAY_POLL_WINDOW_MS = 3e4;
40302
- RelayedCalls = class {
40303
- calls = /* @__PURE__ */ new Map();
40304
- /** The ids whose call is still running: joined by `join`, forgotten as each answers. */
40305
- running = /* @__PURE__ */ new Set();
40306
- ending = new AbortController();
40307
- get size() {
40308
- return this.calls.size;
40309
- }
40310
- /** Aborted when the run ends: the signal every call here runs under. */
40311
- get signal() {
40312
- return this.ending.signal;
40313
- }
40314
- /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
40315
- join(callId, start3) {
40316
- let answer = this.calls.get(callId);
40317
- if (!answer) {
40318
- answer = start3();
40319
- this.calls.set(callId, answer);
40320
- this.running.add(callId);
40321
- const done = () => void this.running.delete(callId);
40322
- answer.then(done, done);
40323
- }
40324
- return answer;
40325
- }
40326
- /** The ids of the calls still running, in the order they were started. */
40327
- inFlight() {
40328
- return [...this.running];
40329
- }
40330
- /** Every call still running, awaited together up to one window — the relay
40331
- * window a request waits, by default — for a relaunch that rebuilds the
40332
- * process's session (harness-pi item 8): a call that answers inside it is
40333
- * `done` with its answer, the result the rebuilt session carries; one still
40334
- * running after it is not, keeps running here for an ask by the same id,
40335
- * and the rebuilt session carries the still-running note in its place
40336
- * (`stillRunningNote`). A call whose `start` rejected is `done` with an
40337
- * error answer, as `runRelayedTool` answers its own throws, so one
40338
- * straggler's exception never loses the others' answers. Nothing is ended
40339
- * or re-run. Answers at once with nothing in flight. */
40340
- async awaitInFlight(opts = {}) {
40341
- const ids = this.inFlight();
40342
- if (ids.length === 0) return [];
40343
- const window = new AbortController();
40344
- const sleep2 = opts.sleep ?? sleepUnlessAborted;
40345
- const closed = sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({
40346
- done: false
40347
- }));
40348
- const progress = await Promise.all(
40349
- ids.map(async (callId) => {
40350
- const answered = this.calls.get(callId).then(
40351
- (answer) => ({ done: true, answer }),
40352
- (err2) => ({ done: true, answer: errorAnswerOf(err2) })
40353
- );
40354
- return { callId, ...await Promise.race([answered, closed]) };
40355
- })
40356
- );
40357
- window.abort();
40358
- return progress;
40359
- }
40360
- /** The call's answer is known before it is asked (harness-pi item 8): the
40361
- * record's settlement for a call in flight when the previous generation
40362
- * died, whose extension asks again with the same id. Every ask reads it and
40363
- * the tool never runs; a call already joined keeps the answer it has. */
40364
- settle(callId, answer) {
40365
- if (!this.calls.has(callId)) this.calls.set(callId, Promise.resolve(answer));
40366
- }
40367
- end() {
40368
- this.ending.abort();
40369
- this.calls.clear();
40370
- this.running.clear();
40371
- }
40372
- };
40373
- }
40374
- });
40375
-
40376
40872
  // ../../src/core/runLedger/takeover.ts
40377
40873
  var LedgerTakeover;
40378
40874
  var init_takeover = __esm({
@@ -41479,7 +41975,8 @@ function buildMcp(config, secrets, opts = {}) {
41479
41975
  fetch: webFetch,
41480
41976
  publicBaseUrl: opts.publicBaseUrl,
41481
41977
  bearers: secrets,
41482
- resolveEmail: opts.resolveEmail
41978
+ resolveEmail: opts.resolveEmail,
41979
+ resolveName: opts.resolveName
41483
41980
  });
41484
41981
  return { source: service.source, service };
41485
41982
  }
@@ -42285,6 +42782,45 @@ var init_dedupe = __esm({
42285
42782
  });
42286
42783
 
42287
42784
  // ../../src/channels/slack/lookups.ts
42785
+ function resolvePersonByEmail(client, email, opts = {}) {
42786
+ const key = email.trim().toLowerCase();
42787
+ if (!key.includes("@") || typeof client.users.lookupByEmail !== "function") return Promise.resolve(void 0);
42788
+ const now = opts.now ?? Date.now;
42789
+ const cached = personByEmailCache.get(key);
42790
+ if (cached && cached.expiresAt > now()) return Promise.resolve(cached.person);
42791
+ const inFlight = personByEmailInFlight.get(key);
42792
+ if (inFlight) return inFlight;
42793
+ const lookup2 = (async () => {
42794
+ let timer;
42795
+ try {
42796
+ const call2 = client.users.lookupByEmail({ email: key }).then(
42797
+ (r) => r.user,
42798
+ () => void 0
42799
+ );
42800
+ const timedOut = /* @__PURE__ */ Symbol("person lookup timed out");
42801
+ const answer = await Promise.race([
42802
+ call2,
42803
+ new Promise((resolve12) => {
42804
+ timer = setTimeout(() => resolve12(timedOut), opts.timeoutMs ?? PERSON_LINK_TIMEOUT_MS);
42805
+ })
42806
+ ]);
42807
+ if (answer === timedOut) return void 0;
42808
+ const u = answer;
42809
+ const person = u?.id && !u.deleted && !u.is_bot && !u.is_app_user ? { id: `slack:${u.id}`, ...displayNameOf2(u) ? { name: displayNameOf2(u) } : {} } : void 0;
42810
+ personByEmailCache.set(key, { person, expiresAt: now() + PERSON_LINK_TTL_MS });
42811
+ if (personByEmailCache.size > NAME_CACHE_MAX) {
42812
+ const oldest = personByEmailCache.keys().next().value;
42813
+ if (oldest !== void 0) personByEmailCache.delete(oldest);
42814
+ }
42815
+ return person;
42816
+ } finally {
42817
+ if (timer) clearTimeout(timer);
42818
+ personByEmailInFlight.delete(key);
42819
+ }
42820
+ })();
42821
+ personByEmailInFlight.set(key, lookup2);
42822
+ return lookup2;
42823
+ }
42288
42824
  function cachePut(cache2, key, value) {
42289
42825
  cache2.set(key, value);
42290
42826
  if (cache2.size > NAME_CACHE_MAX) {
@@ -42338,10 +42874,15 @@ async function resolveTeamUrl(client) {
42338
42874
  }
42339
42875
  return teamUrl;
42340
42876
  }
42341
- var NAME_CACHE_MAX, channelNameCache, userNameCache, teamUrl;
42877
+ var PERSON_LINK_TTL_MS, PERSON_LINK_TIMEOUT_MS, personByEmailCache, personByEmailInFlight, displayNameOf2, NAME_CACHE_MAX, channelNameCache, userNameCache, teamUrl;
42342
42878
  var init_lookups = __esm({
42343
42879
  "../../src/channels/slack/lookups.ts"() {
42344
42880
  "use strict";
42881
+ PERSON_LINK_TTL_MS = 10 * 6e4;
42882
+ PERSON_LINK_TIMEOUT_MS = 1500;
42883
+ personByEmailCache = /* @__PURE__ */ new Map();
42884
+ personByEmailInFlight = /* @__PURE__ */ new Map();
42885
+ displayNameOf2 = (u) => u?.profile?.display_name || u?.profile?.real_name || u?.real_name || u?.name || void 0;
42345
42886
  NAME_CACHE_MAX = 1e3;
42346
42887
  channelNameCache = /* @__PURE__ */ new Map();
42347
42888
  userNameCache = /* @__PURE__ */ new Map();
@@ -44000,8 +44541,8 @@ function parseIndexCursor(params) {
44000
44541
  if (!params.has("before") || !Number.isFinite(before) || before <= 0 || beforeId === "") return void 0;
44001
44542
  return { before, beforeId };
44002
44543
  }
44003
- function olderRunsHref(cursor) {
44004
- return `/runs?all=1&before=${cursor.finishedAt}&beforeId=${encodeURIComponent(cursor.id)}`;
44544
+ function olderRunsHref(cursor, opts = {}) {
44545
+ return `/runs?all=1${opts.mine ? "&mine=1" : ""}&before=${cursor.finishedAt}&beforeId=${encodeURIComponent(cursor.id)}`;
44005
44546
  }
44006
44547
  function readDecision(actor, view) {
44007
44548
  const admitted = authorize(actor, "runs:read", RUNS_GET);
@@ -44099,7 +44640,7 @@ function createLiveViewHandler(deps) {
44099
44640
  })
44100
44641
  );
44101
44642
  };
44102
- const mergedRows = async (live, visibleTo, cursor) => {
44643
+ const mergedRows = async (live, visibleTo, cursor, view = {}) => {
44103
44644
  const tokens = new Map(live.map((s) => [s.id, s.token]));
44104
44645
  const { runs, nextBefore, storeUnavailable } = await service.listRuns({
44105
44646
  status: "all",
@@ -44111,7 +44652,7 @@ function createLiveViewHandler(deps) {
44111
44652
  return {
44112
44653
  rows,
44113
44654
  ...storeUnavailable ? { storeUnavailable: true } : {},
44114
- ...nextBefore ? { olderHref: olderRunsHref(nextBefore) } : {},
44655
+ ...nextBefore ? { olderHref: olderRunsHref(nextBefore, view) } : {},
44115
44656
  ...cursor ? { olderThan: cursor.before } : {}
44116
44657
  };
44117
44658
  };
@@ -44128,7 +44669,9 @@ function createLiveViewHandler(deps) {
44128
44669
  }
44129
44670
  if (route3.kind === "index") {
44130
44671
  const all = url.searchParams.get("all") === "1";
44131
- const visibleTo = readableRuns(ctx.actor);
44672
+ const mine = url.searchParams.get("mine") === "1";
44673
+ const readable2 = readableRuns(ctx.actor);
44674
+ const visibleTo = mine ? allOf([readable2, ownedBy(ctx.actor)]) : readable2;
44132
44675
  if (url.searchParams.get("stream") === "1") {
44133
44676
  serveIndexEvents(
44134
44677
  visibleIndexFeed((onEvent) => index.subscribeIndex(onEvent), visibleTo),
@@ -44144,6 +44687,8 @@ function createLiveViewHandler(deps) {
44144
44687
  const seed = {
44145
44688
  page: "runs",
44146
44689
  all,
44690
+ mine,
44691
+ ...ctx.actor.asUser ? { asUser: ctx.actor.asUser } : {},
44147
44692
  retentionDays: deps.retention ? deps.retention.retentionDays : null,
44148
44693
  now: now(),
44149
44694
  rows: [...page2.rows],
@@ -44161,7 +44706,7 @@ function createLiveViewHandler(deps) {
44161
44706
  );
44162
44707
  return true;
44163
44708
  }
44164
- run2(res, async () => render3(await mergedRows(live, visibleTo, parseIndexCursor(url.searchParams))));
44709
+ run2(res, async () => render3(await mergedRows(live, visibleTo, parseIndexCursor(url.searchParams), { mine })));
44165
44710
  return true;
44166
44711
  }
44167
44712
  if (route3.kind === "scheduled") {
@@ -44945,15 +45490,28 @@ function createSettingsViewHandler(deps, shell) {
44945
45490
  action,
44946
45491
  kind === "org" ? { type: "config-scope", kind: "org" } : { type: "config-scope", kind: "channel", id: id ?? "" }
44947
45492
  ).allow;
45493
+ const serversOf = (answer) => answer.ok ? answer.value.servers ?? [] : [];
44948
45494
  async function mcpsSeed(caller, channel) {
44949
- const listed = await deps.commands.invoke("mcp.list", channel ? { options: { channel } } : {}, caller);
44950
45495
  const write = {
44951
45496
  org: canWrite(caller, "mcp:write", "org"),
44952
45497
  channel: channel !== void 0 && canWrite(caller, "mcp:write", "channel", channel)
44953
45498
  };
45499
+ const listed = await deps.commands.invoke(
45500
+ "mcp.list",
45501
+ write.org ? { options: { all: true } } : channel ? { options: { channel } } : {},
45502
+ caller
45503
+ );
44954
45504
  if (!listed.ok)
44955
45505
  return { ...channel ? { channel } : {}, servers: [], unavailable: failureText(listed), canWrite: write };
44956
- const servers = listed.value.servers ?? [];
45506
+ if (write.org)
45507
+ return { ...channel ? { channel } : {}, allTiers: true, servers: serversOf(listed), canWrite: write };
45508
+ const servers = serversOf(listed);
45509
+ const readable = await deps.commands.invoke("config.overrides", {}, caller);
45510
+ const others = (readable.ok ? readable.value.channels ?? [] : []).filter((r) => r.channelId !== channel && r.settings.includes("mcpServers")).map((r) => r.channelId);
45511
+ const tiers = await Promise.all(
45512
+ others.map((id) => deps.commands.invoke("mcp.list", { options: { channel: id } }, caller))
45513
+ );
45514
+ for (const answer of tiers) for (const s of serversOf(answer)) if (s.scope === "channel") servers.push(s);
44957
45515
  return { ...channel ? { channel } : {}, servers, canWrite: write };
44958
45516
  }
44959
45517
  async function channelsSeed(caller, channel) {
@@ -44976,9 +45534,7 @@ function createSettingsViewHandler(deps, shell) {
44976
45534
  plain4(res, 405, "method not allowed", { allow: "GET" });
44977
45535
  return true;
44978
45536
  }
44979
- const caller = deps.callerFor(ctx.identity);
44980
45537
  const tab = route3.tab === "home" ? homeTab(deps.capabilities) : route3.tab;
44981
- const base = { page: "settings", tab, viewer: caller.id, vocabulary: deps.vocabulary };
44982
45538
  const failed = (err2) => {
44983
45539
  const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
44984
45540
  if (res.headersSent) {
@@ -44993,20 +45549,27 @@ function createSettingsViewHandler(deps, shell) {
44993
45549
  res.end(shell("Settings", seed));
44994
45550
  };
44995
45551
  const channel = "channel" in route3 ? route3.channel : void 0;
44996
- if (tab === "installation") {
44997
- if (!authorize(caller.actor, "config:read", { type: "command", id: "config.show" }).allow) {
44998
- plain4(res, 403, "forbidden");
44999
- return true;
45552
+ deps.callerFor(ctx.identity).then(async (caller) => {
45553
+ const base = {
45554
+ page: "settings",
45555
+ tab,
45556
+ viewer: caller.id,
45557
+ ...caller.actor.asUser ? { asUser: caller.actor.asUser } : {},
45558
+ vocabulary: deps.vocabulary
45559
+ };
45560
+ if (tab === "installation") {
45561
+ if (!authorize(caller.actor, "config:read", { type: "command", id: "config.show" }).allow) {
45562
+ plain4(res, 403, "forbidden");
45563
+ return;
45564
+ }
45565
+ render3({ ...base, installation: deps.installation() });
45566
+ return;
45000
45567
  }
45001
- render3({ ...base, installation: deps.installation() });
45002
- return true;
45003
- }
45004
- const build = tab === "mcps" ? mcpsSeed(caller, channel) : channelsSeed(caller, channel);
45005
- build.then(
45006
- (part) => render3(
45568
+ const part = tab === "mcps" ? await mcpsSeed(caller, channel) : await channelsSeed(caller, channel);
45569
+ render3(
45007
45570
  tab === "mcps" ? { ...base, mcps: part } : { ...base, channels: part }
45008
- )
45009
- ).catch(failed);
45571
+ );
45572
+ }).catch(failed);
45010
45573
  return true;
45011
45574
  };
45012
45575
  }
@@ -47459,7 +48022,18 @@ async function merge(body, deps, subject2) {
47459
48022
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
47460
48023
  if (isReleasePullRequest(facts2))
47461
48024
  return refused6(`${where} is the release pull request \u2014 always a person's merge, never the runner's`);
47462
- if (facts2.state !== "open") return refused6(`${where} is ${facts2.state}`);
48025
+ if (facts2.state !== "open") {
48026
+ if (facts2.mergedAt !== void 0 && facts2.mergeCommitSha !== void 0)
48027
+ return json(200, {
48028
+ ok: true,
48029
+ outcome: "merged",
48030
+ by: "other",
48031
+ sha: facts2.mergeCommitSha,
48032
+ mergedAt: facts2.mergedAt,
48033
+ at
48034
+ });
48035
+ return refused6(`${where} is ${facts2.state}`);
48036
+ }
47463
48037
  if (facts2.headRef !== row2.branch)
47464
48038
  return refused6(`${where} heads \`${facts2.headRef ?? "?"}\`, not the unit's branch \`${row2.branch}\``);
47465
48039
  if (facts2.headSha === void 0 || !sameCommit(facts2.headSha, headSha))
@@ -47857,8 +48431,24 @@ function accessActor(identity2, grantsFor) {
47857
48431
  function serviceTokenAllowed(pathname, identity2) {
47858
48432
  return !isServiceToken(identity2) || isCommandPath(pathname);
47859
48433
  }
47860
- function callerFor(identity2, opts) {
47861
- return { kind: "access", id: callerIdFor(identity2), actor: accessActor(identity2, opts.grantsFor) };
48434
+ async function resolveAccessActor(identity2, opts) {
48435
+ const actor = accessActor(identity2, opts.grantsFor);
48436
+ if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
48437
+ const person = await opts.personByEmail(identity2.email).catch(() => void 0);
48438
+ if (!person || !person.id.startsWith("slack:")) return actor;
48439
+ return {
48440
+ ...actor,
48441
+ self: [actor.id, person.id],
48442
+ asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
48443
+ };
48444
+ }
48445
+ async function callerFor(identity2, opts) {
48446
+ return {
48447
+ kind: "access",
48448
+ id: callerIdFor(identity2),
48449
+ actor: await resolveAccessActor(identity2, opts),
48450
+ ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
48451
+ };
47862
48452
  }
47863
48453
  function hostOf2(url) {
47864
48454
  if (!url) return void 0;
@@ -47925,7 +48515,7 @@ function createCommandHttpHandler(commands, opts) {
47925
48515
  refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
47926
48516
  return;
47927
48517
  }
47928
- const caller = callerFor(identity2, opts);
48518
+ const caller = await callerFor(identity2, opts);
47929
48519
  if (CommandRegistry.refuses(cmd, caller)) {
47930
48520
  refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
47931
48521
  return;
@@ -48013,9 +48603,14 @@ async function runBot() {
48013
48603
  const completions = new PiAiProviders(config.config.providers);
48014
48604
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
48015
48605
  let slackEmailLookup;
48606
+ let slackNameLookup;
48607
+ let slackPersonByEmail;
48608
+ const personByEmail = (email) => slackPersonByEmail ? slackPersonByEmail(email) : Promise.resolve(void 0);
48016
48609
  const mcpWiring = buildMcp(config, processSecrets, {
48017
48610
  publicBaseUrl: process.env.PUBLIC_BASE_URL,
48018
48611
  resolveEmail: (userId) => slackEmailLookup ? slackEmailLookup(userId) : Promise.resolve(void 0),
48612
+ // `addedBy` as a name on the MCP lists (record 0042): the cached lookup the runs index uses.
48613
+ resolveName: (userId) => slackNameLookup ? slackNameLookup(userId) : Promise.resolve(void 0),
48019
48614
  warn: (m) => console.warn(`[mcp] ${m}`)
48020
48615
  });
48021
48616
  const mcp = mcpWiring.source ?? new NullMcpToolSource();
@@ -48211,6 +48806,8 @@ async function runBot() {
48211
48806
  const conversationReader = new SlackConversationReader(app.client);
48212
48807
  deps.conversationReaders = [conversationReader];
48213
48808
  slackEmailLookup = (userId) => userId.startsWith("slack:") ? resolveUserEmail(app.client, userId.slice("slack:".length)) : Promise.resolve(void 0);
48809
+ slackNameLookup = (userId) => userId.startsWith("slack:") ? resolveUserName(app.client, userId.slice("slack:".length)) : Promise.resolve(void 0);
48810
+ slackPersonByEmail = (email) => resolvePersonByEmail(app.client, email);
48214
48811
  const pendingHistoryWrites = () => runHistoryWriter.pending();
48215
48812
  const inFlight = () => activeRunCount() + pendingReflectionCount() + pendingHistoryWrites();
48216
48813
  let draining = false;
@@ -48287,7 +48884,7 @@ async function runBot() {
48287
48884
  const settingsView = createSettingsViewHandler(
48288
48885
  {
48289
48886
  commands,
48290
- callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id) }),
48887
+ callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail }),
48291
48888
  installation: () => installationSettings(config.config, capabilities),
48292
48889
  vocabulary: {
48293
48890
  agents: Object.keys(AGENTS),
@@ -48338,6 +48935,7 @@ async function runBot() {
48338
48935
  const accessState = `dashboard auth: ${dashboardAuth.describe()}`;
48339
48936
  const commandHttp = createCommandHttpHandler(commands, {
48340
48937
  grantsFor: (id) => config.grantsFor(id),
48938
+ personByEmail,
48341
48939
  publicBaseUrl
48342
48940
  });
48343
48941
  const commandHttpState = `GET|POST /api/<group>.<verb> (${commands.list().length} commands)`;
@@ -48401,7 +48999,7 @@ async function runBot() {
48401
48999
  }
48402
49000
  if (webAssets.serve(req, res)) return;
48403
49001
  if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/")) {
48404
- dashboardAuth.verify(req).then((gate) => {
49002
+ dashboardAuth.verify(req).then(async (gate) => {
48405
49003
  if (!gate.ok) {
48406
49004
  res.writeHead(gate.status, { "content-type": "text/plain; charset=utf-8" });
48407
49005
  res.end(gate.body);
@@ -48417,7 +49015,10 @@ async function runBot() {
48417
49015
  return;
48418
49016
  }
48419
49017
  if (isCommandPath(path)) return commandHttp(req, res, gate.identity);
48420
- const actor = accessActor(gate.identity, (id) => config.grantsFor(id));
49018
+ const actor = await resolveAccessActor(gate.identity, {
49019
+ grantsFor: (id) => config.grantsFor(id),
49020
+ personByEmail
49021
+ });
48421
49022
  if (liveView2(req, res, { actor })) return;
48422
49023
  if (mcpConnectView(req, res, gate.identity)) return;
48423
49024
  if (residentsView(req, res, { actor })) return;