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.
Files changed (148) hide show
  1. package/docs/openapi.json +151 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-CHKzD4RY.js → MessageBubble-BrEKymoV.js} +2 -2
  4. package/public/assets/{MessageBubble-CHKzD4RY.js.map → MessageBubble-BrEKymoV.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BFVTJa0q.js → PlanGraphPanel-CXix1C-H.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BFVTJa0q.js.map → PlanGraphPanel-CXix1C-H.js.map} +1 -1
  7. package/public/assets/{activity-M-dFLU_O.js → activity-CRafQ1Je.js} +2 -2
  8. package/public/assets/{activity-M-dFLU_O.js.map → activity-CRafQ1Je.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CvHSoyCX.js → agent-profiles-DW4o5-ND.js} +2 -2
  10. package/public/assets/{agent-profiles-CvHSoyCX.js.map → agent-profiles-DW4o5-ND.js.map} +1 -1
  11. package/public/assets/agents-Cbegv0Hm.js +2 -0
  12. package/public/assets/{agents-DZBM5dp_.js.map → agents-Cbegv0Hm.js.map} +1 -1
  13. package/public/assets/analytics-DX6T7JX3.js +910 -0
  14. package/public/assets/{analytics-bvjb3Urs.js.map → analytics-DX6T7JX3.js.map} +1 -1
  15. package/public/assets/{automation-CDSn9Uq-.js → automation-CzSBv4gg.js} +2 -2
  16. package/public/assets/{automation-CDSn9Uq-.js.map → automation-CzSBv4gg.js.map} +1 -1
  17. package/public/assets/{badge-Dj6PREuI.js → badge-Cy6qO_Jz.js} +2 -2
  18. package/public/assets/{badge-Dj6PREuI.js.map → badge-Cy6qO_Jz.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-CdYg2HYc.js → branch-state-badge-CRA50r81.js} +2 -2
  20. package/public/assets/{branch-state-badge-CdYg2HYc.js.map → branch-state-badge-CRA50r81.js.map} +1 -1
  21. package/public/assets/{button-Cqpufqam.js → button-73wpRw65.js} +2 -2
  22. package/public/assets/{button-Cqpufqam.js.map → button-73wpRw65.js.map} +1 -1
  23. package/public/assets/{card-OmDF73CE.js → card-BU6PvVHv.js} +2 -2
  24. package/public/assets/{card-OmDF73CE.js.map → card-BU6PvVHv.js.map} +1 -1
  25. package/public/assets/{channels-BkxrdOFZ.js → channels-B1MQCPCT.js} +2 -2
  26. package/public/assets/{channels-BkxrdOFZ.js.map → channels-B1MQCPCT.js.map} +1 -1
  27. package/public/assets/chat-BqIaFYGJ.js +2 -0
  28. package/public/assets/{chat-YzX1z76l.js.map → chat-BqIaFYGJ.js.map} +1 -1
  29. package/public/assets/{connectors-Bg3rHgl_.js → connectors-C9aFJIrh.js} +2 -2
  30. package/public/assets/{connectors-Bg3rHgl_.js.map → connectors-C9aFJIrh.js.map} +1 -1
  31. package/public/assets/{copy-button-DI7b8A7_.js → copy-button-HRopUwui.js} +2 -2
  32. package/public/assets/{copy-button-DI7b8A7_.js.map → copy-button-HRopUwui.js.map} +1 -1
  33. package/public/assets/display-CketawEF.js.map +1 -1
  34. package/public/assets/{dist-CNjnTZEI.js → dist-CC5f7oYw.js} +2 -2
  35. package/public/assets/{dist-CNjnTZEI.js.map → dist-CC5f7oYw.js.map} +1 -1
  36. package/public/assets/{dist-DkeTrbTA.js → dist-CLHMeXYi.js} +2 -2
  37. package/public/assets/{dist-DkeTrbTA.js.map → dist-CLHMeXYi.js.map} +1 -1
  38. package/public/assets/{dist-C5t_OxI0.js → dist-Ci1MM51P.js} +2 -2
  39. package/public/assets/{dist-C5t_OxI0.js.map → dist-Ci1MM51P.js.map} +1 -1
  40. package/public/assets/{dist-CV3sgFiv.js → dist-Cko2KPW8.js} +2 -2
  41. package/public/assets/{dist-CV3sgFiv.js.map → dist-Cko2KPW8.js.map} +1 -1
  42. package/public/assets/dist-D41tzJYg.js +2 -0
  43. package/public/assets/{dist-yYauqYKR.js.map → dist-D41tzJYg.js.map} +1 -1
  44. package/public/assets/{dist-CoUvwLTy.js → dist-DFLciw0h.js} +2 -2
  45. package/public/assets/{dist-CoUvwLTy.js.map → dist-DFLciw0h.js.map} +1 -1
  46. package/public/assets/dist-DUoZnlB2.js +2 -0
  47. package/public/assets/{dist-DmhyU0Na.js.map → dist-DUoZnlB2.js.map} +1 -1
  48. package/public/assets/dist-De2oguBm.js +2 -0
  49. package/public/assets/{dist-DxkMwkrr.js.map → dist-De2oguBm.js.map} +1 -1
  50. package/public/assets/{dist-1x-LE9Uk.js → dist-DmN94rc8.js} +2 -2
  51. package/public/assets/{dist-1x-LE9Uk.js.map → dist-DmN94rc8.js.map} +1 -1
  52. package/public/assets/{dist-DV2tZ2fq.js → dist-Kkpnjx_M.js} +2 -2
  53. package/public/assets/{dist-DV2tZ2fq.js.map → dist-Kkpnjx_M.js.map} +1 -1
  54. package/public/assets/{dist-Ca3ujqG_.js → dist-T7u7IZGJ.js} +2 -2
  55. package/public/assets/{dist-Ca3ujqG_.js.map → dist-T7u7IZGJ.js.map} +1 -1
  56. package/public/assets/{es2015-DcavLcQ0.js → es2015-Df08Opjx.js} +2 -2
  57. package/public/assets/{es2015-DcavLcQ0.js.map → es2015-Df08Opjx.js.map} +1 -1
  58. package/public/assets/{formatted-body-impl-D8i9qqL1.js → formatted-body-impl-DOXH7oDF.js} +2 -2
  59. package/public/assets/{formatted-body-impl-D8i9qqL1.js.map → formatted-body-impl-DOXH7oDF.js.map} +1 -1
  60. package/public/assets/index-DaNOQJci.css +2 -0
  61. package/public/assets/index-br_Y6Cvt.js +21 -0
  62. package/public/assets/{index-Ba2RDJaA.js.map → index-br_Y6Cvt.js.map} +1 -1
  63. package/public/assets/{input-C_dl_9fG.js → input-B2EDz-BE.js} +2 -2
  64. package/public/assets/{input-C_dl_9fG.js.map → input-B2EDz-BE.js.map} +1 -1
  65. package/public/assets/{insights-D1aANwZ2.js → insights-B1edq5lk.js} +2 -2
  66. package/public/assets/{insights-D1aANwZ2.js.map → insights-B1edq5lk.js.map} +1 -1
  67. package/public/assets/{integrations-BrAr9bap.js → integrations-Cm4fjp7_.js} +2 -2
  68. package/public/assets/{integrations-BrAr9bap.js.map → integrations-Cm4fjp7_.js.map} +1 -1
  69. package/public/assets/{lib-B3iMVsf-.js → lib-BZrjIp-o.js} +3 -3
  70. package/public/assets/{lib-B3iMVsf-.js.map → lib-BZrjIp-o.js.map} +1 -1
  71. package/public/assets/lucide-react-H_-8G79A.js +9 -0
  72. package/public/assets/{lucide-react-CMhBract.js.map → lucide-react-H_-8G79A.js.map} +1 -1
  73. package/public/assets/{maintenance-C-xspsMG.js → maintenance-D6zvw0fU.js} +2 -2
  74. package/public/assets/{maintenance-C-xspsMG.js.map → maintenance-D6zvw0fU.js.map} +1 -1
  75. package/public/assets/{managed-agents-Bf3ni0vo.js → managed-agents-CzfEznM1.js} +2 -2
  76. package/public/assets/{managed-agents-Bf3ni0vo.js.map → managed-agents-CzfEznM1.js.map} +1 -1
  77. package/public/assets/{markdown-preview-impl-DKbUBTf_.js → markdown-preview-impl-DZlCaTHQ.js} +2 -2
  78. package/public/assets/{markdown-preview-impl-DKbUBTf_.js.map → markdown-preview-impl-DZlCaTHQ.js.map} +1 -1
  79. package/public/assets/memory-CUUAzJDc.js +2 -0
  80. package/public/assets/{memory-DeLwJFwH.js.map → memory-CUUAzJDc.js.map} +1 -1
  81. package/public/assets/messages-6QZH46LG.js +2 -0
  82. package/public/assets/{messages-Cww6dZnM.js.map → messages-6QZH46LG.js.map} +1 -1
  83. package/public/assets/{orchestrators-DRjwROXw.js → orchestrators-CrcqkPuc.js} +2 -2
  84. package/public/assets/{orchestrators-DRjwROXw.js.map → orchestrators-CrcqkPuc.js.map} +1 -1
  85. package/public/assets/overview-418xXZO_.js +2 -0
  86. package/public/assets/{overview-DjcRomU_.js.map → overview-418xXZO_.js.map} +1 -1
  87. package/public/assets/pairs-CnedATgT.js +2 -0
  88. package/public/assets/{pairs-DT0u2fnW.js.map → pairs-CnedATgT.js.map} +1 -1
  89. package/public/assets/{react-dom-Dtl9-x_1.js → react-dom-WI6TjVe7.js} +2 -2
  90. package/public/assets/{react-dom-Dtl9-x_1.js.map → react-dom-WI6TjVe7.js.map} +1 -1
  91. package/public/assets/{security-Bwg3oFI8.js → security-DS13pbag.js} +2 -2
  92. package/public/assets/{security-Bwg3oFI8.js.map → security-DS13pbag.js.map} +1 -1
  93. package/public/assets/{select-Cariwv69.js → select-BOprGZDS.js} +2 -2
  94. package/public/assets/{select-Cariwv69.js.map → select-BOprGZDS.js.map} +1 -1
  95. package/public/assets/{settings-Bd6Ug3DO.js → settings-BgInn_f3.js} +4 -4
  96. package/public/assets/{settings-Bd6Ug3DO.js.map → settings-BgInn_f3.js.map} +1 -1
  97. package/public/assets/{store-BgJq_lN9.js → store-DHO_a8BD.js} +5 -5
  98. package/public/assets/{store-BgJq_lN9.js.map → store-DHO_a8BD.js.map} +1 -1
  99. package/public/assets/{tasks-DVz1zhOU.js → tasks-B-4oCPuv.js} +2 -2
  100. package/public/assets/{tasks-DVz1zhOU.js.map → tasks-B-4oCPuv.js.map} +1 -1
  101. package/public/assets/teams-sCQAs_QA.js +2 -0
  102. package/public/assets/{teams-TqFntFB_.js.map → teams-sCQAs_QA.js.map} +1 -1
  103. package/public/assets/{terminal-viewer-impl-OsJ5crlx.js → terminal-viewer-impl-BhqS2XpS.js} +3 -3
  104. package/public/assets/{terminal-viewer-impl-OsJ5crlx.js.map → terminal-viewer-impl-BhqS2XpS.js.map} +1 -1
  105. package/public/assets/{use-keyboard-viewport-BYHcL7sP.js → use-keyboard-viewport-fOL_3RHB.js} +2 -2
  106. package/public/assets/{use-keyboard-viewport-BYHcL7sP.js.map → use-keyboard-viewport-fOL_3RHB.js.map} +1 -1
  107. package/public/assets/{work-queue-B4b2jPLR.js → work-queue-BDhjlwUH.js} +2 -2
  108. package/public/assets/{work-queue-B4b2jPLR.js.map → work-queue-BDhjlwUH.js.map} +1 -1
  109. package/public/assets/workspaces-6I-C69nf.js +3 -0
  110. package/public/assets/{workspaces-m97LP4Eu.js.map → workspaces-6I-C69nf.js.map} +1 -1
  111. package/public/index.html +21 -21
  112. package/src/db/index.ts +2 -0
  113. package/src/db/project-entries.ts +270 -0
  114. package/src/db/projects.ts +304 -0
  115. package/src/db/schema.ts +50 -0
  116. package/src/db/stats.ts +209 -38
  117. package/src/db/teams.ts +13 -2
  118. package/src/maintenance.ts +9 -2
  119. package/src/mcp-project-tools.ts +352 -0
  120. package/src/mcp.ts +6 -0
  121. package/src/project-ingest.ts +186 -0
  122. package/src/project-instructions.ts +176 -0
  123. package/src/routes/agents-spawn.ts +2 -0
  124. package/src/routes/commands.ts +11 -5
  125. package/src/routes/index.ts +5 -0
  126. package/src/routes/projects.ts +55 -0
  127. package/src/runtime-tokens.ts +6 -0
  128. package/src/security.ts +4 -0
  129. package/src/services/spawn-agent.ts +33 -2
  130. package/src/services/team.ts +5 -1
  131. package/src/token-db.ts +3 -3
  132. package/src/workspace-merge.ts +11 -0
  133. package/src/workspace-pr-completion.ts +57 -1
  134. package/public/assets/agents-DZBM5dp_.js +0 -2
  135. package/public/assets/analytics-bvjb3Urs.js +0 -910
  136. package/public/assets/chat-YzX1z76l.js +0 -2
  137. package/public/assets/dist-DmhyU0Na.js +0 -2
  138. package/public/assets/dist-DxkMwkrr.js +0 -2
  139. package/public/assets/dist-yYauqYKR.js +0 -2
  140. package/public/assets/index-Ba2RDJaA.js +0 -21
  141. package/public/assets/index-CtFXWxBJ.css +0 -2
  142. package/public/assets/lucide-react-CMhBract.js +0 -9
  143. package/public/assets/memory-DeLwJFwH.js +0 -2
  144. package/public/assets/messages-Cww6dZnM.js +0 -2
  145. package/public/assets/overview-DjcRomU_.js +0 -2
  146. package/public/assets/pairs-DT0u2fnW.js +0 -2
  147. package/public/assets/teams-TqFntFB_.js +0 -2
  148. 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 }),
@@ -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). Only on a
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
- if (command.result.merged === true && landedWorkspace) {
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: cleanString(command.result.subject, "result.subject", { max: 200 }),
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
  }
@@ -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
+ };
@@ -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: input.systemPromptAppend,
272
+ systemPromptAppend,
242
273
  ...(expectReply ? { expectReply } : {}),
243
274
  policyName: input.policyName,
244
275
  spawnRequestId,
@@ -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,
@@ -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