agent-relay-server 0.120.1 → 0.120.3

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 (181) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/MessageBubble-Cqi3XsZ7.js +2 -0
  4. package/public/assets/{MessageBubble-jpshgh3K.js.map → MessageBubble-Cqi3XsZ7.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-CjVG-Jsq.js → PlanGraphPanel-D6Um05oW.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CjVG-Jsq.js.map → PlanGraphPanel-D6Um05oW.js.map} +1 -1
  7. package/public/assets/{activity-DLTFF8Dz.js → activity-B0LsQHEz.js} +2 -2
  8. package/public/assets/{activity-DLTFF8Dz.js.map → activity-B0LsQHEz.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BcCLIrON.js → agent-profiles-O--pe5Ic.js} +2 -2
  10. package/public/assets/{agent-profiles-BcCLIrON.js.map → agent-profiles-O--pe5Ic.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-BEz393ra.js → agent-type-icon-CeEoMouY.js} +2 -2
  12. package/public/assets/{agent-type-icon-BEz393ra.js.map → agent-type-icon-CeEoMouY.js.map} +1 -1
  13. package/public/assets/agents-Ii6XlVwR.js +2 -0
  14. package/public/assets/{agents-QemvuxKO.js.map → agents-Ii6XlVwR.js.map} +1 -1
  15. package/public/assets/{analytics-DMLoMwta.js → analytics-BCzdYWRT.js} +2 -2
  16. package/public/assets/{analytics-DMLoMwta.js.map → analytics-BCzdYWRT.js.map} +1 -1
  17. package/public/assets/{automation-DACSOffp.js → automation-Crr3Xzui.js} +2 -2
  18. package/public/assets/{automation-DACSOffp.js.map → automation-Crr3Xzui.js.map} +1 -1
  19. package/public/assets/{badge-BhUZ1cAb.js → badge-DCIT3Irj.js} +2 -2
  20. package/public/assets/{badge-BhUZ1cAb.js.map → badge-DCIT3Irj.js.map} +1 -1
  21. package/public/assets/{branch-state-badge-BYE2U_y4.js → branch-state-badge-Bhw2rloy.js} +2 -2
  22. package/public/assets/{branch-state-badge-BYE2U_y4.js.map → branch-state-badge-Bhw2rloy.js.map} +1 -1
  23. package/public/assets/{button-C-V4oij1.js → button-CzdTkRIf.js} +2 -2
  24. package/public/assets/{button-C-V4oij1.js.map → button-CzdTkRIf.js.map} +1 -1
  25. package/public/assets/{card-Br1JLjTH.js → card-C3BGuVC6.js} +2 -2
  26. package/public/assets/{card-Br1JLjTH.js.map → card-C3BGuVC6.js.map} +1 -1
  27. package/public/assets/{channels-3SlDQL3j.js → channels-CKs9rvsn.js} +2 -2
  28. package/public/assets/{channels-3SlDQL3j.js.map → channels-CKs9rvsn.js.map} +1 -1
  29. package/public/assets/chat-BCwEUj5R.js +2 -0
  30. package/public/assets/chat-BCwEUj5R.js.map +1 -0
  31. package/public/assets/{connectors-CrEW591Q.js → connectors-D_kQR_vw.js} +2 -2
  32. package/public/assets/{connectors-CrEW591Q.js.map → connectors-D_kQR_vw.js.map} +1 -1
  33. package/public/assets/{copy-button-BStzXgtd.js → copy-button-B3o1jpLu.js} +2 -2
  34. package/public/assets/{copy-button-BStzXgtd.js.map → copy-button-B3o1jpLu.js.map} +1 -1
  35. package/public/assets/{dist-ChDX_t12.js → dist-B-T7lg0a.js} +2 -2
  36. package/public/assets/{dist-ChDX_t12.js.map → dist-B-T7lg0a.js.map} +1 -1
  37. package/public/assets/{dist-DV83j9tm.js → dist-B3e4B9pg.js} +2 -2
  38. package/public/assets/{dist-DV83j9tm.js.map → dist-B3e4B9pg.js.map} +1 -1
  39. package/public/assets/{dist-y03IwOrd.js → dist-B6woQ9Tb.js} +2 -2
  40. package/public/assets/{dist-y03IwOrd.js.map → dist-B6woQ9Tb.js.map} +1 -1
  41. package/public/assets/{dist-CQ0Z-TDi.js → dist-B9G83DXv.js} +2 -2
  42. package/public/assets/{dist-CQ0Z-TDi.js.map → dist-B9G83DXv.js.map} +1 -1
  43. package/public/assets/{dist-Cs8X0P1d.js → dist-BIjlN-xB.js} +2 -2
  44. package/public/assets/{dist-Cs8X0P1d.js.map → dist-BIjlN-xB.js.map} +1 -1
  45. package/public/assets/{dist-DyNJNwpi.js → dist-BInckSJC.js} +2 -2
  46. package/public/assets/{dist-DyNJNwpi.js.map → dist-BInckSJC.js.map} +1 -1
  47. package/public/assets/{dist-CmFeEXMe.js → dist-BlzVGRhh.js} +2 -2
  48. package/public/assets/{dist-CmFeEXMe.js.map → dist-BlzVGRhh.js.map} +1 -1
  49. package/public/assets/{dist-CDZZxxJq.js → dist-C7aTlosb.js} +2 -2
  50. package/public/assets/{dist-CDZZxxJq.js.map → dist-C7aTlosb.js.map} +1 -1
  51. package/public/assets/{dist-D2jvo5oA.js → dist-DGfp5dLz.js} +2 -2
  52. package/public/assets/{dist-D2jvo5oA.js.map → dist-DGfp5dLz.js.map} +1 -1
  53. package/public/assets/{dist-mm7c7nBt.js → dist-DT-OV6sQ.js} +2 -2
  54. package/public/assets/{dist-mm7c7nBt.js.map → dist-DT-OV6sQ.js.map} +1 -1
  55. package/public/assets/{dist-CBMWGhWc.js → dist-OUPTEP6q.js} +2 -2
  56. package/public/assets/{dist-CBMWGhWc.js.map → dist-OUPTEP6q.js.map} +1 -1
  57. package/public/assets/{es2015-VnZ2kVIz.js → es2015-CnKXYKpf.js} +2 -2
  58. package/public/assets/{es2015-VnZ2kVIz.js.map → es2015-CnKXYKpf.js.map} +1 -1
  59. package/public/assets/formatted-body-impl-CSRGuMcM.js +3 -0
  60. package/public/assets/formatted-body-impl-CSRGuMcM.js.map +1 -0
  61. package/public/assets/formatted-body-q_qzt1L-.js +3 -0
  62. package/public/assets/{formatted-body-Dg9R3RD7.js.map → formatted-body-q_qzt1L-.js.map} +1 -1
  63. package/public/assets/{index-D_fNJnsI.js → index-Ceiwd02X.js} +10 -10
  64. package/public/assets/index-Ceiwd02X.js.map +1 -0
  65. package/public/assets/{input-CtLT7WXi.js → input-B6Q0NWCS.js} +2 -2
  66. package/public/assets/{input-CtLT7WXi.js.map → input-B6Q0NWCS.js.map} +1 -1
  67. package/public/assets/{insights-D48n2ojF.js → insights-DUhXqZTZ.js} +2 -2
  68. package/public/assets/{insights-D48n2ojF.js.map → insights-DUhXqZTZ.js.map} +1 -1
  69. package/public/assets/{integrations-B-bdliwU.js → integrations-lbg5CjtE.js} +2 -2
  70. package/public/assets/{integrations-B-bdliwU.js.map → integrations-lbg5CjtE.js.map} +1 -1
  71. package/public/assets/{lib-ByLQqcD1.js → lib-BHsWPMKB.js} +2 -2
  72. package/public/assets/{lib-ByLQqcD1.js.map → lib-BHsWPMKB.js.map} +1 -1
  73. package/public/assets/lucide-react-DuhlF2Sj.js +10 -0
  74. package/public/assets/lucide-react-DuhlF2Sj.js.map +1 -0
  75. package/public/assets/{maintenance-LQ_U3Nqy.js → maintenance-CQgneSVf.js} +2 -2
  76. package/public/assets/{maintenance-LQ_U3Nqy.js.map → maintenance-CQgneSVf.js.map} +1 -1
  77. package/public/assets/{managed-agents-Cfh8FvY8.js → managed-agents-DsnYf6fm.js} +4 -4
  78. package/public/assets/{managed-agents-Cfh8FvY8.js.map → managed-agents-DsnYf6fm.js.map} +1 -1
  79. package/public/assets/{markdown-preview-impl-BSa7zYx1.js → markdown-preview-impl-C1ZtGJp6.js} +2 -2
  80. package/public/assets/{markdown-preview-impl-BSa7zYx1.js.map → markdown-preview-impl-C1ZtGJp6.js.map} +1 -1
  81. package/public/assets/{memory-Dr1iYKkC.js → memory-SSLUTOI2.js} +2 -2
  82. package/public/assets/{memory-Dr1iYKkC.js.map → memory-SSLUTOI2.js.map} +1 -1
  83. package/public/assets/messages-BY26OP4K.js +2 -0
  84. package/public/assets/{messages-DRKulS-2.js.map → messages-BY26OP4K.js.map} +1 -1
  85. package/public/assets/{orchestrators-BguyeViG.js → orchestrators-OY7yPzVI.js} +2 -2
  86. package/public/assets/{orchestrators-BguyeViG.js.map → orchestrators-OY7yPzVI.js.map} +1 -1
  87. package/public/assets/overview-Dj0u1tPG.js +2 -0
  88. package/public/assets/{overview-Dbq2lr7Y.js.map → overview-Dj0u1tPG.js.map} +1 -1
  89. package/public/assets/{pairs-DD7i2scF.js → pairs-CrUR50ou.js} +2 -2
  90. package/public/assets/{pairs-DD7i2scF.js.map → pairs-CrUR50ou.js.map} +1 -1
  91. package/public/assets/{projects-DEn85LQb.js → projects-B_AtxHtH.js} +2 -2
  92. package/public/assets/{projects-DEn85LQb.js.map → projects-B_AtxHtH.js.map} +1 -1
  93. package/public/assets/{prompt-settings-DwgrPKY3.js → prompt-settings-CPtYSban.js} +2 -2
  94. package/public/assets/{prompt-settings-DwgrPKY3.js.map → prompt-settings-CPtYSban.js.map} +1 -1
  95. package/public/assets/{react-dom-cLkVFBsS.js → react-dom-DWBmcawP.js} +2 -2
  96. package/public/assets/{react-dom-cLkVFBsS.js.map → react-dom-DWBmcawP.js.map} +1 -1
  97. package/public/assets/{registry-DCR3xFpe.js → registry-BgUALWn5.js} +2 -2
  98. package/public/assets/{registry-DCR3xFpe.js.map → registry-BgUALWn5.js.map} +1 -1
  99. package/public/assets/security-5lHfGk7g.js +3 -0
  100. package/public/assets/security-5lHfGk7g.js.map +1 -0
  101. package/public/assets/{select-CpyCRBK3.js → select-Cgx4QSLj.js} +2 -2
  102. package/public/assets/{select-CpyCRBK3.js.map → select-Cgx4QSLj.js.map} +1 -1
  103. package/public/assets/settings-DFGgxyun.js +6 -0
  104. package/public/assets/{settings-DYNFGC2-.js.map → settings-DFGgxyun.js.map} +1 -1
  105. package/public/assets/store-BM4zM89B.js +9 -0
  106. package/public/assets/store-BM4zM89B.js.map +1 -0
  107. package/public/assets/{task-chip-kxk4eNm1.js → task-chip-DXfvXBDx.js} +2 -2
  108. package/public/assets/{task-chip-kxk4eNm1.js.map → task-chip-DXfvXBDx.js.map} +1 -1
  109. package/public/assets/{tasks-rdy9Nsbf.js → tasks-BvH5rluu.js} +2 -2
  110. package/public/assets/{tasks-rdy9Nsbf.js.map → tasks-BvH5rluu.js.map} +1 -1
  111. package/public/assets/{teams-DHloBagJ.js → teams-VZOIOdoE.js} +2 -2
  112. package/public/assets/{teams-DHloBagJ.js.map → teams-VZOIOdoE.js.map} +1 -1
  113. package/public/assets/{terminal-viewer-impl-DWTuNejR.js → terminal-viewer-impl-DYY3Wmoe.js} +2 -2
  114. package/public/assets/{terminal-viewer-impl-DWTuNejR.js.map → terminal-viewer-impl-DYY3Wmoe.js.map} +1 -1
  115. package/public/assets/types-uVOXgvMT.js.map +1 -1
  116. package/public/assets/{use-keyboard-viewport-B06XtRUZ.js → use-keyboard-viewport-CP109w9y.js} +2 -2
  117. package/public/assets/{use-keyboard-viewport-B06XtRUZ.js.map → use-keyboard-viewport-CP109w9y.js.map} +1 -1
  118. package/public/assets/{user-avatar-DuwCFrER.js → user-avatar-Dw6mzWhv.js} +2 -2
  119. package/public/assets/{user-avatar-DuwCFrER.js.map → user-avatar-Dw6mzWhv.js.map} +1 -1
  120. package/public/assets/{work-queue-CShT7nTC.js → work-queue-B6Xk7QNe.js} +2 -2
  121. package/public/assets/{work-queue-CShT7nTC.js.map → work-queue-B6Xk7QNe.js.map} +1 -1
  122. package/public/assets/{workspaces-DjQ6EdrZ.js → workspaces-CPIzAHAe.js} +2 -2
  123. package/public/assets/{workspaces-DjQ6EdrZ.js.map → workspaces-CPIzAHAe.js.map} +1 -1
  124. package/public/index.html +23 -23
  125. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  126. package/src/automations/reconcile.ts +69 -4
  127. package/src/branch-landed.ts +18 -11
  128. package/src/bus.ts +25 -18
  129. package/src/db/connection.ts +53 -1
  130. package/src/db/maintenance-worker.ts +37 -0
  131. package/src/db/message-reads.ts +86 -45
  132. package/src/db/migrations.ts +1 -0
  133. package/src/db/provider-quotas.ts +6 -7
  134. package/src/db/schema.ts +2 -1
  135. package/src/maintenance/jobs.ts +19 -13
  136. package/src/maintenance/scheduler.ts +8 -3
  137. package/src/maintenance/types.ts +5 -1
  138. package/src/mcp/index.ts +5 -5
  139. package/src/mcp/tools-workspace.ts +8 -3
  140. package/src/routes/orchestrator-proxy.ts +7 -6
  141. package/src/routes/sse.ts +2 -1
  142. package/src/routes/tasks.ts +29 -0
  143. package/src/routes/workspaces.ts +9 -2
  144. package/src/runtime-tokens.ts +4 -0
  145. package/src/security.ts +31 -7
  146. package/src/server.ts +40 -18
  147. package/src/services/git-process.ts +61 -0
  148. package/src/services/git-remote.ts +87 -7
  149. package/src/services/issue-lifecycle.ts +6 -6
  150. package/src/services/project.ts +47 -1
  151. package/src/services/send-message.ts +2 -4
  152. package/src/services/spawn-agent.ts +2 -2
  153. package/src/services/task-lifecycle.ts +6 -9
  154. package/src/settings/registry.ts +6 -4
  155. package/src/spawn-targets.ts +2 -2
  156. package/src/sse.ts +131 -46
  157. package/src/steward.ts +10 -6
  158. package/src/terminal-authz.ts +50 -0
  159. package/src/token-db.ts +3 -3
  160. package/src/validation.ts +1 -1
  161. package/src/workspace-actions.ts +46 -4
  162. package/src/workspace-auto-merge.ts +120 -13
  163. package/src/workspace-orphans.ts +1 -1
  164. package/src/workspace-probe.ts +19 -10
  165. package/public/assets/MessageBubble-jpshgh3K.js +0 -2
  166. package/public/assets/agents-QemvuxKO.js +0 -2
  167. package/public/assets/chat-xKXCafN9.js +0 -2
  168. package/public/assets/chat-xKXCafN9.js.map +0 -1
  169. package/public/assets/formatted-body-Dg9R3RD7.js +0 -3
  170. package/public/assets/formatted-body-impl-DlasTD8X.js +0 -3
  171. package/public/assets/formatted-body-impl-DlasTD8X.js.map +0 -1
  172. package/public/assets/index-D_fNJnsI.js.map +0 -1
  173. package/public/assets/lucide-react-Dc3ZwCBi.js +0 -9
  174. package/public/assets/lucide-react-Dc3ZwCBi.js.map +0 -1
  175. package/public/assets/messages-DRKulS-2.js +0 -2
  176. package/public/assets/overview-Dbq2lr7Y.js +0 -2
  177. package/public/assets/security-BH8kTdA-.js +0 -3
  178. package/public/assets/security-BH8kTdA-.js.map +0 -1
  179. package/public/assets/settings-DYNFGC2-.js +0 -6
  180. package/public/assets/store-BuuJUn70.js +0 -9
  181. package/public/assets/store-BuuJUn70.js.map +0 -1
@@ -0,0 +1,61 @@
1
+ export const LAND_GIT_TIMEOUT_MS = 2_000;
2
+
3
+ export interface GitProcessResult {
4
+ stdout: string;
5
+ stderr: string;
6
+ exitCode: number;
7
+ timedOut: boolean;
8
+ }
9
+
10
+ export async function runGit(args: string[], timeoutMs = LAND_GIT_TIMEOUT_MS): Promise<GitProcessResult> {
11
+ const proc = Bun.spawn(["git", ...args], {
12
+ stdin: "ignore",
13
+ stdout: "pipe",
14
+ stderr: "pipe",
15
+ });
16
+ const stdoutPromise = streamText(proc.stdout);
17
+ const stderrPromise = streamText(proc.stderr);
18
+ const timedOut = await waitForExitOrTimeout(proc, timeoutMs);
19
+ const [stdout, stderr] = timedOut
20
+ ? await Promise.all([settleText(stdoutPromise), settleText(stderrPromise)])
21
+ : await Promise.all([stdoutPromise, stderrPromise]);
22
+ return { stdout, stderr, exitCode: proc.exitCode ?? 1, timedOut };
23
+ }
24
+
25
+ async function streamText(stream: ReadableStream<Uint8Array> | null): Promise<string> {
26
+ if (!stream) return "";
27
+ try {
28
+ return await new Response(stream).text();
29
+ } catch {
30
+ return "";
31
+ }
32
+ }
33
+
34
+ async function settleText(promise: Promise<string>): Promise<string> {
35
+ return Promise.race([promise, new Promise<string>((resolve) => setTimeout(() => resolve(""), 25))]);
36
+ }
37
+
38
+ async function waitForExitOrTimeout(proc: Bun.Subprocess<"ignore", "pipe", "pipe">, timeoutMs: number | undefined): Promise<boolean> {
39
+ if (!timeoutMs || timeoutMs <= 0) {
40
+ await proc.exited;
41
+ return false;
42
+ }
43
+ let timedOut = false;
44
+ let kill: ReturnType<typeof setTimeout> | undefined;
45
+ const timeout = setTimeout(() => {
46
+ timedOut = true;
47
+ try { proc.kill("SIGTERM"); } catch {}
48
+ kill = setTimeout(() => {
49
+ try { proc.kill("SIGKILL"); } catch {}
50
+ }, 1_000);
51
+ kill.unref?.();
52
+ }, timeoutMs);
53
+ timeout.unref?.();
54
+ try {
55
+ await proc.exited;
56
+ } finally {
57
+ clearTimeout(timeout);
58
+ if (kill) clearTimeout(kill);
59
+ }
60
+ return timedOut;
61
+ }
@@ -3,6 +3,9 @@
3
3
  // not drift: issue-lifecycle land-close (which repo a bare `#NNN` in a commit subject
4
4
  // closes) AND project auto-seed (a project's `issueRepo` for chat #NNN linkify). Promoted
5
5
  // out of issue-lifecycle.ts where it was private — see the DRY rule in CLAUDE.md.
6
+ import { runGit } from "./git-process";
7
+ import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
9
 
7
10
  /**
8
11
  * `owner/repo` out of any git remote URL form: `git@github.com:owner/repo.git`,
@@ -18,18 +21,14 @@ export function parseOwnerRepoFromRemote(url: string | undefined): string | unde
18
21
  let remoteRepoCache: Map<string, string | undefined> | undefined;
19
22
  let remoteUrlCache: Map<string, string | undefined> | undefined;
20
23
 
21
- export function remoteUrlFromGitRemote(repoRoot: string | undefined): string | undefined {
24
+ export async function remoteUrlFromGitRemoteAsync(repoRoot: string | undefined): Promise<string | undefined> {
22
25
  if (!repoRoot) return undefined;
23
26
  if (!remoteUrlCache) remoteUrlCache = new Map();
24
27
  if (remoteUrlCache.has(repoRoot)) return remoteUrlCache.get(repoRoot);
25
28
  let url: string | undefined;
26
29
  try {
27
- const proc = Bun.spawnSync(["git", "-C", repoRoot, "remote", "get-url", "origin"], {
28
- stdin: "ignore",
29
- stdout: "pipe",
30
- stderr: "ignore",
31
- });
32
- if (proc.exitCode === 0) url = Buffer.from(proc.stdout).toString("utf8").trim() || undefined;
30
+ const { stdout, exitCode } = await runGit(["-C", repoRoot, "remote", "get-url", "origin"]);
31
+ if (exitCode === 0) url = stdout.trim() || undefined;
33
32
  } catch {
34
33
  url = undefined;
35
34
  }
@@ -37,6 +36,24 @@ export function remoteUrlFromGitRemote(repoRoot: string | undefined): string | u
37
36
  return url;
38
37
  }
39
38
 
39
+ export function remoteUrlFromGitRemote(repoRoot: string | undefined): string | undefined {
40
+ if (!repoRoot) return undefined;
41
+ if (!remoteUrlCache) remoteUrlCache = new Map();
42
+ if (remoteUrlCache.has(repoRoot)) return remoteUrlCache.get(repoRoot);
43
+ const url = remoteUrlFromGitConfig(repoRoot);
44
+ remoteUrlCache.set(repoRoot, url);
45
+ return url;
46
+ }
47
+
48
+ export async function repoFromGitRemoteAsync(repoRoot: string | undefined): Promise<string | undefined> {
49
+ if (!repoRoot) return undefined;
50
+ if (!remoteRepoCache) remoteRepoCache = new Map();
51
+ if (remoteRepoCache.has(repoRoot)) return remoteRepoCache.get(repoRoot);
52
+ const repo = parseOwnerRepoFromRemote(await remoteUrlFromGitRemoteAsync(repoRoot));
53
+ remoteRepoCache.set(repoRoot, repo);
54
+ return repo;
55
+ }
56
+
40
57
  /**
41
58
  * `owner/repo` from a repo root's `origin` remote, cached per root. Works from any
42
59
  * subdirectory of the repo (`git -C` walks up). Returns undefined when the dir isn't a
@@ -55,3 +72,66 @@ export function __resetRemoteRepoCacheForTests(): void {
55
72
  remoteRepoCache = undefined;
56
73
  remoteUrlCache = undefined;
57
74
  }
75
+
76
+ function remoteUrlFromGitConfig(repoRoot: string): string | undefined {
77
+ const gitDir = findGitDir(repoRoot);
78
+ if (!gitDir) return undefined;
79
+ return originUrlFromConfig(join(gitDir, "config")) ?? originUrlFromConfig(join(commonGitDir(gitDir) ?? gitDir, "config"));
80
+ }
81
+
82
+ function findGitDir(start: string): string | undefined {
83
+ let dir = resolve(start);
84
+ while (true) {
85
+ const marker = join(dir, ".git");
86
+ try {
87
+ const stat = statSync(marker);
88
+ if (stat.isDirectory()) return marker;
89
+ if (stat.isFile()) return gitDirFromFile(marker, dir);
90
+ } catch {}
91
+ const parent = dirname(dir);
92
+ if (parent === dir) return undefined;
93
+ dir = parent;
94
+ }
95
+ }
96
+
97
+ function gitDirFromFile(marker: string, worktreeRoot: string): string | undefined {
98
+ try {
99
+ const content = readFileSync(marker, "utf8").trim();
100
+ const match = content.match(/^gitdir:\s*(.+)$/i);
101
+ if (!match?.[1]) return undefined;
102
+ return isAbsolute(match[1]) ? match[1] : resolve(worktreeRoot, match[1]);
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ function commonGitDir(gitDir: string): string | undefined {
109
+ const commonDirFile = join(gitDir, "commondir");
110
+ if (!existsSync(commonDirFile)) return undefined;
111
+ try {
112
+ const common = readFileSync(commonDirFile, "utf8").trim();
113
+ if (!common) return undefined;
114
+ return isAbsolute(common) ? common : resolve(gitDir, common);
115
+ } catch {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ function originUrlFromConfig(configPath: string): string | undefined {
121
+ let inOrigin = false;
122
+ try {
123
+ for (const line of readFileSync(configPath, "utf8").split(/\r?\n/)) {
124
+ const section = line.match(/^\s*\[(.+)]\s*$/);
125
+ if (section) {
126
+ inOrigin = /^remote\s+"origin"$/.test(section[1]!.trim());
127
+ continue;
128
+ }
129
+ if (!inOrigin) continue;
130
+ const url = line.match(/^\s*url\s*=\s*(.+?)\s*$/);
131
+ if (url?.[1]) return url[1].trim() || undefined;
132
+ }
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ return undefined;
137
+ }
@@ -13,7 +13,7 @@ import { defaultIssueRepo } from "../config";
13
13
  // `repoFromGitRemote` / `parseOwnerRepoFromRemote` live in the shared ./git-remote module
14
14
  // (#533) so project auto-seed reuses the exact same logic. Re-exported so this module's
15
15
  // existing importers and tests keep their entry point.
16
- import { parseOwnerRepoFromRemote, repoFromGitRemote, __resetRemoteRepoCacheForTests } from "./git-remote";
16
+ import { parseOwnerRepoFromRemote, repoFromGitRemote, repoFromGitRemoteAsync, __resetRemoteRepoCacheForTests } from "./git-remote";
17
17
  export { parseOwnerRepoFromRemote, repoFromGitRemote, __resetRemoteRepoCacheForTests };
18
18
  import type { IssueTrackerAdapter, IssueTrackerIssueSummary } from "../issue-tracker";
19
19
  import { recordAndEmitIssueActivity } from "./issue-activity";
@@ -90,8 +90,8 @@ function logFailure(opts: IssueLifecycleOptions, message: string): void {
90
90
  * from its own git remote so a single relay serving many repos closes the RIGHT repo's issue;
91
91
  * env (`AGENT_RELAY_DEFAULT_ISSUE_REPO`/`GITHUB_REPOSITORY`) is an override/fallback.
92
92
  */
93
- function landDefaultRepo(workspace: LifecycleWorkspace): string | undefined {
94
- return repoFromGitRemote(workspace.repoRoot) ?? defaultIssueRepo();
93
+ async function landDefaultRepo(workspace: LifecycleWorkspace): Promise<string | undefined> {
94
+ return (await repoFromGitRemoteAsync(workspace.repoRoot)) ?? defaultIssueRepo();
95
95
  }
96
96
 
97
97
  function cleanText(value: string | undefined): string | undefined {
@@ -197,10 +197,10 @@ export function parseBacklinkIssueRefs(text: string | undefined, fallbackDefault
197
197
  return issueRefMap(refs);
198
198
  }
199
199
 
200
- function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source: LandIssueRefSource; refs: IssueRef[] } {
200
+ async function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): Promise<{ source: LandIssueRefSource; refs: IssueRef[] }> {
201
201
  const associated = associatedIssueRefs(input.workspace);
202
202
  if (associated.length > 0) return { source: "association", refs: associated };
203
- const fallbackDefaultRepo = landDefaultRepo(input.workspace);
203
+ const fallbackDefaultRepo = await landDefaultRepo(input.workspace);
204
204
  const subjects = landedSubjectTexts(input);
205
205
  const keyworded = issueRefMap(subjects.flatMap((subject) => parseClosingIssueRefs(subject, fallbackDefaultRepo)));
206
206
  if (keyworded.length > 0) return { source: "commit", refs: keyworded };
@@ -399,7 +399,7 @@ export async function handleLandedIssueLifecycle(input: LandedIssueLifecycleInpu
399
399
  skipped: string[];
400
400
  }> {
401
401
  if ((input.workspace.baseRef ?? "main") !== "main") return { source: "commit", closed: [], reopened: [], skipped: ["non-default-branch"] };
402
- const resolved = resolveIssueRefsForLand(input);
402
+ const resolved = await resolveIssueRefsForLand(input);
403
403
  const closed: CloseIssueLifecycleResult[] = [];
404
404
  for (const ref of resolved.refs) {
405
405
  closed.push(await closeIssueRefForLand(ref, resolved.source, input, opts));
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { addProjectRoot, ensureProject, resolveProjectForCwd, rootsForProject, setProjectIssueRepo } from "../db";
4
4
  import { ingestClaudeMd } from "../project-ingest";
5
- import { remoteUrlFromGitRemote, repoFromGitRemote } from "./git-remote";
5
+ import { remoteUrlFromGitRemote, remoteUrlFromGitRemoteAsync, repoFromGitRemote, repoFromGitRemoteAsync } from "./git-remote";
6
6
  import type { Project, ProjectRoot } from "../types";
7
7
 
8
8
  interface EnsureProjectRootInput {
@@ -56,6 +56,26 @@ export function ensureProjectRoot(input: EnsureProjectRootInput): EnsureProjectR
56
56
  return { project, roots };
57
57
  }
58
58
 
59
+ export async function ensureProjectRootAsync(input: EnsureProjectRootInput): Promise<EnsureProjectRootResult> {
60
+ const project = ensureProject({
61
+ slug: input.slug,
62
+ ...(input.title !== undefined ? { title: input.title } : {}),
63
+ ...(input.description !== undefined ? { description: input.description } : {}),
64
+ ...(input.tenantId !== undefined ? { tenantId: input.tenantId } : {}),
65
+ ...(input.issueRepo !== undefined ? { issueRepo: input.issueRepo } : {}),
66
+ });
67
+ const remoteUrl = input.remoteUrl ?? await remoteUrlFromGitRemoteAsync(input.rootPath);
68
+ const roots = addProjectRoot(project.id, input.rootPath, { remoteUrl, ref: input.ref });
69
+ if (input.issueRepo === undefined && !project.issueRepo) {
70
+ const derived = await repoFromGitRemoteAsync(input.rootPath);
71
+ if (derived) {
72
+ const updated = setProjectIssueRepo(project.id, derived, { onlyIfNull: true });
73
+ if (updated) return { project: updated, roots };
74
+ }
75
+ }
76
+ return { project, roots };
77
+ }
78
+
59
79
  export function ingestProjectClaudeMd(projectId: string, authorId: string): ClaudeMdIngestResult {
60
80
  return ingestClaudeMd(projectId, authorId);
61
81
  }
@@ -112,3 +132,29 @@ export function autoSeedProjectForCwd(cwd: string, authorId: string): AutoSeedPr
112
132
  return { project, repoRoot, roots, ingestError: err instanceof Error ? err : new Error(String(err)) };
113
133
  }
114
134
  }
135
+
136
+ export async function autoSeedProjectForCwdAsync(cwd: string, authorId: string): Promise<AutoSeedProjectResult | null> {
137
+ const existing = resolveProjectForCwd(cwd);
138
+ if (existing) {
139
+ const repoRoot = findGitRepoRoot(cwd) ?? cwd;
140
+ let project = existing;
141
+ if (!existing.issueRepo) {
142
+ const derived = await repoFromGitRemoteAsync(repoRoot);
143
+ const updated = derived ? setProjectIssueRepo(existing.id, derived, { onlyIfNull: true }) : null;
144
+ if (updated) project = updated;
145
+ }
146
+ return { project, repoRoot, roots: rootsForProject(existing.id) };
147
+ }
148
+
149
+ const repoRoot = findGitRepoRoot(cwd);
150
+ if (!repoRoot) return null;
151
+ const slug = basename(repoRoot).trim();
152
+ if (!slug) return null;
153
+
154
+ const { project, roots } = await ensureProjectRootAsync({ slug, rootPath: repoRoot });
155
+ try {
156
+ return { project, repoRoot, roots, ingest: ingestProjectClaudeMd(project.id, authorId) };
157
+ } catch (err) {
158
+ return { project, repoRoot, roots, ingestError: err instanceof Error ? err : new Error(String(err)) };
159
+ }
160
+ }
@@ -30,7 +30,6 @@ import {
30
30
  promoteLineageCapturedResponse,
31
31
  sendMessageWithResult,
32
32
  channelObligationAnsweredBy,
33
- spawnObligationAnsweredBy,
34
33
  } from "../db";
35
34
  import { planSend, type DeliveryReceipt } from "../agent-ref";
36
35
  import { emitMessageQueued, emitNewMessage } from "../sse";
@@ -178,12 +177,11 @@ export function sendMessageService(
178
177
  ): SendMessageResult {
179
178
  const receipt = validateSendMessageAuthorization(input, ctx, opts);
180
179
  // Reply-obligation linking: some real answers are ordinary sends without replyTo/thread linkage.
181
- // Channel bridge replies and spawn report-backs both have a single unambiguous open obligation case;
180
+ // Channel bridge replies have a single unambiguous open obligation case;
182
181
  // link those at send time so the obligation clears with one message and the Stop hook does not nag
183
182
  // the agent into a redundant `/reply`.
184
183
  if (!input.replyTo && !isMechanicalMessageKind(input.kind)) {
185
- const answered = channelObligationAnsweredBy(input.from, input.to)
186
- ?? spawnObligationAnsweredBy(input.from, input.to);
184
+ const answered = channelObligationAnsweredBy(input.from, input.to);
187
185
  if (answered !== null) input.replyTo = answered;
188
186
  }
189
187
  const suppressedParent = suppressResultsOnlyAck(input);
@@ -39,7 +39,7 @@ import type { ProjectMcpPolicy } from "../project-mcp";
39
39
  import type { ProjectSettingsPolicy } from "../project-settings";
40
40
  import type { ProjectSkillsPolicy } from "../project-skills";
41
41
  import { applyDefaultSharedMcp, type SharedMcpSpawnOverride } from "../shared-mcp";
42
- import { autoSeedProjectForCwd } from "./project";
42
+ import { autoSeedProjectForCwdAsync } from "./project";
43
43
  import { hydrateTaskContext, projectTaskHydration } from "./task-context-hydration";
44
44
  import { createTaskEntity, recordTaskEntityDispatchDecision } from "./task-entity";
45
45
  import { autoSubscribeTaskCoordinator } from "./task-semantic-events";
@@ -414,7 +414,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
414
414
  // stays filterable by project.
415
415
  let callerProject = resolveProjectForCwd(resolvedCwd);
416
416
  try {
417
- const seeded = autoSeedProjectForCwd(resolvedCwd, requestedBy);
417
+ const seeded = await autoSeedProjectForCwdAsync(resolvedCwd, requestedBy);
418
418
  callerProject = seeded?.project ?? callerProject;
419
419
  if (seeded?.ingestError) {
420
420
  console.warn(`project auto-seed failed to ingest CLAUDE.md for ${resolvedCwd}: ${seeded.ingestError.message}`);
@@ -38,6 +38,7 @@ import { routeNotification } from "../notification-router";
38
38
  import { resolveCoordinatorAgentId } from "../steward-identity";
39
39
  import { isAgentExitDoneWhen, stageSatisfiesDoneWhen } from "./task-done-when";
40
40
  import { emitTaskSemanticNotification, type TaskSemanticTransition } from "./task-semantic-events";
41
+ import { runGit } from "./git-process";
41
42
  import type { IssueRef, TaskDeliverableStage, TaskGateState, WorkspaceRecord } from "../types";
42
43
  import type { IssueTrackerAdapter } from "../issue-tracker";
43
44
 
@@ -100,20 +101,16 @@ export function __setDriveTaskOnLandForTests(resolver: DriveTaskResolver | undef
100
101
  driveTaskOnLandForTests = resolver;
101
102
  }
102
103
 
103
- function landedDiffSummary(input: LandedTaskLifecycleInput, log: LogFn): string | undefined {
104
+ async function landedDiffSummary(input: LandedTaskLifecycleInput, log: LogFn): Promise<string | undefined> {
104
105
  if (diffSummaryResolverForTests) return diffSummaryResolverForTests(input);
105
106
  const repoRoot = input.workspace.repoRoot?.trim();
106
107
  const baseSha = input.workspace.baseSha?.trim();
107
108
  const tipSha = input.mergedSha?.trim();
108
109
  if (!repoRoot || !baseSha || !tipSha || baseSha === tipSha) return undefined;
109
110
  try {
110
- const proc = Bun.spawnSync(["git", "-C", repoRoot, "diff", "--shortstat", `${baseSha}..${tipSha}`], {
111
- stdin: "ignore",
112
- stdout: "pipe",
113
- stderr: "pipe",
114
- });
115
- if (proc.exitCode !== 0) return undefined;
116
- const out = Buffer.from(proc.stdout).toString("utf8").trim();
111
+ const { stdout, exitCode } = await runGit(["-C", repoRoot, "diff", "--shortstat", `${baseSha}..${tipSha}`]);
112
+ if (exitCode !== 0) return undefined;
113
+ const out = stdout.trim();
117
114
  return out || undefined;
118
115
  } catch (error) {
119
116
  log(`task lifecycle diff summary failed for ${input.workspace.id}: ${errMessage(error)}`);
@@ -454,7 +451,7 @@ export async function handleLandedTaskLifecycle(
454
451
  const taskIds = boundTaskIdsForWorkspace(input.workspace);
455
452
  if (taskIds.length === 0) return { handled: false, reason: "no-bound-task", tasks: [] };
456
453
 
457
- const diffSummary = landedDiffSummary(input, log);
454
+ const diffSummary = await landedDiffSummary(input, log);
458
455
  const outcomes: TaskLandOutcome[] = [];
459
456
  for (const taskId of taskIds) {
460
457
  try {
@@ -238,10 +238,10 @@ const DISPLAY_PREFS_SCHEMA: JSONSchema = {
238
238
  // sticky-bottom intent follow them across devices instead of dying in per-browser module state.
239
239
  // The value is a MAP keyed by conversation/agent id — one user-settings row holds every
240
240
  // conversation's anchor, so this is ONE descriptor (not a per-agent descriptor explosion).
241
- // Each entry is a LOGICAL anchor, never a pixel scrollTop: `topKey` is the timeline entry key
242
- // (`msg-<id>` — a server-stable id), `offset` the within-row pixel offset, `stick` the
243
- // follow-the-bottom intent. `maxProperties` bounds unbounded conversation growth; the dashboard
244
- // store evicts oldest-touched before it can be hit.
241
+ // Each entry is a LOGICAL anchor, never a pixel scrollTop: `anchorMessageId` is the durable
242
+ // message-id anchor, `topKey` is the compatibility timeline entry key, `offset` the within-row
243
+ // pixel offset, `stick` the follow-the-bottom intent. `maxProperties` bounds unbounded
244
+ // conversation growth; the dashboard store evicts oldest-touched before it can be hit.
245
245
  const CHAT_SCROLL_SCHEMA: JSONSchema = {
246
246
  type: "object",
247
247
  maxProperties: 200,
@@ -252,6 +252,8 @@ const CHAT_SCROLL_SCHEMA: JSONSchema = {
252
252
  properties: {
253
253
  stick: { type: "boolean" },
254
254
  topKey: { type: ["string", "null"], maxLength: 200 },
255
+ anchorMessageId: { type: "number", minimum: 0, maximum: 10_000_000_000 },
256
+ anchorEndMessageId: { type: "number", minimum: 0, maximum: 10_000_000_000 },
255
257
  offset: { type: "number", minimum: 0, maximum: 10_000_000 },
256
258
  },
257
259
  },
@@ -277,7 +277,7 @@ function spawnQuotaWindow(
277
277
  ? Math.max(0, burnWindow.earlyWallMs) / timeToResetMs
278
278
  : null;
279
279
  const weight = burnWindow.role === "7d" ? 3 : 1;
280
- const hardBackstop = providerCfg?.hardBackstopUtilization ?? 0.99;
280
+ const hardBackstop = providerCfg?.hardBackstopUtilization;
281
281
  // #760 — floor default is now 0 (off). The projection/pace gate (earlyFraction≈0 when the
282
282
  // recency-weighted rate projects no early wall) replaces the blunt utilization floor, which
283
283
  // used to hide a legitimate low-util-high-burst alarm. Operators may still opt back in.
@@ -289,7 +289,7 @@ function spawnQuotaWindow(
289
289
  // over-pace penalty and ≥ preferSlackAt projected headroom at reset.
290
290
  const target = providerCfg?.targetUtilization ?? DEFAULT_QUOTA_TARGET_UTILIZATION;
291
291
  const preferSlackAt = providerCfg?.preferSlackAt ?? DEFAULT_QUOTA_PREFER_SLACK_AT;
292
- const blockedNow = quotaWindow.utilization >= hardBackstop;
292
+ const blockedNow = hardBackstop === 0 ? false : quotaWindow.utilization >= (hardBackstop ?? 0.99);
293
293
  let burnRatio = burnWindow.burnRatio;
294
294
  let projectedUtilAtReset = burnWindow.projectedUtilAtReset;
295
295
  let slack = burnWindow.slack;