agent-relay-server 0.57.2 → 0.59.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.
- package/docs/openapi.json +151 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-CHKzD4RY.js → MessageBubble-BrEKymoV.js} +2 -2
- package/public/assets/{MessageBubble-CHKzD4RY.js.map → MessageBubble-BrEKymoV.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BFVTJa0q.js → PlanGraphPanel-CXix1C-H.js} +2 -2
- package/public/assets/{PlanGraphPanel-BFVTJa0q.js.map → PlanGraphPanel-CXix1C-H.js.map} +1 -1
- package/public/assets/{activity-M-dFLU_O.js → activity-CRafQ1Je.js} +2 -2
- package/public/assets/{activity-M-dFLU_O.js.map → activity-CRafQ1Je.js.map} +1 -1
- package/public/assets/{agent-profiles-CvHSoyCX.js → agent-profiles-DW4o5-ND.js} +2 -2
- package/public/assets/{agent-profiles-CvHSoyCX.js.map → agent-profiles-DW4o5-ND.js.map} +1 -1
- package/public/assets/agents-Cbegv0Hm.js +2 -0
- package/public/assets/{agents-DZBM5dp_.js.map → agents-Cbegv0Hm.js.map} +1 -1
- package/public/assets/analytics-DX6T7JX3.js +910 -0
- package/public/assets/{analytics-bvjb3Urs.js.map → analytics-DX6T7JX3.js.map} +1 -1
- package/public/assets/{automation-CDSn9Uq-.js → automation-CzSBv4gg.js} +2 -2
- package/public/assets/{automation-CDSn9Uq-.js.map → automation-CzSBv4gg.js.map} +1 -1
- package/public/assets/{badge-Dj6PREuI.js → badge-Cy6qO_Jz.js} +2 -2
- package/public/assets/{badge-Dj6PREuI.js.map → badge-Cy6qO_Jz.js.map} +1 -1
- package/public/assets/{branch-state-badge-CdYg2HYc.js → branch-state-badge-CRA50r81.js} +2 -2
- package/public/assets/{branch-state-badge-CdYg2HYc.js.map → branch-state-badge-CRA50r81.js.map} +1 -1
- package/public/assets/{button-Cqpufqam.js → button-73wpRw65.js} +2 -2
- package/public/assets/{button-Cqpufqam.js.map → button-73wpRw65.js.map} +1 -1
- package/public/assets/{card-OmDF73CE.js → card-BU6PvVHv.js} +2 -2
- package/public/assets/{card-OmDF73CE.js.map → card-BU6PvVHv.js.map} +1 -1
- package/public/assets/{channels-BkxrdOFZ.js → channels-B1MQCPCT.js} +2 -2
- package/public/assets/{channels-BkxrdOFZ.js.map → channels-B1MQCPCT.js.map} +1 -1
- package/public/assets/chat-BqIaFYGJ.js +2 -0
- package/public/assets/{chat-YzX1z76l.js.map → chat-BqIaFYGJ.js.map} +1 -1
- package/public/assets/{connectors-Bg3rHgl_.js → connectors-C9aFJIrh.js} +2 -2
- package/public/assets/{connectors-Bg3rHgl_.js.map → connectors-C9aFJIrh.js.map} +1 -1
- package/public/assets/{copy-button-DI7b8A7_.js → copy-button-HRopUwui.js} +2 -2
- package/public/assets/{copy-button-DI7b8A7_.js.map → copy-button-HRopUwui.js.map} +1 -1
- package/public/assets/display-CketawEF.js.map +1 -1
- package/public/assets/{dist-CNjnTZEI.js → dist-CC5f7oYw.js} +2 -2
- package/public/assets/{dist-CNjnTZEI.js.map → dist-CC5f7oYw.js.map} +1 -1
- package/public/assets/{dist-DkeTrbTA.js → dist-CLHMeXYi.js} +2 -2
- package/public/assets/{dist-DkeTrbTA.js.map → dist-CLHMeXYi.js.map} +1 -1
- package/public/assets/{dist-C5t_OxI0.js → dist-Ci1MM51P.js} +2 -2
- package/public/assets/{dist-C5t_OxI0.js.map → dist-Ci1MM51P.js.map} +1 -1
- package/public/assets/{dist-CV3sgFiv.js → dist-Cko2KPW8.js} +2 -2
- package/public/assets/{dist-CV3sgFiv.js.map → dist-Cko2KPW8.js.map} +1 -1
- package/public/assets/dist-D41tzJYg.js +2 -0
- package/public/assets/{dist-yYauqYKR.js.map → dist-D41tzJYg.js.map} +1 -1
- package/public/assets/{dist-CoUvwLTy.js → dist-DFLciw0h.js} +2 -2
- package/public/assets/{dist-CoUvwLTy.js.map → dist-DFLciw0h.js.map} +1 -1
- package/public/assets/dist-DUoZnlB2.js +2 -0
- package/public/assets/{dist-DmhyU0Na.js.map → dist-DUoZnlB2.js.map} +1 -1
- package/public/assets/dist-De2oguBm.js +2 -0
- package/public/assets/{dist-DxkMwkrr.js.map → dist-De2oguBm.js.map} +1 -1
- package/public/assets/{dist-1x-LE9Uk.js → dist-DmN94rc8.js} +2 -2
- package/public/assets/{dist-1x-LE9Uk.js.map → dist-DmN94rc8.js.map} +1 -1
- package/public/assets/{dist-DV2tZ2fq.js → dist-Kkpnjx_M.js} +2 -2
- package/public/assets/{dist-DV2tZ2fq.js.map → dist-Kkpnjx_M.js.map} +1 -1
- package/public/assets/{dist-Ca3ujqG_.js → dist-T7u7IZGJ.js} +2 -2
- package/public/assets/{dist-Ca3ujqG_.js.map → dist-T7u7IZGJ.js.map} +1 -1
- package/public/assets/{es2015-DcavLcQ0.js → es2015-Df08Opjx.js} +2 -2
- package/public/assets/{es2015-DcavLcQ0.js.map → es2015-Df08Opjx.js.map} +1 -1
- package/public/assets/{formatted-body-impl-D8i9qqL1.js → formatted-body-impl-DOXH7oDF.js} +2 -2
- package/public/assets/{formatted-body-impl-D8i9qqL1.js.map → formatted-body-impl-DOXH7oDF.js.map} +1 -1
- package/public/assets/index-DaNOQJci.css +2 -0
- package/public/assets/index-br_Y6Cvt.js +21 -0
- package/public/assets/{index-Ba2RDJaA.js.map → index-br_Y6Cvt.js.map} +1 -1
- package/public/assets/{input-C_dl_9fG.js → input-B2EDz-BE.js} +2 -2
- package/public/assets/{input-C_dl_9fG.js.map → input-B2EDz-BE.js.map} +1 -1
- package/public/assets/{insights-D1aANwZ2.js → insights-B1edq5lk.js} +2 -2
- package/public/assets/{insights-D1aANwZ2.js.map → insights-B1edq5lk.js.map} +1 -1
- package/public/assets/{integrations-BrAr9bap.js → integrations-Cm4fjp7_.js} +2 -2
- package/public/assets/{integrations-BrAr9bap.js.map → integrations-Cm4fjp7_.js.map} +1 -1
- package/public/assets/{lib-B3iMVsf-.js → lib-BZrjIp-o.js} +3 -3
- package/public/assets/{lib-B3iMVsf-.js.map → lib-BZrjIp-o.js.map} +1 -1
- package/public/assets/lucide-react-H_-8G79A.js +9 -0
- package/public/assets/{lucide-react-CMhBract.js.map → lucide-react-H_-8G79A.js.map} +1 -1
- package/public/assets/{maintenance-C-xspsMG.js → maintenance-D6zvw0fU.js} +2 -2
- package/public/assets/{maintenance-C-xspsMG.js.map → maintenance-D6zvw0fU.js.map} +1 -1
- package/public/assets/{managed-agents-Bf3ni0vo.js → managed-agents-CzfEznM1.js} +2 -2
- package/public/assets/{managed-agents-Bf3ni0vo.js.map → managed-agents-CzfEznM1.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DKbUBTf_.js → markdown-preview-impl-DZlCaTHQ.js} +2 -2
- package/public/assets/{markdown-preview-impl-DKbUBTf_.js.map → markdown-preview-impl-DZlCaTHQ.js.map} +1 -1
- package/public/assets/memory-CUUAzJDc.js +2 -0
- package/public/assets/{memory-DeLwJFwH.js.map → memory-CUUAzJDc.js.map} +1 -1
- package/public/assets/messages-6QZH46LG.js +2 -0
- package/public/assets/{messages-Cww6dZnM.js.map → messages-6QZH46LG.js.map} +1 -1
- package/public/assets/{orchestrators-DRjwROXw.js → orchestrators-CrcqkPuc.js} +2 -2
- package/public/assets/{orchestrators-DRjwROXw.js.map → orchestrators-CrcqkPuc.js.map} +1 -1
- package/public/assets/overview-418xXZO_.js +2 -0
- package/public/assets/{overview-DjcRomU_.js.map → overview-418xXZO_.js.map} +1 -1
- package/public/assets/pairs-CnedATgT.js +2 -0
- package/public/assets/{pairs-DT0u2fnW.js.map → pairs-CnedATgT.js.map} +1 -1
- package/public/assets/{react-dom-Dtl9-x_1.js → react-dom-WI6TjVe7.js} +2 -2
- package/public/assets/{react-dom-Dtl9-x_1.js.map → react-dom-WI6TjVe7.js.map} +1 -1
- package/public/assets/{security-Bwg3oFI8.js → security-DS13pbag.js} +2 -2
- package/public/assets/{security-Bwg3oFI8.js.map → security-DS13pbag.js.map} +1 -1
- package/public/assets/{select-Cariwv69.js → select-BOprGZDS.js} +2 -2
- package/public/assets/{select-Cariwv69.js.map → select-BOprGZDS.js.map} +1 -1
- package/public/assets/{settings-Bd6Ug3DO.js → settings-BgInn_f3.js} +4 -4
- package/public/assets/{settings-Bd6Ug3DO.js.map → settings-BgInn_f3.js.map} +1 -1
- package/public/assets/{store-BgJq_lN9.js → store-DHO_a8BD.js} +5 -5
- package/public/assets/{store-BgJq_lN9.js.map → store-DHO_a8BD.js.map} +1 -1
- package/public/assets/{tasks-DVz1zhOU.js → tasks-B-4oCPuv.js} +2 -2
- package/public/assets/{tasks-DVz1zhOU.js.map → tasks-B-4oCPuv.js.map} +1 -1
- package/public/assets/teams-sCQAs_QA.js +2 -0
- package/public/assets/{teams-TqFntFB_.js.map → teams-sCQAs_QA.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-OsJ5crlx.js → terminal-viewer-impl-BhqS2XpS.js} +3 -3
- package/public/assets/{terminal-viewer-impl-OsJ5crlx.js.map → terminal-viewer-impl-BhqS2XpS.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-BYHcL7sP.js → use-keyboard-viewport-fOL_3RHB.js} +2 -2
- package/public/assets/{use-keyboard-viewport-BYHcL7sP.js.map → use-keyboard-viewport-fOL_3RHB.js.map} +1 -1
- package/public/assets/{work-queue-B4b2jPLR.js → work-queue-BDhjlwUH.js} +2 -2
- package/public/assets/{work-queue-B4b2jPLR.js.map → work-queue-BDhjlwUH.js.map} +1 -1
- package/public/assets/workspaces-6I-C69nf.js +3 -0
- package/public/assets/{workspaces-m97LP4Eu.js.map → workspaces-6I-C69nf.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/db/index.ts +2 -0
- package/src/db/project-entries.ts +270 -0
- package/src/db/projects.ts +304 -0
- package/src/db/schema.ts +50 -0
- package/src/db/stats.ts +209 -38
- package/src/db/teams.ts +13 -2
- package/src/maintenance.ts +9 -2
- package/src/mcp-project-tools.ts +352 -0
- package/src/mcp.ts +6 -0
- package/src/project-ingest.ts +186 -0
- package/src/project-instructions.ts +176 -0
- package/src/routes/agents-spawn.ts +2 -0
- package/src/routes/commands.ts +11 -5
- package/src/routes/index.ts +5 -0
- package/src/routes/projects.ts +55 -0
- package/src/runtime-tokens.ts +6 -0
- package/src/security.ts +4 -0
- package/src/services/spawn-agent.ts +33 -2
- package/src/services/team.ts +5 -1
- package/src/token-db.ts +3 -3
- package/src/workspace-merge.ts +11 -0
- package/src/workspace-pr-completion.ts +57 -1
- package/public/assets/agents-DZBM5dp_.js +0 -2
- package/public/assets/analytics-bvjb3Urs.js +0 -910
- package/public/assets/chat-YzX1z76l.js +0 -2
- package/public/assets/dist-DmhyU0Na.js +0 -2
- package/public/assets/dist-DxkMwkrr.js +0 -2
- package/public/assets/dist-yYauqYKR.js +0 -2
- package/public/assets/index-Ba2RDJaA.js +0 -21
- package/public/assets/index-CtFXWxBJ.css +0 -2
- package/public/assets/lucide-react-CMhBract.js +0 -9
- package/public/assets/memory-DeLwJFwH.js +0 -2
- package/public/assets/messages-Cww6dZnM.js +0 -2
- package/public/assets/overview-DjcRomU_.js +0 -2
- package/public/assets/pairs-DT0u2fnW.js +0 -2
- package/public/assets/teams-TqFntFB_.js +0 -2
- package/public/assets/workspaces-m97LP4Eu.js +0 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Project instruction projection (#409) — the relay-owned replacement for host
|
|
2
|
+
// CLAUDE.md bleed-in. Given a resolved project, render its `rule`/`fact` entries
|
|
3
|
+
// (own + inherited `contains`-ancestor entries from #406's cascade) into a compact,
|
|
4
|
+
// DETERMINISTIC, size-bounded instruction block that can be prepended to a spawned
|
|
5
|
+
// agent's systemPromptAppend.
|
|
6
|
+
//
|
|
7
|
+
// ADDITIVE-FROM-EMPTY (#403 decision 4): this composer builds the instruction world
|
|
8
|
+
// from NOTHING and ADDS project knowledge. It NEVER reads host CLAUDE.md or subtracts
|
|
9
|
+
// from it. Going fully vanilla later = simply not including host-derived (`ingested`)
|
|
10
|
+
// content — a policy switch here, zero schema/composer change. `project_entries.source`
|
|
11
|
+
// is that lever: the included-source set is a policy PARAMETER, and the `vanilla`
|
|
12
|
+
// policy includes `manual`+`promoted` while excluding `ingested`.
|
|
13
|
+
import { resolveProjectEntries } from "./db";
|
|
14
|
+
import { PROJECT_ENTRY_SOURCES, type Project, type ProjectEntryKind, type ProjectEntrySource, type ResolvedProjectEntry } from "./types";
|
|
15
|
+
|
|
16
|
+
/** Named source policies — THE additive-from-empty lever (#403 decision 4).
|
|
17
|
+
* `all` — every source (manual + promoted + ingested host-CLAUDE.md content).
|
|
18
|
+
* `vanilla` — manual + promoted only; EXCLUDES `ingested`, so a vanilla agent gets
|
|
19
|
+
* zero host-CLAUDE.md bleed-in while still receiving relay-native knowledge. */
|
|
20
|
+
export const PROJECT_INSTRUCTION_SOURCE_SETS = {
|
|
21
|
+
all: PROJECT_ENTRY_SOURCES,
|
|
22
|
+
vanilla: ["manual", "promoted"],
|
|
23
|
+
} as const satisfies Record<string, readonly ProjectEntrySource[]>;
|
|
24
|
+
|
|
25
|
+
export const PROJECT_INSTRUCTION_POLICY_NAMES = ["all", "vanilla"] as const;
|
|
26
|
+
export type ProjectInstructionPolicyName = (typeof PROJECT_INSTRUCTION_POLICY_NAMES)[number];
|
|
27
|
+
|
|
28
|
+
/** Per-spawn projection request. Presence (a non-undefined value on the spawn input)
|
|
29
|
+
* means "project the preamble"; absence means OFF. `policy` selects the source set. */
|
|
30
|
+
export interface ProjectInstructionPolicy {
|
|
31
|
+
policy?: ProjectInstructionPolicyName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ComposeProjectInstructionsOptions {
|
|
35
|
+
/** Named policy selecting the included source set. Default `all`. */
|
|
36
|
+
policy?: ProjectInstructionPolicyName;
|
|
37
|
+
/** Explicit source set; overrides `policy` when given (the low-level lever). */
|
|
38
|
+
includeSources?: readonly ProjectEntrySource[];
|
|
39
|
+
/** Entry kinds rendered, in this order. Default `["rule", "fact"]` (the directive +
|
|
40
|
+
* truth kinds; `decision`/`memory` are log/scratch, not preamble material). */
|
|
41
|
+
includeKinds?: readonly ProjectEntryKind[];
|
|
42
|
+
/** Merge transitive `contains`-ancestor entries (#406 cascade). Default true. */
|
|
43
|
+
includeInherited?: boolean;
|
|
44
|
+
/** Hard byte cap on the rendered block (UTF-8). Default 8192. The block is truncated
|
|
45
|
+
* deterministically (lowest-priority entries dropped first) to stay within it. */
|
|
46
|
+
maxBytes?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_KINDS: readonly ProjectEntryKind[] = ["rule", "fact"];
|
|
50
|
+
const DEFAULT_MAX_BYTES = 8192;
|
|
51
|
+
const TRUNCATION_NOTICE = "_(… additional project entries omitted to fit the instruction budget.)_";
|
|
52
|
+
|
|
53
|
+
const KIND_HEADINGS: Record<ProjectEntryKind, string> = {
|
|
54
|
+
rule: "Rules",
|
|
55
|
+
fact: "Facts",
|
|
56
|
+
decision: "Decisions",
|
|
57
|
+
memory: "Memory",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function byteLen(value: string): number {
|
|
61
|
+
return Buffer.byteLength(value, "utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Stable dedup key — same kind + same (trimmed) title + body collapses to one entry.
|
|
65
|
+
* Uses NUL separators so distinct fields can't collide by concatenation. */
|
|
66
|
+
function dedupKey(entry: ResolvedProjectEntry): string {
|
|
67
|
+
return `${entry.kind}\u0000${entry.title.trim()}\u0000${(entry.body ?? "").trim()}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function entryLine(entry: ResolvedProjectEntry): string {
|
|
71
|
+
const body = entry.body?.trim();
|
|
72
|
+
const tag = entry.inherited ? " _(inherited)_" : "";
|
|
73
|
+
return body ? `- ${entry.title.trim()}: ${body}${tag}` : `- ${entry.title.trim()}${tag}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render a project's instruction block. Deterministic: ordering is by kind (in
|
|
78
|
+
* `includeKinds` order), then own-before-inherited, then title (code-point), then id —
|
|
79
|
+
* fully independent of DB row timestamps. Output is deduped and hard-bounded to
|
|
80
|
+
* `maxBytes`. Returns "" when no entry survives the source/kind filter (the caller then
|
|
81
|
+
* leaves systemPromptAppend untouched, so the spawn stays byte-identical).
|
|
82
|
+
*/
|
|
83
|
+
export function composeProjectInstructions(
|
|
84
|
+
project: Pick<Project, "id" | "slug" | "title">,
|
|
85
|
+
opts: ComposeProjectInstructionsOptions = {},
|
|
86
|
+
): string {
|
|
87
|
+
const kinds = opts.includeKinds ?? DEFAULT_KINDS;
|
|
88
|
+
const kindRank = new Map<ProjectEntryKind, number>(kinds.map((kind, index) => [kind, index]));
|
|
89
|
+
const sources = new Set<ProjectEntrySource>(
|
|
90
|
+
opts.includeSources ?? PROJECT_INSTRUCTION_SOURCE_SETS[opts.policy ?? "all"],
|
|
91
|
+
);
|
|
92
|
+
const includeInherited = opts.includeInherited !== false;
|
|
93
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
94
|
+
|
|
95
|
+
const resolved = resolveProjectEntries(project.id, { includeInherited });
|
|
96
|
+
const filtered = resolved.filter((entry) => kindRank.has(entry.kind) && sources.has(entry.source));
|
|
97
|
+
|
|
98
|
+
// Deterministic ordering, independent of DB row order/timestamps.
|
|
99
|
+
filtered.sort((a, b) => {
|
|
100
|
+
const byKind = (kindRank.get(a.kind) ?? 0) - (kindRank.get(b.kind) ?? 0);
|
|
101
|
+
if (byKind !== 0) return byKind;
|
|
102
|
+
const byInherited = Number(a.inherited) - Number(b.inherited); // own (false) first
|
|
103
|
+
if (byInherited !== 0) return byInherited;
|
|
104
|
+
const ta = a.title.trim();
|
|
105
|
+
const tb = b.title.trim();
|
|
106
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
107
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Dedup, own-wins (own sorts before inherited above).
|
|
111
|
+
const seen = new Set<string>();
|
|
112
|
+
const deduped: ResolvedProjectEntry[] = [];
|
|
113
|
+
for (const entry of filtered) {
|
|
114
|
+
const key = dedupKey(entry);
|
|
115
|
+
if (seen.has(key)) continue;
|
|
116
|
+
seen.add(key);
|
|
117
|
+
deduped.push(entry);
|
|
118
|
+
}
|
|
119
|
+
if (deduped.length === 0) return "";
|
|
120
|
+
|
|
121
|
+
// Greedy size-bounded render. Entries are contiguous by kind (primary sort key), so a
|
|
122
|
+
// per-kind `## heading` is emitted once when the first entry of that kind is included.
|
|
123
|
+
const header = `# Project knowledge: ${project.title?.trim() || project.slug}`;
|
|
124
|
+
const budget = maxBytes - byteLen(`\n\n${TRUNCATION_NOTICE}`);
|
|
125
|
+
const lines: string[] = [header];
|
|
126
|
+
let bytes = byteLen(header);
|
|
127
|
+
let lastKind: ProjectEntryKind | null = null;
|
|
128
|
+
let truncated = false;
|
|
129
|
+
|
|
130
|
+
for (const entry of deduped) {
|
|
131
|
+
const segments: string[] = [];
|
|
132
|
+
if (entry.kind !== lastKind) segments.push("", `## ${KIND_HEADINGS[entry.kind]}`);
|
|
133
|
+
segments.push(entryLine(entry));
|
|
134
|
+
const addition = `\n${segments.join("\n")}`;
|
|
135
|
+
if (bytes + byteLen(addition) > budget) {
|
|
136
|
+
truncated = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
lines.push(...segments);
|
|
140
|
+
bytes += byteLen(addition);
|
|
141
|
+
lastKind = entry.kind;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (truncated) lines.push("", TRUNCATION_NOTICE);
|
|
145
|
+
let out = lines.join("\n");
|
|
146
|
+
// Safety net: guarantee the hard byte cap even if the header + first entry overshoot.
|
|
147
|
+
if (byteLen(out) > maxBytes) {
|
|
148
|
+
out = Buffer.from(out, "utf8").subarray(0, maxBytes).toString("utf8");
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse a spawn-transport's `projectInstructions` wire value into the normalized policy
|
|
155
|
+
* (or undefined = OFF). Shared by the MCP and HTTP spawn paths so they cannot drift.
|
|
156
|
+
* - undefined / null / false → undefined (OFF)
|
|
157
|
+
* - true → {} (ON, default policy)
|
|
158
|
+
* - { policy } → validated policy
|
|
159
|
+
* Throws `onInvalid(message)` for a malformed shape or unknown policy name.
|
|
160
|
+
*/
|
|
161
|
+
export function parseProjectInstructions(
|
|
162
|
+
value: unknown,
|
|
163
|
+
onInvalid: (message: string) => Error,
|
|
164
|
+
): ProjectInstructionPolicy | undefined {
|
|
165
|
+
if (value === undefined || value === null || value === false) return undefined;
|
|
166
|
+
if (value === true) return {};
|
|
167
|
+
if (typeof value !== "object") {
|
|
168
|
+
throw onInvalid("projectInstructions must be a boolean or { policy } object");
|
|
169
|
+
}
|
|
170
|
+
const policy = (value as Record<string, unknown>).policy;
|
|
171
|
+
if (policy === undefined) return {};
|
|
172
|
+
if (typeof policy !== "string" || !PROJECT_INSTRUCTION_POLICY_NAMES.includes(policy as ProjectInstructionPolicyName)) {
|
|
173
|
+
throw onInvalid(`projectInstructions.policy must be one of: ${PROJECT_INSTRUCTION_POLICY_NAMES.join(", ")}`);
|
|
174
|
+
}
|
|
175
|
+
return { policy: policy as ProjectInstructionPolicyName };
|
|
176
|
+
}
|
|
@@ -6,6 +6,7 @@ import { ValidationError, listAgents } from "../db";
|
|
|
6
6
|
import { cleanMeta, cleanNullableString, cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
7
7
|
import { registerAgent } from "../services/register-agent";
|
|
8
8
|
import { spawnAgent, type SpawnAgentInput } from "../services/spawn-agent";
|
|
9
|
+
import { parseProjectInstructions } from "../project-instructions";
|
|
9
10
|
import { authContextFromRequest } from "../services/auth-context";
|
|
10
11
|
import { listHostDirectories } from "../agent-spawn";
|
|
11
12
|
import { rankRouteCandidates, type RouteAdvisorInput } from "../context-router";
|
|
@@ -116,6 +117,7 @@ export const postAgentSpawn: Handler = async (req) => {
|
|
|
116
117
|
profile: cleanString(parsed.body.profile, "profile", { max: 120 }),
|
|
117
118
|
prompt: cleanString(parsed.body.prompt, "prompt", { max: 16000 }),
|
|
118
119
|
systemPromptAppend: cleanString(parsed.body.systemPromptAppend, "systemPromptAppend", { max: 64_000 }),
|
|
120
|
+
projectInstructions: parseProjectInstructions(parsed.body.projectInstructions, (m) => new ValidationError(m)),
|
|
119
121
|
tags: cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }),
|
|
120
122
|
capabilities: cleanStringArray(parsed.body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 }),
|
|
121
123
|
providerArgs: cleanStringArray(parsed.body.providerArgs, "providerArgs", { itemMax: 80, maxItems: 50 }),
|
package/src/routes/commands.ts
CHANGED
|
@@ -229,15 +229,21 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
229
229
|
if (newBranch) {
|
|
230
230
|
setWorkspaceBranch(workspaceId, newBranch, baseSha ?? mergedSha);
|
|
231
231
|
}
|
|
232
|
-
// #239 — push the author a "your branch landed" notice (no polling).
|
|
232
|
+
// #239 — push the author a "your branch landed" notice (no polling). Fires on a
|
|
233
233
|
// real land; a no-op resolution (#230) merged nothing, so it earns no notice.
|
|
234
|
-
|
|
234
|
+
// #423 — a PR-land RECYCLE is a no-op merge (`merged:false`) but a land DID happen
|
|
235
|
+
// on the remote: the relay carries that ground truth in `params.prLanded`, so the
|
|
236
|
+
// notice fires from it with the recycled `newBranch` — full rebase-ff parity.
|
|
237
|
+
const prLanded = isRecord(command.params.prLanded) ? command.params.prLanded : undefined;
|
|
238
|
+
if ((command.result.merged === true || prLanded) && landedWorkspace) {
|
|
235
239
|
notifyBranchLanded({
|
|
236
240
|
workspace: landedWorkspace,
|
|
237
|
-
mergedSha,
|
|
238
|
-
subject:
|
|
241
|
+
mergedSha: prLanded ? cleanString(prLanded.sha, "params.prLanded.sha", { max: 64 }) ?? mergedSha : mergedSha,
|
|
242
|
+
subject: prLanded
|
|
243
|
+
? cleanString(prLanded.subject, "params.prLanded.subject", { max: 200 })
|
|
244
|
+
: cleanString(command.result.subject, "result.subject", { max: 200 }),
|
|
239
245
|
newBranch,
|
|
240
|
-
pushed: typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
|
|
246
|
+
pushed: prLanded ? true : typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
|
|
241
247
|
});
|
|
242
248
|
}
|
|
243
249
|
}
|
package/src/routes/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { getOrchestratorDirectories, getOrchestratorFileRead, getOrchestratorFil
|
|
|
28
28
|
import { getPairById, getPairs, postAcceptPair, postHangupPair, postPair, postPairMessage, postRejectPair } from "./pairs";
|
|
29
29
|
import { getPlanByIdRoute, getPlansRoute } from "./plans";
|
|
30
30
|
import { getBlackboardEntryRoute, getBlackboardRoute } from "./blackboard";
|
|
31
|
+
import { getProjectEntriesRoute, getProjectEntryRoute, getProjectRoute, getProjectsRoute } from "./projects";
|
|
31
32
|
import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
|
|
32
33
|
import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
|
|
33
34
|
import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim } from "./tasks";
|
|
@@ -280,6 +281,10 @@ const routes: Route[] = [
|
|
|
280
281
|
route("GET", "/api/plans/:id", getPlanByIdRoute),
|
|
281
282
|
route("GET", "/api/blackboard", getBlackboardRoute),
|
|
282
283
|
route("GET", "/api/blackboard/:id", getBlackboardEntryRoute),
|
|
284
|
+
route("GET", "/api/projects", getProjectsRoute),
|
|
285
|
+
route("GET", "/api/projects/:id", getProjectRoute),
|
|
286
|
+
route("GET", "/api/projects/:id/entries", getProjectEntriesRoute),
|
|
287
|
+
route("GET", "/api/projects/:id/entries/:entryId", getProjectEntryRoute),
|
|
283
288
|
|
|
284
289
|
route("GET", "/api/spec", getApiSpec),
|
|
285
290
|
route("GET", "/api/docs", getApiDocs),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getProjectEntry, getProjectEntryChain, listProjectEntries, listProjects, resolveProject, rootsForProject } from "../db";
|
|
2
|
+
import { cleanString, optionalEnum } from "../validation";
|
|
3
|
+
import { error, json, type Handler } from "./_shared";
|
|
4
|
+
import { PROJECT_ENTRY_KINDS, type ProjectEntryKind } from "../types";
|
|
5
|
+
|
|
6
|
+
// Read routes for the durable Projects scope (#405), templated on routes/blackboard.ts.
|
|
7
|
+
// Writes go through MCP (relay_project_*), mirroring the blackboard precedent where the
|
|
8
|
+
// HTTP surface is read-only. Scope-gated /api/projects → project:read in src/security.ts.
|
|
9
|
+
|
|
10
|
+
function routeKind(req: Request): ProjectEntryKind | undefined {
|
|
11
|
+
return optionalEnum(new URL(req.url).searchParams.get("kind") ?? undefined, "kind", PROJECT_ENTRY_KINDS) as ProjectEntryKind | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getProjectsRoute: Handler = (req) => {
|
|
15
|
+
try {
|
|
16
|
+
const includeArchived = new URL(req.url).searchParams.get("includeArchived") === "true";
|
|
17
|
+
return json({ projects: listProjects({ includeArchived }) });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return error(err instanceof Error ? err.message : "projects route failed");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getProjectRoute: Handler = (req, params) => {
|
|
24
|
+
try {
|
|
25
|
+
const ref = cleanString(params.id, "id", { required: true, max: 200 })!;
|
|
26
|
+
const project = resolveProject(ref);
|
|
27
|
+
if (!project) return error("project not found", 404);
|
|
28
|
+
return json({ project, roots: rootsForProject(project.id) });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return error(err instanceof Error ? err.message : "projects route failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getProjectEntriesRoute: Handler = (req, params) => {
|
|
35
|
+
try {
|
|
36
|
+
const ref = cleanString(params.id, "id", { required: true, max: 200 })!;
|
|
37
|
+
const project = resolveProject(ref);
|
|
38
|
+
if (!project) return error("project not found", 404);
|
|
39
|
+
const includeInactive = new URL(req.url).searchParams.get("includeInactive") === "true";
|
|
40
|
+
return json({ entries: listProjectEntries(project.id, { kind: routeKind(req), includeInactive }) });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return error(err instanceof Error ? err.message : "projects route failed");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const getProjectEntryRoute: Handler = (req, params) => {
|
|
47
|
+
try {
|
|
48
|
+
const id = cleanString(params.entryId, "entryId", { required: true, max: 120 })!;
|
|
49
|
+
const entry = getProjectEntry(id);
|
|
50
|
+
if (!entry) return error("project entry not found", 404);
|
|
51
|
+
return json({ entry, chain: getProjectEntryChain(id) });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return error(err instanceof Error ? err.message : "projects route failed");
|
|
54
|
+
}
|
|
55
|
+
};
|
package/src/runtime-tokens.ts
CHANGED
|
@@ -104,6 +104,8 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
104
104
|
spawnedBy?: string;
|
|
105
105
|
/** Durable team id — stamped so the child registers into the coordinator's team. */
|
|
106
106
|
teamId?: string;
|
|
107
|
+
/** Durable project id resolved from the spawn cwd (#405) — stamped so callerProject(auth) resolves it. */
|
|
108
|
+
projectId?: string;
|
|
107
109
|
}): RuntimeTokenResult {
|
|
108
110
|
const grant = spawnGrantForProfile(input.profile, input.agentInitiated);
|
|
109
111
|
const subject = input.policyName
|
|
@@ -124,6 +126,7 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
124
126
|
...(grant.canSpawn && grant.maxSpawnedAgents ? { maxSpawnedAgents: grant.maxSpawnedAgents } : {}),
|
|
125
127
|
...(input.spawnedBy ? { spawnedBy: input.spawnedBy } : {}),
|
|
126
128
|
...(input.teamId ? { teamId: input.teamId } : {}),
|
|
129
|
+
...(input.projectId ? { projectId: input.projectId } : {}),
|
|
127
130
|
},
|
|
128
131
|
createdBy: input.createdBy ?? "runtime",
|
|
129
132
|
});
|
|
@@ -244,6 +247,9 @@ export function runnerRuntimeTokenEnv(input: {
|
|
|
244
247
|
/** Agent-requested spawn → forces a non-spawn-capable child (no grandchildren). */
|
|
245
248
|
agentInitiated?: boolean;
|
|
246
249
|
spawnedBy?: string;
|
|
250
|
+
teamId?: string;
|
|
251
|
+
/** Durable project id resolved from the spawn cwd (#405). */
|
|
252
|
+
projectId?: string;
|
|
247
253
|
}): Record<string, string> {
|
|
248
254
|
const issued = issueRunnerRuntimeToken(input);
|
|
249
255
|
return {
|
package/src/security.ts
CHANGED
|
@@ -206,6 +206,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
206
206
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
207
207
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
208
208
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
209
|
+
if (pathname.startsWith("/api/projects")) return method === "GET" ? "project:read" : "project:write";
|
|
209
210
|
// Insights config (the feature toggle) stays admin-only via the default; only the
|
|
210
211
|
// mechanical observation feed is writable by lower-privilege callers.
|
|
211
212
|
if (pathname === "/api/insights/observations") return method === "GET" ? "insights:read" : "insights:write";
|
|
@@ -282,6 +283,9 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
282
283
|
if (pathname.startsWith("/api/plans")) return method === "GET" ? "plans:read" : "plans:write";
|
|
283
284
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
284
285
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
286
|
+
// Fail-closed default returns system:admin, so this case is load-bearing: without it
|
|
287
|
+
// every /api/projects call from a scoped component token would 403-blackhole (#405).
|
|
288
|
+
if (pathname.startsWith("/api/projects")) return method === "GET" ? "project:read" : "project:write";
|
|
285
289
|
// The Runner posts the #184 context-gathering signal here (source:"server") via its
|
|
286
290
|
// provider token. Without this case the path fell through to the system:admin default
|
|
287
291
|
// below and every observation was 403-dropped — the whole Insights feed stayed empty.
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
// deliberately out of scope here — this slice converges the two external TRANSPORTS.
|
|
26
26
|
import type { SpawnProvider } from "agent-relay-sdk";
|
|
27
27
|
import { stringValue } from "agent-relay-sdk";
|
|
28
|
-
import { countLiveSpawnedAgents, createActivityEvent, ensureTeam, getAgent, listAgents, setAgentTeam, ValidationError } from "../db";
|
|
28
|
+
import { countLiveSpawnedAgents, createActivityEvent, ensureTeam, getAgent, listAgents, resolveProjectForCwd, setAgentTeam, ValidationError } from "../db";
|
|
29
29
|
import { createCommand } from "../commands-db";
|
|
30
30
|
import { emitCommandEvent } from "../command-events";
|
|
31
31
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
|
|
32
32
|
import { runnerRuntimeTokenEnv } from "../runtime-tokens";
|
|
33
33
|
import { selectSpawnOrchestrator } from "../spawn-targets";
|
|
34
34
|
import { getAgentProfile } from "../config-store";
|
|
35
|
+
import { composeProjectInstructions, type ProjectInstructionPolicy } from "../project-instructions";
|
|
35
36
|
import { hasComponentScope, isComponentAuthorizedFor } from "../security";
|
|
36
37
|
import { isPathWithinBase } from "../utils";
|
|
37
38
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
@@ -62,6 +63,16 @@ export interface SpawnAgentInput {
|
|
|
62
63
|
profile?: string;
|
|
63
64
|
prompt?: string;
|
|
64
65
|
systemPromptAppend?: string;
|
|
66
|
+
/**
|
|
67
|
+
* #409 — project instruction projection. When set, relay composes an instruction
|
|
68
|
+
* preamble from the spawn's resolved project (own entries + inherited `contains`-
|
|
69
|
+
* ancestors) and PREPENDS it to systemPromptAppend. Additive-from-empty (#403
|
|
70
|
+
* decision 4): the block is built only from `project_entries`, never from host
|
|
71
|
+
* CLAUDE.md. Absent (undefined) = OFF → the spawn command is byte-identical to today.
|
|
72
|
+
* `policy` selects the included source set: `all` (manual+promoted+ingested) or
|
|
73
|
+
* `vanilla` (manual+promoted; excludes `ingested` host-CLAUDE.md content).
|
|
74
|
+
*/
|
|
75
|
+
projectInstructions?: ProjectInstructionPolicy;
|
|
65
76
|
/**
|
|
66
77
|
* #413 — does this spawn carry a report-back contract? When true, relay seeds a pending
|
|
67
78
|
* reply obligation from the spawner to the child at registration, so the child can't go
|
|
@@ -192,6 +203,25 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
192
203
|
const callerTeam = callerId ? (caller?.teamId ? { id: caller.teamId } : ensureTeam(callerId)) : undefined;
|
|
193
204
|
if (callerId && callerTeam && !caller?.teamId) setAgentTeam(callerId, callerTeam.id);
|
|
194
205
|
|
|
206
|
+
// #405 — durable project binding: the project whose registered root contains the spawn cwd.
|
|
207
|
+
// Stamped on the child token's constraints (identity-only, like teamId) so callerProject(auth)
|
|
208
|
+
// resolves it. Absent (graceful) when the cwd is inside no registered root.
|
|
209
|
+
const callerProject = resolveProjectForCwd(resolvedCwd);
|
|
210
|
+
|
|
211
|
+
// #409 — instruction projection (default OFF). Only when the caller opts in AND the
|
|
212
|
+
// spawn cwd resolves to a project do we compose a knowledge preamble and PREPEND it to
|
|
213
|
+
// systemPromptAppend. Absent flag / no project / empty block ⇒ systemPromptAppend is
|
|
214
|
+
// left exactly as the caller passed it, so the spawn command stays byte-identical.
|
|
215
|
+
let systemPromptAppend = input.systemPromptAppend;
|
|
216
|
+
if (input.projectInstructions && callerProject) {
|
|
217
|
+
const block = composeProjectInstructions(callerProject, {
|
|
218
|
+
...(input.projectInstructions.policy ? { policy: input.projectInstructions.policy } : {}),
|
|
219
|
+
});
|
|
220
|
+
if (block) {
|
|
221
|
+
systemPromptAppend = systemPromptAppend ? `${block}\n\n${systemPromptAppend}` : block;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
195
225
|
// Child runner token. `agentInitiated` (set whenever an agent is the caller) forces a
|
|
196
226
|
// non-spawn-capable child (no grandchildren); `spawnedBy` stamps authoritative lineage the
|
|
197
227
|
// child can't forge (it registers with spawnedBy = caller, read from the signed token).
|
|
@@ -211,6 +241,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
211
241
|
profile: input.profile || undefined,
|
|
212
242
|
...(callerId ? { agentInitiated: true, spawnedBy: callerId } : {}),
|
|
213
243
|
...(callerTeam ? { teamId: callerTeam.id } : {}),
|
|
244
|
+
...(callerProject ? { projectId: callerProject.id } : {}),
|
|
214
245
|
});
|
|
215
246
|
|
|
216
247
|
// #413 — default the report-back obligation ON for an agent coordinator handing a worker a
|
|
@@ -238,7 +269,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
238
269
|
permissionMode: approvalMode,
|
|
239
270
|
providerArgs: input.providerArgs ?? [],
|
|
240
271
|
prompt: input.prompt,
|
|
241
|
-
systemPromptAppend
|
|
272
|
+
systemPromptAppend,
|
|
242
273
|
...(expectReply ? { expectReply } : {}),
|
|
243
274
|
policyName: input.policyName,
|
|
244
275
|
spawnRequestId,
|
package/src/services/team.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function setCallerTeamBrief(ctx: AuthContext, input: TeamBriefInput): Tea
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function dissolveTeamById(teamId: string, input: TeamDissolveServiceInput = {}): Team {
|
|
59
|
-
const team = dissolveTeam(teamId, input);
|
|
59
|
+
const { team, unpromotedBlackboard } = dissolveTeam(teamId, input);
|
|
60
60
|
emitRelayEvent({
|
|
61
61
|
type: "team.dissolved",
|
|
62
62
|
source: "system",
|
|
@@ -65,6 +65,10 @@ export function dissolveTeamById(teamId: string, input: TeamDissolveServiceInput
|
|
|
65
65
|
teamId: team.id,
|
|
66
66
|
outcomeStatus: team.outcomeStatus ?? null,
|
|
67
67
|
reason: team.outcomeSummary ?? null,
|
|
68
|
+
// Surface (don't auto-promote) the lossy findings the team is leaving behind so a
|
|
69
|
+
// coordinator/operator can decide what to lift into durable project knowledge (#404).
|
|
70
|
+
unpromotedBlackboardCount: unpromotedBlackboard.count,
|
|
71
|
+
unpromotedBlackboard: unpromotedBlackboard.entries,
|
|
68
72
|
},
|
|
69
73
|
});
|
|
70
74
|
return team;
|
package/src/token-db.ts
CHANGED
|
@@ -57,7 +57,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
57
57
|
name: "Provider Agent",
|
|
58
58
|
description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
|
|
59
59
|
role: "provider",
|
|
60
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "insights:write"],
|
|
60
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
|
|
61
61
|
ttlSeconds: 24 * 60 * 60,
|
|
62
62
|
builtIn: true,
|
|
63
63
|
createdBy: "system",
|
|
@@ -67,7 +67,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
67
67
|
name: "Provider Child Agent",
|
|
68
68
|
description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
|
|
69
69
|
role: "provider",
|
|
70
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "insights:write"],
|
|
70
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
|
|
71
71
|
constraints: { canDelegate: false },
|
|
72
72
|
ttlSeconds: 2 * 60 * 60,
|
|
73
73
|
builtIn: true,
|
|
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
78
78
|
name: "Provider Interactive Agent",
|
|
79
79
|
description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
|
|
80
80
|
role: "provider",
|
|
81
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "insights:write"],
|
|
81
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
|
|
82
82
|
constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
|
|
83
83
|
ttlSeconds: 30 * 24 * 60 * 60,
|
|
84
84
|
builtIn: true,
|
package/src/workspace-merge.ts
CHANGED
|
@@ -113,6 +113,16 @@ interface RequestWorkspaceMergeOptions {
|
|
|
113
113
|
reviewer?: string;
|
|
114
114
|
/** Extra metadata merged onto the workspace row when moving to merge_planned. */
|
|
115
115
|
metadata?: Record<string, unknown>;
|
|
116
|
+
/**
|
|
117
|
+
* Set when this "merge" is really a land-and-continue RECYCLE of a workspace whose PR
|
|
118
|
+
* already merged on the remote out-of-band (#423). The host preview is then a no-op
|
|
119
|
+
* (work is already in base) so `mergeWorkspace` recycles the live owner onto a fresh
|
|
120
|
+
* branch without merging anything — `result.merged` is false. The relay still owns the
|
|
121
|
+
* ground truth that a land happened (it observed `prMerged`), so it carries the merge
|
|
122
|
+
* SHA/subject here; the merge-result handler fires the `branch.landed` notice from it,
|
|
123
|
+
* giving full parity with a rebase-ff land-and-continue.
|
|
124
|
+
*/
|
|
125
|
+
prLanded?: { sha?: string; subject?: string };
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
type RequestWorkspaceMergeResult =
|
|
@@ -253,6 +263,7 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
|
|
|
253
263
|
prBody,
|
|
254
264
|
...(autoMerge !== undefined ? { autoMerge } : {}),
|
|
255
265
|
...(opts.reviewer !== undefined ? { reviewer: opts.reviewer } : {}),
|
|
266
|
+
...(opts.prLanded !== undefined ? { prLanded: opts.prLanded } : {}),
|
|
256
267
|
requestedBy: opts.requestedBy,
|
|
257
268
|
requestedAt: Date.now(),
|
|
258
269
|
},
|
|
@@ -1,9 +1,65 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
2
|
import { createCommand, getCommand } from "./commands-db";
|
|
3
|
-
import { patchWorkspaceMetadata } from "./db";
|
|
3
|
+
import { createActivityEvent, patchWorkspaceMetadata } from "./db";
|
|
4
4
|
import { handleReviewerPipelineScan, isAwaitingReviewerApproval } from "./reviewer-pipeline";
|
|
5
|
+
import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
|
|
5
6
|
import type { Command, WorkspaceAutoMergePolicy, WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Decide what to do when a workspace's PR has merged on the remote (`prMerged`/`landed`),
|
|
10
|
+
* out-of-band of the relay's land path. Splits on owner liveness:
|
|
11
|
+
*
|
|
12
|
+
* - LIVE owner → land-and-continue RECYCLE (#423). The rebase-ff land path recycles a live
|
|
13
|
+
* owner onto a fresh `agent/...-N` branch off the advanced base; a PR land never ran through
|
|
14
|
+
* it, stranding the owner on its merged branch at terminal `merged` (`relay_workspace_status`
|
|
15
|
+
* then reports "you don't own an isolated workspace"). Dispatch a `rebase-ff` merge carrying
|
|
16
|
+
* `prLanded` (the merge SHA/subject — our ground truth). The host honors prLanded and recycles
|
|
17
|
+
* the live owner onto a fresh branch cut off the UPDATED upstream base, returning it to
|
|
18
|
+
* `active` in the same CWD. (A bare no-op assumption would NOT hold for a squash merge onto an
|
|
19
|
+
* advanced base — local cherry/tree detection can't see the squash commit, so the host's own
|
|
20
|
+
* no-op derivation reports ahead>0 and would soft-loop via review_requested; prLanded is what
|
|
21
|
+
* short-circuits that, see `mergeWorkspace`.) Explicit `rebase-ff` so a (prMerged-gated,
|
|
22
|
+
* near-impossible) non-recycle never opens a duplicate PR; the merge-result handler fires the
|
|
23
|
+
* `branch.landed` notice from `prLanded` (SHA + subject) since the recycle reports no land.
|
|
24
|
+
* - GONE owner (offline/absent) → finalize terminal `merged` + store-ahead `branch.landed` for
|
|
25
|
+
* its next poll (#304). Signalled by `kind: "finalize"`; the caller invokes the finalizer.
|
|
26
|
+
*
|
|
27
|
+
* `kind: "wait"` means the per-repo merge lease is held this tick (a concurrent merge) — retry
|
|
28
|
+
* next sweep; the caller must NOT finalize, or it re-strands the live owner.
|
|
29
|
+
*/
|
|
30
|
+
export function reconcileLandedPr(workspace: WorkspaceRecord, preview: WorkspaceMergePreview):
|
|
31
|
+
| { kind: "recycle"; command: Command }
|
|
32
|
+
| { kind: "wait"; reason: string }
|
|
33
|
+
| { kind: "finalize" } {
|
|
34
|
+
if (!isOwnerAlive(workspace.ownerAgentId)) return { kind: "finalize" };
|
|
35
|
+
const recycle = requestWorkspaceMerge(workspace, {
|
|
36
|
+
strategy: "rebase-ff",
|
|
37
|
+
requestedBy: "pr-land-recycle",
|
|
38
|
+
prLanded: {
|
|
39
|
+
sha: typeof preview.prMergeSha === "string" ? preview.prMergeSha : undefined,
|
|
40
|
+
subject: typeof preview.prMergeSubject === "string" ? preview.prMergeSubject : undefined,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (recycle.ok) {
|
|
44
|
+
createActivityEvent({
|
|
45
|
+
clientId: `workspace-pr-recycle-${workspace.id}-${Date.now()}`,
|
|
46
|
+
kind: "state",
|
|
47
|
+
title: "Workspace recycling after PR land",
|
|
48
|
+
body: `${workspace.branch ?? workspace.id}'s PR merged into ${preview.baseRef ?? "base"} — recycling the live owner onto a fresh branch (land-and-continue)`,
|
|
49
|
+
meta: workspace.branch ?? workspace.id,
|
|
50
|
+
icon: "ti-git-merge",
|
|
51
|
+
view: "orchestrators",
|
|
52
|
+
metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: workspace.id, commandId: recycle.command.id, landedVia: "pr" },
|
|
53
|
+
});
|
|
54
|
+
return { kind: "recycle", command: recycle.command };
|
|
55
|
+
}
|
|
56
|
+
// 409 = lease held by an in-flight merge — retry next sweep, don't finalize (re-strands a
|
|
57
|
+
// live owner). Any other failure (e.g. lost worktree) falls through to finalize so the row
|
|
58
|
+
// doesn't loop forever in a reconcilable status.
|
|
59
|
+
if (recycle.status === 409) return { kind: "wait", reason: "recycle-lease-held" };
|
|
60
|
+
return { kind: "finalize" };
|
|
61
|
+
}
|
|
62
|
+
|
|
7
63
|
const ACTIVE_COMMAND_STATUSES = new Set(["pending", "accepted", "running"]);
|
|
8
64
|
const PASSING_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
|
|
9
65
|
const PASSING_STATES = new Set(["SUCCESS"]);
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{At as t,B as n,Cn as r,G as i,I as a,J as o,Kn as s,Mn as c,On as l,P as u,Pn as d,Rt as f,g as p,i as m,l as h,m as g,st as _}from"./lucide-react-CMhBract.js";import{o as ee,t as v}from"./store-BgJq_lN9.js";import{G as y,K as b,U as x,V as S,a as C,d as w,i as te,n as ne}from"./display-CketawEF.js";import{o as T,r as E}from"./dist-DV2tZ2fq.js";import{t as D}from"./dist-CoUvwLTy.js";import{i as O,n as k}from"./dist-CV3sgFiv.js";import{t as A}from"./dist-C5t_OxI0.js";import{t as j}from"./dist-1x-LE9Uk.js";import{a as M,i as N,n as re,o as ie,r as ae,s as oe,t as P}from"./es2015-DcavLcQ0.js";import{a as F,i as se,n as I,r as L,t as R}from"./dist-Ca3ujqG_.js";import{t as z}from"./badge-Dj6PREuI.js";import{t as B}from"./button-Cqpufqam.js";import{t as ce}from"./copy-button-DI7b8A7_.js";import{B as le,H as V,P as ue,S as H,U,V as W,d as de,g as fe,h as pe,l as me,m as he,p as ge,u as _e,x as G}from"./index-Ba2RDJaA.js";import{n as ve,t as ye}from"./card-OmDF73CE.js";import{n as be,t as xe}from"./branch-state-badge-CdYg2HYc.js";var K=e(s(),1),q=d(),J=`Popover`,[Se,Ce]=O(J,[F]),Y=F(),[we,X]=Se(J),Te=e=>{let{__scopePopover:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!1}=e,s=Y(t),c=K.useRef(null),[l,u]=K.useState(!1),[d,f]=A({prop:r,defaultProp:i??!1,onChange:a,caller:J});return(0,q.jsx)(se,{...s,children:(0,q.jsx)(we,{scope:t,contentId:oe(),triggerRef:c,open:d,onOpenChange:f,onOpenToggle:K.useCallback(()=>f(e=>!e),[f]),hasCustomAnchor:l,onCustomAnchorAdd:K.useCallback(()=>u(!0),[]),onCustomAnchorRemove:K.useCallback(()=>u(!1),[]),modal:o,children:n})})};Te.displayName=J;var Ee=`PopoverAnchor`,De=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Ee,n),a=Y(n),{onCustomAnchorAdd:o,onCustomAnchorRemove:s}=i;return K.useEffect(()=>(o(),()=>s()),[o,s]),(0,q.jsx)(R,{...a,...r,ref:t})});De.displayName=Ee;var Oe=`PopoverTrigger`,ke=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Oe,n),a=Y(n),o=T(t,i.triggerRef),s=(0,q.jsx)(D.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.contentId,"data-state":Ve(i.open),...r,ref:o,onClick:k(e.onClick,i.onOpenToggle)});return i.hasCustomAnchor?s:(0,q.jsx)(R,{asChild:!0,...a,children:s})});ke.displayName=Oe;var Z=`PopoverPortal`,[Ae,je]=Se(Z,{forceMount:void 0}),Me=e=>{let{__scopePopover:t,forceMount:n,children:r,container:i}=e,a=X(Z,t);return(0,q.jsx)(Ae,{scope:t,forceMount:n,children:(0,q.jsx)(j,{present:n||a.open,children:(0,q.jsx)(N,{asChild:!0,container:i,children:r})})})};Me.displayName=Z;var Q=`PopoverContent`,Ne=K.forwardRef((e,t)=>{let n=je(Q,e.__scopePopover),{forceMount:r=n.forceMount,...i}=e,a=X(Q,e.__scopePopover);return(0,q.jsx)(j,{present:r||a.open,children:a.modal?(0,q.jsx)(Fe,{...i,ref:t}):(0,q.jsx)(Ie,{...i,ref:t})})});Ne.displayName=Q;var Pe=E(`PopoverContent.RemoveScroll`),Fe=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(null),i=T(t,r),a=K.useRef(!1);return K.useEffect(()=>{let e=r.current;if(e)return P(e)},[]),(0,q.jsx)(re,{as:Pe,allowPinchZoom:!0,children:(0,q.jsx)(Le,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:k(e.onCloseAutoFocus,e=>{e.preventDefault(),a.current||n.triggerRef.current?.focus()}),onPointerDownOutside:k(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;a.current=t.button===2||n},{checkForDefaultPrevented:!1}),onFocusOutside:k(e.onFocusOutside,e=>e.preventDefault(),{checkForDefaultPrevented:!1})})})}),Ie=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(!1),i=K.useRef(!1);return(0,q.jsx)(Le,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),Le=K.forwardRef((e,t)=>{let{__scopePopover:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,disableOutsidePointerEvents:o,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onInteractOutside:u,...d}=e,f=X(Q,n),p=Y(n);return ae(),(0,q.jsx)(M,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,q.jsx)(ie,{asChild:!0,disableOutsidePointerEvents:o,onInteractOutside:u,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onDismiss:()=>f.onOpenChange(!1),children:(0,q.jsx)(L,{"data-state":Ve(f.open),role:`dialog`,id:f.contentId,...p,...d,ref:t,style:{...d.style,"--radix-popover-content-transform-origin":`var(--radix-popper-transform-origin)`,"--radix-popover-content-available-width":`var(--radix-popper-available-width)`,"--radix-popover-content-available-height":`var(--radix-popper-available-height)`,"--radix-popover-trigger-width":`var(--radix-popper-anchor-width)`,"--radix-popover-trigger-height":`var(--radix-popper-anchor-height)`}})})})}),$=`PopoverClose`,Re=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X($,n);return(0,q.jsx)(D.button,{type:`button`,...r,ref:t,onClick:k(e.onClick,()=>i.onOpenChange(!1))})});Re.displayName=$;var ze=`PopoverArrow`,Be=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=Y(n);return(0,q.jsx)(I,{...i,...r,ref:t})});Be.displayName=ze;function Ve(e){return e?`open`:`closed`}var He=Te,Ue=ke,We=Me,Ge=Ne;function Ke({...e}){return(0,q.jsx)(He,{"data-slot":`popover`,...e})}function qe({...e}){return(0,q.jsx)(Ue,{"data-slot":`popover-trigger`,...e})}function Je({className:e,align:t=`center`,sideOffset:n=4,...r}){return(0,q.jsx)(We,{children:(0,q.jsx)(Ge,{"data-slot":`popover-content`,align:t,sideOffset:n,className:c(`z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r})})}function Ye({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-header`,className:c(`flex flex-col gap-0.5 text-sm`,e),...t})}function Xe({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-title`,className:c(`font-medium`,e),...t})}function Ze({agent:e,childAgents:r=[],parentAgent:a}){let s=ee();v(e=>e.set);let c=v(e=>e.switchView),l=v(e=>e.openInboxThread),d=v(e=>e.openAgentDetail),m=v(e=>e.openPairInvite),T=v(e=>e.openRename),E=v(e=>e.openConfirm),D=v(e=>e.doAgentAction),O=v(e=>e.doDeleteAgent),k=v(e=>e.openFilesForAgent),A=v(e=>e.workspaceAction),j=v(e=>e.openWorkspaceFocus),{terminalOpen:M,terminalTarget:N,terminalOpening:re,openTerminal:ie,closeTerminal:ae}=be(e.id),oe=le(),P=te(s,e,ue(e),oe[e.id]||null),F=C(e,`shutdown`),se=C(e,`compact`),I=C(e,`clearContext`),L=!!(e.providerCapabilities?.terminal?.live?.read||e.providerCapabilities?.terminal?.attach?.create),R=!!(e.providerCapabilities?.terminal?.live?.write||e.providerCapabilities?.model?.provider===`claude`),V=ne(e),U=a?w(a):e.spawnedBy,W=r.length>0;return(0,q.jsxs)(q.Fragment,{children:[(0,q.jsx)(ye,{className:`group hover:border-zinc-600 transition-colors cursor-pointer`,onClick:()=>d(e),children:(0,q.jsxs)(ve,{className:`p-4`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2.5 mb-2.5`,children:[(0,q.jsx)(H,{agent:e,now:s,className:`mt-0.5`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,q.jsx)(`div`,{className:`text-sm font-medium truncate`,children:w(e)}),(0,q.jsx)(`div`,{className:`text-xs mt-0.5 ${y(P.tone)}`,children:P.label}),typeof e.meta?.cwd==`string`&&e.meta.cwd&&(0,q.jsx)(`button`,{type:`button`,className:`mt-0.5 block max-w-full truncate font-mono text-[10px] text-muted-foreground hover:text-foreground`,title:`Open ${e.meta.cwd} in Files`,onClick:t=>{t.stopPropagation(),k(e)},children:S(e.meta.cwd)})]}),e.branchState&&e.branchWorkspaceId&&(0,q.jsx)(xe,{state:e.branchState,className:`mt-0.5 shrink-0`,onClick:t=>{t.stopPropagation(),j(e.branchWorkspaceId)}})]}),P.badges.length>0&&(0,q.jsx)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:P.badges.map(e=>(0,q.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${e.className}`,children:e.label},e.label))}),(W||e.spawnedBy&&U)&&(0,q.jsxs)(`div`,{className:`mb-2 flex flex-wrap gap-1.5`,onClick:e=>e.stopPropagation(),children:[W&&(0,q.jsxs)(Ke,{children:[(0,q.jsx)(qe,{asChild:!0,children:(0,q.jsxs)(`button`,{type:`button`,className:`inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 text-[10px] font-medium text-sky-300 transition-colors hover:bg-sky-500/15`,title:`${r.length} live child ${r.length===1?`agent`:`agents`}`,children:[(0,q.jsx)(h,{className:`h-3 w-3`}),(0,q.jsx)(`span`,{children:r.length}),(0,q.jsx)(`span`,{className:`flex -space-x-1`,children:r.slice(0,4).map(e=>(0,q.jsx)(H,{agent:e,now:s,className:`h-2 w-2 ring-1 ring-background`},e.id))})]})}),(0,q.jsxs)(Je,{align:`start`,className:`w-80`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(Ye,{children:(0,q.jsx)(Xe,{children:`Team`})}),(0,q.jsx)(`div`,{className:`space-y-1.5`,children:r.map(e=>(0,q.jsxs)(`div`,{className:`rounded-md border border-border bg-card/60 p-2`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,q.jsx)(H,{agent:e,now:s,className:`mt-1`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,q.jsx)(`div`,{className:`truncate text-xs font-medium`,children:w(e)}),(0,q.jsx)(`div`,{className:`truncate text-[10px] text-muted-foreground`,children:e.providerCapabilities?.model?.alias||e.providerCapabilities?.model?.id||e.providerCapabilities?.model?.provider||e.status})]}),(0,q.jsx)(z,{variant:`outline`,className:`h-4 px-1.5 text-[9px]`,children:e.status})]}),(0,q.jsxs)(`div`,{className:`mt-2 flex gap-1`,children:[(0,q.jsx)(B,{size:`xs`,variant:`outline`,onClick:()=>d(e),children:`Open`}),(0,q.jsxs)(B,{size:`xs`,variant:`ghost`,onClick:()=>{l(e.id),c(`chat`)},children:[(0,q.jsx)(o,{className:`h-3 w-3`}),`Chat`]})]})]},e.id))})]})]}),e.spawnedBy&&U&&(0,q.jsx)(z,{variant:`outline`,className:`max-w-full text-[10px] text-muted-foreground`,children:(0,q.jsxs)(`span`,{className:`truncate`,children:[`spawned by `,U]})})]}),(e.tags.length>0||b(e.capabilities).length>0)&&(0,q.jsxs)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:[e.tags.slice(0,3).map(e=>(0,q.jsx)(z,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e)),b(e.capabilities).slice(0,2).map(e=>(0,q.jsx)(z,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))]}),(0,q.jsxs)(`div`,{className:`space-y-2 mb-2`,children:[(0,q.jsx)(pe,{agent:e}),(0,q.jsx)(he,{agent:e,compact:!0}),(0,q.jsx)(fe,{agent:e})]}),(0,q.jsx)(`div`,{className:`text-xs text-muted-foreground`,children:e.id!==`user`&&e.id!==`system`&&x(s,e.lastSeen)}),(0,q.jsxs)(`div`,{className:`flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(ce,{value:e.id,label:`Copy agent ID`,size:`icon`,className:`h-7 w-7`,iconClassName:`w-3 h-3`}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Chat`,onClick:()=>{l(e.id),c(`chat`)},children:(0,q.jsx)(o,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Pair`,onClick:()=>m(e.id),children:(0,q.jsx)(_,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Rename`,onClick:()=>T(e),children:(0,q.jsx)(n,{className:`w-3 h-3`})}),L&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Terminal`,disabled:re,onClick:()=>void ie(),children:(0,q.jsx)(p,{className:`w-3 h-3`})}),(e.branchState===`changes`||e.branchState===`idle`)&&e.branchWorkspaceId&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-amber-400 hover:text-amber-300`,title:`Mark workspace ready — hand off to the auto-merge`,onClick:()=>E(`Mark Workspace Ready`,`Mark ${w(e)}'s branch ready to land? The relay auto-merge will rebase and land it.`,()=>A(e.branchWorkspaceId,`ready`)),children:(0,q.jsx)(t,{className:`w-3 h-3`})}),se&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Compact`,onClick:()=>E(`Compact Agent`,`Compact context for ${w(e)}?`,()=>D(e,`compact`,`agents-grid`)),children:(0,q.jsx)(i,{className:`w-3 h-3`})}),I&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Clear Context`,onClick:()=>E(`Clear Context`,`Clear context for ${w(e)}?`,()=>D(e,`clearContext`,`agents-grid`)),children:(0,q.jsx)(f,{className:`w-3 h-3`})}),F&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Shutdown`,onClick:()=>E(`Shutdown Agent`,`Shutdown ${w(e)}?`,()=>D(e,`shutdown`,`agents-grid`)),children:(0,q.jsx)(u,{className:`w-3 h-3`})}),V&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Forget stale agent`,onClick:()=>E(`Forget Agent`,`Forget ${w(e)}?`,()=>O(e.id)),children:(0,q.jsx)(g,{className:`w-3 h-3`})})]})]})}),N&&(0,q.jsx)(ge,{open:M,onOpenChange:ae,orchestratorId:N.orchestratorId,session:N.session,interactive:N.interactive||R})]})}function Qe(){let e=v(e=>e.agentPresetFilter),t=v(e=>e.agentStatusFilter),n=v(e=>e.agentTagFilter),i=v(e=>e.agentHostFilter),o=v(e=>e.agentSort),s=v(e=>e.agentSortDir),c=v(e=>e.showOffline),u=v(e=>e.set),d=v(e=>e.openAgentSpawn),f=v(e=>e.agentsById),p=ee(),h=W(),g=U(),_=V(),y=me(h,p),b=de(h,p),x=e||t||n||i;function S(){u({agentPresetFilter:``,agentStatusFilter:``,agentTagFilter:``,agentHostFilter:``})}function C(){u({agentSortDir:s===`asc`?`desc`:`asc`})}return(0,q.jsxs)(`div`,{className:`space-y-4`,children:[(0,q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,q.jsxs)(`select`,{value:e,onChange:e=>u({agentPresetFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`All agents`}),(0,q.jsx)(`option`,{value:`active`,children:`Active`}),(0,q.jsx)(`option`,{value:`claude`,children:`Claude`}),(0,q.jsx)(`option`,{value:`codex`,children:`Codex`}),(0,q.jsx)(`option`,{value:`paired`,children:`Paired`}),(0,q.jsx)(`option`,{value:`unpaired`,children:`Unpaired`}),(0,q.jsx)(`option`,{value:`offline_stale`,children:`Offline / Stale`})]}),(0,q.jsxs)(`select`,{value:t,onChange:e=>u({agentStatusFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any status`}),(0,q.jsx)(`option`,{value:`online`,children:`Online`}),(0,q.jsx)(`option`,{value:`idle`,children:`Idle`}),(0,q.jsx)(`option`,{value:`busy`,children:`Busy`}),(0,q.jsx)(`option`,{value:`offline`,children:`Offline`}),(0,q.jsx)(`option`,{value:`starting`,children:`Starting`})]}),g.length>0&&(0,q.jsxs)(`select`,{value:n,onChange:e=>u({agentTagFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any tag`}),g.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),_.length>1&&(0,q.jsxs)(`select`,{value:i,onChange:e=>u({agentHostFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any host`}),_.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),(0,q.jsxs)(`select`,{value:o,onChange:e=>u({agentSort:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:`status`,children:`Sort: Status`}),(0,q.jsx)(`option`,{value:`name`,children:`Sort: Name`}),(0,q.jsx)(`option`,{value:`lastSeen`,children:`Sort: Last seen`}),(0,q.jsx)(`option`,{value:`created`,children:`Sort: Created`})]}),(0,q.jsx)(B,{size:`icon`,variant:`outline`,className:`h-8 w-8`,title:`Toggle sort direction`,onClick:C,children:s===`asc`?(0,q.jsx)(r,{className:`w-3.5 h-3.5`}):(0,q.jsx)(l,{className:`w-3.5 h-3.5`})}),(0,q.jsxs)(`label`,{className:`flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer select-none`,children:[(0,q.jsx)(`input`,{type:`checkbox`,checked:c,onChange:e=>u({showOffline:e.target.checked}),className:`rounded`}),`Show offline`]}),x&&(0,q.jsxs)(B,{size:`sm`,variant:`ghost`,className:`h-8 text-muted-foreground hover:text-foreground gap-1`,onClick:S,children:[(0,q.jsx)(m,{className:`w-3 h-3`}),`Clear filters`]}),(0,q.jsx)(`div`,{className:`ml-auto`,children:(0,q.jsxs)(B,{size:`sm`,onClick:d,className:`gap-1`,children:[(0,q.jsx)(a,{className:`w-3.5 h-3.5`}),`Spawn`]})})]}),b.length===0?(0,q.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No agents match the current filters.`}):(0,q.jsx)(`div`,{className:`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3`,children:b.map(e=>(0,q.jsx)(Ze,{agent:e,parentAgent:e.spawnedBy?f[e.spawnedBy]??null:null,childAgents:_e(y.get(e.id)??[])},e.id))})]})}export{Qe as AgentsView};
|
|
2
|
-
//# sourceMappingURL=agents-DZBM5dp_.js.map
|