@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
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The roles a person assigns models to: Developer, Analyst, Reviewer and
3
+ * Supervisor. Each binds one of the model roles in ./roles, so there is one
4
+ * routing system: Developer is `implement`, Supervisor is `orchestrator`,
5
+ * and Analyst and Reviewer have a model role of their own. Imported by the
6
+ * browser too, so no Node modules here.
7
+ */
8
+
9
+ import type { ModelRole } from "./roles";
10
+
11
+ export const AGENT_ROLES = [
12
+ "developer",
13
+ "analyst",
14
+ "reviewer",
15
+ "supervisor",
16
+ ] as const;
17
+
18
+ export type AgentRole = (typeof AGENT_ROLES)[number];
19
+
20
+ export interface AgentRoleDef {
21
+ id: AgentRole;
22
+ label: string;
23
+ // The one line shown beside its model.
24
+ purpose: string;
25
+ modelRole: ModelRole;
26
+ // Appended to the instructions of a session started in the role.
27
+ brief: string;
28
+ // Whether it may change files.
29
+ edits: boolean;
30
+ }
31
+
32
+ export const AGENT_ROLE_DEFS: Record<AgentRole, AgentRoleDef> = {
33
+ developer: {
34
+ id: "developer",
35
+ label: "Developer",
36
+ purpose: "Writes the code: tasks, and sessions started as Developer",
37
+ modelRole: "implement",
38
+ edits: true,
39
+ brief:
40
+ "You are the Developer in this crew. You write and change the code. When your work is ready, tell the Reviewer with `aos send @reviewer`; answer the Analyst's questions with `aos send @analyst`.",
41
+ },
42
+ analyst: {
43
+ id: "analyst",
44
+ label: "Analyst",
45
+ purpose:
46
+ "Reads code, logs and the Developer's work; explains and proposes; doesn't commit",
47
+ modelRole: "analyze",
48
+ edits: false,
49
+ brief:
50
+ "You are the Analyst in this crew. You read code, logs and data, and the Developer's transcript (`aos read @developer`), then explain what you find and propose changes. You don't edit files, commit or open pull requests; send proposals to the Developer with `aos send @developer`.",
51
+ },
52
+ reviewer: {
53
+ id: "reviewer",
54
+ label: "Reviewer",
55
+ purpose:
56
+ "Reviews another role's diff and suggests improvements. Advice only: not the merge gate",
57
+ modelRole: "advise",
58
+ edits: false,
59
+ brief:
60
+ "You are the Reviewer in this crew. Read the Developer's diff (`git diff` in its worktree, `aos read @developer`) and send concrete suggestions with `aos send @developer`. You don't edit files or open pull requests. Your review is advice; the merge gate is separate.",
61
+ },
62
+ supervisor: {
63
+ id: "supervisor",
64
+ label: "Supervisor",
65
+ purpose:
66
+ "Watches a crew, sequences work, unblocks and reports. The workspace orchestrator",
67
+ modelRole: "orchestrator",
68
+ edits: false,
69
+ brief:
70
+ "You are the Supervisor in this crew. Watch the other roles (`aos peers`, `aos read @developer`), unblock them, sequence the work and report to the user. You don't write the code yourself.",
71
+ },
72
+ };
73
+
74
+ export const isAgentRole = (v: unknown): v is AgentRole =>
75
+ typeof v === "string" && (AGENT_ROLES as readonly string[]).includes(v);
76
+
77
+ export const agentRoleOf = (modelRole: ModelRole): AgentRole | undefined =>
78
+ AGENT_ROLES.find((r) => AGENT_ROLE_DEFS[r].modelRole === modelRole);
79
+
80
+ // The model roles under the four above, shown as "internal" roles.
81
+ export const INTERNAL_MODEL_ROLES = (all: readonly ModelRole[]) =>
82
+ all.filter((r) => !agentRoleOf(r));
83
+
84
+ // A session's brief for its role, or "" with none.
85
+ export function roleBrief(role: string | null | undefined): string {
86
+ return isAgentRole(role) ? AGENT_ROLE_DEFS[role].brief : "";
87
+ }
@@ -0,0 +1,17 @@
1
+ import { modelCatalog } from "./catalog";
2
+ import { modelAvailability as probeRef } from "./catalog";
3
+ import type { Availability } from "./availability";
4
+
5
+ // Every local model's availability by ref, probing the stale or all of them.
6
+ export async function modelAvailability(
7
+ refresh = false
8
+ ): Promise<Record<string, Availability>> {
9
+ const local = modelCatalog().filter((c) => c.locality === "local");
10
+ const out: Record<string, Availability> = {};
11
+ await Promise.all(
12
+ local.map(async (c) => {
13
+ out[c.ref] = await probeRef(c.ref, undefined, refresh);
14
+ })
15
+ );
16
+ return out;
17
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Whether a model's server answers. Only endpoints on the local networks
3
+ * are probed (a cloud provider's key isn't proof and a probe costs), one
4
+ * short GET, cached for a while. Any HTTP answer means the server is up;
5
+ * what's down is a refused connection or no answer in time.
6
+ */
7
+
8
+ export type AvailabilityState = "up" | "down" | "unknown";
9
+
10
+ export interface Availability {
11
+ state: AvailabilityState;
12
+ checkedAt: string | null;
13
+ error?: string;
14
+ }
15
+
16
+ const TTL_MS = 30_000;
17
+ const TIMEOUT_MS = 1_500;
18
+
19
+ interface Entry extends Availability {
20
+ at: number;
21
+ }
22
+ const cache = new Map<string, Entry>();
23
+ const probing = new Map<string, Promise<Entry>>();
24
+
25
+ const UNKNOWN: Availability = { state: "unknown", checkedAt: null };
26
+
27
+ // What a server of that API lists its models at.
28
+ export const probeUrl = (baseUrl: string, api: "anthropic" | "openai") => {
29
+ const base = baseUrl.replace(/\/+$/, "");
30
+ return api === "anthropic" && !/\/v1$/.test(base)
31
+ ? `${base}/v1/models`
32
+ : `${base}/models`;
33
+ };
34
+
35
+ async function probe(url: string): Promise<Entry> {
36
+ const at = Date.now();
37
+ try {
38
+ await fetch(url, {
39
+ method: "GET",
40
+ signal: AbortSignal.timeout(TIMEOUT_MS),
41
+ redirect: "manual",
42
+ });
43
+ return { state: "up", checkedAt: new Date(at).toISOString(), at };
44
+ } catch (error) {
45
+ const cause = (error as { cause?: { code?: string } }).cause?.code;
46
+ const timedOut = (error as Error).name === "TimeoutError";
47
+ return {
48
+ state: "down",
49
+ checkedAt: new Date(at).toISOString(),
50
+ at,
51
+ error: timedOut
52
+ ? `no answer in ${TIMEOUT_MS / 1000}s`
53
+ : (cause ?? (error as Error).message),
54
+ };
55
+ }
56
+ }
57
+
58
+ const strip = ({ at: _at, ...rest }: Entry): Availability => rest;
59
+
60
+ // The last check of a URL, probing in the background when it's stale: a
61
+ // request handler never waits on a probe.
62
+ export function cachedAvailability(url: string): Availability {
63
+ const hit = cache.get(url);
64
+ if (!hit || Date.now() - hit.at >= TTL_MS) void checkAvailability(url);
65
+ return hit ? strip(hit) : UNKNOWN;
66
+ }
67
+
68
+ // A fresh enough answer, waiting for the probe if there isn't one.
69
+ export async function checkAvailability(
70
+ url: string,
71
+ refresh = false
72
+ ): Promise<Availability> {
73
+ const hit = cache.get(url);
74
+ if (!refresh && hit && Date.now() - hit.at < TTL_MS) return strip(hit);
75
+ let running = probing.get(url);
76
+ if (!running) {
77
+ running = probe(url)
78
+ .then((e) => {
79
+ cache.set(url, e);
80
+ return e;
81
+ })
82
+ .finally(() => probing.delete(url));
83
+ probing.set(url, running);
84
+ }
85
+ return strip(await running);
86
+ }
87
+
88
+ export function clearAvailability(): void {
89
+ cache.clear();
90
+ probing.clear();
91
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The one list of models every picker shows, built on the server: each
3
+ * enabled provider's models, and the models Pi reaches (local servers in its
4
+ * models.json). Nothing about a model, provider or its locality is decided
5
+ * in the browser.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path from "path";
11
+ import { effectiveModels, type EffectiveModels } from "./config";
12
+ import { ANTHROPIC } from "./roles";
13
+ import { PI, type ModelProvider } from "./providers";
14
+ import { hostLocality, type Locality } from "./locality";
15
+ import {
16
+ cachedAvailability,
17
+ checkAvailability,
18
+ probeUrl,
19
+ type Availability,
20
+ } from "./availability";
21
+
22
+ export interface ModelChoice {
23
+ // "anthropic/claude-opus-5-5", "pi/local/qwen3-27b"
24
+ ref: string;
25
+ label: string;
26
+ // The provider's name: "Anthropic", "DeepSeek", "Pi · local".
27
+ group: string;
28
+ // What runs it: Claude Code, or the Pi agent.
29
+ agent: "claude" | "pi";
30
+ locality: Locality;
31
+ availability: Availability;
32
+ // May serve a merge gate: Claude Code can run it.
33
+ gateCapable: boolean;
34
+ // Where the server is (never a key).
35
+ endpoint?: string;
36
+ }
37
+
38
+ interface PiProvider {
39
+ id: string;
40
+ baseUrl: string;
41
+ models: { id: string; name?: string }[];
42
+ }
43
+
44
+ const piModelsFile = () =>
45
+ process.env.AGENTOS_PI_MODELS_FILE ||
46
+ path.join(os.homedir(), ".pi", "agent", "models.json");
47
+
48
+ // The providers in Pi's own config. A missing or unreadable file means Pi
49
+ // has no servers of ours to list.
50
+ export function piProviders(file = piModelsFile()): PiProvider[] {
51
+ let raw: unknown;
52
+ try {
53
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
54
+ } catch {
55
+ return [];
56
+ }
57
+ const providers = (raw as { providers?: Record<string, unknown> })?.providers;
58
+ if (!providers || typeof providers !== "object") return [];
59
+ const out: PiProvider[] = [];
60
+ for (const [id, p] of Object.entries(providers)) {
61
+ const q = p as { baseUrl?: unknown; models?: unknown };
62
+ if (typeof q?.baseUrl !== "string" || !Array.isArray(q.models)) continue;
63
+ const models = q.models
64
+ .map((m) =>
65
+ typeof m === "string"
66
+ ? { id: m }
67
+ : {
68
+ id: (m as { id?: unknown })?.id,
69
+ name: (m as { name?: unknown })?.name,
70
+ }
71
+ )
72
+ .filter(
73
+ (m): m is { id: string; name?: string } => typeof m.id === "string"
74
+ )
75
+ .map((m) => ({
76
+ id: m.id,
77
+ ...(typeof m.name === "string" ? { name: m.name } : {}),
78
+ }));
79
+ out.push({ id, baseUrl: q.baseUrl, models });
80
+ }
81
+ return out;
82
+ }
83
+
84
+ const providerEndpoint = (p: ModelProvider) =>
85
+ p.baseUrl ? probeUrl(p.baseUrl, p.api) : undefined;
86
+
87
+ export function modelCatalog(
88
+ effective: EffectiveModels = effectiveModels(),
89
+ pi: PiProvider[] = piProviders()
90
+ ): ModelChoice[] {
91
+ const out: ModelChoice[] = [];
92
+ for (const p of effective.providers.filter((x) => x.enabled)) {
93
+ const endpoint = providerEndpoint(p);
94
+ const locality = hostLocality(p.baseUrl);
95
+ const availability: Availability =
96
+ p.id === ANTHROPIC
97
+ ? { state: "up", checkedAt: null }
98
+ : locality === "local" && endpoint
99
+ ? cachedAvailability(endpoint)
100
+ : { state: "unknown", checkedAt: null };
101
+ for (const m of p.models)
102
+ out.push({
103
+ ref: `${p.id}/${m}`,
104
+ label: m,
105
+ group: p.label,
106
+ agent: "claude",
107
+ locality,
108
+ availability,
109
+ gateCapable: p.api === "anthropic",
110
+ endpoint: p.baseUrl,
111
+ });
112
+ }
113
+ for (const p of pi) {
114
+ const locality = hostLocality(p.baseUrl);
115
+ const endpoint = probeUrl(p.baseUrl, "openai");
116
+ const availability: Availability =
117
+ locality === "local"
118
+ ? cachedAvailability(endpoint)
119
+ : { state: "unknown", checkedAt: null };
120
+ for (const m of p.models)
121
+ out.push({
122
+ ref: `${PI}/${p.id}/${m.id}`,
123
+ label: m.name ?? m.id,
124
+ group: `Pi · ${p.id}`,
125
+ agent: "pi",
126
+ locality,
127
+ availability,
128
+ gateCapable: false,
129
+ endpoint: p.baseUrl,
130
+ });
131
+ }
132
+ return out;
133
+ }
134
+
135
+ export const choiceFor = (
136
+ ref: string,
137
+ effective?: EffectiveModels
138
+ ): ModelChoice | undefined =>
139
+ modelCatalog(effective).find((c) => c.ref === ref);
140
+
141
+ // Whether the model's server answers now, waiting for a probe. Cloud and
142
+ // unlisted models aren't probed: they count as available.
143
+ export async function modelAvailability(
144
+ ref: string,
145
+ effective?: EffectiveModels,
146
+ refresh = false
147
+ ): Promise<Availability> {
148
+ const choice = choiceFor(ref, effective);
149
+ if (!choice || choice.locality !== "local" || !choice.endpoint)
150
+ return { state: "unknown", checkedAt: null };
151
+ const url =
152
+ choice.agent === "pi"
153
+ ? probeUrl(choice.endpoint, "openai")
154
+ : probeUrl(choice.endpoint, "anthropic");
155
+ return checkAvailability(url, refresh);
156
+ }
@@ -138,25 +138,35 @@ const subagentMap = z
138
138
  });
139
139
  });
140
140
 
141
+ // A role may name a model to start on when its own can't be reached.
142
+ const fallbackMap = z.partialRecord(z.enum(MODEL_ROLES), roleRef);
143
+
141
144
  export const modelSettingsSchema = z.strictObject({
142
145
  roles: roleMap.optional(),
143
146
  subagents: subagentMap.optional(),
144
147
  providers: z.array(providerSchema).optional(),
145
148
  prices: z.record(z.string().regex(REF_PATTERN), price).optional(),
146
149
  planStep: z.boolean().optional(),
150
+ fallbacks: fallbackMap.optional(),
147
151
  });
148
152
 
149
153
  export const workspaceModelsSchema = z.strictObject({
150
154
  roles: roleMap.optional(),
151
155
  });
152
156
 
157
+ export const projectModelsSchema = z.strictObject({
158
+ roles: roleMap.optional(),
159
+ });
160
+
153
161
  export type ModelSettings = z.infer<typeof modelSettingsSchema>;
154
162
  export type WorkspaceModels = z.infer<typeof workspaceModelsSchema>;
163
+ export type ProjectModels = z.infer<typeof projectModelsSchema>;
155
164
  export type RoleMap = Partial<Record<ModelRole, string>>;
156
165
  export const roleMapSchema = roleMap;
157
166
 
158
167
  const GLOBAL_KEY = "models";
159
168
  const workspaceKey = (id: string) => `models.workspace.${id}`;
169
+ const projectKey = (id: string) => `models.project.${id}`;
160
170
 
161
171
  function read<T>(key: string, schema: z.ZodType<T>): T | null {
162
172
  const row = db
@@ -221,6 +231,23 @@ export function setWorkspaceModels(
221
231
  return saved;
222
232
  }
223
233
 
234
+ // Set in Settings, never in the repo, so work can't choose its own models.
235
+ export const projectModels = (projectId: string): ProjectModels =>
236
+ read(projectKey(projectId), projectModelsSchema) ?? {};
237
+
238
+ export function setProjectModels(
239
+ projectId: string,
240
+ value: unknown
241
+ ): ProjectModels {
242
+ const saved = write(projectKey(projectId), projectModelsSchema, value);
243
+ if (!Object.keys(saved.roles ?? {}).length) clearProjectModels(projectId);
244
+ return saved;
245
+ }
246
+
247
+ export const clearProjectModels = (projectId: string): void => {
248
+ db.prepare(`DELETE FROM settings WHERE key = ?`).run(projectKey(projectId));
249
+ };
250
+
224
251
  // The settings in force: the presets with any saved edits laid over them
225
252
  // (by id), saved providers that aren't presets, and every role filled.
226
253
  export interface EffectiveModels {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Whether a model endpoint is on this machine's own networks (local) or
3
+ * out on the internet (cloud), read from its host, never from a name.
4
+ * Imported by the browser too.
5
+ */
6
+
7
+ export type Locality = "local" | "cloud";
8
+
9
+ const PRIVATE_V4 = [
10
+ /^127\./,
11
+ /^10\./,
12
+ /^192\.168\./,
13
+ /^172\.(1[6-9]|2\d|3[01])\./,
14
+ /^169\.254\./,
15
+ // The tailnet: 100.64.0.0/10.
16
+ /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./,
17
+ ];
18
+
19
+ export function hostLocality(url: string | null | undefined): Locality {
20
+ if (!url) return "cloud";
21
+ let host: string;
22
+ try {
23
+ host = new URL(url).hostname.toLowerCase();
24
+ } catch {
25
+ return "cloud";
26
+ }
27
+ host = host.replace(/^\[|\]$/g, "");
28
+ if (host === "localhost" || host === "::1" || host.endsWith(".localhost"))
29
+ return "local";
30
+ if (host.endsWith(".local") || host.endsWith(".ts.net")) return "local";
31
+ if (/^f[cd][0-9a-f]{2}:/.test(host)) return "local";
32
+ return PRIVATE_V4.some((re) => re.test(host)) ? "local" : "cloud";
33
+ }
@@ -63,6 +63,17 @@ export const ANTHROPIC_PROVIDER: ModelProvider = {
63
63
  enabled: true,
64
64
  };
65
65
 
66
+ // Pi's own models ("pi/<its provider>/<model>"): local servers Pi reaches,
67
+ // run by the Pi agent rather than through Claude Code's endpoint variables.
68
+ export const PI = "pi";
69
+ export const PI_PROVIDER: ModelProvider = {
70
+ id: PI,
71
+ label: "Pi",
72
+ api: "openai",
73
+ models: [],
74
+ enabled: true,
75
+ };
76
+
66
77
  // Checked against each provider's docs on 2026-10-11: DeepSeek
67
78
  // (api-docs.deepseek.com/guides/anthropic_api) and Z.ai
68
79
  // (docs.z.ai/devpack/tool/claude). Off until a key reference is set.
@@ -12,11 +12,17 @@ import {
12
12
  effectiveModels,
13
13
  roleMapSchema,
14
14
  savedModelSettings,
15
+ projectModels,
15
16
  workspaceModels,
16
17
  type EffectiveModels,
17
18
  type RoleMap,
18
19
  } from "./config";
19
- import { ANTHROPIC_PROVIDER, type ModelProvider } from "./providers";
20
+ import {
21
+ ANTHROPIC_PROVIDER,
22
+ PI,
23
+ PI_PROVIDER,
24
+ type ModelProvider,
25
+ } from "./providers";
20
26
  import {
21
27
  ANTHROPIC,
22
28
  DEFAULT_ROLE_MODELS,
@@ -33,7 +39,14 @@ import {
33
39
  type SecretCommand,
34
40
  } from "./secrets";
35
41
 
36
- export type RoleSource = "task" | "workspace" | "settings" | "env" | "default";
42
+ export type RoleSource =
43
+ | "task"
44
+ | "project"
45
+ | "workspace"
46
+ | "settings"
47
+ | "env"
48
+ | "inherited"
49
+ | "default";
37
50
 
38
51
  // The server's own environment can still stand in for a role's default:
39
52
  // what AgentOS read before model routing existed.
@@ -87,9 +100,15 @@ const overrideFor = (o: ModelOverride | null | undefined, role: ModelRole) =>
87
100
  // The provider by id, or throws why it can't be used.
88
101
  export function providerFor(
89
102
  id: string,
90
- models: EffectiveModels = effectiveModels()
103
+ models: EffectiveModels = effectiveModels(),
104
+ allowPi = false
91
105
  ): ModelProvider {
92
106
  if (id === ANTHROPIC) return ANTHROPIC_PROVIDER;
107
+ if (id === PI) {
108
+ if (!allowPi)
109
+ throw new Error("Pi models run only in a session started in a role");
110
+ return PI_PROVIDER;
111
+ }
93
112
  const p = models.providers.find((x) => x.id === id);
94
113
  if (!p) throw new Error(`No model provider "${id}" (Settings > Models)`);
95
114
  if (!p.enabled) throw new Error(`The ${p.label} provider is turned off`);
@@ -104,10 +123,19 @@ export function providerFor(
104
123
 
105
124
  export function resolveRole(
106
125
  role: ModelRole,
107
- opts: { workspaceId?: string | null; override?: ModelOverride | null } = {},
126
+ opts: {
127
+ workspaceId?: string | null;
128
+ projectId?: string | null;
129
+ override?: ModelOverride | null;
130
+ // A role session runs Pi models; a task or gate can't.
131
+ allowPi?: boolean;
132
+ } = {},
108
133
  models: EffectiveModels = effectiveModels()
109
134
  ): RoutedModel {
110
135
  const task = overrideFor(opts.override, role);
136
+ const proj = opts.projectId
137
+ ? projectModels(opts.projectId).roles?.[role]
138
+ : undefined;
111
139
  const ws = opts.workspaceId
112
140
  ? workspaceModels(opts.workspaceId).roles?.[role]
113
141
  : undefined;
@@ -117,20 +145,30 @@ export function resolveRole(
117
145
  saved === DEFAULT_ROLE_MODELS[role] && !savedModelSettings().roles?.[role]
118
146
  ? envModel(role)
119
147
  : undefined;
148
+ // The reviewer's advice follows the gate's model until it's set.
149
+ const named = !!savedModelSettings().roles?.[role];
150
+ const inherited =
151
+ role === "advise" && !env && !named
152
+ ? resolveRole("review", { ...opts, override: null }, models)
153
+ : null;
120
154
  const [ref, source]: [string, RoleSource] = task
121
155
  ? [task, "task"]
122
- : ws
123
- ? [ws, "workspace"]
124
- : saved !== DEFAULT_ROLE_MODELS[role]
125
- ? [saved, "settings"]
126
- : env
127
- ? [env, "env"]
128
- : [saved, "default"];
156
+ : proj
157
+ ? [proj, "project"]
158
+ : ws
159
+ ? [ws, "workspace"]
160
+ : saved !== DEFAULT_ROLE_MODELS[role] || (role === "advise" && named)
161
+ ? [saved, "settings"]
162
+ : env
163
+ ? [env, "env"]
164
+ : inherited
165
+ ? [inherited.ref, "inherited"]
166
+ : [saved, "default"];
129
167
  const parsed = parseModelRef(ref);
130
168
  return {
131
169
  role,
132
170
  ref: formatModelRef(parsed),
133
- provider: providerFor(parsed.provider, models),
171
+ provider: providerFor(parsed.provider, models, opts.allowPi),
134
172
  model: parsed.model,
135
173
  source,
136
174
  };
@@ -9,6 +9,8 @@
9
9
  export const MODEL_ROLES = [
10
10
  "plan",
11
11
  "implement",
12
+ "analyze",
13
+ "advise",
12
14
  "orchestrator",
13
15
  "review",
14
16
  "scope",
@@ -28,6 +30,14 @@ export const ROLE_LABELS: Record<ModelRole, { label: string; hint: string }> = {
28
30
  label: "Task implementation",
29
31
  hint: "A task's agent, following the plan in its worktree",
30
32
  },
33
+ analyze: {
34
+ label: "Analyst",
35
+ hint: "Reads code, logs and a developer's work; explains and proposes",
36
+ },
37
+ advise: {
38
+ label: "Reviewer",
39
+ hint: "Advises on another role's diff. Not the merge gate",
40
+ },
31
41
  orchestrator: { label: "Orchestrator", hint: "A workspace's orchestrator" },
32
42
  review: {
33
43
  label: "Gate review",
@@ -58,6 +68,8 @@ export const DEFAULT_ROLE_MODELS: Record<ModelRole, string> = {
58
68
  plan: OPUS,
59
69
  review: OPUS,
60
70
  implement: SONNET,
71
+ analyze: OPUS,
72
+ advise: OPUS,
61
73
  orchestrator: OPUS,
62
74
  scope: SONNET,
63
75
  interactive: OPUS,
package/lib/needs-you.ts CHANGED
@@ -17,7 +17,7 @@ export function markSeen(sessionId: string): void {
17
17
  notifyStatusChanged();
18
18
  }
19
19
 
20
- function latestChatItem(sessionId: string): ChatItem | null {
20
+ export function latestChatItem(sessionId: string): ChatItem | null {
21
21
  const row = getDb()
22
22
  .prepare(
23
23
  `SELECT data FROM chat_items WHERE session_id = ? ORDER BY seq DESC LIMIT 1`
@@ -19,6 +19,7 @@ import { UNSET_SERVER_ENV } from "./agents/server-env";
19
19
  import { runInBackground } from "./async-operations";
20
20
  import { tmux } from "./tmux/exec";
21
21
  import { endTmuxSession } from "./tmux/end";
22
+ import { inheritGroup } from "@/lib/session-groups/assignSessions";
22
23
 
23
24
  export interface SpawnWorkerOptions {
24
25
  conductorSessionId: string;
@@ -146,6 +147,7 @@ export async function spawnWorker(
146
147
  agentType,
147
148
  "uncategorized" // project_id
148
149
  );
150
+ inheritGroup(conductorSessionId, sessionId);
149
151
 
150
152
  // Update worktree info if created
151
153
  if (worktreePath) {