@coreplane/switchboard 1.225.0 → 1.226.1

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 (28) hide show
  1. package/dist/assets/package-lock.json +3 -3
  2. package/dist/assets/package.json +1 -1
  3. package/dist/assets/source.json +3 -3
  4. package/dist/assets/src/agents/registry.ts +1 -1
  5. package/dist/assets/src/core/chatMessage.ts +77 -0
  6. package/dist/assets/src/{providers/types.ts → core/provider.ts} +21 -71
  7. package/dist/assets/src/core/reviewVerdict.ts +1 -1
  8. package/dist/assets/src/core/runEvents.ts +22 -3
  9. package/dist/assets/src/core/runLedger/sessionLog.ts +1 -1
  10. package/dist/assets/src/core/runLedger/transcript.ts +1 -1
  11. package/dist/assets/src/core/runLedger/types.ts +2 -1
  12. package/dist/assets/web/dist/.vite/manifest.json +20 -20
  13. package/dist/assets/web/dist/assets/CostsPage-5pl3HB2F.js +2 -0
  14. package/dist/assets/web/dist/assets/{ResidentDetailPage-Cu4Ve2j4.js → ResidentDetailPage-BBOpejGX.js} +1 -1
  15. package/dist/assets/web/dist/assets/{ResidentsIndexPage-_QIDVyOj.js → ResidentsIndexPage-B8kFWHpB.js} +1 -1
  16. package/dist/assets/web/dist/assets/RunRoutePage-COWeVtIE.js +12 -0
  17. package/dist/assets/web/dist/assets/{RunsIndexPage-JQvnDmo0.js → RunsIndexPage-BjH93cKx.js} +1 -1
  18. package/dist/assets/web/dist/assets/{ScheduledPage-DEVRWqZi.js → ScheduledPage-grlNKvCA.js} +1 -1
  19. package/dist/assets/web/dist/assets/{StatusDot-BDAKF1Bp.js → StatusDot-BQNoaXO6.js} +1 -1
  20. package/dist/assets/web/dist/assets/{Tooltip-BFncyNUQ.js → Tooltip-DVCeIzaa.js} +1 -1
  21. package/dist/assets/web/dist/assets/{dist-DJfQXPjH.js → dist-B5Wfk-oY.js} +1 -1
  22. package/dist/assets/web/dist/assets/{main-BjhiJxQq.js → main-CN0U7d6s.js} +2 -2
  23. package/dist/assets/web/dist/assets/main-xLAsdkfB.css +1 -0
  24. package/dist/cli.js +1228 -930
  25. package/package.json +1 -1
  26. package/dist/assets/web/dist/assets/CostsPage-BlPwsI2T.js +0 -3
  27. package/dist/assets/web/dist/assets/RunRoutePage-DxgcHX-0.js +0 -12
  28. package/dist/assets/web/dist/assets/main-DymOuq2l.css +0 -1
package/dist/cli.js CHANGED
@@ -11448,25 +11448,7 @@ var init_config3 = __esm({
11448
11448
  }
11449
11449
  });
11450
11450
 
11451
- // ../../src/providers/types.ts
11452
- function toolResultText(content) {
11453
- if (typeof content === "string") return content;
11454
- return content.map((p) => {
11455
- if (p.type === "text") return p.text;
11456
- if (p.type === "image") return `[image ${p.mediaType}, ${base64Bytes(p.data)} bytes]`;
11457
- return `[document ${p.name ?? "document"} (${p.mediaType}), ${base64Bytes(p.data)} bytes]`;
11458
- }).join("\n");
11459
- }
11460
- function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
11461
- const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
11462
- \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
11463
- if (typeof content === "string") return capText(content);
11464
- return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
11465
- }
11466
- function base64Bytes(b64) {
11467
- const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
11468
- return Math.floor(b64.length * 3 / 4) - padding;
11469
- }
11451
+ // ../../src/core/provider.ts
11470
11452
  function parseModelRef(ref) {
11471
11453
  const i = ref.indexOf("/");
11472
11454
  if (i === -1) {
@@ -11474,11 +11456,10 @@ function parseModelRef(ref) {
11474
11456
  }
11475
11457
  return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
11476
11458
  }
11477
- var MAX_TOOL_RESULT_CHARS, ANTHROPIC_API_KEY_ENV;
11478
- var init_types2 = __esm({
11479
- "../../src/providers/types.ts"() {
11459
+ var ANTHROPIC_API_KEY_ENV;
11460
+ var init_provider = __esm({
11461
+ "../../src/core/provider.ts"() {
11480
11462
  "use strict";
11481
- MAX_TOOL_RESULT_CHARS = 12e4;
11482
11463
  ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
11483
11464
  }
11484
11465
  });
@@ -11608,7 +11589,7 @@ var init_anthropic = __esm({
11608
11589
  "../../src/providers/anthropic.ts"() {
11609
11590
  "use strict";
11610
11591
  init_secrets2();
11611
- init_types2();
11592
+ init_provider();
11612
11593
  AnthropicProvider = class {
11613
11594
  name;
11614
11595
  client;
@@ -15954,7 +15935,7 @@ function isRunEvent(v) {
15954
15935
  case "skill_use":
15955
15936
  return typeof o.skill === "string" && typeof o.agent === "string" && typeof o.bodyBytes === "number";
15956
15937
  case "artifact":
15957
- return (o.direction === "in" || o.direction === "out") && typeof o.key === "string" && typeof o.name === "string" && typeof o.size === "number" && typeof o.contentType === "string" && o.url === void 0;
15938
+ return (o.direction === "in" && typeof o.messageId === "string" || o.direction === "out" && typeof o.callId === "string") && typeof o.key === "string" && typeof o.name === "string" && typeof o.size === "number" && typeof o.contentType === "string" && o.url === void 0;
15958
15939
  case "review_artifact":
15959
15940
  if (o.artifact === "pr_description") {
15960
15941
  return (o.origin === "submitted" || o.origin === "parsed") && typeof o.repo === "string" && typeof o.pr === "number" && typeof o.title === "string" && typeof o.body === "string" && Array.isArray(o.tour);
@@ -16733,7 +16714,7 @@ var init_connect = __esm({
16733
16714
 
16734
16715
  // ../../src/mcp/types.ts
16735
16716
  var McpError;
16736
- var init_types3 = __esm({
16717
+ var init_types2 = __esm({
16737
16718
  "../../src/mcp/types.ts"() {
16738
16719
  "use strict";
16739
16720
  McpError = class extends Error {
@@ -16828,7 +16809,7 @@ var MCP_PROTOCOL_VERSION, MCP_REQUEST_TIMEOUT_MS, MCP_MAX_RESPONSE_BYTES, MCP_MA
16828
16809
  var init_client = __esm({
16829
16810
  "../../src/mcp/client.ts"() {
16830
16811
  "use strict";
16831
- init_types3();
16812
+ init_types2();
16832
16813
  MCP_PROTOCOL_VERSION = "2025-06-18";
16833
16814
  MCP_REQUEST_TIMEOUT_MS = 3e4;
16834
16815
  MCP_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
@@ -17543,7 +17524,7 @@ var init_bridge = __esm({
17543
17524
  "../../src/mcp/bridge.ts"() {
17544
17525
  "use strict";
17545
17526
  init_commandRegistry();
17546
- init_types3();
17527
+ init_types2();
17547
17528
  init_classify();
17548
17529
  MCP_TOOL_NAME_MAX = 64;
17549
17530
  MCP_TOOL_PREFIX = "mcp__";
@@ -18113,23 +18094,31 @@ var init_service = __esm({
18113
18094
  callbackUrl() {
18114
18095
  return `${(this.opts.publicBaseUrl ?? "").replace(/\/+$/, "")}/mcp/oauth/callback`;
18115
18096
  }
18116
- /** In-flight refreshes by server id, so N concurrent runs refresh once. */
18097
+ /** Refreshes by server id, keyed by the expiry of the credential that
18098
+ * triggered them, so N concurrent runs refresh once. A settled entry stays:
18099
+ * a run whose credential read raced the store write joins it instead of
18100
+ * refreshing again. A failed refresh is dropped so the next run retries; a
18101
+ * newer expiry (the refreshed set aging into the skew) starts a new one. */
18117
18102
  refreshing = /* @__PURE__ */ new Map();
18118
18103
  /** A usable access token for the run: the stored one while it lives, else
18119
18104
  * a refreshed set (stored back, the cached client dropped so it is rebuilt
18120
18105
  * with the new header). */
18121
18106
  freshOAuth(serverId, cred) {
18122
18107
  if (!needsRefresh(cred, this.now())) return Promise.resolve(cred);
18123
- let inflight = this.refreshing.get(serverId);
18124
- if (!inflight) {
18125
- inflight = (async () => {
18126
- const next = await refreshCredential(this.opts.fetch, cred, this.now());
18127
- await this.storeOAuth(serverId, next);
18128
- return next;
18129
- })().finally(() => this.refreshing.delete(serverId));
18130
- this.refreshing.set(serverId, inflight);
18131
- }
18132
- return inflight;
18108
+ const seed = cred.expiresAt ?? 0;
18109
+ const hit = this.refreshing.get(serverId);
18110
+ if (hit && hit.seed >= seed) return hit.done;
18111
+ const done = (async () => {
18112
+ const next = await refreshCredential(this.opts.fetch, cred, this.now());
18113
+ await this.storeOAuth(serverId, next);
18114
+ return next;
18115
+ })();
18116
+ const entry = { seed, done };
18117
+ this.refreshing.set(serverId, entry);
18118
+ done.catch(() => {
18119
+ if (this.refreshing.get(serverId) === entry) this.refreshing.delete(serverId);
18120
+ });
18121
+ return done;
18133
18122
  }
18134
18123
  // ---- the run-time view ----------------------------------------------------------
18135
18124
  /** Servers a run may use: every tier's entries whose agents include the
@@ -18841,7 +18830,7 @@ var init_authz = __esm({
18841
18830
  });
18842
18831
 
18843
18832
  // ../../src/core/llmOutput/types.ts
18844
- var init_types4 = __esm({
18833
+ var init_types3 = __esm({
18845
18834
  "../../src/core/llmOutput/types.ts"() {
18846
18835
  "use strict";
18847
18836
  }
@@ -18900,10 +18889,26 @@ var init_markdown = __esm({
18900
18889
  });
18901
18890
 
18902
18891
  // ../../src/core/llmOutput/json.ts
18903
- function stripJsonFence(text) {
18904
- const trimmed = text.trim();
18905
- const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
18906
- return fenced ? fenced[1] : trimmed;
18892
+ function extractJson(text) {
18893
+ const start3 = text.search(/[[{]/);
18894
+ if (start3 === -1) return text.trim();
18895
+ let depth = 0;
18896
+ let inString = false;
18897
+ for (let i = start3; i < text.length; i++) {
18898
+ const ch = text[i];
18899
+ if (inString) {
18900
+ if (ch === "\\") i++;
18901
+ else if (ch === '"') inString = false;
18902
+ } else if (ch === '"') {
18903
+ inString = true;
18904
+ } else if (ch === "{" || ch === "[") {
18905
+ depth++;
18906
+ } else if (ch === "}" || ch === "]") {
18907
+ depth--;
18908
+ if (depth === 0) return text.slice(start3, i + 1);
18909
+ }
18910
+ }
18911
+ return text.slice(start3);
18907
18912
  }
18908
18913
  function jsonValueEquals(a, b) {
18909
18914
  if (Object.is(a, b)) return true;
@@ -18923,7 +18928,7 @@ function jsonOutput(schema, opts = {}) {
18923
18928
  parse(raw) {
18924
18929
  let value;
18925
18930
  try {
18926
- value = JSON.parse(stripJsonFence(raw));
18931
+ value = JSON.parse(extractJson(raw));
18927
18932
  } catch (err2) {
18928
18933
  const observed = `not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`;
18929
18934
  return { ok: false, failure: { kind: "syntax", observed } };
@@ -18956,7 +18961,7 @@ var init_json = __esm({
18956
18961
  var init_llmOutput = __esm({
18957
18962
  "../../src/core/llmOutput/index.ts"() {
18958
18963
  "use strict";
18959
- init_types4();
18964
+ init_types3();
18960
18965
  init_markdown();
18961
18966
  init_json();
18962
18967
  }
@@ -19085,6 +19090,8 @@ function placeCandidate(cand, actor, origin, keys, scopeOf) {
19085
19090
  async function reflect(deps) {
19086
19091
  const warn = deps.onWarn ?? (() => {
19087
19092
  });
19093
+ const info = deps.onInfo ?? (() => {
19094
+ });
19088
19095
  try {
19089
19096
  const query = `${deps.request} ${deps.answer}`.slice(0, MAX_RETRIEVE_QUERY_CHARS);
19090
19097
  const existing = (await Promise.all(
@@ -19102,11 +19109,18 @@ async function reflect(deps) {
19102
19109
  const reply = result.content.filter((p) => p.type === "text").map((p) => p.text).join("");
19103
19110
  const prov = { sourceThreadKey: deps.sourceThreadKey, sourceRunId: deps.sourceRunId };
19104
19111
  const parsed = parseReflection(reply, prov, new Set(existing.map((r) => r.id)));
19112
+ const truncated = result.stopReason === "max_tokens" ? `truncated at ${REFLECTION_MAX_TOKENS} max tokens` : void 0;
19113
+ const stopNote = truncated ?? (result.stopReason === "end_turn" ? void 0 : `stop: ${result.stopReason}`);
19105
19114
  if (!parsed.ok) {
19106
- warn(`reflection output rejected (${parsed.error}); nothing written`);
19115
+ const why = truncated ?? (stopNote ? `${parsed.error}; ${stopNote}` : parsed.error);
19116
+ warn(`reflection output rejected (${why}); nothing written`);
19117
+ return;
19118
+ }
19119
+ const outcomeTail = stopNote ? ` (${stopNote})` : "";
19120
+ if (parsed.candidates.length === 0) {
19121
+ info(`reflection: nothing to write${outcomeTail}`);
19107
19122
  return;
19108
19123
  }
19109
- if (parsed.candidates.length === 0) return;
19110
19124
  const scopeOf = new Map(existing.map((r) => [r.id, r.scopeKey]));
19111
19125
  const origin = deps.originChannelVisibility ?? "unknown";
19112
19126
  const byScope = /* @__PURE__ */ new Map();
@@ -19135,6 +19149,10 @@ async function reflect(deps) {
19135
19149
  if (dropped.length > 0)
19136
19150
  warn(`write denied by policy, ${dropped.length} candidate(s) dropped: ${dropped.join(", ")}`);
19137
19151
  for (const [scopeKey, records] of byScope) await deps.store.write(scopeKey, records);
19152
+ const written = [...byScope.values()].flat();
19153
+ const facts2 = written.filter((r) => r.kind === "fact").length;
19154
+ const summary = written.some((r) => r.kind === "summary") ? ", summary" : "";
19155
+ info(`reflection wrote ${facts2} fact(s)${summary}${outcomeTail}`);
19138
19156
  } catch (err2) {
19139
19157
  warn(`reflection failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
19140
19158
  }
@@ -19346,6 +19364,7 @@ var init_buildStore2 = __esm({
19346
19364
  function scheduleReflection(input) {
19347
19365
  if (!input.cfg?.enabled || !shouldReflect(input.gate)) return;
19348
19366
  const warn = (m) => console.warn(`[memory] ${input.threadKey} ${m}`);
19367
+ const info = (m) => console.log(`[memory] ${input.threadKey} ${m}`);
19349
19368
  let provider;
19350
19369
  let model;
19351
19370
  try {
@@ -19369,7 +19388,8 @@ function scheduleReflection(input) {
19369
19388
  answer: input.answer,
19370
19389
  sourceThreadKey: input.threadKey,
19371
19390
  sourceRunId: input.runId,
19372
- onWarn: warn
19391
+ onWarn: warn,
19392
+ onInfo: info
19373
19393
  })
19374
19394
  );
19375
19395
  }
@@ -19397,7 +19417,7 @@ async function memoryContextBlock(organization, cfg, injected, query, userId, sc
19397
19417
  var init_memory = __esm({
19398
19418
  "../../src/core/memory/index.ts"() {
19399
19419
  "use strict";
19400
- init_types2();
19420
+ init_provider();
19401
19421
  init_scorer();
19402
19422
  init_scope();
19403
19423
  init_stores();
@@ -22641,6 +22661,7 @@ function durableInboxMessage(msg, text, at, from) {
22641
22661
  ...msg.userName !== void 0 ? { userName: msg.userName } : {},
22642
22662
  ...msg.sourceUrl !== void 0 ? { sourceUrl: msg.sourceUrl } : {},
22643
22663
  ...msg.channelName !== void 0 ? { channelName: msg.channelName } : {},
22664
+ ...msg.messageId !== void 0 ? { messageId: msg.messageId } : {},
22644
22665
  ...from !== void 0 ? { fromRunId: from.runId } : {},
22645
22666
  // Staged references are metadata (record 0033) — a few hundred bytes each —
22646
22667
  // so they ride the base row and survive the restart whatever the
@@ -22681,6 +22702,7 @@ function messageFromInbox(stored, fallbackAt) {
22681
22702
  const userName = str7("userName");
22682
22703
  const sourceUrl = str7("sourceUrl");
22683
22704
  const channelName = str7("channelName");
22705
+ const messageId = str7("messageId");
22684
22706
  const fromRunId = str7("fromRunId");
22685
22707
  const at = typeof m.at === "number" && Number.isFinite(m.at) ? m.at : fallbackAt;
22686
22708
  const images = attachmentsFromInbox(m.images);
@@ -22697,6 +22719,7 @@ ${note}` : text,
22697
22719
  ...userName !== void 0 ? { userName } : {},
22698
22720
  ...sourceUrl !== void 0 ? { sourceUrl } : {},
22699
22721
  ...channelName !== void 0 ? { channelName } : {},
22722
+ ...messageId !== void 0 ? { messageId } : {},
22700
22723
  ...images ? { images } : {},
22701
22724
  ...documents ? { documents } : {},
22702
22725
  ...staged ? { staged } : {}
@@ -22735,6 +22758,9 @@ var init_inboxMessage = __esm({
22735
22758
  });
22736
22759
 
22737
22760
  // ../../src/core/threadAdmission.ts
22761
+ function followUpMessageId(input) {
22762
+ return input.messageId ?? (input.ledgerSeq !== void 0 ? `inbox-${input.ledgerSeq}` : `at-${input.at}`);
22763
+ }
22738
22764
  function decideFollowUp(live, requested) {
22739
22765
  if (requested.agent !== void 0 && requested.agent !== live.agent)
22740
22766
  return { kind: "refuse", reason: "agent_mismatch", requestedAgent: requested.agent };
@@ -23127,6 +23153,7 @@ function followUpOf(msg, text, at, opts) {
23127
23153
  ...msg.images !== void 0 ? { images: msg.images } : {},
23128
23154
  ...msg.documents !== void 0 ? { documents: msg.documents } : {},
23129
23155
  ...msg.staged !== void 0 ? { staged: msg.staged } : {},
23156
+ ...msg.messageId !== void 0 ? { messageId: msg.messageId } : {},
23130
23157
  at,
23131
23158
  ...opts.ledgerSeq !== void 0 ? { ledgerSeq: opts.ledgerSeq } : {},
23132
23159
  ...opts.from !== void 0 ? { from: opts.from } : {},
@@ -24798,6 +24825,16 @@ var init_runRegistry = __esm({
24798
24825
  }
24799
24826
  });
24800
24827
 
24828
+ // ../../src/core/types.ts
24829
+ function messageIdOf(msg, runId3) {
24830
+ return msg.messageId ?? runId3;
24831
+ }
24832
+ var init_types4 = __esm({
24833
+ "../../src/core/types.ts"() {
24834
+ "use strict";
24835
+ }
24836
+ });
24837
+
24801
24838
  // ../../src/core/dispatch/fastPath.ts
24802
24839
  function isInlineRunCommand(id) {
24803
24840
  return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove)$/.test(id);
@@ -24884,7 +24921,12 @@ async function runInlineCommandRun(deps, msg, command2, io, execute, ending, tra
24884
24921
  );
24885
24922
  trace.bindRun(run2.id, (e) => registry.publish(run2.id, e));
24886
24923
  io.runStarted?.({ id: run2.id });
24887
- registry.publish(run2.id, { type: "input", text: redactSecrets(msg.text), at: clock3() });
24924
+ registry.publish(run2.id, {
24925
+ type: "input",
24926
+ text: redactSecrets(msg.text),
24927
+ messageId: messageIdOf(msg, run2.id),
24928
+ at: clock3()
24929
+ });
24888
24930
  registry.publish(run2.id, { type: "run_meta", agent: COMMAND_RUN_AGENT, traceId: root.traceId, at: clock3() });
24889
24931
  let result;
24890
24932
  try {
@@ -24969,6 +25011,7 @@ var init_fastPath = __esm({
24969
25011
  init_commandChat();
24970
25012
  init_commandSurface();
24971
25013
  init_runRegistry();
25014
+ init_types4();
24972
25015
  init_record();
24973
25016
  init_reply();
24974
25017
  }
@@ -25139,379 +25182,247 @@ var init_resolve = __esm({
25139
25182
  init_registry();
25140
25183
  init_profile();
25141
25184
  init_directives();
25142
- init_types2();
25185
+ init_provider();
25143
25186
  init_factory();
25144
25187
  init_githubRepoProbe();
25145
25188
  init_repoContext();
25146
25189
  }
25147
25190
  });
25148
25191
 
25149
- // ../../src/core/prDescription.ts
25150
- import { z as z20 } from "zod";
25151
- function parsePrDescription(input) {
25152
- return PrDescriptionSchema.parse(input);
25153
- }
25154
- function redactPrDescription(d) {
25155
- return mapStringLeaves(d, redactSecrets);
25192
+ // ../../src/artifacts/contentType.ts
25193
+ function contentTypeFor(name) {
25194
+ const dot = name.lastIndexOf(".");
25195
+ if (dot < 0 || dot === name.length - 1) return OCTET_STREAM;
25196
+ return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? OCTET_STREAM;
25156
25197
  }
25157
- function mapStringLeaves(value, fn) {
25158
- if (typeof value === "string") return fn(value);
25159
- if (Array.isArray(value)) return value.map((v) => mapStringLeaves(v, fn));
25160
- if (typeof value === "object" && value !== null) {
25161
- return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, mapStringLeaves(v, fn)]));
25198
+ var BY_EXTENSION, OCTET_STREAM, INLINE_IMAGE_TYPES;
25199
+ var init_contentType = __esm({
25200
+ "../../src/artifacts/contentType.ts"() {
25201
+ "use strict";
25202
+ BY_EXTENSION = {
25203
+ png: "image/png",
25204
+ jpg: "image/jpeg",
25205
+ jpeg: "image/jpeg",
25206
+ gif: "image/gif",
25207
+ webp: "image/webp",
25208
+ svg: "image/svg+xml",
25209
+ pdf: "application/pdf",
25210
+ mp4: "video/mp4",
25211
+ webm: "video/webm",
25212
+ mov: "video/quicktime",
25213
+ mp3: "audio/mpeg",
25214
+ wav: "audio/wav",
25215
+ txt: "text/plain",
25216
+ md: "text/markdown",
25217
+ csv: "text/csv",
25218
+ json: "application/json",
25219
+ html: "text/html",
25220
+ zip: "application/zip",
25221
+ gz: "application/gzip",
25222
+ tar: "application/x-tar"
25223
+ };
25224
+ OCTET_STREAM = "application/octet-stream";
25225
+ INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
25162
25226
  }
25163
- return value;
25164
- }
25165
- function encodeGithubPathSegments(path) {
25166
- return path.split("/").map(encodeURIComponent).join("/");
25227
+ });
25228
+
25229
+ // ../../src/artifacts/keys.ts
25230
+ function safeBasename(name) {
25231
+ const segments2 = name.split(/[\\/]+/).filter((s) => s.length > 0);
25232
+ const last = segments2[segments2.length - 1] ?? "";
25233
+ const reduced = last.replace(SAFE, "_").replace(/\.{2,}/g, "_").replace(/_+/g, "_");
25234
+ if (reduced === "" || /^[._-]+$/.test(reduced)) return "file";
25235
+ return reduced;
25167
25236
  }
25168
- function anchorUrl(ctx, a) {
25169
- return `https://github.com/${ctx.repo}/blob/${ctx.headSha}/${encodeGithubPathSegments(a.path)}#L${a.from}-L${a.to}`;
25237
+ function threadKeySafe(threadKey) {
25238
+ return threadKey.replace(/:/g, "-").replace(SAFE, "_");
25170
25239
  }
25171
- function cell2(s) {
25172
- return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
25240
+ function outboundKey(runId3, seq, name) {
25241
+ return `runs/${runId3}/out/${seq}-${safeBasename(name)}`;
25173
25242
  }
25174
- function renderPrDescriptionMarkdown(desc, ctx) {
25175
- if (!REPO_RE.test(ctx.repo))
25176
- throw new Error(`renderPrDescriptionMarkdown: repo must be owner/name, got "${ctx.repo}"`);
25177
- if (!SHA_RE2.test(ctx.headSha))
25178
- throw new Error(`renderPrDescriptionMarkdown: headSha must be a full 40-char lowercase sha, got "${ctx.headSha}"`);
25179
- const out = [];
25180
- out.push("## TL;DR", "", desc.tldr, "");
25181
- out.push("## What & why", "", desc.whatWhy, "");
25182
- out.push("## Tour", "");
25183
- desc.tour.forEach((step, i) => {
25184
- out.push(`### ${i + 1}. ${step.title}`, "", step.description, "");
25185
- if (step.lookFor) out.push(`**Look for:** ${step.lookFor}`, "");
25186
- out.push(anchorUrl(ctx, step.anchor), "");
25187
- });
25188
- out.push(`### ${desc.tour.length + 1}. Remaining changes`, "");
25189
- if (desc.remaining.length === 0) out.push("- none \u2014 every touched file is covered by a step above", "");
25190
- else {
25191
- for (const r of desc.remaining) out.push(`- \`${r.path}\` \u2014 ${r.note}`);
25192
- out.push("");
25193
- }
25194
- out.push("## Decisions", "");
25195
- for (const d of desc.decisions) out.push(`- **${d.title.replace(/\.?$/, ".")}** ${d.rationale}`);
25196
- out.push("");
25197
- out.push("## Risks & implications", "", desc.risks, "");
25198
- out.push("## Validation", "");
25199
- if (desc.validation.summary) out.push(desc.validation.summary, "");
25200
- out.push("| Criterion | Proof |", "|---|---|");
25201
- for (const c of desc.validation.criteria) out.push(`| ${cell2(c.criterion)} | ${cell2(c.proof)} |`);
25202
- out.push("", GENERATED_FOOTER, "");
25203
- return out.join("\n");
25243
+ function inboundKey(threadKey, messageTs, index, name) {
25244
+ return `threads/${threadKeySafe(threadKey)}/in/${messageTs}/${index}-${safeBasename(name)}`;
25204
25245
  }
25205
- function parsePrDescriptionMarkdown(body) {
25206
- const problems = [];
25207
- const lines = body.replace(/\r\n?/g, "\n").split("\n").filter((l) => l.trim() !== GENERATED_FOOTER);
25208
- const fenced = fenceMask(lines);
25209
- const sections = splitSections(lines, fenced, problems);
25210
- const description = { tour: [] };
25211
- const tldr = sections.get("tldr");
25212
- if (tldr) setProse(description, "tldr", tldr, problems);
25213
- else {
25214
- problems.push("no `## TL;DR` section \u2014 the tldr is the body's first paragraph");
25215
- const first = firstParagraph(lines, fenced);
25216
- if (first) description.tldr = first;
25217
- }
25218
- const whatWhy = sections.get("whatWhy");
25219
- if (whatWhy) setProse(description, "whatWhy", whatWhy, problems);
25220
- const tour = sections.get("tour");
25221
- if (tour) {
25222
- const parsed = parseTour(tour.lines, fenced.slice(tour.start, tour.start + tour.lines.length), problems);
25223
- description.tour = parsed.tour;
25224
- if (parsed.remaining) description.remaining = parsed.remaining;
25225
- }
25226
- const decisions = sections.get("decisions");
25227
- if (decisions) description.decisions = parseDecisions(decisions.lines, problems);
25228
- const risks = sections.get("risks");
25229
- if (risks) setProse(description, "risks", risks, problems);
25230
- const validation = sections.get("validation");
25231
- if (validation) description.validation = parseValidation(validation.lines, problems);
25232
- for (const [key, heading] of SECTIONS) {
25233
- if (key === "tldr" || sections.has(key)) continue;
25234
- problems.push(`no \`## ${heading}\` section`);
25246
+ var SAFE;
25247
+ var init_keys = __esm({
25248
+ "../../src/artifacts/keys.ts"() {
25249
+ "use strict";
25250
+ SAFE = /[^A-Za-z0-9._-]+/g;
25235
25251
  }
25236
- return { description, complete: problems.length === 0, problems };
25252
+ });
25253
+
25254
+ // ../../src/tools/attach.ts
25255
+ function transferBudgetMs(size) {
25256
+ return Math.ceil(size / TRANSFER_FLOOR_BYTES_PER_MS) + TRANSFER_SETUP_MS;
25237
25257
  }
25238
- function fenceMask(lines) {
25239
- let open2;
25240
- return lines.map((l) => {
25241
- const m = /^\s*(`{3,}|~{3,})/.exec(l);
25242
- if (m && (open2 === void 0 || m[1][0] === open2[0])) {
25243
- const wasOpen = open2 !== void 0;
25244
- open2 = wasOpen ? void 0 : m[1];
25245
- return true;
25246
- }
25247
- return open2 !== void 0;
25248
- });
25258
+ function fileNameOf2(path) {
25259
+ const segments2 = path.split("/").filter(Boolean);
25260
+ return segments2[segments2.length - 1] ?? path;
25249
25261
  }
25250
- function splitSections(lines, fenced, problems) {
25251
- const byHeading = new Map(SECTIONS.map(([key, heading]) => [heading.toLowerCase(), key]));
25252
- const out = /* @__PURE__ */ new Map();
25253
- let current;
25254
- lines.forEach((line3, i) => {
25255
- const m = fenced[i] ? null : HEADING_RE.exec(line3);
25256
- if (m && m[1] === "##") {
25257
- const key = byHeading.get(m[2].replace(/\s+/g, " ").toLowerCase());
25258
- if (key === void 0)
25259
- current = void 0;
25260
- else if (out.has(key)) {
25261
- problems.push(`duplicate \`## ${SECTIONS.find(([k]) => k === key)[1]}\` section \u2014 the first one stands`);
25262
- current = void 0;
25263
- } else {
25264
- current = { start: i + 1, lines: [] };
25265
- out.set(key, current);
25266
- }
25267
- return;
25268
- }
25269
- current?.lines.push(line3);
25270
- });
25271
- return out;
25262
+ function putCommandFor(path, contentType, url) {
25263
+ return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} -o /dev/null -w ${shellQuote(PUT_REPORT_FORMAT)} ${shellQuote(url)}`;
25272
25264
  }
25273
- function firstParagraph(lines, fenced) {
25274
- const para = [];
25275
- for (let i = 0; i < lines.length; i++) {
25276
- const l = lines[i];
25277
- if (l.trim() === "") {
25278
- if (para.length) break;
25279
- continue;
25280
- }
25281
- if (fenced[i] || para.length === 0 && HEADING_RE.test(l)) continue;
25282
- para.push(l.trim());
25283
- }
25284
- return para.length ? para.join("\n") : void 0;
25265
+ function parsePutReport(output) {
25266
+ const m = /(\d{3}) (\d{1,15})\s*$/.exec(output);
25267
+ return m ? { httpCode: Number(m[1]), sentBytes: Number(m[2]) } : null;
25285
25268
  }
25286
- function setProse(description, key, section, problems) {
25287
- const text = section.lines.join("\n").trim();
25288
- if (text === "") problems.push(`\`## ${SECTIONS.find(([k]) => k === key)[1]}\` section is empty`);
25289
- else description[key] = text;
25269
+ function postCommandFor(path, url) {
25270
+ return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
25290
25271
  }
25291
- function parseTour(lines, fenced, problems) {
25292
- const steps = [];
25293
- let preamble = "";
25294
- lines.forEach((line3, i) => {
25295
- const m = fenced[i] ? null : STEP_RE.exec(line3);
25296
- if (m) steps.push({ label: m[1] ?? String(steps.length + 1), title: m[2], lines: [] });
25297
- else if (steps.length) steps[steps.length - 1].lines.push(line3);
25298
- else preamble += line3;
25299
- });
25300
- if (preamble.trim() !== "") problems.push("Tour: text before the first `### N.` step is ignored");
25301
- const isRemaining = (title) => /^remaining changes$/i.test(title);
25302
- const tour = [];
25303
- let remaining;
25304
- for (const step of steps) {
25305
- if (isRemaining(step.title)) {
25306
- remaining = parseRemaining(step.lines, problems);
25307
- continue;
25308
- }
25309
- const parsed = parseStep(step, problems);
25310
- if (parsed) tour.push(parsed);
25272
+ async function attachThroughStore(path, name, lead, artifacts, ctx) {
25273
+ const exec = (command2, timeoutMs2) => {
25274
+ const opts = { timeoutMs: timeoutMs2, ...ctx.signal ? { signal: ctx.signal } : {} };
25275
+ return ctx.executor.exec(command2, opts);
25276
+ };
25277
+ const callId = ctx.callId;
25278
+ if (callId === void 0) return "error: attach_file was run without its call id \u2014 the file cannot be recorded";
25279
+ const statOut = await exec(statCommandFor(shellQuote(path)), STAT_TIMEOUT_MS);
25280
+ if (parseExitPrefix(statOut).failed) return `error: could not read ${path}: ${statOut.trim()}`;
25281
+ const size = parseByteSize(statOut);
25282
+ if (size === null) return `error: could not measure ${path}: ${statOut.trim()}`;
25283
+ if (size === 0) return `error: ${path} is empty \u2014 nothing to attach`;
25284
+ if (size > MAX_ARTIFACT_BYTES) {
25285
+ return `error: ${path} is ${size} bytes; attach_file takes files up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) \u2014 link to the file instead`;
25311
25286
  }
25312
- if (!steps.some((s) => !isRemaining(s.title))) problems.push("Tour has no steps");
25313
- if (remaining === void 0) problems.push("Tour has no `Remaining changes` step");
25314
- return { tour, ...remaining ? { remaining } : {} };
25315
- }
25316
- function parseStep(step, problems) {
25317
- const where = `step ${step.label} (${step.title})`;
25318
- let link;
25319
- const descLines = [];
25320
- const lookForLines = [];
25321
- let inLookFor = false;
25322
- for (const line3 of step.lines) {
25323
- const m = PERMALINK_RE.exec(line3.trim());
25324
- if (m) {
25325
- if (link) problems.push(`${where}: more than one permalink \u2014 the first is the anchor`);
25326
- else link = m;
25327
- inLookFor = false;
25328
- continue;
25329
- }
25330
- const lf = LOOK_FOR_RE.exec(line3.trim());
25331
- if (lf) {
25332
- lookForLines.push(lf[1]);
25333
- inLookFor = true;
25334
- continue;
25335
- }
25336
- if (inLookFor) {
25337
- if (line3.trim() === "") inLookFor = false;
25338
- else lookForLines.push(line3.trim());
25339
- continue;
25340
- }
25341
- descLines.push(line3);
25287
+ let timeoutMs = BASH_TIMEOUT_MAX_MS;
25288
+ if (ctx.remainingMs) {
25289
+ const budget = bashBudgetWithinRun(BASH_TIMEOUT_MAX_MS, ctx.remainingMs());
25290
+ if (budget.kind === "exhausted") return `error: ${budget.note}`;
25291
+ if (budget.kind === "clipped") timeoutMs = budget.timeoutMs;
25342
25292
  }
25343
- if (!link) {
25344
- problems.push(`${where}: no permalink line \u2014 dropped`);
25345
- return void 0;
25293
+ const needed = transferBudgetMs(size);
25294
+ if (needed > timeoutMs) {
25295
+ return `error: ${name} is ${size} bytes and needs about ${secs(needed)}s to move at ${TRANSFER_FLOOR_BYTES_PER_MS} KB/s; the run has ${secs(timeoutMs)}s of command budget left \u2014 write up what you have and link to the file instead`;
25346
25296
  }
25347
- let path;
25297
+ const contentType = contentTypeFor(name);
25298
+ const key = outboundKey(artifacts.runId, artifacts.nextSeq(), name);
25299
+ const putUrl = await artifacts.store.presignPut(key, contentType);
25300
+ const putOut = await exec(putCommandFor(path, contentType, putUrl), timeoutMs);
25301
+ if (parseExitPrefix(putOut).failed) {
25302
+ return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
25303
+ }
25304
+ const report2 = parsePutReport(putOut);
25305
+ const curlSaid = report2 ? `; curl reported HTTP ${report2.httpCode} after sending ${report2.sentBytes} bytes` : "";
25306
+ const head = await artifacts.store.head(key);
25307
+ if (!head) {
25308
+ return `error: the artifact store holds nothing under ${key} after the upload of ${name}${curlSaid} \u2014 nothing was posted`;
25309
+ }
25310
+ if (head.size !== size) {
25311
+ return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
25312
+ }
25313
+ ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType, callId });
25314
+ if (!ctx.uploadTicket) {
25315
+ const link = artifacts.artifactUrl?.(key);
25316
+ const where = link ? ` \u2014 ${link}` : ` is on the run page as ${key}`;
25317
+ await artifacts.reply(`${lead}
25318
+ \u{1F4CE} ${name} (${size} bytes)${where}`);
25319
+ return `attached ${name} (${size} bytes) to the run page as ${key}; this conversation's channel takes no file uploads, so the lead and the file's link were posted instead`;
25320
+ }
25321
+ const kept = "the file is kept on the run page";
25322
+ let ticket;
25348
25323
  try {
25349
- path = link[2].split("/").map(decodeURIComponent).join("/");
25350
- } catch {
25351
- problems.push(`${where}: permalink path is not valid percent-encoding \u2014 dropped`);
25352
- return void 0;
25324
+ ticket = await ctx.uploadTicket({ name, size });
25325
+ } catch (err2) {
25326
+ return `error: the channel refused an upload ticket for ${name}: ${describe2(err2)}; ${kept}`;
25353
25327
  }
25354
- const from = Number(link[3]);
25355
- const anchor = TourAnchorSchema.safeParse({ path, from, to: link[4] === void 0 ? from : Number(link[4]) });
25356
- if (!anchor.success) {
25357
- const issue = anchor.error.issues[0];
25358
- problems.push(`${where}: ${["anchor", ...issue.path].join(".")} ${issue.message} \u2014 dropped`);
25359
- return void 0;
25328
+ const postOut = await exec(postCommandFor(path, ticket.url), timeoutMs);
25329
+ if (parseExitPrefix(postOut).failed) {
25330
+ return `error: the upload of ${name} to the channel failed: ${postOut.trim()}; ${kept}`;
25360
25331
  }
25361
- const lookFor = lookForLines.join("\n").trim();
25362
- return {
25363
- title: step.title,
25364
- // A parsed step may carry an empty description (a human's heading +
25365
- // permalink): this is a read-back, not a submission the schema gates.
25366
- description: descLines.join("\n").replace(/\n{3,}/g, "\n\n").trim(),
25367
- ...lookFor ? { lookFor } : {},
25368
- anchor: { ...anchor.data, sha: link[1] }
25369
- };
25370
- }
25371
- function parseRemaining(lines, problems) {
25372
- const out = [];
25373
- for (const raw of lines) {
25374
- const line3 = raw.trim();
25375
- if (line3 === "" || REMAINING_NONE_RE.test(line3)) continue;
25376
- const m = REMAINING_LINE_RE.exec(line3);
25377
- if (m) out.push({ path: m[1], note: m[2].trim() });
25378
- else if (!line3.startsWith("-") && out.length) out[out.length - 1].note += `
25379
- ${line3}`;
25380
- else problems.push(`Remaining changes: unrecognized line \`${line3}\``);
25332
+ try {
25333
+ await ticket.complete(lead);
25334
+ } catch (err2) {
25335
+ return `error: the channel refused to complete the upload of ${name}: ${describe2(err2)}; ${kept}`;
25381
25336
  }
25382
- return out;
25337
+ return `attached ${name} (${size} bytes) to the conversation and the run page`;
25383
25338
  }
25384
- function parseDecisions(lines, problems) {
25385
- const out = [];
25386
- for (const raw of lines) {
25387
- const line3 = raw.trim();
25388
- if (line3 === "") continue;
25389
- const m = DECISION_RE.exec(line3);
25390
- if (m) out.push({ title: m[1].replace(/\.$/, ""), rationale: m[2].trim() });
25391
- else if (!line3.startsWith("-") && out.length) out[out.length - 1].rationale += `
25392
- ${line3}`;
25393
- else problems.push(`Decisions: unrecognized line \`${line3}\``);
25339
+ var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, PUT_REPORT_FORMAT, attachFileTool;
25340
+ var init_attach = __esm({
25341
+ "../../src/tools/attach.ts"() {
25342
+ "use strict";
25343
+ init_contentType();
25344
+ init_keys();
25345
+ init_runEvents();
25346
+ init_bashTimeout();
25347
+ init_binaryRead();
25348
+ init_shellQuote();
25349
+ MAX_ARTIFACT_BYTES = 1073741824;
25350
+ TRANSFER_FLOOR_BYTES_PER_MS = 1024;
25351
+ TRANSFER_SETUP_MS = 1e4;
25352
+ STAT_TIMEOUT_MS = 3e4;
25353
+ describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
25354
+ secs = (ms) => Math.max(0, Math.round(ms / 1e3));
25355
+ PUT_REPORT_FORMAT = "\\n%{http_code} %{size_upload}";
25356
+ attachFileTool = {
25357
+ name: "attach_file",
25358
+ failsInText: true,
25359
+ description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
25360
+ inputSchema: {
25361
+ type: "object",
25362
+ properties: {
25363
+ path: { type: "string", description: "Relative path of the file in the workspace" },
25364
+ comment: {
25365
+ type: "string",
25366
+ description: "One line posted with the file saying what it shows (default: the file name)"
25367
+ }
25368
+ },
25369
+ required: ["path"]
25370
+ },
25371
+ async run(input, ctx) {
25372
+ const path = String(input.path ?? "").trim();
25373
+ if (!path) return "error: path is required";
25374
+ const name = fileNameOf2(path);
25375
+ const lead = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : name;
25376
+ if (ctx.artifacts) return attachThroughStore(path, name, lead, ctx.artifacts, ctx);
25377
+ if (!ctx.attach) {
25378
+ return "attach_file is not available here: this conversation's channel takes no file uploads \u2014 link to the file instead";
25379
+ }
25380
+ const readBytes = ctx.executor.readBytes?.bind(ctx.executor);
25381
+ if (!readBytes) {
25382
+ return "attach_file is not available here: this workspace cannot hand files over \u2014 link to the file instead";
25383
+ }
25384
+ let bytes;
25385
+ try {
25386
+ bytes = await readBytes(path);
25387
+ } catch (err2) {
25388
+ return `error: could not read ${path}: ${describe2(err2)}`;
25389
+ }
25390
+ if (bytes.byteLength === 0) return `error: ${path} is empty \u2014 nothing to attach`;
25391
+ try {
25392
+ await ctx.attach({ name, bytes, lead });
25393
+ } catch (err2) {
25394
+ return `error: the channel refused the upload of ${name}: ${describe2(err2)}`;
25395
+ }
25396
+ return `attached ${name} (${bytes.byteLength} bytes) to the conversation`;
25397
+ }
25398
+ };
25394
25399
  }
25395
- return out;
25400
+ });
25401
+
25402
+ // ../../src/core/diffDigest.ts
25403
+ function basename2(p) {
25404
+ const i = p.lastIndexOf("/");
25405
+ return i === -1 ? p : p.slice(i + 1);
25396
25406
  }
25397
- function parseValidation(lines, problems) {
25398
- const summary = [];
25399
- const criteria = [];
25400
- let header4 = false;
25401
- for (const raw of lines) {
25402
- const line3 = raw.trim();
25403
- if (!header4) {
25404
- if (TABLE_HEADER_RE.test(line3)) header4 = true;
25405
- else summary.push(raw);
25407
+ function decodeGitQuoted(s) {
25408
+ const bytes = [];
25409
+ for (let i = 0; i < s.length; i++) {
25410
+ if (s[i] === "\\" && i + 1 < s.length) {
25411
+ const oct = s.slice(i + 1).match(/^[0-7]{1,3}/)?.[0];
25412
+ if (oct) {
25413
+ bytes.push(parseInt(oct, 8) & 255);
25414
+ i += oct.length;
25415
+ continue;
25416
+ }
25417
+ const simple = { t: 9, n: 10, r: 13, '"': 34, "\\": 92 };
25418
+ const nx = s[i + 1];
25419
+ bytes.push(nx in simple ? simple[nx] : nx.charCodeAt(0) & 255);
25420
+ i += 1;
25406
25421
  continue;
25407
25422
  }
25408
- if (line3 === "" || TABLE_RULE_RE.test(line3)) continue;
25409
- const cells = splitCells(line3);
25410
- if (cells.length >= 2) criteria.push({ criterion: cells[0], proof: cells[1] });
25411
- else problems.push(`Validation: unrecognized table line \`${line3}\``);
25423
+ bytes.push(s.charCodeAt(i) & 255);
25412
25424
  }
25413
- if (!header4) problems.push("Validation: no `| Criterion | Proof |` table");
25414
- const text = summary.join("\n").trim();
25415
- return { ...text ? { summary: text } : {}, criteria };
25416
- }
25417
- function splitCells(row) {
25418
- const cells = [];
25419
- let cur = "";
25420
- const inner = row.replace(/^\|/, "").replace(/\|$/, "");
25421
- for (let i = 0; i < inner.length; i++) {
25422
- const ch = inner[i];
25423
- if (ch === "\\" && i + 1 < inner.length) {
25424
- cur += inner[++i];
25425
- } else if (ch === "|") {
25426
- cells.push(cur.trim());
25427
- cur = "";
25428
- } else cur += ch;
25429
- }
25430
- cells.push(cur.trim());
25431
- return cells;
25432
- }
25433
- var line2, prose, TourAnchorSchema, TourStepSchema, PrDescriptionSchema, REPO_RE, SHA_RE2, GENERATED_FOOTER, SECTIONS, PERMALINK_RE, HEADING_RE, STEP_RE, LOOK_FOR_RE, REMAINING_LINE_RE, REMAINING_NONE_RE, DECISION_RE, TABLE_HEADER_RE, TABLE_RULE_RE;
25434
- var init_prDescription = __esm({
25435
- "../../src/core/prDescription.ts"() {
25436
- "use strict";
25437
- init_redact();
25438
- line2 = z20.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
25439
- prose = z20.string().trim().min(1);
25440
- TourAnchorSchema = z20.object({
25441
- // Repo-relative: no leading `/`, no `..` SEGMENT (a filename like `a..b.ts` is fine).
25442
- path: z20.string().trim().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), "path must be repo-relative"),
25443
- from: z20.number().int().positive(),
25444
- to: z20.number().int().positive()
25445
- }).refine((a) => a.to >= a.from, "to must be >= from");
25446
- TourStepSchema = z20.object({
25447
- title: line2,
25448
- description: prose,
25449
- lookFor: prose.optional(),
25450
- anchor: TourAnchorSchema
25451
- });
25452
- PrDescriptionSchema = z20.object({
25453
- /** The PR title's single source. Metadata for the PR's own title field —
25454
- * never rendered into the body (GitHub shows the title itself). */
25455
- title: line2,
25456
- tldr: prose,
25457
- whatWhy: prose,
25458
- tour: z20.array(TourStepSchema).min(1),
25459
- /** Every touched file the Tour steps did not cover, one line each. */
25460
- remaining: z20.array(z20.object({ path: line2, note: prose })),
25461
- decisions: z20.array(z20.object({ title: line2, rationale: prose })).min(1),
25462
- risks: prose,
25463
- validation: z20.object({
25464
- summary: prose.optional(),
25465
- criteria: z20.array(z20.object({ criterion: prose, proof: prose })).min(1)
25466
- })
25467
- });
25468
- REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
25469
- SHA_RE2 = /^[0-9a-f]{40}$/;
25470
- GENERATED_FOOTER = "\u{1F916} Generated with [Claude Code](https://claude.com/claude-code)";
25471
- SECTIONS = [
25472
- ["tldr", "TL;DR"],
25473
- ["whatWhy", "What & why"],
25474
- ["tour", "Tour"],
25475
- ["decisions", "Decisions"],
25476
- ["risks", "Risks & implications"],
25477
- ["validation", "Validation"]
25478
- ];
25479
- PERMALINK_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/blob\/([0-9a-f]{7,40})\/([^#\s]+)#L(\d+)(?:-L(\d+))?$/;
25480
- HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
25481
- STEP_RE = /^###\s+(?:(\d+)\.\s+)?(.+?)\s*$/;
25482
- LOOK_FOR_RE = /^\*\*Look for:\*\*\s*(.*)$/;
25483
- REMAINING_LINE_RE = /^-\s+`([^`]+)`\s+—\s*(.*)$/;
25484
- REMAINING_NONE_RE = /^-\s+none\b/i;
25485
- DECISION_RE = /^-\s+\*\*(.+?)\*\*\s*(.*)$/;
25486
- TABLE_HEADER_RE = /^\|\s*criterion\s*\|\s*proof\s*\|$/i;
25487
- TABLE_RULE_RE = /^\|?\s*:?-{3,}:?\s*\|/;
25488
- }
25489
- });
25490
-
25491
- // ../../src/core/diffDigest.ts
25492
- function basename2(p) {
25493
- const i = p.lastIndexOf("/");
25494
- return i === -1 ? p : p.slice(i + 1);
25495
- }
25496
- function decodeGitQuoted(s) {
25497
- const bytes = [];
25498
- for (let i = 0; i < s.length; i++) {
25499
- if (s[i] === "\\" && i + 1 < s.length) {
25500
- const oct = s.slice(i + 1).match(/^[0-7]{1,3}/)?.[0];
25501
- if (oct) {
25502
- bytes.push(parseInt(oct, 8) & 255);
25503
- i += oct.length;
25504
- continue;
25505
- }
25506
- const simple = { t: 9, n: 10, r: 13, '"': 34, "\\": 92 };
25507
- const nx = s[i + 1];
25508
- bytes.push(nx in simple ? simple[nx] : nx.charCodeAt(0) & 255);
25509
- i += 1;
25510
- continue;
25511
- }
25512
- bytes.push(s.charCodeAt(i) & 255);
25513
- }
25514
- return Buffer.from(bytes).toString("utf8");
25425
+ return Buffer.from(bytes).toString("utf8");
25515
25426
  }
25516
25427
  function unquotePath(raw) {
25517
25428
  const s = raw.trim();
@@ -25646,10 +25557,71 @@ var init_diffDigest = __esm({
25646
25557
  }
25647
25558
  });
25648
25559
 
25560
+ // ../../src/tools/diffDigest.ts
25561
+ var NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool;
25562
+ var init_diffDigest2 = __esm({
25563
+ "../../src/tools/diffDigest.ts"() {
25564
+ "use strict";
25565
+ init_diffDigest();
25566
+ init_shellQuote();
25567
+ NAME_STATUS_MARK = "@@diff_digest:name-status@@";
25568
+ NAME_STATUS_MARK_LINE = /^@@diff_digest:name-status@@$/m;
25569
+ OUTPUT_CUT_RE = /^\.\.\.\[truncated \d+ chars\]$/m;
25570
+ diffDigestTool = {
25571
+ sideEffectFree: true,
25572
+ name: "diff_digest",
25573
+ description: "Summarize the current branch's whole change against a base ref as a compact digest: per-file +adds/-dels, totals, and risky-file flags (migrations/schema, auth/permission, whole-file deletions, lockfiles, very large files). This is a DISTILLED summary, not the raw diff \u2014 use it to shape a PR description, or to orient before a review. Runs `git diff --numstat` and `--name-status` over the merge-base range `<base>...HEAD` in the workspace, so it covers every file however large the change; base defaults to the repo's default branch (origin/HEAD). The totals it states are the whole change \u2014 a diff you read that shows fewer files or lines was cut short.",
25574
+ inputSchema: {
25575
+ type: "object",
25576
+ properties: {
25577
+ base: {
25578
+ type: "string",
25579
+ description: "Base ref to diff against (e.g. 'main' or a SHA). Defaults to the repo's default branch via origin/HEAD."
25580
+ }
25581
+ }
25582
+ },
25583
+ async run(input, ctx) {
25584
+ const base = String(input.base ?? "").trim();
25585
+ const baseName = base || "origin/HEAD";
25586
+ if (base.startsWith("-")) {
25587
+ return "diff_digest: base ref may not start with '-' (rejected to prevent git option injection).";
25588
+ }
25589
+ const baseExpr = base ? shellQuote(base) : '"$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo origin/main)"';
25590
+ const range = `--end-of-options ${baseExpr}...HEAD`;
25591
+ const listing = `git diff --numstat ${range} && echo ${NAME_STATUS_MARK} && git diff --name-status ${range}`;
25592
+ let raw = await ctx.executor.exec(listing);
25593
+ const incomplete = () => {
25594
+ const reason = "the file listing exceeded the executor's output cap";
25595
+ ctx.onDigest?.({ complete: false, base: baseName, reason });
25596
+ return `diff_digest: ${reason} \u2014 the digest cannot state totals for this change (base \`${baseName}\`). Read the diff file by file (\`git diff --name-only ${baseName}...HEAD\`).`;
25597
+ };
25598
+ if (OUTPUT_CUT_RE.test(raw)) return incomplete();
25599
+ let marker = NAME_STATUS_MARK_LINE.exec(raw);
25600
+ if (!marker) {
25601
+ const shallow = (await ctx.executor.exec("git rev-parse --is-shallow-repository")).trim() === "true";
25602
+ if (shallow) {
25603
+ await ctx.executor.exec('git fetch --unshallow --quiet origin "+refs/heads/*:refs/remotes/origin/*"');
25604
+ raw = await ctx.executor.exec(listing);
25605
+ if (OUTPUT_CUT_RE.test(raw)) return incomplete();
25606
+ marker = NAME_STATUS_MARK_LINE.exec(raw);
25607
+ }
25608
+ }
25609
+ if (!marker) {
25610
+ return `diff_digest: could not compute the diff (base \`${baseName}\`).
25611
+ ${raw.trim()}`;
25612
+ }
25613
+ const digest = distillDiffStats(raw.slice(0, marker.index), raw.slice(marker.index + marker[0].length));
25614
+ ctx.onDigest?.({ complete: true, base: baseName, totals: digest.totals });
25615
+ return digest.text;
25616
+ }
25617
+ };
25618
+ }
25619
+ });
25620
+
25649
25621
  // ../../src/tools/github.ts
25650
25622
  function repoOf(input) {
25651
25623
  const raw = String(input.repo ?? "").trim().replace(/\.git$/i, "");
25652
- if (!REPO_RE2.test(raw))
25624
+ if (!REPO_RE.test(raw))
25653
25625
  return {
25654
25626
  error: `repo must be an owner/name slug (got ${JSON.stringify(raw)}); github_repos lists the repos you can reach.`
25655
25627
  };
@@ -25697,12 +25669,12 @@ function writeGate(tool, ctx, repo) {
25697
25669
  return `${tool}: you are not allowed to write to ${repo} (it is restricted and you hold no grant for it) \u2014 say so to the user instead of retrying.`;
25698
25670
  return void 0;
25699
25671
  }
25700
- var REPO_RE2, UNAVAILABLE, MAX_TREE_ENTRIES, MAX_ISSUE_BODY_SHOWN, githubReposTool, githubFileTool, githubTreeTool, githubSearchCodeTool, githubIssueListTool, githubIssueGetTool, githubIssueCreateTool, githubIssueUpdateTool, githubIssueCommentTool, githubIssueDeleteTool, GITHUB_READ_TOOLS, GITHUB_ISSUE_WRITE_TOOLS;
25672
+ var REPO_RE, UNAVAILABLE, MAX_TREE_ENTRIES, MAX_ISSUE_BODY_SHOWN, githubReposTool, githubFileTool, githubTreeTool, githubSearchCodeTool, githubIssueListTool, githubIssueGetTool, githubIssueCreateTool, githubIssueUpdateTool, githubIssueCommentTool, githubIssueDeleteTool, GITHUB_READ_TOOLS, GITHUB_ISSUE_WRITE_TOOLS;
25701
25673
  var init_github = __esm({
25702
25674
  "../../src/tools/github.ts"() {
25703
25675
  "use strict";
25704
25676
  init_githubApi();
25705
- REPO_RE2 = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
25677
+ REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
25706
25678
  UNAVAILABLE = "GitHub tools are not available in this context.";
25707
25679
  MAX_TREE_ENTRIES = 300;
25708
25680
  MAX_ISSUE_BODY_SHOWN = 6e3;
@@ -26059,321 +26031,52 @@ ${url}`;
26059
26031
  }
26060
26032
  });
26061
26033
 
26062
- // ../../src/tools/skills.ts
26063
- var listSkillsTool, useSkillTool;
26064
- var init_skills = __esm({
26065
- "../../src/tools/skills.ts"() {
26066
- "use strict";
26067
- listSkillsTool = {
26068
- sideEffectFree: true,
26069
- name: "list_skills",
26070
- description: "List the skills available to you: each is a reusable methodology you can load into context with use_skill. Returns each skill's name and a one-line description. The same list is also in your system prompt.",
26071
- inputSchema: {
26072
- type: "object",
26073
- properties: {}
26074
- },
26075
- async run(_input, ctx) {
26076
- if (!ctx.skills) return "Skills are not available in this context.";
26077
- const metas = ctx.skills.list(ctx.agentName ?? "");
26078
- if (metas.length === 0) return "No skills are available to you.";
26079
- return metas.map((m) => `- ${m.name}: ${m.description}`).join("\n");
26080
- }
26034
+ // ../../src/core/dispatch/awaitChildren.ts
26035
+ function decideWait(inputs) {
26036
+ const { children, now, budgetEndsAt, timeoutAt, stop, followUpPending } = inputs;
26037
+ if ([...children.values()].every(isTerminal)) return { kind: "end", why: "all_ended" };
26038
+ if (stop !== void 0) return { kind: "end", why: "stop" };
26039
+ if (followUpPending) return { kind: "end", why: "follow_up" };
26040
+ if (now >= budgetEndsAt) return { kind: "end", why: "budget" };
26041
+ if (timeoutAt !== void 0 && now >= timeoutAt) return { kind: "end", why: "timeout" };
26042
+ return { kind: "wait", until: Math.min(budgetEndsAt, timeoutAt ?? Number.POSITIVE_INFINITY) };
26043
+ }
26044
+ function budgetEndOf(now, remainingMs) {
26045
+ return now + remainingMs - RUN_DEADLINE_RESERVE_MS;
26046
+ }
26047
+ function sleepUnlessAborted(ms, signal2) {
26048
+ return new Promise((resolve12) => {
26049
+ if (signal2?.aborted) return resolve12();
26050
+ const done = () => {
26051
+ clearTimeout(timer);
26052
+ signal2?.removeEventListener("abort", done);
26053
+ resolve12();
26081
26054
  };
26082
- useSkillTool = {
26083
- sideEffectFree: true,
26084
- name: "use_skill",
26085
- description: "Load a skill's full instructions into your context by name (get names from list_skills or your system prompt). Returns the skill body \u2014 read it and follow it for the work at hand.",
26086
- inputSchema: {
26087
- type: "object",
26088
- properties: {
26089
- name: { type: "string", description: "The skill name to load (e.g. code-review-and-quality)" }
26090
- },
26091
- required: ["name"]
26092
- },
26093
- async run(input, ctx) {
26094
- if (!ctx.skills) return "Skills are not available in this context.";
26095
- const name = String(input.name ?? "").trim();
26096
- if (!name) return "use_skill: provide a skill `name` (see list_skills).";
26097
- const skill = ctx.skills.get(name);
26098
- const inScope = skill && skill.agents.includes(ctx.agentName ?? "");
26099
- if (!skill || !inScope) {
26100
- const available = ctx.skills.list(ctx.agentName ?? "").map((m) => m.name);
26101
- return `use_skill: no skill named "${name}" is available to you.` + (available.length > 0 ? ` Available: ${available.join(", ")}.` : "");
26055
+ const timer = setTimeout(done, ms);
26056
+ signal2?.addEventListener("abort", done, { once: true });
26057
+ });
26058
+ }
26059
+ function waitCapabilityFor(deps) {
26060
+ return {
26061
+ stopRequested: () => deps.control.requested,
26062
+ followUpsPending: () => deps.inbox.size,
26063
+ watch: (ids, onChange) => {
26064
+ let replaying = true;
26065
+ const unsubscribe = deps.registry.subscribeIndex((event) => {
26066
+ if (event.type === "removed") {
26067
+ if (ids.has(event.id)) onChange();
26068
+ return;
26102
26069
  }
26103
- ctx.publish?.({
26104
- type: "skill_use",
26105
- skill: skill.name,
26106
- description: skill.description,
26107
- agent: ctx.agentName ?? "",
26108
- ...skill.source ? { source: skill.source } : {},
26109
- ...skill.upstream ? { upstream: { repo: skill.upstream.repo, commit: skill.upstream.commit } } : {},
26110
- bodyBytes: Buffer.byteLength(skill.body, "utf8")
26111
- });
26112
- const footer = skill.source ? `
26113
-
26114
- _(Skill source: ${skill.source})_` : "";
26115
- return `# Skill: ${skill.name}
26116
-
26117
- ${skill.body}${footer}`;
26118
- }
26119
- };
26120
- }
26121
- });
26122
-
26123
- // ../../src/artifacts/contentType.ts
26124
- function contentTypeFor(name) {
26125
- const dot = name.lastIndexOf(".");
26126
- if (dot < 0 || dot === name.length - 1) return OCTET_STREAM;
26127
- return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? OCTET_STREAM;
26128
- }
26129
- var BY_EXTENSION, OCTET_STREAM, INLINE_IMAGE_TYPES;
26130
- var init_contentType = __esm({
26131
- "../../src/artifacts/contentType.ts"() {
26132
- "use strict";
26133
- BY_EXTENSION = {
26134
- png: "image/png",
26135
- jpg: "image/jpeg",
26136
- jpeg: "image/jpeg",
26137
- gif: "image/gif",
26138
- webp: "image/webp",
26139
- svg: "image/svg+xml",
26140
- pdf: "application/pdf",
26141
- mp4: "video/mp4",
26142
- webm: "video/webm",
26143
- mov: "video/quicktime",
26144
- mp3: "audio/mpeg",
26145
- wav: "audio/wav",
26146
- txt: "text/plain",
26147
- md: "text/markdown",
26148
- csv: "text/csv",
26149
- json: "application/json",
26150
- html: "text/html",
26151
- zip: "application/zip",
26152
- gz: "application/gzip",
26153
- tar: "application/x-tar"
26154
- };
26155
- OCTET_STREAM = "application/octet-stream";
26156
- INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
26157
- }
26158
- });
26159
-
26160
- // ../../src/artifacts/keys.ts
26161
- function safeBasename(name) {
26162
- const segments2 = name.split(/[\\/]+/).filter((s) => s.length > 0);
26163
- const last = segments2[segments2.length - 1] ?? "";
26164
- const reduced = last.replace(SAFE, "_").replace(/\.{2,}/g, "_").replace(/_+/g, "_");
26165
- if (reduced === "" || /^[._-]+$/.test(reduced)) return "file";
26166
- return reduced;
26167
- }
26168
- function threadKeySafe(threadKey) {
26169
- return threadKey.replace(/:/g, "-").replace(SAFE, "_");
26170
- }
26171
- function outboundKey(runId3, seq, name) {
26172
- return `runs/${runId3}/out/${seq}-${safeBasename(name)}`;
26173
- }
26174
- function inboundKey(threadKey, messageTs, index, name) {
26175
- return `threads/${threadKeySafe(threadKey)}/in/${messageTs}/${index}-${safeBasename(name)}`;
26176
- }
26177
- var SAFE;
26178
- var init_keys = __esm({
26179
- "../../src/artifacts/keys.ts"() {
26180
- "use strict";
26181
- SAFE = /[^A-Za-z0-9._-]+/g;
26182
- }
26183
- });
26184
-
26185
- // ../../src/tools/attach.ts
26186
- function transferBudgetMs(size) {
26187
- return Math.ceil(size / TRANSFER_FLOOR_BYTES_PER_MS) + TRANSFER_SETUP_MS;
26188
- }
26189
- function fileNameOf2(path) {
26190
- const segments2 = path.split("/").filter(Boolean);
26191
- return segments2[segments2.length - 1] ?? path;
26192
- }
26193
- function putCommandFor(path, contentType, url) {
26194
- return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} -o /dev/null -w ${shellQuote(PUT_REPORT_FORMAT)} ${shellQuote(url)}`;
26195
- }
26196
- function parsePutReport(output) {
26197
- const m = /(\d{3}) (\d{1,15})\s*$/.exec(output);
26198
- return m ? { httpCode: Number(m[1]), sentBytes: Number(m[2]) } : null;
26199
- }
26200
- function postCommandFor(path, url) {
26201
- return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
26202
- }
26203
- async function attachThroughStore(path, name, lead, artifacts, ctx) {
26204
- const exec = (command2, timeoutMs2) => {
26205
- const opts = { timeoutMs: timeoutMs2, ...ctx.signal ? { signal: ctx.signal } : {} };
26206
- return ctx.executor.exec(command2, opts);
26207
- };
26208
- const statOut = await exec(statCommandFor(shellQuote(path)), STAT_TIMEOUT_MS);
26209
- if (parseExitPrefix(statOut).failed) return `error: could not read ${path}: ${statOut.trim()}`;
26210
- const size = parseByteSize(statOut);
26211
- if (size === null) return `error: could not measure ${path}: ${statOut.trim()}`;
26212
- if (size === 0) return `error: ${path} is empty \u2014 nothing to attach`;
26213
- if (size > MAX_ARTIFACT_BYTES) {
26214
- return `error: ${path} is ${size} bytes; attach_file takes files up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) \u2014 link to the file instead`;
26215
- }
26216
- let timeoutMs = BASH_TIMEOUT_MAX_MS;
26217
- if (ctx.remainingMs) {
26218
- const budget = bashBudgetWithinRun(BASH_TIMEOUT_MAX_MS, ctx.remainingMs());
26219
- if (budget.kind === "exhausted") return `error: ${budget.note}`;
26220
- if (budget.kind === "clipped") timeoutMs = budget.timeoutMs;
26221
- }
26222
- const needed = transferBudgetMs(size);
26223
- if (needed > timeoutMs) {
26224
- return `error: ${name} is ${size} bytes and needs about ${secs(needed)}s to move at ${TRANSFER_FLOOR_BYTES_PER_MS} KB/s; the run has ${secs(timeoutMs)}s of command budget left \u2014 write up what you have and link to the file instead`;
26225
- }
26226
- const contentType = contentTypeFor(name);
26227
- const key = outboundKey(artifacts.runId, artifacts.nextSeq(), name);
26228
- const putUrl = await artifacts.store.presignPut(key, contentType);
26229
- const putOut = await exec(putCommandFor(path, contentType, putUrl), timeoutMs);
26230
- if (parseExitPrefix(putOut).failed) {
26231
- return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
26232
- }
26233
- const report2 = parsePutReport(putOut);
26234
- const curlSaid = report2 ? `; curl reported HTTP ${report2.httpCode} after sending ${report2.sentBytes} bytes` : "";
26235
- const head = await artifacts.store.head(key);
26236
- if (!head) {
26237
- return `error: the artifact store holds nothing under ${key} after the upload of ${name}${curlSaid} \u2014 nothing was posted`;
26238
- }
26239
- if (head.size !== size) {
26240
- return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
26241
- }
26242
- ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType });
26243
- if (!ctx.uploadTicket) {
26244
- const link = artifacts.artifactUrl?.(key);
26245
- const where = link ? ` \u2014 ${link}` : ` is on the run page as ${key}`;
26246
- await artifacts.reply(`${lead}
26247
- \u{1F4CE} ${name} (${size} bytes)${where}`);
26248
- return `attached ${name} (${size} bytes) to the run page as ${key}; this conversation's channel takes no file uploads, so the lead and the file's link were posted instead`;
26249
- }
26250
- const kept = "the file is kept on the run page";
26251
- let ticket;
26252
- try {
26253
- ticket = await ctx.uploadTicket({ name, size });
26254
- } catch (err2) {
26255
- return `error: the channel refused an upload ticket for ${name}: ${describe2(err2)}; ${kept}`;
26256
- }
26257
- const postOut = await exec(postCommandFor(path, ticket.url), timeoutMs);
26258
- if (parseExitPrefix(postOut).failed) {
26259
- return `error: the upload of ${name} to the channel failed: ${postOut.trim()}; ${kept}`;
26260
- }
26261
- try {
26262
- await ticket.complete(lead);
26263
- } catch (err2) {
26264
- return `error: the channel refused to complete the upload of ${name}: ${describe2(err2)}; ${kept}`;
26265
- }
26266
- return `attached ${name} (${size} bytes) to the conversation and the run page`;
26267
- }
26268
- var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, PUT_REPORT_FORMAT, attachFileTool;
26269
- var init_attach = __esm({
26270
- "../../src/tools/attach.ts"() {
26271
- "use strict";
26272
- init_contentType();
26273
- init_keys();
26274
- init_runEvents();
26275
- init_bashTimeout();
26276
- init_binaryRead();
26277
- init_shellQuote();
26278
- MAX_ARTIFACT_BYTES = 1073741824;
26279
- TRANSFER_FLOOR_BYTES_PER_MS = 1024;
26280
- TRANSFER_SETUP_MS = 1e4;
26281
- STAT_TIMEOUT_MS = 3e4;
26282
- describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
26283
- secs = (ms) => Math.max(0, Math.round(ms / 1e3));
26284
- PUT_REPORT_FORMAT = "\\n%{http_code} %{size_upload}";
26285
- attachFileTool = {
26286
- name: "attach_file",
26287
- failsInText: true,
26288
- description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
26289
- inputSchema: {
26290
- type: "object",
26291
- properties: {
26292
- path: { type: "string", description: "Relative path of the file in the workspace" },
26293
- comment: {
26294
- type: "string",
26295
- description: "One line posted with the file saying what it shows (default: the file name)"
26296
- }
26297
- },
26298
- required: ["path"]
26299
- },
26300
- async run(input, ctx) {
26301
- const path = String(input.path ?? "").trim();
26302
- if (!path) return "error: path is required";
26303
- const name = fileNameOf2(path);
26304
- const lead = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : name;
26305
- if (ctx.artifacts) return attachThroughStore(path, name, lead, ctx.artifacts, ctx);
26306
- if (!ctx.attach) {
26307
- return "attach_file is not available here: this conversation's channel takes no file uploads \u2014 link to the file instead";
26308
- }
26309
- const readBytes = ctx.executor.readBytes?.bind(ctx.executor);
26310
- if (!readBytes) {
26311
- return "attach_file is not available here: this workspace cannot hand files over \u2014 link to the file instead";
26312
- }
26313
- let bytes;
26314
- try {
26315
- bytes = await readBytes(path);
26316
- } catch (err2) {
26317
- return `error: could not read ${path}: ${describe2(err2)}`;
26318
- }
26319
- if (bytes.byteLength === 0) return `error: ${path} is empty \u2014 nothing to attach`;
26320
- try {
26321
- await ctx.attach({ name, bytes, lead });
26322
- } catch (err2) {
26323
- return `error: the channel refused the upload of ${name}: ${describe2(err2)}`;
26324
- }
26325
- return `attached ${name} (${bytes.byteLength} bytes) to the conversation`;
26326
- }
26327
- };
26328
- }
26329
- });
26330
-
26331
- // ../../src/core/dispatch/awaitChildren.ts
26332
- function decideWait(inputs) {
26333
- const { children, now, budgetEndsAt, timeoutAt, stop, followUpPending } = inputs;
26334
- if ([...children.values()].every(isTerminal)) return { kind: "end", why: "all_ended" };
26335
- if (stop !== void 0) return { kind: "end", why: "stop" };
26336
- if (followUpPending) return { kind: "end", why: "follow_up" };
26337
- if (now >= budgetEndsAt) return { kind: "end", why: "budget" };
26338
- if (timeoutAt !== void 0 && now >= timeoutAt) return { kind: "end", why: "timeout" };
26339
- return { kind: "wait", until: Math.min(budgetEndsAt, timeoutAt ?? Number.POSITIVE_INFINITY) };
26340
- }
26341
- function budgetEndOf(now, remainingMs) {
26342
- return now + remainingMs - RUN_DEADLINE_RESERVE_MS;
26343
- }
26344
- function sleepUnlessAborted(ms, signal2) {
26345
- return new Promise((resolve12) => {
26346
- if (signal2?.aborted) return resolve12();
26347
- const done = () => {
26348
- clearTimeout(timer);
26349
- signal2?.removeEventListener("abort", done);
26350
- resolve12();
26351
- };
26352
- const timer = setTimeout(done, ms);
26353
- signal2?.addEventListener("abort", done, { once: true });
26354
- });
26355
- }
26356
- function waitCapabilityFor(deps) {
26357
- return {
26358
- stopRequested: () => deps.control.requested,
26359
- followUpsPending: () => deps.inbox.size,
26360
- watch: (ids, onChange) => {
26361
- let replaying = true;
26362
- const unsubscribe = deps.registry.subscribeIndex((event) => {
26363
- if (event.type === "removed") {
26364
- if (ids.has(event.id)) onChange();
26365
- return;
26366
- }
26367
- if (!event.run.finished) return;
26368
- if (ids.has(event.run.id)) onChange();
26369
- else if (!replaying && deps.runId !== void 0 && event.run.parentRunId === deps.runId) onChange();
26370
- });
26371
- replaying = false;
26372
- return unsubscribe;
26373
- },
26374
- now: deps.clock,
26375
- sleep: deps.sleep ?? sleepUnlessAborted
26376
- };
26070
+ if (!event.run.finished) return;
26071
+ if (ids.has(event.run.id)) onChange();
26072
+ else if (!replaying && deps.runId !== void 0 && event.run.parentRunId === deps.runId) onChange();
26073
+ });
26074
+ replaying = false;
26075
+ return unsubscribe;
26076
+ },
26077
+ now: deps.clock,
26078
+ sleep: deps.sleep ?? sleepUnlessAborted
26079
+ };
26377
26080
  }
26378
26081
  var AWAIT_POLL_MS, isTerminal, ChildrenWatch;
26379
26082
  var init_awaitChildren = __esm({
@@ -27093,7 +26796,8 @@ async function copyStaged(files, deps) {
27093
26796
  key,
27094
26797
  name: file.name,
27095
26798
  size: file.size,
27096
- contentType: file.type
26799
+ contentType: file.type,
26800
+ messageId: file.messageId
27097
26801
  });
27098
26802
  return { file, basename: basename4, key };
27099
26803
  } catch (err2) {
@@ -27152,7 +26856,8 @@ function stageThreadArtifacts(artifacts, deps) {
27152
26856
  key: a.key,
27153
26857
  name: a.name,
27154
26858
  size: a.size,
27155
- contentType: a.contentType
26859
+ contentType: a.contentType,
26860
+ messageId: deps.messageId
27156
26861
  });
27157
26862
  outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
27158
26863
  }
@@ -27458,145 +27163,444 @@ var init_session = __esm({
27458
27163
  }
27459
27164
  });
27460
27165
 
27461
- // ../../src/tools/workspace.ts
27462
- import { z as z21 } from "zod";
27463
- var bashTool, readFileTool, writeFileTool, NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool, submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, handoffEntry, submitHandoffTool, updateStatusTool, TOOLSETS;
27464
- var init_workspace = __esm({
27465
- "../../src/tools/workspace.ts"() {
27166
+ // ../../src/tools/skills.ts
27167
+ var listSkillsTool, useSkillTool;
27168
+ var init_skills = __esm({
27169
+ "../../src/tools/skills.ts"() {
27466
27170
  "use strict";
27467
- init_prDescription();
27468
- init_handoff();
27469
- init_reviewVerdict();
27470
- init_bashTimeout();
27471
- init_executor();
27472
- init_shellQuote();
27473
- init_diffDigest();
27474
- init_web();
27475
- init_github();
27476
- init_skills();
27477
- init_attach();
27478
- init_runs2();
27479
- init_session();
27480
- bashTool = {
27481
- name: "bash",
27482
- description: "Run a bash command in the workspace directory. Use for git, gh, tests, builds, and inspecting files. Commands time out after 5 minutes (300000 ms) by default; pass timeoutMs when a command legitimately needs longer \u2014 a full test suite, a large build \u2014 up to the 1200000 ms (20 minute) maximum; a command is also clipped to the run's own remaining time, so it can never outlive the run. Output is truncated at 30k characters.",
27483
- inputSchema: {
27484
- type: "object",
27485
- properties: {
27486
- command: { type: "string", description: "The bash command to run" },
27487
- timeoutMs: {
27488
- type: "integer",
27489
- description: "Optional timeout in milliseconds for this command (default 300000 = 5 min; max 1200000 = 20 min). Values outside [1000, 1200000] are clamped."
27490
- }
27491
- },
27492
- required: ["command"]
27493
- },
27494
- async run(input, ctx) {
27495
- const requested = input.timeoutMs;
27496
- let timeoutMs = typeof requested === "number" && Number.isFinite(requested) ? clampBashTimeout(requested) : void 0;
27497
- let clipNote = "";
27498
- if (ctx.remainingMs) {
27499
- const budget = bashBudgetWithinRun(timeoutMs ?? BASH_TIMEOUT_MS, ctx.remainingMs());
27500
- if (budget.kind === "exhausted") return `exit 124:
27501
- ${budget.note}`;
27502
- if (budget.kind === "clipped") {
27503
- timeoutMs = budget.timeoutMs;
27504
- clipNote = budget.note;
27505
- }
27506
- }
27507
- const opts = {
27508
- ...ctx.signal ? { signal: ctx.signal } : {},
27509
- ...timeoutMs !== void 0 ? { timeoutMs } : {}
27510
- };
27511
- const out = await ctx.executor.exec(
27512
- String(input.command ?? ""),
27513
- opts.signal !== void 0 || opts.timeoutMs !== void 0 ? opts : void 0
27514
- );
27515
- return clipNote ? `${out}
27516
- ${clipNote}` : out;
27517
- }
27518
- };
27519
- readFileTool = {
27171
+ listSkillsTool = {
27520
27172
  sideEffectFree: true,
27521
- name: "read_file",
27522
- description: "Read a file from the workspace. Path is relative to the workspace root.",
27173
+ name: "list_skills",
27174
+ description: "List the skills available to you: each is a reusable methodology you can load into context with use_skill. Returns each skill's name and a one-line description. The same list is also in your system prompt.",
27523
27175
  inputSchema: {
27524
27176
  type: "object",
27525
- properties: {
27526
- path: { type: "string", description: "Relative path to the file" }
27527
- },
27528
- required: ["path"]
27177
+ properties: {}
27529
27178
  },
27530
- run(input, ctx) {
27531
- return ctx.executor.readFile(String(input.path ?? ""));
27179
+ async run(_input, ctx) {
27180
+ if (!ctx.skills) return "Skills are not available in this context.";
27181
+ const metas = ctx.skills.list(ctx.agentName ?? "");
27182
+ if (metas.length === 0) return "No skills are available to you.";
27183
+ return metas.map((m) => `- ${m.name}: ${m.description}`).join("\n");
27532
27184
  }
27533
27185
  };
27534
- writeFileTool = {
27535
- name: "write_file",
27536
- description: "Write a file in the workspace (creates parent directories). Path is relative to the workspace root.",
27186
+ useSkillTool = {
27187
+ sideEffectFree: true,
27188
+ name: "use_skill",
27189
+ description: "Load a skill's full instructions into your context by name (get names from list_skills or your system prompt). Returns the skill body \u2014 read it and follow it for the work at hand.",
27537
27190
  inputSchema: {
27538
27191
  type: "object",
27539
27192
  properties: {
27540
- path: { type: "string", description: "Relative path to the file" },
27541
- content: { type: "string", description: "Full file contents" }
27193
+ name: { type: "string", description: "The skill name to load (e.g. code-review-and-quality)" }
27542
27194
  },
27543
- required: ["path", "content"]
27195
+ required: ["name"]
27544
27196
  },
27545
- run(input, ctx) {
27546
- return ctx.executor.writeFile(String(input.path ?? ""), String(input.content ?? ""));
27197
+ async run(input, ctx) {
27198
+ if (!ctx.skills) return "Skills are not available in this context.";
27199
+ const name = String(input.name ?? "").trim();
27200
+ if (!name) return "use_skill: provide a skill `name` (see list_skills).";
27201
+ const skill = ctx.skills.get(name);
27202
+ const inScope = skill && skill.agents.includes(ctx.agentName ?? "");
27203
+ if (!skill || !inScope) {
27204
+ const available = ctx.skills.list(ctx.agentName ?? "").map((m) => m.name);
27205
+ return `use_skill: no skill named "${name}" is available to you.` + (available.length > 0 ? ` Available: ${available.join(", ")}.` : "");
27206
+ }
27207
+ ctx.publish?.({
27208
+ type: "skill_use",
27209
+ skill: skill.name,
27210
+ description: skill.description,
27211
+ agent: ctx.agentName ?? "",
27212
+ ...skill.source ? { source: skill.source } : {},
27213
+ ...skill.upstream ? { upstream: { repo: skill.upstream.repo, commit: skill.upstream.commit } } : {},
27214
+ bodyBytes: Buffer.byteLength(skill.body, "utf8")
27215
+ });
27216
+ const footer = skill.source ? `
27217
+
27218
+ _(Skill source: ${skill.source})_` : "";
27219
+ return `# Skill: ${skill.name}
27220
+
27221
+ ${skill.body}${footer}`;
27547
27222
  }
27548
27223
  };
27549
- NAME_STATUS_MARK = "@@diff_digest:name-status@@";
27550
- NAME_STATUS_MARK_LINE = /^@@diff_digest:name-status@@$/m;
27551
- OUTPUT_CUT_RE = /^\.\.\.\[truncated \d+ chars\]$/m;
27552
- diffDigestTool = {
27553
- sideEffectFree: true,
27554
- name: "diff_digest",
27555
- description: "Summarize the current branch's whole change against a base ref as a compact digest: per-file +adds/-dels, totals, and risky-file flags (migrations/schema, auth/permission, whole-file deletions, lockfiles, very large files). This is a DISTILLED summary, not the raw diff \u2014 use it to shape a PR description, or to orient before a review. Runs `git diff --numstat` and `--name-status` over the merge-base range `<base>...HEAD` in the workspace, so it covers every file however large the change; base defaults to the repo's default branch (origin/HEAD). The totals it states are the whole change \u2014 a diff you read that shows fewer files or lines was cut short.",
27224
+ }
27225
+ });
27226
+
27227
+ // ../../src/tools/status.ts
27228
+ var updateStatusTool;
27229
+ var init_status2 = __esm({
27230
+ "../../src/tools/status.ts"() {
27231
+ "use strict";
27232
+ updateStatusTool = {
27233
+ name: "update_status",
27234
+ description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
27556
27235
  inputSchema: {
27557
27236
  type: "object",
27558
27237
  properties: {
27559
- base: {
27238
+ checklist: {
27560
27239
  type: "string",
27561
- description: "Base ref to diff against (e.g. 'main' or a SHA). Defaults to the repo's default branch via origin/HEAD."
27240
+ description: "The full checklist, one '\u2713|\u2731|\u25CB item' per line (replaces the previous one)"
27562
27241
  }
27563
- }
27242
+ },
27243
+ required: ["checklist"]
27564
27244
  },
27565
27245
  async run(input, ctx) {
27566
- const base = String(input.base ?? "").trim();
27567
- const baseName = base || "origin/HEAD";
27568
- if (base.startsWith("-")) {
27569
- return "diff_digest: base ref may not start with '-' (rejected to prevent git option injection).";
27570
- }
27571
- const baseExpr = base ? shellQuote(base) : '"$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo origin/main)"';
27572
- const range = `--end-of-options ${baseExpr}...HEAD`;
27573
- const listing = `git diff --numstat ${range} && echo ${NAME_STATUS_MARK} && git diff --name-status ${range}`;
27574
- let raw = await ctx.executor.exec(listing);
27575
- const incomplete = () => {
27576
- const reason = "the file listing exceeded the executor's output cap";
27577
- ctx.onDigest?.({ complete: false, base: baseName, reason });
27578
- return `diff_digest: ${reason} \u2014 the digest cannot state totals for this change (base \`${baseName}\`). Read the diff file by file (\`git diff --name-only ${baseName}...HEAD\`).`;
27579
- };
27580
- if (OUTPUT_CUT_RE.test(raw)) return incomplete();
27581
- let marker = NAME_STATUS_MARK_LINE.exec(raw);
27582
- if (!marker) {
27583
- const shallow = (await ctx.executor.exec("git rev-parse --is-shallow-repository")).trim() === "true";
27584
- if (shallow) {
27585
- await ctx.executor.exec('git fetch --unshallow --quiet origin "+refs/heads/*:refs/remotes/origin/*"');
27586
- raw = await ctx.executor.exec(listing);
27587
- if (OUTPUT_CUT_RE.test(raw)) return incomplete();
27588
- marker = NAME_STATUS_MARK_LINE.exec(raw);
27589
- }
27590
- }
27591
- if (!marker) {
27592
- return `diff_digest: could not compute the diff (base \`${baseName}\`).
27593
- ${raw.trim()}`;
27594
- }
27595
- const digest = distillDiffStats(raw.slice(0, marker.index), raw.slice(marker.index + marker[0].length));
27596
- ctx.onDigest?.({ complete: true, base: baseName, totals: digest.totals });
27597
- return digest.text;
27246
+ ctx.reportProgress?.(String(input.checklist ?? ""));
27247
+ return "status updated";
27598
27248
  }
27599
27249
  };
27250
+ }
27251
+ });
27252
+
27253
+ // ../../src/core/prDescription.ts
27254
+ import { z as z20 } from "zod";
27255
+ function parsePrDescription(input) {
27256
+ return PrDescriptionSchema.parse(input);
27257
+ }
27258
+ function redactPrDescription(d) {
27259
+ return mapStringLeaves(d, redactSecrets);
27260
+ }
27261
+ function mapStringLeaves(value, fn) {
27262
+ if (typeof value === "string") return fn(value);
27263
+ if (Array.isArray(value)) return value.map((v) => mapStringLeaves(v, fn));
27264
+ if (typeof value === "object" && value !== null) {
27265
+ return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, mapStringLeaves(v, fn)]));
27266
+ }
27267
+ return value;
27268
+ }
27269
+ function encodeGithubPathSegments(path) {
27270
+ return path.split("/").map(encodeURIComponent).join("/");
27271
+ }
27272
+ function anchorUrl(ctx, a) {
27273
+ return `https://github.com/${ctx.repo}/blob/${ctx.headSha}/${encodeGithubPathSegments(a.path)}#L${a.from}-L${a.to}`;
27274
+ }
27275
+ function cell2(s) {
27276
+ return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
27277
+ }
27278
+ function renderPrDescriptionMarkdown(desc, ctx) {
27279
+ if (!REPO_RE2.test(ctx.repo))
27280
+ throw new Error(`renderPrDescriptionMarkdown: repo must be owner/name, got "${ctx.repo}"`);
27281
+ if (!SHA_RE2.test(ctx.headSha))
27282
+ throw new Error(`renderPrDescriptionMarkdown: headSha must be a full 40-char lowercase sha, got "${ctx.headSha}"`);
27283
+ const out = [];
27284
+ out.push("## TL;DR", "", desc.tldr, "");
27285
+ out.push("## What & why", "", desc.whatWhy, "");
27286
+ out.push("## Tour", "");
27287
+ desc.tour.forEach((step, i) => {
27288
+ out.push(`### ${i + 1}. ${step.title}`, "", step.description, "");
27289
+ if (step.lookFor) out.push(`**Look for:** ${step.lookFor}`, "");
27290
+ out.push(anchorUrl(ctx, step.anchor), "");
27291
+ });
27292
+ out.push(`### ${desc.tour.length + 1}. Remaining changes`, "");
27293
+ if (desc.remaining.length === 0) out.push("- none \u2014 every touched file is covered by a step above", "");
27294
+ else {
27295
+ for (const r of desc.remaining) out.push(`- \`${r.path}\` \u2014 ${r.note}`);
27296
+ out.push("");
27297
+ }
27298
+ out.push("## Decisions", "");
27299
+ for (const d of desc.decisions) out.push(`- **${d.title.replace(/\.?$/, ".")}** ${d.rationale}`);
27300
+ out.push("");
27301
+ out.push("## Risks & implications", "", desc.risks, "");
27302
+ out.push("## Validation", "");
27303
+ if (desc.validation.summary) out.push(desc.validation.summary, "");
27304
+ out.push("| Criterion | Proof |", "|---|---|");
27305
+ for (const c of desc.validation.criteria) out.push(`| ${cell2(c.criterion)} | ${cell2(c.proof)} |`);
27306
+ out.push("", GENERATED_FOOTER, "");
27307
+ return out.join("\n");
27308
+ }
27309
+ function parsePrDescriptionMarkdown(body) {
27310
+ const problems = [];
27311
+ const lines = body.replace(/\r\n?/g, "\n").split("\n").filter((l) => l.trim() !== GENERATED_FOOTER);
27312
+ const fenced = fenceMask(lines);
27313
+ const sections = splitSections(lines, fenced, problems);
27314
+ const description = { tour: [] };
27315
+ const tldr = sections.get("tldr");
27316
+ if (tldr) setProse(description, "tldr", tldr, problems);
27317
+ else {
27318
+ problems.push("no `## TL;DR` section \u2014 the tldr is the body's first paragraph");
27319
+ const first = firstParagraph(lines, fenced);
27320
+ if (first) description.tldr = first;
27321
+ }
27322
+ const whatWhy = sections.get("whatWhy");
27323
+ if (whatWhy) setProse(description, "whatWhy", whatWhy, problems);
27324
+ const tour = sections.get("tour");
27325
+ if (tour) {
27326
+ const parsed = parseTour(tour.lines, fenced.slice(tour.start, tour.start + tour.lines.length), problems);
27327
+ description.tour = parsed.tour;
27328
+ if (parsed.remaining) description.remaining = parsed.remaining;
27329
+ }
27330
+ const decisions = sections.get("decisions");
27331
+ if (decisions) description.decisions = parseDecisions(decisions.lines, problems);
27332
+ const risks = sections.get("risks");
27333
+ if (risks) setProse(description, "risks", risks, problems);
27334
+ const validation = sections.get("validation");
27335
+ if (validation) description.validation = parseValidation(validation.lines, problems);
27336
+ for (const [key, heading] of SECTIONS) {
27337
+ if (key === "tldr" || sections.has(key)) continue;
27338
+ problems.push(`no \`## ${heading}\` section`);
27339
+ }
27340
+ return { description, complete: problems.length === 0, problems };
27341
+ }
27342
+ function fenceMask(lines) {
27343
+ let open2;
27344
+ return lines.map((l) => {
27345
+ const m = /^\s*(`{3,}|~{3,})/.exec(l);
27346
+ if (m && (open2 === void 0 || m[1][0] === open2[0])) {
27347
+ const wasOpen = open2 !== void 0;
27348
+ open2 = wasOpen ? void 0 : m[1];
27349
+ return true;
27350
+ }
27351
+ return open2 !== void 0;
27352
+ });
27353
+ }
27354
+ function splitSections(lines, fenced, problems) {
27355
+ const byHeading = new Map(SECTIONS.map(([key, heading]) => [heading.toLowerCase(), key]));
27356
+ const out = /* @__PURE__ */ new Map();
27357
+ let current;
27358
+ lines.forEach((line3, i) => {
27359
+ const m = fenced[i] ? null : HEADING_RE.exec(line3);
27360
+ if (m && m[1] === "##") {
27361
+ const key = byHeading.get(m[2].replace(/\s+/g, " ").toLowerCase());
27362
+ if (key === void 0)
27363
+ current = void 0;
27364
+ else if (out.has(key)) {
27365
+ problems.push(`duplicate \`## ${SECTIONS.find(([k]) => k === key)[1]}\` section \u2014 the first one stands`);
27366
+ current = void 0;
27367
+ } else {
27368
+ current = { start: i + 1, lines: [] };
27369
+ out.set(key, current);
27370
+ }
27371
+ return;
27372
+ }
27373
+ current?.lines.push(line3);
27374
+ });
27375
+ return out;
27376
+ }
27377
+ function firstParagraph(lines, fenced) {
27378
+ const para = [];
27379
+ for (let i = 0; i < lines.length; i++) {
27380
+ const l = lines[i];
27381
+ if (l.trim() === "") {
27382
+ if (para.length) break;
27383
+ continue;
27384
+ }
27385
+ if (fenced[i] || para.length === 0 && HEADING_RE.test(l)) continue;
27386
+ para.push(l.trim());
27387
+ }
27388
+ return para.length ? para.join("\n") : void 0;
27389
+ }
27390
+ function setProse(description, key, section, problems) {
27391
+ const text = section.lines.join("\n").trim();
27392
+ if (text === "") problems.push(`\`## ${SECTIONS.find(([k]) => k === key)[1]}\` section is empty`);
27393
+ else description[key] = text;
27394
+ }
27395
+ function parseTour(lines, fenced, problems) {
27396
+ const steps = [];
27397
+ let preamble = "";
27398
+ lines.forEach((line3, i) => {
27399
+ const m = fenced[i] ? null : STEP_RE.exec(line3);
27400
+ if (m) steps.push({ label: m[1] ?? String(steps.length + 1), title: m[2], lines: [] });
27401
+ else if (steps.length) steps[steps.length - 1].lines.push(line3);
27402
+ else preamble += line3;
27403
+ });
27404
+ if (preamble.trim() !== "") problems.push("Tour: text before the first `### N.` step is ignored");
27405
+ const isRemaining = (title) => /^remaining changes$/i.test(title);
27406
+ const tour = [];
27407
+ let remaining;
27408
+ for (const step of steps) {
27409
+ if (isRemaining(step.title)) {
27410
+ remaining = parseRemaining(step.lines, problems);
27411
+ continue;
27412
+ }
27413
+ const parsed = parseStep(step, problems);
27414
+ if (parsed) tour.push(parsed);
27415
+ }
27416
+ if (!steps.some((s) => !isRemaining(s.title))) problems.push("Tour has no steps");
27417
+ if (remaining === void 0) problems.push("Tour has no `Remaining changes` step");
27418
+ return { tour, ...remaining ? { remaining } : {} };
27419
+ }
27420
+ function parseStep(step, problems) {
27421
+ const where = `step ${step.label} (${step.title})`;
27422
+ let link;
27423
+ const descLines = [];
27424
+ const lookForLines = [];
27425
+ let inLookFor = false;
27426
+ for (const line3 of step.lines) {
27427
+ const m = PERMALINK_RE.exec(line3.trim());
27428
+ if (m) {
27429
+ if (link) problems.push(`${where}: more than one permalink \u2014 the first is the anchor`);
27430
+ else link = m;
27431
+ inLookFor = false;
27432
+ continue;
27433
+ }
27434
+ const lf = LOOK_FOR_RE.exec(line3.trim());
27435
+ if (lf) {
27436
+ lookForLines.push(lf[1]);
27437
+ inLookFor = true;
27438
+ continue;
27439
+ }
27440
+ if (inLookFor) {
27441
+ if (line3.trim() === "") inLookFor = false;
27442
+ else lookForLines.push(line3.trim());
27443
+ continue;
27444
+ }
27445
+ descLines.push(line3);
27446
+ }
27447
+ if (!link) {
27448
+ problems.push(`${where}: no permalink line \u2014 dropped`);
27449
+ return void 0;
27450
+ }
27451
+ let path;
27452
+ try {
27453
+ path = link[2].split("/").map(decodeURIComponent).join("/");
27454
+ } catch {
27455
+ problems.push(`${where}: permalink path is not valid percent-encoding \u2014 dropped`);
27456
+ return void 0;
27457
+ }
27458
+ const from = Number(link[3]);
27459
+ const anchor = TourAnchorSchema.safeParse({ path, from, to: link[4] === void 0 ? from : Number(link[4]) });
27460
+ if (!anchor.success) {
27461
+ const issue = anchor.error.issues[0];
27462
+ problems.push(`${where}: ${["anchor", ...issue.path].join(".")} ${issue.message} \u2014 dropped`);
27463
+ return void 0;
27464
+ }
27465
+ const lookFor = lookForLines.join("\n").trim();
27466
+ return {
27467
+ title: step.title,
27468
+ // A parsed step may carry an empty description (a human's heading +
27469
+ // permalink): this is a read-back, not a submission the schema gates.
27470
+ description: descLines.join("\n").replace(/\n{3,}/g, "\n\n").trim(),
27471
+ ...lookFor ? { lookFor } : {},
27472
+ anchor: { ...anchor.data, sha: link[1] }
27473
+ };
27474
+ }
27475
+ function parseRemaining(lines, problems) {
27476
+ const out = [];
27477
+ for (const raw of lines) {
27478
+ const line3 = raw.trim();
27479
+ if (line3 === "" || REMAINING_NONE_RE.test(line3)) continue;
27480
+ const m = REMAINING_LINE_RE.exec(line3);
27481
+ if (m) out.push({ path: m[1], note: m[2].trim() });
27482
+ else if (!line3.startsWith("-") && out.length) out[out.length - 1].note += `
27483
+ ${line3}`;
27484
+ else problems.push(`Remaining changes: unrecognized line \`${line3}\``);
27485
+ }
27486
+ return out;
27487
+ }
27488
+ function parseDecisions(lines, problems) {
27489
+ const out = [];
27490
+ for (const raw of lines) {
27491
+ const line3 = raw.trim();
27492
+ if (line3 === "") continue;
27493
+ const m = DECISION_RE.exec(line3);
27494
+ if (m) out.push({ title: m[1].replace(/\.$/, ""), rationale: m[2].trim() });
27495
+ else if (!line3.startsWith("-") && out.length) out[out.length - 1].rationale += `
27496
+ ${line3}`;
27497
+ else problems.push(`Decisions: unrecognized line \`${line3}\``);
27498
+ }
27499
+ return out;
27500
+ }
27501
+ function parseValidation(lines, problems) {
27502
+ const summary = [];
27503
+ const criteria = [];
27504
+ let header4 = false;
27505
+ for (const raw of lines) {
27506
+ const line3 = raw.trim();
27507
+ if (!header4) {
27508
+ if (TABLE_HEADER_RE.test(line3)) header4 = true;
27509
+ else summary.push(raw);
27510
+ continue;
27511
+ }
27512
+ if (line3 === "" || TABLE_RULE_RE.test(line3)) continue;
27513
+ const cells = splitCells(line3);
27514
+ if (cells.length >= 2) criteria.push({ criterion: cells[0], proof: cells[1] });
27515
+ else problems.push(`Validation: unrecognized table line \`${line3}\``);
27516
+ }
27517
+ if (!header4) problems.push("Validation: no `| Criterion | Proof |` table");
27518
+ const text = summary.join("\n").trim();
27519
+ return { ...text ? { summary: text } : {}, criteria };
27520
+ }
27521
+ function splitCells(row) {
27522
+ const cells = [];
27523
+ let cur = "";
27524
+ const inner = row.replace(/^\|/, "").replace(/\|$/, "");
27525
+ for (let i = 0; i < inner.length; i++) {
27526
+ const ch = inner[i];
27527
+ if (ch === "\\" && i + 1 < inner.length) {
27528
+ cur += inner[++i];
27529
+ } else if (ch === "|") {
27530
+ cells.push(cur.trim());
27531
+ cur = "";
27532
+ } else cur += ch;
27533
+ }
27534
+ cells.push(cur.trim());
27535
+ return cells;
27536
+ }
27537
+ var line2, prose, TourAnchorSchema, TourStepSchema, PrDescriptionSchema, REPO_RE2, SHA_RE2, GENERATED_FOOTER, SECTIONS, PERMALINK_RE, HEADING_RE, STEP_RE, LOOK_FOR_RE, REMAINING_LINE_RE, REMAINING_NONE_RE, DECISION_RE, TABLE_HEADER_RE, TABLE_RULE_RE;
27538
+ var init_prDescription = __esm({
27539
+ "../../src/core/prDescription.ts"() {
27540
+ "use strict";
27541
+ init_redact();
27542
+ line2 = z20.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
27543
+ prose = z20.string().trim().min(1);
27544
+ TourAnchorSchema = z20.object({
27545
+ // Repo-relative: no leading `/`, no `..` SEGMENT (a filename like `a..b.ts` is fine).
27546
+ path: z20.string().trim().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), "path must be repo-relative"),
27547
+ from: z20.number().int().positive(),
27548
+ to: z20.number().int().positive()
27549
+ }).refine((a) => a.to >= a.from, "to must be >= from");
27550
+ TourStepSchema = z20.object({
27551
+ title: line2,
27552
+ description: prose,
27553
+ lookFor: prose.optional(),
27554
+ anchor: TourAnchorSchema
27555
+ });
27556
+ PrDescriptionSchema = z20.object({
27557
+ /** The PR title's single source. Metadata for the PR's own title field —
27558
+ * never rendered into the body (GitHub shows the title itself). */
27559
+ title: line2,
27560
+ tldr: prose,
27561
+ whatWhy: prose,
27562
+ tour: z20.array(TourStepSchema).min(1),
27563
+ /** Every touched file the Tour steps did not cover, one line each. */
27564
+ remaining: z20.array(z20.object({ path: line2, note: prose })),
27565
+ decisions: z20.array(z20.object({ title: line2, rationale: prose })).min(1),
27566
+ risks: prose,
27567
+ validation: z20.object({
27568
+ summary: prose.optional(),
27569
+ criteria: z20.array(z20.object({ criterion: prose, proof: prose })).min(1)
27570
+ })
27571
+ });
27572
+ REPO_RE2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
27573
+ SHA_RE2 = /^[0-9a-f]{40}$/;
27574
+ GENERATED_FOOTER = "\u{1F916} Generated with [Claude Code](https://claude.com/claude-code)";
27575
+ SECTIONS = [
27576
+ ["tldr", "TL;DR"],
27577
+ ["whatWhy", "What & why"],
27578
+ ["tour", "Tour"],
27579
+ ["decisions", "Decisions"],
27580
+ ["risks", "Risks & implications"],
27581
+ ["validation", "Validation"]
27582
+ ];
27583
+ PERMALINK_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/blob\/([0-9a-f]{7,40})\/([^#\s]+)#L(\d+)(?:-L(\d+))?$/;
27584
+ HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
27585
+ STEP_RE = /^###\s+(?:(\d+)\.\s+)?(.+?)\s*$/;
27586
+ LOOK_FOR_RE = /^\*\*Look for:\*\*\s*(.*)$/;
27587
+ REMAINING_LINE_RE = /^-\s+`([^`]+)`\s+—\s*(.*)$/;
27588
+ REMAINING_NONE_RE = /^-\s+none\b/i;
27589
+ DECISION_RE = /^-\s+\*\*(.+?)\*\*\s*(.*)$/;
27590
+ TABLE_HEADER_RE = /^\|\s*criterion\s*\|\s*proof\s*\|$/i;
27591
+ TABLE_RULE_RE = /^\|?\s*:?-{3,}:?\s*\|/;
27592
+ }
27593
+ });
27594
+
27595
+ // ../../src/tools/submit.ts
27596
+ import { z as z21 } from "zod";
27597
+ var submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, handoffEntry, submitHandoffTool;
27598
+ var init_submit = __esm({
27599
+ "../../src/tools/submit.ts"() {
27600
+ "use strict";
27601
+ init_prDescription();
27602
+ init_reviewVerdict();
27603
+ init_handoff();
27600
27604
  submitVerdictTool = {
27601
27605
  name: "submit_verdict",
27602
27606
  failsInText: true,
@@ -27817,22 +27821,92 @@ ${raw.trim()}`;
27817
27821
  return `handoff recorded: ${count(h.deviations.length, "deviation", "deviations")}, ${count(h.followUps.length, "follow-up", "follow-ups")}, ${h.unproven.length} unproven; a later call replaces this one`;
27818
27822
  }
27819
27823
  };
27820
- updateStatusTool = {
27821
- name: "update_status",
27822
- description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
27824
+ }
27825
+ });
27826
+
27827
+ // ../../src/tools/workspace.ts
27828
+ var bashTool, readFileTool, writeFileTool, TOOLSETS;
27829
+ var init_workspace = __esm({
27830
+ "../../src/tools/workspace.ts"() {
27831
+ "use strict";
27832
+ init_bashTimeout();
27833
+ init_executor();
27834
+ init_attach();
27835
+ init_diffDigest2();
27836
+ init_github();
27837
+ init_runs2();
27838
+ init_session();
27839
+ init_skills();
27840
+ init_status2();
27841
+ init_submit();
27842
+ init_web();
27843
+ bashTool = {
27844
+ name: "bash",
27845
+ description: "Run a bash command in the workspace directory. Use for git, gh, tests, builds, and inspecting files. Commands time out after 5 minutes (300000 ms) by default; pass timeoutMs when a command legitimately needs longer \u2014 a full test suite, a large build \u2014 up to the 1200000 ms (20 minute) maximum; a command is also clipped to the run's own remaining time, so it can never outlive the run. Output is truncated at 30k characters.",
27823
27846
  inputSchema: {
27824
27847
  type: "object",
27825
27848
  properties: {
27826
- checklist: {
27827
- type: "string",
27828
- description: "The full checklist, one '\u2713|\u2731|\u25CB item' per line (replaces the previous one)"
27849
+ command: { type: "string", description: "The bash command to run" },
27850
+ timeoutMs: {
27851
+ type: "integer",
27852
+ description: "Optional timeout in milliseconds for this command (default 300000 = 5 min; max 1200000 = 20 min). Values outside [1000, 1200000] are clamped."
27829
27853
  }
27830
27854
  },
27831
- required: ["checklist"]
27855
+ required: ["command"]
27832
27856
  },
27833
27857
  async run(input, ctx) {
27834
- ctx.reportProgress?.(String(input.checklist ?? ""));
27835
- return "status updated";
27858
+ const requested = input.timeoutMs;
27859
+ let timeoutMs = typeof requested === "number" && Number.isFinite(requested) ? clampBashTimeout(requested) : void 0;
27860
+ let clipNote = "";
27861
+ if (ctx.remainingMs) {
27862
+ const budget = bashBudgetWithinRun(timeoutMs ?? BASH_TIMEOUT_MS, ctx.remainingMs());
27863
+ if (budget.kind === "exhausted") return `exit 124:
27864
+ ${budget.note}`;
27865
+ if (budget.kind === "clipped") {
27866
+ timeoutMs = budget.timeoutMs;
27867
+ clipNote = budget.note;
27868
+ }
27869
+ }
27870
+ const opts = {
27871
+ ...ctx.signal ? { signal: ctx.signal } : {},
27872
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
27873
+ };
27874
+ const out = await ctx.executor.exec(
27875
+ String(input.command ?? ""),
27876
+ opts.signal !== void 0 || opts.timeoutMs !== void 0 ? opts : void 0
27877
+ );
27878
+ return clipNote ? `${out}
27879
+ ${clipNote}` : out;
27880
+ }
27881
+ };
27882
+ readFileTool = {
27883
+ sideEffectFree: true,
27884
+ name: "read_file",
27885
+ description: "Read a file from the workspace. Path is relative to the workspace root.",
27886
+ inputSchema: {
27887
+ type: "object",
27888
+ properties: {
27889
+ path: { type: "string", description: "Relative path to the file" }
27890
+ },
27891
+ required: ["path"]
27892
+ },
27893
+ run(input, ctx) {
27894
+ return ctx.executor.readFile(String(input.path ?? ""));
27895
+ }
27896
+ };
27897
+ writeFileTool = {
27898
+ name: "write_file",
27899
+ description: "Write a file in the workspace (creates parent directories). Path is relative to the workspace root.",
27900
+ inputSchema: {
27901
+ type: "object",
27902
+ properties: {
27903
+ path: { type: "string", description: "Relative path to the file" },
27904
+ content: { type: "string", description: "Full file contents" }
27905
+ },
27906
+ required: ["path", "content"]
27907
+ },
27908
+ run(input, ctx) {
27909
+ return ctx.executor.writeFile(String(input.path ?? ""), String(input.content ?? ""));
27836
27910
  }
27837
27911
  };
27838
27912
  TOOLSETS = {
@@ -28292,7 +28366,7 @@ var init_route = __esm({
28292
28366
  init_registry();
28293
28367
  init_workspace();
28294
28368
  init_config2();
28295
- init_types2();
28369
+ init_provider();
28296
28370
  init_redact();
28297
28371
  init_statusCardFrame();
28298
28372
  init_spawn();
@@ -28309,6 +28383,33 @@ var init_route = __esm({
28309
28383
  }
28310
28384
  });
28311
28385
 
28386
+ // ../../src/core/chatMessage.ts
28387
+ function toolResultText(content) {
28388
+ if (typeof content === "string") return content;
28389
+ return content.map((p) => {
28390
+ if (p.type === "text") return p.text;
28391
+ if (p.type === "image") return `[image ${p.mediaType}, ${base64Bytes(p.data)} bytes]`;
28392
+ return `[document ${p.name ?? "document"} (${p.mediaType}), ${base64Bytes(p.data)} bytes]`;
28393
+ }).join("\n");
28394
+ }
28395
+ function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
28396
+ const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
28397
+ \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
28398
+ if (typeof content === "string") return capText(content);
28399
+ return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
28400
+ }
28401
+ function base64Bytes(b64) {
28402
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
28403
+ return Math.floor(b64.length * 3 / 4) - padding;
28404
+ }
28405
+ var MAX_TOOL_RESULT_CHARS;
28406
+ var init_chatMessage = __esm({
28407
+ "../../src/core/chatMessage.ts"() {
28408
+ "use strict";
28409
+ MAX_TOOL_RESULT_CHARS = 12e4;
28410
+ }
28411
+ });
28412
+
28312
28413
  // ../../src/execution/tracingExecutor.ts
28313
28414
  var TracingExecutor;
28314
28415
  var init_tracingExecutor = __esm({
@@ -28507,7 +28608,12 @@ async function runLoop(opts, now, note, emit, agentSpan) {
28507
28608
  ...input.userName ? { user: input.userName } : {},
28508
28609
  ...input.from ? { run: input.from.runId } : {}
28509
28610
  };
28510
- emit({ type: "input", text: redactSecrets(input.text), ...Object.keys(source2).length > 0 ? { source: source2 } : {} });
28611
+ emit({
28612
+ type: "input",
28613
+ text: redactSecrets(input.text),
28614
+ messageId: followUpMessageId(input),
28615
+ ...Object.keys(source2).length > 0 ? { source: source2 } : {}
28616
+ });
28511
28617
  note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
28512
28618
  }
28513
28619
  const prompt = followUpPrompt(inputs, { superseded });
@@ -28554,13 +28660,14 @@ ${stagedLine}` : prompt }];
28554
28660
  }
28555
28661
  const ctx = callSpan ? {
28556
28662
  ...toolContext,
28663
+ callId: tu.id,
28557
28664
  span: callSpan,
28558
28665
  executor: new TracingExecutor(execTracker, callSpan, opts.backend),
28559
28666
  publish: (e) => emit(withSpanId(e, callSpan.id)),
28560
28667
  // The GitHub client as a view under this call: its requests and any
28561
28668
  // token mint become `github.*` children (docs/reference/specs/tracing.md item 23).
28562
28669
  ...toolContext.github?.api.withSpan ? { github: { ...toolContext.github, api: toolContext.github.api.withSpan(callSpan) } } : {}
28563
- } : toolContext;
28670
+ } : { ...toolContext, callId: tu.id };
28564
28671
  try {
28565
28672
  const output = await untilHardStop(tool.run(tu.input ?? {}, ctx));
28566
28673
  const text2 = toolResultText(output);
@@ -28899,7 +29006,7 @@ var MAX_CONSECUTIVE_INFRA_FAILURES, HardStopError, HARD_STOP_MESSAGE, wrapUpNote
28899
29006
  var init_runner = __esm({
28900
29007
  "../../src/runner.ts"() {
28901
29008
  "use strict";
28902
- init_types2();
29009
+ init_chatMessage();
28903
29010
  init_runEvents();
28904
29011
  init_threadAdmission();
28905
29012
  init_executor();
@@ -29295,42 +29402,42 @@ async function settle(input, span) {
29295
29402
  }
29296
29403
  verdict = void 0;
29297
29404
  reviewHead = current;
29298
- const system = input.composeSystem({ sha: current, verified: worktreeMoved });
29405
+ const followUpText = rereviewFollowUp({
29406
+ where,
29407
+ reviewed: expected,
29408
+ current,
29409
+ move,
29410
+ before: classified.before,
29411
+ after: classified.after,
29412
+ worktreeMoved
29413
+ });
29299
29414
  input.messages.push(
29300
29415
  { role: "assistant", content: [{ type: "text", text: answer }] },
29301
- {
29302
- role: "user",
29303
- content: [
29304
- {
29305
- type: "text",
29306
- text: rereviewFollowUp({
29307
- where,
29308
- reviewed: expected,
29309
- current,
29310
- move,
29311
- before: classified.before,
29312
- after: classified.after,
29313
- worktreeMoved
29314
- })
29315
- }
29316
- ]
29317
- }
29416
+ { role: "user", content: [{ type: "text", text: followUpText }] }
29318
29417
  );
29319
- answer = await runAgent({
29418
+ const toolContext = {
29419
+ ...turn.toolContext,
29420
+ onVerdict: (v) => {
29421
+ verdict = v;
29422
+ }
29423
+ };
29424
+ answer = turn.followUp ? await turn.followUp({
29425
+ text: followUpText,
29426
+ maxTurns: turn.agent.maxTurns,
29427
+ maxMinutes: turn.agent.maxMinutes,
29428
+ toolContext,
29429
+ ...span ? { span } : {}
29430
+ }) : await runAgent({
29320
29431
  provider: turn.provider,
29321
29432
  model: turn.model,
29322
29433
  agent: turn.agent,
29323
29434
  messages: input.messages,
29324
- system,
29435
+ // Recomposed at the new head, so the prompt does not contradict the follow-up.
29436
+ system: input.composeSystem({ sha: current, verified: worktreeMoved }),
29325
29437
  effort: turn.effort,
29326
29438
  ...span ? { span } : {},
29327
29439
  ...turn.backend ? { backend: turn.backend } : {},
29328
- toolContext: {
29329
- ...turn.toolContext,
29330
- onVerdict: (v) => {
29331
- verdict = v;
29332
- }
29333
- },
29440
+ toolContext,
29334
29441
  ...turn.extraTools && turn.extraTools.length > 0 ? { extraTools: turn.extraTools } : {},
29335
29442
  onProgress: turn.onProgress,
29336
29443
  onEvent: turn.onEvent,
@@ -30390,7 +30497,8 @@ async function registerRun(deps, ctx) {
30390
30497
  trace.bindRun(run2.id, (e) => registry.publish(run2.id, e));
30391
30498
  const publishText = (type, text, source3, raw) => {
30392
30499
  const redacted = redactSecrets(text);
30393
- const event = { type, text: redacted, ...source3 ? { source: source3 } : {}, at: clock3() };
30500
+ const body = { text: redacted, ...source3 ? { source: source3 } : {}, at: clock3() };
30501
+ const event = type === "input" ? { type, messageId: messageIdOf(msg, run2.id), ...body } : { type, ...body };
30394
30502
  const withRaw = raw !== void 0 ? { ...event, raw: redactSecrets(raw) } : event;
30395
30503
  registry.publish(run2.id, utf8ByteLength(JSON.stringify(withRaw)) <= MAX_EVENT_BYTES ? withRaw : event);
30396
30504
  console.log(`[event] ${msg.threadKey} type=${type} bytes=${utf8ByteLength(redacted)}`);
@@ -30689,7 +30797,7 @@ var init_provision = __esm({
30689
30797
  init_resident();
30690
30798
  init_memory();
30691
30799
  init_runBearers();
30692
- init_types2();
30800
+ init_provider();
30693
30801
  init_skills2();
30694
30802
  init_source();
30695
30803
  init_configAwareness();
@@ -30701,6 +30809,7 @@ var init_provision = __esm({
30701
30809
  init_inboxMessage();
30702
30810
  init_statusCardFrame();
30703
30811
  init_statusCoalescer();
30812
+ init_types4();
30704
30813
  init_record();
30705
30814
  init_reply();
30706
30815
  init_messages();
@@ -31665,6 +31774,7 @@ var init_bridge2 = __esm({
31665
31774
  PiBridge = class {
31666
31775
  constructor(deps) {
31667
31776
  this.deps = deps;
31777
+ this.agentSpan = deps.agentSpan;
31668
31778
  }
31669
31779
  deps;
31670
31780
  /** Model turns that did work, the guard's count — the loop's rule: a turn whose every call is `update_status` is bookkeeping. */
@@ -31686,6 +31796,24 @@ var init_bridge2 = __esm({
31686
31796
  * were vetted by the bot generation that died, and this one never heard. */
31687
31797
  judgeGate = true;
31688
31798
  summarizationRetries = 0;
31799
+ /** The span a starting call's `tool.<name>` opens under: the run's
31800
+ * `run.agent` for the loop, a follow-up turn's own for its duration
31801
+ * (`under`; harness-pi item 14). */
31802
+ agentSpan;
31803
+ /** From here on, new tool spans hang under `span` — a follow-up turn's
31804
+ * `run.agent`, opened under the caller's span the way the native loop's
31805
+ * would be (tracing.md item 17); `undefined` opens none. */
31806
+ under(span) {
31807
+ this.agentSpan = span;
31808
+ }
31809
+ /** A new prompt is about to be sent on the session (harness-pi item 14): the
31810
+ * reply pi settled the last loop on is not this turn's answer, so the
31811
+ * answer state starts over — what pi settles on next is the turn's. */
31812
+ newPrompt() {
31813
+ this.answerText = void 0;
31814
+ this.heldAnswer = void 0;
31815
+ this.assistantStartedAt = void 0;
31816
+ }
31689
31817
  /** The gate saw this call: the extension's `tool_call` hook asked the bot
31690
31818
  * for it, whatever the answer. Called from the authorize route, before pi
31691
31819
  * runs the tool — so it always precedes the call's `tool_execution_end`. */
@@ -31794,7 +31922,7 @@ var init_bridge2 = __esm({
31794
31922
  onToolStart(event) {
31795
31923
  const tool = str5(event.toolName);
31796
31924
  const callId = str5(event.toolCallId);
31797
- const span = this.deps.agentSpan?.start(`tool.${tool}`);
31925
+ const span = this.agentSpan?.start(`tool.${tool}`);
31798
31926
  this.openTools.set(callId, { span, tool, judged: this.judgeGate });
31799
31927
  this.toolCalls++;
31800
31928
  const input = isRecord3(event.args) ? event.args : void 0;
@@ -32317,7 +32445,7 @@ function ledgerTailOf(resume) {
32317
32445
  function settlementAnswer(s) {
32318
32446
  return { content: [{ type: "text", text: settlementText(s) }], isError: true };
32319
32447
  }
32320
- async function runPiHarness(deps, run2) {
32448
+ async function runPiHarnessOpen(deps, run2) {
32321
32449
  const { container, clock: clock3 } = deps;
32322
32450
  const now = () => clock3();
32323
32451
  const agentSpan = run2.span?.start("run.agent");
@@ -32412,6 +32540,26 @@ async function runPiHarness(deps, run2) {
32412
32540
  save();
32413
32541
  };
32414
32542
  let catchingUp = false;
32543
+ const clearWriteUp = () => {
32544
+ writeUp = void 0;
32545
+ writeUpAt = void 0;
32546
+ };
32547
+ let followUps = 0;
32548
+ let sessionEnded = false;
32549
+ const end = async () => {
32550
+ if (sessionEnded) return;
32551
+ sessionEnded = true;
32552
+ transport?.close();
32553
+ bridge.closeOpenSpans(
32554
+ hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
32555
+ );
32556
+ save();
32557
+ forget();
32558
+ if (pid !== void 0) await container.kill(pid).catch(() => {
32559
+ });
32560
+ if (paths !== void 0) await container.remove(paths).catch(() => {
32561
+ });
32562
+ };
32415
32563
  try {
32416
32564
  const recorded = run2.resume?.facts;
32417
32565
  let reattached = false;
@@ -32551,7 +32699,12 @@ async function runPiHarness(deps, run2) {
32551
32699
  ...input.userName ? { user: input.userName } : {},
32552
32700
  ...input.from ? { run: input.from.runId } : {}
32553
32701
  };
32554
- emit({ type: "input", text: redactSecrets(input.text), ...Object.keys(source2).length > 0 ? { source: source2 } : {} });
32702
+ emit({
32703
+ type: "input",
32704
+ text: redactSecrets(input.text),
32705
+ messageId: followUpMessageId(input),
32706
+ ...Object.keys(source2).length > 0 ? { source: source2 } : {}
32707
+ });
32555
32708
  note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
32556
32709
  }
32557
32710
  const images = inputs.flatMap(
@@ -32689,33 +32842,137 @@ ${stagedLine}` : prompt,
32689
32842
  check();
32690
32843
  if (hardStopped) break;
32691
32844
  }
32845
+ let answer;
32692
32846
  if (hardStopped) {
32693
32847
  note("stopped", hardStopNote(), "hard");
32694
- return HARD_STOP_MESSAGE;
32695
- }
32696
- if (bypass) throw bypass;
32697
- if (!settled) {
32698
- const tail2 = await container.tail(paths.errLog, 2e3);
32699
- throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
32700
- }
32701
- if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
32702
- const text = bridge.answer() ?? "";
32703
- if (writeUp?.kind === "time") return timeBudgetAnswer(text, run2.agent.maxMinutes);
32704
- if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace);
32705
- if (writeUp?.kind === "soft" || stopMode === "soft") return softStopAnswer(text);
32706
- return text || "_(no response)_";
32707
- } finally {
32708
- transport?.close();
32709
- bridge.closeOpenSpans(
32710
- hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
32711
- );
32712
- save();
32713
- forget();
32714
- if (pid !== void 0) await container.kill(pid).catch(() => {
32715
- });
32716
- if (paths !== void 0) await container.remove(paths).catch(() => {
32717
- });
32718
- agentSpan?.end(hardStopped || bypass ? "error" : "ok");
32848
+ answer = HARD_STOP_MESSAGE;
32849
+ } else {
32850
+ if (bypass) throw bypass;
32851
+ if (!settled) {
32852
+ const tail2 = await container.tail(paths.errLog, 2e3);
32853
+ throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
32854
+ }
32855
+ if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
32856
+ const text = bridge.answer() ?? "";
32857
+ answer = writeUp?.kind === "time" ? timeBudgetAnswer(text, run2.agent.maxMinutes) : writeUp?.kind === "turns" ? turnGuardAnswer(text, writeUp.pace) : writeUp?.kind === "soft" || stopMode === "soft" ? softStopAnswer(text) : text || "_(no response)_";
32858
+ }
32859
+ agentSpan?.end(hardStopped ? "error" : "ok");
32860
+ const rpc = transport;
32861
+ const root = paths;
32862
+ const followUp = async (input) => {
32863
+ if (sessionEnded) throw new Error("the pi session has ended: no follow-up turn can run on it");
32864
+ const turnSpan = input.span?.start("run.agent");
32865
+ if (turnSpan) deps.bearers?.reparent(run2.runId, turnSpan);
32866
+ bridge.under(turnSpan);
32867
+ bridge.newPrompt();
32868
+ const runContext = live.toolContext;
32869
+ live.toolContext = input.toolContext;
32870
+ const turnDeadline = now() + input.maxMinutes * 6e4;
32871
+ input.toolContext.remainingMs = () => turnDeadline - now();
32872
+ const turnsBefore = bridge.turns;
32873
+ clearWriteUp();
32874
+ const id = `${ids.prompt}:follow-up:${++followUps}`;
32875
+ let turnSettled = false;
32876
+ let turnError;
32877
+ let turnFailed = false;
32878
+ const turnCheck = () => {
32879
+ if (run2.control?.requested === "hard") {
32880
+ if (!hardStopped) {
32881
+ hardStopped = true;
32882
+ rpc.send({ type: "abort" });
32883
+ }
32884
+ return;
32885
+ }
32886
+ if (writeUp) {
32887
+ if (writeUpAt !== void 0 && now() - writeUpAt >= (deps.finaleTimeoutMs ?? FINALE_TIMEOUT_MS2)) {
32888
+ writeUpAt = void 0;
32889
+ run2.onProgress?.("finale timed out \u2014 closing the turn without a write-up");
32890
+ rpc.send({ type: "abort" });
32891
+ }
32892
+ return;
32893
+ }
32894
+ if (run2.control?.requested === "soft") {
32895
+ note("stopped", softStopNote(), "soft");
32896
+ startWriteUp({ kind: "soft" }, SOFT_STOP_INSTRUCTION);
32897
+ return;
32898
+ }
32899
+ if (now() >= turnDeadline) {
32900
+ note("time_budget_exhausted", timeBudgetNote());
32901
+ startWriteUp({ kind: "time" }, timeBudgetInstruction());
32902
+ return;
32903
+ }
32904
+ const turns = bridge.turns - turnsBefore;
32905
+ if (turns >= input.maxTurns) {
32906
+ const pace = turnGuardPace(turns, input.maxMinutes * 6e4 - (turnDeadline - now()));
32907
+ note("turn_budget_exhausted", turnGuardNote(pace));
32908
+ startWriteUp({ kind: "turns", pace }, turnGuardInstruction(pace));
32909
+ }
32910
+ };
32911
+ try {
32912
+ rpc.send({ id, type: "prompt", message: input.text });
32913
+ turnCheck();
32914
+ for (; ; ) {
32915
+ pending2 ??= iterator.next();
32916
+ const tick = deps.sleep(deps.tickMs ?? 1e3).then(() => "tick");
32917
+ const next = await Promise.race([pending2, tick]);
32918
+ if (next === "tick") {
32919
+ turnCheck();
32920
+ if (hardStopped) break;
32921
+ continue;
32922
+ }
32923
+ pending2 = void 0;
32924
+ if (next.done) break;
32925
+ const event = parsePiLine(next.value);
32926
+ if (!event) continue;
32927
+ const obs = bridge.observe(event);
32928
+ for (const reply of obs.replies) rpc.send(reply);
32929
+ if (obs.gateBypassed) {
32930
+ bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
32931
+ note("harness_error", `${bypass.message} \u2014 the turn is stopped`);
32932
+ rpc.send({ type: "abort" });
32933
+ break;
32934
+ }
32935
+ if (obs.response?.id === id && obs.response.success === false)
32936
+ throw new PromptRefused(String(obs.response.error ?? "no reason"));
32937
+ if (obs.providerError !== void 0) turnError = obs.providerError;
32938
+ if (obs.settled) {
32939
+ turnSettled = true;
32940
+ break;
32941
+ }
32942
+ turnCheck();
32943
+ if (hardStopped) break;
32944
+ }
32945
+ if (hardStopped) {
32946
+ note("stopped", hardStopNote(), "hard");
32947
+ return HARD_STOP_MESSAGE;
32948
+ }
32949
+ if (bypass) throw bypass;
32950
+ if (!turnSettled) {
32951
+ const tail2 = await container.tail(root.errLog, 2e3);
32952
+ throw new Error(
32953
+ `pi exited before the turn settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`
32954
+ );
32955
+ }
32956
+ if (turnError !== void 0) throw new Error(`the model call failed: ${turnError}`);
32957
+ const text = bridge.answer() ?? "";
32958
+ if (writeUp?.kind === "time") return timeBudgetAnswer(text, input.maxMinutes);
32959
+ if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace);
32960
+ if (writeUp?.kind === "soft") return softStopAnswer(text);
32961
+ return text || "_(no response)_";
32962
+ } catch (err2) {
32963
+ turnFailed = true;
32964
+ throw err2;
32965
+ } finally {
32966
+ live.toolContext = runContext;
32967
+ bridge.under(void 0);
32968
+ turnSpan?.end(hardStopped || bypass || turnFailed ? "error" : "ok");
32969
+ }
32970
+ };
32971
+ return { answer, followUp, end };
32972
+ } catch (err2) {
32973
+ agentSpan?.end("error");
32974
+ await end();
32975
+ throw err2;
32719
32976
  }
32720
32977
  }
32721
32978
  var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CALL_SEEN_WAIT_MS, CALL_SEEN_TICK_MS, CONTINUE_PROMPT, PromptRefused, GateBypassed;
@@ -33346,37 +33603,49 @@ async function runDescriptionTurn(input) {
33346
33603
  turn.onEvent({ type: "run_note", kind: "description_turn", summary, at: systemClock() });
33347
33604
  turn.onProgress(`re-evaluating the description of ${t.repo}#${t.pr.number} at ${t.headSha.slice(0, 7)}`);
33348
33605
  let submitted;
33606
+ const followUpText = descriptionFollowUp(t);
33349
33607
  input.messages.push(
33350
33608
  { role: "assistant", content: [{ type: "text", text: input.answer }] },
33351
- { role: "user", content: [{ type: "text", text: descriptionFollowUp(t) }] }
33609
+ { role: "user", content: [{ type: "text", text: followUpText }] }
33352
33610
  );
33353
33611
  const agent = {
33354
33612
  ...turn.agent,
33355
33613
  maxTurns: Math.min(turn.agent.maxTurns, DESCRIPTION_TURN_MAX_TURNS),
33356
33614
  maxMinutes: Math.min(turn.agent.maxMinutes, DESCRIPTION_TURN_MAX_MINUTES)
33357
33615
  };
33616
+ const toolContext = {
33617
+ ...turn.toolContext,
33618
+ onPrDescription: (d) => {
33619
+ submitted = d;
33620
+ turn.toolContext.onPrDescription?.(d);
33621
+ }
33622
+ };
33358
33623
  try {
33359
- await runAgent({
33360
- provider: turn.provider,
33361
- model: turn.model,
33362
- agent,
33363
- messages: input.messages,
33364
- ...input.system !== void 0 ? { system: input.system } : {},
33365
- ...turn.effort !== void 0 ? { effort: turn.effort } : {},
33366
- ...input.span ? { span: input.span } : {},
33367
- ...turn.backend ? { backend: turn.backend } : {},
33368
- toolContext: {
33369
- ...turn.toolContext,
33370
- onPrDescription: (d) => {
33371
- submitted = d;
33372
- turn.toolContext.onPrDescription?.(d);
33373
- }
33374
- },
33375
- ...turn.extraTools && turn.extraTools.length > 0 ? { extraTools: turn.extraTools } : {},
33376
- onProgress: turn.onProgress,
33377
- onEvent: turn.onEvent,
33378
- control: turn.control
33379
- });
33624
+ if (turn.followUp) {
33625
+ await turn.followUp({
33626
+ text: followUpText,
33627
+ maxTurns: agent.maxTurns,
33628
+ maxMinutes: agent.maxMinutes,
33629
+ toolContext,
33630
+ ...input.span ? { span: input.span } : {}
33631
+ });
33632
+ } else {
33633
+ await runAgent({
33634
+ provider: turn.provider,
33635
+ model: turn.model,
33636
+ agent,
33637
+ messages: input.messages,
33638
+ ...input.system !== void 0 ? { system: input.system } : {},
33639
+ ...turn.effort !== void 0 ? { effort: turn.effort } : {},
33640
+ ...input.span ? { span: input.span } : {},
33641
+ ...turn.backend ? { backend: turn.backend } : {},
33642
+ toolContext,
33643
+ ...turn.extraTools && turn.extraTools.length > 0 ? { extraTools: turn.extraTools } : {},
33644
+ onProgress: turn.onProgress,
33645
+ onEvent: turn.onEvent,
33646
+ control: turn.control
33647
+ });
33648
+ }
33380
33649
  } catch (err2) {
33381
33650
  console.error(`[description-turn] ${logKey} turn failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
33382
33651
  }
@@ -33574,6 +33843,7 @@ async function runLoop2(deps, ctx) {
33574
33843
  let carried;
33575
33844
  let runFailed = false;
33576
33845
  let runDiagnosis;
33846
+ let piSession;
33577
33847
  const releaseWorkspace = (span) => round2.release({ hardStopped: run2.control.requested === "hard", ...span ? { span } : {} });
33578
33848
  const attachFile = io.attachFile?.bind(io);
33579
33849
  const uploadTicket = io.uploadTicket?.bind(io);
@@ -33676,7 +33946,7 @@ async function runLoop2(deps, ctx) {
33676
33946
  )
33677
33947
  )
33678
33948
  ];
33679
- answer = await runPiHarness(
33949
+ piSession = await runPiHarnessOpen(
33680
33950
  {
33681
33951
  container: deps.harness.containerFor?.(executor, profile.machine) ?? piContainerFor(executor, profile.machine),
33682
33952
  bearer: ctx.bearer,
@@ -33728,6 +33998,7 @@ async function runLoop2(deps, ctx) {
33728
33998
  } : {}
33729
33999
  }
33730
34000
  );
34001
+ answer = piSession.answer;
33731
34002
  } else {
33732
34003
  answer = await runAgent({
33733
34004
  provider,
@@ -33784,7 +34055,8 @@ async function runLoop2(deps, ctx) {
33784
34055
  onProgress,
33785
34056
  onEvent,
33786
34057
  control: run2.control,
33787
- ...round2.selection.backend ? { backend: round2.selection.backend } : {}
34058
+ ...round2.selection.backend ? { backend: round2.selection.backend } : {},
34059
+ ...piSession ? { followUp: piSession.followUp } : {}
33788
34060
  },
33789
34061
  fetchPrHead: deps.fetchPrHead ?? currentPrHeadSha,
33790
34062
  fetchPrCommits: deps.fetchPrCommits ?? prCommitsSince,
@@ -33864,7 +34136,8 @@ async function runLoop2(deps, ctx) {
33864
34136
  onProgress,
33865
34137
  onEvent,
33866
34138
  control: run2.control,
33867
- ...round2.selection.backend ? { backend: round2.selection.backend } : {}
34139
+ ...round2.selection.backend ? { backend: round2.selection.backend } : {},
34140
+ ...piSession ? { followUp: piSession.followUp } : {}
33868
34141
  },
33869
34142
  logKey: msg.threadKey
33870
34143
  })
@@ -33872,6 +34145,7 @@ async function runLoop2(deps, ctx) {
33872
34145
  if (!run2.control.hardSignal.aborted) await observeWorkspaceNow();
33873
34146
  }
33874
34147
  }
34148
+ await piSession?.end();
33875
34149
  if (prDescription) {
33876
34150
  registry.publish(run2.id, {
33877
34151
  type: "pr_description",
@@ -33932,6 +34206,7 @@ async function runLoop2(deps, ctx) {
33932
34206
  );
33933
34207
  } catch (err2) {
33934
34208
  runFailed = true;
34209
+ await piSession?.end();
33935
34210
  await root.span("post.workspace_release", (span) => releaseWorkspace(span));
33936
34211
  throw err2;
33937
34212
  } finally {
@@ -34022,7 +34297,7 @@ var init_runLoop = __esm({
34022
34297
  "use strict";
34023
34298
  init_profile();
34024
34299
  init_runner();
34025
- init_types2();
34300
+ init_provider();
34026
34301
  init_workspace();
34027
34302
  init_select();
34028
34303
  init_botHostContainer();
@@ -35099,7 +35374,11 @@ async function runShipBranch(deps, msg, io, ctx) {
35099
35374
  io.runStarted?.({ id: run2.id });
35100
35375
  const publishText = (type, text, source3) => {
35101
35376
  const redacted = redactSecrets(text);
35102
- registry.publish(run2.id, { type, text: redacted, ...source3 ? { source: source3 } : {}, at: clock3() });
35377
+ const body = { text: redacted, ...source3 ? { source: source3 } : {}, at: clock3() };
35378
+ registry.publish(
35379
+ run2.id,
35380
+ type === "input" ? { type, messageId: messageIdOf(msg, run2.id), ...body } : { type, ...body }
35381
+ );
35103
35382
  console.log(`[event] ${msg.threadKey} type=${type} bytes=${utf8ByteLength(redacted)}`);
35104
35383
  };
35105
35384
  const humanize = isMrkdwnChannel(msg.channelId);
@@ -35297,6 +35576,7 @@ var init_ship = __esm({
35297
35576
  init_run2();
35298
35577
  init_runRegistry();
35299
35578
  init_statusCardFrame();
35579
+ init_types4();
35300
35580
  }
35301
35581
  });
35302
35582
 
@@ -35571,6 +35851,16 @@ function sessionSeed(input) {
35571
35851
  const summary = compactions.length ? compactions[compactions.length - 1].entry.summary : void 0;
35572
35852
  const logFrom = start3 < 0 ? from + transcript.turns : from + start3 + compactions.length;
35573
35853
  const kept = start3 < 0 ? [] : transcript.messages.slice(start3).map(withoutThinking);
35854
+ if (kept.length > 0 && kept[0].role === "user") {
35855
+ const first = kept[0];
35856
+ const orphans = first.content.filter((p) => p.type === "tool_result").length;
35857
+ if (orphans > 0) {
35858
+ kept[0] = { ...first, content: first.content.filter((p) => p.type !== "tool_result") };
35859
+ notes.push(
35860
+ orphans === 1 ? "session seed: one tool result answering a call before the cut was dropped from the tail's first turn" : `session seed: ${orphans} tool results answering calls before the cut were dropped from the tail's first turn`
35861
+ );
35862
+ }
35863
+ }
35574
35864
  if (kept.length === 0) {
35575
35865
  notes.push(
35576
35866
  transcript.turns === 0 ? "session seed: the log's newest turn alone is over the seed budget \u2014 the run starts from the request" : "session seed: no user text turn in the tail within the seed budget \u2014 the run starts from the request"
@@ -36036,7 +36326,11 @@ async function dispatch(deps, msg, io, opts = {}) {
36036
36326
  const earlierStaged = threadAssets && agent.machine !== "none" ? threadAssets.then(
36037
36327
  (assets) => stageThreadArtifacts(
36038
36328
  assets.filter((a) => a.direction === "in"),
36039
- { nextIndex: nextStagedIndex, publish: (e) => registry.publish(runId3, e) }
36329
+ {
36330
+ nextIndex: nextStagedIndex,
36331
+ messageId: messageIdOf(msg, runId3),
36332
+ publish: (e) => registry.publish(runId3, e)
36333
+ }
36040
36334
  )
36041
36335
  ) : void 0;
36042
36336
  const reservation = await reserveRun(deps, {
@@ -36446,6 +36740,7 @@ var init_dispatcher = __esm({
36446
36740
  init_runsService();
36447
36741
  init_runRegistry();
36448
36742
  init_runEnding();
36743
+ init_types4();
36449
36744
  activeRuns = 0;
36450
36745
  }
36451
36746
  });
@@ -37583,7 +37878,7 @@ var OpenAICompatProvider;
37583
37878
  var init_openaiCompat = __esm({
37584
37879
  "../../src/providers/openaiCompat.ts"() {
37585
37880
  "use strict";
37586
- init_types2();
37881
+ init_chatMessage();
37587
37882
  init_secrets2();
37588
37883
  OpenAICompatProvider = class {
37589
37884
  name;
@@ -37827,7 +38122,7 @@ var ANTHROPIC_BASE_URL, ZERO_RATES, ZERO_USAGE, CONTEXT_WINDOW, PiAiProviders, P
37827
38122
  var init_piAi = __esm({
37828
38123
  "../../src/core/harness/piAi.ts"() {
37829
38124
  "use strict";
37830
- init_types2();
38125
+ init_provider();
37831
38126
  init_secrets2();
37832
38127
  init_clock();
37833
38128
  ANTHROPIC_BASE_URL = "https://api.anthropic.com";
@@ -38114,7 +38409,7 @@ var init_mcp2 = __esm({
38114
38409
  init_sealed();
38115
38410
  init_secretStore();
38116
38411
  init_service();
38117
- init_types3();
38412
+ init_types2();
38118
38413
  init_registry2();
38119
38414
  init_client();
38120
38415
  init_fake();
@@ -39232,6 +39527,7 @@ async function receiveSlackMessage(client, ev, span, policy) {
39232
39527
  userId: `${PLATFORM}:${ev.user}`,
39233
39528
  threadKey: `${PLATFORM}:${ev.channel}:${ev.threadTs}`,
39234
39529
  text: ev.text + note,
39530
+ messageId: ev.ts,
39235
39531
  channelName,
39236
39532
  userName,
39237
39533
  sourceUrl: team ? slackPermalink(team, ev.channel, ev.ts, ev.threadTs) : void 0,
@@ -43022,7 +43318,7 @@ var init_modelProxy = __esm({
43022
43318
  "use strict";
43023
43319
  init_anthropic();
43024
43320
  init_openaiCompat();
43025
- init_types2();
43321
+ init_provider();
43026
43322
  init_http();
43027
43323
  ANTHROPIC_MESSAGES_PATH = "/v1/messages";
43028
43324
  OPENAI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
@@ -43351,7 +43647,9 @@ async function runRelayedTool(harness, ask, opts = {}) {
43351
43647
  ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
43352
43648
  } : { ...base, publish: (e) => harness.emit(e) },
43353
43649
  ...opts.signal && !base.signal ? { signal: opts.signal } : {},
43354
- ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {}
43650
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {},
43651
+ // After the spreads, so the call's own id can never be shadowed by one on the base context.
43652
+ callId: ask.toolCallId
43355
43653
  };
43356
43654
  try {
43357
43655
  const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
@@ -43385,7 +43683,7 @@ var init_relay = __esm({
43385
43683
  "../../src/core/harness/pi/relay.ts"() {
43386
43684
  "use strict";
43387
43685
  init_tracingExecutor();
43388
- init_types2();
43686
+ init_chatMessage();
43389
43687
  init_awaitChildren();
43390
43688
  init_runEvents();
43391
43689
  init_toolRules();