@tangle-network/agent-app 0.60.169 → 0.60.171

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 (34) hide show
  1. package/dist/alerting/index.js +1 -1
  2. package/dist/alerting/slack.d.ts +1 -1
  3. package/dist/assistant/index.js +5 -5
  4. package/dist/chat-react/index.d.ts +1 -1
  5. package/dist/chat-react/index.js +3 -3
  6. package/dist/chat-routes/index.js +2 -2
  7. package/dist/{chunk-CCTQQ45L.js → chunk-CRHXK3QT.js} +3 -3
  8. package/dist/{chunk-J3D3HQPY.js → chunk-DQSGUZZZ.js} +1 -1
  9. package/dist/chunk-DQSGUZZZ.js.map +1 -0
  10. package/dist/{chunk-COP2K4LF.js → chunk-F2VX6TTJ.js} +1 -1
  11. package/dist/chunk-F2VX6TTJ.js.map +1 -0
  12. package/dist/{chunk-XSPUKVHF.js → chunk-KICTB6RK.js} +2 -2
  13. package/dist/{chunk-WVESWAJJ.js → chunk-MXNVSY6J.js} +28 -2
  14. package/dist/{chunk-WVESWAJJ.js.map → chunk-MXNVSY6J.js.map} +1 -1
  15. package/dist/{chunk-ZEMO7S2G.js → chunk-PZHZV6QY.js} +2 -2
  16. package/dist/{chunk-RHDP4VIG.js → chunk-RCRC2JKP.js} +2 -2
  17. package/dist/chunk-RCRC2JKP.js.map +1 -0
  18. package/dist/harness/index.d.ts +19 -0
  19. package/dist/harness/index.js +3 -1
  20. package/dist/sandbox/index.d.ts +1 -1
  21. package/dist/sandbox/index.js +2 -2
  22. package/dist/sandbox/warm-lease.d.ts +1 -1
  23. package/dist/skills-placement/index.js +1 -1
  24. package/dist/turn-health/index.js +1 -1
  25. package/dist/web-react/agent-profile-editor.js +3 -3
  26. package/dist/web-react/index.js +5 -5
  27. package/dist/web-react/use-file-mentions.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/dist/chunk-COP2K4LF.js.map +0 -1
  30. package/dist/chunk-J3D3HQPY.js.map +0 -1
  31. package/dist/chunk-RHDP4VIG.js.map +0 -1
  32. /package/dist/{chunk-CCTQQ45L.js.map → chunk-CRHXK3QT.js.map} +0 -0
  33. /package/dist/{chunk-XSPUKVHF.js.map → chunk-KICTB6RK.js.map} +0 -0
  34. /package/dist/{chunk-ZEMO7S2G.js.map → chunk-PZHZV6QY.js.map} +0 -0
@@ -187,4 +187,4 @@ export {
187
187
  postSlackAlert,
188
188
  checkSlackCredential
189
189
  };
190
- //# sourceMappingURL=chunk-COP2K4LF.js.map
190
+ //# sourceMappingURL=chunk-F2VX6TTJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerting/slack.ts"],"sourcesContent":["/**\n * `/alerting` — post an operational alert to Slack, and say honestly when it\n * did not arrive.\n *\n * WHY THIS EXISTS: on 2026-08-06 an audit of every Slack credential the org\n * held found three of four dead — the fleet incoming webhook 404ing\n * (`no_service`), a second company webhook likewise, and the bot token\n * answering `account_inactive`. One ops webhook was still live. Nobody knew\n * which was which until each was tested by hand, and that is the actual\n * problem: the credentials were indistinguishable from where the code stood.\n *\n * This failure has already been paid for once, with numbers: in\n * `agent-container`, a revoked webhook let the CI healthcheck sit dead for\n * 18 days — 2,567 consecutive failures, 27 successes, zero alerts — because\n * fifteen call sites across six workflows posted with no body inspection and\n * then asserted delivery. That repo fixed the reporting (its `post-slack.sh`\n * confirms 2xx AND Slack's literal `ok` body, and fails closed otherwise) and\n * that half of the lesson is theirs, adopted here.\n *\n * What their fix cannot do, and this module can, is answer the question BEFORE\n * an alert needs to fire. A fail-closed post still only discovers a dead\n * credential at the moment a page is lost. That is the second half.\n *\n * Two design consequences, and they are the whole module:\n *\n * 1. **A bot token and `chat.postMessage` is the preferred transport; an\n * incoming webhook is supported because one is usually what you already\n * have.** A token reaches every channel (a webhook is bolted to one), is\n * revocable and rotatable in place, and — the part that matters — is\n * VERIFIABLE: `auth.test` answers whether the credential is alive without\n * posting anything, which is what lets `/preflight` fail a deploy on a dead\n * alerting channel instead of discovering it during an incident. A webhook\n * can only be tested by posting to it, which is why the three dead\n * credentials above went unnoticed. A token is never fallen back FROM: if\n * one is configured and dead, delivering over a webhook instead would hide\n * the very condition worth reporting.\n *\n * 2. **Slack answers `ok:false` under HTTP 200.** `invalid_auth`,\n * `channel_not_found` and `not_in_channel` all arrive as a successful\n * response with a failure inside it, so a status check reads a dead channel\n * as a delivered page. The body is always parsed, and the outcome\n * distinguishes a MISSING credential (configuration absent — not an\n * incident) from a DEAD one (the alerting channel itself is broken — the\n * loudest thing this module can report). A summary that calls those two the\n * same thing is the defect wearing a different hat.\n *\n * The caller decides what to do with a non-delivery; this module never throws\n * on one, because an alerting path that can take down the thing it reports on\n * is worse than the outage it was watching for. It is also never the only\n * channel: a durable record (an issue, an audit row) is the caller's job, and\n * that is what survives the credential going dead again.\n *\n * Server-only: holds a bot token. This subpath must never reach a browser\n * bundle.\n */\n\nconst SLACK_API = 'https://slack.com/api'\nconst DEFAULT_TIMEOUT_MS = 10_000\nconst DEFAULT_ATTEMPTS = 3\n\n/**\n * Why an alert did not reach Slack. The split is by what a human must DO about\n * it, since that is the only distinction a caller can act on.\n */\nexport type SlackFailureReason =\n /** No token or no channel configured. Configuration is absent, nothing is broken. */\n | 'not-configured'\n /**\n * The token is dead — revoked, or its app removed from the workspace. No\n * alert will EVER arrive until a human mints a new one. This is itself an\n * incident and the caller should escalate it on a channel that does not\n * depend on Slack.\n */\n | 'credential'\n /**\n * The credential is alive but cannot post HERE: the channel is wrong,\n * archived, or the bot was never invited to it. One human action fixes it.\n */\n | 'channel'\n /** Slack asked us to slow down. Transient; the alert is worth retrying. */\n | 'rate-limited'\n /** The request never got an answer — network, DNS, timeout. Transient. */\n | 'transport'\n /** Slack refused for some other reason; `detail` carries its error code. */\n | 'api'\n\n/** An alert that did not arrive, and why. Named because the classifiers only ever produce this half. */\nexport interface SlackAlertFailure {\n delivered: false\n reason: SlackFailureReason\n /** One line naming what is wrong and what fixes it. Safe to log; carries no token. */\n detail: string\n}\n\n/** What one `postSlackAlert` call did. Never a bare boolean — the caller pages differently per reason. */\nexport type SlackAlertOutcome =\n | {\n delivered: true\n /** The channel id Slack resolved (not necessarily the name that was passed). */\n channel: string\n /** Slack's message timestamp — the message's identity, for a later thread reply. */\n ts: string\n }\n | SlackAlertFailure\n\n/** Define configuration options for posting an alert message to a Slack channel */\nexport interface SlackAlertOptions {\n /**\n * Slack bot token (`xoxb-…`) with `chat:write`. The PREFERRED transport,\n * because it is the only one whose liveness can be checked before an alert\n * needs it. An empty or absent value is `not-configured`, never an error — a\n * product that has not adopted Slack yet must not fail its alerting path.\n */\n token?: string | undefined\n /**\n * Channel to post to: a name (`#infra-alerts`) or an id (`C01234567`). The\n * bot must be a member; Slack answers `not_in_channel` otherwise. Required\n * with `token`, meaningless with `webhookUrl` (a webhook carries its own\n * channel, fixed when it was created).\n */\n channel?: string | undefined\n /**\n * Slack incoming-webhook URL, used only when no `token` is configured.\n *\n * It works and it needs no setup, which is why it is supported — but it\n * cannot be verified without posting, cannot be pointed at a second channel,\n * and gives back an error token instead of a code. Two of the three webhooks\n * this org has held were found revoked. Treat it as the transport you have,\n * not the one you want.\n *\n * When a `token` is also configured this is IGNORED rather than used as a\n * fallback: a dead token must surface as the incident it is, and quietly\n * succeeding over a second transport is how the last outage stayed invisible.\n */\n webhookUrl?: string | undefined\n /** Message body as Slack mrkdwn. Newlines are preserved. */\n text: string\n /**\n * Attempts for a TRANSIENT failure (429 / 5xx / transport). Default 3. A\n * dead credential or a wrong channel is never retried — the answer will not\n * change, and retrying an auth failure is how a token gets rate-limited.\n */\n attempts?: number\n /** Per-request deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n /** Injection seam for tests; defaults to a real delay between retries. */\n sleepImpl?: (ms: number) => Promise<void>\n}\n\n/** Slack error codes that mean the CREDENTIAL is dead, not the request. */\nconst CREDENTIAL_ERRORS = new Set([\n 'invalid_auth',\n 'not_authed',\n 'account_inactive',\n 'token_revoked',\n 'token_expired',\n 'no_permission',\n 'missing_scope',\n 'ekm_access_denied',\n])\n\n/** Slack error codes that mean the credential is fine but this CHANNEL is not reachable. */\nconst CHANNEL_ERRORS = new Set([\n 'channel_not_found',\n 'not_in_channel',\n 'is_archived',\n 'restricted_action',\n 'restricted_action_read_only_channel',\n])\n\nfunction remedyFor(reason: SlackFailureReason, error: string, channel: string): string {\n switch (reason) {\n case 'credential':\n return (\n `Slack rejected the bot token (${error}) — no alert can arrive until a human mints a new one. ` +\n 'Reinstall the Slack app and update SLACK_BOT_TOKEN wherever it is stored.'\n )\n case 'channel':\n return (\n `Slack accepted the token but refused ${channel} (${error}) — ` +\n `invite the bot to ${channel}, or correct the channel name.`\n )\n default:\n return `Slack refused the post (${error}).`\n }\n}\n\nfunction classifySlackError(error: string, channel: string): SlackAlertFailure {\n const reason: SlackFailureReason = CREDENTIAL_ERRORS.has(error)\n ? 'credential'\n : CHANNEL_ERRORS.has(error)\n ? 'channel'\n : 'api'\n return { delivered: false, reason, detail: remedyFor(reason, error, channel) }\n}\n\n/** Slack's `Retry-After` is in SECONDS. Bounded so a hostile header cannot park the caller. */\nfunction retryDelayMs(response: Response, attempt: number): number {\n const header = Number(response.headers.get('retry-after'))\n if (Number.isFinite(header) && header > 0) return Math.min(header * 1000, 30_000)\n return Math.min(500 * 2 ** (attempt - 1), 8_000)\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/**\n * Post one alert to a Slack channel.\n *\n * Never throws and never retries a failure whose answer cannot change. The\n * returned outcome is the whole result — a caller that ignores it has an\n * alerting path it cannot prove works, which is the failure this module was\n * written for.\n */\nexport async function postSlackAlert(options: SlackAlertOptions): Promise<SlackAlertOutcome> {\n const token = options.token?.trim()\n const channel = options.channel?.trim()\n const webhookUrl = options.webhookUrl?.trim()\n\n if (!token && !webhookUrl) {\n return {\n delivered: false,\n reason: 'not-configured',\n detail:\n 'no Slack credential configured — set SLACK_BOT_TOKEN (preferred, verifiable) ' +\n 'or SLACK_WEBHOOK_URL to route alerts to Slack',\n }\n }\n if (token && !channel) {\n return {\n delivered: false,\n reason: 'not-configured',\n detail: 'a Slack bot token is set but no channel is — set the alert channel (e.g. #infra-alerts)',\n }\n }\n\n const fetchImpl = options.fetchImpl ?? fetch\n const sleep = options.sleepImpl ?? defaultSleep\n const attempts = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS)\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n // A token is never fallen back FROM. If one is configured and dead, that is\n // the incident, and delivering over a webhook instead would hide exactly the\n // condition this module exists to surface.\n const transport: SlackTransport = token\n ? { kind: 'token', token, channel: channel as string }\n : { kind: 'webhook', url: webhookUrl as string }\n\n let lastTransient: SlackAlertFailure = {\n delivered: false,\n reason: 'transport',\n detail: 'Slack was never reached',\n }\n\n for (let attempt = 1; attempt <= attempts; attempt += 1) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n let response: Response\n try {\n response = await fetchImpl(requestUrl(transport), {\n method: 'POST',\n headers: requestHeaders(transport),\n body: JSON.stringify(requestBody(transport, options.text)),\n signal: controller.signal,\n })\n } catch (error) {\n lastTransient = {\n delivered: false,\n reason: 'transport',\n detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`,\n }\n if (attempt < attempts) await sleep(Math.min(500 * 2 ** (attempt - 1), 8_000))\n continue\n } finally {\n clearTimeout(timer)\n }\n\n if (response.status === 429 || response.status >= 500) {\n lastTransient = {\n delivered: false,\n reason: response.status === 429 ? 'rate-limited' : 'transport',\n detail: `Slack returned ${response.status}`,\n }\n if (attempt < attempts) await sleep(retryDelayMs(response, attempt))\n continue\n }\n\n // Everything below is a settled answer. BOTH transports put the verdict in\n // the BODY under a 200 — the API as `{\"ok\":false,\"error\":…}`, a webhook as\n // an error token where the literal `ok` should be — so a status check alone\n // reads a revoked credential as a delivered page. That misreading is why\n // this module exists.\n return transport.kind === 'token'\n ? await settleTokenResponse(response, transport.channel)\n : await settleWebhookResponse(response)\n }\n\n return lastTransient\n}\n\ntype SlackTransport =\n | { kind: 'token'; token: string; channel: string }\n | { kind: 'webhook'; url: string }\n\nfunction requestUrl(transport: SlackTransport): string {\n return transport.kind === 'token' ? `${SLACK_API}/chat.postMessage` : transport.url\n}\n\nfunction requestHeaders(transport: SlackTransport): Record<string, string> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json; charset=utf-8' }\n // The token rides a header, never a query string: a URL is logged by proxies\n // and retained in error messages; a header is not. (A webhook URL IS the\n // credential and has no such option — one more reason to prefer a token.)\n if (transport.kind === 'token') headers.Authorization = `Bearer ${transport.token}`\n return headers\n}\n\nfunction requestBody(transport: SlackTransport, text: string): Record<string, string> {\n // A webhook carries its own channel, fixed when it was created; naming one\n // here would be ignored at best and rejected at worst.\n return transport.kind === 'token' ? { channel: transport.channel, text } : { text }\n}\n\nasync function settleTokenResponse(\n response: Response,\n channel: string,\n): Promise<SlackAlertOutcome> {\n let body: { ok?: boolean; error?: string; channel?: string; ts?: string }\n try {\n body = (await response.json()) as typeof body\n } catch {\n return {\n delivered: false,\n reason: 'api',\n detail: `Slack returned ${response.status} with an unreadable body`,\n }\n }\n if (body.ok === true) {\n return { delivered: true, channel: body.channel ?? channel, ts: body.ts ?? '' }\n }\n return classifySlackError(body.error ?? `http_${response.status}`, channel)\n}\n\n/**\n * A webhook confirms delivery with a 2xx AND the literal three-byte body `ok`.\n * A revoked one answers `no_service` / `no_team`, sometimes under a 200. Both\n * halves are required: this is the shape `agent-container` proved against a\n * stand-in webhook, where a healthy, a revoked and a deleted webhook produced\n * byte-identical output under a status-only check.\n */\nasync function settleWebhookResponse(response: Response): Promise<SlackAlertOutcome> {\n const body = (await response.text().catch(() => '')).trim()\n if (response.ok && body === 'ok') {\n // A webhook reports neither the channel it posted to nor a message id.\n return { delivered: true, channel: '(webhook)', ts: '' }\n }\n const error = body || `http_${response.status}`\n const reason: SlackFailureReason = WEBHOOK_CREDENTIAL_ERRORS.has(error)\n ? 'credential'\n : WEBHOOK_CHANNEL_ERRORS.has(error)\n ? 'channel'\n : 'api'\n return {\n delivered: false,\n reason,\n detail:\n reason === 'credential'\n ? `the Slack webhook is revoked (${error}) — no alert can arrive until a human mints a new one. ` +\n 'Prefer replacing it with a bot token, whose liveness can be checked before an alert needs it.'\n : reason === 'channel'\n ? `Slack refused the webhook's channel (${error}) — the channel was archived, or the app lost access.`\n : `Slack refused the webhook post (${error}).`,\n }\n}\n\n/** Webhook error tokens that mean the webhook itself is gone. */\nconst WEBHOOK_CREDENTIAL_ERRORS = new Set(['no_service', 'no_team', 'invalid_token'])\n\n/** Webhook error tokens that mean the webhook is valid but its channel is not usable. */\nconst WEBHOOK_CHANNEL_ERRORS = new Set(['channel_not_found', 'channel_is_archived', 'action_prohibited'])\n\n/** Define configuration options for verifying that a Slack bot token is live */\nexport interface SlackCredentialCheckOptions {\n token: string | undefined\n /** Per-request deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/** What `checkSlackCredential` concluded. `team`/`botId` are present only when live. */\nexport type SlackCredentialVerdict =\n | { live: true; team: string; botId: string }\n | { live: false; reason: SlackFailureReason; detail: string }\n\n/**\n * Ask Slack whether the bot token is alive, WITHOUT posting anything.\n *\n * This is the check that was missing. An incoming webhook offers no equivalent\n * — the only way to test one is to post to it — which is how a revoked webhook\n * sat in a repo secret for months looking configured. Wire it into `/preflight`\n * so a dead alerting channel fails a deploy rather than an incident.\n */\nexport async function checkSlackCredential(\n options: SlackCredentialCheckOptions,\n): Promise<SlackCredentialVerdict> {\n const token = options.token?.trim()\n if (!token) {\n return {\n live: false,\n reason: 'not-configured',\n detail: 'no Slack bot token configured — set SLACK_BOT_TOKEN',\n }\n }\n const fetchImpl = options.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n try {\n const response = await fetchImpl(`${SLACK_API}/auth.test`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${token}` },\n signal: controller.signal,\n })\n const body = (await response.json()) as {\n ok?: boolean\n error?: string\n team?: string\n bot_id?: string\n }\n if (body.ok === true) {\n return { live: true, team: body.team ?? 'unknown', botId: body.bot_id ?? 'unknown' }\n }\n const outcome = classifySlackError(body.error ?? `http_${response.status}`, '(auth.test)')\n // `classifySlackError` is shaped for a post; auth.test only ever answers\n // about the credential, so a non-credential code here still means the\n // token cannot be used.\n return {\n live: false,\n reason: outcome.reason === 'channel' ? 'credential' : outcome.reason,\n detail: outcome.detail,\n }\n } catch (error) {\n return {\n live: false,\n reason: 'transport',\n detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`,\n }\n } finally {\n clearTimeout(timer)\n }\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ import {
15
15
  import {
16
16
  snapHarnessToModel,
17
17
  snapModelToHarness
18
- } from "./chunk-WVESWAJJ.js";
18
+ } from "./chunk-MXNVSY6J.js";
19
19
 
20
20
  // src/web-react/composer-file-accept.ts
21
21
  function isAcceptedFileType(file, accept) {
@@ -576,4 +576,4 @@ export {
576
576
  HarnessPicker,
577
577
  AgentSessionControls
578
578
  };
579
- //# sourceMappingURL=chunk-XSPUKVHF.js.map
579
+ //# sourceMappingURL=chunk-KICTB6RK.js.map
@@ -83,6 +83,31 @@ function assertHarnessModelCompatible(harness, selection) {
83
83
  );
84
84
  }
85
85
  }
86
+ var HARNESS_CAPABILITIES = {
87
+ "claude-code": { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".claude/skills" },
88
+ nanoclaw: { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".claude/skills" },
89
+ opencode: { subagents: true, mcp: "config-file", toolControls: "runtime", skillsDir: ".opencode/skills" },
90
+ codex: { subagents: true, mcp: "config-file", toolControls: "runtime", skillsDir: ".codex/skills" },
91
+ "kimi-code": { subagents: false, mcp: "config-file", toolControls: "runtime", skillsDir: ".kimi/skills" },
92
+ pi: { subagents: false, mcp: "extension", toolControls: "startup", skillsDir: ".pi/skills" },
93
+ prime: { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".prime/agent/skills" },
94
+ hermes: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
95
+ openclaw: { subagents: false, mcp: "config-file", toolControls: "runtime", skillsDir: "skills" },
96
+ // Harnesses the platform placement map does not bridge yet: conservative
97
+ // facts — assert nothing they have not proven. Each upgrades to its real
98
+ // facts when the materializer grows support for it.
99
+ acp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
100
+ amp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
101
+ "factory-droids": { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
102
+ forge: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
103
+ cursor: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
104
+ "cli-base": { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null }
105
+ };
106
+ function harnessCapabilities(harness) {
107
+ const caps = HARNESS_CAPABILITIES[harness];
108
+ if (!caps) throw new Error(`harnessCapabilities: no capability facts for "${harness}"`);
109
+ return caps;
110
+ }
86
111
 
87
112
  export {
88
113
  modelProvider,
@@ -95,6 +120,7 @@ export {
95
120
  isModelCompatibleWithHarness,
96
121
  snapModelToHarness,
97
122
  snapHarnessToModel,
98
- assertHarnessModelCompatible
123
+ assertHarnessModelCompatible,
124
+ harnessCapabilities
99
125
  };
100
- //# sourceMappingURL=chunk-WVESWAJJ.js.map
126
+ //# sourceMappingURL=chunk-MXNVSY6J.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list is derived from `@tangle-network/agent-interface`'s canonical\n * harness enum and mirrors the sandbox SDK's `BackendType` (no sandbox dependency). The consumer\n * owns storage — which harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n *\n * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it\n * comes from `@tangle-network/agent-interface`, the single source of truth shared with the\n * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the\n * session lock, and its server assertion also checks resolved transport-provider evidence.\n */\n\nimport {\n harnessProviders,\n harnessSupportsModel,\n harnessTypeSchema,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\n\n/**\n * Canonical harnesses that are NOT sandbox backends. Each value in `KNOWN_HARNESSES` is dispatched\n * as `backend.type`, so a harness the platform ships no provider adapter for must stay out —\n * offering it would resolve a session onto a runner that cannot start.\n */\nconst NON_BACKEND_HARNESSES = ['gemini'] as const satisfies readonly HarnessType[]\n\nconst nonBackendHarnesses = new Set<string>(NON_BACKEND_HARNESSES)\n\n/** A coding-agent backend this shell can dispatch a session onto. */\nexport type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>\n\n/**\n * The known coding-agent backends: the canonical harness set minus what has no provider adapter,\n * Derived from `harnessTypeSchema.options` so a harness added upstream reaches this shell without\n * an edit here, and kept structural so this module still needs no sandbox dependency.\n */\nexport const KNOWN_HARNESSES: readonly Harness[] = [\n ...harnessTypeSchema.options.filter(\n (harness): harness is Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]> =>\n !nonBackendHarnesses.has(harness),\n ),\n]\n\n/** Define the default harness to use for code execution and testing environments */\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\n/** Determine if a value is a recognized harness string identifier */\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\n/** Resolve input options to determine the appropriate session harness to use */\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\n/** Represent resolved session state including harness, lock status, and swap attempt flag */\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * An app's harness configuration, read from its deploy configuration (for example the env vars\n * `AGENT_HARNESS` and `AGENT_HARNESS_WORKSPACES`), so no product hard-codes `backend.type`.\n */\nexport interface HarnessConfig {\n /** The app default for new sessions. Unset or blank means {@link DEFAULT_HARNESS}. */\n default?: string\n /** Per-workspace choices: a JSON object mapping a workspace id to a harness. */\n workspaces?: string\n}\n\nfunction configHarness(value: unknown, where: string): Harness {\n if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`)\n return value\n}\n\n/**\n * The harness a workspace's new sessions start on: its own choice, else the app default, else\n * {@link DEFAULT_HARNESS}. Pass the result to {@link resolveSessionHarness} as `workspaceDefault`\n * so a session that already ran keeps its harness. A value naming an unknown harness throws:\n * a configuration typo must fail loudly, never run a different harness than the one configured.\n */\nexport function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness {\n const appDefault = config.default?.trim()\n ? configHarness(config.default.trim(), 'The harness default') : DEFAULT_HARNESS\n const raw = config.workspaces?.trim()\n if (!raw) return appDefault\n let choices: unknown\n try {\n choices = JSON.parse(raw)\n } catch {\n throw new Error('The per-workspace harness configuration is not JSON')\n }\n if (!choices || typeof choices !== 'object' || Array.isArray(choices)) {\n throw new Error('The per-workspace harness configuration must map workspace ids to harnesses')\n }\n // Every entry is checked, not only the caller's, so one bad entry fails every workspace's\n // first turn after the deploy instead of hiding until that workspace sends a message.\n const parsed = new Map(Object.entries(choices).map(([id, value]) =>\n [id, configHarness(value, `The harness for workspace ${id}`)]))\n return (workspaceId === undefined ? undefined : parsed.get(workspaceId)) ?? appDefault\n}\n\n/**\n * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.\n *\n * `Harness` is the dispatchable subset of `HarnessType`: this shell drops only canonical harnesses\n * for which the platform has no backend. Compatibility therefore comes entirely from Interface;\n * this package keeps no second harness taxonomy.\n */\n\nexport { modelProvider }\n\n/** A resolved model and the transport provider that will execute it. */\nexport interface HarnessModelSelection {\n model: string\n provider?: string\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n const snapped = aiSnapHarnessToModel(harness, modelId)\n if (!isHarness(snapped)) {\n throw new Error(\n `Harness \"${harness}\" snapped to \"${snapped}\" for model \"${modelId}\", which this platform has no backend for.`,\n )\n }\n return snapped\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * A resolved model tuple supplies provider evidence when its id is unqualified.\n * Call before dispatching a sandbox turn so a bypassed UI cannot reach the\n * sidecar with an incompatible or unproven pair. */\nexport function assertHarnessModelCompatible(\n harness: Harness,\n selection: string | HarnessModelSelection,\n): void {\n const modelId = typeof selection === 'string' ? selection : selection.model\n const transportProvider = typeof selection === 'string' ? undefined : selection.provider\n const providers = harnessProviders(harness)\n const provider = modelProvider(modelId) ?? transportProvider ?? null\n // An omitted/default model is deliberately left to the harness. Once a\n // caller names a model, however, a vendor-locked harness must have provider\n // evidence. Treating a router provider as compatible with every native CLI\n // is how a bare Gemini model reached Claude Code and produced \"Not logged in\".\n const isDefaultSentinel = modelId.trim() === '' || modelId.trim() === 'default'\n const compatible = isDefaultSentinel\n || providers === null\n || (provider !== null && providers.includes(provider))\n if (!compatible) {\n const native = preferredHarnessForModel(modelId)\n const providerDescription = provider ?? 'unqualified'\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${providerDescription}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list is derived from `@tangle-network/agent-interface`'s canonical\n * harness enum and mirrors the sandbox SDK's `BackendType` (no sandbox dependency). The consumer\n * owns storage — which harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n *\n * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it\n * comes from `@tangle-network/agent-interface`, the single source of truth shared with the\n * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the\n * session lock, and its server assertion also checks resolved transport-provider evidence.\n */\n\nimport {\n harnessProviders,\n harnessSupportsModel,\n harnessTypeSchema,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\n\n/**\n * Canonical harnesses that are NOT sandbox backends. Each value in `KNOWN_HARNESSES` is dispatched\n * as `backend.type`, so a harness the platform ships no provider adapter for must stay out —\n * offering it would resolve a session onto a runner that cannot start.\n */\nconst NON_BACKEND_HARNESSES = ['gemini'] as const satisfies readonly HarnessType[]\n\nconst nonBackendHarnesses = new Set<string>(NON_BACKEND_HARNESSES)\n\n/** A coding-agent backend this shell can dispatch a session onto. */\nexport type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>\n\n/**\n * The known coding-agent backends: the canonical harness set minus what has no provider adapter,\n * Derived from `harnessTypeSchema.options` so a harness added upstream reaches this shell without\n * an edit here, and kept structural so this module still needs no sandbox dependency.\n */\nexport const KNOWN_HARNESSES: readonly Harness[] = [\n ...harnessTypeSchema.options.filter(\n (harness): harness is Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]> =>\n !nonBackendHarnesses.has(harness),\n ),\n]\n\n/** Define the default harness to use for code execution and testing environments */\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\n/** Determine if a value is a recognized harness string identifier */\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\n/** Resolve input options to determine the appropriate session harness to use */\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\n/** Represent resolved session state including harness, lock status, and swap attempt flag */\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * An app's harness configuration, read from its deploy configuration (for example the env vars\n * `AGENT_HARNESS` and `AGENT_HARNESS_WORKSPACES`), so no product hard-codes `backend.type`.\n */\nexport interface HarnessConfig {\n /** The app default for new sessions. Unset or blank means {@link DEFAULT_HARNESS}. */\n default?: string\n /** Per-workspace choices: a JSON object mapping a workspace id to a harness. */\n workspaces?: string\n}\n\nfunction configHarness(value: unknown, where: string): Harness {\n if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`)\n return value\n}\n\n/**\n * The harness a workspace's new sessions start on: its own choice, else the app default, else\n * {@link DEFAULT_HARNESS}. Pass the result to {@link resolveSessionHarness} as `workspaceDefault`\n * so a session that already ran keeps its harness. A value naming an unknown harness throws:\n * a configuration typo must fail loudly, never run a different harness than the one configured.\n */\nexport function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness {\n const appDefault = config.default?.trim()\n ? configHarness(config.default.trim(), 'The harness default') : DEFAULT_HARNESS\n const raw = config.workspaces?.trim()\n if (!raw) return appDefault\n let choices: unknown\n try {\n choices = JSON.parse(raw)\n } catch {\n throw new Error('The per-workspace harness configuration is not JSON')\n }\n if (!choices || typeof choices !== 'object' || Array.isArray(choices)) {\n throw new Error('The per-workspace harness configuration must map workspace ids to harnesses')\n }\n // Every entry is checked, not only the caller's, so one bad entry fails every workspace's\n // first turn after the deploy instead of hiding until that workspace sends a message.\n const parsed = new Map(Object.entries(choices).map(([id, value]) =>\n [id, configHarness(value, `The harness for workspace ${id}`)]))\n return (workspaceId === undefined ? undefined : parsed.get(workspaceId)) ?? appDefault\n}\n\n/**\n * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.\n *\n * `Harness` is the dispatchable subset of `HarnessType`: this shell drops only canonical harnesses\n * for which the platform has no backend. Compatibility therefore comes entirely from Interface;\n * this package keeps no second harness taxonomy.\n */\n\nexport { modelProvider }\n\n/** A resolved model and the transport provider that will execute it. */\nexport interface HarnessModelSelection {\n model: string\n provider?: string\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n const snapped = aiSnapHarnessToModel(harness, modelId)\n if (!isHarness(snapped)) {\n throw new Error(\n `Harness \"${harness}\" snapped to \"${snapped}\" for model \"${modelId}\", which this platform has no backend for.`,\n )\n }\n return snapped\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * A resolved model tuple supplies provider evidence when its id is unqualified.\n * Call before dispatching a sandbox turn so a bypassed UI cannot reach the\n * sidecar with an incompatible or unproven pair. */\nexport function assertHarnessModelCompatible(\n harness: Harness,\n selection: string | HarnessModelSelection,\n): void {\n const modelId = typeof selection === 'string' ? selection : selection.model\n const transportProvider = typeof selection === 'string' ? undefined : selection.provider\n const providers = harnessProviders(harness)\n const provider = modelProvider(modelId) ?? transportProvider ?? null\n // An omitted/default model is deliberately left to the harness. Once a\n // caller names a model, however, a vendor-locked harness must have provider\n // evidence. Treating a router provider as compatible with every native CLI\n // is how a bare Gemini model reached Claude Code and produced \"Not logged in\".\n const isDefaultSentinel = modelId.trim() === '' || modelId.trim() === 'default'\n const compatible = isDefaultSentinel\n || providers === null\n || (provider !== null && providers.includes(provider))\n if (!compatible) {\n const native = preferredHarnessForModel(modelId)\n const providerDescription = provider ?? 'unqualified'\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${providerDescription}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n\n// ── Harness capabilities ───────────────────────────────────────────────────\n//\n// The one place a product learns WHAT a harness can express, before it\n// assembles a profile — so incompatible content is never added in the first\n// place instead of being dropped (or refused) after the fact. Facts here are\n// sourced from the platform materializer's behavior (the per-harness subagent\n// lowering, the MCP config channel, the startup control validation) and the\n// placement map; when those change, this table changes with them.\n\n/** What a harness can express, for profile assembly. */\nexport interface HarnessCapabilities {\n /** Lower `profile.subagents` into native cwd files (e.g. `.claude/agents/`).\n * False: a profile carrying subagents is refused at materialization —\n * builders must not add them. */\n subagents: boolean\n /** How `profile.mcp` reaches the harness. */\n mcp: 'native' | 'config-file' | 'extension' | 'none'\n /** When tool controls are validated: 'startup' means an allowlist naming\n * MCP-carried verbs is checked before MCP tools register (spawn-time\n * validation refuses unknown names); 'runtime' tolerates them. */\n toolControls: 'startup' | 'runtime'\n /** cwd-relative directory `resources.skills` materializes into, or null\n * when the harness has no cwd skill primitive (falls back to inline). */\n skillsDir: string | null\n}\n\nconst HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities> = {\n 'claude-code': { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n nanoclaw: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n opencode: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.opencode/skills' },\n codex: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.codex/skills' },\n 'kimi-code': { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.kimi/skills' },\n pi: { subagents: false, mcp: 'extension', toolControls: 'startup', skillsDir: '.pi/skills' },\n prime: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.prime/agent/skills' },\n hermes: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n openclaw: { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: 'skills' },\n // Harnesses the platform placement map does not bridge yet: conservative\n // facts — assert nothing they have not proven. Each upgrades to its real\n // facts when the materializer grows support for it.\n acp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n amp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'factory-droids': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n forge: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n cursor: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'cli-base': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n}\n\n/** The capability facts for `harness`. Every harness in KNOWN_HARNESSES has\n * an entry; the map is exhaustive by construction (tests assert parity). */\nexport function harnessCapabilities(harness: Harness): HarnessCapabilities {\n const caps = HARNESS_CAPABILITIES[harness]\n if (!caps) throw new Error(`harnessCapabilities: no capability facts for \"${harness}\"`)\n return caps\n}\n"],"mappings":"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","names":[]}
@@ -10,7 +10,7 @@ import {
10
10
  filterAcceptedFiles,
11
11
  isAcceptedFileType,
12
12
  renamePastedImages
13
- } from "./chunk-XSPUKVHF.js";
13
+ } from "./chunk-KICTB6RK.js";
14
14
  import {
15
15
  OVERLAY_SHADOW,
16
16
  POPOVER_OPTION_FOCUS,
@@ -1552,4 +1552,4 @@ export {
1552
1552
  ComposerModeControls,
1553
1553
  EntryComposer
1554
1554
  };
1555
- //# sourceMappingURL=chunk-ZEMO7S2G.js.map
1555
+ //# sourceMappingURL=chunk-PZHZV6QY.js.map
@@ -13,7 +13,7 @@ import {
13
13
  DEFAULT_HARNESS,
14
14
  assertHarnessModelCompatible,
15
15
  isHarness
16
- } from "./chunk-WVESWAJJ.js";
16
+ } from "./chunk-MXNVSY6J.js";
17
17
  import {
18
18
  assertProfilePromptWithinBudget,
19
19
  fingerprintAgentProfile
@@ -3343,4 +3343,4 @@ export {
3343
3343
  isTerminalPromptEvent,
3344
3344
  detectInteractiveQuestion
3345
3345
  };
3346
- //# sourceMappingURL=chunk-RHDP4VIG.js.map
3346
+ //# sourceMappingURL=chunk-RCRC2JKP.js.map