@saadnvd1/agent-os 0.3.700 → 0.3.723

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 (306) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/groups/route.ts +44 -0
  11. package/app/api/bus/spawn/route.ts +34 -2
  12. package/app/api/claims/[id]/route.ts +20 -0
  13. package/app/api/claims/check/route.ts +23 -0
  14. package/app/api/claims/check.test.ts +113 -0
  15. package/app/api/claims/exchange/route.ts +42 -0
  16. package/app/api/claims/outside.test.ts +90 -0
  17. package/app/api/claims/route.test.ts +193 -0
  18. package/app/api/claims/route.ts +73 -0
  19. package/app/api/hosts/[id]/route.ts +9 -1
  20. package/app/api/models/availability/route.ts +9 -0
  21. package/app/api/models/choices/route.ts +9 -0
  22. package/app/api/peer/unlink/route.ts +17 -0
  23. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  24. package/app/api/peers/[hostId]/route.ts +20 -0
  25. package/app/api/peers/gate.test.ts +64 -1
  26. package/app/api/peers/pending/route.test.ts +37 -0
  27. package/app/api/peers/pending/route.ts +11 -4
  28. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  29. package/app/api/session-groups/[id]/route.ts +25 -0
  30. package/app/api/session-groups/order/route.ts +15 -0
  31. package/app/api/session-groups/route.ts +21 -0
  32. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  33. package/app/api/sessions/[id]/crew/route.ts +38 -0
  34. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  35. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  36. package/app/api/sessions/[id]/export/route.ts +34 -0
  37. package/app/api/sessions/[id]/fork/route.ts +3 -0
  38. package/app/api/sessions/[id]/group/route.ts +18 -0
  39. package/app/api/sessions/[id]/model/route.ts +49 -0
  40. package/app/api/sessions/[id]/route.ts +14 -4
  41. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  42. package/app/api/sessions/claims.test.ts +122 -0
  43. package/app/api/sessions/group/route.ts +17 -0
  44. package/app/api/sessions/route.ts +31 -14
  45. package/app/page.tsx +10 -0
  46. package/components/Chat/Messages.tsx +104 -10
  47. package/components/Claims/ClaimFiles.tsx +38 -0
  48. package/components/Claims/ClaimRow.tsx +82 -0
  49. package/components/Claims/ClaimsSheet.tsx +176 -0
  50. package/components/Claims/OverlapPill.tsx +31 -0
  51. package/components/Claims/StartClaimsNotice.tsx +70 -0
  52. package/components/Claims/claims-ui.test.ts +161 -0
  53. package/components/Claims/format.ts +57 -0
  54. package/components/Claims/index.ts +3 -0
  55. package/components/Draft/DraftChips.tsx +6 -1
  56. package/components/Draft/DraftPanel.tsx +90 -3
  57. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  58. package/components/Draft/RoleChips.tsx +110 -0
  59. package/components/Export/ExportActions.tsx +136 -0
  60. package/components/Export/ExportSheet.tsx +217 -0
  61. package/components/Export/RangePicker.tsx +144 -0
  62. package/components/Export/Seg.tsx +38 -0
  63. package/components/Export/TurnSelectBar.tsx +45 -0
  64. package/components/Hosts/AccountMachines.tsx +32 -18
  65. package/components/Hosts/HostRow.tsx +155 -14
  66. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  67. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  68. package/components/Models/AgentRoleCard.tsx +114 -0
  69. package/components/Models/AgentRolesSection.tsx +132 -0
  70. package/components/Models/ModelBadges.tsx +48 -0
  71. package/components/Models/ModelsSection.tsx +4 -2
  72. package/components/Models/RoleModelSelect.tsx +87 -0
  73. package/components/Pane/DesktopTabBar.tsx +2 -0
  74. package/components/Pane/MobileTabBar.tsx +12 -0
  75. package/components/Pane/PaneMenu.tsx +10 -0
  76. package/components/SessionList/GroupShelves.tsx +195 -0
  77. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  78. package/components/SessionList/RowSubtitle.tsx +7 -1
  79. package/components/SessionList/SelectionToolbar.tsx +52 -1
  80. package/components/SessionList/SessionRow.tsx +19 -7
  81. package/components/SessionList/SessionRowMenu.tsx +23 -0
  82. package/components/SessionList/SessionShelves.tsx +5 -0
  83. package/components/SessionList/SidebarBody.tsx +6 -0
  84. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  85. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  86. package/components/SessionList/SidebarHeader.tsx +5 -0
  87. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  88. package/components/SessionList/index.tsx +18 -14
  89. package/components/SessionList/useSidebarData.ts +10 -0
  90. package/components/Sessions/RolePill.tsx +196 -0
  91. package/components/Settings/SettingsDialog.tsx +9 -0
  92. package/components/Share/ShareDialog.tsx +13 -0
  93. package/components/ui/notice.tsx +46 -0
  94. package/components/ui/status-pill.tsx +39 -0
  95. package/data/analyst/index.ts +3 -0
  96. package/data/analyst/keys.ts +6 -0
  97. package/data/analyst/mutations.ts +116 -0
  98. package/data/analyst/queries.ts +35 -0
  99. package/data/claims/index.ts +2 -0
  100. package/data/claims/keys.ts +5 -0
  101. package/data/claims/queries.ts +47 -0
  102. package/data/exports/index.ts +2 -0
  103. package/data/exports/keys.ts +3 -0
  104. package/data/exports/queries.ts +51 -0
  105. package/data/models/index.ts +98 -0
  106. package/data/peers/keys.ts +1 -0
  107. package/data/peers/queries.ts +31 -0
  108. package/data/push/topics.ts +9 -0
  109. package/data/session-groups/index.ts +156 -0
  110. package/data/sessions/index.ts +0 -1
  111. package/data/sessions/pin.ts +2 -6
  112. package/data/sessions/queries.ts +36 -29
  113. package/data/sessions/shelf.ts +2 -6
  114. package/data/tasks/index.ts +2 -0
  115. package/hooks/useSessions.ts +0 -12
  116. package/lib/__fixtures__/peer-rpc.ts +65 -33
  117. package/lib/agents/brief.ts +1 -0
  118. package/lib/agents/spawn.ts +4 -0
  119. package/lib/analyst/__fixtures__/items.ts +57 -0
  120. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  121. package/lib/analyst/brief.ts +17 -0
  122. package/lib/analyst/buildDigest.ts +204 -0
  123. package/lib/analyst/createLink.ts +98 -0
  124. package/lib/analyst/endLink.ts +31 -0
  125. package/lib/analyst/forward.ts +109 -0
  126. package/lib/analyst/index.ts +16 -0
  127. package/lib/analyst/linkView.ts +38 -0
  128. package/lib/analyst/onTurnEnd.ts +119 -0
  129. package/lib/analyst/parseReview.ts +41 -0
  130. package/lib/analyst/pushDigest.ts +143 -0
  131. package/lib/analyst/resume.ts +35 -0
  132. package/lib/analyst/route.ts +50 -0
  133. package/lib/analyst/signals.ts +121 -0
  134. package/lib/analyst/store.ts +303 -0
  135. package/lib/analyst/types.ts +140 -0
  136. package/lib/bus/condense.ts +5 -1
  137. package/lib/bus/index.ts +13 -0
  138. package/lib/bus/read-scope.ts +12 -0
  139. package/lib/bus/role-alias.ts +70 -0
  140. package/lib/bus/targets.ts +3 -0
  141. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  142. package/lib/chat/drivers/pi-approvals.ts +7 -4
  143. package/lib/chat/drivers/pi-mapper.ts +38 -0
  144. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  145. package/lib/chat/drivers/pi.ts +8 -3
  146. package/lib/chat/drivers/turn-items.ts +36 -11
  147. package/lib/chat/drivers/turn-outcome.ts +108 -0
  148. package/lib/chat/events.ts +8 -0
  149. package/lib/chat/registry.ts +13 -0
  150. package/lib/chat/store.ts +19 -0
  151. package/lib/chat/turns.ts +1 -0
  152. package/lib/chat/worker/extras.ts +4 -2
  153. package/lib/claims/collectLocal.ts +123 -0
  154. package/lib/claims/describe.ts +41 -0
  155. package/lib/claims/exchange.ts +91 -0
  156. package/lib/claims/goal.ts +45 -0
  157. package/lib/claims/index.ts +10 -0
  158. package/lib/claims/loop.ts +51 -0
  159. package/lib/claims/machine.ts +21 -0
  160. package/lib/claims/mine.ts +43 -0
  161. package/lib/claims/notifyOverlaps.ts +101 -0
  162. package/lib/claims/onMerged.ts +85 -0
  163. package/lib/claims/outside.ts +36 -0
  164. package/lib/claims/overlap.ts +87 -0
  165. package/lib/claims/overlapCache.ts +86 -0
  166. package/lib/claims/overlapText.ts +36 -0
  167. package/lib/claims/peers.ts +56 -0
  168. package/lib/claims/startCheck.ts +100 -0
  169. package/lib/claims/startWarning.ts +29 -0
  170. package/lib/claims/store.ts +366 -0
  171. package/lib/claims/types.ts +102 -0
  172. package/lib/claims/view.ts +80 -0
  173. package/lib/claims/yield.ts +24 -0
  174. package/lib/db/changes.ts +12 -0
  175. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  176. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  177. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  178. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  179. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  180. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  181. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  182. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  183. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  184. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  185. package/lib/db/queries.ts +0 -40
  186. package/lib/db/session-parts/columns.ts +3 -0
  187. package/lib/db/types.ts +19 -8
  188. package/lib/doctor/checks.ts +64 -1
  189. package/lib/done/archive.ts +2 -0
  190. package/lib/drafts.ts +4 -0
  191. package/lib/export/build.ts +470 -0
  192. package/lib/export/compact.ts +156 -0
  193. package/lib/export/estimate.ts +83 -0
  194. package/lib/export/fixtures.ts +45 -0
  195. package/lib/export/html.tsx +195 -0
  196. package/lib/export/json.ts +39 -0
  197. package/lib/export/load.ts +64 -0
  198. package/lib/export/markdown.ts +127 -0
  199. package/lib/export/request.ts +36 -0
  200. package/lib/export/service.ts +142 -0
  201. package/lib/export/summary.ts +86 -0
  202. package/lib/export/turns.ts +63 -0
  203. package/lib/export/view.ts +173 -0
  204. package/lib/git-remote.ts +43 -0
  205. package/lib/hosts/index.ts +10 -1
  206. package/lib/models/agent-binding.ts +102 -0
  207. package/lib/models/agent-roles-view.ts +157 -0
  208. package/lib/models/agent-roles.ts +87 -0
  209. package/lib/models/availability-refs.ts +17 -0
  210. package/lib/models/availability.ts +91 -0
  211. package/lib/models/catalog.ts +156 -0
  212. package/lib/models/config.ts +27 -0
  213. package/lib/models/locality.ts +33 -0
  214. package/lib/models/providers.ts +11 -0
  215. package/lib/models/resolve.ts +50 -12
  216. package/lib/models/roles.ts +12 -0
  217. package/lib/needs-you.ts +1 -1
  218. package/lib/orchestration.ts +2 -0
  219. package/lib/orchestrator/act.ts +54 -4
  220. package/lib/orchestrator/brief.ts +2 -1
  221. package/lib/orchestrator/claim-wait.ts +71 -0
  222. package/lib/orchestrator/conditions.ts +28 -1
  223. package/lib/orchestrator/describe.ts +6 -1
  224. package/lib/orchestrator/facts.ts +12 -2
  225. package/lib/orchestrator/read.ts +5 -1
  226. package/lib/orchestrator/serve.ts +37 -2
  227. package/lib/orchestrator/tool-names.ts +1 -0
  228. package/lib/orchestrator/tool-schemas.ts +31 -1
  229. package/lib/orchestrator/tools.ts +3 -1
  230. package/lib/orchestrator/untrusted.ts +5 -0
  231. package/lib/orchestrator/watcher.ts +19 -1
  232. package/lib/peer/account.ts +74 -2
  233. package/lib/peer/allowlist.ts +1 -1
  234. package/lib/peer/client.ts +26 -1
  235. package/lib/peer/link-cli.ts +19 -4
  236. package/lib/peer/links.ts +117 -5
  237. package/lib/peer/machines.ts +14 -2
  238. package/lib/peer/name.ts +32 -0
  239. package/lib/peer/pairing.ts +34 -5
  240. package/lib/peer/pin.ts +10 -3
  241. package/lib/peer/reasons.ts +7 -0
  242. package/lib/peer/revoke.ts +75 -0
  243. package/lib/peer/rpc-server.ts +53 -5
  244. package/lib/peer/unlink.ts +62 -0
  245. package/lib/peer/unlinked-proof.ts +44 -0
  246. package/lib/projects.ts +6 -2
  247. package/lib/security/demo.ts +7 -0
  248. package/lib/session-artifacts/agentos-files.ts +94 -0
  249. package/lib/session-artifacts/collect.ts +17 -0
  250. package/lib/session-groups/assignSessions.ts +81 -0
  251. package/lib/session-groups/cli.ts +61 -0
  252. package/lib/session-groups/createGroup.ts +28 -0
  253. package/lib/session-groups/index.ts +21 -0
  254. package/lib/session-groups/removeGroup.ts +36 -0
  255. package/lib/session-groups/reorderGroups.ts +31 -0
  256. package/lib/session-groups/resolveGroup.ts +19 -0
  257. package/lib/session-groups/route.ts +32 -0
  258. package/lib/session-groups/store.ts +85 -0
  259. package/lib/session-groups/types.ts +25 -0
  260. package/lib/session-groups/updateGroup.ts +35 -0
  261. package/lib/session-groups/workspace.ts +21 -0
  262. package/lib/sessions/crew.ts +72 -0
  263. package/lib/sessions/launch.ts +54 -11
  264. package/lib/settings-url.ts +1 -0
  265. package/lib/share/markdown.ts +2 -2
  266. package/lib/share/redact.ts +16 -10
  267. package/lib/share/render.tsx +1 -1
  268. package/lib/share/snapshot.ts +2 -2
  269. package/lib/sidebar/rollup.ts +1 -0
  270. package/lib/sidebar/row-menu.ts +8 -1
  271. package/lib/sidebar/shelves.ts +90 -15
  272. package/lib/sidebar/visible-order.ts +5 -0
  273. package/lib/tasks/hold-checks.ts +3 -1
  274. package/lib/tasks/holds.ts +3 -0
  275. package/lib/tasks/index.ts +11 -0
  276. package/lib/tasks/queue-rows.ts +8 -1
  277. package/lib/tasks/queue-start.ts +1 -0
  278. package/lib/tasks/queue-types.ts +9 -1
  279. package/lib/tasks/queue.ts +15 -5
  280. package/lib/tasks/schedule-types.ts +1 -0
  281. package/lib/test-temp.ts +19 -3
  282. package/lib/workspaces.ts +11 -3
  283. package/lib/worktree-deps/caches.ts +3 -2
  284. package/lib/worktree-deps/clone.ts +3 -1
  285. package/lib/worktree-deps/paths.ts +26 -0
  286. package/lib/worktree-sweep/testTemp.ts +23 -106
  287. package/package.json +1 -1
  288. package/scripts/agent-os +1 -1
  289. package/scripts/autodeploy +2 -0
  290. package/scripts/autodeploy.test.ts +31 -1
  291. package/scripts/claim-hook.sh +31 -0
  292. package/scripts/claim-hook.test.ts +230 -0
  293. package/scripts/lib/cli-links.sh +36 -0
  294. package/scripts/lib/commands.sh +3 -0
  295. package/scripts/lib/machine.sh +27 -2
  296. package/scripts/lib/machine.test.ts +25 -3
  297. package/scripts/sync-cli-links +7 -0
  298. package/scripts/sync-cli-links.test.ts +69 -0
  299. package/server.ts +12 -1
  300. package/stores/claimsUi.ts +22 -0
  301. package/stores/exportUi.ts +87 -0
  302. package/app/api/groups/[...path]/route.ts +0 -136
  303. package/app/api/groups/route.ts +0 -93
  304. package/data/groups/index.ts +0 -1
  305. package/data/groups/mutations.ts +0 -95
  306. package/hooks/useGroups.ts +0 -37
package/README.md CHANGED
@@ -28,9 +28,11 @@ and check on all of it from your phone.
28
28
  - **Project config.** `agentos.json` gives each session its own ports, environment, files and a private Postgres copy. [More](docs/reference/project-config.md)
29
29
  - **Schedules and recipes.** Start work on a timer, or save a job that comes up again. [More](docs/guides/schedules.md)
30
30
  - **Plans in pieces and stacks.** A plan splits into several PRs only for a real reason; a board can run as stacked PRs. [More](docs/guides/tasks.md#plans-in-pieces)
31
+ - **Export a session for AI.** A whole chat or a range of its turns as Markdown, JSON or HTML, with a compact mode, the same masking as Share, a size and token estimate, and every cut labelled. [More](docs/guides/export.md)
31
32
  - **Machine load and status.** A load gauge, each session's share of CPU and memory, and Needs you, Working, In review and Done as it changes. [More](docs/guides/sessions.md)
32
33
  - **A health watcher.** Zero-token checks on the orchestrator's tick catch a red machine load, a held queue, a quiet background job, a restart loop, slow CI, a PR armed for hours and a piece started twice. Each trip wakes one short, tool-less Sonnet call that judges it and posts one line to the orchestrator; only a thing you alone can clear reaches your phone. Settings → Health lists what is open, the history, the notes on what normal looks like and the thresholds. Report only: it never acts. [More](docs/reference/health.md)
33
34
  - **Workspaces.** Group projects, finish work with Done, and get a log of every merged task. [More](docs/reference/workspaces.md)
35
+ - **Session groups.** Named shelves inside a workspace, under Pinned, for sessions that belong together; a group stays put while its sessions change state. [More](docs/guides/sessions.md#groups)
34
36
  - **LumifyHub (optional).** Boards, cards and docs for a workspace, if you want them. [More](docs/lumifyhub.md)
35
37
  - **No approvals mode.** One switch in Settings that approves every ask, lets brakes and plan-only tasks through, and starts every agent with its permission prompts off; each approval it makes is logged. Off by default. [More](docs/reference/orchestrator.md#no-approvals-mode)
36
38
  - **Experiments.** Settings → Experiments lists features still being tried, each off until you turn it on, with what it has cost and how often its suggestions were taken. The first is Learning cards: when a session ends it can offer to save the work as a recipe or keep a lesson where it belongs. [More](docs/decisions/2026-10-13-experiments.md)
@@ -0,0 +1,67 @@
1
+ import { NextRequest } from "next/server";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { db } from "@/lib/db";
4
+ import { GET, PUT } from "./route";
5
+ import { TRUST_HEADER } from "@/lib/security/auth";
6
+
7
+ const url = "http://localhost/api/agent-roles";
8
+ const put = (body: unknown, trusted = true) =>
9
+ PUT(
10
+ new NextRequest(url, {
11
+ method: "PUT",
12
+ body: JSON.stringify(body),
13
+ headers: trusted ? { [TRUST_HEADER]: "loopback" } : {},
14
+ })
15
+ );
16
+ const get = (q = "") => GET(new NextRequest(`${url}${q}`));
17
+
18
+ beforeEach(() => {
19
+ db.prepare(`DELETE FROM settings WHERE key LIKE 'models%'`).run();
20
+ });
21
+
22
+ describe("/api/agent-roles", () => {
23
+ it("lists the four roles with the model in force", async () => {
24
+ const { roles } = await (await get()).json();
25
+ expect(roles.map((r: { id: string }) => r.id)).toEqual([
26
+ "developer",
27
+ "analyst",
28
+ "reviewer",
29
+ "supervisor",
30
+ ]);
31
+ expect(roles[0].binding).toMatchObject({ source: "default" });
32
+ });
33
+
34
+ it("saves a role and shows it back", async () => {
35
+ const res = await put({
36
+ scope: "settings",
37
+ roles: { analyze: "anthropic/claude-sonnet-5-5" },
38
+ });
39
+ expect(res.status).toBe(200);
40
+ const { roles } = await res.json();
41
+ expect(roles.find((r: { id: string }) => r.id === "analyst")).toMatchObject(
42
+ {
43
+ value: "anthropic/claude-sonnet-5-5",
44
+ binding: { source: "settings" },
45
+ }
46
+ );
47
+ });
48
+
49
+ it("refuses a request that isn't from this machine or the tailnet", async () => {
50
+ expect((await put({ scope: "settings", roles: {} }, false)).status).toBe(
51
+ 403
52
+ );
53
+ });
54
+
55
+ it("refuses a Pi model for a role that can't run it", async () => {
56
+ const res = await put({
57
+ scope: "settings",
58
+ roles: { orchestrator: "pi/local/qwen3-27b" },
59
+ });
60
+ expect(res.status).toBe(400);
61
+ });
62
+
63
+ it("404s an unknown project and 400s a bad scope", async () => {
64
+ expect((await get("?scope=project&id=nope")).status).toBe(404);
65
+ expect((await get("?scope=galaxy")).status).toBe(400);
66
+ });
67
+ });
@@ -0,0 +1,61 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { agentRolesView, saveAgentRoles } from "@/lib/models/agent-roles-view";
4
+ import { roleMapSchema } from "@/lib/models/config";
5
+ import { getProject } from "@/lib/projects";
6
+ import { getWorkspace } from "@/lib/workspaces";
7
+ import { requireLocalTrust } from "@/lib/security/route-guard";
8
+
9
+ const scopeQuery = z.object({
10
+ scope: z.enum(["settings", "workspace", "project"]).default("settings"),
11
+ id: z.string().min(1).optional(),
12
+ });
13
+
14
+ function known({ scope, id }: z.infer<typeof scopeQuery>): string | null {
15
+ if (scope === "settings") return null;
16
+ if (!id) return `${scope} needs an id`;
17
+ if (scope === "workspace" && !getWorkspace(id)) return "Workspace not found";
18
+ if (scope === "project" && !getProject(id)) return "Project not found";
19
+ return null;
20
+ }
21
+
22
+ // GET /api/agent-roles?scope=settings|workspace|project&id= - the four
23
+ // agent roles and the internal ones: what's saved at the scope, the model in
24
+ // force and where it came from, and the model's locality and availability.
25
+ export async function GET(request: NextRequest) {
26
+ const q = scopeQuery.safeParse(
27
+ Object.fromEntries(request.nextUrl.searchParams)
28
+ );
29
+ if (!q.success)
30
+ return NextResponse.json({ error: "Bad scope" }, { status: 400 });
31
+ const missing = known(q.data);
32
+ if (missing) return NextResponse.json({ error: missing }, { status: 404 });
33
+ return NextResponse.json(agentRolesView(q.data));
34
+ }
35
+
36
+ const body = scopeQuery.extend({
37
+ roles: roleMapSchema,
38
+ fallbacks: roleMapSchema.optional(),
39
+ });
40
+
41
+ // PUT /api/agent-roles {scope, id?, roles, fallbacks?} - replaces the roles
42
+ // saved at the scope. Only from this machine or the tailnet, like Settings.
43
+ export async function PUT(request: NextRequest) {
44
+ const refused = requireLocalTrust(request);
45
+ if (refused) return refused;
46
+ try {
47
+ const b = body.parse(await request.json());
48
+ const missing = known(b);
49
+ if (missing) return NextResponse.json({ error: missing }, { status: 404 });
50
+ saveAgentRoles(b, b.roles, b.fallbacks);
51
+ return NextResponse.json(agentRolesView(b));
52
+ } catch (error) {
53
+ const message =
54
+ error instanceof z.ZodError
55
+ ? error.issues.map((i) => i.message).join("; ")
56
+ : error instanceof Error
57
+ ? error.message
58
+ : String(error);
59
+ return NextResponse.json({ error: message }, { status: 400 });
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { askAgain } from "@/lib/analyst";
3
+ import { analystRoute } from "@/lib/analyst/route";
4
+
5
+ type Ctx = { params: Promise<{ id: string; recId: string }> };
6
+
7
+ // POST /api/analyst/:id/recommendations/:recId/again - Ask again, for a
8
+ // reply that carried no valid recommendation.
9
+ export async function POST(request: Request, { params }: Ctx) {
10
+ const { id, recId } = await params;
11
+ return analystRoute(request, z.object({}), async () => {
12
+ await askAgain(id, recId);
13
+ return { ok: true };
14
+ });
15
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { dismissRecommendation } from "@/lib/analyst";
3
+ import { analystRoute } from "@/lib/analyst/route";
4
+
5
+ type Ctx = { params: Promise<{ id: string; recId: string }> };
6
+
7
+ // POST /api/analyst/:id/recommendations/:recId/dismiss - 409 once decided.
8
+ export async function POST(request: Request, { params }: Ctx) {
9
+ const { id, recId } = await params;
10
+ return analystRoute(request, z.object({}), () => ({
11
+ recommendation: dismissRecommendation(id, recId),
12
+ }));
13
+ }
@@ -0,0 +1,13 @@
1
+ import { sendRecommendation, sendSchema } from "@/lib/analyst";
2
+ import { analystRoute } from "@/lib/analyst/route";
3
+
4
+ type Ctx = { params: Promise<{ id: string; recId: string }> };
5
+
6
+ // POST /api/analyst/:id/recommendations/:recId/send { text? } - Send to
7
+ // Coding Agent, edited when text is given. 409 once decided.
8
+ export async function POST(request: Request, { params }: Ctx) {
9
+ const { id, recId } = await params;
10
+ return analystRoute(request, sendSchema, async (b) => ({
11
+ recommendation: await sendRecommendation(id, recId, b.text),
12
+ }));
13
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { AnalystError, getLinkOrThrow, pushDigest } from "@/lib/analyst";
3
+ import { analystRoute } from "@/lib/analyst/route";
4
+
5
+ type Ctx = { params: Promise<{ id: string }> };
6
+
7
+ // POST /api/analyst/:id/review - Review now: the turns since the last
8
+ // review, or the last turn again.
9
+ export async function POST(request: Request, { params }: Ctx) {
10
+ const { id } = await params;
11
+ return analystRoute(request, z.object({}), async () => {
12
+ const link = getLinkOrThrow(id);
13
+ if (link.endedAt) throw new AnalystError("This pairing has ended", 409);
14
+ const result = await pushDigest(id, { force: true });
15
+ if (result === "nothing")
16
+ throw new AnalystError(
17
+ "The coding agent hasn't finished a turn yet",
18
+ 409
19
+ );
20
+ return { result };
21
+ });
22
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import {
3
+ AnalystError,
4
+ endLink,
5
+ linkView,
6
+ patchLinkSchema,
7
+ setPaused,
8
+ } from "@/lib/analyst";
9
+ import { analystRoute, run } from "@/lib/analyst/route";
10
+
11
+ type Ctx = { params: Promise<{ id: string }> };
12
+
13
+ const view = (id: string) => {
14
+ const link = linkView(id);
15
+ if (!link) throw new AnalystError("Pairing not found", 404);
16
+ return { link };
17
+ };
18
+
19
+ // GET /api/analyst/:id - the pairing, both sessions and its latest cards.
20
+ export async function GET(_request: Request, { params }: Ctx) {
21
+ const { id } = await params;
22
+ return run(() => view(id));
23
+ }
24
+
25
+ // PATCH /api/analyst/:id { paused } - stop or resume sending turns.
26
+ export async function PATCH(request: Request, { params }: Ctx) {
27
+ const { id } = await params;
28
+ return analystRoute(request, patchLinkSchema, (b) => {
29
+ setPaused(id, b.paused);
30
+ return view(id);
31
+ });
32
+ }
33
+
34
+ const unlink = z.object({ endAnalyst: z.boolean().optional() });
35
+
36
+ // DELETE /api/analyst/:id { endAnalyst? } - unlink; the analyst is settled
37
+ // unless endAnalyst is false. History is kept.
38
+ export async function DELETE(request: Request, { params }: Ctx) {
39
+ const { id } = await params;
40
+ return analystRoute(request, unlink, (b) => {
41
+ endLink(id, b);
42
+ return view(id);
43
+ });
44
+ }
@@ -0,0 +1,10 @@
1
+ import { createLink, createLinkSchema, linkView } from "@/lib/analyst";
2
+ import { analystRoute } from "@/lib/analyst/route";
3
+
4
+ // POST /api/analyst { coderSessionId | newCoder, analystModel?, reviewLast? }
5
+ // - pairs an analyst chat with a coding chat (a new one with newCoder).
6
+ export async function POST(request: Request) {
7
+ return analystRoute(request, createLinkSchema, async (b) => ({
8
+ link: linkView((await createLink(b)).id),
9
+ }));
10
+ }
@@ -0,0 +1,44 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { groupCommand } from "@/lib/session-groups/cli";
4
+ import { GROUP_STATUS, GroupError } from "@/lib/session-groups";
5
+
6
+ const refs = z.array(z.string().min(1)).min(1);
7
+ const body = z.object({
8
+ from: z.string().min(1, "aos group runs from inside an AgentOS session"),
9
+ command: z.discriminatedUnion("action", [
10
+ z.object({ action: z.literal("list") }),
11
+ z.object({ action: z.literal("new"), name: z.string() }),
12
+ z.object({
13
+ action: z.literal("rename"),
14
+ group: z.string(),
15
+ name: z.string(),
16
+ }),
17
+ z.object({
18
+ action: z.literal("add"),
19
+ group: z.string(),
20
+ sessions: refs,
21
+ }),
22
+ z.object({ action: z.literal("rm"), sessions: refs }),
23
+ ]),
24
+ });
25
+
26
+ // POST /api/bus/groups { from, command } - `aos group …`, scoped to the
27
+ // asking session's workspace like `aos read`.
28
+ export async function POST(request: NextRequest) {
29
+ const parsed = body.safeParse(await request.json().catch(() => null));
30
+ if (!parsed.success)
31
+ return NextResponse.json(
32
+ { error: parsed.error.issues[0]?.message ?? "Bad request" },
33
+ { status: 400 }
34
+ );
35
+ try {
36
+ return NextResponse.json({
37
+ summary: groupCommand(parsed.data.from, parsed.data.command),
38
+ });
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ const status = error instanceof GroupError ? GROUP_STATUS[error.code] : 409;
42
+ return NextResponse.json({ error: message }, { status });
43
+ }
44
+ }
@@ -4,7 +4,9 @@ import { createTask } from "@/lib/tasks";
4
4
  import { startOrQueue, stampPriority } from "@/lib/tasks/queue";
5
5
  import { isPriority } from "@/lib/tasks/schedule-types";
6
6
  import { hostIdNamed } from "@/lib/hosts";
7
- import { startView } from "@/lib/sessions/launch";
7
+ import { resolveGroup } from "@/lib/session-groups";
8
+ import { launchSession, startView } from "@/lib/sessions/launch";
9
+ import { isAgentRole } from "@/lib/models/agent-roles";
8
10
  import { callerSession, starterOf } from "@/lib/tasks/starter";
9
11
  import { callingDevice } from "@/lib/security/device-me";
10
12
  import {
@@ -33,6 +35,8 @@ export async function POST(request: NextRequest) {
33
35
  from,
34
36
  cwd,
35
37
  piece,
38
+ group,
39
+ agentRole,
36
40
  } = await request.json();
37
41
  // Whoever called: a session (by `from`, else the worktree it runs in) or a person.
38
42
  const starter = starterOf(
@@ -53,8 +57,15 @@ export async function POST(request: NextRequest) {
53
57
  throw new Error('machine is "mac" or "any"');
54
58
  if (piece !== undefined && mode !== "task")
55
59
  throw new Error("--piece works for tasks only");
60
+ const target = findProject(String(project ?? ""));
61
+ if (group !== undefined && (typeof group !== "string" || !group.trim()))
62
+ throw new Error("--group needs a group name");
63
+ const groupId =
64
+ typeof group === "string"
65
+ ? resolveGroup(target.workspace_id ?? null, group).id
66
+ : undefined;
56
67
  if (mode === "task") {
57
- const projectId = findProject(String(project ?? "")).id;
68
+ const projectId = target.id;
58
69
  // A task whose plan has pieces waiting here must say which it builds.
59
70
  const waiting = startedBy ? waitingPiecesFor(startedBy, projectId) : [];
60
71
  let pieceId: number | undefined;
@@ -73,6 +84,7 @@ export async function POST(request: NextRequest) {
73
84
  model,
74
85
  hostId: on ? hostIdNamed(String(on)) : undefined,
75
86
  view: startView(view),
87
+ groupId,
76
88
  planOnly: planOnly === true ? true : undefined,
77
89
  };
78
90
  const out = await startOrQueue(
@@ -93,6 +105,25 @@ export async function POST(request: NextRequest) {
93
105
  ? NextResponse.json({ queued: out.queued, builds }, { status: 202 })
94
106
  : NextResponse.json({ session: out.started, builds }, { status: 201 });
95
107
  }
108
+ if (agentRole !== undefined) {
109
+ if (mode !== "session") throw new Error("--role works for spawn only");
110
+ if (!isAgentRole(agentRole))
111
+ throw new Error("--role is developer, analyst, reviewer or supervisor");
112
+ const { session } = await launchSession({
113
+ projectId: target.id,
114
+ agentType: "claude",
115
+ prompt,
116
+ name: given,
117
+ model: typeof model === "string" ? model : null,
118
+ autoApprove: true,
119
+ startedBy,
120
+ startedVia: starter.startedVia,
121
+ parentSessionId: starter.parentSessionId,
122
+ groupId,
123
+ agentRole,
124
+ });
125
+ return NextResponse.json({ session }, { status: 201 });
126
+ }
96
127
  const session = await spawnSession({
97
128
  project: String(project ?? ""),
98
129
  prompt,
@@ -100,6 +131,7 @@ export async function POST(request: NextRequest) {
100
131
  model,
101
132
  startedBy,
102
133
  parentSessionId: starter.parentSessionId,
134
+ groupId,
103
135
  });
104
136
  return NextResponse.json({ session }, { status: 201 });
105
137
  } catch (error) {
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from "next/server";
2
+ import { endOutside } from "@/lib/claims";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ // DELETE /api/claims/:id - an outside session ends its own claim. A task's
7
+ // or another machine's claim can't be ended this way.
8
+ export async function DELETE(
9
+ _request: Request,
10
+ { params }: { params: Promise<{ id: string }> }
11
+ ) {
12
+ const { id } = await params;
13
+ const result = endOutside(id);
14
+ if (result === "forbidden")
15
+ return NextResponse.json(
16
+ { error: "Only an outside session's claim can be ended here" },
17
+ { status: 403 }
18
+ );
19
+ return NextResponse.json({ ok: true, ended: result === "ended" });
20
+ }
@@ -0,0 +1,23 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { checkStart } from "@/lib/claims/startCheck";
4
+ import { startWarning } from "@/lib/claims/startWarning";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ const body = z.object({
9
+ projectId: z.string().min(1).max(100),
10
+ name: z.string().max(200).optional(),
11
+ prompt: z.string().max(100_000),
12
+ branch: z.string().max(250).nullable().optional(),
13
+ });
14
+
15
+ // POST /api/claims/check - before a person starts a task: who else is on the
16
+ // same work. Reads claims and the repo's file list; never blocks a start.
17
+ export async function POST(request: NextRequest) {
18
+ const parsed = body.safeParse(await request.json().catch(() => null));
19
+ if (!parsed.success)
20
+ return NextResponse.json({ error: "Bad check" }, { status: 400 });
21
+ const { overlaps } = await checkStart(parsed.data);
22
+ return NextResponse.json({ overlaps: overlaps.map(startWarning) });
23
+ }
@@ -0,0 +1,113 @@
1
+ import { execFileSync } from "child_process";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { NextRequest } from "next/server";
6
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { tempDir } from "@/lib/test-temp";
8
+ import { seedWorkspace } from "@/lib/orchestrator/testing";
9
+
10
+ const { db } = await import("@/lib/db");
11
+ const { machineId } = await import("@/lib/claims/machine");
12
+ const { upsertRemote } = await import("@/lib/claims/store");
13
+ const { putSetting } = await import("@/lib/settings-store");
14
+ const { POST } = await import("./check/route");
15
+
16
+ const FILES = ["lib/auth/session.ts", "lib/auth/index.ts", "lib/auth/token.ts"];
17
+ const REPO = "git.example.test/team/widgets";
18
+
19
+ let dir: string;
20
+ let projectId: string;
21
+
22
+ const sh = (cwd: string, ...args: string[]) =>
23
+ execFileSync("git", args, { cwd, stdio: "pipe" });
24
+
25
+ const check = (body: unknown) =>
26
+ POST(
27
+ new NextRequest("http://localhost/api/claims/check", {
28
+ method: "POST",
29
+ body: typeof body === "string" ? body : JSON.stringify(body),
30
+ })
31
+ );
32
+
33
+ beforeAll(() => {
34
+ vi.spyOn(os, "homedir").mockReturnValue(tempDir("aos-claim-check-"));
35
+ dir = tempDir("claim-check-repo-");
36
+ sh(dir, "init", "-q", "-b", "main");
37
+ sh(dir, "config", "user.email", "t@example.test");
38
+ sh(dir, "config", "user.name", "t");
39
+ sh(dir, "remote", "add", "origin", "git@git.example.test:team/widgets.git");
40
+ for (const f of FILES) {
41
+ fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true });
42
+ fs.writeFileSync(path.join(dir, f), "x");
43
+ }
44
+ sh(dir, "add", "-A");
45
+ sh(dir, "commit", "-q", "-m", "init");
46
+ });
47
+
48
+ beforeEach(() => {
49
+ db.prepare(`DELETE FROM work_claims`).run();
50
+ putSetting("claims.share", "on");
51
+ const t = seedWorkspace();
52
+ projectId = t.app.id;
53
+ db.prepare(`UPDATE projects SET working_directory = ? WHERE id = ?`).run(
54
+ dir,
55
+ projectId
56
+ );
57
+ });
58
+
59
+ const remote = (files: string[]) =>
60
+ upsertRemote(machineId(), "peer-1", {
61
+ id: "claim-box-1",
62
+ origin: "box-machine",
63
+ origin_name: "box",
64
+ hops: 0,
65
+ source: "session",
66
+ session_name: "Rework login sessions",
67
+ repo: REPO,
68
+ branch: "feature/login",
69
+ base: "main",
70
+ goal: "Rework login sessions",
71
+ files,
72
+ files_more: 0,
73
+ pr_url: null,
74
+ ttl_s: 600,
75
+ age_s: 60,
76
+ released: null,
77
+ });
78
+
79
+ describe("POST /api/claims/check", () => {
80
+ it("refuses a body that isn't a check", async () => {
81
+ expect((await check("nope")).status).toBe(400);
82
+ expect((await check({ projectId })).status).toBe(400);
83
+ expect((await check({ projectId, prompt: 5 })).status).toBe(400);
84
+ });
85
+
86
+ it("says nothing when no one else is on the repo", async () => {
87
+ const res = await check({ projectId, prompt: "Tidy auth" });
88
+ expect(await res.json()).toEqual({ overlaps: [] });
89
+ });
90
+
91
+ it("returns the overlap with the other claim, strong first", async () => {
92
+ remote(FILES);
93
+ const res = await check({
94
+ projectId,
95
+ name: "Tidy auth",
96
+ prompt: `Touch ${FILES.join(" ")}`,
97
+ });
98
+ const { overlaps } = await res.json();
99
+ expect(overlaps).toHaveLength(1);
100
+ expect(overlaps[0]).toMatchObject({
101
+ strength: "strong",
102
+ claim: { id: "claim-box-1", machine: "box", branch: "feature/login" },
103
+ });
104
+ expect(overlaps[0].shared).toEqual([...FILES].sort());
105
+ });
106
+
107
+ it("returns no overlaps when sharing is off", async () => {
108
+ remote(FILES);
109
+ putSetting("claims.share", "off");
110
+ const res = await check({ projectId, prompt: `Touch ${FILES.join(" ")}` });
111
+ expect(await res.json()).toEqual({ overlaps: [] });
112
+ });
113
+ });
@@ -0,0 +1,42 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { acceptExchange, answerExchange } from "@/lib/claims/exchange";
3
+ import { claimsShared } from "@/lib/claims/machine";
4
+ import { rememberPeer } from "@/lib/claims/peers";
5
+ import { exchangeBody } from "@/lib/claims/types";
6
+ import { pinForHost } from "@/lib/peer/links";
7
+ import { PEER_HEADER, TRUST_HEADER } from "@/lib/security/auth";
8
+ import { readCapped } from "@/lib/security/read-capped";
9
+
10
+ export const dynamic = "force-dynamic";
11
+
12
+ const MAX_BYTES = 2 * 1024 * 1024;
13
+
14
+ // POST /api/claims/exchange - a linked machine sends its claims and gets
15
+ // ours back. Paths and one-line goals only; the device-token gate applies.
16
+ export async function POST(request: NextRequest) {
17
+ // A Connect peer arrives as `peer` trust naming its pin. The RPC layer has
18
+ // already refused suspended and unknown ones; a pin suspended since is
19
+ // refused here too.
20
+ let viaHost: string | null = null;
21
+ if (request.headers.get(TRUST_HEADER) === "peer") {
22
+ const hostId = request.headers.get(PEER_HEADER);
23
+ if (!hostId || pinForHost(hostId)?.state !== "active")
24
+ return NextResponse.json({ error: "forbidden" }, { status: 403 });
25
+ viaHost = hostId;
26
+ }
27
+ const raw = await readCapped(request, MAX_BYTES);
28
+ if (raw === null)
29
+ return NextResponse.json({ error: "Too large" }, { status: 413 });
30
+ let json: unknown;
31
+ try {
32
+ json = JSON.parse(raw);
33
+ } catch {
34
+ return NextResponse.json({ error: "Not JSON" }, { status: 400 });
35
+ }
36
+ const body = exchangeBody.safeParse(json);
37
+ if (!body.success)
38
+ return NextResponse.json({ error: "Bad claims" }, { status: 400 });
39
+ if (viaHost && claimsShared()) rememberPeer(viaHost, body.data.machine.id);
40
+ acceptExchange(body.data, viaHost);
41
+ return NextResponse.json(answerExchange(body.data));
42
+ }