@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,303 @@
1
+ import { randomUUID } from "crypto";
2
+ import { db } from "../db";
3
+ import {
4
+ AnalystError,
5
+ type AnalystLink,
6
+ type DigestSignals,
7
+ type Issue,
8
+ type Recommendation,
9
+ type RecommendationState,
10
+ type Verdict,
11
+ } from "./types";
12
+
13
+ interface LinkRow {
14
+ id: string;
15
+ coder_session_id: string;
16
+ analyst_session_id: string;
17
+ paused: number;
18
+ reviewed_through: string | null;
19
+ pending_digest_id: string | null;
20
+ pending_through: string | null;
21
+ digest_signals: string | null;
22
+ created_at: number;
23
+ ended_at: number | null;
24
+ }
25
+
26
+ interface RecRow {
27
+ id: string;
28
+ link_id: string;
29
+ analyst_item_id: string;
30
+ coder_through: string | null;
31
+ verdict: string | null;
32
+ summary: string;
33
+ issues: string;
34
+ signals: string | null;
35
+ prompt: string | null;
36
+ state: string;
37
+ error: string | null;
38
+ sent_text: string | null;
39
+ sent_item_id: string | null;
40
+ edited: number;
41
+ created_at: number;
42
+ decided_at: number | null;
43
+ }
44
+
45
+ const parse = <T>(s: string | null, fallback: T): T => {
46
+ if (!s) return fallback;
47
+ try {
48
+ return JSON.parse(s) as T;
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ };
53
+
54
+ const toLink = (r: LinkRow): AnalystLink => ({
55
+ id: r.id,
56
+ coderSessionId: r.coder_session_id,
57
+ analystSessionId: r.analyst_session_id,
58
+ paused: !!r.paused,
59
+ reviewedThrough: r.reviewed_through,
60
+ pendingDigestId: r.pending_digest_id,
61
+ pendingThrough: r.pending_through,
62
+ digestSignals: parse<DigestSignals | null>(r.digest_signals, null),
63
+ createdAt: r.created_at,
64
+ endedAt: r.ended_at,
65
+ });
66
+
67
+ const toRec = (r: RecRow): Recommendation => ({
68
+ id: r.id,
69
+ linkId: r.link_id,
70
+ analystItemId: r.analyst_item_id,
71
+ coderThrough: r.coder_through,
72
+ verdict: r.verdict as Verdict | null,
73
+ summary: r.summary,
74
+ issues: parse<Issue[]>(r.issues, []),
75
+ signals: parse<DigestSignals | null>(r.signals, null),
76
+ prompt: r.prompt,
77
+ state: r.state as RecommendationState,
78
+ error: r.error,
79
+ sentText: r.sent_text,
80
+ sentItemId: r.sent_item_id,
81
+ edited: !!r.edited,
82
+ createdAt: r.created_at,
83
+ decidedAt: r.decided_at,
84
+ });
85
+
86
+ export function getLink(id: string): AnalystLink | null {
87
+ const r = db.prepare(`SELECT * FROM analyst_links WHERE id = ?`).get(id) as
88
+ LinkRow | undefined;
89
+ return r ? toLink(r) : null;
90
+ }
91
+
92
+ export function getLinkOrThrow(id: string): AnalystLink {
93
+ const link = getLink(id);
94
+ if (!link) throw new AnalystError("Pairing not found", 404);
95
+ return link;
96
+ }
97
+
98
+ // The live link a session is in, on either side.
99
+ export function liveLinkFor(sessionId: string): AnalystLink | null {
100
+ const r = db
101
+ .prepare(
102
+ `SELECT * FROM analyst_links WHERE ended_at IS NULL
103
+ AND (coder_session_id = ? OR analyst_session_id = ?)`
104
+ )
105
+ .get(sessionId, sessionId) as LinkRow | undefined;
106
+ return r ? toLink(r) : null;
107
+ }
108
+
109
+ // Any link, live or ended, the session was the analyst of.
110
+ export function linkOfAnalyst(sessionId: string): AnalystLink | null {
111
+ const r = db
112
+ .prepare(`SELECT * FROM analyst_links WHERE analyst_session_id = ?`)
113
+ .get(sessionId) as LinkRow | undefined;
114
+ return r ? toLink(r) : null;
115
+ }
116
+
117
+ export function liveLinks(): AnalystLink[] {
118
+ return (
119
+ db
120
+ .prepare(`SELECT * FROM analyst_links WHERE ended_at IS NULL`)
121
+ .all() as LinkRow[]
122
+ ).map(toLink);
123
+ }
124
+
125
+ export function insertLink(
126
+ coderSessionId: string,
127
+ analystSessionId: string,
128
+ reviewedThrough: string | null
129
+ ): AnalystLink {
130
+ const id = randomUUID();
131
+ db.prepare(
132
+ `INSERT INTO analyst_links (id, coder_session_id, analyst_session_id,
133
+ reviewed_through, created_at) VALUES (?, ?, ?, ?, ?)`
134
+ ).run(id, coderSessionId, analystSessionId, reviewedThrough, Date.now());
135
+ return getLink(id)!;
136
+ }
137
+
138
+ export function setPausedRow(id: string, paused: boolean): void {
139
+ db.prepare(`UPDATE analyst_links SET paused = ? WHERE id = ?`).run(
140
+ paused ? 1 : 0,
141
+ id
142
+ );
143
+ }
144
+
145
+ export function endLinkRow(id: string): boolean {
146
+ return (
147
+ db
148
+ .prepare(
149
+ `UPDATE analyst_links SET ended_at = ? WHERE id = ? AND ended_at IS NULL`
150
+ )
151
+ .run(Date.now(), id).changes > 0
152
+ );
153
+ }
154
+
155
+ // Recorded before the digest is sent, so a restart can send it again.
156
+ export function markDigestPending(
157
+ id: string,
158
+ digestId: string,
159
+ through: string,
160
+ signals: DigestSignals
161
+ ): void {
162
+ db.prepare(
163
+ `UPDATE analyst_links SET pending_digest_id = ?, pending_through = ?,
164
+ digest_signals = ? WHERE id = ?`
165
+ ).run(digestId, through, JSON.stringify(signals), id);
166
+ }
167
+
168
+ // The analyst answered the digest: its turns count as reviewed.
169
+ export function settleDigest(id: string, digestId: string): boolean {
170
+ return (
171
+ db
172
+ .prepare(
173
+ `UPDATE analyst_links SET reviewed_through = pending_through,
174
+ pending_digest_id = NULL, pending_through = NULL
175
+ WHERE id = ? AND pending_digest_id = ?`
176
+ )
177
+ .run(id, digestId).changes > 0
178
+ );
179
+ }
180
+
181
+ // A digest that never arrived: dropped, so the next one covers its turns.
182
+ export function dropPendingDigest(id: string): void {
183
+ db.prepare(
184
+ `UPDATE analyst_links SET pending_digest_id = NULL, pending_through = NULL
185
+ WHERE id = ?`
186
+ ).run(id);
187
+ }
188
+
189
+ export function listRecommendations(
190
+ linkId: string,
191
+ limit = 20
192
+ ): Recommendation[] {
193
+ return (
194
+ db
195
+ .prepare(
196
+ `SELECT * FROM analyst_recommendations WHERE link_id = ?
197
+ ORDER BY created_at DESC, rowid DESC LIMIT ?`
198
+ )
199
+ .all(linkId, limit) as RecRow[]
200
+ ).map(toRec);
201
+ }
202
+
203
+ export function getRecommendation(id: string): Recommendation | null {
204
+ const r = db
205
+ .prepare(`SELECT * FROM analyst_recommendations WHERE id = ?`)
206
+ .get(id) as RecRow | undefined;
207
+ return r ? toRec(r) : null;
208
+ }
209
+
210
+ export function recommendationFor(
211
+ linkId: string,
212
+ analystItemId: string
213
+ ): Recommendation | null {
214
+ const r = db
215
+ .prepare(
216
+ `SELECT * FROM analyst_recommendations WHERE link_id = ? AND analyst_item_id = ?`
217
+ )
218
+ .get(linkId, analystItemId) as RecRow | undefined;
219
+ return r ? toRec(r) : null;
220
+ }
221
+
222
+ export interface NewRecommendation {
223
+ linkId: string;
224
+ analystItemId: string;
225
+ coderThrough: string | null;
226
+ verdict: Verdict | null;
227
+ summary: string;
228
+ issues: Issue[];
229
+ signals: DigestSignals | null;
230
+ prompt: string | null;
231
+ state: "open" | "invalid";
232
+ error?: string | null;
233
+ }
234
+
235
+ // Once per analyst reply: a repeat is a no-op. A new one retires the
236
+ // cards still open, so only one is ever actionable.
237
+ export function insertRecommendation(
238
+ r: NewRecommendation
239
+ ): Recommendation | null {
240
+ const id = randomUUID();
241
+ const inserted = db.transaction(() => {
242
+ const changes = db
243
+ .prepare(
244
+ `INSERT OR IGNORE INTO analyst_recommendations (id, link_id,
245
+ analyst_item_id, coder_through, verdict, summary, issues, signals,
246
+ prompt, state, error, created_at)
247
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
248
+ )
249
+ .run(
250
+ id,
251
+ r.linkId,
252
+ r.analystItemId,
253
+ r.coderThrough,
254
+ r.verdict,
255
+ r.summary,
256
+ JSON.stringify(r.issues),
257
+ r.signals ? JSON.stringify(r.signals) : null,
258
+ r.prompt,
259
+ r.state,
260
+ r.error ?? null,
261
+ Date.now()
262
+ ).changes;
263
+ if (!changes) return false;
264
+ db.prepare(
265
+ `UPDATE analyst_recommendations SET state = 'superseded', decided_at = ?
266
+ WHERE link_id = ? AND id != ? AND state IN ('open', 'invalid')`
267
+ ).run(Date.now(), r.linkId, id);
268
+ return true;
269
+ })();
270
+ return inserted ? getRecommendation(id) : null;
271
+ }
272
+
273
+ // Moves an open card to its decision, once: false when it was decided.
274
+ export function decideRecommendation(
275
+ id: string,
276
+ state: "sent" | "dismissed",
277
+ sent?: { text: string; itemId: string; edited: boolean }
278
+ ): boolean {
279
+ return (
280
+ db
281
+ .prepare(
282
+ `UPDATE analyst_recommendations SET state = ?, decided_at = ?,
283
+ sent_text = ?, sent_item_id = ?, edited = ?
284
+ WHERE id = ? AND state IN ('open', 'invalid')`
285
+ )
286
+ .run(
287
+ state,
288
+ Date.now(),
289
+ sent?.text ?? null,
290
+ sent?.itemId ?? null,
291
+ sent?.edited ? 1 : 0,
292
+ id
293
+ ).changes > 0
294
+ );
295
+ }
296
+
297
+ // Back to open: a send that failed after the card was claimed.
298
+ export function reopenRecommendation(id: string): void {
299
+ db.prepare(
300
+ `UPDATE analyst_recommendations SET state = 'open', decided_at = NULL,
301
+ sent_text = NULL, sent_item_id = NULL, edited = 0 WHERE id = ?`
302
+ ).run(id);
303
+ }
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+
3
+ // What the analyst says the coding agent should do next.
4
+ export const VERDICTS = ["continue", "done", "blocked", "needs_you"] as const;
5
+ export type Verdict = (typeof VERDICTS)[number];
6
+
7
+ export const RECOMMENDATION_STATES = [
8
+ "open",
9
+ "sent",
10
+ "dismissed",
11
+ "superseded",
12
+ "invalid",
13
+ ] as const;
14
+ export type RecommendationState = (typeof RECOMMENDATION_STATES)[number];
15
+
16
+ export const issueSchema = z.object({
17
+ severity: z.enum(["high", "medium", "low"]),
18
+ title: z.string().trim().min(1).max(200),
19
+ detail: z.string().trim().max(2_000).default(""),
20
+ where: z.string().trim().max(300).optional(),
21
+ });
22
+ export type Issue = z.infer<typeof issueSchema>;
23
+
24
+ export const MAX_ISSUES = 8;
25
+ export const MAX_PROMPT = 4_000;
26
+
27
+ // The fenced `agentos-review` block the analyst ends each review with.
28
+ export const reviewSchema = z.object({
29
+ verdict: z.enum(VERDICTS),
30
+ summary: z.string().trim().min(1).max(1_000),
31
+ issues: z.array(issueSchema).max(MAX_ISSUES).default([]),
32
+ prompt: z.string().trim().max(MAX_PROMPT).nullable().optional(),
33
+ });
34
+ export type Review = z.infer<typeof reviewSchema>;
35
+
36
+ // What a batch of the coding agent's turns did, worked out without a model.
37
+ export interface DigestSignals {
38
+ turns: number;
39
+ toolCalls: number;
40
+ failedTools: number;
41
+ // Test, build, typecheck or lint commands, and whether each passed.
42
+ checks: { command: string; passed: boolean }[];
43
+ filesChanged: string[];
44
+ durationMs: number;
45
+ costUsd: number;
46
+ interrupted: boolean;
47
+ errors: number;
48
+ // Turns that ended without a single tool call.
49
+ noToolTurns: number;
50
+ // Fetched web content: a prompt built on it shouldn't be sent unseen.
51
+ readWeb: boolean;
52
+ }
53
+
54
+ export interface AnalystLink {
55
+ id: string;
56
+ coderSessionId: string;
57
+ analystSessionId: string;
58
+ paused: boolean;
59
+ // The coding agent's last turn_end the analyst has answered for.
60
+ reviewedThrough: string | null;
61
+ // A digest sent and not yet answered, the turn_end it runs to, and what
62
+ // its turns did.
63
+ pendingDigestId: string | null;
64
+ pendingThrough: string | null;
65
+ digestSignals: DigestSignals | null;
66
+ createdAt: number;
67
+ endedAt: number | null;
68
+ }
69
+
70
+ export interface Recommendation {
71
+ id: string;
72
+ linkId: string;
73
+ analystItemId: string;
74
+ coderThrough: string | null;
75
+ verdict: Verdict | null;
76
+ summary: string;
77
+ issues: Issue[];
78
+ signals: DigestSignals | null;
79
+ prompt: string | null;
80
+ state: RecommendationState;
81
+ // Why it's invalid.
82
+ error: string | null;
83
+ sentText: string | null;
84
+ sentItemId: string | null;
85
+ edited: boolean;
86
+ createdAt: number;
87
+ decidedAt: number | null;
88
+ }
89
+
90
+ export interface PaneSession {
91
+ id: string;
92
+ name: string;
93
+ agentType: string;
94
+ model: string | null;
95
+ }
96
+
97
+ // A link as the panes and the native app read it.
98
+ export interface AnalystLinkView extends AnalystLink {
99
+ coder: PaneSession;
100
+ analyst: PaneSession;
101
+ // The coding agent's latest finished turn, to tell a card it has moved on.
102
+ coderLastTurn: string | null;
103
+ recommendations: Recommendation[];
104
+ }
105
+
106
+ export class AnalystError extends Error {
107
+ constructor(
108
+ message: string,
109
+ readonly status: 400 | 404 | 409 = 400
110
+ ) {
111
+ super(message);
112
+ }
113
+ }
114
+
115
+ export const createLinkSchema = z
116
+ .object({
117
+ coderSessionId: z.string().min(1).optional(),
118
+ newCoder: z
119
+ .object({
120
+ projectId: z.string().min(1),
121
+ agentType: z.string().min(1),
122
+ model: z.string().trim().min(1).optional(),
123
+ prompt: z.string().trim().max(20_000).optional(),
124
+ useWorktree: z.boolean().optional(),
125
+ })
126
+ .optional(),
127
+ // The analyst's model for this pair, "provider/model"; else its role's.
128
+ analystModel: z.string().trim().min(1).max(200).optional(),
129
+ reviewLast: z.boolean().optional(),
130
+ })
131
+ .refine((b) => !!b.coderSessionId !== !!b.newCoder, {
132
+ message: "Give coderSessionId or newCoder, not both",
133
+ });
134
+ export type CreateLinkInput = z.infer<typeof createLinkSchema>;
135
+
136
+ export const patchLinkSchema = z.object({ paused: z.boolean() });
137
+
138
+ export const sendSchema = z.object({
139
+ text: z.string().trim().min(1).max(20_000).optional(),
140
+ });
@@ -102,7 +102,11 @@ function lineOf(i: ChatItem): string | null {
102
102
  case "compacted":
103
103
  return "[conversation compacted]";
104
104
  case "turn_end":
105
- return i.interrupted ? "[turn stopped]" : null;
105
+ return i.interrupted
106
+ ? "[turn stopped]"
107
+ : i.ended
108
+ ? `[turn ended early: ${i.ended.reason}]`
109
+ : null;
106
110
  default:
107
111
  return null;
108
112
  }
package/lib/bus/index.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  waitBadges,
39
39
  } from "./waits/store";
40
40
  import { HUMAN, overRateLimit, wakeLine, type BusMessageView } from "./format";
41
+ import { groupNames } from "../session-groups/store";
41
42
 
42
43
  export * from "./format";
43
44
  export {
@@ -64,6 +65,11 @@ export interface Peer {
64
65
  // Names of the sessions it waits on, and of those waiting on it.
65
66
  waitingOn: string[];
66
67
  waitedBy: string[];
68
+ // The user's group it sits in, when it is in one.
69
+ group?: string;
70
+ // The agent role it was started in (lib/models/agent-roles), and its model.
71
+ agentRole?: string;
72
+ model?: string;
67
73
  }
68
74
 
69
75
  // A chat has no pane: its worker is the state, and it can always take a
@@ -99,6 +105,7 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
99
105
  const caller = sessionById(callerId);
100
106
  const orchestrator = heldOrchestrator(caller);
101
107
  const badges = waitBadges();
108
+ const groups = groupNames();
102
109
  const listed = await Promise.all(
103
110
  sessions.map(async (s, i) => {
104
111
  const info = await peerState(s);
@@ -114,6 +121,12 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
114
121
  orchestratorOf: orchestratorOf(s),
115
122
  waitingOn: badges.get(s.id)?.on.map((w) => w.name) ?? [],
116
123
  waitedBy: badges.get(s.id)?.by.map((w) => w.name) ?? [],
124
+ ...(s.agent_role && { agentRole: s.agent_role }),
125
+ ...(s.agent_role && s.model && { model: s.model }),
126
+ ...(s.session_group_id &&
127
+ groups.has(s.session_group_id) && {
128
+ group: groups.get(s.session_group_id),
129
+ }),
117
130
  ...(caller?.workspace_id &&
118
131
  s.role === "orchestrator" &&
119
132
  s.workspace_id === caller.workspace_id && { yours: true }),
@@ -4,6 +4,7 @@ import { getProject } from "../projects";
4
4
  import { getWorkspace } from "../workspaces";
5
5
  import { previousNames } from "../session-names";
6
6
  import { resolveRef, type Candidate } from "./resolve";
7
+ import { resolveRoleAlias, roleAliasOf } from "./role-alias";
7
8
 
8
9
  export type ReadableSession = Session & { project_name: string | null };
9
10
 
@@ -79,6 +80,17 @@ export function resolveReadable(callerId: string, ref: string): Readable {
79
80
  a.updated_at < b.updated_at ? 1 : -1
80
81
  );
81
82
  const mine = all.filter((s) => inScope(s, scope));
83
+ const alias = roleAliasOf(ref);
84
+ if (alias) {
85
+ const found = resolveRoleAlias(
86
+ alias,
87
+ mine.filter(isLive),
88
+ all.find((s) => s.id === callerId)
89
+ );
90
+ return found.ok
91
+ ? { ok: true, session: found.session }
92
+ : { ok: false, external: found.none, error: found.error };
93
+ }
82
94
  let none = "";
83
95
  for (const group of [mine.filter(isLive), mine.filter((s) => !isLive(s))]) {
84
96
  const r = resolveRef(ref, candidates(group));
@@ -0,0 +1,70 @@
1
+ /**
2
+ * "@reviewer", "@developer", "@analyst", "@supervisor": the live session in
3
+ * that agent role. The caller's crew first, then its project; a supervisor
4
+ * nobody started is the workspace's orchestrator. More than one match
5
+ * refuses and lists them, like any other name.
6
+ */
7
+
8
+ import type { Session } from "../db";
9
+
10
+ type RoleSession = Pick<
11
+ Session,
12
+ | "id"
13
+ | "name"
14
+ | "role"
15
+ | "agent_role"
16
+ | "crew_id"
17
+ | "project_id"
18
+ | "workspace_id"
19
+ >;
20
+ import {
21
+ AGENT_ROLES,
22
+ isAgentRole,
23
+ type AgentRole,
24
+ } from "../models/agent-roles";
25
+
26
+ export type RoleAlias<T extends RoleSession = Session> =
27
+ { ok: true; session: T } | { ok: false; none: boolean; error: string };
28
+
29
+ export const roleAliasOf = (ref: string): AgentRole | null => {
30
+ const m = /^@([a-z]+)$/i.exec(ref.trim());
31
+ const role = m?.[1].toLowerCase();
32
+ return isAgentRole(role) ? role : null;
33
+ };
34
+
35
+ const label = (s: RoleSession) => `${s.name} (id ${s.id.slice(0, 8)})`;
36
+
37
+ export function resolveRoleAlias<T extends RoleSession>(
38
+ role: AgentRole,
39
+ sessions: T[],
40
+ caller: RoleSession | undefined
41
+ ): RoleAlias<T> {
42
+ const others = sessions.filter((s) => s.id !== caller?.id);
43
+ const inRole = others.filter((s) => s.agent_role === role);
44
+ const scopes = [
45
+ caller?.crew_id ? inRole.filter((s) => s.crew_id === caller.crew_id) : [],
46
+ caller?.project_id && caller.project_id !== "uncategorized"
47
+ ? inRole.filter((s) => s.project_id === caller.project_id)
48
+ : [],
49
+ ];
50
+ for (const found of scopes) {
51
+ if (found.length === 1) return { ok: true, session: found[0] };
52
+ if (found.length > 1)
53
+ return {
54
+ ok: false,
55
+ none: false,
56
+ error: `"@${role}" matches ${found.length} sessions: ${found.map(label).join(", ")}. Use the name or the id`,
57
+ };
58
+ }
59
+ if (role === "supervisor" && caller?.workspace_id) {
60
+ const orchestrator = others.find(
61
+ (s) => s.role === "orchestrator" && s.workspace_id === caller.workspace_id
62
+ );
63
+ if (orchestrator) return { ok: true, session: orchestrator };
64
+ }
65
+ return {
66
+ ok: false,
67
+ none: true,
68
+ error: `No ${role} is running in your crew or project. Roles: ${AGENT_ROLES.map((r) => `@${r}`).join(", ")}. Run: aos peers`,
69
+ };
70
+ }
@@ -9,6 +9,7 @@ import { previousNames } from "../session-names";
9
9
  import { sessionWorkspace } from "../schedules/store";
10
10
  import { heldTarget } from "./held-reply";
11
11
  import { resolveRef, type Candidate } from "./resolve";
12
+ import { resolveRoleAlias, roleAliasOf } from "./role-alias";
12
13
 
13
14
  export function liveSessions(): Session[] {
14
15
  return db
@@ -86,6 +87,8 @@ export function tryResolveTarget(
86
87
  | { ok: true; session: Session; note?: string }
87
88
  | { ok: false; none: boolean; error: string } {
88
89
  const sessions = liveSessions();
90
+ const alias = roleAliasOf(ref);
91
+ if (alias) return resolveRoleAlias(alias, sessions, sessionById(callerId));
89
92
  const own = ownOrchestrator(ref, sessions, callerId);
90
93
  if (own) return { ok: true, session: own };
91
94
  const r = resolveRef(ref, candidates(sessions));