@shipers-dev/multi 0.52.0 → 0.63.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 (2) hide show
  1. package/dist/index.js +1518 -283
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import { createRequire } from "node:module";
3
4
  var __defProp = Object.defineProperty;
4
5
  var __returnValue = (v) => v;
5
6
  function __exportSetter(name, newValue) {
@@ -15,6 +16,7 @@ var __export = (target, all) => {
15
16
  });
16
17
  };
17
18
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
20
 
19
21
  // ../../node_modules/effect/dist/esm/Function.js
20
22
  function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
@@ -27970,7 +27972,7 @@ function finalize(ctx, schema) {
27970
27972
  result.$schema = "http://json-schema.org/draft-07/schema#";
27971
27973
  } else if (ctx.target === "draft-04") {
27972
27974
  result.$schema = "http://json-schema.org/draft-04/schema#";
27973
- } else if (ctx.target === "openapi-3.0") {} else {}
27975
+ } else if (ctx.target === "openapi-3.0") {}
27974
27976
  if (ctx.external?.uri) {
27975
27977
  const id3 = ctx.external.registry.get(schema)?.id;
27976
27978
  if (!id3)
@@ -28235,7 +28237,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
28235
28237
  if (val === undefined) {
28236
28238
  if (ctx.unrepresentable === "throw") {
28237
28239
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
28238
- } else {}
28240
+ }
28239
28241
  } else if (typeof val === "bigint") {
28240
28242
  if (ctx.unrepresentable === "throw") {
28241
28243
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -33196,6 +33198,35 @@ function isPidAlive(pid) {
33196
33198
  return false;
33197
33199
  }
33198
33200
  }
33201
+ function listAdapterProcs() {
33202
+ try {
33203
+ const r = spawnSync("ps", ["-axo", "pid=,ppid=,command="], { encoding: "utf8" });
33204
+ if (r.status !== 0)
33205
+ return [];
33206
+ const out = [];
33207
+ for (const line of (r.stdout || "").split(`
33208
+ `)) {
33209
+ const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
33210
+ if (!m)
33211
+ continue;
33212
+ const command = m[3];
33213
+ if (!command.includes("claude-code-acp") && !command.includes("acpx"))
33214
+ continue;
33215
+ out.push({ pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), command });
33216
+ }
33217
+ return out;
33218
+ } catch {
33219
+ return [];
33220
+ }
33221
+ }
33222
+ function killOne(pid, log3, label, command) {
33223
+ log3(`[reap] ${label} pid=${pid} cmd=${command.slice(0, 120)}`);
33224
+ tryKill(pid, "SIGTERM");
33225
+ setTimeout(() => {
33226
+ if (isPidAlive(pid))
33227
+ tryKill(pid, "SIGKILL");
33228
+ }, 1500).unref?.();
33229
+ }
33199
33230
  function reapStaleAdapters(log3 = () => {}) {
33200
33231
  ensureDir();
33201
33232
  let entries2 = [];
@@ -33247,8 +33278,37 @@ function reapStaleAdapters(log3 = () => {}) {
33247
33278
  } catch {}
33248
33279
  reaped++;
33249
33280
  }
33281
+ const accountedFor = new Set;
33282
+ try {
33283
+ for (const name of readdirSync2(ADAPTERS_DIR)) {
33284
+ const m = name.match(/^(\d+)\.json$/);
33285
+ if (m)
33286
+ accountedFor.add(parseInt(m[1], 10));
33287
+ }
33288
+ } catch {}
33289
+ for (const p of listAdapterProcs()) {
33290
+ if (p.ppid !== 1)
33291
+ continue;
33292
+ if (accountedFor.has(p.pid))
33293
+ continue;
33294
+ killOne(p.pid, log3, "killing pidfile-less orphan", p.command);
33295
+ reaped++;
33296
+ }
33250
33297
  return reaped;
33251
33298
  }
33299
+ function killDaemonChildren(daemonPid, log3 = () => {}) {
33300
+ let killed = 0;
33301
+ for (const p of listAdapterProcs()) {
33302
+ if (p.ppid !== daemonPid)
33303
+ continue;
33304
+ killOne(p.pid, log3, "daemon-exit killing", p.command);
33305
+ try {
33306
+ unlinkSync3(pidfilePath(p.pid));
33307
+ } catch {}
33308
+ killed++;
33309
+ }
33310
+ return killed;
33311
+ }
33252
33312
  var ADAPTERS_DIR;
33253
33313
  var init_adapter_pidfile = __esm(() => {
33254
33314
  ADAPTERS_DIR = join7(homedir(), ".multi", "adapters");
@@ -33456,8 +33516,20 @@ async function runAcp(opts) {
33456
33516
  return { stopReason, sessionId: activeSessionId, summaryText };
33457
33517
  } finally {
33458
33518
  try {
33459
- child.kill();
33519
+ child.kill("SIGTERM");
33460
33520
  } catch {}
33521
+ const killTimer = setTimeout(() => {
33522
+ try {
33523
+ child.kill("SIGKILL");
33524
+ } catch {}
33525
+ }, 3000);
33526
+ try {
33527
+ killTimer.unref?.();
33528
+ } catch {}
33529
+ try {
33530
+ await child.exited;
33531
+ } catch {}
33532
+ clearTimeout(killTimer);
33461
33533
  if (typeof child.pid === "number")
33462
33534
  removeAdapterPidfile(child.pid);
33463
33535
  }
@@ -34148,12 +34220,12 @@ function parsePlanBlocks(text) {
34148
34220
  }
34149
34221
  return { actions, errors: errors3 };
34150
34222
  }
34151
- var PLAN_SCHEMA_VERSION = 7, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34223
+ var PLAN_SCHEMA_VERSION = 10, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34152
34224
  var init_plans = __esm(() => {
34153
34225
  init_zod();
34154
34226
  Priority = exports_external.enum(["low", "medium", "high"]);
34155
34227
  AssigneeType = exports_external.enum(["human", "agent"]);
34156
- IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped", "cancelled"]);
34228
+ IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped"]);
34157
34229
  SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
34158
34230
  SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
34159
34231
  EvalPolicy = exports_external.object({
@@ -34193,6 +34265,14 @@ var init_plans = __esm(() => {
34193
34265
  id: exports_external.string().min(1),
34194
34266
  assignee_id: exports_external.string().min(1)
34195
34267
  }),
34268
+ exports_external.object({
34269
+ type: exports_external.literal("handoff"),
34270
+ target_agent_id: exports_external.string().min(1),
34271
+ prompt: exports_external.string().min(1).max(8000),
34272
+ expect: exports_external.enum(["summary", "patch"]).optional(),
34273
+ return_to: exports_external.string().min(1).optional(),
34274
+ title: exports_external.string().min(1).max(200).optional()
34275
+ }),
34196
34276
  exports_external.object({
34197
34277
  type: exports_external.literal("issue.comment"),
34198
34278
  id: exports_external.string().min(1),
@@ -34222,6 +34302,18 @@ var init_plans = __esm(() => {
34222
34302
  query: exports_external.string().min(1).max(500),
34223
34303
  limit: exports_external.number().int().min(1).max(100).optional()
34224
34304
  }),
34305
+ exports_external.object({
34306
+ type: exports_external.literal("issue.get"),
34307
+ id: exports_external.string().min(1)
34308
+ }),
34309
+ exports_external.object({
34310
+ type: exports_external.literal("issue.start"),
34311
+ id: exports_external.string().min(1)
34312
+ }),
34313
+ exports_external.object({
34314
+ type: exports_external.literal("issue.stop"),
34315
+ id: exports_external.string().min(1)
34316
+ }),
34225
34317
  exports_external.object({
34226
34318
  type: exports_external.literal("agent.create"),
34227
34319
  name: exports_external.string().min(1).max(120),
@@ -34359,13 +34451,13 @@ var init_chat = __esm(() => {
34359
34451
  title: exports_external.string().min(1).max(200),
34360
34452
  primary_agent_id: exports_external.string().nullable().optional(),
34361
34453
  device_id: exports_external.string().nullable().optional(),
34362
- runtime: exports_external.string().nullable().optional()
34454
+ runtime: exports_external.string().nullable().optional(),
34455
+ working_dir: exports_external.string().nullable().optional(),
34456
+ branch: exports_external.string().nullable().optional()
34363
34457
  });
34364
34458
  UpdateChatBodySchema = exports_external.object({
34365
34459
  title: exports_external.string().min(1).max(200).optional(),
34366
- primary_agent_id: exports_external.string().nullable().optional(),
34367
- device_id: exports_external.string().nullable().optional(),
34368
- runtime: exports_external.string().nullable().optional()
34460
+ primary_agent_id: exports_external.string().nullable().optional()
34369
34461
  });
34370
34462
  });
34371
34463
  // ../lib/index.ts
@@ -34679,14 +34771,28 @@ function log3(msg) {
34679
34771
  appendFileSync4(LOG_PATH3, line);
34680
34772
  process.stdout.write(line);
34681
34773
  }
34682
- function nestedIssueBase(apiUrl, wsId, projectId, issueId) {
34683
- return `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
34684
- }
34685
34774
  async function patchIssueStatus(apiUrl, wsId, issueId, status3) {
34775
+ if (status3 === "todo") {
34776
+ throw new Error("patchIssueStatus: cannot return an issue to todo (Phase 4 one-way invariant)");
34777
+ }
34686
34778
  if (!wsId)
34687
34779
  return { success: false, error: "no workspace id" };
34688
34780
  return apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, { action: "update", id: issueId, status: status3 });
34689
34781
  }
34782
+ async function postBoundChatMessage(apiUrl, task, body, authorKind = "agent") {
34783
+ const wsId = task?.tenant_workspace_id ?? null;
34784
+ if (!wsId)
34785
+ return;
34786
+ try {
34787
+ await apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, {
34788
+ action: "comment",
34789
+ id: task.issue_id,
34790
+ body
34791
+ });
34792
+ } catch (e) {
34793
+ log3(`[bound-chat] post failed: ${String(e).slice(0, 200)}`);
34794
+ }
34795
+ }
34690
34796
  async function markStopped(apiUrl, task, reason) {
34691
34797
  const wsId = task?.tenant_workspace_id ?? null;
34692
34798
  const pid = task?.project_id ?? null;
@@ -34696,14 +34802,7 @@ async function markStopped(apiUrl, task, reason) {
34696
34802
  await patchIssueStatus(apiUrl, wsId, issueId, "stopped");
34697
34803
  } catch {}
34698
34804
  try {
34699
- if (wsId && pid) {
34700
- await apiClient.post(`${nestedIssueBase(apiUrl, wsId, pid, issueId)}/comments`, {
34701
- author_type: "agent",
34702
- author_id: "daemon",
34703
- author_name: "daemon",
34704
- body: `⏹ Stopped: ${reason}`
34705
- });
34706
- }
34805
+ await postBoundChatMessage(apiUrl, task, `⏹ Stopped: ${reason}`);
34707
34806
  } catch {}
34708
34807
  await postStream(apiUrl, issueId, "stopped", { reason });
34709
34808
  }
@@ -34817,48 +34916,18 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34817
34916
  if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
34818
34917
  const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
34819
34918
  const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
34820
- attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
34821
- if (attachmentRefs.length)
34822
- log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
34919
+ try {
34920
+ attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
34921
+ if (attachmentRefs.length)
34922
+ log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
34923
+ } catch (e) {
34924
+ log3(`[from_comment_id] attachment fetch failed — comments table may be gone (Phase 6): ${String(e).slice(0, 200)}`);
34925
+ }
34823
34926
  }
34824
34927
  const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
34825
- let liveCommentId;
34826
- let liveBody = "";
34827
34928
  let hadError = false;
34828
34929
  let hasAssistantText = false;
34829
- let liveCommentPromise = null;
34830
34930
  let memorySummary = null;
34831
- const ensureLiveComment = () => {
34832
- if (liveCommentId)
34833
- return Promise.resolve();
34834
- if (!ISSUE_BASE)
34835
- return Promise.resolve();
34836
- if (!liveCommentPromise) {
34837
- liveCommentPromise = apiClient.post(`${ISSUE_BASE}/comments`, {
34838
- author_type: "agent",
34839
- author_id: task.agent_id,
34840
- author_name: "agent",
34841
- body: "…"
34842
- }).then((res) => {
34843
- liveCommentId = res.data?.id;
34844
- });
34845
- }
34846
- return liveCommentPromise;
34847
- };
34848
- const patchLive = async (body) => {
34849
- if (!liveCommentId || !ISSUE_BASE)
34850
- return;
34851
- try {
34852
- await apiClient.patch(`${ISSUE_BASE}/comments/${liveCommentId}`, { body: body || "…" });
34853
- } catch {}
34854
- };
34855
- const postComment = async (body) => {
34856
- if (!ISSUE_BASE)
34857
- return;
34858
- try {
34859
- await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body });
34860
- } catch {}
34861
- };
34862
34931
  const turn = {
34863
34932
  blocks: [],
34864
34933
  tools: new Map,
@@ -34961,18 +35030,7 @@ _${bits.join(" · ")}_`);
34961
35030
 
34962
35031
  `) || "…";
34963
35032
  };
34964
- let renderScheduled = false;
34965
- const schedulePatch = () => {
34966
- if (renderScheduled)
34967
- return;
34968
- renderScheduled = true;
34969
- queueMicrotask(async () => {
34970
- renderScheduled = false;
34971
- try {
34972
- await patchLive(render2());
34973
- } catch {}
34974
- });
34975
- };
35033
+ const schedulePatch = () => {};
34976
35034
  const eventHandler = async (event) => {
34977
35035
  if (event.event_type === "error")
34978
35036
  hadError = true;
@@ -34985,7 +35043,6 @@ _${bits.join(" · ")}_`);
34985
35043
  break;
34986
35044
  }
34987
35045
  case "assistant_text": {
34988
- await ensureLiveComment();
34989
35046
  appendText(p.text);
34990
35047
  hasAssistantText = true;
34991
35048
  schedulePatch();
@@ -34993,7 +35050,6 @@ _${bits.join(" · ")}_`);
34993
35050
  }
34994
35051
  case "stdout": {
34995
35052
  if (p.line) {
34996
- await ensureLiveComment();
34997
35053
  appendText((turn.blocks.length ? `
34998
35054
  ` : "") + p.line);
34999
35055
  hasAssistantText = true;
@@ -35002,14 +35058,12 @@ _${bits.join(" · ")}_`);
35002
35058
  break;
35003
35059
  }
35004
35060
  case "tool_call": {
35005
- await ensureLiveComment();
35006
35061
  const id3 = p.id || `anon-${turn.tools.size}`;
35007
35062
  upsertTool(id3, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
35008
35063
  schedulePatch();
35009
35064
  break;
35010
35065
  }
35011
35066
  case "tool_result": {
35012
- await ensureLiveComment();
35013
35067
  const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
35014
35068
  const id3 = p.tool_use_id || lastToolId;
35015
35069
  const entry = id3 ? turn.tools.get(id3) : undefined;
@@ -35026,7 +35080,6 @@ _${bits.join(" · ")}_`);
35026
35080
  break;
35027
35081
  }
35028
35082
  case "result": {
35029
- await ensureLiveComment();
35030
35083
  if (p.is_error)
35031
35084
  hadError = true;
35032
35085
  if (!hasAssistantText && p.result) {
@@ -35038,7 +35091,6 @@ _${bits.join(" · ")}_`);
35038
35091
  break;
35039
35092
  }
35040
35093
  case "error": {
35041
- await ensureLiveComment();
35042
35094
  turn.error = p.message || "error";
35043
35095
  schedulePatch();
35044
35096
  break;
@@ -35265,10 +35317,33 @@ ${userPart}` : userPart;
35265
35317
  for await (const event of runner(task))
35266
35318
  await eventHandler(event);
35267
35319
  }
35268
- if (liveCommentId && task.tenant_workspace_id && task.project_id) {
35269
- const n = await uploadOutputDir(apiUrl, task.tenant_workspace_id, task.project_id, task.issue_id, liveCommentId, outDir);
35270
- if (n > 0)
35271
- log3(` \uD83D\uDCCE uploaded ${n} output file(s)`);
35320
+ const finalBody = (() => {
35321
+ const parts2 = [];
35322
+ for (const b of turn.blocks) {
35323
+ if (b.kind === "text") {
35324
+ if (b.text)
35325
+ parts2.push(b.text);
35326
+ } else if (b.kind === "tool") {
35327
+ const t = turn.tools.get(b.id);
35328
+ if (t)
35329
+ parts2.push(`- \uD83D\uDD27 ${t.tool}${t.status === "completed" ? " ✓" : t.status === "failed" ? " ✗" : ""}`);
35330
+ }
35331
+ }
35332
+ return parts2.join(`
35333
+ `).trim();
35334
+ })();
35335
+ if (finalBody)
35336
+ await postBoundChatMessage(apiUrl, task, finalBody);
35337
+ if (existsSync12(outDir)) {
35338
+ const files = readdirSync3(outDir).filter((f) => {
35339
+ try {
35340
+ return statSync2(join12(outDir, f)).isFile();
35341
+ } catch {
35342
+ return false;
35343
+ }
35344
+ });
35345
+ if (files.length > 0)
35346
+ log3(` [Phase 4.5] agent wrote ${files.length} file(s) to .multi-out; attachments via chat upload not wired in this path`);
35272
35347
  }
35273
35348
  if (ctx) {
35274
35349
  const fullText = turn.blocks.filter((b) => b.kind === "text").map((b) => b.text).join(`
@@ -35278,15 +35353,14 @@ ${userPart}` : userPart;
35278
35353
  const summary5 = await executePlanActions(apiUrl, task, actions, ctx, parseErrors);
35279
35354
  if (summary5) {
35280
35355
  try {
35281
- if (ISSUE_BASE)
35282
- await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body: summary5 });
35356
+ await postBoundChatMessage(apiUrl, task, summary5);
35283
35357
  } catch {}
35284
35358
  }
35285
35359
  }
35286
35360
  }
35287
35361
  if (!hasAssistantText && !hadError) {
35288
35362
  const stopReason = turn.result?.stopReason || "unknown";
35289
- await postComment(`⚠️ Agent returned no output (stopReason=${stopReason}). Adapter may be stuck on a stale session — try starting a new issue or clearing session_id.`);
35363
+ await postBoundChatMessage(apiUrl, task, `⚠️ Agent returned no output (stopReason=${stopReason}). Adapter may be stuck on a stale session — try starting a new issue or clearing session_id.`);
35290
35364
  log3(` ⚠ ${task.key} produced no assistant output (stopReason=${stopReason})`);
35291
35365
  }
35292
35366
  if (ctx?.runEntry?.stopped) {
@@ -35319,7 +35393,7 @@ ${userPart}` : userPart;
35319
35393
  log3(` ⏹ ${task.key} stopped (${msg})`);
35320
35394
  } else {
35321
35395
  await postStream(apiUrl, issueId, "error", { message: msg });
35322
- await postComment(`❌ spawn error: ${msg}`);
35396
+ await postBoundChatMessage(apiUrl, task, `❌ spawn error: ${msg}`);
35323
35397
  if (ISSUE_BASE)
35324
35398
  await apiClient.post(`${ISSUE_BASE}/fail`, {});
35325
35399
  log3(` ✗ ${task.key} failed: ${msg}`);
@@ -35381,6 +35455,9 @@ Issue actions:
35381
35455
  {"type":"issue.delete_where","status":"todo"},
35382
35456
  {"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
35383
35457
  {"type":"issue.search","query":"flaky tests","limit":10},
35458
+ {"type":"issue.get","id":"<issue id or key>"},
35459
+ {"type":"issue.start","id":"<issue id or key>"},
35460
+ {"type":"issue.stop","id":"<issue id or key>"},
35384
35461
  {"type":"memory.search","query":"how does the deploy pipeline work","limit":10},
35385
35462
  {"type":"memory.write","text":"Long-form note worth remembering across sessions...","summary":"short index hint","kind":"agent_note"}
35386
35463
  ]}
@@ -35388,9 +35465,11 @@ Issue actions:
35388
35465
 
35389
35466
  Status values: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when you are pausing to wait on a human decision** — confirmation, a choice between approaches, or missing context. Marking such an issue \`done\` is wrong: the work isn't finished, you're waiting on review. Use \`archived\` to hide a completed/abandoned issue from default views.
35390
35467
 
35468
+ **\`todo\` is a one-way exit.** Once an issue moves to any other status, it cannot return to \`todo\`. Use \`blocked\` to pause an issue that needs human input.
35469
+
35391
35470
  Prefer the bulk \`issue.delete_where\` over \`issue.list\` + per-issue \`issue.delete\` when the user's intent matches a filter ("delete all todo issues", "remove anything assigned to agent X"). It runs in one turn instead of two.
35392
35471
 
35393
- Read actions (\`issue.list\`, \`issue.search\`, \`memory.search\`) return the matched rows in the action summary comment. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting).
35472
+ Read actions (\`issue.list\`, \`issue.search\`, \`issue.get\`, \`memory.search\`) return the matched rows in the action summary message posted to the issue's bound chat. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting). Use \`issue.get\` to deep-read a single issue (status, autonomy, assignee, live dispatch, bound chat) before deciding to start/stop it. \`issue.start\` dispatches the assigned agent (no-op if no agent is assigned). \`issue.stop\` gracefully stops the live dispatch; safe to call on a non-running issue.
35394
35473
 
35395
35474
  Memory actions are project-scoped persistent storage (FTS5 + vector). Use \`memory.search\` to recall facts learned in past sessions; the hits land in the next-turn context like \`issue.search\`. Use \`memory.write\` sparingly to record durable notes (decisions, gotchas, references) that will be useful to future agents on this project — not transient task state. \`kind\` defaults to \`agent_note\`; use a stable kind string (e.g. \`decision\`, \`gotcha\`) if you want to filter later.
35396
35475
 
@@ -35442,7 +35521,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35442
35521
  results.push({ type: "note", status: "note", message: `${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})` });
35443
35522
  }
35444
35523
  }
35445
- const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3, "memory.search": 5, "memory.write": 5 };
35524
+ const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3, "memory.search": 5, "memory.write": 5, "issue.get": 10, "issue.start": 3, "issue.stop": 3 };
35446
35525
  const counts = {};
35447
35526
  actions = actions.filter((a) => {
35448
35527
  const cap = SUBCAPS[a.type];
@@ -35624,6 +35703,42 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35624
35703
  lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
35625
35704
  }
35626
35705
  results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
35706
+ } else if (a.type === "issue.get") {
35707
+ const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
35708
+ if (!res.success) {
35709
+ lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
35710
+ results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
35711
+ continue;
35712
+ }
35713
+ const r = res.data || {};
35714
+ const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
35715
+ lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
35716
+ lines.push(` - ${r.title}`);
35717
+ if (r.bound_chat_id)
35718
+ lines.push(` - bound chat: ${r.bound_chat_id}`);
35719
+ if (r.live_dispatch?.last_error)
35720
+ lines.push(` - last error: ${r.live_dispatch.last_error}`);
35721
+ results.push({ type: "issue.get", status: "ok", issue_id: r.id, key: r.key, issue_status: r.status, autonomy_level: r.autonomy_level, assignee_id: r.assignee_id, bound_chat_id: r.bound_chat_id, live_dispatch_status: r.live_dispatch?.status ?? null });
35722
+ } else if (a.type === "issue.start") {
35723
+ const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
35724
+ if (!res.success) {
35725
+ lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
35726
+ results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
35727
+ continue;
35728
+ }
35729
+ const r = res.data || {};
35730
+ lines.push(r.dispatched ? `- [ok] issue.start ${r.key || a.id} -> dispatched` : `- [warn] issue.start ${r.key || a.id} not dispatched (autonomy=${r.autonomy_level} or no online device)`);
35731
+ results.push({ type: "issue.start", status: "ok", issue_id: r.id || a.id, key: r.key, dispatched: !!r.dispatched, dispatch_id: r.dispatch_id ?? null, autonomy_level: r.autonomy_level });
35732
+ } else if (a.type === "issue.stop") {
35733
+ const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
35734
+ if (!res.success) {
35735
+ lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
35736
+ results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
35737
+ continue;
35738
+ }
35739
+ const r = res.data || {};
35740
+ lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
35741
+ results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
35627
35742
  } else if (a.type === "memory.search") {
35628
35743
  const projectId = a.project_id || parentProjectId;
35629
35744
  if (!projectId) {
@@ -35916,56 +36031,6 @@ function authTokenHeader() {
35916
36031
  return null;
35917
36032
  }
35918
36033
  }
35919
- async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
35920
- if (!existsSync12(dir))
35921
- return 0;
35922
- const files = [];
35923
- const walk = (d, depth = 0) => {
35924
- if (depth > 3)
35925
- return;
35926
- for (const name of readdirSync3(d)) {
35927
- const p = join12(d, name);
35928
- try {
35929
- const st = statSync2(p);
35930
- if (st.isDirectory())
35931
- walk(p, depth + 1);
35932
- else if (st.isFile())
35933
- files.push(p);
35934
- } catch {}
35935
- }
35936
- };
35937
- walk(dir);
35938
- if (!files.length)
35939
- return 0;
35940
- const token = authTokenHeader();
35941
- const issueBase = `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
35942
- let uploaded = 0;
35943
- for (const f of files) {
35944
- try {
35945
- const data = readFileSync10(f);
35946
- const form = new FormData;
35947
- const blob = new Blob([data]);
35948
- form.append("file", blob, f.split("/").pop() || "file");
35949
- const headers = {};
35950
- if (token)
35951
- headers.Authorization = token;
35952
- const res = await fetch(`${issueBase}/comments/${commentId}/attachments`, {
35953
- method: "POST",
35954
- body: form,
35955
- headers
35956
- });
35957
- if (res.ok) {
35958
- uploaded++;
35959
- try {
35960
- unlinkSync6(f);
35961
- } catch {}
35962
- }
35963
- } catch (e) {
35964
- log3(`upload failed for ${f}: ${String(e)}`);
35965
- }
35966
- }
35967
- return uploaded;
35968
- }
35969
36034
  function pickRunner(detected, preferType) {
35970
36035
  const forceStub = process.env.MULTI_STUB === "1";
35971
36036
  if (forceStub || !detected.length)
@@ -36239,6 +36304,116 @@ var init_run_task = __esm(() => {
36239
36304
  SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
36240
36305
  });
36241
36306
 
36307
+ // src/_impl/worktree-create.ts
36308
+ var exports_worktree_create = {};
36309
+ __export(exports_worktree_create, {
36310
+ removeWorktree: () => removeWorktree2,
36311
+ materialiseWorktree: () => materialiseWorktree,
36312
+ codename: () => codename
36313
+ });
36314
+ import { homedir as homedir3 } from "os";
36315
+ import { existsSync as existsSync13 } from "fs";
36316
+ import { join as join13 } from "path";
36317
+ function codename() {
36318
+ const c = CITY_CODENAMES[Math.floor(Math.random() * CITY_CODENAMES.length)];
36319
+ const suffix = Math.random().toString(36).slice(2, 6);
36320
+ return `${c}-${suffix}`;
36321
+ }
36322
+ async function run5(cmd, cwd) {
36323
+ const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
36324
+ const [stdout, stderr] = await Promise.all([
36325
+ new Response(proc.stdout).text(),
36326
+ new Response(proc.stderr).text()
36327
+ ]);
36328
+ await proc.exited;
36329
+ return { ok: proc.exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim() };
36330
+ }
36331
+ async function materialiseWorktree(opts) {
36332
+ const { baseDir, name, log: log4 } = opts;
36333
+ if (!existsSync13(baseDir)) {
36334
+ throw new Error(`base_dir does not exist: ${baseDir}`);
36335
+ }
36336
+ const top = await run5(["git", "rev-parse", "--show-toplevel"], baseDir);
36337
+ if (!top.ok || !top.stdout) {
36338
+ throw new Error(`base_dir is not in a git repo: ${baseDir}`);
36339
+ }
36340
+ const repoRoot = top.stdout;
36341
+ let baseBranch = opts.baseBranch;
36342
+ if (!baseBranch) {
36343
+ const head5 = await run5(["git", "rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
36344
+ baseBranch = head5.ok ? head5.stdout : "HEAD";
36345
+ }
36346
+ const worktreesDir = join13(homedir3(), ".multi", "worktrees");
36347
+ const path = join13(worktreesDir, name);
36348
+ if (existsSync13(path)) {
36349
+ throw new Error(`worktree path already exists: ${path}`);
36350
+ }
36351
+ const branch = `multi/${name}`;
36352
+ log4(`worktree: creating ${path} (branch ${branch} off ${baseBranch})`);
36353
+ const create = await run5(["git", "worktree", "add", "-b", branch, path, baseBranch], repoRoot);
36354
+ if (!create.ok) {
36355
+ throw new Error(`git worktree add failed: ${create.stderr || create.stdout}`);
36356
+ }
36357
+ return { path, branch, base_branch: baseBranch, name };
36358
+ }
36359
+ async function removeWorktree2(path, log4) {
36360
+ if (!existsSync13(path)) {
36361
+ return { ok: true };
36362
+ }
36363
+ const r = await run5(["git", "worktree", "remove", "--force", path], path);
36364
+ if (!r.ok) {
36365
+ log4(`worktree remove failed: ${r.stderr || r.stdout}`);
36366
+ return { ok: false, error: r.stderr || r.stdout };
36367
+ }
36368
+ return { ok: true };
36369
+ }
36370
+ var CITY_CODENAMES;
36371
+ var init_worktree_create = __esm(() => {
36372
+ CITY_CODENAMES = [
36373
+ "raleigh",
36374
+ "yokohama",
36375
+ "sydney",
36376
+ "porto",
36377
+ "kyoto",
36378
+ "bristol",
36379
+ "oslo",
36380
+ "havana",
36381
+ "lisbon",
36382
+ "tallinn",
36383
+ "salem",
36384
+ "marrakech",
36385
+ "perth",
36386
+ "boise",
36387
+ "nantes",
36388
+ "munich",
36389
+ "asheville",
36390
+ "stockholm",
36391
+ "halifax",
36392
+ "kolkata",
36393
+ "tucson",
36394
+ "valencia",
36395
+ "kazan",
36396
+ "savannah",
36397
+ "lyon",
36398
+ "iquitos",
36399
+ "wuhan",
36400
+ "leeds",
36401
+ "ankara",
36402
+ "auckland",
36403
+ "geneva",
36404
+ "izmir",
36405
+ "anchorage",
36406
+ "split",
36407
+ "kigali",
36408
+ "miami",
36409
+ "kazan",
36410
+ "krakow",
36411
+ "bilbao",
36412
+ "calgary",
36413
+ "dakar"
36414
+ ];
36415
+ });
36416
+
36242
36417
  // ../lib/chat-doc.ts
36243
36418
  import { LoroDoc, LoroMap, LoroList } from "loro-crdt";
36244
36419
  function appendMessage(doc2, msg) {
@@ -36328,8 +36503,8 @@ var init_chat_doc = __esm(() => {
36328
36503
  });
36329
36504
 
36330
36505
  // src/_impl/chat-peer.ts
36331
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
36332
- import { dirname as dirname10, join as join13 } from "path";
36506
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
36507
+ import { dirname as dirname10, join as join14 } from "path";
36333
36508
  import { LoroDoc as LoroDoc2 } from "loro-crdt";
36334
36509
 
36335
36510
  class ChatPeer {
@@ -36341,19 +36516,20 @@ class ChatPeer {
36341
36516
  dirtySinceWrite = 0;
36342
36517
  seenIds = new Set;
36343
36518
  closed = false;
36519
+ localSubs = new Map;
36344
36520
  firstFrameSinceConnect = true;
36345
36521
  reconnectTimer = null;
36346
36522
  constructor(opts) {
36347
36523
  this.opts = opts;
36348
36524
  this.chatId = opts.chatId;
36349
- this.snapshotPath = join13(MULTI_DIR, "chats", `${opts.chatId}.loro`);
36525
+ this.snapshotPath = join14(MULTI_DIR, "chats", `${opts.chatId}.loro`);
36350
36526
  this.doc = this.loadFromDisk();
36351
36527
  for (const m of listMessages(this.doc))
36352
36528
  this.seenIds.add(m.id);
36353
36529
  }
36354
36530
  loadFromDisk() {
36355
36531
  try {
36356
- if (existsSync13(this.snapshotPath)) {
36532
+ if (existsSync14(this.snapshotPath)) {
36357
36533
  const bytes = readFileSync11(this.snapshotPath);
36358
36534
  return importSnapshot(new Uint8Array(bytes));
36359
36535
  }
@@ -36361,7 +36537,7 @@ class ChatPeer {
36361
36537
  return new LoroDoc2;
36362
36538
  }
36363
36539
  persist() {
36364
- if (!existsSync13(dirname10(this.snapshotPath)))
36540
+ if (!existsSync14(dirname10(this.snapshotPath)))
36365
36541
  mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
36366
36542
  writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
36367
36543
  this.dirtySinceWrite = 0;
@@ -36517,14 +36693,70 @@ class ChatPeer {
36517
36693
  }, 3000);
36518
36694
  }
36519
36695
  sendLoro(update5) {
36520
- if (!this.ws || this.ws.readyState !== 1)
36521
- return;
36522
36696
  const frame = new Uint8Array(1 + update5.byteLength);
36523
36697
  frame[0] = FRAME_LORO;
36524
36698
  frame.set(update5, 1);
36699
+ if (this.ws && this.ws.readyState === 1) {
36700
+ try {
36701
+ this.ws.send(frame);
36702
+ } catch {}
36703
+ }
36704
+ this.broadcastLocal(frame, null);
36705
+ }
36706
+ broadcastLocal(frame, except) {
36707
+ for (const [token, send] of this.localSubs) {
36708
+ if (token === except)
36709
+ continue;
36710
+ try {
36711
+ send(frame);
36712
+ } catch {}
36713
+ }
36714
+ }
36715
+ addLocalSubscriber(send) {
36716
+ const token = Symbol("local-chat-sub");
36717
+ this.localSubs.set(token, send);
36718
+ return {
36719
+ token,
36720
+ close: () => {
36721
+ this.localSubs.delete(token);
36722
+ }
36723
+ };
36724
+ }
36725
+ buildSnapshotGreet() {
36726
+ const snap = exportSnapshot(this.doc);
36727
+ const frame = new Uint8Array(1 + snap.byteLength);
36728
+ frame[0] = FRAME_LORO;
36729
+ frame.set(snap, 1);
36730
+ return frame;
36731
+ }
36732
+ ingestLocalUpdate(update5, fromToken) {
36733
+ const before2 = new Set(this.seenIds);
36525
36734
  try {
36526
- this.ws.send(frame);
36527
- } catch {}
36735
+ applyUpdate(this.doc, update5);
36736
+ } catch {
36737
+ return;
36738
+ }
36739
+ const frame = new Uint8Array(1 + update5.byteLength);
36740
+ frame[0] = FRAME_LORO;
36741
+ frame.set(update5, 1);
36742
+ if (this.ws && this.ws.readyState === 1) {
36743
+ try {
36744
+ this.ws.send(frame);
36745
+ } catch {}
36746
+ }
36747
+ this.broadcastLocal(frame, fromToken);
36748
+ for (const m of listMessages(this.doc)) {
36749
+ if (before2.has(m.id))
36750
+ continue;
36751
+ this.seenIds.add(m.id);
36752
+ if (m.author?.kind === "user" && !m.partial && this.opts.onUserMessage) {
36753
+ const cb = this.opts.onUserMessage;
36754
+ Promise.resolve().then(() => cb(m, this)).catch((e) => this.opts.log(`[chat ${this.chatId}] onUserMessage error: ${e.message}`));
36755
+ }
36756
+ }
36757
+ this.dirtySinceWrite++;
36758
+ if (this.dirtySinceWrite >= 8)
36759
+ this.persist();
36528
36760
  }
36529
36761
  sendJson(obj) {
36530
36762
  if (!this.ws || this.ws.readyState !== 1)
@@ -36544,13 +36776,20 @@ class ChatPeer {
36544
36776
  const tag3 = buf[0];
36545
36777
  if (tag3 === FRAME_LORO) {
36546
36778
  const before2 = new Set(this.seenIds);
36779
+ const updateBytes = buf.subarray(1);
36547
36780
  try {
36548
- applyUpdate(this.doc, buf.subarray(1));
36781
+ applyUpdate(this.doc, updateBytes);
36549
36782
  } catch {
36550
36783
  return;
36551
36784
  }
36552
36785
  const isFirstFrame = this.firstFrameSinceConnect;
36553
36786
  this.firstFrameSinceConnect = false;
36787
+ if (!isFirstFrame && this.localSubs.size > 0) {
36788
+ const frame = new Uint8Array(1 + updateBytes.byteLength);
36789
+ frame[0] = FRAME_LORO;
36790
+ frame.set(updateBytes, 1);
36791
+ this.broadcastLocal(frame, null);
36792
+ }
36554
36793
  for (const m of listMessages(this.doc)) {
36555
36794
  if (before2.has(m.id))
36556
36795
  continue;
@@ -36906,6 +37145,48 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
36906
37145
  lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
36907
37146
  results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
36908
37147
  tally(true);
37148
+ } else if (a.type === "issue.get") {
37149
+ const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
37150
+ if (!res.success) {
37151
+ lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
37152
+ results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
37153
+ tally(false);
37154
+ continue;
37155
+ }
37156
+ const r = res.data || {};
37157
+ const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
37158
+ lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
37159
+ lines.push(` - ${r.title}`);
37160
+ if (r.bound_chat_id)
37161
+ lines.push(` - bound chat: ${r.bound_chat_id}`);
37162
+ if (r.live_dispatch?.last_error)
37163
+ lines.push(` - last error: ${r.live_dispatch.last_error}`);
37164
+ results.push({ type: "issue.get", status: "ok", issue_id: r.id, key: r.key, issue_status: r.status, autonomy_level: r.autonomy_level, assignee_id: r.assignee_id, bound_chat_id: r.bound_chat_id, live_dispatch_status: r.live_dispatch?.status ?? null });
37165
+ tally(true);
37166
+ } else if (a.type === "issue.start") {
37167
+ const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
37168
+ if (!res.success) {
37169
+ lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
37170
+ results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
37171
+ tally(false);
37172
+ continue;
37173
+ }
37174
+ const r = res.data || {};
37175
+ lines.push(r.dispatched ? `- [ok] issue.start ${r.key || a.id} -> dispatched` : `- [warn] issue.start ${r.key || a.id} not dispatched (autonomy=${r.autonomy_level} or no online device)`);
37176
+ results.push({ type: "issue.start", status: "ok", issue_id: r.id || a.id, key: r.key, dispatched: !!r.dispatched, dispatch_id: r.dispatch_id ?? null, autonomy_level: r.autonomy_level });
37177
+ tally(true);
37178
+ } else if (a.type === "issue.stop") {
37179
+ const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
37180
+ if (!res.success) {
37181
+ lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
37182
+ results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
37183
+ tally(false);
37184
+ continue;
37185
+ }
37186
+ const r = res.data || {};
37187
+ lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
37188
+ results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
37189
+ tally(true);
36909
37190
  } else if (a.type === "memory.search") {
36910
37191
  const project_id = a.project_id || ctx.projectId;
36911
37192
  if (!project_id) {
@@ -37075,10 +37356,75 @@ var init_chat_plan_actions = __esm(() => {
37075
37356
  "session.create": 3,
37076
37357
  "issue.comment": 5,
37077
37358
  "memory.search": 5,
37078
- "memory.write": 5
37359
+ "memory.write": 5,
37360
+ "issue.get": 10,
37361
+ "issue.start": 3,
37362
+ "issue.stop": 3
37079
37363
  };
37080
37364
  });
37081
37365
 
37366
+ // src/_impl/chat-attachments.ts
37367
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
37368
+ import { dirname as dirname11, join as join15 } from "path";
37369
+ function safeName(n) {
37370
+ return n.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 80) || "file";
37371
+ }
37372
+ async function downloadUserAttachments(opts) {
37373
+ const { apiUrl, authToken: authToken2, workspaceId, chatId, cwd, message, log: log4 } = opts;
37374
+ const atts = message.attachments || [];
37375
+ if (atts.length === 0)
37376
+ return [];
37377
+ const baseRel = join15(".multi", "attachments", message.id);
37378
+ const baseAbs = join15(cwd, baseRel);
37379
+ if (!existsSync15(baseAbs))
37380
+ mkdirSync11(baseAbs, { recursive: true });
37381
+ const out = [];
37382
+ for (const a of atts) {
37383
+ if (!a.url)
37384
+ continue;
37385
+ const fname = safeName(a.name);
37386
+ const relPath = join15(baseRel, fname);
37387
+ const absPath = join15(cwd, relPath);
37388
+ if (existsSync15(absPath)) {
37389
+ out.push({ attachment: a, absPath, relPath });
37390
+ continue;
37391
+ }
37392
+ try {
37393
+ const base = a.url.startsWith("http") ? a.url : `${apiUrl.replace(/\/$/, "")}${a.url}`;
37394
+ const sep = base.includes("?") ? "&" : "?";
37395
+ const url2 = `${base}${sep}token=${encodeURIComponent(authToken2)}`;
37396
+ const res = await fetch(url2);
37397
+ if (!res.ok) {
37398
+ log4(`[chat ${chatId}] attachment ${a.id} fetch ${res.status}`);
37399
+ continue;
37400
+ }
37401
+ const buf = new Uint8Array(await res.arrayBuffer());
37402
+ if (!existsSync15(dirname11(absPath)))
37403
+ mkdirSync11(dirname11(absPath), { recursive: true });
37404
+ writeFileSync9(absPath, buf);
37405
+ out.push({ attachment: a, absPath, relPath });
37406
+ } catch (e) {
37407
+ log4(`[chat ${chatId}] attachment ${a.id} err: ${e.message}`);
37408
+ }
37409
+ }
37410
+ return out;
37411
+ }
37412
+ function renderAttachmentPreamble(fetched) {
37413
+ if (fetched.length === 0)
37414
+ return "";
37415
+ const lines = fetched.map((f) => {
37416
+ const sizeBit = typeof f.attachment.size === "number" ? ` (${f.attachment.size}B)` : "";
37417
+ return `- ${f.relPath}${sizeBit}`;
37418
+ });
37419
+ return [
37420
+ `[multi system note — the user attached files; they are saved in the working directory:]`,
37421
+ ...lines,
37422
+ ``
37423
+ ].join(`
37424
+ `);
37425
+ }
37426
+ var init_chat_attachments = () => {};
37427
+
37082
37428
  // src/_impl/chat-supervisor.ts
37083
37429
  async function runChatTurnWithPeer(peer, opts) {
37084
37430
  const { apiUrl, authToken: authToken2, workspaceId, chatId, messageId, log: log4 } = opts;
@@ -37096,6 +37442,45 @@ async function runChatTurnWithPeer(peer, opts) {
37096
37442
  }
37097
37443
  await processUserMessage(chat2, msg, peer, { apiUrl, authToken: authToken2, workspaceId, log: log4 });
37098
37444
  }
37445
+ function buildIssueContextMarkdown(issue2) {
37446
+ const key = issue2.key ?? issue2.id ?? "";
37447
+ const title = issue2.title ?? "";
37448
+ const description = typeof issue2.description === "string" ? issue2.description.trim() : "";
37449
+ const priority = issue2.priority ?? null;
37450
+ const status3 = issue2.status ?? null;
37451
+ const MAX_DESC = 8 * 1024;
37452
+ const desc = description.length > MAX_DESC ? description.slice(0, MAX_DESC) + `
37453
+
37454
+ […truncated]` : description;
37455
+ const out = [];
37456
+ out.push(`## Issue ${key}: ${title}`);
37457
+ if (desc) {
37458
+ out.push("");
37459
+ out.push(desc);
37460
+ }
37461
+ const meta3 = [];
37462
+ if (priority)
37463
+ meta3.push(`priority: ${priority}`);
37464
+ if (status3)
37465
+ meta3.push(`status: ${status3}`);
37466
+ if (meta3.length) {
37467
+ out.push("");
37468
+ out.push(`_${meta3.join(" · ")}_`);
37469
+ }
37470
+ return out.join(`
37471
+ `);
37472
+ }
37473
+ async function fetchIssueContext(apiUrl, workspaceId, authToken2, issueId) {
37474
+ try {
37475
+ const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/issues/${issueId}`, { headers: { authorization: `Bearer ${authToken2}` } });
37476
+ if (!r.ok)
37477
+ return null;
37478
+ const issue2 = await r.json();
37479
+ return buildIssueContextMarkdown(issue2);
37480
+ } catch {
37481
+ return null;
37482
+ }
37483
+ }
37099
37484
  async function fetchAgents(apiUrl, workspaceId, authToken2) {
37100
37485
  const headers = { authorization: `Bearer ${authToken2}` };
37101
37486
  try {
@@ -37118,6 +37503,9 @@ function resolveAgentFromMention(agents, mentioned) {
37118
37503
  async function resolveCwd(apiUrl, workspaceId, authToken2, chat2) {
37119
37504
  if (!chat2.project_id || !chat2.device_id)
37120
37505
  return;
37506
+ if (chat2.working_dir) {
37507
+ return chat2.working_dir;
37508
+ }
37121
37509
  const headers = { authorization: `Bearer ${authToken2}` };
37122
37510
  try {
37123
37511
  const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/projects/${chat2.project_id}/devices`, { headers });
@@ -37202,21 +37590,28 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
37202
37590
  if (typeof r.filename === "string")
37203
37591
  out.filename = r.filename;
37204
37592
  if (typeof r.command === "string")
37205
- out.command = String(r.command).slice(0, 400);
37593
+ out.command = r.command;
37206
37594
  if (typeof r.pattern === "string")
37207
- out.pattern = String(r.pattern).slice(0, 200);
37595
+ out.pattern = r.pattern;
37208
37596
  if (typeof r.query === "string")
37209
- out.query = String(r.query).slice(0, 200);
37597
+ out.query = r.query;
37210
37598
  if (typeof r.url === "string")
37211
37599
  out.url = r.url;
37600
+ if (typeof r.old_string === "string")
37601
+ out.old_string = r.old_string;
37602
+ if (typeof r.new_string === "string")
37603
+ out.new_string = r.new_string;
37604
+ if (typeof r.replace_all === "boolean")
37605
+ out.replace_all = r.replace_all;
37606
+ if (typeof r.content === "string")
37607
+ out.content = r.content;
37212
37608
  return Object.keys(out).length ? out : undefined;
37213
37609
  };
37214
37610
  const summarizeContent = (raw) => {
37215
37611
  const bytes = raw?.length ?? 0;
37216
37612
  const lines = raw ? raw.split(`
37217
37613
  `).length : 0;
37218
- const trimmed = raw && raw.length > 240 ? raw.slice(0, 240) + "" : raw || "";
37219
- return { content: trimmed, bytes, lines };
37614
+ return { content: raw || "", bytes, lines };
37220
37615
  };
37221
37616
  const mapToolKind = (raw) => {
37222
37617
  const s = (raw || "").toLowerCase();
@@ -37354,7 +37749,42 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
37354
37749
  return null;
37355
37750
  }
37356
37751
  };
37357
- await runOneTurn(userMsg.text, true);
37752
+ let attachmentPrefix = "";
37753
+ if (cwd && userMsg.attachments && userMsg.attachments.length > 0) {
37754
+ try {
37755
+ const fetched = await downloadUserAttachments({
37756
+ apiUrl,
37757
+ authToken: authToken2,
37758
+ workspaceId,
37759
+ chatId: chat2.id,
37760
+ cwd,
37761
+ message: userMsg,
37762
+ log: log4
37763
+ });
37764
+ attachmentPrefix = renderAttachmentPreamble(fetched);
37765
+ if (fetched.length > 0)
37766
+ log4(`[chat ${chat2.id}] attachments: ${fetched.length} ready`);
37767
+ } catch (e) {
37768
+ log4(`[chat ${chat2.id}] attachment download err: ${e.message}`);
37769
+ }
37770
+ }
37771
+ let issueCtxBlock = "";
37772
+ if (chat2.issue_id && !issueContextSent.has(chat2.id)) {
37773
+ const ctx2 = await fetchIssueContext(apiUrl, workspaceId, authToken2, chat2.issue_id);
37774
+ if (ctx2) {
37775
+ issueCtxBlock = `${ctx2}
37776
+
37777
+ ---
37778
+
37779
+ ## New message
37780
+
37781
+ `;
37782
+ log4(`[chat ${chat2.id}] injected issue context (${ctx2.length} chars)`);
37783
+ }
37784
+ }
37785
+ await runOneTurn(`${issueCtxBlock}${attachmentPrefix}${userMsg.text}`, true);
37786
+ if (issueCtxBlock)
37787
+ issueContextSent.add(chat2.id);
37358
37788
  const firstPlan = await execAndPostPlan();
37359
37789
  if (firstPlan && firstPlan.ok > 0) {
37360
37790
  const resultText = firstPlan.lines.join(`
@@ -37453,6 +37883,9 @@ Action vocabulary:
37453
37883
  {"type":"issue.delete_where","status":"todo","limit":50},
37454
37884
  {"type":"issue.list","status":"todo","limit":20},
37455
37885
  {"type":"issue.search","query":"flaky tests","limit":10},
37886
+ {"type":"issue.get","id":"<issue id or key>"},
37887
+ {"type":"issue.start","id":"<issue id or key>"},
37888
+ {"type":"issue.stop","id":"<issue id or key>"},
37456
37889
  {"type":"memory.search","query":"deploy pipeline","limit":10},
37457
37890
  {"type":"memory.write","text":"durable note worth recalling next session","summary":"short hint","kind":"agent_note"},
37458
37891
  {"type":"agent.create","name":"refactor-bot","prompt":"...","allowed_tools":["Read","Edit","Bash"]},
@@ -37466,7 +37899,8 @@ Action vocabulary:
37466
37899
  Rules:
37467
37900
  - Omit the block entirely if no actions are needed. Don't emit empty arrays.
37468
37901
  - Status values for \`update\`: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when the issue is paused waiting on a human decision** — confirmation, choice, or missing context. Marking such an issue \`done\` misrepresents finished work. Use \`archived\` to hide a completed/abandoned issue from default views.
37469
- - Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5, memory.search=5, memory.write=5.
37902
+ - Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5, memory.search=5, memory.write=5, issue.get=10, issue.start=3, issue.stop=3.
37903
+ - Use \`issue.get\` to inspect an issue's current state (status, autonomy, assignee, live dispatch, bound chat) — the result lands in the next-turn summary. \`issue.start\` dispatches the assigned agent (equivalent to mentioning them); honors the autonomy gate. \`issue.stop\` gracefully stops a running dispatch; safe to call on a non-running issue (no-op).
37470
37904
  - Memory actions are project-scoped persistent notes (FTS5 + vector). \`memory.search\` returns hits in the action summary; the agent reads them next turn. \`memory.write\` records durable facts for future sessions — use sparingly, not for transient task state. Both require a pinned project.
37471
37905
  - Chat-initiated agent.create / agent.update / skill.attach are auto-approved (the user is reading this reply right now). skill.create still queues for human review.
37472
37906
  - Use \`issue.comment\` with \`@<agent name>\` mention to dispatch an agent on an existing issue. Plain comments without an @mention are recorded but do not trigger a run. Issues whose autonomy is \`manual\` will not dispatch.
@@ -37490,11 +37924,14 @@ Rules:
37490
37924
 
37491
37925
  ${agentsBlock}`;
37492
37926
  }
37927
+ var issueContextSent;
37493
37928
  var init_chat_supervisor = __esm(() => {
37494
37929
  init_chat_peer();
37495
37930
  init_chat_turn();
37496
37931
  init_chat_plan_actions();
37932
+ init_chat_attachments();
37497
37933
  init_lib();
37934
+ issueContextSent = new Set;
37498
37935
  });
37499
37936
 
37500
37937
  // src/_impl/chat-session-registry.ts
@@ -37607,6 +38044,9 @@ var init_chat_session_registry = __esm(() => {
37607
38044
  }
37608
38045
  s.inFlight = runTurn(s, k, messageId, ctx, chatId);
37609
38046
  },
38047
+ ensurePeer(wsId, chatId, ctx) {
38048
+ return ensureSession(wsId, chatId, ctx).peer;
38049
+ },
37610
38050
  async closeAll() {
37611
38051
  for (const [k, s] of sessions) {
37612
38052
  clearIdle(s);
@@ -38241,7 +38681,7 @@ import { parseArgs } from "util";
38241
38681
  // package.json
38242
38682
  var package_default = {
38243
38683
  name: "@shipers-dev/multi",
38244
- version: "0.52.0",
38684
+ version: "0.63.0",
38245
38685
  type: "module",
38246
38686
  bin: {
38247
38687
  "multi-agent": "./dist/index.js"
@@ -38621,8 +39061,6 @@ revision=${Date.now()}
38621
39061
  }
38622
39062
  }
38623
39063
  function writeManagedAgent(slug, agent, skillsForAgent) {
38624
- if (agent.type !== "claude-code")
38625
- return;
38626
39064
  mkdirSync4(CLAUDE_AGENTS, { recursive: true });
38627
39065
  const out = join6(CLAUDE_AGENTS, `${slug}.md`);
38628
39066
  safeRmManaged(out);
@@ -39156,7 +39594,7 @@ var killStaleConnects = () => {
39156
39594
  const cmd = m[2];
39157
39595
  if (!Number.isFinite(pid) || pid === self || pid === parent)
39158
39596
  continue;
39159
- if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd))
39597
+ if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd) && !/multi-daemon/.test(cmd))
39160
39598
  continue;
39161
39599
  if (!/\bconnect\b/.test(cmd))
39162
39600
  continue;
@@ -39319,22 +39757,24 @@ init_client();
39319
39757
  init_detect();
39320
39758
  init_run_task();
39321
39759
  import { Database as Database3 } from "bun:sqlite";
39322
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7 } from "fs";
39323
- import { homedir as homedir3 } from "os";
39324
- import { join as join14 } from "path";
39760
+ import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7 } from "fs";
39761
+ import { homedir as homedir4 } from "os";
39762
+ import { join as join16 } from "path";
39325
39763
  init_adapter_pidfile();
39326
39764
  init_errors();
39327
- var MULTI_DIR5 = join14(homedir3(), ".multi");
39328
- var PID_PATH2 = join14(MULTI_DIR5, "agent.pid");
39329
- var PORT_PATH2 = join14(MULTI_DIR5, "agent.port");
39330
- var STOP_PATH2 = join14(MULTI_DIR5, "stop.flag");
39331
- var TASKS_DB_PATH3 = join14(MULTI_DIR5, "tasks.db");
39765
+ var MULTI_DIR5 = join16(homedir4(), ".multi");
39766
+ var PID_PATH2 = join16(MULTI_DIR5, "agent.pid");
39767
+ var PORT_PATH2 = join16(MULTI_DIR5, "agent.port");
39768
+ var STOP_PATH2 = join16(MULTI_DIR5, "stop.flag");
39769
+ var TASKS_DB_PATH3 = join16(MULTI_DIR5, "tasks.db");
39770
+ var LOCAL_SERVER_DIR = process.env.MULTI_HOME || join16(homedir4(), ".multi");
39771
+ var LOCAL_SERVER_PATH = join16(LOCAL_SERVER_DIR, "local-server.json");
39332
39772
  function ensureDirs2() {
39333
- if (!existsSync14(MULTI_DIR5))
39334
- mkdirSync11(MULTI_DIR5, { recursive: true });
39335
- const logs = join14(MULTI_DIR5, "logs");
39336
- if (!existsSync14(logs))
39337
- mkdirSync11(logs, { recursive: true });
39773
+ if (!existsSync16(MULTI_DIR5))
39774
+ mkdirSync12(MULTI_DIR5, { recursive: true });
39775
+ const logs = join16(MULTI_DIR5, "logs");
39776
+ if (!existsSync16(logs))
39777
+ mkdirSync12(logs, { recursive: true });
39338
39778
  }
39339
39779
  function isLocalApi(url2) {
39340
39780
  try {
@@ -39626,15 +40066,18 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39626
40066
  if (cfg.authToken)
39627
40067
  setAuthToken(cfg.authToken);
39628
40068
  ensureDirs2();
39629
- if (existsSync14(STOP_PATH2))
40069
+ if (existsSync16(STOP_PATH2))
39630
40070
  unlinkSync7(STOP_PATH2);
39631
40071
  const reaped = reapStaleAdapters((m) => log3(m));
39632
40072
  if (reaped > 0)
39633
40073
  log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
39634
40074
  const detected = yield* exports_Effect.promise(() => detectAgents());
39635
- const localMode = isLocalApi(apiUrl);
40075
+ const skipTunnel = process.env.MULTI_NO_TUNNEL === "1";
40076
+ const localMode = isLocalApi(apiUrl) || skipTunnel;
39636
40077
  log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
39637
- log3(` API: ${apiUrl}${localMode ? " (local — cloudflared skipped)" : ""}`);
40078
+ log3(` API: ${apiUrl}${localMode ? skipTunnel && !isLocalApi(apiUrl) ? " (no-tunnel mode)" : " (local — cloudflared skipped)" : ""}`);
40079
+ log3(` PATH=${(process.env.PATH || "").slice(0, 500)}`);
40080
+ log3(` detected: ${JSON.stringify(detected.map((d) => ({ type: d.type, path: d.path })))}`);
39638
40081
  log3(` runtimes: ${detected.map((d) => d.type).join(", ") || "stub"}`);
39639
40082
  const db2 = openTasksDb();
39640
40083
  db2.run("UPDATE tasks SET status = 'queued' WHERE status = 'running'");
@@ -39646,128 +40089,573 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39646
40089
  const stopDeferred = yield* exports_Deferred.make();
39647
40090
  const port = yield* exports_Effect.promise(() => pickFreePort());
39648
40091
  const expectedAuth = `Bearer ${cfg.dispatchSecret}`;
40092
+ const corsHeaders = (origin) => ({
40093
+ "Access-Control-Allow-Origin": origin || "*",
40094
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
40095
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
40096
+ "Access-Control-Max-Age": "600",
40097
+ Vary: "Origin"
40098
+ });
40099
+ const withCors = (res, origin) => {
40100
+ for (const [k, v] of Object.entries(corsHeaders(origin))) {
40101
+ res.headers.set(k, v);
40102
+ }
40103
+ return res;
40104
+ };
39649
40105
  const server = Bun.serve({
39650
40106
  port,
39651
40107
  hostname: "127.0.0.1",
39652
- fetch(req) {
40108
+ fetch(req, server2) {
39653
40109
  const url2 = new URL(req.url);
39654
- if (url2.pathname === "/health")
39655
- return Response.json({ ok: true, device_id: cfg.deviceId });
39656
- if (url2.pathname === "/run" && req.method === "POST") {
39657
- if (req.headers.get("authorization") !== expectedAuth)
39658
- return new Response("unauthorized", { status: 401 });
39659
- return (async () => {
39660
- try {
39661
- const body = await req.json();
39662
- const t = body?.task || {};
39663
- const taskId = t.issue_id ? `${t.issue_id}-${Date.now()}` : crypto.randomUUID();
39664
- db2.run("INSERT INTO tasks (id, status, payload, agent_id, issue_id) VALUES (?, 'queued', ?, ?, ?)", [taskId, JSON.stringify(t), t.agent_id ?? null, t.issue_id ?? null]);
39665
- const pos = db2.query("SELECT COUNT(*) AS c FROM tasks WHERE status = 'queued' AND created_at <= (SELECT created_at FROM tasks WHERE id = ?)").get(taskId)?.c ?? 1;
39666
- if (t.issue_id)
39667
- postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
39668
- if (t.dispatch_id && cfg.workspaceId)
39669
- ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
39670
- try {
39671
- exports_Queue.unsafeOffer(wakeQ, undefined);
39672
- } catch {}
39673
- return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
39674
- } catch (e) {
39675
- return Response.json({ error: String(e) }, { status: 400 });
40110
+ const origin = req.headers.get("origin");
40111
+ if (req.method === "OPTIONS") {
40112
+ return new Response(null, { status: 204, headers: corsHeaders(origin) });
40113
+ }
40114
+ {
40115
+ const m = /^\/chat-sync\/([^/]+)$/.exec(url2.pathname);
40116
+ if (m && req.headers.get("upgrade") === "websocket") {
40117
+ const token = url2.searchParams.get("token") || "";
40118
+ if (token !== cfg.dispatchSecret) {
40119
+ return new Response("unauthorized", { status: 401 });
39676
40120
  }
39677
- })();
40121
+ if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
40122
+ return new Response("daemon not configured", { status: 503 });
40123
+ }
40124
+ const chatId = decodeURIComponent(m[1]);
40125
+ const ok = server2.upgrade(req, {
40126
+ data: { kind: "chat-sync", chatId, wsId: cfg.workspaceId }
40127
+ });
40128
+ if (!ok)
40129
+ return new Response("upgrade failed", { status: 500 });
40130
+ return;
40131
+ }
39678
40132
  }
39679
- if (url2.pathname === "/run-chat-turn" && req.method === "POST") {
39680
- if (req.headers.get("authorization") !== expectedAuth)
39681
- return new Response("unauthorized", { status: 401 });
39682
- return (async () => {
39683
- try {
39684
- const body = await req.json();
39685
- if (!body?.chat_id || !body?.message_id) {
39686
- return Response.json({ error: "chat_id and message_id required" }, { status: 400 });
40133
+ const route = () => {
40134
+ if (url2.pathname === "/health")
40135
+ return Response.json({ ok: true, device_id: cfg.deviceId });
40136
+ if (url2.pathname === "/files" && req.method === "GET") {
40137
+ if (req.headers.get("authorization") !== expectedAuth)
40138
+ return new Response("unauthorized", { status: 401 });
40139
+ return (async () => {
40140
+ try {
40141
+ const dir = url2.searchParams.get("dir") || "";
40142
+ if (!dir || !dir.startsWith("/")) {
40143
+ return Response.json({ error: "absolute dir required" }, { status: 400 });
40144
+ }
40145
+ const showHidden = url2.searchParams.get("hidden") === "1";
40146
+ const { readdir } = await import("fs/promises");
40147
+ const path = await import("path");
40148
+ const entries2 = await readdir(dir, { withFileTypes: true });
40149
+ const repoRoot = await (async () => {
40150
+ try {
40151
+ const p = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
40152
+ cwd: dir,
40153
+ stdout: "pipe",
40154
+ stderr: "ignore"
40155
+ });
40156
+ const out3 = (await new Response(p.stdout).text()).trim();
40157
+ await p.exited;
40158
+ return p.exitCode === 0 && out3 ? out3 : null;
40159
+ } catch {
40160
+ return null;
40161
+ }
40162
+ })();
40163
+ const status3 = new Map;
40164
+ if (repoRoot) {
40165
+ try {
40166
+ const p = Bun.spawn(["git", "status", "--porcelain=v1", "--ignored", "-z"], { cwd: repoRoot, stdout: "pipe", stderr: "ignore" });
40167
+ const out3 = await new Response(p.stdout).text();
40168
+ await p.exited;
40169
+ if (p.exitCode === 0) {
40170
+ const parts2 = out3.split("\x00").filter((s) => s.length > 0);
40171
+ for (let i = 0;i < parts2.length; i++) {
40172
+ const entry = parts2[i];
40173
+ const code = entry.slice(0, 2);
40174
+ const rel = entry.slice(3);
40175
+ if (code[0] === "R" || code[0] === "C")
40176
+ i++;
40177
+ const abs = path.join(repoRoot, rel);
40178
+ const kind = code === "!!" ? "ignored" : code === "??" ? "untracked" : code.includes("D") ? "deleted" : code.includes("A") ? "added" : code.includes("R") ? "renamed" : code.includes("M") ? "modified" : "tracked";
40179
+ status3.set(abs.replace(/\/$/, ""), kind);
40180
+ }
40181
+ }
40182
+ } catch {}
40183
+ }
40184
+ const lookupStatus = (abs, isDir) => {
40185
+ const trimmed = abs.replace(/\/$/, "");
40186
+ if (status3.has(trimmed))
40187
+ return status3.get(trimmed);
40188
+ if (isDir) {
40189
+ for (let p = trimmed;p && p !== repoRoot; p = p.replace(/\/[^/]*$/, "")) {
40190
+ const s = status3.get(p);
40191
+ if (s === "ignored" || s === "untracked")
40192
+ return s;
40193
+ if (!p.includes("/"))
40194
+ break;
40195
+ }
40196
+ }
40197
+ if (!repoRoot)
40198
+ return "tracked";
40199
+ return "tracked";
40200
+ };
40201
+ const out2 = entries2.filter((e) => showHidden || !e.name.startsWith(".")).map((e) => {
40202
+ const abs = path.join(dir, e.name);
40203
+ return {
40204
+ name: e.name,
40205
+ path: abs,
40206
+ is_dir: e.isDirectory(),
40207
+ status: lookupStatus(abs, e.isDirectory())
40208
+ };
40209
+ }).sort((a, b) => a.is_dir === b.is_dir ? a.name.localeCompare(b.name) : a.is_dir ? -1 : 1);
40210
+ return Response.json({ dir, repo_root: repoRoot, entries: out2 });
40211
+ } catch (e) {
40212
+ return Response.json({ error: String(e) }, { status: 400 });
39687
40213
  }
39688
- if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
39689
- return Response.json({ error: "daemon not configured" }, { status: 503 });
40214
+ })();
40215
+ }
40216
+ if (url2.pathname === "/worktree/create" && req.method === "POST") {
40217
+ if (req.headers.get("authorization") !== expectedAuth)
40218
+ return new Response("unauthorized", { status: 401 });
40219
+ return (async () => {
40220
+ try {
40221
+ const body = await req.json();
40222
+ if (!body?.base_dir || !body.base_dir.startsWith("/")) {
40223
+ return Response.json({ error: "absolute base_dir required" }, { status: 400 });
40224
+ }
40225
+ const { codename: codename2, materialiseWorktree: materialiseWorktree2 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
40226
+ const name = (body.name || codename2()).replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
40227
+ const result = await materialiseWorktree2({
40228
+ baseDir: body.base_dir,
40229
+ baseBranch: body.base_branch || null,
40230
+ name,
40231
+ chatId: body.chat_id || null,
40232
+ log: (m) => log3(m)
40233
+ });
40234
+ return Response.json(result);
40235
+ } catch (e) {
40236
+ return Response.json({ error: String(e.message || e) }, { status: 400 });
39690
40237
  }
39691
- const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
39692
- chatSessionRegistry2.ensureAndProcess(cfg.workspaceId, body.chat_id, body.message_id, {
39693
- apiUrl,
39694
- authToken: cfg.authToken,
39695
- workspaceId: cfg.workspaceId,
39696
- deviceId: cfg.deviceId,
39697
- log: log3
39698
- }).catch((e) => log3(`[chat ${body.chat_id}] runChatTurn error: ${e.message}`));
39699
- return Response.json({ accepted: true }, { status: 202 });
39700
- } catch (e) {
39701
- return Response.json({ error: String(e) }, { status: 400 });
39702
- }
39703
- })();
39704
- }
39705
- if (url2.pathname === "/run-supervisor-tick" && req.method === "POST") {
39706
- if (req.headers.get("authorization") !== expectedAuth)
39707
- return new Response("unauthorized", { status: 401 });
39708
- return (async () => {
39709
- try {
39710
- const body = await req.json();
39711
- if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
39712
- return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
40238
+ })();
40239
+ }
40240
+ if (url2.pathname === "/worktree/delete" && req.method === "POST") {
40241
+ if (req.headers.get("authorization") !== expectedAuth)
40242
+ return new Response("unauthorized", { status: 401 });
40243
+ return (async () => {
40244
+ try {
40245
+ const body = await req.json();
40246
+ if (!body?.path || !body.path.startsWith("/")) {
40247
+ return Response.json({ error: "absolute path required" }, { status: 400 });
40248
+ }
40249
+ const { removeWorktree: removeWorktree3 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
40250
+ const result = await removeWorktree3(body.path, (m) => log3(m));
40251
+ return Response.json(result);
40252
+ } catch (e) {
40253
+ return Response.json({ error: String(e.message || e) }, { status: 400 });
39713
40254
  }
39714
- const { runSupervisorTick: runSupervisorTick2 } = await Promise.resolve().then(() => (init_supervisor_tick(), exports_supervisor_tick));
39715
- runSupervisorTick2({
39716
- apiUrl,
39717
- tickId: body.tick_id,
39718
- projectId: body.project_id,
39719
- system: body.system,
39720
- user: body.user,
39721
- callbackUrl: body.callback_url,
39722
- callbackToken: body.callback_token,
39723
- log: log3
39724
- }).catch((e) => log3(`[sup ${body.tick_id}] runSupervisorTick error: ${e.message}`));
39725
- return Response.json({ accepted: true }, { status: 202 });
39726
- } catch (e) {
39727
- return Response.json({ error: String(e) }, { status: 400 });
39728
- }
39729
- })();
39730
- }
39731
- if (url2.pathname === "/stop" && req.method === "POST") {
39732
- if (req.headers.get("authorization") !== expectedAuth)
39733
- return new Response("unauthorized", { status: 401 });
39734
- return (async () => {
39735
- try {
39736
- const { issue_id } = await req.json();
39737
- if (!issue_id)
39738
- return Response.json({ error: "issue_id required" }, { status: 400 });
39739
- const entries2 = Array.from(running3.values()).filter((e) => e.issueId === issue_id);
39740
- if (!entries2.length) {
39741
- db2.run("UPDATE tasks SET status = 'failed', error = 'stopped before start', finished_at = unixepoch() WHERE issue_id = ? AND status = 'queued'", [issue_id]);
39742
- return Response.json({ ok: true, state: "queued-cancelled" });
40255
+ })();
40256
+ }
40257
+ if (url2.pathname === "/branch" && req.method === "GET") {
40258
+ if (req.headers.get("authorization") !== expectedAuth)
40259
+ return new Response("unauthorized", { status: 401 });
40260
+ return (async () => {
40261
+ try {
40262
+ const dir = url2.searchParams.get("dir") || "";
40263
+ if (!dir || !dir.startsWith("/")) {
40264
+ return Response.json({ error: "absolute dir required" }, { status: 400 });
40265
+ }
40266
+ const runGit = async (args2) => {
40267
+ const proc = Bun.spawn(["git", ...args2], {
40268
+ cwd: dir,
40269
+ stdout: "pipe",
40270
+ stderr: "ignore"
40271
+ });
40272
+ const out2 = (await new Response(proc.stdout).text()).trim();
40273
+ await proc.exited;
40274
+ return { ok: proc.exitCode === 0, out: out2 };
40275
+ };
40276
+ const branchR = await runGit(["branch", "--show-current"]);
40277
+ const branch = branchR.ok ? branchR.out || null : null;
40278
+ const upstreamR = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
40279
+ const upstream = upstreamR.ok ? upstreamR.out || null : null;
40280
+ let ahead = 0;
40281
+ let behind = 0;
40282
+ const unpushed = [];
40283
+ if (upstream) {
40284
+ const countR = await runGit(["rev-list", "--left-right", "--count", `${upstream}...HEAD`]);
40285
+ if (countR.ok) {
40286
+ const [b, a] = countR.out.split(/\s+/);
40287
+ behind = Number(b) || 0;
40288
+ ahead = Number(a) || 0;
40289
+ }
40290
+ if (ahead > 0) {
40291
+ const logR = await runGit(["log", `${upstream}..HEAD`, "--pretty=format:%h\x1F%s", "-n", "50"]);
40292
+ if (logR.ok && logR.out) {
40293
+ for (const line of logR.out.split(`
40294
+ `)) {
40295
+ const idx = line.indexOf("\x1F");
40296
+ if (idx === -1)
40297
+ continue;
40298
+ unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
40299
+ }
40300
+ }
40301
+ }
40302
+ } else if (branch) {
40303
+ const logR = await runGit(["log", "HEAD", "--not", "--remotes", "--pretty=format:%h\x1F%s", "-n", "50"]);
40304
+ if (logR.ok && logR.out) {
40305
+ for (const line of logR.out.split(`
40306
+ `)) {
40307
+ const idx = line.indexOf("\x1F");
40308
+ if (idx === -1)
40309
+ continue;
40310
+ unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
40311
+ }
40312
+ ahead = unpushed.length;
40313
+ }
40314
+ }
40315
+ return Response.json({ dir, branch, upstream, ahead, behind, unpushed });
40316
+ } catch (e) {
40317
+ return Response.json({ error: String(e) }, { status: 400 });
40318
+ }
40319
+ })();
40320
+ }
40321
+ if (url2.pathname === "/changes" && req.method === "GET") {
40322
+ if (req.headers.get("authorization") !== expectedAuth)
40323
+ return new Response("unauthorized", { status: 401 });
40324
+ return (async () => {
40325
+ try {
40326
+ const dir = url2.searchParams.get("dir") || "";
40327
+ if (!dir || !dir.startsWith("/")) {
40328
+ return Response.json({ error: "absolute dir required" }, { status: 400 });
40329
+ }
40330
+ const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
40331
+ cwd: dir,
40332
+ stdout: "pipe",
40333
+ stderr: "ignore"
40334
+ });
40335
+ const repoRoot = (await new Response(rootP.stdout).text()).trim();
40336
+ await rootP.exited;
40337
+ const proc = Bun.spawn(["git", "status", "--porcelain=v1", "-z"], {
40338
+ cwd: dir,
40339
+ stdout: "pipe",
40340
+ stderr: "pipe"
40341
+ });
40342
+ const out2 = await new Response(proc.stdout).text();
40343
+ const errOut = await new Response(proc.stderr).text();
40344
+ await proc.exited;
40345
+ if (proc.exitCode !== 0) {
40346
+ return Response.json({ dir, changes: [], error: errOut.trim() || "git failed" });
40347
+ }
40348
+ const changes = [];
40349
+ const parts2 = out2.split("\x00").filter((p) => p.length > 0);
40350
+ for (let i = 0;i < parts2.length; i++) {
40351
+ const entry = parts2[i];
40352
+ const code = entry.slice(0, 2);
40353
+ let p = entry.slice(3);
40354
+ if (code[0] === "R" || code[0] === "C") {
40355
+ i++;
40356
+ }
40357
+ changes.push({ code, path: p });
40358
+ }
40359
+ return Response.json({ dir, repo_root: repoRoot || null, changes });
40360
+ } catch (e) {
40361
+ return Response.json({ error: String(e) }, { status: 400 });
40362
+ }
40363
+ })();
40364
+ }
40365
+ if (url2.pathname === "/read-file" && req.method === "GET") {
40366
+ if (req.headers.get("authorization") !== expectedAuth)
40367
+ return new Response("unauthorized", { status: 401 });
40368
+ return (async () => {
40369
+ try {
40370
+ const p = url2.searchParams.get("path") || "";
40371
+ if (!p || !p.startsWith("/")) {
40372
+ return Response.json({ error: "absolute path required" }, { status: 400 });
40373
+ }
40374
+ const fs3 = await import("fs/promises");
40375
+ const stat = await fs3.stat(p);
40376
+ if (stat.isDirectory()) {
40377
+ return Response.json({ error: "is a directory" }, { status: 400 });
40378
+ }
40379
+ const MAX = 2097152;
40380
+ if (stat.size > MAX) {
40381
+ return Response.json({ path: p, size: stat.size, truncated: true, error: `file too large (${stat.size} bytes)` }, { status: 413 });
40382
+ }
40383
+ const buf = await fs3.readFile(p);
40384
+ const head5 = buf.subarray(0, Math.min(8192, buf.length));
40385
+ let nonPrint = 0;
40386
+ for (let i = 0;i < head5.length; i++) {
40387
+ const b = head5[i];
40388
+ if (b === 0) {
40389
+ nonPrint = head5.length;
40390
+ break;
40391
+ }
40392
+ if (b < 9 || b > 13 && b < 32)
40393
+ nonPrint++;
40394
+ }
40395
+ const isBinary = head5.length > 0 && nonPrint / head5.length > 0.3;
40396
+ if (isBinary) {
40397
+ return Response.json({ path: p, size: stat.size, binary: true });
40398
+ }
40399
+ return Response.json({ path: p, size: stat.size, content: buf.toString("utf8") });
40400
+ } catch (e) {
40401
+ return Response.json({ error: String(e.message || e) }, { status: 400 });
39743
40402
  }
39744
- for (const entry of entries2) {
39745
- entry.stopped = true;
39746
- entry.stopReason = "user requested";
40403
+ })();
40404
+ }
40405
+ if (url2.pathname === "/diff" && req.method === "GET") {
40406
+ if (req.headers.get("authorization") !== expectedAuth)
40407
+ return new Response("unauthorized", { status: 401 });
40408
+ return (async () => {
40409
+ try {
40410
+ const p = url2.searchParams.get("path") || "";
40411
+ if (!p || !p.startsWith("/")) {
40412
+ return Response.json({ error: "absolute path required" }, { status: 400 });
40413
+ }
40414
+ const path = await import("path");
40415
+ const repoDir = path.dirname(p);
40416
+ const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
40417
+ cwd: repoDir,
40418
+ stdout: "pipe",
40419
+ stderr: "ignore"
40420
+ });
40421
+ const rootOut = (await new Response(rootP.stdout).text()).trim();
40422
+ await rootP.exited;
40423
+ if (rootP.exitCode !== 0 || !rootOut) {
40424
+ return Response.json({ path: p, diff: "", error: "not a git repo" });
40425
+ }
40426
+ const proc = Bun.spawn(["git", "diff", "HEAD", "--", p], { cwd: rootOut, stdout: "pipe", stderr: "pipe" });
40427
+ const out2 = await new Response(proc.stdout).text();
40428
+ const err = await new Response(proc.stderr).text();
40429
+ await proc.exited;
40430
+ if (proc.exitCode !== 0) {
40431
+ return Response.json({ path: p, diff: "", error: err.trim() || "git diff failed" });
40432
+ }
40433
+ return Response.json({ path: p, diff: out2 });
40434
+ } catch (e) {
40435
+ return Response.json({ error: String(e.message || e) }, { status: 400 });
40436
+ }
40437
+ })();
40438
+ }
40439
+ if (url2.pathname === "/run" && req.method === "POST") {
40440
+ if (req.headers.get("authorization") !== expectedAuth)
40441
+ return new Response("unauthorized", { status: 401 });
40442
+ return (async () => {
40443
+ try {
40444
+ const body = await req.json();
40445
+ const t = body?.task || {};
40446
+ const taskId = t.issue_id ? `${t.issue_id}-${Date.now()}` : crypto.randomUUID();
40447
+ db2.run("INSERT INTO tasks (id, status, payload, agent_id, issue_id) VALUES (?, 'queued', ?, ?, ?)", [taskId, JSON.stringify(t), t.agent_id ?? null, t.issue_id ?? null]);
40448
+ const pos = db2.query("SELECT COUNT(*) AS c FROM tasks WHERE status = 'queued' AND created_at <= (SELECT created_at FROM tasks WHERE id = ?)").get(taskId)?.c ?? 1;
40449
+ if (t.issue_id)
40450
+ postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
40451
+ if (t.dispatch_id && cfg.workspaceId)
40452
+ ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
39747
40453
  try {
39748
- entry.child?.kill("SIGTERM");
40454
+ exports_Queue.unsafeOffer(wakeQ, undefined);
39749
40455
  } catch {}
39750
- setTimeout(() => {
40456
+ return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
40457
+ } catch (e) {
40458
+ return Response.json({ error: String(e) }, { status: 400 });
40459
+ }
40460
+ })();
40461
+ }
40462
+ {
40463
+ const m = /^\/chat-snapshot\/([^/]+)$/.exec(url2.pathname);
40464
+ if (m && req.method === "GET") {
40465
+ const token = url2.searchParams.get("token") || "";
40466
+ const headerToken = req.headers.get("authorization") === expectedAuth;
40467
+ if (!headerToken && token !== cfg.dispatchSecret) {
40468
+ return new Response("unauthorized", { status: 401 });
40469
+ }
40470
+ if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
40471
+ return new Response("daemon not configured", { status: 503 });
40472
+ }
40473
+ const chatId = decodeURIComponent(m[1]);
40474
+ return (async () => {
40475
+ const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
40476
+ const peer = chatSessionRegistry2.ensurePeer(cfg.workspaceId, chatId, {
40477
+ apiUrl,
40478
+ authToken: cfg.authToken,
40479
+ workspaceId: cfg.workspaceId,
40480
+ deviceId: cfg.deviceId,
40481
+ log: log3
40482
+ });
40483
+ const greet = peer.buildSnapshotGreet();
40484
+ return new Response(greet.subarray(1), {
40485
+ headers: { "content-type": "application/octet-stream" }
40486
+ });
40487
+ })();
40488
+ }
40489
+ }
40490
+ if (url2.pathname === "/run-chat-turn" && req.method === "POST") {
40491
+ if (req.headers.get("authorization") !== expectedAuth)
40492
+ return new Response("unauthorized", { status: 401 });
40493
+ return (async () => {
40494
+ try {
40495
+ const body = await req.json();
40496
+ if (!body?.chat_id || !body?.message_id) {
40497
+ return Response.json({ error: "chat_id and message_id required" }, { status: 400 });
40498
+ }
40499
+ if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
40500
+ return Response.json({ error: "daemon not configured" }, { status: 503 });
40501
+ }
40502
+ const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
40503
+ chatSessionRegistry2.ensureAndProcess(cfg.workspaceId, body.chat_id, body.message_id, {
40504
+ apiUrl,
40505
+ authToken: cfg.authToken,
40506
+ workspaceId: cfg.workspaceId,
40507
+ deviceId: cfg.deviceId,
40508
+ log: log3
40509
+ }).catch((e) => log3(`[chat ${body.chat_id}] runChatTurn error: ${e.message}`));
40510
+ return Response.json({ accepted: true }, { status: 202 });
40511
+ } catch (e) {
40512
+ return Response.json({ error: String(e) }, { status: 400 });
40513
+ }
40514
+ })();
40515
+ }
40516
+ if (url2.pathname === "/run-supervisor-tick" && req.method === "POST") {
40517
+ if (req.headers.get("authorization") !== expectedAuth)
40518
+ return new Response("unauthorized", { status: 401 });
40519
+ return (async () => {
40520
+ try {
40521
+ const body = await req.json();
40522
+ if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
40523
+ return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
40524
+ }
40525
+ const { runSupervisorTick: runSupervisorTick2 } = await Promise.resolve().then(() => (init_supervisor_tick(), exports_supervisor_tick));
40526
+ runSupervisorTick2({
40527
+ apiUrl,
40528
+ tickId: body.tick_id,
40529
+ projectId: body.project_id,
40530
+ system: body.system,
40531
+ user: body.user,
40532
+ callbackUrl: body.callback_url,
40533
+ callbackToken: body.callback_token,
40534
+ log: log3
40535
+ }).catch((e) => log3(`[sup ${body.tick_id}] runSupervisorTick error: ${e.message}`));
40536
+ return Response.json({ accepted: true }, { status: 202 });
40537
+ } catch (e) {
40538
+ return Response.json({ error: String(e) }, { status: 400 });
40539
+ }
40540
+ })();
40541
+ }
40542
+ if (url2.pathname === "/stop" && req.method === "POST") {
40543
+ if (req.headers.get("authorization") !== expectedAuth)
40544
+ return new Response("unauthorized", { status: 401 });
40545
+ return (async () => {
40546
+ try {
40547
+ const { issue_id } = await req.json();
40548
+ if (!issue_id)
40549
+ return Response.json({ error: "issue_id required" }, { status: 400 });
40550
+ const entries2 = Array.from(running3.values()).filter((e) => e.issueId === issue_id);
40551
+ if (!entries2.length) {
40552
+ db2.run("UPDATE tasks SET status = 'failed', error = 'stopped before start', finished_at = unixepoch() WHERE issue_id = ? AND status = 'queued'", [issue_id]);
40553
+ return Response.json({ ok: true, state: "queued-cancelled" });
40554
+ }
40555
+ for (const entry of entries2) {
40556
+ entry.stopped = true;
40557
+ entry.stopReason = "user requested";
39751
40558
  try {
39752
- entry.child?.kill("SIGKILL");
40559
+ entry.child?.kill("SIGTERM");
39753
40560
  } catch {}
39754
- }, 1500);
40561
+ setTimeout(() => {
40562
+ try {
40563
+ entry.child?.kill("SIGKILL");
40564
+ } catch {}
40565
+ }, 1500);
40566
+ }
40567
+ return Response.json({ ok: true, state: "running-signalled" });
40568
+ } catch (e) {
40569
+ return Response.json({ error: String(e) }, { status: 400 });
39755
40570
  }
39756
- return Response.json({ ok: true, state: "running-signalled" });
39757
- } catch (e) {
39758
- return Response.json({ error: String(e) }, { status: 400 });
39759
- }
39760
- })();
40571
+ })();
40572
+ }
40573
+ return new Response("not found", { status: 404 });
40574
+ };
40575
+ const out = route();
40576
+ return out instanceof Promise ? out.then((r) => withCors(r, origin)) : withCors(out, origin);
40577
+ },
40578
+ websocket: {
40579
+ async open(ws) {
40580
+ const data = ws.data;
40581
+ if (!data || data.kind !== "chat-sync")
40582
+ return;
40583
+ if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
40584
+ try {
40585
+ ws.close(1011, "daemon not configured");
40586
+ } catch {}
40587
+ return;
40588
+ }
40589
+ const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
40590
+ const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
40591
+ apiUrl,
40592
+ authToken: cfg.authToken,
40593
+ workspaceId: cfg.workspaceId,
40594
+ deviceId: cfg.deviceId,
40595
+ log: log3
40596
+ });
40597
+ const sub = peer.addLocalSubscriber((frame) => {
40598
+ try {
40599
+ ws.sendBinary(frame);
40600
+ } catch {}
40601
+ });
40602
+ ws.data.peerToken = sub.token;
40603
+ ws.data.unsubscribe = sub.close;
40604
+ try {
40605
+ ws.sendBinary(peer.buildSnapshotGreet());
40606
+ } catch {}
40607
+ },
40608
+ async message(ws, message) {
40609
+ const data = ws.data;
40610
+ if (!data || data.kind !== "chat-sync" || !data.peerToken)
40611
+ return;
40612
+ if (typeof message === "string")
40613
+ return;
40614
+ const buf = message instanceof Uint8Array ? message : new Uint8Array(message);
40615
+ if (buf.byteLength < 1)
40616
+ return;
40617
+ const tag3 = buf[0];
40618
+ if (tag3 === 1) {
40619
+ const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
40620
+ const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
40621
+ apiUrl,
40622
+ authToken: cfg.authToken,
40623
+ workspaceId: cfg.workspaceId,
40624
+ deviceId: cfg.deviceId,
40625
+ log: log3
40626
+ });
40627
+ peer.ingestLocalUpdate(buf.subarray(1), data.peerToken);
40628
+ }
40629
+ },
40630
+ close(ws) {
40631
+ const data = ws.data;
40632
+ if (data?.unsubscribe) {
40633
+ try {
40634
+ data.unsubscribe();
40635
+ } catch {}
40636
+ }
39761
40637
  }
39762
- return new Response("not found", { status: 404 });
39763
40638
  }
39764
40639
  });
39765
40640
  log3(`Local server: http://127.0.0.1:${port}`);
39766
40641
  try {
39767
- writeFileSync9(PORT_PATH2, String(port));
40642
+ writeFileSync10(PORT_PATH2, String(port));
40643
+ } catch {}
40644
+ try {
40645
+ writeFileSync10(PID_PATH2, String(process.pid));
39768
40646
  } catch {}
39769
40647
  try {
39770
- writeFileSync9(PID_PATH2, String(process.pid));
40648
+ if (!existsSync16(LOCAL_SERVER_DIR))
40649
+ mkdirSync12(LOCAL_SERVER_DIR, { recursive: true });
40650
+ writeFileSync10(LOCAL_SERVER_PATH, JSON.stringify({
40651
+ url: `http://127.0.0.1:${port}`,
40652
+ port,
40653
+ token: cfg.dispatchSecret,
40654
+ device_id: cfg.deviceId,
40655
+ workspace_id: cfg.workspaceId,
40656
+ api_url: apiUrl,
40657
+ pid: process.pid
40658
+ }, null, 2));
39771
40659
  } catch {}
39772
40660
  let tunnel;
39773
40661
  if (localMode) {
@@ -39831,7 +40719,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39831
40719
  let probeFailures = 0;
39832
40720
  while (true) {
39833
40721
  yield* exports_Effect.sleep(exports_Duration.seconds(120));
39834
- if (existsSync14(STOP_PATH2)) {
40722
+ if (existsSync16(STOP_PATH2)) {
39835
40723
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39836
40724
  return;
39837
40725
  }
@@ -39856,7 +40744,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39856
40744
  yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
39857
40745
  while (true) {
39858
40746
  yield* exports_Effect.sleep(exports_Duration.seconds(5));
39859
- if (existsSync14(STOP_PATH2)) {
40747
+ if (existsSync16(STOP_PATH2)) {
39860
40748
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39861
40749
  return;
39862
40750
  }
@@ -39895,13 +40783,22 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39895
40783
  if (inFlight.length) {
39896
40784
  yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
39897
40785
  }
40786
+ try {
40787
+ const killed = killDaemonChildren(process.pid, (m) => log3(m));
40788
+ if (killed > 0)
40789
+ log3(`killed ${killed} adapter child${killed === 1 ? "" : "ren"} on shutdown`);
40790
+ } catch (e) {
40791
+ log3(`adapter shutdown sweep failed: ${String(e)}`);
40792
+ }
39898
40793
  yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
39899
- if (existsSync14(PID_PATH2))
40794
+ if (existsSync16(PID_PATH2))
39900
40795
  unlinkSync7(PID_PATH2);
39901
- if (existsSync14(STOP_PATH2))
40796
+ if (existsSync16(STOP_PATH2))
39902
40797
  unlinkSync7(STOP_PATH2);
39903
- if (existsSync14(PORT_PATH2))
40798
+ if (existsSync16(PORT_PATH2))
39904
40799
  unlinkSync7(PORT_PATH2);
40800
+ if (existsSync16(LOCAL_SERVER_PATH))
40801
+ unlinkSync7(LOCAL_SERVER_PATH);
39905
40802
  db2.close();
39906
40803
  log3("disconnected");
39907
40804
  });
@@ -39914,7 +40811,81 @@ async function daemonMain(opts) {
39914
40811
  }
39915
40812
 
39916
40813
  // src/commands/connect.ts
40814
+ init_detect();
39917
40815
  init_outbox();
40816
+
40817
+ // src/_impl/pair-flow.ts
40818
+ import { existsSync as existsSync17, mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "fs";
40819
+ import { homedir as homedir5 } from "os";
40820
+ import { join as join17 } from "path";
40821
+ var LOCAL_SERVER_DIR2 = process.env.MULTI_HOME || join17(homedir5(), ".multi");
40822
+ var LOCAL_SERVER_PATH2 = join17(LOCAL_SERVER_DIR2, "local-server.json");
40823
+ function writePairingInfo(payload) {
40824
+ try {
40825
+ if (!existsSync17(LOCAL_SERVER_DIR2))
40826
+ mkdirSync13(LOCAL_SERVER_DIR2, { recursive: true });
40827
+ writeFileSync11(LOCAL_SERVER_PATH2, JSON.stringify(payload, null, 2));
40828
+ } catch {}
40829
+ }
40830
+ async function awaitPairing(apiUrl, deviceName, opts = {}) {
40831
+ const log4 = opts.log ?? ((m) => console.log(m));
40832
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
40833
+ const startRes = await fetch(`${apiUrl}/api/pair/start`, {
40834
+ method: "POST",
40835
+ headers: { "content-type": "application/json" },
40836
+ body: JSON.stringify({
40837
+ name: deviceName,
40838
+ platform: opts.platform ?? process.platform,
40839
+ arch: opts.arch ?? process.arch,
40840
+ os_version: opts.osVersion ?? process.version,
40841
+ detected_runtimes: opts.detectedRuntimes ?? []
40842
+ })
40843
+ });
40844
+ if (!startRes.ok) {
40845
+ throw new Error(`pair/start failed: ${startRes.status} ${await startRes.text()}`);
40846
+ }
40847
+ const start3 = await startRes.json();
40848
+ const code = start3.code;
40849
+ const pairUrl = `${apiUrl}/pair/${code}`;
40850
+ writePairingInfo({
40851
+ status: "pairing",
40852
+ pair_code: code,
40853
+ pair_url: pairUrl,
40854
+ device_name: deviceName,
40855
+ api_url: apiUrl,
40856
+ expires_at: start3.expires_at
40857
+ });
40858
+ log4(`pair: code=${code} url=${pairUrl}`);
40859
+ const deadline = Date.now() + timeoutMs;
40860
+ while (Date.now() < deadline) {
40861
+ await new Promise((r) => setTimeout(r, 3000));
40862
+ let res;
40863
+ try {
40864
+ res = await fetch(`${apiUrl}/api/pair/poll/${code}`);
40865
+ } catch {
40866
+ continue;
40867
+ }
40868
+ if (res.status === 410)
40869
+ throw new Error("Pairing expired.");
40870
+ if (!res.ok)
40871
+ continue;
40872
+ const j = await res.json();
40873
+ if (j.status === "approved" && j.device_id && j.token) {
40874
+ return {
40875
+ device_id: j.device_id,
40876
+ token: j.token,
40877
+ dispatch_secret: j.dispatch_secret ?? null,
40878
+ workspace_id: j.workspace_id ?? null
40879
+ };
40880
+ }
40881
+ }
40882
+ throw new Error("Pairing timed out.");
40883
+ }
40884
+ function deviceNameFromEnv() {
40885
+ return process.env.MULTI_DEVICE_NAME || process.env.HOSTNAME || process.env.COMPUTERNAME || "Multi Desktop";
40886
+ }
40887
+
40888
+ // src/commands/connect.ts
39918
40889
  init_paths();
39919
40890
  init_errors();
39920
40891
  var connectCmd = exports_Effect.fn("connectCmd")(function* () {
@@ -39922,9 +40893,28 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
39922
40893
  const logger = yield* Logger4;
39923
40894
  const api2 = yield* Api2;
39924
40895
  yield* config2.ensureDirs;
39925
- const cfg = yield* config2.load;
39926
- if (!cfg.deviceId) {
39927
- return yield* exports_Effect.fail(new UsageError({ message: "Not registered. Run 'multi-agent setup' first." }));
40896
+ let cfg = yield* config2.load;
40897
+ if (!cfg.deviceId || !cfg.authToken) {
40898
+ const apiUrl = cfg.apiUrl || process.env.MULTI_API || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
40899
+ const name = deviceNameFromEnv();
40900
+ const detected = yield* exports_Effect.promise(() => detectAgents());
40901
+ yield* logger.log(`connect: no device registered — starting pair flow for "${name}" ` + `(runtimes: ${detected.map((d) => d.type).join(", ") || "none"})`);
40902
+ const bundle = yield* exports_Effect.tryPromise({
40903
+ try: () => awaitPairing(apiUrl, name, {
40904
+ log: (m) => console.log(m),
40905
+ detectedRuntimes: detected.map((d) => d.type)
40906
+ }),
40907
+ catch: (cause3) => new ApiError({ url: `${apiUrl}/api/pair/start`, status: 0, message: cause3.message })
40908
+ });
40909
+ yield* config2.save({
40910
+ apiUrl,
40911
+ deviceId: bundle.device_id,
40912
+ authToken: bundle.token,
40913
+ dispatchSecret: bundle.dispatch_secret ?? undefined,
40914
+ workspaceId: bundle.workspace_id ?? undefined
40915
+ });
40916
+ cfg = yield* config2.load;
40917
+ yield* logger.log(`connect: paired device=${cfg.deviceId} workspace=${cfg.workspaceId}`);
39928
40918
  }
39929
40919
  if (!cfg.dispatchSecret) {
39930
40920
  return yield* exports_Effect.fail(new UsageError({ message: "Missing dispatch secret. Re-pair via 'multi-agent setup'." }));
@@ -39961,6 +40951,250 @@ ${err?.stack ?? ""}`);
39961
40951
  }).pipe(exports_Effect.ensuring(exports_Effect.sync(() => stopFlusher())));
39962
40952
  });
39963
40953
 
40954
+ // src/commands/service.ts
40955
+ init_esm();
40956
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
40957
+ import { homedir as homedir6, platform } from "os";
40958
+ import { join as join18 } from "path";
40959
+ init_errors();
40960
+ init_paths();
40961
+ var LABEL = "dev.shipers.multi.daemon";
40962
+ var HOME5 = homedir6();
40963
+ var PLIST_PATH = join18(HOME5, "Library", "LaunchAgents", `${LABEL}.plist`);
40964
+ var SYSTEMD_UNIT_PATH = join18(HOME5, ".config", "systemd", "user", "multi-daemon.service");
40965
+ var isMac = () => platform() === "darwin";
40966
+ var isLinux = () => platform() === "linux";
40967
+ var isRunningPid = (pid) => {
40968
+ try {
40969
+ process.kill(pid, 0);
40970
+ return true;
40971
+ } catch {
40972
+ return false;
40973
+ }
40974
+ };
40975
+ var readPid = () => {
40976
+ try {
40977
+ if (!existsSync18(PID_PATH))
40978
+ return null;
40979
+ const n = Number(readFileSync12(PID_PATH, "utf8").trim());
40980
+ return Number.isFinite(n) && n > 0 ? n : null;
40981
+ } catch {
40982
+ return null;
40983
+ }
40984
+ };
40985
+ var resolveBinary = exports_Effect.fn("service.resolveBinary")(function* () {
40986
+ const proc = Bun.spawn(["which", "multi-agent"], { stdout: "pipe", stderr: "pipe" });
40987
+ yield* exports_Effect.promise(() => proc.exited);
40988
+ const out = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
40989
+ const path = out.trim();
40990
+ if (!path || !existsSync18(path)) {
40991
+ return yield* exports_Effect.fail(new DaemonError({
40992
+ message: "Could not find `multi-agent` on PATH. Install with: bun install -g @shipers-dev/multi"
40993
+ }));
40994
+ }
40995
+ return path;
40996
+ });
40997
+ var run6 = exports_Effect.fn("service.run")(function* (cmd, args2) {
40998
+ const proc = Bun.spawn([cmd, ...args2], { stdout: "pipe", stderr: "pipe" });
40999
+ yield* exports_Effect.promise(() => proc.exited);
41000
+ const stdout = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
41001
+ const stderr = yield* exports_Effect.promise(() => new Response(proc.stderr).text());
41002
+ if (proc.exitCode !== 0) {
41003
+ return yield* exports_Effect.fail(new SpawnError({
41004
+ cmd: `${cmd} ${args2.join(" ")}`,
41005
+ cause: stderr || stdout || `exit ${proc.exitCode}`
41006
+ }));
41007
+ }
41008
+ return { stdout, stderr };
41009
+ });
41010
+ var buildPlist = (binary, env) => {
41011
+ const envEntries = Object.entries(env).map(([k, v]) => ` <key>${k}</key>
41012
+ <string>${escapeXml(v)}</string>`).join(`
41013
+ `);
41014
+ const logDir = join18(MULTI_DIR, "logs");
41015
+ return `<?xml version="1.0" encoding="UTF-8"?>
41016
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
41017
+ <plist version="1.0">
41018
+ <dict>
41019
+ <key>Label</key>
41020
+ <string>${LABEL}</string>
41021
+ <key>ProgramArguments</key>
41022
+ <array>
41023
+ <string>${escapeXml(binary)}</string>
41024
+ <string>connect</string>
41025
+ </array>
41026
+ <key>RunAtLoad</key>
41027
+ <true/>
41028
+ <key>KeepAlive</key>
41029
+ <true/>
41030
+ <key>WorkingDirectory</key>
41031
+ <string>${escapeXml(HOME5)}</string>
41032
+ <key>StandardOutPath</key>
41033
+ <string>${escapeXml(join18(logDir, "launchd.out.log"))}</string>
41034
+ <key>StandardErrorPath</key>
41035
+ <string>${escapeXml(join18(logDir, "launchd.err.log"))}</string>
41036
+ <key>EnvironmentVariables</key>
41037
+ <dict>
41038
+ ${envEntries}
41039
+ </dict>
41040
+ <key>ProcessType</key>
41041
+ <string>Interactive</string>
41042
+ </dict>
41043
+ </plist>
41044
+ `;
41045
+ };
41046
+ var escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
41047
+ var buildSystemdUnit = (binary, env) => {
41048
+ const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}`).join(`
41049
+ `);
41050
+ return `[Unit]
41051
+ Description=Multi agent daemon
41052
+ After=network-online.target
41053
+
41054
+ [Service]
41055
+ Type=simple
41056
+ ExecStart=${binary} connect
41057
+ Restart=always
41058
+ RestartSec=3
41059
+ WorkingDirectory=${HOME5}
41060
+ ${envLines}
41061
+
41062
+ [Install]
41063
+ WantedBy=default.target
41064
+ `;
41065
+ };
41066
+ var collectServiceEnv = () => {
41067
+ const env = {};
41068
+ if (process.env.PATH)
41069
+ env.PATH = process.env.PATH;
41070
+ if (process.env.HOME)
41071
+ env.HOME = process.env.HOME;
41072
+ if (process.env.MULTI_HOME)
41073
+ env.MULTI_HOME = process.env.MULTI_HOME;
41074
+ if (process.env.MULTI_API)
41075
+ env.MULTI_API = process.env.MULTI_API;
41076
+ if (process.env.SHELL)
41077
+ env.SHELL = process.env.SHELL;
41078
+ if (process.env.LANG)
41079
+ env.LANG = process.env.LANG;
41080
+ return env;
41081
+ };
41082
+ var ensureSupported = exports_Effect.fn("service.ensureSupported")(function* () {
41083
+ if (!isMac() && !isLinux()) {
41084
+ return yield* exports_Effect.fail(new UsageError({
41085
+ message: `service commands are not supported on ${platform()} yet (macOS + Linux only).`
41086
+ }));
41087
+ }
41088
+ });
41089
+ var installMac = exports_Effect.fn("service.installMac")(function* (binary) {
41090
+ const fs3 = yield* FileSystem;
41091
+ const env = collectServiceEnv();
41092
+ const plist = buildPlist(binary, env);
41093
+ yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
41094
+ yield* fs3.writeText(PLIST_PATH, plist);
41095
+ yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
41096
+ yield* run6("launchctl", ["load", "-w", PLIST_PATH]);
41097
+ console.log(`✅ Installed launchd agent at ${PLIST_PATH}`);
41098
+ console.log(` The daemon will start now and on every login.`);
41099
+ });
41100
+ var installLinux = exports_Effect.fn("service.installLinux")(function* (binary) {
41101
+ const fs3 = yield* FileSystem;
41102
+ const env = collectServiceEnv();
41103
+ const unit = buildSystemdUnit(binary, env);
41104
+ yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
41105
+ yield* fs3.writeText(SYSTEMD_UNIT_PATH, unit);
41106
+ yield* run6("systemctl", ["--user", "daemon-reload"]);
41107
+ yield* run6("systemctl", ["--user", "enable", "--now", "multi-daemon.service"]);
41108
+ console.log(`✅ Installed systemd user unit at ${SYSTEMD_UNIT_PATH}`);
41109
+ console.log(` The daemon will start now and on every login.`);
41110
+ console.log(` To keep it running after logout: loginctl enable-linger ${process.env.USER || ""}`.trimEnd());
41111
+ });
41112
+ var uninstallMac = exports_Effect.fn("service.uninstallMac")(function* () {
41113
+ const fs3 = yield* FileSystem;
41114
+ if (existsSync18(PLIST_PATH)) {
41115
+ yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
41116
+ yield* fs3.remove(PLIST_PATH);
41117
+ console.log(`\uD83D\uDDD1 Removed ${PLIST_PATH}`);
41118
+ } else {
41119
+ console.log("Nothing to uninstall (no plist found).");
41120
+ }
41121
+ });
41122
+ var uninstallLinux = exports_Effect.fn("service.uninstallLinux")(function* () {
41123
+ const fs3 = yield* FileSystem;
41124
+ if (existsSync18(SYSTEMD_UNIT_PATH)) {
41125
+ yield* run6("systemctl", ["--user", "disable", "--now", "multi-daemon.service"]).pipe(exports_Effect.ignore);
41126
+ yield* fs3.remove(SYSTEMD_UNIT_PATH);
41127
+ yield* run6("systemctl", ["--user", "daemon-reload"]).pipe(exports_Effect.ignore);
41128
+ console.log(`\uD83D\uDDD1 Removed ${SYSTEMD_UNIT_PATH}`);
41129
+ } else {
41130
+ console.log("Nothing to uninstall (no unit found).");
41131
+ }
41132
+ });
41133
+ var serviceCmd = exports_Effect.fn("serviceCmd")(function* (sub) {
41134
+ yield* ensureSupported();
41135
+ switch (sub) {
41136
+ case "install": {
41137
+ const binary = yield* resolveBinary();
41138
+ if (isMac())
41139
+ yield* installMac(binary);
41140
+ else
41141
+ yield* installLinux(binary);
41142
+ return;
41143
+ }
41144
+ case "uninstall": {
41145
+ if (isMac())
41146
+ yield* uninstallMac();
41147
+ else
41148
+ yield* uninstallLinux();
41149
+ return;
41150
+ }
41151
+ case "start": {
41152
+ if (isMac()) {
41153
+ if (!existsSync18(PLIST_PATH)) {
41154
+ return yield* exports_Effect.fail(new DaemonError({
41155
+ message: "Not installed. Run: multi-agent service install"
41156
+ }));
41157
+ }
41158
+ yield* run6("launchctl", ["kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${LABEL}`]).pipe(exports_Effect.ignore);
41159
+ yield* run6("launchctl", ["load", "-w", PLIST_PATH]).pipe(exports_Effect.ignore);
41160
+ console.log("▶ Started.");
41161
+ } else {
41162
+ yield* run6("systemctl", ["--user", "start", "multi-daemon.service"]);
41163
+ console.log("▶ Started.");
41164
+ }
41165
+ return;
41166
+ }
41167
+ case "stop": {
41168
+ if (isMac()) {
41169
+ yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
41170
+ console.log("⏹ Stopped.");
41171
+ } else {
41172
+ yield* run6("systemctl", ["--user", "stop", "multi-daemon.service"]);
41173
+ console.log("⏹ Stopped.");
41174
+ }
41175
+ return;
41176
+ }
41177
+ case "status":
41178
+ case undefined: {
41179
+ const installed = isMac() ? existsSync18(PLIST_PATH) : existsSync18(SYSTEMD_UNIT_PATH);
41180
+ const pid = readPid();
41181
+ const running3 = pid !== null && isRunningPid(pid);
41182
+ console.log(`Service: ${isMac() ? "launchd" : "systemd --user"}`);
41183
+ console.log(`Installed: ${installed ? "yes" : "no"}`);
41184
+ console.log(`Unit path: ${isMac() ? PLIST_PATH : SYSTEMD_UNIT_PATH}`);
41185
+ console.log(`Daemon: ${running3 ? `running (pid ${pid})` : "stopped"}`);
41186
+ if (!installed)
41187
+ console.log(`
41188
+ Not installed. Run: multi-agent service install`);
41189
+ return;
41190
+ }
41191
+ default:
41192
+ return yield* exports_Effect.fail(new UsageError({
41193
+ message: `Unknown service subcommand: ${sub}. Use: install | uninstall | start | stop | status`
41194
+ }));
41195
+ }
41196
+ });
41197
+
39964
41198
  // src/index.ts
39965
41199
  var VERSION = package_default.version;
39966
41200
  var COMMANDS = {
@@ -39973,7 +41207,8 @@ var COMMANDS = {
39973
41207
  restart: "Stop and relaunch the daemon in background",
39974
41208
  logs: "View execution logs",
39975
41209
  reset: "Reset acpx session for an issue (--issue <id>)",
39976
- worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)"
41210
+ worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)",
41211
+ service: "Manage the always-on daemon (subcommands: install, uninstall, start, stop, status)"
39977
41212
  };
39978
41213
  function printUsage() {
39979
41214
  console.log(`multi-agent v${VERSION}
@@ -40028,7 +41263,7 @@ var program = exports_Effect.gen(function* () {
40028
41263
  force: !!values3.force,
40029
41264
  noPush: !!values3["no-push"],
40030
41265
  key: values3.key
40031
- })), exports_Match.exhaustive);
41266
+ })), exports_Match.when("service", () => serviceCmd(positionals[1])), exports_Match.exhaustive);
40032
41267
  });
40033
41268
  var main = program.pipe(exports_Effect.provide(AppLayer), exports_Effect.tapErrorCause((cause3) => exports_Effect.sync(() => {
40034
41269
  console.error(exports_Cause.pretty(cause3));