@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
@@ -22,6 +22,7 @@ import {
22
22
  import { assertMovable, createTask, dropTask, moveTask } from "../tasks";
23
23
  import { getHost, hostIdNamed } from "../hosts";
24
24
  import { queueEvent } from "./events";
25
+ import { claimQueuedEvent, claimWait } from "./claim-wait";
25
26
  import {
26
27
  findQueued,
27
28
  queuedLabel,
@@ -45,6 +46,7 @@ import { braked } from "./brakes";
45
46
  import { getOrchestrator } from "./home";
46
47
  import { addNote } from "./notes";
47
48
  import { findWorkspaceSession } from "./read";
49
+ import { assignSessions, resolveGroup } from "../session-groups";
48
50
  import {
49
51
  assertTakesMessages,
50
52
  findQueuedTarget,
@@ -162,7 +164,9 @@ export async function startTask(
162
164
  machine?: "mac" | "any";
163
165
  now?: boolean;
164
166
  nowReason?: string;
167
+ ignoreClaims?: boolean;
165
168
  piece?: string;
169
+ group?: string;
166
170
  } = {}
167
171
  ): Promise<string> {
168
172
  if (opts.planOnly && opts.plan === false)
@@ -170,6 +174,7 @@ export async function startTask(
170
174
  if (opts.now && !opts.nowReason)
171
175
  throw new Error("now needs now_reason: say why it can't wait for a slot");
172
176
  const project = workspaceProject(workspaceId, projectRef);
177
+ const groupId = groupIdOf(workspaceId, opts.group);
173
178
  const model = parseOverride(opts.model) ?? undefined;
174
179
  const startedBy = orchestratorId(workspaceId);
175
180
  const pieceId = opts.piece
@@ -182,6 +187,19 @@ export async function startTask(
182
187
  );
183
188
  const view = opts.view ?? DEFAULT_START_VIEW;
184
189
  const slots = slotState(project.id);
190
+ // Work claimed elsewhere: a strong overlap queues this start after it,
191
+ // unless it starts now or the caller said to ignore claims.
192
+ const claims = opts.ignoreClaims
193
+ ? null
194
+ : await claimWait(
195
+ { projectId: project.id, name: opts.name, prompt, base: opts.base },
196
+ {
197
+ workspaceId,
198
+ queue: !opts.now && !opts.after,
199
+ }
200
+ );
201
+ const after = opts.after ?? claims?.after;
202
+ const headsUp = claims?.headsUp ?? "";
185
203
  // Held for a slot or its `after`, it starts by itself through the brakes;
186
204
  // you get an event when it does.
187
205
  const out = await startOrQueue(
@@ -199,11 +217,12 @@ export async function startTask(
199
217
  startedVia: "orchestrator",
200
218
  pieceId,
201
219
  baseBranch: opts.base || undefined,
202
- after: opts.after,
220
+ after,
203
221
  originWorkspaceId: workspaceId,
204
222
  priority: opts.priority,
205
223
  machine: opts.machine,
206
224
  now: opts.now,
225
+ groupId,
207
226
  },
208
227
  () =>
209
228
  braked(
@@ -224,14 +243,17 @@ export async function startTask(
224
243
  parentSessionId: startedBy,
225
244
  startedVia: "orchestrator",
226
245
  pieceId,
246
+ groupId,
227
247
  }),
228
248
  (s) => s.id
229
249
  )
230
250
  );
231
251
  if ("queued" in out) {
232
252
  const { queued } = out;
253
+ if (claims?.strong && !opts.after)
254
+ claimQueuedEvent(workspaceId, queued, claims.strong);
233
255
  const builds = pieceId ? ` It builds ${pieceLabel(pieceId)}.` : "";
234
- return `Queued (position ${queued.position}) task "${queued.name}" (id ${queued.id.slice(0, 8)}) in ${project.name}${queued.after ? `, after ${queued.after}` : ""}.${builds} It starts by itself; you'll get an event when it does.`;
256
+ return `Queued (position ${queued.position}) task "${queued.name}" (id ${queued.id.slice(0, 8)}) in ${project.name}${queued.after ? `, after ${queued.after}` : ""}.${builds} It starts by itself; you'll get an event when it does.${headsUp}`;
235
257
  }
236
258
  const task = out.started;
237
259
  if (opts.priority) stampPriority(task.id, opts.priority);
@@ -248,7 +270,7 @@ export async function startTask(
248
270
  opts.plan && !opts.planOnly && task.plan_phase !== "planning"
249
271
  ? ` Plan step skipped: ${view === "terminal" ? "a terminal task has none" : "planning and implementation use the same model"}.`
250
272
  : "";
251
- return `Started task "${task.name}" (id ${task.id.slice(0, 8)}, ${view}) in ${project.name} on ${task.branch_name} (from ${task.base_branch}). It ends in a PR; you'll get an event when it opens.${pieceId ? ` It builds ${pieceLabel(pieceId)}.` : ""}${skipped}${planOnlyNote}${past ? " Started past the running-task limit." : ""}`;
273
+ return `Started task "${task.name}" (id ${task.id.slice(0, 8)}, ${view}) in ${project.name} on ${task.branch_name} (from ${task.base_branch}). It ends in a PR; you'll get an event when it opens.${pieceId ? ` It builds ${pieceLabel(pieceId)}.` : ""}${skipped}${planOnlyNote}${past ? " Started past the running-task limit." : ""}${headsUp}`;
252
274
  }
253
275
 
254
276
  // Starts a queued task now, past the running-task limit and its `after`.
@@ -299,9 +321,15 @@ export async function startSession(
299
321
  workspaceId: string,
300
322
  projectRef: string,
301
323
  prompt: string,
302
- opts: { name?: string; view?: StartView; model?: string } = {}
324
+ opts: {
325
+ name?: string;
326
+ view?: StartView;
327
+ model?: string;
328
+ group?: string;
329
+ } = {}
303
330
  ): Promise<string> {
304
331
  const project = workspaceProject(workspaceId, projectRef);
332
+ const groupId = groupIdOf(workspaceId, opts.group);
305
333
  const startedBy = orchestratorId(workspaceId);
306
334
  const model = parseOverride(opts.model);
307
335
  if (model && typeof model !== "string")
@@ -326,6 +354,7 @@ export async function startSession(
326
354
  startedBy,
327
355
  parentSessionId: startedBy,
328
356
  startedVia: "orchestrator",
357
+ groupId,
329
358
  })
330
359
  ).session
331
360
  : spawnSession({
@@ -335,6 +364,7 @@ export async function startSession(
335
364
  model,
336
365
  startedBy,
337
366
  parentSessionId: startedBy,
367
+ groupId,
338
368
  }),
339
369
  (s) => s.id
340
370
  );
@@ -408,6 +438,26 @@ export async function drop(
408
438
  return `Dropped ${task.name}: its PR is closed and its worktree removed.`;
409
439
  }
410
440
 
441
+ // One of Saad's groups in the workspace by name; the error lists the ones
442
+ // there are. The orchestrator files into groups, it never creates them.
443
+ function groupIdOf(workspaceId: string, name?: string): string | undefined {
444
+ return name ? resolveGroup(workspaceId, name).id : undefined;
445
+ }
446
+
447
+ // Files a session of this workspace in a group, or out of its group (null).
448
+ export async function setGroup(
449
+ workspaceId: string,
450
+ ref: string,
451
+ group: string | null
452
+ ): Promise<string> {
453
+ const s = findWorkspaceSession(workspaceId, ref);
454
+ const target = group === null ? null : resolveGroup(workspaceId, group);
455
+ assignSessions([s.id], target?.id ?? null);
456
+ return target
457
+ ? `Filed ${s.name} in "${target.name}".`
458
+ : `Took ${s.name} out of its group.`;
459
+ }
460
+
411
461
  // Finished work: merged through the gates if its PR is open, then
412
462
  // stopped and cleaned up, then left on the Done shelf (lib/done).
413
463
  export async function done(workspaceId: string, ref: string): Promise<string> {
@@ -122,11 +122,12 @@ Reading:
122
122
  Acting:
123
123
  - \`${TOOL_NAMES.send}\` (session, message, scope_change?): message a session; it arrives as its next prompt. scope_change records it on the task's brief for its review.
124
124
  - \`${TOOL_NAMES.message_orchestrator}\` (workspace, message): message another workspace's orchestrator, the only thing you reach outside this workspace. Use it to hand over a bug or ask for something in its projects; it decides what to do there.
125
- - \`${TOOL_NAMES.start_task}\` (project, prompt, base?, name?, view?, after?, priority?, machine?, now?, now_reason?, model?, plan, plan_only?, plan_reason): a task in its own worktree that ends in a PR. priority (urgent, blocking, normal, background) sets where it stands in the queue; machine mac pins work that needs this Mac. A busy or loaded machine holds a queued start and says why; don't retry it. Write the prompt as a full brief; name it in 2-6 words, or it's named from the prompt. It runs as a chat; pass view terminal only when the job needs a TUI. Pass after (a task's id or name, or "any") to hold it until that task finishes, instead of remembering it in your notes. Over the workspace's running task limit it's queued too: the result says "Queued (position N)", it starts by itself, and you get an event when it does. now true (with now_reason, not with after) starts it at once past the limit instead; it's logged for Saad, so use it only when it can't wait. Its models follow the role settings (it plans on the planning model, then implements on the implementation one); State plan and plan_reason for every task in a production project (see "Triage every job"); a fast project skips the plan unless you pass plan true. Pass model ("provider/model", or a map of role to one) only when Saad asks for one, or to put implementation on Opus when the work needs deep reasoning (say why in plan_reason or a note); another provider than anthropic only when Saad asks.
125
+ - \`${TOOL_NAMES.start_task}\` (project, prompt, base?, name?, view?, after?, priority?, machine?, now?, now_reason?, ignore_claims?, model?, plan, plan_only?, plan_reason): a task in its own worktree that ends in a PR. priority (urgent, blocking, normal, background) sets where it stands in the queue; machine mac pins work that needs this Mac. A busy or loaded machine holds a queued start and says why; don't retry it. Write the prompt as a full brief; name it in 2-6 words, or it's named from the prompt. It runs as a chat; pass view terminal only when the job needs a TUI. Pass after (a task's id or name, or "any") to hold it until that task finishes, instead of remembering it in your notes. Over the workspace's running task limit it's queued too: the result says "Queued (position N)", it starts by itself, and you get an event when it does. If another session (here or on a linked machine) has claimed the same branch or files, it is queued after that work and you get an event saying so; pass ignore_claims only when the overlap is wrong. now true (with now_reason, not with after) starts it at once past the limit instead; it's logged for Saad, so use it only when it can't wait. Its models follow the role settings (it plans on the planning model, then implements on the implementation one); State plan and plan_reason for every task in a production project (see "Triage every job"); a fast project skips the plan unless you pass plan true. Pass model ("provider/model", or a map of role to one) only when Saad asks for one, or to put implementation on Opus when the work needs deep reasoning (say why in plan_reason or a note); another provider than anthropic only when Saad asks.
126
126
  - \`${TOOL_NAMES.plan_pieces}\` (task, merge?, drop?): when a plan split into pieces (the plan-done event names them), list them; merge or drop edits the ones still waiting. A piece starts by itself once what it waits on has merged (or deployed); you don't start it.
127
127
  - \`${TOOL_NAMES.approve_plan}\` (task, message?): continue a plan_only task whose plan is ready; it implements on the implementation model, once. message amends the plan and is recorded on the task's brief as a scope change. Reject a plan with drop.
128
128
  - \`${TOOL_NAMES.set_task_model}\` (task, model, message?): move a paused task to another model and carry on in the same conversation and worktree; it keeps that model from then on. Only when Saad asks.
129
129
  - \`${TOOL_NAMES.start_session}\` (project, prompt, name?, view?, model?): a session without a worktree or PR, a chat unless view is terminal. It runs on the implementation model (Sonnet) unless you pass model; use it for work outside a repository, with the steps in the prompt.
130
+ - \`${TOOL_NAMES.set_group}\` (session, group): file a session in one of Saad's groups by name, or out of its group with null. Saad's groups organise his sidebar: put a task in one when he asks, or when it continues work that's already in that group. \`start_task\` and \`start_session\` take a group too. A group has to exist: you can't create, rename or delete one, so if he asks for one that doesn't, say so.
130
131
  - \`${TOOL_NAMES.recipes}\` (name?): the workspace's recipes with their last runs; with a name, one in full.
131
132
  - \`${TOOL_NAMES.run_recipe}\` (name, note?): run a recipe now: a fresh task or session from its saved brief. See "Recipes".
132
133
  - \`${TOOL_NAMES.recipe_save}\` (name, project, kind, brief, model?, view?): save a recipe (kind task ends in a PR; session has none).
@@ -0,0 +1,71 @@
1
+ // What the orchestrator's start_task does about work claimed elsewhere: a
2
+ // strong overlap queues the start after that claim, any other overlap is a
3
+ // heads-up. A person's start never comes through here.
4
+
5
+ import { checkStart, overlapText } from "../claims/startCheck";
6
+ import { machineId } from "../claims/machine";
7
+ import { IN_FLIGHT, resolveAfter, sessionOf } from "../tasks/queue-rows";
8
+ import { queueEvent } from "./events";
9
+ import type { Overlap } from "../claims/types";
10
+
11
+ export interface ClaimWait {
12
+ // The `after` to queue with: a local task's id, or "claim:<id>".
13
+ after?: string;
14
+ // The overlap that set it, for the event once the start is queued.
15
+ strong: Overlap | null;
16
+ headsUp: string;
17
+ }
18
+
19
+ const isLocalTask = (sessionId: string) => {
20
+ const s = sessionOf(sessionId);
21
+ return !!s?.task_status && IN_FLIGHT.includes(s.task_status);
22
+ };
23
+
24
+ // A local task is waited on as a task (the existing `after`); anything else
25
+ // as a claim.
26
+ function afterOf(o: Overlap, workspaceId: string): string | undefined {
27
+ const c = o.b;
28
+ if (c.origin !== machineId() || !c.sessionId) return `claim:${c.id}`;
29
+ if (!isLocalTask(c.sessionId)) return `claim:${c.id}`;
30
+ try {
31
+ return resolveAfter(c.sessionId, workspaceId).id;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ export async function claimWait(
38
+ start: { projectId: string; name?: string; prompt: string; base?: string },
39
+ opts: { workspaceId: string; queue: boolean }
40
+ ): Promise<ClaimWait> {
41
+ const check = await checkStart(start);
42
+ if (!check.overlaps.length) return { strong: null, headsUp: "" };
43
+ const strong =
44
+ (opts.queue && check.overlaps.find((o) => o.strength === "strong")) || null;
45
+ const after = strong ? afterOf(strong, opts.workspaceId) : undefined;
46
+ const lines = check.overlaps
47
+ .slice(0, 3)
48
+ .map((o) => `- ${o.strength}: ${overlapText(o)}`);
49
+ return {
50
+ after,
51
+ strong: after ? strong : null,
52
+ headsUp: `\nHeads-up, work already claimed:\n${lines.join("\n")}`,
53
+ };
54
+ }
55
+
56
+ // Tells the orchestrator it was queued behind a claim, and how to override.
57
+ export function claimQueuedEvent(
58
+ workspaceId: string,
59
+ queued: { id: string; name: string },
60
+ o: Overlap
61
+ ): void {
62
+ const shared = o.shared.length
63
+ ? ` (shared: ${o.shared.slice(0, 5).join(", ")})`
64
+ : "";
65
+ queueEvent(
66
+ workspaceId,
67
+ `claim-queued:${queued.id}`,
68
+ queued.id,
69
+ `Queued "${queued.name}" (id ${queued.id.slice(0, 8)}) after ${overlapText(o)}${shared}. It starts when that work ends; start_now overrides, or drop it.`
70
+ );
71
+ }
@@ -10,6 +10,8 @@
10
10
  import { runForSession } from "../recipes/store";
11
11
  import { db, type StackItemStatus, type StackStatus } from "../db";
12
12
  import { isFinished, type TaskPR } from "../tasks/state";
13
+ import { eventLine } from "../claims/overlapText";
14
+ import type { SessionOverlap } from "../claims/mine";
13
15
  import { ciWord } from "./describe";
14
16
  import type { SessionFacts } from "./facts";
15
17
  import { untrusted } from "./untrusted";
@@ -192,6 +194,29 @@ function stackConditions(s: StackFacts): Condition[] {
192
194
  return out;
193
195
  }
194
196
 
197
+ // A strong overlap between a session here and other work, anywhere. Held
198
+ // until it ends, so it goes through the events' two-diff damping and
199
+ // REFIRE_MS, and an overlap that only flickers is dropped.
200
+ function overlapConditions(
201
+ facts: SessionFacts[],
202
+ overlaps: SessionOverlap[]
203
+ ): Condition[] {
204
+ const names = new Map(facts.map((f) => [f.id, f.name]));
205
+ return overlaps.flatMap((o) => {
206
+ const name = names.get(o.sessionId);
207
+ if (!name) return [];
208
+ return [
209
+ {
210
+ key: conditionKey(`overlap:${o.pairKey}`, o.sessionId),
211
+ subject: o.sessionId,
212
+ line: eventLine(o, name),
213
+ sticky: false,
214
+ low: true,
215
+ },
216
+ ];
217
+ });
218
+ }
219
+
195
220
  // Sessions the orchestrator itself messaged this recently don't raise
196
221
  // "needs input": that's most likely it waiting on its own question.
197
222
  export const QUIET_AFTER_MESSAGE_MS = 2 * 60 * 1000;
@@ -200,11 +225,13 @@ export function conditionsFor(
200
225
  facts: SessionFacts[],
201
226
  stacks: StackFacts[],
202
227
  now = Date.now(),
203
- quiet: ReadonlySet<string> = new Set()
228
+ quiet: ReadonlySet<string> = new Set(),
229
+ overlaps: SessionOverlap[] = []
204
230
  ): Condition[] {
205
231
  return [
206
232
  ...facts.flatMap((f) => sessionConditions(f, now, quiet.has(f.id))),
207
233
  ...stacks.flatMap(stackConditions),
234
+ ...overlapConditions(facts, overlaps),
208
235
  ];
209
236
  }
210
237
 
@@ -37,6 +37,7 @@ function sessionLine(f: SessionFacts): string {
37
37
  task += `, BLOCKED: ${untrusted(f.name, f.task.blocked)}`;
38
38
  parts.push(task);
39
39
  }
40
+ if (f.group) parts.push(`in group "${f.group}"`);
40
41
  if (f.plan)
41
42
  parts.push(
42
43
  `${f.plan.awaiting ? "plan ready, awaiting your approve_plan" : f.plan.planned ? "planned" : "no plan"}${f.plan.reason ? `: ${untrusted(f.name, f.plan.reason)}` : ""}`
@@ -66,7 +67,8 @@ function sessionLine(f: SessionFacts): string {
66
67
  // The sessions tool's answer: one line per session, busiest news first.
67
68
  export function describeSessions(
68
69
  workspace: string,
69
- facts: SessionFacts[]
70
+ facts: SessionFacts[],
71
+ groups: readonly string[] = []
70
72
  ): string {
71
73
  if (!facts.length) return `No sessions are running in ${workspace}.`;
72
74
  const rank = { waiting: 0, running: 1, idle: 2, dead: 3 };
@@ -82,5 +84,8 @@ export function describeSessions(
82
84
  return [
83
85
  `${facts.length} ${noun} in ${workspace}${summary}:`,
84
86
  ...sorted.map(sessionLine),
87
+ ...(groups.length
88
+ ? [`Saad's groups here: ${groups.map((g) => `"${g}"`).join(", ")}.`]
89
+ : []),
85
90
  ].join("\n");
86
91
  }
@@ -5,6 +5,7 @@
5
5
  * read this.
6
6
  */
7
7
 
8
+ import { groupNames } from "../session-groups/store";
8
9
  import { db, type Session, type StackItemStatus } from "../db";
9
10
  import { statusDetector } from "../status-detector";
10
11
  import { hostLink } from "../hosts/remote-api";
@@ -12,7 +13,7 @@ import { peerStatus } from "../hosts/peer-sessions";
12
13
  import { chatCompacting, chatState } from "../chat/runner";
13
14
  import { chatActivityLine, chatStall, type Stall } from "../chat/activity";
14
15
  import { lastUserTask } from "../chat/store";
15
- import { needsYou } from "../needs-you";
16
+ import { latestChatItem, needsYou } from "../needs-you";
16
17
  import { programSummary } from "../program-status/store";
17
18
  import { sessionRowInfo } from "../session-meta";
18
19
  import { taskView, type TaskPR, type TaskState } from "../tasks";
@@ -43,6 +44,8 @@ export interface SessionFacts {
43
44
  stalled?: Stall | null;
44
45
  // Its agent is summarising the context: not stuck, not plain work.
45
46
  compacting?: boolean;
47
+ // Saad's group it sits in, when it is in one.
48
+ group?: string | null;
46
49
  // Last sign of life, in ms.
47
50
  lastActive: number;
48
51
  // Open waits (aos send --await, aos wait): who it waits on, and who waits on it.
@@ -167,9 +170,14 @@ export async function statusOf(s: Session): Promise<{
167
170
  const state = chatState(s.id);
168
171
  const status: FactStatus =
169
172
  state === "running" ? "running" : needsYou(s, state) ? "waiting" : "idle";
173
+ const latest = status === "running" ? null : latestChatItem(s.id);
174
+ const early =
175
+ latest?.kind === "turn_end" && !latest.interrupted ? latest.ended : null;
170
176
  return {
171
177
  status,
172
- activity: chatActivityLine(s.id) ?? lastUserTask(s.id),
178
+ activity: early
179
+ ? `ended its turn early (${early.reason}): the task is not finished`
180
+ : (chatActivityLine(s.id) ?? lastUserTask(s.id)),
173
181
  needsInput: state === "waiting",
174
182
  };
175
183
  }
@@ -308,6 +316,7 @@ export async function sessionFacts(
308
316
  ): Promise<SessionFacts[]> {
309
317
  await statusDetector.refreshCache();
310
318
  const badges = waitBadges();
319
+ const groups = groupNames();
311
320
  return Promise.all(
312
321
  workspaceSessions(workspaceId).map(async (s) => {
313
322
  const [{ status, activity, needsInput }, task] = await Promise.all([
@@ -336,6 +345,7 @@ export async function sessionFacts(
336
345
  s.view === "chat" && !hostLink(s.host_id) ? chatStall(s.id) : null,
337
346
  compacting:
338
347
  s.view === "chat" && !hostLink(s.host_id) && !!chatCompacting(s.id),
348
+ group: groups.get(s.session_group_id ?? "") ?? null,
339
349
  lastActive: await lastActiveFor(s),
340
350
  branch: s.branch_name,
341
351
  quick: !!s.quick_of,
@@ -91,7 +91,11 @@ function itemText(i: ChatItem): string | null {
91
91
  case "error":
92
92
  return `[error] ${i.message}`;
93
93
  case "turn_end":
94
- return i.interrupted ? "[turn stopped]" : "[turn ended]";
94
+ return i.interrupted
95
+ ? "[turn stopped]"
96
+ : i.ended
97
+ ? `[turn ended early: ${i.ended.reason}] ${i.ended.notice}`
98
+ : "[turn ended]";
95
99
  case "command_output":
96
100
  return i.text;
97
101
  case "note":
@@ -1,6 +1,7 @@
1
1
  import { resumeOnModel } from "../models/resume";
2
2
  import { workspaceTask } from "./targets";
3
3
  import { getWorkspace } from "../workspaces";
4
+ import { listGroups } from "../session-groups/store";
4
5
  import {
5
6
  approveTaskPlan,
6
7
  archive,
@@ -11,6 +12,7 @@ import {
11
12
  send,
12
13
  stack,
13
14
  stackStatus,
15
+ setGroup,
14
16
  startSession,
15
17
  startTask,
16
18
  startNow,
@@ -19,6 +21,10 @@ import {
19
21
  resumeTask,
20
22
  stop,
21
23
  } from "./act";
24
+ import { describeClaims, liveClaims } from "../claims";
25
+ import { repoKey } from "../git-remote";
26
+ import { expandHome } from "../tasks/session";
27
+ import { workspaceProjects } from "./brief";
22
28
  import { readCards } from "./cards";
23
29
  import { readLogs } from "./logs";
24
30
  import { describeSessions } from "./describe";
@@ -59,6 +65,7 @@ const ACTING = new Set<ToolName>([
59
65
  "start_task",
60
66
  "set_task_model",
61
67
  "start_session",
68
+ "set_group",
62
69
  "run_recipe",
63
70
  "recipe_save",
64
71
  "recipe_update",
@@ -83,6 +90,21 @@ const ACTING = new Set<ToolName>([
83
90
  "add_project",
84
91
  ]);
85
92
 
93
+ // What live sessions on other machines claim in this workspace's repos.
94
+ async function claimedElsewhere(w: string): Promise<string> {
95
+ const keys = await Promise.all(
96
+ workspaceProjects(w)
97
+ .filter((p) => !p.host_id || p.host_id === "local")
98
+ .map((p) => repoKey(expandHome(p.working_directory)))
99
+ );
100
+ const repos = new Set(keys.filter((k): k is string => !!k));
101
+ if (!repos.size) return "";
102
+ return describeClaims(
103
+ liveClaims().filter((c) => repos.has(c.repo)),
104
+ { onlyOthers: true }
105
+ );
106
+ }
107
+
86
108
  // The orchestrator's tools, each scoped to its workspace and its arguments
87
109
  // validated, answered as compact text.
88
110
  export async function runTool(
@@ -97,14 +119,20 @@ export async function runTool(
97
119
  switch (tool) {
98
120
  case "sessions": {
99
121
  p(tool, raw);
100
- const [facts, external] = await Promise.all([
122
+ const [facts, external, claimed] = await Promise.all([
101
123
  sessionFacts(w),
102
124
  externalSessions(w).catch(() => []),
125
+ claimedElsewhere(w).catch(() => ""),
103
126
  ]);
104
127
  resolveStaleAsks(w);
105
128
  return [
106
- describeSessions(workspace.name, facts),
129
+ describeSessions(
130
+ workspace.name,
131
+ facts,
132
+ listGroups(w).map((g) => g.name)
133
+ ),
107
134
  describeExternal(external),
135
+ claimed,
108
136
  ]
109
137
  .filter(Boolean)
110
138
  .join("\n\n");
@@ -147,7 +175,9 @@ export async function runTool(
147
175
  machine: a.machine,
148
176
  now: a.now,
149
177
  nowReason: a.now_reason,
178
+ ignoreClaims: a.ignore_claims,
150
179
  piece: a.piece,
180
+ group: a.group,
151
181
  });
152
182
  }
153
183
  case "start_now": {
@@ -187,8 +217,13 @@ export async function runTool(
187
217
  name: a.name,
188
218
  view: a.view,
189
219
  model: a.model,
220
+ group: a.group,
190
221
  });
191
222
  }
223
+ case "set_group": {
224
+ const a = p(tool, raw);
225
+ return setGroup(w, a.session, a.group);
226
+ }
192
227
  case "quick_change": {
193
228
  const a = p(tool, raw);
194
229
  return quickChange(w, a.project, a.name);
@@ -20,6 +20,7 @@ export const TOOL_NAMES = {
20
20
  start_task: name("start_task"),
21
21
  set_task_model: name("set_task_model"),
22
22
  start_session: name("start_session"),
23
+ set_group: name("set_group"),
23
24
  quick_change: name("quick_change"),
24
25
  quick_pr: name("quick_pr"),
25
26
  stack: name("stack"),
@@ -22,6 +22,16 @@ const view = z
22
22
  "How its agent runs: chat (the default), or terminal only when the job needs a TUI (an interactive program, a full-screen tool)"
23
23
  );
24
24
 
25
+ const group = z
26
+ .string()
27
+ .trim()
28
+ .min(1)
29
+ .max(60)
30
+ .optional()
31
+ .describe(
32
+ "One of Saad's groups in this workspace, by name (see sessions): file it there when he asked, or when it continues work already in that group. It must exist; you can't create groups"
33
+ );
34
+
25
35
  const sweepMachine = z
26
36
  .string()
27
37
  .trim()
@@ -96,6 +106,7 @@ export const TOOL_SHAPES = {
96
106
  },
97
107
  start_task: {
98
108
  project,
109
+ group,
99
110
  prompt: z
100
111
  .string()
101
112
  .trim()
@@ -122,7 +133,7 @@ export const TOOL_SHAPES = {
122
133
  .max(200)
123
134
  .optional()
124
135
  .describe(
125
- 'Queue it until this task finishes (id or name), or "any" for whichever running task finishes first'
136
+ 'Queue it until this task finishes (id or name), or "any" for whichever running task finishes first, or "claim:<id>" for work another machine has claimed'
126
137
  ),
127
138
  priority: z
128
139
  .enum(PRIORITIES)
@@ -149,6 +160,12 @@ export const TOOL_SHAPES = {
149
160
  .max(300)
150
161
  .optional()
151
162
  .describe("With now: why it can't wait for a slot; logged for Saad"),
163
+ ignore_claims: z
164
+ .boolean()
165
+ .optional()
166
+ .describe(
167
+ "Don't queue it after work another session (here or on a linked machine) has claimed on the same branch or files; start it as usual. Not needed with now"
168
+ ),
152
169
  piece: z
153
170
  .string()
154
171
  .trim()
@@ -196,6 +213,7 @@ export const TOOL_SHAPES = {
196
213
  },
197
214
  start_session: {
198
215
  project,
216
+ group,
199
217
  prompt: z.string().trim().min(1).max(20000),
200
218
  name: z
201
219
  .string()
@@ -211,6 +229,18 @@ export const TOOL_SHAPES = {
211
229
  'One model for the session, "provider/model"; default: the implementation model. Opus only when the work needs deep reasoning'
212
230
  ),
213
231
  },
232
+ set_group: {
233
+ session,
234
+ group: z
235
+ .string()
236
+ .trim()
237
+ .min(1)
238
+ .max(60)
239
+ .nullable()
240
+ .describe(
241
+ "One of Saad's groups in this workspace by name, or null to take the session out of its group"
242
+ ),
243
+ },
214
244
  quick_change: {
215
245
  project,
216
246
  name: z
@@ -126,11 +126,13 @@ const DESCRIPTIONS: Record<ToolName, string> = {
126
126
  message_orchestrator:
127
127
  "Message another workspace's orchestrator, by workspace name; never any other session there. It arrives as its next prompt, marked as from you and fenced as untrusted.",
128
128
  start_task:
129
- 'Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). priority (urgent, blocking, normal, background) says where it stands in the queue; machine "mac" pins work that needs this Mac. A busy machine holds a start and the result says why. With after (a task\'s id or name, or "any"), or over the workspace\'s running task limit, it\'s queued and starts by itself; the result says Queued (position N). Its models follow the role settings (planning, implementation, review, scope); give model only when Saad asks for one. Always state plan (true or false) and plan_reason (plan_only true plans, then stops and waits for approve_plan instead of implementing): plan true for a big, unclear or cross-cutting job, false for a small, clear one whose steps you write into the prompt. now true (with now_reason) starts it at once past the running-task limit instead of queueing for a slot, logged; not with after. piece names the waiting piece of a plan this task builds, so it is claimed and not started a second time. Refused while a brake holds.',
129
+ 'Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). priority (urgent, blocking, normal, background) says where it stands in the queue; machine "mac" pins work that needs this Mac. A busy machine holds a start and the result says why. With after (a task\'s id or name, or "any"), or over the workspace\'s running task limit, it\'s queued and starts by itself; the result says Queued (position N). Its models follow the role settings (planning, implementation, review, scope); give model only when Saad asks for one. Always state plan (true or false) and plan_reason (plan_only true plans, then stops and waits for approve_plan instead of implementing): plan true for a big, unclear or cross-cutting job, false for a small, clear one whose steps you write into the prompt. now true (with now_reason) starts it at once past the running-task limit instead of queueing for a slot, logged; not with after. A strong overlap with work another session has claimed (same branch, or the same files, here or on a linked machine) queues the start after it and says so; the result also lists weaker overlaps. ignore_claims true skips that. piece names the waiting piece of a plan this task builds, so it is claimed and not started a second time. Refused while a brake holds.',
130
130
  set_task_model:
131
131
  'Resume a paused task on another model ("provider/model", or a map of role to one), keeping its conversation and worktree; it\'s told to carry on (or message). Refused mid-turn.',
132
132
  start_session:
133
133
  'Start an agent session in a project with a prompt, for work that isn\'t a PR task. It runs on the implementation model unless you give model (one "provider/model"; Opus only when the work needs deep reasoning), as a chat unless view is terminal (only for a job that needs a TUI). Refused while a brake holds.',
134
+ set_group:
135
+ "File a session of this workspace in one of Saad's groups (by name), or out of its group with null. Groups only organise his sidebar; they change nothing about the session. You can use groups that exist but can't create, rename or delete them.",
134
136
  quick_change:
135
137
  "Make a change yourself (without full access, a small low-risk one: a typo, a config value, a one-file fix): cuts a worktree and branch from the project's default branch and returns its path. Its dependencies install in the background (a note says when). You may write there (Write, Edit) and, with Orchestrator full access, run any git there; without it, only `git -C <path> ...` status, diff, add, rm, mv, restore, reset, log, show, and nowhere else in a project. Without full access, anything needing real work, tests or an investigation is a task (start_task).",
136
138
  quick_pr:
@@ -31,6 +31,10 @@ export const looksRandom = (s: string) =>
31
31
  export const SECRET_ASSIGNMENT =
32
32
  /^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CREDENTIALS?|AUTH)[A-Z0-9_]*\s*[=:]\s*).+$/gm;
33
33
 
34
+ // The same assignment inline, ahead of a command: API_TOKEN=... npm test.
35
+ const INLINE_ASSIGNMENT =
36
+ /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CREDENTIALS?|AUTH)[A-Z0-9_]*=)(?!\[redacted\])("[^"]*"|'[^']*'|\S+)/g;
37
+
34
38
  // A URL's userinfo (https://key@host, postgres://user:pw@host) and a query
35
39
  // parameter that carries a credential (?token=, &api_key=).
36
40
  export const URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^@\s/]+@/gi;
@@ -47,6 +51,7 @@ const URL_PASSWORD = /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s]+@/gi;
47
51
  export function redact(text: string): string {
48
52
  let out = text
49
53
  .replace(SECRET_ASSIGNMENT, "$1[redacted]")
54
+ .replace(INLINE_ASSIGNMENT, "$1[redacted]")
50
55
  .replace(URL_PASSWORD, "$1[redacted]@");
51
56
  for (const p of TOKEN_PATTERNS) out = out.replace(p, "[redacted]");
52
57
  return out.replace(BASE64_RUN, (m) => (looksRandom(m) ? "[redacted]" : m));
@@ -14,6 +14,9 @@ import { deliverEvents } from "./deliver";
14
14
  import { recordConditions } from "./events";
15
15
  import { sessionFacts, type SessionFacts } from "./facts";
16
16
  import { listOrchestrators } from "./home";
17
+ import { claimsShared, machineId } from "../claims/machine";
18
+ import { sessionOverlaps, type SessionOverlap } from "../claims/mine";
19
+ import { liveClaims } from "../claims/store";
17
20
  import { BRAKE_SUBJECT, openAsks } from "./asks";
18
21
  import { settleStaleAsks } from "./ask-settle";
19
22
  import { brakesOn, liftBrake } from "./brakes";
@@ -35,6 +38,15 @@ async function settleAsks(workspaceId: string): Promise<void> {
35
38
  if (brake && !(await brakesOn(workspaceId)).length) liftBrake(workspaceId);
36
39
  }
37
40
 
41
+ // Claims code never breaks the watcher: any error means no overlaps.
42
+ function localOverlaps(): SessionOverlap[] {
43
+ try {
44
+ return claimsShared() ? sessionOverlaps(machineId(), liveClaims()) : [];
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
49
+
38
50
  const DELIVER_EVERY_MS = 5000;
39
51
  export const DIFF_BUSY_MS = 15 * 1000;
40
52
  export const DIFF_IDLE_MS = 60 * 1000;
@@ -58,7 +70,13 @@ export async function diffWorkspace(
58
70
  ];
59
71
  recordConditions(
60
72
  workspaceId,
61
- conditionsFor(facts, stacks, now, recentlyMessaged(orchestratorId, now)),
73
+ conditionsFor(
74
+ facts,
75
+ stacks,
76
+ now,
77
+ recentlyMessaged(orchestratorId, now),
78
+ localOverlaps()
79
+ ),
62
80
  subjects,
63
81
  now
64
82
  );