@coreplane/switchboard 1.224.0 → 1.226.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/assets/config/config.example.yaml +5 -1
  2. package/dist/assets/deploy/cloudflare-resident/Dockerfile +47 -12
  3. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +28 -9
  4. package/dist/assets/package-lock.json +980 -4
  5. package/dist/assets/package.json +2 -1
  6. package/dist/assets/source.json +3 -3
  7. package/dist/assets/src/agents/registry.ts +1 -1
  8. package/dist/assets/src/core/chatMessage.ts +77 -0
  9. package/dist/assets/src/core/provider.ts +111 -0
  10. package/dist/assets/src/core/reviewVerdict.ts +1 -1
  11. package/dist/assets/src/core/runEvents.ts +22 -3
  12. package/dist/assets/src/core/runLedger/sessionLog.ts +1 -1
  13. package/dist/assets/src/core/runLedger/transcript.ts +1 -1
  14. package/dist/assets/src/core/runLedger/types.ts +2 -1
  15. package/dist/assets/web/dist/.vite/manifest.json +20 -20
  16. package/dist/assets/web/dist/assets/CostsPage-5pl3HB2F.js +2 -0
  17. package/dist/assets/web/dist/assets/{ResidentDetailPage-BXwWQRJt.js → ResidentDetailPage-BBOpejGX.js} +1 -1
  18. package/dist/assets/web/dist/assets/{ResidentsIndexPage-CwVN-3tl.js → ResidentsIndexPage-B8kFWHpB.js} +1 -1
  19. package/dist/assets/web/dist/assets/RunRoutePage-COWeVtIE.js +12 -0
  20. package/dist/assets/web/dist/assets/{RunsIndexPage-Tx5P767G.js → RunsIndexPage-BjH93cKx.js} +1 -1
  21. package/dist/assets/web/dist/assets/{ScheduledPage-C80zFu3G.js → ScheduledPage-grlNKvCA.js} +1 -1
  22. package/dist/assets/web/dist/assets/{StatusDot-BaGkjYs1.js → StatusDot-BQNoaXO6.js} +1 -1
  23. package/dist/assets/web/dist/assets/{Tooltip-BL-Cwe59.js → Tooltip-DVCeIzaa.js} +1 -1
  24. package/dist/assets/web/dist/assets/{dist-DZgVjemt.js → dist-B5Wfk-oY.js} +1 -1
  25. package/dist/assets/web/dist/assets/{main-BB6FMkeU.js → main-CN0U7d6s.js} +2 -2
  26. package/dist/assets/web/dist/assets/main-xLAsdkfB.css +1 -0
  27. package/dist/cli.js +1509 -953
  28. package/package.json +2 -1
  29. package/dist/assets/src/providers/types.ts +0 -158
  30. package/dist/assets/web/dist/assets/CostsPage-C-ximW1N.js +0 -2
  31. package/dist/assets/web/dist/assets/RunRoutePage-C0-RUb8o.js +0 -12
  32. package/dist/assets/web/dist/assets/main-43n8Y5qO.css +0 -1
package/dist/cli.js CHANGED
@@ -11448,6 +11448,22 @@ var init_config3 = __esm({
11448
11448
  }
11449
11449
  });
11450
11450
 
11451
+ // ../../src/core/provider.ts
11452
+ function parseModelRef(ref) {
11453
+ const i = ref.indexOf("/");
11454
+ if (i === -1) {
11455
+ throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>", e.g. "anthropic/claude-opus-5"`);
11456
+ }
11457
+ return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
11458
+ }
11459
+ var ANTHROPIC_API_KEY_ENV;
11460
+ var init_provider = __esm({
11461
+ "../../src/core/provider.ts"() {
11462
+ "use strict";
11463
+ ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
11464
+ }
11465
+ });
11466
+
11451
11467
  // ../../src/providers/anthropic.ts
11452
11468
  import Anthropic from "@anthropic-ai/sdk";
11453
11469
  function anthropicApiKey(providers, secrets) {
@@ -11568,12 +11584,12 @@ function usageFromAnthropic(u) {
11568
11584
  if (typeof o.cache_creation_input_tokens === "number") usage.cacheWriteTokens = o.cache_creation_input_tokens;
11569
11585
  return usage;
11570
11586
  }
11571
- var ANTHROPIC_API_KEY_ENV, AnthropicProvider, EPHEMERAL, CACHEABLE_BLOCKS;
11587
+ var AnthropicProvider, EPHEMERAL, CACHEABLE_BLOCKS;
11572
11588
  var init_anthropic = __esm({
11573
11589
  "../../src/providers/anthropic.ts"() {
11574
11590
  "use strict";
11575
11591
  init_secrets2();
11576
- ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
11592
+ init_provider();
11577
11593
  AnthropicProvider = class {
11578
11594
  name;
11579
11595
  client;
@@ -11671,6 +11687,8 @@ function parseCostsConfig(raw) {
11671
11687
  const r = raw;
11672
11688
  if (typeof r.cloudflareAccountId !== "string" || !r.cloudflareAccountId)
11673
11689
  throw new Error("costs.cloudflareAccountId is required");
11690
+ if (r.cloudflareAccountName !== void 0 && typeof r.cloudflareAccountName !== "string")
11691
+ throw new Error("costs.cloudflareAccountName must be a string when given");
11674
11692
  if (!r.groups || typeof r.groups !== "object")
11675
11693
  throw new Error("costs.groups must be a mapping of group \u2192 { workers, containerApps }");
11676
11694
  const groups = {};
@@ -11693,6 +11711,7 @@ function parseCostsConfig(raw) {
11693
11711
  }
11694
11712
  return {
11695
11713
  cloudflareAccountId: r.cloudflareAccountId,
11714
+ ...typeof r.cloudflareAccountName === "string" && r.cloudflareAccountName ? { cloudflareAccountName: r.cloudflareAccountName } : {},
11696
11715
  cloudflareTokenEnv: typeof r.cloudflareTokenEnv === "string" ? r.cloudflareTokenEnv : DEFAULT_CF_TOKEN_ENV,
11697
11716
  anthropicAdminKeyEnv: typeof r.anthropicAdminKeyEnv === "string" ? r.anthropicAdminKeyEnv : DEFAULT_ANTHROPIC_ADMIN_ENV,
11698
11717
  groups
@@ -11772,7 +11791,7 @@ function eachDay(range) {
11772
11791
  for (let d = range.from; d <= range.to; d = addDays2(d, 1)) out.push(d);
11773
11792
  return out;
11774
11793
  }
11775
- function buildCostReport(group, cfg, usage, llm, range) {
11794
+ function buildCostReport(group, cfg, usage, llm, range, meta) {
11776
11795
  const attribution = attributionOf(cfg, usage);
11777
11796
  const workers = new Set(cfg.workers);
11778
11797
  const namespaces = attribution.durableObjectNamespaces;
@@ -11890,7 +11909,12 @@ function buildCostReport(group, cfg, usage, llm, range) {
11890
11909
  llmAvailable: llm !== null && !!cfg.anthropicWorkspaceId,
11891
11910
  days,
11892
11911
  totals: { cloudUsd, llmUsd, total: cloudUsd + llmUsd, byResource },
11893
- account: { cloudUsd: accountCloudUsd },
11912
+ generatedAt: meta.generatedAt ?? systemClock(),
11913
+ account: {
11914
+ id: meta.accountId,
11915
+ ...meta.accountName ? { name: meta.accountName } : {},
11916
+ cloudUsd: accountCloudUsd
11917
+ },
11894
11918
  attribution
11895
11919
  };
11896
11920
  }
@@ -11931,9 +11955,14 @@ function createCostsService(cfg, cloudflare, llm, now = () => new Date(systemClo
11931
11955
  async report(group, daysParam) {
11932
11956
  const g = cfg.groups[group];
11933
11957
  if (!g) throw new Error(`unknown cost group ${group}`);
11934
- const range = resolveRange(daysParam, now());
11958
+ const at = now();
11959
+ const range = resolveRange(daysParam, at);
11935
11960
  const [usage, llmRows] = await Promise.all([cloudflare.fetchUsage(range), llm.fetchDailyCost(range)]);
11936
- return buildCostReport(group, g, usage, llmRows, range);
11961
+ return buildCostReport(group, g, usage, llmRows, range, {
11962
+ accountId: cfg.cloudflareAccountId,
11963
+ accountName: cfg.cloudflareAccountName,
11964
+ generatedAt: at.getTime()
11965
+ });
11937
11966
  }
11938
11967
  };
11939
11968
  }
@@ -15906,7 +15935,7 @@ function isRunEvent(v) {
15906
15935
  case "skill_use":
15907
15936
  return typeof o.skill === "string" && typeof o.agent === "string" && typeof o.bodyBytes === "number";
15908
15937
  case "artifact":
15909
- 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;
15910
15939
  case "review_artifact":
15911
15940
  if (o.artifact === "pr_description") {
15912
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);
@@ -18065,23 +18094,31 @@ var init_service = __esm({
18065
18094
  callbackUrl() {
18066
18095
  return `${(this.opts.publicBaseUrl ?? "").replace(/\/+$/, "")}/mcp/oauth/callback`;
18067
18096
  }
18068
- /** 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. */
18069
18102
  refreshing = /* @__PURE__ */ new Map();
18070
18103
  /** A usable access token for the run: the stored one while it lives, else
18071
18104
  * a refreshed set (stored back, the cached client dropped so it is rebuilt
18072
18105
  * with the new header). */
18073
18106
  freshOAuth(serverId, cred) {
18074
18107
  if (!needsRefresh(cred, this.now())) return Promise.resolve(cred);
18075
- let inflight = this.refreshing.get(serverId);
18076
- if (!inflight) {
18077
- inflight = (async () => {
18078
- const next = await refreshCredential(this.opts.fetch, cred, this.now());
18079
- await this.storeOAuth(serverId, next);
18080
- return next;
18081
- })().finally(() => this.refreshing.delete(serverId));
18082
- this.refreshing.set(serverId, inflight);
18083
- }
18084
- 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;
18085
18122
  }
18086
18123
  // ---- the run-time view ----------------------------------------------------------
18087
18124
  /** Servers a run may use: every tier's entries whose agents include the
@@ -18527,40 +18564,6 @@ var init_mcp = __esm({
18527
18564
  }
18528
18565
  });
18529
18566
 
18530
- // ../../src/providers/types.ts
18531
- function toolResultText(content) {
18532
- if (typeof content === "string") return content;
18533
- return content.map((p) => {
18534
- if (p.type === "text") return p.text;
18535
- if (p.type === "image") return `[image ${p.mediaType}, ${base64Bytes(p.data)} bytes]`;
18536
- return `[document ${p.name ?? "document"} (${p.mediaType}), ${base64Bytes(p.data)} bytes]`;
18537
- }).join("\n");
18538
- }
18539
- function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
18540
- const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
18541
- \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
18542
- if (typeof content === "string") return capText(content);
18543
- return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
18544
- }
18545
- function base64Bytes(b64) {
18546
- const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
18547
- return Math.floor(b64.length * 3 / 4) - padding;
18548
- }
18549
- function parseModelRef(ref) {
18550
- const i = ref.indexOf("/");
18551
- if (i === -1) {
18552
- throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>", e.g. "anthropic/claude-opus-5"`);
18553
- }
18554
- return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
18555
- }
18556
- var MAX_TOOL_RESULT_CHARS;
18557
- var init_types3 = __esm({
18558
- "../../src/providers/types.ts"() {
18559
- "use strict";
18560
- MAX_TOOL_RESULT_CHARS = 12e4;
18561
- }
18562
- });
18563
-
18564
18567
  // ../../src/core/memory/scorer.ts
18565
18568
  function tokenize2(text) {
18566
18569
  return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
@@ -18827,7 +18830,7 @@ var init_authz = __esm({
18827
18830
  });
18828
18831
 
18829
18832
  // ../../src/core/llmOutput/types.ts
18830
- var init_types4 = __esm({
18833
+ var init_types3 = __esm({
18831
18834
  "../../src/core/llmOutput/types.ts"() {
18832
18835
  "use strict";
18833
18836
  }
@@ -18886,10 +18889,26 @@ var init_markdown = __esm({
18886
18889
  });
18887
18890
 
18888
18891
  // ../../src/core/llmOutput/json.ts
18889
- function stripJsonFence(text) {
18890
- const trimmed = text.trim();
18891
- const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
18892
- 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);
18893
18912
  }
18894
18913
  function jsonValueEquals(a, b) {
18895
18914
  if (Object.is(a, b)) return true;
@@ -18909,7 +18928,7 @@ function jsonOutput(schema, opts = {}) {
18909
18928
  parse(raw) {
18910
18929
  let value;
18911
18930
  try {
18912
- value = JSON.parse(stripJsonFence(raw));
18931
+ value = JSON.parse(extractJson(raw));
18913
18932
  } catch (err2) {
18914
18933
  const observed = `not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`;
18915
18934
  return { ok: false, failure: { kind: "syntax", observed } };
@@ -18942,7 +18961,7 @@ var init_json = __esm({
18942
18961
  var init_llmOutput = __esm({
18943
18962
  "../../src/core/llmOutput/index.ts"() {
18944
18963
  "use strict";
18945
- init_types4();
18964
+ init_types3();
18946
18965
  init_markdown();
18947
18966
  init_json();
18948
18967
  }
@@ -19071,6 +19090,8 @@ function placeCandidate(cand, actor, origin, keys, scopeOf) {
19071
19090
  async function reflect(deps) {
19072
19091
  const warn = deps.onWarn ?? (() => {
19073
19092
  });
19093
+ const info = deps.onInfo ?? (() => {
19094
+ });
19074
19095
  try {
19075
19096
  const query = `${deps.request} ${deps.answer}`.slice(0, MAX_RETRIEVE_QUERY_CHARS);
19076
19097
  const existing = (await Promise.all(
@@ -19088,11 +19109,18 @@ async function reflect(deps) {
19088
19109
  const reply = result.content.filter((p) => p.type === "text").map((p) => p.text).join("");
19089
19110
  const prov = { sourceThreadKey: deps.sourceThreadKey, sourceRunId: deps.sourceRunId };
19090
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}`);
19091
19114
  if (!parsed.ok) {
19092
- 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}`);
19093
19122
  return;
19094
19123
  }
19095
- if (parsed.candidates.length === 0) return;
19096
19124
  const scopeOf = new Map(existing.map((r) => [r.id, r.scopeKey]));
19097
19125
  const origin = deps.originChannelVisibility ?? "unknown";
19098
19126
  const byScope = /* @__PURE__ */ new Map();
@@ -19121,6 +19149,10 @@ async function reflect(deps) {
19121
19149
  if (dropped.length > 0)
19122
19150
  warn(`write denied by policy, ${dropped.length} candidate(s) dropped: ${dropped.join(", ")}`);
19123
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}`);
19124
19156
  } catch (err2) {
19125
19157
  warn(`reflection failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
19126
19158
  }
@@ -19332,6 +19364,7 @@ var init_buildStore2 = __esm({
19332
19364
  function scheduleReflection(input) {
19333
19365
  if (!input.cfg?.enabled || !shouldReflect(input.gate)) return;
19334
19366
  const warn = (m) => console.warn(`[memory] ${input.threadKey} ${m}`);
19367
+ const info = (m) => console.log(`[memory] ${input.threadKey} ${m}`);
19335
19368
  let provider;
19336
19369
  let model;
19337
19370
  try {
@@ -19355,7 +19388,8 @@ function scheduleReflection(input) {
19355
19388
  answer: input.answer,
19356
19389
  sourceThreadKey: input.threadKey,
19357
19390
  sourceRunId: input.runId,
19358
- onWarn: warn
19391
+ onWarn: warn,
19392
+ onInfo: info
19359
19393
  })
19360
19394
  );
19361
19395
  }
@@ -19383,7 +19417,7 @@ async function memoryContextBlock(organization, cfg, injected, query, userId, sc
19383
19417
  var init_memory = __esm({
19384
19418
  "../../src/core/memory/index.ts"() {
19385
19419
  "use strict";
19386
- init_types3();
19420
+ init_provider();
19387
19421
  init_scorer();
19388
19422
  init_scope();
19389
19423
  init_stores();
@@ -22578,7 +22612,9 @@ function afterReply(deps, ctx) {
22578
22612
  scheduleReflection({
22579
22613
  cfg: deps.config.config.memory,
22580
22614
  store: deps.memory,
22581
- providers: deps.providers,
22615
+ // The extractor's one call goes through pi's model library
22616
+ // (harness-pi.md item 13), never the loop's own provider adapters.
22617
+ providers: deps.completions,
22582
22618
  runModelRef: resolved.modelRef,
22583
22619
  gate: { toolCalls, historyTurns: history.length, agentName: resolved.agentName },
22584
22620
  threadKey: msg.threadKey,
@@ -22625,6 +22661,7 @@ function durableInboxMessage(msg, text, at, from) {
22625
22661
  ...msg.userName !== void 0 ? { userName: msg.userName } : {},
22626
22662
  ...msg.sourceUrl !== void 0 ? { sourceUrl: msg.sourceUrl } : {},
22627
22663
  ...msg.channelName !== void 0 ? { channelName: msg.channelName } : {},
22664
+ ...msg.messageId !== void 0 ? { messageId: msg.messageId } : {},
22628
22665
  ...from !== void 0 ? { fromRunId: from.runId } : {},
22629
22666
  // Staged references are metadata (record 0033) — a few hundred bytes each —
22630
22667
  // so they ride the base row and survive the restart whatever the
@@ -22665,6 +22702,7 @@ function messageFromInbox(stored, fallbackAt) {
22665
22702
  const userName = str7("userName");
22666
22703
  const sourceUrl = str7("sourceUrl");
22667
22704
  const channelName = str7("channelName");
22705
+ const messageId = str7("messageId");
22668
22706
  const fromRunId = str7("fromRunId");
22669
22707
  const at = typeof m.at === "number" && Number.isFinite(m.at) ? m.at : fallbackAt;
22670
22708
  const images = attachmentsFromInbox(m.images);
@@ -22681,6 +22719,7 @@ ${note}` : text,
22681
22719
  ...userName !== void 0 ? { userName } : {},
22682
22720
  ...sourceUrl !== void 0 ? { sourceUrl } : {},
22683
22721
  ...channelName !== void 0 ? { channelName } : {},
22722
+ ...messageId !== void 0 ? { messageId } : {},
22684
22723
  ...images ? { images } : {},
22685
22724
  ...documents ? { documents } : {},
22686
22725
  ...staged ? { staged } : {}
@@ -22719,6 +22758,9 @@ var init_inboxMessage = __esm({
22719
22758
  });
22720
22759
 
22721
22760
  // ../../src/core/threadAdmission.ts
22761
+ function followUpMessageId(input) {
22762
+ return input.messageId ?? (input.ledgerSeq !== void 0 ? `inbox-${input.ledgerSeq}` : `at-${input.at}`);
22763
+ }
22722
22764
  function decideFollowUp(live, requested) {
22723
22765
  if (requested.agent !== void 0 && requested.agent !== live.agent)
22724
22766
  return { kind: "refuse", reason: "agent_mismatch", requestedAgent: requested.agent };
@@ -23111,6 +23153,7 @@ function followUpOf(msg, text, at, opts) {
23111
23153
  ...msg.images !== void 0 ? { images: msg.images } : {},
23112
23154
  ...msg.documents !== void 0 ? { documents: msg.documents } : {},
23113
23155
  ...msg.staged !== void 0 ? { staged: msg.staged } : {},
23156
+ ...msg.messageId !== void 0 ? { messageId: msg.messageId } : {},
23114
23157
  at,
23115
23158
  ...opts.ledgerSeq !== void 0 ? { ledgerSeq: opts.ledgerSeq } : {},
23116
23159
  ...opts.from !== void 0 ? { from: opts.from } : {},
@@ -24782,6 +24825,16 @@ var init_runRegistry = __esm({
24782
24825
  }
24783
24826
  });
24784
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
+
24785
24838
  // ../../src/core/dispatch/fastPath.ts
24786
24839
  function isInlineRunCommand(id) {
24787
24840
  return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove)$/.test(id);
@@ -24868,7 +24921,12 @@ async function runInlineCommandRun(deps, msg, command2, io, execute, ending, tra
24868
24921
  );
24869
24922
  trace.bindRun(run2.id, (e) => registry.publish(run2.id, e));
24870
24923
  io.runStarted?.({ id: run2.id });
24871
- 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
+ });
24872
24930
  registry.publish(run2.id, { type: "run_meta", agent: COMMAND_RUN_AGENT, traceId: root.traceId, at: clock3() });
24873
24931
  let result;
24874
24932
  try {
@@ -24953,6 +25011,7 @@ var init_fastPath = __esm({
24953
25011
  init_commandChat();
24954
25012
  init_commandSurface();
24955
25013
  init_runRegistry();
25014
+ init_types4();
24956
25015
  init_record();
24957
25016
  init_reply();
24958
25017
  }
@@ -25123,431 +25182,299 @@ var init_resolve = __esm({
25123
25182
  init_registry();
25124
25183
  init_profile();
25125
25184
  init_directives();
25126
- init_types3();
25185
+ init_provider();
25127
25186
  init_factory();
25128
25187
  init_githubRepoProbe();
25129
25188
  init_repoContext();
25130
25189
  }
25131
25190
  });
25132
25191
 
25133
- // ../../src/core/prDescription.ts
25134
- import { z as z20 } from "zod";
25135
- function parsePrDescription(input) {
25136
- return PrDescriptionSchema.parse(input);
25137
- }
25138
- function redactPrDescription(d) {
25139
- 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;
25140
25197
  }
25141
- function mapStringLeaves(value, fn) {
25142
- if (typeof value === "string") return fn(value);
25143
- if (Array.isArray(value)) return value.map((v) => mapStringLeaves(v, fn));
25144
- if (typeof value === "object" && value !== null) {
25145
- 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"]);
25146
25226
  }
25147
- return value;
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;
25148
25236
  }
25149
- function encodeGithubPathSegments(path) {
25150
- return path.split("/").map(encodeURIComponent).join("/");
25237
+ function threadKeySafe(threadKey) {
25238
+ return threadKey.replace(/:/g, "-").replace(SAFE, "_");
25151
25239
  }
25152
- function anchorUrl(ctx, a) {
25153
- return `https://github.com/${ctx.repo}/blob/${ctx.headSha}/${encodeGithubPathSegments(a.path)}#L${a.from}-L${a.to}`;
25240
+ function outboundKey(runId3, seq, name) {
25241
+ return `runs/${runId3}/out/${seq}-${safeBasename(name)}`;
25154
25242
  }
25155
- function cell2(s) {
25156
- return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
25243
+ function inboundKey(threadKey, messageTs, index, name) {
25244
+ return `threads/${threadKeySafe(threadKey)}/in/${messageTs}/${index}-${safeBasename(name)}`;
25157
25245
  }
25158
- function renderPrDescriptionMarkdown(desc, ctx) {
25159
- if (!REPO_RE.test(ctx.repo))
25160
- throw new Error(`renderPrDescriptionMarkdown: repo must be owner/name, got "${ctx.repo}"`);
25161
- if (!SHA_RE2.test(ctx.headSha))
25162
- throw new Error(`renderPrDescriptionMarkdown: headSha must be a full 40-char lowercase sha, got "${ctx.headSha}"`);
25163
- const out = [];
25164
- out.push("## TL;DR", "", desc.tldr, "");
25165
- out.push("## What & why", "", desc.whatWhy, "");
25166
- out.push("## Tour", "");
25167
- desc.tour.forEach((step, i) => {
25168
- out.push(`### ${i + 1}. ${step.title}`, "", step.description, "");
25169
- if (step.lookFor) out.push(`**Look for:** ${step.lookFor}`, "");
25170
- out.push(anchorUrl(ctx, step.anchor), "");
25171
- });
25172
- out.push(`### ${desc.tour.length + 1}. Remaining changes`, "");
25173
- if (desc.remaining.length === 0) out.push("- none \u2014 every touched file is covered by a step above", "");
25174
- else {
25175
- for (const r of desc.remaining) out.push(`- \`${r.path}\` \u2014 ${r.note}`);
25176
- out.push("");
25246
+ var SAFE;
25247
+ var init_keys = __esm({
25248
+ "../../src/artifacts/keys.ts"() {
25249
+ "use strict";
25250
+ SAFE = /[^A-Za-z0-9._-]+/g;
25177
25251
  }
25178
- out.push("## Decisions", "");
25179
- for (const d of desc.decisions) out.push(`- **${d.title.replace(/\.?$/, ".")}** ${d.rationale}`);
25180
- out.push("");
25181
- out.push("## Risks & implications", "", desc.risks, "");
25182
- out.push("## Validation", "");
25183
- if (desc.validation.summary) out.push(desc.validation.summary, "");
25184
- out.push("| Criterion | Proof |", "|---|---|");
25185
- for (const c of desc.validation.criteria) out.push(`| ${cell2(c.criterion)} | ${cell2(c.proof)} |`);
25186
- out.push("", GENERATED_FOOTER, "");
25187
- return out.join("\n");
25252
+ });
25253
+
25254
+ // ../../src/tools/attach.ts
25255
+ function transferBudgetMs(size) {
25256
+ return Math.ceil(size / TRANSFER_FLOOR_BYTES_PER_MS) + TRANSFER_SETUP_MS;
25188
25257
  }
25189
- function parsePrDescriptionMarkdown(body) {
25190
- const problems = [];
25191
- const lines = body.replace(/\r\n?/g, "\n").split("\n").filter((l) => l.trim() !== GENERATED_FOOTER);
25192
- const fenced = fenceMask(lines);
25193
- const sections = splitSections(lines, fenced, problems);
25194
- const description = { tour: [] };
25195
- const tldr = sections.get("tldr");
25196
- if (tldr) setProse(description, "tldr", tldr, problems);
25197
- else {
25198
- problems.push("no `## TL;DR` section \u2014 the tldr is the body's first paragraph");
25199
- const first = firstParagraph(lines, fenced);
25200
- if (first) description.tldr = first;
25258
+ function fileNameOf2(path) {
25259
+ const segments2 = path.split("/").filter(Boolean);
25260
+ return segments2[segments2.length - 1] ?? path;
25261
+ }
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)}`;
25264
+ }
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;
25268
+ }
25269
+ function postCommandFor(path, url) {
25270
+ return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
25271
+ }
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`;
25201
25286
  }
25202
- const whatWhy = sections.get("whatWhy");
25203
- if (whatWhy) setProse(description, "whatWhy", whatWhy, problems);
25204
- const tour = sections.get("tour");
25205
- if (tour) {
25206
- const parsed = parseTour(tour.lines, fenced.slice(tour.start, tour.start + tour.lines.length), problems);
25207
- description.tour = parsed.tour;
25208
- if (parsed.remaining) description.remaining = parsed.remaining;
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;
25209
25292
  }
25210
- const decisions = sections.get("decisions");
25211
- if (decisions) description.decisions = parseDecisions(decisions.lines, problems);
25212
- const risks = sections.get("risks");
25213
- if (risks) setProse(description, "risks", risks, problems);
25214
- const validation = sections.get("validation");
25215
- if (validation) description.validation = parseValidation(validation.lines, problems);
25216
- for (const [key, heading] of SECTIONS) {
25217
- if (key === "tldr" || sections.has(key)) continue;
25218
- problems.push(`no \`## ${heading}\` section`);
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`;
25219
25296
  }
25220
- return { description, complete: problems.length === 0, problems };
25221
- }
25222
- function fenceMask(lines) {
25223
- let open2;
25224
- return lines.map((l) => {
25225
- const m = /^\s*(`{3,}|~{3,})/.exec(l);
25226
- if (m && (open2 === void 0 || m[1][0] === open2[0])) {
25227
- const wasOpen = open2 !== void 0;
25228
- open2 = wasOpen ? void 0 : m[1];
25229
- return true;
25230
- }
25231
- return open2 !== void 0;
25232
- });
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;
25323
+ try {
25324
+ ticket = await ctx.uploadTicket({ name, size });
25325
+ } catch (err2) {
25326
+ return `error: the channel refused an upload ticket for ${name}: ${describe2(err2)}; ${kept}`;
25327
+ }
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}`;
25331
+ }
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}`;
25336
+ }
25337
+ return `attached ${name} (${size} bytes) to the conversation and the run page`;
25233
25338
  }
25234
- function splitSections(lines, fenced, problems) {
25235
- const byHeading = new Map(SECTIONS.map(([key, heading]) => [heading.toLowerCase(), key]));
25236
- const out = /* @__PURE__ */ new Map();
25237
- let current;
25238
- lines.forEach((line3, i) => {
25239
- const m = fenced[i] ? null : HEADING_RE.exec(line3);
25240
- if (m && m[1] === "##") {
25241
- const key = byHeading.get(m[2].replace(/\s+/g, " ").toLowerCase());
25242
- if (key === void 0)
25243
- current = void 0;
25244
- else if (out.has(key)) {
25245
- problems.push(`duplicate \`## ${SECTIONS.find(([k]) => k === key)[1]}\` section \u2014 the first one stands`);
25246
- current = void 0;
25247
- } else {
25248
- current = { start: i + 1, lines: [] };
25249
- out.set(key, current);
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`;
25250
25397
  }
25251
- return;
25252
- }
25253
- current?.lines.push(line3);
25254
- });
25255
- return out;
25398
+ };
25399
+ }
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);
25256
25406
  }
25257
- function firstParagraph(lines, fenced) {
25258
- const para = [];
25259
- for (let i = 0; i < lines.length; i++) {
25260
- const l = lines[i];
25261
- if (l.trim() === "") {
25262
- if (para.length) break;
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;
25263
25421
  continue;
25264
25422
  }
25265
- if (fenced[i] || para.length === 0 && HEADING_RE.test(l)) continue;
25266
- para.push(l.trim());
25423
+ bytes.push(s.charCodeAt(i) & 255);
25267
25424
  }
25268
- return para.length ? para.join("\n") : void 0;
25425
+ return Buffer.from(bytes).toString("utf8");
25269
25426
  }
25270
- function setProse(description, key, section, problems) {
25271
- const text = section.lines.join("\n").trim();
25272
- if (text === "") problems.push(`\`## ${SECTIONS.find(([k]) => k === key)[1]}\` section is empty`);
25273
- else description[key] = text;
25427
+ function unquotePath(raw) {
25428
+ const s = raw.trim();
25429
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return decodeGitQuoted(s.slice(1, -1));
25430
+ return s;
25274
25431
  }
25275
- function parseTour(lines, fenced, problems) {
25276
- const steps = [];
25277
- let preamble = "";
25278
- lines.forEach((line3, i) => {
25279
- const m = fenced[i] ? null : STEP_RE.exec(line3);
25280
- if (m) steps.push({ label: m[1] ?? String(steps.length + 1), title: m[2], lines: [] });
25281
- else if (steps.length) steps[steps.length - 1].lines.push(line3);
25282
- else preamble += line3;
25283
- });
25284
- if (preamble.trim() !== "") problems.push("Tour: text before the first `### N.` step is ignored");
25285
- const isRemaining = (title) => /^remaining changes$/i.test(title);
25286
- const tour = [];
25287
- let remaining;
25288
- for (const step of steps) {
25289
- if (isRemaining(step.title)) {
25290
- remaining = parseRemaining(step.lines, problems);
25291
- continue;
25292
- }
25293
- const parsed = parseStep(step, problems);
25294
- if (parsed) tour.push(parsed);
25432
+ function riskReasons(entry, paths) {
25433
+ const reasons = [];
25434
+ const candidates = [...new Set(paths.filter(Boolean))];
25435
+ const any = (re) => candidates.some((p) => re.test(p));
25436
+ if (any(MIGRATION_RE)) reasons.push("migration/schema");
25437
+ if (any(AUTH_RE)) reasons.push("auth/permission-sensitive");
25438
+ if (any(INFRA_RE)) reasons.push("infra/deploy config");
25439
+ if (entry.status === "deleted") reasons.push("whole-file deletion");
25440
+ if (candidates.some((p) => LOCKFILES.has(basename2(p).toLowerCase()))) reasons.push("lockfile");
25441
+ const churn = entry.adds + entry.dels;
25442
+ if (churn >= LARGE_CHURN) reasons.push(`large change (${churn} lines)`);
25443
+ return reasons;
25444
+ }
25445
+ function parseNumstat(text) {
25446
+ const out = [];
25447
+ for (const line3 of text.split("\n")) {
25448
+ const m = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line3);
25449
+ if (!m) continue;
25450
+ const binary = m[1] === "-" || m[2] === "-";
25451
+ out.push({
25452
+ adds: binary ? 0 : Number(m[1]),
25453
+ dels: binary ? 0 : Number(m[2]),
25454
+ binary,
25455
+ path: unquotePath(m[3])
25456
+ });
25295
25457
  }
25296
- if (!steps.some((s) => !isRemaining(s.title))) problems.push("Tour has no steps");
25297
- if (remaining === void 0) problems.push("Tour has no `Remaining changes` step");
25298
- return { tour, ...remaining ? { remaining } : {} };
25458
+ return out;
25299
25459
  }
25300
- function parseStep(step, problems) {
25301
- const where = `step ${step.label} (${step.title})`;
25302
- let link;
25303
- const descLines = [];
25304
- const lookForLines = [];
25305
- let inLookFor = false;
25306
- for (const line3 of step.lines) {
25307
- const m = PERMALINK_RE.exec(line3.trim());
25308
- if (m) {
25309
- if (link) problems.push(`${where}: more than one permalink \u2014 the first is the anchor`);
25310
- else link = m;
25311
- inLookFor = false;
25312
- continue;
25313
- }
25314
- const lf = LOOK_FOR_RE.exec(line3.trim());
25315
- if (lf) {
25316
- lookForLines.push(lf[1]);
25317
- inLookFor = true;
25318
- continue;
25319
- }
25320
- if (inLookFor) {
25321
- if (line3.trim() === "") inLookFor = false;
25322
- else lookForLines.push(line3.trim());
25460
+ function parseNameStatus(text) {
25461
+ const out = [];
25462
+ for (const line3 of text.split("\n")) {
25463
+ const m = /^([A-Z])\d*\t(.+)$/.exec(line3);
25464
+ if (!m) continue;
25465
+ const parts = m[2].split(" ").map(unquotePath);
25466
+ const code = m[1];
25467
+ if (code === "R" || code === "C") {
25468
+ const [oldPath, newPath] = parts;
25469
+ out.push({
25470
+ status: code === "R" ? "renamed" : "added",
25471
+ oldPath: oldPath ?? "",
25472
+ newPath: newPath ?? oldPath ?? ""
25473
+ });
25323
25474
  continue;
25324
25475
  }
25325
- descLines.push(line3);
25326
- }
25327
- if (!link) {
25328
- problems.push(`${where}: no permalink line \u2014 dropped`);
25329
- return void 0;
25330
- }
25331
- let path;
25332
- try {
25333
- path = link[2].split("/").map(decodeURIComponent).join("/");
25334
- } catch {
25335
- problems.push(`${where}: permalink path is not valid percent-encoding \u2014 dropped`);
25336
- return void 0;
25337
- }
25338
- const from = Number(link[3]);
25339
- const anchor = TourAnchorSchema.safeParse({ path, from, to: link[4] === void 0 ? from : Number(link[4]) });
25340
- if (!anchor.success) {
25341
- const issue = anchor.error.issues[0];
25342
- problems.push(`${where}: ${["anchor", ...issue.path].join(".")} ${issue.message} \u2014 dropped`);
25343
- return void 0;
25344
- }
25345
- const lookFor = lookForLines.join("\n").trim();
25346
- return {
25347
- title: step.title,
25348
- // A parsed step may carry an empty description (a human's heading +
25349
- // permalink): this is a read-back, not a submission the schema gates.
25350
- description: descLines.join("\n").replace(/\n{3,}/g, "\n\n").trim(),
25351
- ...lookFor ? { lookFor } : {},
25352
- anchor: { ...anchor.data, sha: link[1] }
25353
- };
25354
- }
25355
- function parseRemaining(lines, problems) {
25356
- const out = [];
25357
- for (const raw of lines) {
25358
- const line3 = raw.trim();
25359
- if (line3 === "" || REMAINING_NONE_RE.test(line3)) continue;
25360
- const m = REMAINING_LINE_RE.exec(line3);
25361
- if (m) out.push({ path: m[1], note: m[2].trim() });
25362
- else if (!line3.startsWith("-") && out.length) out[out.length - 1].note += `
25363
- ${line3}`;
25364
- else problems.push(`Remaining changes: unrecognized line \`${line3}\``);
25365
- }
25366
- return out;
25367
- }
25368
- function parseDecisions(lines, problems) {
25369
- const out = [];
25370
- for (const raw of lines) {
25371
- const line3 = raw.trim();
25372
- if (line3 === "") continue;
25373
- const m = DECISION_RE.exec(line3);
25374
- if (m) out.push({ title: m[1].replace(/\.$/, ""), rationale: m[2].trim() });
25375
- else if (!line3.startsWith("-") && out.length) out[out.length - 1].rationale += `
25376
- ${line3}`;
25377
- else problems.push(`Decisions: unrecognized line \`${line3}\``);
25378
- }
25379
- return out;
25380
- }
25381
- function parseValidation(lines, problems) {
25382
- const summary = [];
25383
- const criteria = [];
25384
- let header4 = false;
25385
- for (const raw of lines) {
25386
- const line3 = raw.trim();
25387
- if (!header4) {
25388
- if (TABLE_HEADER_RE.test(line3)) header4 = true;
25389
- else summary.push(raw);
25390
- continue;
25391
- }
25392
- if (line3 === "" || TABLE_RULE_RE.test(line3)) continue;
25393
- const cells = splitCells(line3);
25394
- if (cells.length >= 2) criteria.push({ criterion: cells[0], proof: cells[1] });
25395
- else problems.push(`Validation: unrecognized table line \`${line3}\``);
25396
- }
25397
- if (!header4) problems.push("Validation: no `| Criterion | Proof |` table");
25398
- const text = summary.join("\n").trim();
25399
- return { ...text ? { summary: text } : {}, criteria };
25400
- }
25401
- function splitCells(row) {
25402
- const cells = [];
25403
- let cur = "";
25404
- const inner = row.replace(/^\|/, "").replace(/\|$/, "");
25405
- for (let i = 0; i < inner.length; i++) {
25406
- const ch = inner[i];
25407
- if (ch === "\\" && i + 1 < inner.length) {
25408
- cur += inner[++i];
25409
- } else if (ch === "|") {
25410
- cells.push(cur.trim());
25411
- cur = "";
25412
- } else cur += ch;
25413
- }
25414
- cells.push(cur.trim());
25415
- return cells;
25416
- }
25417
- 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;
25418
- var init_prDescription = __esm({
25419
- "../../src/core/prDescription.ts"() {
25420
- "use strict";
25421
- init_redact();
25422
- line2 = z20.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
25423
- prose = z20.string().trim().min(1);
25424
- TourAnchorSchema = z20.object({
25425
- // Repo-relative: no leading `/`, no `..` SEGMENT (a filename like `a..b.ts` is fine).
25426
- path: z20.string().trim().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), "path must be repo-relative"),
25427
- from: z20.number().int().positive(),
25428
- to: z20.number().int().positive()
25429
- }).refine((a) => a.to >= a.from, "to must be >= from");
25430
- TourStepSchema = z20.object({
25431
- title: line2,
25432
- description: prose,
25433
- lookFor: prose.optional(),
25434
- anchor: TourAnchorSchema
25435
- });
25436
- PrDescriptionSchema = z20.object({
25437
- /** The PR title's single source. Metadata for the PR's own title field —
25438
- * never rendered into the body (GitHub shows the title itself). */
25439
- title: line2,
25440
- tldr: prose,
25441
- whatWhy: prose,
25442
- tour: z20.array(TourStepSchema).min(1),
25443
- /** Every touched file the Tour steps did not cover, one line each. */
25444
- remaining: z20.array(z20.object({ path: line2, note: prose })),
25445
- decisions: z20.array(z20.object({ title: line2, rationale: prose })).min(1),
25446
- risks: prose,
25447
- validation: z20.object({
25448
- summary: prose.optional(),
25449
- criteria: z20.array(z20.object({ criterion: prose, proof: prose })).min(1)
25450
- })
25451
- });
25452
- REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
25453
- SHA_RE2 = /^[0-9a-f]{40}$/;
25454
- GENERATED_FOOTER = "\u{1F916} Generated with [Claude Code](https://claude.com/claude-code)";
25455
- SECTIONS = [
25456
- ["tldr", "TL;DR"],
25457
- ["whatWhy", "What & why"],
25458
- ["tour", "Tour"],
25459
- ["decisions", "Decisions"],
25460
- ["risks", "Risks & implications"],
25461
- ["validation", "Validation"]
25462
- ];
25463
- PERMALINK_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/blob\/([0-9a-f]{7,40})\/([^#\s]+)#L(\d+)(?:-L(\d+))?$/;
25464
- HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
25465
- STEP_RE = /^###\s+(?:(\d+)\.\s+)?(.+?)\s*$/;
25466
- LOOK_FOR_RE = /^\*\*Look for:\*\*\s*(.*)$/;
25467
- REMAINING_LINE_RE = /^-\s+`([^`]+)`\s+—\s*(.*)$/;
25468
- REMAINING_NONE_RE = /^-\s+none\b/i;
25469
- DECISION_RE = /^-\s+\*\*(.+?)\*\*\s*(.*)$/;
25470
- TABLE_HEADER_RE = /^\|\s*criterion\s*\|\s*proof\s*\|$/i;
25471
- TABLE_RULE_RE = /^\|?\s*:?-{3,}:?\s*\|/;
25472
- }
25473
- });
25474
-
25475
- // ../../src/core/diffDigest.ts
25476
- function basename2(p) {
25477
- const i = p.lastIndexOf("/");
25478
- return i === -1 ? p : p.slice(i + 1);
25479
- }
25480
- function decodeGitQuoted(s) {
25481
- const bytes = [];
25482
- for (let i = 0; i < s.length; i++) {
25483
- if (s[i] === "\\" && i + 1 < s.length) {
25484
- const oct = s.slice(i + 1).match(/^[0-7]{1,3}/)?.[0];
25485
- if (oct) {
25486
- bytes.push(parseInt(oct, 8) & 255);
25487
- i += oct.length;
25488
- continue;
25489
- }
25490
- const simple = { t: 9, n: 10, r: 13, '"': 34, "\\": 92 };
25491
- const nx = s[i + 1];
25492
- bytes.push(nx in simple ? simple[nx] : nx.charCodeAt(0) & 255);
25493
- i += 1;
25494
- continue;
25495
- }
25496
- bytes.push(s.charCodeAt(i) & 255);
25497
- }
25498
- return Buffer.from(bytes).toString("utf8");
25499
- }
25500
- function unquotePath(raw) {
25501
- const s = raw.trim();
25502
- if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return decodeGitQuoted(s.slice(1, -1));
25503
- return s;
25504
- }
25505
- function riskReasons(entry, paths) {
25506
- const reasons = [];
25507
- const candidates = [...new Set(paths.filter(Boolean))];
25508
- const any = (re) => candidates.some((p) => re.test(p));
25509
- if (any(MIGRATION_RE)) reasons.push("migration/schema");
25510
- if (any(AUTH_RE)) reasons.push("auth/permission-sensitive");
25511
- if (any(INFRA_RE)) reasons.push("infra/deploy config");
25512
- if (entry.status === "deleted") reasons.push("whole-file deletion");
25513
- if (candidates.some((p) => LOCKFILES.has(basename2(p).toLowerCase()))) reasons.push("lockfile");
25514
- const churn = entry.adds + entry.dels;
25515
- if (churn >= LARGE_CHURN) reasons.push(`large change (${churn} lines)`);
25516
- return reasons;
25517
- }
25518
- function parseNumstat(text) {
25519
- const out = [];
25520
- for (const line3 of text.split("\n")) {
25521
- const m = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line3);
25522
- if (!m) continue;
25523
- const binary = m[1] === "-" || m[2] === "-";
25524
- out.push({
25525
- adds: binary ? 0 : Number(m[1]),
25526
- dels: binary ? 0 : Number(m[2]),
25527
- binary,
25528
- path: unquotePath(m[3])
25529
- });
25530
- }
25531
- return out;
25532
- }
25533
- function parseNameStatus(text) {
25534
- const out = [];
25535
- for (const line3 of text.split("\n")) {
25536
- const m = /^([A-Z])\d*\t(.+)$/.exec(line3);
25537
- if (!m) continue;
25538
- const parts = m[2].split(" ").map(unquotePath);
25539
- const code = m[1];
25540
- if (code === "R" || code === "C") {
25541
- const [oldPath, newPath] = parts;
25542
- out.push({
25543
- status: code === "R" ? "renamed" : "added",
25544
- oldPath: oldPath ?? "",
25545
- newPath: newPath ?? oldPath ?? ""
25546
- });
25547
- continue;
25548
- }
25549
- const p = parts[0] ?? "";
25550
- out.push({ status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", oldPath: p, newPath: p });
25476
+ const p = parts[0] ?? "";
25477
+ out.push({ status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", oldPath: p, newPath: p });
25551
25478
  }
25552
25479
  return out;
25553
25480
  }
@@ -25630,10 +25557,71 @@ var init_diffDigest = __esm({
25630
25557
  }
25631
25558
  });
25632
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
+
25633
25621
  // ../../src/tools/github.ts
25634
25622
  function repoOf(input) {
25635
25623
  const raw = String(input.repo ?? "").trim().replace(/\.git$/i, "");
25636
- if (!REPO_RE2.test(raw))
25624
+ if (!REPO_RE.test(raw))
25637
25625
  return {
25638
25626
  error: `repo must be an owner/name slug (got ${JSON.stringify(raw)}); github_repos lists the repos you can reach.`
25639
25627
  };
@@ -25681,12 +25669,12 @@ function writeGate(tool, ctx, repo) {
25681
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.`;
25682
25670
  return void 0;
25683
25671
  }
25684
- 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;
25685
25673
  var init_github = __esm({
25686
25674
  "../../src/tools/github.ts"() {
25687
25675
  "use strict";
25688
25676
  init_githubApi();
25689
- 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])?$/;
25690
25678
  UNAVAILABLE = "GitHub tools are not available in this context.";
25691
25679
  MAX_TREE_ENTRIES = 300;
25692
25680
  MAX_ISSUE_BODY_SHOWN = 6e3;
@@ -26043,275 +26031,6 @@ ${url}`;
26043
26031
  }
26044
26032
  });
26045
26033
 
26046
- // ../../src/tools/skills.ts
26047
- var listSkillsTool, useSkillTool;
26048
- var init_skills = __esm({
26049
- "../../src/tools/skills.ts"() {
26050
- "use strict";
26051
- listSkillsTool = {
26052
- sideEffectFree: true,
26053
- name: "list_skills",
26054
- 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.",
26055
- inputSchema: {
26056
- type: "object",
26057
- properties: {}
26058
- },
26059
- async run(_input, ctx) {
26060
- if (!ctx.skills) return "Skills are not available in this context.";
26061
- const metas = ctx.skills.list(ctx.agentName ?? "");
26062
- if (metas.length === 0) return "No skills are available to you.";
26063
- return metas.map((m) => `- ${m.name}: ${m.description}`).join("\n");
26064
- }
26065
- };
26066
- useSkillTool = {
26067
- sideEffectFree: true,
26068
- name: "use_skill",
26069
- 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.",
26070
- inputSchema: {
26071
- type: "object",
26072
- properties: {
26073
- name: { type: "string", description: "The skill name to load (e.g. code-review-and-quality)" }
26074
- },
26075
- required: ["name"]
26076
- },
26077
- async run(input, ctx) {
26078
- if (!ctx.skills) return "Skills are not available in this context.";
26079
- const name = String(input.name ?? "").trim();
26080
- if (!name) return "use_skill: provide a skill `name` (see list_skills).";
26081
- const skill = ctx.skills.get(name);
26082
- const inScope = skill && skill.agents.includes(ctx.agentName ?? "");
26083
- if (!skill || !inScope) {
26084
- const available = ctx.skills.list(ctx.agentName ?? "").map((m) => m.name);
26085
- return `use_skill: no skill named "${name}" is available to you.` + (available.length > 0 ? ` Available: ${available.join(", ")}.` : "");
26086
- }
26087
- ctx.publish?.({
26088
- type: "skill_use",
26089
- skill: skill.name,
26090
- description: skill.description,
26091
- agent: ctx.agentName ?? "",
26092
- ...skill.source ? { source: skill.source } : {},
26093
- ...skill.upstream ? { upstream: { repo: skill.upstream.repo, commit: skill.upstream.commit } } : {},
26094
- bodyBytes: Buffer.byteLength(skill.body, "utf8")
26095
- });
26096
- const footer = skill.source ? `
26097
-
26098
- _(Skill source: ${skill.source})_` : "";
26099
- return `# Skill: ${skill.name}
26100
-
26101
- ${skill.body}${footer}`;
26102
- }
26103
- };
26104
- }
26105
- });
26106
-
26107
- // ../../src/artifacts/contentType.ts
26108
- function contentTypeFor(name) {
26109
- const dot = name.lastIndexOf(".");
26110
- if (dot < 0 || dot === name.length - 1) return OCTET_STREAM;
26111
- return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? OCTET_STREAM;
26112
- }
26113
- var BY_EXTENSION, OCTET_STREAM, INLINE_IMAGE_TYPES;
26114
- var init_contentType = __esm({
26115
- "../../src/artifacts/contentType.ts"() {
26116
- "use strict";
26117
- BY_EXTENSION = {
26118
- png: "image/png",
26119
- jpg: "image/jpeg",
26120
- jpeg: "image/jpeg",
26121
- gif: "image/gif",
26122
- webp: "image/webp",
26123
- svg: "image/svg+xml",
26124
- pdf: "application/pdf",
26125
- mp4: "video/mp4",
26126
- webm: "video/webm",
26127
- mov: "video/quicktime",
26128
- mp3: "audio/mpeg",
26129
- wav: "audio/wav",
26130
- txt: "text/plain",
26131
- md: "text/markdown",
26132
- csv: "text/csv",
26133
- json: "application/json",
26134
- html: "text/html",
26135
- zip: "application/zip",
26136
- gz: "application/gzip",
26137
- tar: "application/x-tar"
26138
- };
26139
- OCTET_STREAM = "application/octet-stream";
26140
- INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
26141
- }
26142
- });
26143
-
26144
- // ../../src/artifacts/keys.ts
26145
- function safeBasename(name) {
26146
- const segments2 = name.split(/[\\/]+/).filter((s) => s.length > 0);
26147
- const last = segments2[segments2.length - 1] ?? "";
26148
- const reduced = last.replace(SAFE, "_").replace(/\.{2,}/g, "_").replace(/_+/g, "_");
26149
- if (reduced === "" || /^[._-]+$/.test(reduced)) return "file";
26150
- return reduced;
26151
- }
26152
- function threadKeySafe(threadKey) {
26153
- return threadKey.replace(/:/g, "-").replace(SAFE, "_");
26154
- }
26155
- function outboundKey(runId3, seq, name) {
26156
- return `runs/${runId3}/out/${seq}-${safeBasename(name)}`;
26157
- }
26158
- function inboundKey(threadKey, messageTs, index, name) {
26159
- return `threads/${threadKeySafe(threadKey)}/in/${messageTs}/${index}-${safeBasename(name)}`;
26160
- }
26161
- var SAFE;
26162
- var init_keys = __esm({
26163
- "../../src/artifacts/keys.ts"() {
26164
- "use strict";
26165
- SAFE = /[^A-Za-z0-9._-]+/g;
26166
- }
26167
- });
26168
-
26169
- // ../../src/tools/attach.ts
26170
- function transferBudgetMs(size) {
26171
- return Math.ceil(size / TRANSFER_FLOOR_BYTES_PER_MS) + TRANSFER_SETUP_MS;
26172
- }
26173
- function fileNameOf2(path) {
26174
- const segments2 = path.split("/").filter(Boolean);
26175
- return segments2[segments2.length - 1] ?? path;
26176
- }
26177
- function putCommandFor(path, contentType, url) {
26178
- return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} -o /dev/null -w ${shellQuote(PUT_REPORT_FORMAT)} ${shellQuote(url)}`;
26179
- }
26180
- function parsePutReport(output) {
26181
- const m = /(\d{3}) (\d{1,15})\s*$/.exec(output);
26182
- return m ? { httpCode: Number(m[1]), sentBytes: Number(m[2]) } : null;
26183
- }
26184
- function postCommandFor(path, url) {
26185
- return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
26186
- }
26187
- async function attachThroughStore(path, name, lead, artifacts, ctx) {
26188
- const exec = (command2, timeoutMs2) => {
26189
- const opts = { timeoutMs: timeoutMs2, ...ctx.signal ? { signal: ctx.signal } : {} };
26190
- return ctx.executor.exec(command2, opts);
26191
- };
26192
- const statOut = await exec(statCommandFor(shellQuote(path)), STAT_TIMEOUT_MS);
26193
- if (parseExitPrefix(statOut).failed) return `error: could not read ${path}: ${statOut.trim()}`;
26194
- const size = parseByteSize(statOut);
26195
- if (size === null) return `error: could not measure ${path}: ${statOut.trim()}`;
26196
- if (size === 0) return `error: ${path} is empty \u2014 nothing to attach`;
26197
- if (size > MAX_ARTIFACT_BYTES) {
26198
- return `error: ${path} is ${size} bytes; attach_file takes files up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) \u2014 link to the file instead`;
26199
- }
26200
- let timeoutMs = BASH_TIMEOUT_MAX_MS;
26201
- if (ctx.remainingMs) {
26202
- const budget = bashBudgetWithinRun(BASH_TIMEOUT_MAX_MS, ctx.remainingMs());
26203
- if (budget.kind === "exhausted") return `error: ${budget.note}`;
26204
- if (budget.kind === "clipped") timeoutMs = budget.timeoutMs;
26205
- }
26206
- const needed = transferBudgetMs(size);
26207
- if (needed > timeoutMs) {
26208
- 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`;
26209
- }
26210
- const contentType = contentTypeFor(name);
26211
- const key = outboundKey(artifacts.runId, artifacts.nextSeq(), name);
26212
- const putUrl = await artifacts.store.presignPut(key, contentType);
26213
- const putOut = await exec(putCommandFor(path, contentType, putUrl), timeoutMs);
26214
- if (parseExitPrefix(putOut).failed) {
26215
- return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
26216
- }
26217
- const report2 = parsePutReport(putOut);
26218
- const curlSaid = report2 ? `; curl reported HTTP ${report2.httpCode} after sending ${report2.sentBytes} bytes` : "";
26219
- const head = await artifacts.store.head(key);
26220
- if (!head) {
26221
- return `error: the artifact store holds nothing under ${key} after the upload of ${name}${curlSaid} \u2014 nothing was posted`;
26222
- }
26223
- if (head.size !== size) {
26224
- return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
26225
- }
26226
- ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType });
26227
- if (!ctx.uploadTicket) {
26228
- const link = artifacts.artifactUrl?.(key);
26229
- const where = link ? ` \u2014 ${link}` : ` is on the run page as ${key}`;
26230
- await artifacts.reply(`${lead}
26231
- \u{1F4CE} ${name} (${size} bytes)${where}`);
26232
- 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`;
26233
- }
26234
- const kept = "the file is kept on the run page";
26235
- let ticket;
26236
- try {
26237
- ticket = await ctx.uploadTicket({ name, size });
26238
- } catch (err2) {
26239
- return `error: the channel refused an upload ticket for ${name}: ${describe2(err2)}; ${kept}`;
26240
- }
26241
- const postOut = await exec(postCommandFor(path, ticket.url), timeoutMs);
26242
- if (parseExitPrefix(postOut).failed) {
26243
- return `error: the upload of ${name} to the channel failed: ${postOut.trim()}; ${kept}`;
26244
- }
26245
- try {
26246
- await ticket.complete(lead);
26247
- } catch (err2) {
26248
- return `error: the channel refused to complete the upload of ${name}: ${describe2(err2)}; ${kept}`;
26249
- }
26250
- return `attached ${name} (${size} bytes) to the conversation and the run page`;
26251
- }
26252
- var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, PUT_REPORT_FORMAT, attachFileTool;
26253
- var init_attach = __esm({
26254
- "../../src/tools/attach.ts"() {
26255
- "use strict";
26256
- init_contentType();
26257
- init_keys();
26258
- init_runEvents();
26259
- init_bashTimeout();
26260
- init_binaryRead();
26261
- init_shellQuote();
26262
- MAX_ARTIFACT_BYTES = 1073741824;
26263
- TRANSFER_FLOOR_BYTES_PER_MS = 1024;
26264
- TRANSFER_SETUP_MS = 1e4;
26265
- STAT_TIMEOUT_MS = 3e4;
26266
- describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
26267
- secs = (ms) => Math.max(0, Math.round(ms / 1e3));
26268
- PUT_REPORT_FORMAT = "\\n%{http_code} %{size_upload}";
26269
- attachFileTool = {
26270
- name: "attach_file",
26271
- failsInText: true,
26272
- 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.`,
26273
- inputSchema: {
26274
- type: "object",
26275
- properties: {
26276
- path: { type: "string", description: "Relative path of the file in the workspace" },
26277
- comment: {
26278
- type: "string",
26279
- description: "One line posted with the file saying what it shows (default: the file name)"
26280
- }
26281
- },
26282
- required: ["path"]
26283
- },
26284
- async run(input, ctx) {
26285
- const path = String(input.path ?? "").trim();
26286
- if (!path) return "error: path is required";
26287
- const name = fileNameOf2(path);
26288
- const lead = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : name;
26289
- if (ctx.artifacts) return attachThroughStore(path, name, lead, ctx.artifacts, ctx);
26290
- if (!ctx.attach) {
26291
- return "attach_file is not available here: this conversation's channel takes no file uploads \u2014 link to the file instead";
26292
- }
26293
- const readBytes = ctx.executor.readBytes?.bind(ctx.executor);
26294
- if (!readBytes) {
26295
- return "attach_file is not available here: this workspace cannot hand files over \u2014 link to the file instead";
26296
- }
26297
- let bytes;
26298
- try {
26299
- bytes = await readBytes(path);
26300
- } catch (err2) {
26301
- return `error: could not read ${path}: ${describe2(err2)}`;
26302
- }
26303
- if (bytes.byteLength === 0) return `error: ${path} is empty \u2014 nothing to attach`;
26304
- try {
26305
- await ctx.attach({ name, bytes, lead });
26306
- } catch (err2) {
26307
- return `error: the channel refused the upload of ${name}: ${describe2(err2)}`;
26308
- }
26309
- return `attached ${name} (${bytes.byteLength} bytes) to the conversation`;
26310
- }
26311
- };
26312
- }
26313
- });
26314
-
26315
26034
  // ../../src/core/dispatch/awaitChildren.ts
26316
26035
  function decideWait(inputs) {
26317
26036
  const { children, now, budgetEndsAt, timeoutAt, stop, followUpPending } = inputs;
@@ -27077,7 +26796,8 @@ async function copyStaged(files, deps) {
27077
26796
  key,
27078
26797
  name: file.name,
27079
26798
  size: file.size,
27080
- contentType: file.type
26799
+ contentType: file.type,
26800
+ messageId: file.messageId
27081
26801
  });
27082
26802
  return { file, basename: basename4, key };
27083
26803
  } catch (err2) {
@@ -27136,7 +26856,8 @@ function stageThreadArtifacts(artifacts, deps) {
27136
26856
  key: a.key,
27137
26857
  name: a.name,
27138
26858
  size: a.size,
27139
- contentType: a.contentType
26859
+ contentType: a.contentType,
26860
+ messageId: deps.messageId
27140
26861
  });
27141
26862
  outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
27142
26863
  }
@@ -27442,145 +27163,444 @@ var init_session = __esm({
27442
27163
  }
27443
27164
  });
27444
27165
 
27445
- // ../../src/tools/workspace.ts
27446
- import { z as z21 } from "zod";
27447
- var bashTool, readFileTool, writeFileTool, NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool, submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, handoffEntry, submitHandoffTool, updateStatusTool, TOOLSETS;
27448
- var init_workspace = __esm({
27449
- "../../src/tools/workspace.ts"() {
27166
+ // ../../src/tools/skills.ts
27167
+ var listSkillsTool, useSkillTool;
27168
+ var init_skills = __esm({
27169
+ "../../src/tools/skills.ts"() {
27450
27170
  "use strict";
27451
- init_prDescription();
27452
- init_handoff();
27453
- init_reviewVerdict();
27454
- init_bashTimeout();
27455
- init_executor();
27456
- init_shellQuote();
27457
- init_diffDigest();
27458
- init_web();
27459
- init_github();
27460
- init_skills();
27461
- init_attach();
27462
- init_runs2();
27463
- init_session();
27464
- bashTool = {
27465
- name: "bash",
27466
- 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.",
27467
- inputSchema: {
27468
- type: "object",
27469
- properties: {
27470
- command: { type: "string", description: "The bash command to run" },
27471
- timeoutMs: {
27472
- type: "integer",
27473
- description: "Optional timeout in milliseconds for this command (default 300000 = 5 min; max 1200000 = 20 min). Values outside [1000, 1200000] are clamped."
27474
- }
27475
- },
27476
- required: ["command"]
27477
- },
27478
- async run(input, ctx) {
27479
- const requested = input.timeoutMs;
27480
- let timeoutMs = typeof requested === "number" && Number.isFinite(requested) ? clampBashTimeout(requested) : void 0;
27481
- let clipNote = "";
27482
- if (ctx.remainingMs) {
27483
- const budget = bashBudgetWithinRun(timeoutMs ?? BASH_TIMEOUT_MS, ctx.remainingMs());
27484
- if (budget.kind === "exhausted") return `exit 124:
27485
- ${budget.note}`;
27486
- if (budget.kind === "clipped") {
27487
- timeoutMs = budget.timeoutMs;
27488
- clipNote = budget.note;
27489
- }
27490
- }
27491
- const opts = {
27492
- ...ctx.signal ? { signal: ctx.signal } : {},
27493
- ...timeoutMs !== void 0 ? { timeoutMs } : {}
27494
- };
27495
- const out = await ctx.executor.exec(
27496
- String(input.command ?? ""),
27497
- opts.signal !== void 0 || opts.timeoutMs !== void 0 ? opts : void 0
27498
- );
27499
- return clipNote ? `${out}
27500
- ${clipNote}` : out;
27501
- }
27502
- };
27503
- readFileTool = {
27171
+ listSkillsTool = {
27504
27172
  sideEffectFree: true,
27505
- name: "read_file",
27506
- 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.",
27507
27175
  inputSchema: {
27508
27176
  type: "object",
27509
- properties: {
27510
- path: { type: "string", description: "Relative path to the file" }
27511
- },
27512
- required: ["path"]
27177
+ properties: {}
27513
27178
  },
27514
- run(input, ctx) {
27515
- 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");
27516
27184
  }
27517
27185
  };
27518
- writeFileTool = {
27519
- name: "write_file",
27520
- 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.",
27521
27190
  inputSchema: {
27522
27191
  type: "object",
27523
27192
  properties: {
27524
- path: { type: "string", description: "Relative path to the file" },
27525
- content: { type: "string", description: "Full file contents" }
27193
+ name: { type: "string", description: "The skill name to load (e.g. code-review-and-quality)" }
27526
27194
  },
27527
- required: ["path", "content"]
27195
+ required: ["name"]
27528
27196
  },
27529
- run(input, ctx) {
27530
- 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}`;
27531
27222
  }
27532
27223
  };
27533
- NAME_STATUS_MARK = "@@diff_digest:name-status@@";
27534
- NAME_STATUS_MARK_LINE = /^@@diff_digest:name-status@@$/m;
27535
- OUTPUT_CUT_RE = /^\.\.\.\[truncated \d+ chars\]$/m;
27536
- diffDigestTool = {
27537
- sideEffectFree: true,
27538
- name: "diff_digest",
27539
- 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).",
27540
27235
  inputSchema: {
27541
27236
  type: "object",
27542
27237
  properties: {
27543
- base: {
27238
+ checklist: {
27544
27239
  type: "string",
27545
- 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)"
27546
27241
  }
27547
- }
27242
+ },
27243
+ required: ["checklist"]
27548
27244
  },
27549
27245
  async run(input, ctx) {
27550
- const base = String(input.base ?? "").trim();
27551
- const baseName = base || "origin/HEAD";
27552
- if (base.startsWith("-")) {
27553
- return "diff_digest: base ref may not start with '-' (rejected to prevent git option injection).";
27554
- }
27555
- const baseExpr = base ? shellQuote(base) : '"$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo origin/main)"';
27556
- const range = `--end-of-options ${baseExpr}...HEAD`;
27557
- const listing = `git diff --numstat ${range} && echo ${NAME_STATUS_MARK} && git diff --name-status ${range}`;
27558
- let raw = await ctx.executor.exec(listing);
27559
- const incomplete = () => {
27560
- const reason = "the file listing exceeded the executor's output cap";
27561
- ctx.onDigest?.({ complete: false, base: baseName, reason });
27562
- 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\`).`;
27563
- };
27564
- if (OUTPUT_CUT_RE.test(raw)) return incomplete();
27565
- let marker = NAME_STATUS_MARK_LINE.exec(raw);
27566
- if (!marker) {
27567
- const shallow = (await ctx.executor.exec("git rev-parse --is-shallow-repository")).trim() === "true";
27568
- if (shallow) {
27569
- await ctx.executor.exec('git fetch --unshallow --quiet origin "+refs/heads/*:refs/remotes/origin/*"');
27570
- raw = await ctx.executor.exec(listing);
27571
- if (OUTPUT_CUT_RE.test(raw)) return incomplete();
27572
- marker = NAME_STATUS_MARK_LINE.exec(raw);
27573
- }
27574
- }
27575
- if (!marker) {
27576
- return `diff_digest: could not compute the diff (base \`${baseName}\`).
27577
- ${raw.trim()}`;
27578
- }
27579
- const digest = distillDiffStats(raw.slice(0, marker.index), raw.slice(marker.index + marker[0].length));
27580
- ctx.onDigest?.({ complete: true, base: baseName, totals: digest.totals });
27581
- return digest.text;
27246
+ ctx.reportProgress?.(String(input.checklist ?? ""));
27247
+ return "status updated";
27582
27248
  }
27583
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();
27584
27604
  submitVerdictTool = {
27585
27605
  name: "submit_verdict",
27586
27606
  failsInText: true,
@@ -27801,22 +27821,92 @@ ${raw.trim()}`;
27801
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`;
27802
27822
  }
27803
27823
  };
27804
- updateStatusTool = {
27805
- name: "update_status",
27806
- 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.",
27807
27846
  inputSchema: {
27808
27847
  type: "object",
27809
27848
  properties: {
27810
- checklist: {
27811
- type: "string",
27812
- 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."
27813
27853
  }
27814
27854
  },
27815
- required: ["checklist"]
27855
+ required: ["command"]
27816
27856
  },
27817
27857
  async run(input, ctx) {
27818
- ctx.reportProgress?.(String(input.checklist ?? ""));
27819
- 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 ?? ""));
27820
27910
  }
27821
27911
  };
27822
27912
  TOOLSETS = {
@@ -28206,7 +28296,7 @@ async function routeRequest(deps, ctx) {
28206
28296
  if (deps.routeModel) model = deps.routeModel;
28207
28297
  else {
28208
28298
  const ref = parseModelRef(modelRef2);
28209
- model = providerRouteModel(deps.providers.get(ref.provider), ref.model, {
28299
+ model = providerRouteModel(deps.completions.get(ref.provider), ref.model, {
28210
28300
  ...cfg.routing?.answer ? { answer: cfg.routing.answer } : {}
28211
28301
  });
28212
28302
  }
@@ -28276,7 +28366,7 @@ var init_route = __esm({
28276
28366
  init_registry();
28277
28367
  init_workspace();
28278
28368
  init_config2();
28279
- init_types3();
28369
+ init_provider();
28280
28370
  init_redact();
28281
28371
  init_statusCardFrame();
28282
28372
  init_spawn();
@@ -28293,6 +28383,33 @@ var init_route = __esm({
28293
28383
  }
28294
28384
  });
28295
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
+
28296
28413
  // ../../src/execution/tracingExecutor.ts
28297
28414
  var TracingExecutor;
28298
28415
  var init_tracingExecutor = __esm({
@@ -28491,7 +28608,12 @@ async function runLoop(opts, now, note, emit, agentSpan) {
28491
28608
  ...input.userName ? { user: input.userName } : {},
28492
28609
  ...input.from ? { run: input.from.runId } : {}
28493
28610
  };
28494
- 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
+ });
28495
28617
  note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
28496
28618
  }
28497
28619
  const prompt = followUpPrompt(inputs, { superseded });
@@ -28538,13 +28660,14 @@ ${stagedLine}` : prompt }];
28538
28660
  }
28539
28661
  const ctx = callSpan ? {
28540
28662
  ...toolContext,
28663
+ callId: tu.id,
28541
28664
  span: callSpan,
28542
28665
  executor: new TracingExecutor(execTracker, callSpan, opts.backend),
28543
28666
  publish: (e) => emit(withSpanId(e, callSpan.id)),
28544
28667
  // The GitHub client as a view under this call: its requests and any
28545
28668
  // token mint become `github.*` children (docs/reference/specs/tracing.md item 23).
28546
28669
  ...toolContext.github?.api.withSpan ? { github: { ...toolContext.github, api: toolContext.github.api.withSpan(callSpan) } } : {}
28547
- } : toolContext;
28670
+ } : { ...toolContext, callId: tu.id };
28548
28671
  try {
28549
28672
  const output = await untilHardStop(tool.run(tu.input ?? {}, ctx));
28550
28673
  const text2 = toolResultText(output);
@@ -28883,7 +29006,7 @@ var MAX_CONSECUTIVE_INFRA_FAILURES, HardStopError, HARD_STOP_MESSAGE, wrapUpNote
28883
29006
  var init_runner = __esm({
28884
29007
  "../../src/runner.ts"() {
28885
29008
  "use strict";
28886
- init_types3();
29009
+ init_chatMessage();
28887
29010
  init_runEvents();
28888
29011
  init_threadAdmission();
28889
29012
  init_executor();
@@ -29279,42 +29402,42 @@ async function settle(input, span) {
29279
29402
  }
29280
29403
  verdict = void 0;
29281
29404
  reviewHead = current;
29282
- 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
+ });
29283
29414
  input.messages.push(
29284
29415
  { role: "assistant", content: [{ type: "text", text: answer }] },
29285
- {
29286
- role: "user",
29287
- content: [
29288
- {
29289
- type: "text",
29290
- text: rereviewFollowUp({
29291
- where,
29292
- reviewed: expected,
29293
- current,
29294
- move,
29295
- before: classified.before,
29296
- after: classified.after,
29297
- worktreeMoved
29298
- })
29299
- }
29300
- ]
29301
- }
29416
+ { role: "user", content: [{ type: "text", text: followUpText }] }
29302
29417
  );
29303
- 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({
29304
29431
  provider: turn.provider,
29305
29432
  model: turn.model,
29306
29433
  agent: turn.agent,
29307
29434
  messages: input.messages,
29308
- system,
29435
+ // Recomposed at the new head, so the prompt does not contradict the follow-up.
29436
+ system: input.composeSystem({ sha: current, verified: worktreeMoved }),
29309
29437
  effort: turn.effort,
29310
29438
  ...span ? { span } : {},
29311
29439
  ...turn.backend ? { backend: turn.backend } : {},
29312
- toolContext: {
29313
- ...turn.toolContext,
29314
- onVerdict: (v) => {
29315
- verdict = v;
29316
- }
29317
- },
29440
+ toolContext,
29318
29441
  ...turn.extraTools && turn.extraTools.length > 0 ? { extraTools: turn.extraTools } : {},
29319
29442
  onProgress: turn.onProgress,
29320
29443
  onEvent: turn.onEvent,
@@ -30374,7 +30497,8 @@ async function registerRun(deps, ctx) {
30374
30497
  trace.bindRun(run2.id, (e) => registry.publish(run2.id, e));
30375
30498
  const publishText = (type, text, source3, raw) => {
30376
30499
  const redacted = redactSecrets(text);
30377
- 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 };
30378
30502
  const withRaw = raw !== void 0 ? { ...event, raw: redactSecrets(raw) } : event;
30379
30503
  registry.publish(run2.id, utf8ByteLength(JSON.stringify(withRaw)) <= MAX_EVENT_BYTES ? withRaw : event);
30380
30504
  console.log(`[event] ${msg.threadKey} type=${type} bytes=${utf8ByteLength(redacted)}`);
@@ -30673,7 +30797,7 @@ var init_provision = __esm({
30673
30797
  init_resident();
30674
30798
  init_memory();
30675
30799
  init_runBearers();
30676
- init_types3();
30800
+ init_provider();
30677
30801
  init_skills2();
30678
30802
  init_source();
30679
30803
  init_configAwareness();
@@ -30685,6 +30809,7 @@ var init_provision = __esm({
30685
30809
  init_inboxMessage();
30686
30810
  init_statusCardFrame();
30687
30811
  init_statusCoalescer();
30812
+ init_types4();
30688
30813
  init_record();
30689
30814
  init_reply();
30690
30815
  init_messages();
@@ -31649,6 +31774,7 @@ var init_bridge2 = __esm({
31649
31774
  PiBridge = class {
31650
31775
  constructor(deps) {
31651
31776
  this.deps = deps;
31777
+ this.agentSpan = deps.agentSpan;
31652
31778
  }
31653
31779
  deps;
31654
31780
  /** Model turns that did work, the guard's count — the loop's rule: a turn whose every call is `update_status` is bookkeeping. */
@@ -31670,6 +31796,24 @@ var init_bridge2 = __esm({
31670
31796
  * were vetted by the bot generation that died, and this one never heard. */
31671
31797
  judgeGate = true;
31672
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
+ }
31673
31817
  /** The gate saw this call: the extension's `tool_call` hook asked the bot
31674
31818
  * for it, whatever the answer. Called from the authorize route, before pi
31675
31819
  * runs the tool — so it always precedes the call's `tool_execution_end`. */
@@ -31778,7 +31922,7 @@ var init_bridge2 = __esm({
31778
31922
  onToolStart(event) {
31779
31923
  const tool = str5(event.toolName);
31780
31924
  const callId = str5(event.toolCallId);
31781
- const span = this.deps.agentSpan?.start(`tool.${tool}`);
31925
+ const span = this.agentSpan?.start(`tool.${tool}`);
31782
31926
  this.openTools.set(callId, { span, tool, judged: this.judgeGate });
31783
31927
  this.toolCalls++;
31784
31928
  const input = isRecord3(event.args) ? event.args : void 0;
@@ -32301,7 +32445,7 @@ function ledgerTailOf(resume) {
32301
32445
  function settlementAnswer(s) {
32302
32446
  return { content: [{ type: "text", text: settlementText(s) }], isError: true };
32303
32447
  }
32304
- async function runPiHarness(deps, run2) {
32448
+ async function runPiHarnessOpen(deps, run2) {
32305
32449
  const { container, clock: clock3 } = deps;
32306
32450
  const now = () => clock3();
32307
32451
  const agentSpan = run2.span?.start("run.agent");
@@ -32396,6 +32540,26 @@ async function runPiHarness(deps, run2) {
32396
32540
  save();
32397
32541
  };
32398
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
+ };
32399
32563
  try {
32400
32564
  const recorded = run2.resume?.facts;
32401
32565
  let reattached = false;
@@ -32535,7 +32699,12 @@ async function runPiHarness(deps, run2) {
32535
32699
  ...input.userName ? { user: input.userName } : {},
32536
32700
  ...input.from ? { run: input.from.runId } : {}
32537
32701
  };
32538
- 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
+ });
32539
32708
  note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
32540
32709
  }
32541
32710
  const images = inputs.flatMap(
@@ -32673,33 +32842,137 @@ ${stagedLine}` : prompt,
32673
32842
  check();
32674
32843
  if (hardStopped) break;
32675
32844
  }
32845
+ let answer;
32676
32846
  if (hardStopped) {
32677
32847
  note("stopped", hardStopNote(), "hard");
32678
- return HARD_STOP_MESSAGE;
32679
- }
32680
- if (bypass) throw bypass;
32681
- if (!settled) {
32682
- const tail2 = await container.tail(paths.errLog, 2e3);
32683
- throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
32684
- }
32685
- if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
32686
- const text = bridge.answer() ?? "";
32687
- if (writeUp?.kind === "time") return timeBudgetAnswer(text, run2.agent.maxMinutes);
32688
- if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace);
32689
- if (writeUp?.kind === "soft" || stopMode === "soft") return softStopAnswer(text);
32690
- return text || "_(no response)_";
32691
- } finally {
32692
- transport?.close();
32693
- bridge.closeOpenSpans(
32694
- hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
32695
- );
32696
- save();
32697
- forget();
32698
- if (pid !== void 0) await container.kill(pid).catch(() => {
32699
- });
32700
- if (paths !== void 0) await container.remove(paths).catch(() => {
32701
- });
32702
- 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;
32703
32976
  }
32704
32977
  }
32705
32978
  var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CALL_SEEN_WAIT_MS, CALL_SEEN_TICK_MS, CONTINUE_PROMPT, PromptRefused, GateBypassed;
@@ -33330,37 +33603,49 @@ async function runDescriptionTurn(input) {
33330
33603
  turn.onEvent({ type: "run_note", kind: "description_turn", summary, at: systemClock() });
33331
33604
  turn.onProgress(`re-evaluating the description of ${t.repo}#${t.pr.number} at ${t.headSha.slice(0, 7)}`);
33332
33605
  let submitted;
33606
+ const followUpText = descriptionFollowUp(t);
33333
33607
  input.messages.push(
33334
33608
  { role: "assistant", content: [{ type: "text", text: input.answer }] },
33335
- { role: "user", content: [{ type: "text", text: descriptionFollowUp(t) }] }
33609
+ { role: "user", content: [{ type: "text", text: followUpText }] }
33336
33610
  );
33337
33611
  const agent = {
33338
33612
  ...turn.agent,
33339
33613
  maxTurns: Math.min(turn.agent.maxTurns, DESCRIPTION_TURN_MAX_TURNS),
33340
33614
  maxMinutes: Math.min(turn.agent.maxMinutes, DESCRIPTION_TURN_MAX_MINUTES)
33341
33615
  };
33616
+ const toolContext = {
33617
+ ...turn.toolContext,
33618
+ onPrDescription: (d) => {
33619
+ submitted = d;
33620
+ turn.toolContext.onPrDescription?.(d);
33621
+ }
33622
+ };
33342
33623
  try {
33343
- await runAgent({
33344
- provider: turn.provider,
33345
- model: turn.model,
33346
- agent,
33347
- messages: input.messages,
33348
- ...input.system !== void 0 ? { system: input.system } : {},
33349
- ...turn.effort !== void 0 ? { effort: turn.effort } : {},
33350
- ...input.span ? { span: input.span } : {},
33351
- ...turn.backend ? { backend: turn.backend } : {},
33352
- toolContext: {
33353
- ...turn.toolContext,
33354
- onPrDescription: (d) => {
33355
- submitted = d;
33356
- turn.toolContext.onPrDescription?.(d);
33357
- }
33358
- },
33359
- ...turn.extraTools && turn.extraTools.length > 0 ? { extraTools: turn.extraTools } : {},
33360
- onProgress: turn.onProgress,
33361
- onEvent: turn.onEvent,
33362
- control: turn.control
33363
- });
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
+ }
33364
33649
  } catch (err2) {
33365
33650
  console.error(`[description-turn] ${logKey} turn failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
33366
33651
  }
@@ -33558,6 +33843,7 @@ async function runLoop2(deps, ctx) {
33558
33843
  let carried;
33559
33844
  let runFailed = false;
33560
33845
  let runDiagnosis;
33846
+ let piSession;
33561
33847
  const releaseWorkspace = (span) => round2.release({ hardStopped: run2.control.requested === "hard", ...span ? { span } : {} });
33562
33848
  const attachFile = io.attachFile?.bind(io);
33563
33849
  const uploadTicket = io.uploadTicket?.bind(io);
@@ -33660,7 +33946,7 @@ async function runLoop2(deps, ctx) {
33660
33946
  )
33661
33947
  )
33662
33948
  ];
33663
- answer = await runPiHarness(
33949
+ piSession = await runPiHarnessOpen(
33664
33950
  {
33665
33951
  container: deps.harness.containerFor?.(executor, profile.machine) ?? piContainerFor(executor, profile.machine),
33666
33952
  bearer: ctx.bearer,
@@ -33712,6 +33998,7 @@ async function runLoop2(deps, ctx) {
33712
33998
  } : {}
33713
33999
  }
33714
34000
  );
34001
+ answer = piSession.answer;
33715
34002
  } else {
33716
34003
  answer = await runAgent({
33717
34004
  provider,
@@ -33768,7 +34055,8 @@ async function runLoop2(deps, ctx) {
33768
34055
  onProgress,
33769
34056
  onEvent,
33770
34057
  control: run2.control,
33771
- ...round2.selection.backend ? { backend: round2.selection.backend } : {}
34058
+ ...round2.selection.backend ? { backend: round2.selection.backend } : {},
34059
+ ...piSession ? { followUp: piSession.followUp } : {}
33772
34060
  },
33773
34061
  fetchPrHead: deps.fetchPrHead ?? currentPrHeadSha,
33774
34062
  fetchPrCommits: deps.fetchPrCommits ?? prCommitsSince,
@@ -33848,7 +34136,8 @@ async function runLoop2(deps, ctx) {
33848
34136
  onProgress,
33849
34137
  onEvent,
33850
34138
  control: run2.control,
33851
- ...round2.selection.backend ? { backend: round2.selection.backend } : {}
34139
+ ...round2.selection.backend ? { backend: round2.selection.backend } : {},
34140
+ ...piSession ? { followUp: piSession.followUp } : {}
33852
34141
  },
33853
34142
  logKey: msg.threadKey
33854
34143
  })
@@ -33856,6 +34145,7 @@ async function runLoop2(deps, ctx) {
33856
34145
  if (!run2.control.hardSignal.aborted) await observeWorkspaceNow();
33857
34146
  }
33858
34147
  }
34148
+ await piSession?.end();
33859
34149
  if (prDescription) {
33860
34150
  registry.publish(run2.id, {
33861
34151
  type: "pr_description",
@@ -33916,6 +34206,7 @@ async function runLoop2(deps, ctx) {
33916
34206
  );
33917
34207
  } catch (err2) {
33918
34208
  runFailed = true;
34209
+ await piSession?.end();
33919
34210
  await root.span("post.workspace_release", (span) => releaseWorkspace(span));
33920
34211
  throw err2;
33921
34212
  } finally {
@@ -34006,7 +34297,7 @@ var init_runLoop = __esm({
34006
34297
  "use strict";
34007
34298
  init_profile();
34008
34299
  init_runner();
34009
- init_types3();
34300
+ init_provider();
34010
34301
  init_workspace();
34011
34302
  init_select();
34012
34303
  init_botHostContainer();
@@ -35083,7 +35374,11 @@ async function runShipBranch(deps, msg, io, ctx) {
35083
35374
  io.runStarted?.({ id: run2.id });
35084
35375
  const publishText = (type, text, source3) => {
35085
35376
  const redacted = redactSecrets(text);
35086
- 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
+ );
35087
35382
  console.log(`[event] ${msg.threadKey} type=${type} bytes=${utf8ByteLength(redacted)}`);
35088
35383
  };
35089
35384
  const humanize = isMrkdwnChannel(msg.channelId);
@@ -35281,6 +35576,7 @@ var init_ship = __esm({
35281
35576
  init_run2();
35282
35577
  init_runRegistry();
35283
35578
  init_statusCardFrame();
35579
+ init_types4();
35284
35580
  }
35285
35581
  });
35286
35582
 
@@ -35555,6 +35851,16 @@ function sessionSeed(input) {
35555
35851
  const summary = compactions.length ? compactions[compactions.length - 1].entry.summary : void 0;
35556
35852
  const logFrom = start3 < 0 ? from + transcript.turns : from + start3 + compactions.length;
35557
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
+ }
35558
35864
  if (kept.length === 0) {
35559
35865
  notes.push(
35560
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"
@@ -36020,7 +36326,11 @@ async function dispatch(deps, msg, io, opts = {}) {
36020
36326
  const earlierStaged = threadAssets && agent.machine !== "none" ? threadAssets.then(
36021
36327
  (assets) => stageThreadArtifacts(
36022
36328
  assets.filter((a) => a.direction === "in"),
36023
- { 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
+ }
36024
36334
  )
36025
36335
  ) : void 0;
36026
36336
  const reservation = await reserveRun(deps, {
@@ -36430,6 +36740,7 @@ var init_dispatcher = __esm({
36430
36740
  init_runsService();
36431
36741
  init_runRegistry();
36432
36742
  init_runEnding();
36743
+ init_types4();
36433
36744
  activeRuns = 0;
36434
36745
  }
36435
36746
  });
@@ -37567,7 +37878,7 @@ var OpenAICompatProvider;
37567
37878
  var init_openaiCompat = __esm({
37568
37879
  "../../src/providers/openaiCompat.ts"() {
37569
37880
  "use strict";
37570
- init_types3();
37881
+ init_chatMessage();
37571
37882
  init_secrets2();
37572
37883
  OpenAICompatProvider = class {
37573
37884
  name;
@@ -37675,6 +37986,241 @@ var init_registry3 = __esm({
37675
37986
  }
37676
37987
  });
37677
37988
 
37989
+ // ../../src/core/harness/piAi.ts
37990
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
37991
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
37992
+ function piApiFor(type) {
37993
+ return type === "anthropic" ? "anthropic-messages" : "openai-completions";
37994
+ }
37995
+ function piStreamOptions(api, req, key) {
37996
+ const shared = {
37997
+ ...key !== void 0 ? { apiKey: key } : { apiKey: "unused", headers: { Authorization: null } },
37998
+ maxTokens: req.maxTokens,
37999
+ ...req.signal ? { signal: req.signal } : {},
38000
+ cacheRetention: req.cacheTtl === "1h" ? "long" : "short"
38001
+ };
38002
+ if (!req.toolChoice) return shared;
38003
+ return api === "anthropic-messages" ? { ...shared, toolChoice: { type: "tool", name: req.toolChoice.name } } : { ...shared, toolChoice: { type: "function", function: { name: req.toolChoice.name } } };
38004
+ }
38005
+ function toPiContext(req, model, clock3) {
38006
+ const callNames = /* @__PURE__ */ new Map();
38007
+ const messages = [];
38008
+ for (const turn of req.messages) {
38009
+ if (turn.role === "assistant") {
38010
+ for (const part of turn.content) if (part.type === "tool_use") callNames.set(part.id, part.name);
38011
+ messages.push(assistantMessage(turn, model, clock3()));
38012
+ } else {
38013
+ messages.push(...userMessages(turn, callNames, clock3()));
38014
+ }
38015
+ }
38016
+ const tools = req.tools?.map((t) => ({
38017
+ name: t.name,
38018
+ description: t.description,
38019
+ parameters: t.inputSchema
38020
+ }));
38021
+ return {
38022
+ ...req.system !== void 0 ? { systemPrompt: req.system } : {},
38023
+ messages,
38024
+ ...tools && tools.length > 0 ? { tools } : {}
38025
+ };
38026
+ }
38027
+ function assistantMessage(turn, model, timestamp) {
38028
+ const content = [];
38029
+ for (const part of turn.content) {
38030
+ if (part.type === "text") content.push({ type: "text", text: part.text });
38031
+ else if (part.type === "tool_use")
38032
+ content.push({ type: "toolCall", id: part.id, name: part.name, arguments: argumentsOf(part.input) });
38033
+ else if (part.type === "thinking")
38034
+ content.push({ type: "thinking", thinking: part.thinking, thinkingSignature: part.signature });
38035
+ else if (part.type === "redacted_thinking")
38036
+ content.push({ type: "thinking", thinking: "", thinkingSignature: part.data, redacted: true });
38037
+ }
38038
+ return {
38039
+ role: "assistant",
38040
+ content,
38041
+ api: model.api,
38042
+ provider: model.provider,
38043
+ model: model.id,
38044
+ usage: { ...ZERO_USAGE, cost: { ...ZERO_USAGE.cost } },
38045
+ stopReason: content.some((p) => p.type === "toolCall") ? "toolUse" : "stop",
38046
+ timestamp
38047
+ };
38048
+ }
38049
+ function argumentsOf(input) {
38050
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
38051
+ }
38052
+ function userMessages(turn, callNames, timestamp) {
38053
+ const out = [];
38054
+ const rest = [];
38055
+ for (const part of turn.content) {
38056
+ if (part.type === "tool_result") {
38057
+ const result = {
38058
+ role: "toolResult",
38059
+ toolCallId: part.toolUseId,
38060
+ toolName: callNames.get(part.toolUseId) ?? part.toolUseId,
38061
+ content: toolResultParts(part.content),
38062
+ isError: part.isError === true,
38063
+ timestamp
38064
+ };
38065
+ out.push(result);
38066
+ } else {
38067
+ const visible = visiblePart(part);
38068
+ if (visible) rest.push(visible);
38069
+ }
38070
+ }
38071
+ if (rest.length > 0) out.push({ role: "user", content: rest, timestamp });
38072
+ return out;
38073
+ }
38074
+ function toolResultParts(content) {
38075
+ if (typeof content === "string") return [{ type: "text", text: content }];
38076
+ return content.map((part) => visiblePart(part)).filter((p) => p !== void 0);
38077
+ }
38078
+ function visiblePart(part) {
38079
+ switch (part.type) {
38080
+ case "text":
38081
+ return { type: "text", text: part.text };
38082
+ case "image":
38083
+ return { type: "image", data: part.data, mimeType: part.mediaType };
38084
+ case "document":
38085
+ return {
38086
+ type: "text",
38087
+ text: `
38088
+
38089
+ [attached file: ${part.name ?? "document"} (${part.mediaType}); not supported by this provider]
38090
+ `
38091
+ };
38092
+ default:
38093
+ return void 0;
38094
+ }
38095
+ }
38096
+ function fromPiMessage(message3, provider) {
38097
+ if (message3.stopReason === "error" || message3.stopReason === "aborted") {
38098
+ throw new Error(`Provider "${provider}" (${message3.model}): ${message3.errorMessage ?? "the model call failed"}`);
38099
+ }
38100
+ const content = [];
38101
+ for (const part of message3.content) {
38102
+ if (part.type === "text") content.push({ type: "text", text: part.text });
38103
+ else if (part.type === "toolCall")
38104
+ content.push({ type: "tool_use", id: part.id, name: part.name, input: part.arguments });
38105
+ else if (part.type === "thinking") {
38106
+ content.push(
38107
+ part.redacted ? { type: "redacted_thinking", data: part.thinkingSignature ?? "" } : { type: "thinking", thinking: part.thinking, signature: part.thinkingSignature ?? "" }
38108
+ );
38109
+ }
38110
+ }
38111
+ return { content, stopReason: STOP_REASONS[message3.stopReason] ?? "other", usage: usageOf(message3.usage) };
38112
+ }
38113
+ function usageOf(usage) {
38114
+ return {
38115
+ inputTokens: usage.input,
38116
+ outputTokens: usage.output,
38117
+ cacheReadTokens: usage.cacheRead,
38118
+ cacheWriteTokens: usage.cacheWrite
38119
+ };
38120
+ }
38121
+ var ANTHROPIC_BASE_URL, ZERO_RATES, ZERO_USAGE, CONTEXT_WINDOW, PiAiProviders, PiAiProvider, STOP_REASONS;
38122
+ var init_piAi = __esm({
38123
+ "../../src/core/harness/piAi.ts"() {
38124
+ "use strict";
38125
+ init_provider();
38126
+ init_secrets2();
38127
+ init_clock();
38128
+ ANTHROPIC_BASE_URL = "https://api.anthropic.com";
38129
+ ZERO_RATES = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
38130
+ ZERO_USAGE = { ...ZERO_RATES, totalTokens: 0, cost: { ...ZERO_RATES, total: 0 } };
38131
+ CONTEXT_WINDOW = 2e5;
38132
+ PiAiProviders = class {
38133
+ providers = /* @__PURE__ */ new Map();
38134
+ constructor(configs, opts = {}) {
38135
+ for (const [name, cfg] of Object.entries(configs)) this.providers.set(name, new PiAiProvider(name, cfg, opts));
38136
+ }
38137
+ get(name) {
38138
+ const provider = this.providers.get(name);
38139
+ if (!provider) {
38140
+ throw new Error(`Unknown provider "${name}". Configured providers: ${[...this.providers.keys()].join(", ")}`);
38141
+ }
38142
+ return provider;
38143
+ }
38144
+ names() {
38145
+ return [...this.providers.keys()];
38146
+ }
38147
+ };
38148
+ PiAiProvider = class {
38149
+ name;
38150
+ api;
38151
+ baseUrl;
38152
+ keyEnv;
38153
+ secrets;
38154
+ clock;
38155
+ scripted;
38156
+ fetchImpl;
38157
+ streams;
38158
+ constructor(name, cfg, opts = {}) {
38159
+ this.name = name;
38160
+ this.api = piApiFor(cfg.type);
38161
+ if (cfg.type === "openai-compatible" && !cfg.baseUrl) {
38162
+ throw new Error(`Provider "${name}": openai-compatible providers require baseUrl`);
38163
+ }
38164
+ this.baseUrl = (cfg.baseUrl ?? ANTHROPIC_BASE_URL).replace(/\/+$/, "");
38165
+ this.keyEnv = cfg.apiKeyEnv ?? (cfg.type === "anthropic" ? ANTHROPIC_API_KEY_ENV : void 0);
38166
+ this.secrets = opts.secrets ?? processSecrets;
38167
+ this.clock = opts.clock ?? systemClock;
38168
+ this.scripted = opts.apis?.[this.api];
38169
+ this.fetchImpl = opts.fetch;
38170
+ }
38171
+ /** The model pi is handed for one request: the block as its provider, the
38172
+ * request's output cap as its own, no thinking (neither caller asks for
38173
+ * any), text and images in, the zero rate card. */
38174
+ model(id, maxTokens) {
38175
+ return {
38176
+ id,
38177
+ name: id,
38178
+ api: this.api,
38179
+ provider: this.name,
38180
+ baseUrl: this.baseUrl,
38181
+ reasoning: false,
38182
+ input: ["text", "image"],
38183
+ cost: { ...ZERO_RATES },
38184
+ contextWindow: CONTEXT_WINDOW,
38185
+ maxTokens
38186
+ };
38187
+ }
38188
+ /** One completion: the key revealed into pi's request options and nowhere
38189
+ * else, the request in pi's shape, pi's adapter on the wire, its final
38190
+ * message back in the vocabulary. A missing key fails here, by variable
38191
+ * name, before any request; a provider error or an abort is thrown with
38192
+ * pi's message — `route()` and `reflect()` catch either as they catch a
38193
+ * native adapter's throw. */
38194
+ async complete(req) {
38195
+ let key;
38196
+ if (this.keyEnv !== void 0) {
38197
+ const secret2 = this.secrets.named(this.keyEnv);
38198
+ if (!secret2) throw new Error(`Provider "${this.name}": ${this.keyEnv} is not set`);
38199
+ key = secret2.reveal();
38200
+ }
38201
+ const model = this.model(req.model, req.maxTokens);
38202
+ const options = {
38203
+ ...piStreamOptions(this.api, req, key),
38204
+ ...this.fetchImpl ? { fetch: this.fetchImpl } : {}
38205
+ };
38206
+ const message3 = await this.api$().stream(model, toPiContext(req, model, this.clock), options).result();
38207
+ return fromPiMessage(message3, this.name);
38208
+ }
38209
+ /** pi's implementation of this provider's API, loaded on the first call and
38210
+ * kept; a scripted one stands in for it in tests. */
38211
+ api$() {
38212
+ this.streams ??= this.scripted ?? (this.api === "anthropic-messages" ? anthropicMessagesApi() : openAICompletionsApi());
38213
+ return this.streams;
38214
+ }
38215
+ };
38216
+ STOP_REASONS = {
38217
+ stop: "end_turn",
38218
+ toolUse: "tool_use",
38219
+ length: "max_tokens"
38220
+ };
38221
+ }
38222
+ });
38223
+
37678
38224
  // ../../src/mcp/secretStore.ts
37679
38225
  import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
37680
38226
  import { dirname as dirname10, resolve as resolve10 } from "node:path";
@@ -38981,6 +39527,7 @@ async function receiveSlackMessage(client, ev, span, policy) {
38981
39527
  userId: `${PLATFORM}:${ev.user}`,
38982
39528
  threadKey: `${PLATFORM}:${ev.channel}:${ev.threadTs}`,
38983
39529
  text: ev.text + note,
39530
+ messageId: ev.ts,
38984
39531
  channelName,
38985
39532
  userName,
38986
39533
  sourceUrl: team ? slackPermalink(team, ev.channel, ev.ts, ev.threadTs) : void 0,
@@ -40038,7 +40585,7 @@ var init_webShell = __esm({
40038
40585
  init_favicon();
40039
40586
  init_residentsModel();
40040
40587
  init_webSeed();
40041
- PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
40588
+ PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
40042
40589
  FORM_PAGE_CSP = PAGE_CSP.replace("form-action 'none'", "form-action 'self'");
40043
40590
  WEB_HTML_HEADERS = {
40044
40591
  "content-type": "text/html; charset=utf-8",
@@ -42771,6 +43318,7 @@ var init_modelProxy = __esm({
42771
43318
  "use strict";
42772
43319
  init_anthropic();
42773
43320
  init_openaiCompat();
43321
+ init_provider();
42774
43322
  init_http();
42775
43323
  ANTHROPIC_MESSAGES_PATH = "/v1/messages";
42776
43324
  OPENAI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
@@ -43099,7 +43647,9 @@ async function runRelayedTool(harness, ask, opts = {}) {
43099
43647
  ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
43100
43648
  } : { ...base, publish: (e) => harness.emit(e) },
43101
43649
  ...opts.signal && !base.signal ? { signal: opts.signal } : {},
43102
- ...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
43103
43653
  };
43104
43654
  try {
43105
43655
  const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
@@ -43133,7 +43683,7 @@ var init_relay = __esm({
43133
43683
  "../../src/core/harness/pi/relay.ts"() {
43134
43684
  "use strict";
43135
43685
  init_tracingExecutor();
43136
- init_types3();
43686
+ init_chatMessage();
43137
43687
  init_awaitChildren();
43138
43688
  init_runEvents();
43139
43689
  init_toolRules();
@@ -44732,6 +45282,7 @@ async function runBot() {
44732
45282
  });
44733
45283
  console.log(`[capabilities] ${JSON.stringify(capabilities)}`);
44734
45284
  const providers = new ProviderRegistry(config.config.providers);
45285
+ const completions = new PiAiProviders(config.config.providers);
44735
45286
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
44736
45287
  let slackEmailLookup;
44737
45288
  const mcpWiring = buildMcp(config, processSecrets, {
@@ -44839,6 +45390,7 @@ async function runBot() {
44839
45390
  const deps = {
44840
45391
  config,
44841
45392
  providers,
45393
+ completions,
44842
45394
  spanLog,
44843
45395
  runBearers,
44844
45396
  harness: {
@@ -45309,6 +45861,7 @@ var init_index = __esm({
45309
45861
  init_config2();
45310
45862
  init_capabilities();
45311
45863
  init_registry3();
45864
+ init_piAi();
45312
45865
  init_slack();
45313
45866
  init_slackChannelDirectory();
45314
45867
  init_http();
@@ -45427,6 +45980,7 @@ init_memory();
45427
45980
  init_residentAdmin();
45428
45981
  init_residentFleet();
45429
45982
  init_registry3();
45983
+ init_piAi();
45430
45984
  init_skills2();
45431
45985
  init_mcp2();
45432
45986
  init_source();
@@ -45754,6 +46308,7 @@ async function main() {
45754
46308
  globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
45755
46309
  const { config, runStore } = await bot();
45756
46310
  const providers = new ProviderRegistry(config.config.providers);
46311
+ const completions = new PiAiProviders(config.config.providers);
45757
46312
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
45758
46313
  const capabilities = capabilitiesFrom(config.config, publicEnv(), processSecrets, {
45759
46314
  meatBinary: meatOnPath(publicEnv())
@@ -45770,6 +46325,7 @@ async function main() {
45770
46325
  const deps = {
45771
46326
  config,
45772
46327
  providers,
46328
+ completions,
45773
46329
  capabilities,
45774
46330
  residentFleet,
45775
46331
  ...artifacts ? { artifacts } : {},