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.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-Cqi3XsZ7.js +2 -0
- package/public/assets/{MessageBubble-jpshgh3K.js.map → MessageBubble-Cqi3XsZ7.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js → PlanGraphPanel-D6Um05oW.js} +2 -2
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js.map → PlanGraphPanel-D6Um05oW.js.map} +1 -1
- package/public/assets/{activity-DLTFF8Dz.js → activity-B0LsQHEz.js} +2 -2
- package/public/assets/{activity-DLTFF8Dz.js.map → activity-B0LsQHEz.js.map} +1 -1
- package/public/assets/{agent-profiles-BcCLIrON.js → agent-profiles-O--pe5Ic.js} +2 -2
- package/public/assets/{agent-profiles-BcCLIrON.js.map → agent-profiles-O--pe5Ic.js.map} +1 -1
- package/public/assets/{agent-type-icon-BEz393ra.js → agent-type-icon-CeEoMouY.js} +2 -2
- package/public/assets/{agent-type-icon-BEz393ra.js.map → agent-type-icon-CeEoMouY.js.map} +1 -1
- package/public/assets/agents-Ii6XlVwR.js +2 -0
- package/public/assets/{agents-QemvuxKO.js.map → agents-Ii6XlVwR.js.map} +1 -1
- package/public/assets/{analytics-DMLoMwta.js → analytics-BCzdYWRT.js} +2 -2
- package/public/assets/{analytics-DMLoMwta.js.map → analytics-BCzdYWRT.js.map} +1 -1
- package/public/assets/{automation-DACSOffp.js → automation-Crr3Xzui.js} +2 -2
- package/public/assets/{automation-DACSOffp.js.map → automation-Crr3Xzui.js.map} +1 -1
- package/public/assets/{badge-BhUZ1cAb.js → badge-DCIT3Irj.js} +2 -2
- package/public/assets/{badge-BhUZ1cAb.js.map → badge-DCIT3Irj.js.map} +1 -1
- package/public/assets/{branch-state-badge-BYE2U_y4.js → branch-state-badge-Bhw2rloy.js} +2 -2
- package/public/assets/{branch-state-badge-BYE2U_y4.js.map → branch-state-badge-Bhw2rloy.js.map} +1 -1
- package/public/assets/{button-C-V4oij1.js → button-CzdTkRIf.js} +2 -2
- package/public/assets/{button-C-V4oij1.js.map → button-CzdTkRIf.js.map} +1 -1
- package/public/assets/{card-Br1JLjTH.js → card-C3BGuVC6.js} +2 -2
- package/public/assets/{card-Br1JLjTH.js.map → card-C3BGuVC6.js.map} +1 -1
- package/public/assets/{channels-3SlDQL3j.js → channels-CKs9rvsn.js} +2 -2
- package/public/assets/{channels-3SlDQL3j.js.map → channels-CKs9rvsn.js.map} +1 -1
- package/public/assets/chat-BCwEUj5R.js +2 -0
- package/public/assets/chat-BCwEUj5R.js.map +1 -0
- package/public/assets/{connectors-CrEW591Q.js → connectors-D_kQR_vw.js} +2 -2
- package/public/assets/{connectors-CrEW591Q.js.map → connectors-D_kQR_vw.js.map} +1 -1
- package/public/assets/{copy-button-BStzXgtd.js → copy-button-B3o1jpLu.js} +2 -2
- package/public/assets/{copy-button-BStzXgtd.js.map → copy-button-B3o1jpLu.js.map} +1 -1
- package/public/assets/{dist-ChDX_t12.js → dist-B-T7lg0a.js} +2 -2
- package/public/assets/{dist-ChDX_t12.js.map → dist-B-T7lg0a.js.map} +1 -1
- package/public/assets/{dist-DV83j9tm.js → dist-B3e4B9pg.js} +2 -2
- package/public/assets/{dist-DV83j9tm.js.map → dist-B3e4B9pg.js.map} +1 -1
- package/public/assets/{dist-y03IwOrd.js → dist-B6woQ9Tb.js} +2 -2
- package/public/assets/{dist-y03IwOrd.js.map → dist-B6woQ9Tb.js.map} +1 -1
- package/public/assets/{dist-CQ0Z-TDi.js → dist-B9G83DXv.js} +2 -2
- package/public/assets/{dist-CQ0Z-TDi.js.map → dist-B9G83DXv.js.map} +1 -1
- package/public/assets/{dist-Cs8X0P1d.js → dist-BIjlN-xB.js} +2 -2
- package/public/assets/{dist-Cs8X0P1d.js.map → dist-BIjlN-xB.js.map} +1 -1
- package/public/assets/{dist-DyNJNwpi.js → dist-BInckSJC.js} +2 -2
- package/public/assets/{dist-DyNJNwpi.js.map → dist-BInckSJC.js.map} +1 -1
- package/public/assets/{dist-CmFeEXMe.js → dist-BlzVGRhh.js} +2 -2
- package/public/assets/{dist-CmFeEXMe.js.map → dist-BlzVGRhh.js.map} +1 -1
- package/public/assets/{dist-CDZZxxJq.js → dist-C7aTlosb.js} +2 -2
- package/public/assets/{dist-CDZZxxJq.js.map → dist-C7aTlosb.js.map} +1 -1
- package/public/assets/{dist-D2jvo5oA.js → dist-DGfp5dLz.js} +2 -2
- package/public/assets/{dist-D2jvo5oA.js.map → dist-DGfp5dLz.js.map} +1 -1
- package/public/assets/{dist-mm7c7nBt.js → dist-DT-OV6sQ.js} +2 -2
- package/public/assets/{dist-mm7c7nBt.js.map → dist-DT-OV6sQ.js.map} +1 -1
- package/public/assets/{dist-CBMWGhWc.js → dist-OUPTEP6q.js} +2 -2
- package/public/assets/{dist-CBMWGhWc.js.map → dist-OUPTEP6q.js.map} +1 -1
- package/public/assets/{es2015-VnZ2kVIz.js → es2015-CnKXYKpf.js} +2 -2
- package/public/assets/{es2015-VnZ2kVIz.js.map → es2015-CnKXYKpf.js.map} +1 -1
- package/public/assets/formatted-body-impl-CSRGuMcM.js +3 -0
- package/public/assets/formatted-body-impl-CSRGuMcM.js.map +1 -0
- package/public/assets/formatted-body-q_qzt1L-.js +3 -0
- package/public/assets/{formatted-body-Dg9R3RD7.js.map → formatted-body-q_qzt1L-.js.map} +1 -1
- package/public/assets/{index-D_fNJnsI.js → index-Ceiwd02X.js} +10 -10
- package/public/assets/index-Ceiwd02X.js.map +1 -0
- package/public/assets/{input-CtLT7WXi.js → input-B6Q0NWCS.js} +2 -2
- package/public/assets/{input-CtLT7WXi.js.map → input-B6Q0NWCS.js.map} +1 -1
- package/public/assets/{insights-D48n2ojF.js → insights-DUhXqZTZ.js} +2 -2
- package/public/assets/{insights-D48n2ojF.js.map → insights-DUhXqZTZ.js.map} +1 -1
- package/public/assets/{integrations-B-bdliwU.js → integrations-lbg5CjtE.js} +2 -2
- package/public/assets/{integrations-B-bdliwU.js.map → integrations-lbg5CjtE.js.map} +1 -1
- package/public/assets/{lib-ByLQqcD1.js → lib-BHsWPMKB.js} +2 -2
- package/public/assets/{lib-ByLQqcD1.js.map → lib-BHsWPMKB.js.map} +1 -1
- package/public/assets/lucide-react-DuhlF2Sj.js +10 -0
- package/public/assets/lucide-react-DuhlF2Sj.js.map +1 -0
- package/public/assets/{maintenance-LQ_U3Nqy.js → maintenance-CQgneSVf.js} +2 -2
- package/public/assets/{maintenance-LQ_U3Nqy.js.map → maintenance-CQgneSVf.js.map} +1 -1
- package/public/assets/{managed-agents-Cfh8FvY8.js → managed-agents-DsnYf6fm.js} +4 -4
- package/public/assets/{managed-agents-Cfh8FvY8.js.map → managed-agents-DsnYf6fm.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js → markdown-preview-impl-C1ZtGJp6.js} +2 -2
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js.map → markdown-preview-impl-C1ZtGJp6.js.map} +1 -1
- package/public/assets/{memory-Dr1iYKkC.js → memory-SSLUTOI2.js} +2 -2
- package/public/assets/{memory-Dr1iYKkC.js.map → memory-SSLUTOI2.js.map} +1 -1
- package/public/assets/messages-BY26OP4K.js +2 -0
- package/public/assets/{messages-DRKulS-2.js.map → messages-BY26OP4K.js.map} +1 -1
- package/public/assets/{orchestrators-BguyeViG.js → orchestrators-OY7yPzVI.js} +2 -2
- package/public/assets/{orchestrators-BguyeViG.js.map → orchestrators-OY7yPzVI.js.map} +1 -1
- package/public/assets/overview-Dj0u1tPG.js +2 -0
- package/public/assets/{overview-Dbq2lr7Y.js.map → overview-Dj0u1tPG.js.map} +1 -1
- package/public/assets/{pairs-DD7i2scF.js → pairs-CrUR50ou.js} +2 -2
- package/public/assets/{pairs-DD7i2scF.js.map → pairs-CrUR50ou.js.map} +1 -1
- package/public/assets/{projects-DEn85LQb.js → projects-B_AtxHtH.js} +2 -2
- package/public/assets/{projects-DEn85LQb.js.map → projects-B_AtxHtH.js.map} +1 -1
- package/public/assets/{prompt-settings-DwgrPKY3.js → prompt-settings-CPtYSban.js} +2 -2
- package/public/assets/{prompt-settings-DwgrPKY3.js.map → prompt-settings-CPtYSban.js.map} +1 -1
- package/public/assets/{react-dom-cLkVFBsS.js → react-dom-DWBmcawP.js} +2 -2
- package/public/assets/{react-dom-cLkVFBsS.js.map → react-dom-DWBmcawP.js.map} +1 -1
- package/public/assets/{registry-DCR3xFpe.js → registry-BgUALWn5.js} +2 -2
- package/public/assets/{registry-DCR3xFpe.js.map → registry-BgUALWn5.js.map} +1 -1
- package/public/assets/security-5lHfGk7g.js +3 -0
- package/public/assets/security-5lHfGk7g.js.map +1 -0
- package/public/assets/{select-CpyCRBK3.js → select-Cgx4QSLj.js} +2 -2
- package/public/assets/{select-CpyCRBK3.js.map → select-Cgx4QSLj.js.map} +1 -1
- package/public/assets/settings-DFGgxyun.js +6 -0
- package/public/assets/{settings-DYNFGC2-.js.map → settings-DFGgxyun.js.map} +1 -1
- package/public/assets/store-BM4zM89B.js +9 -0
- package/public/assets/store-BM4zM89B.js.map +1 -0
- package/public/assets/{task-chip-kxk4eNm1.js → task-chip-DXfvXBDx.js} +2 -2
- package/public/assets/{task-chip-kxk4eNm1.js.map → task-chip-DXfvXBDx.js.map} +1 -1
- package/public/assets/{tasks-rdy9Nsbf.js → tasks-BvH5rluu.js} +2 -2
- package/public/assets/{tasks-rdy9Nsbf.js.map → tasks-BvH5rluu.js.map} +1 -1
- package/public/assets/{teams-DHloBagJ.js → teams-VZOIOdoE.js} +2 -2
- package/public/assets/{teams-DHloBagJ.js.map → teams-VZOIOdoE.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js → terminal-viewer-impl-DYY3Wmoe.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js.map → terminal-viewer-impl-DYY3Wmoe.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{use-keyboard-viewport-B06XtRUZ.js → use-keyboard-viewport-CP109w9y.js} +2 -2
- package/public/assets/{use-keyboard-viewport-B06XtRUZ.js.map → use-keyboard-viewport-CP109w9y.js.map} +1 -1
- package/public/assets/{user-avatar-DuwCFrER.js → user-avatar-Dw6mzWhv.js} +2 -2
- package/public/assets/{user-avatar-DuwCFrER.js.map → user-avatar-Dw6mzWhv.js.map} +1 -1
- package/public/assets/{work-queue-CShT7nTC.js → work-queue-B6Xk7QNe.js} +2 -2
- package/public/assets/{work-queue-CShT7nTC.js.map → work-queue-B6Xk7QNe.js.map} +1 -1
- package/public/assets/{workspaces-DjQ6EdrZ.js → workspaces-CPIzAHAe.js} +2 -2
- package/public/assets/{workspaces-DjQ6EdrZ.js.map → workspaces-CPIzAHAe.js.map} +1 -1
- package/public/index.html +23 -23
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/automations/reconcile.ts +69 -4
- package/src/branch-landed.ts +18 -11
- package/src/bus.ts +25 -18
- package/src/db/connection.ts +53 -1
- package/src/db/maintenance-worker.ts +37 -0
- package/src/db/message-reads.ts +86 -45
- package/src/db/migrations.ts +1 -0
- package/src/db/provider-quotas.ts +6 -7
- package/src/db/schema.ts +2 -1
- package/src/maintenance/jobs.ts +19 -13
- package/src/maintenance/scheduler.ts +8 -3
- package/src/maintenance/types.ts +5 -1
- package/src/mcp/index.ts +5 -5
- package/src/mcp/tools-workspace.ts +8 -3
- package/src/routes/orchestrator-proxy.ts +7 -6
- package/src/routes/sse.ts +2 -1
- package/src/routes/tasks.ts +29 -0
- package/src/routes/workspaces.ts +9 -2
- package/src/runtime-tokens.ts +4 -0
- package/src/security.ts +31 -7
- package/src/server.ts +40 -18
- package/src/services/git-process.ts +61 -0
- package/src/services/git-remote.ts +87 -7
- package/src/services/issue-lifecycle.ts +6 -6
- package/src/services/project.ts +47 -1
- package/src/services/send-message.ts +2 -4
- package/src/services/spawn-agent.ts +2 -2
- package/src/services/task-lifecycle.ts +6 -9
- package/src/settings/registry.ts +6 -4
- package/src/spawn-targets.ts +2 -2
- package/src/sse.ts +131 -46
- package/src/steward.ts +10 -6
- package/src/terminal-authz.ts +50 -0
- package/src/token-db.ts +3 -3
- package/src/validation.ts +1 -1
- package/src/workspace-actions.ts +46 -4
- package/src/workspace-auto-merge.ts +120 -13
- package/src/workspace-orphans.ts +1 -1
- package/src/workspace-probe.ts +19 -10
- package/public/assets/MessageBubble-jpshgh3K.js +0 -2
- package/public/assets/agents-QemvuxKO.js +0 -2
- package/public/assets/chat-xKXCafN9.js +0 -2
- package/public/assets/chat-xKXCafN9.js.map +0 -1
- package/public/assets/formatted-body-Dg9R3RD7.js +0 -3
- package/public/assets/formatted-body-impl-DlasTD8X.js +0 -3
- package/public/assets/formatted-body-impl-DlasTD8X.js.map +0 -1
- package/public/assets/index-D_fNJnsI.js.map +0 -1
- package/public/assets/lucide-react-Dc3ZwCBi.js +0 -9
- package/public/assets/lucide-react-Dc3ZwCBi.js.map +0 -1
- package/public/assets/messages-DRKulS-2.js +0 -2
- package/public/assets/overview-Dbq2lr7Y.js +0 -2
- package/public/assets/security-BH8kTdA-.js +0 -3
- package/public/assets/security-BH8kTdA-.js.map +0 -1
- package/public/assets/settings-DYNFGC2-.js +0 -6
- package/public/assets/store-BuuJUn70.js +0 -9
- 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
|
|
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
|
|
28
|
-
|
|
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
|
|
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));
|
package/src/services/project.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 =
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
package/src/settings/registry.ts
CHANGED
|
@@ -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: `
|
|
242
|
-
//
|
|
243
|
-
// follow-the-bottom intent. `maxProperties` bounds unbounded
|
|
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
|
},
|
package/src/spawn-targets.ts
CHANGED
|
@@ -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
|
|
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;
|