@saadnvd1/agent-os 0.3.698 → 0.3.715
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.
- package/README.md +1 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/send/route.ts +11 -1
- package/app/api/bus/spawn/route.ts +12 -1
- package/app/api/bus/wait/route.ts +22 -0
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/peer/hello/reveal/route.ts +6 -0
- package/app/api/peer/hello/route.ts +9 -0
- package/app/api/peer/hello/status/route.ts +6 -0
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/account/route.ts +9 -0
- package/app/api/peers/gate.test.ts +174 -0
- package/app/api/peers/link/[id]/confirm/route.ts +27 -0
- package/app/api/peers/link/[id]/route.ts +20 -0
- package/app/api/peers/link/route.ts +23 -0
- package/app/api/peers/pending/[id]/accept/route.ts +31 -0
- package/app/api/peers/pending/[id]/decline/route.ts +13 -0
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +14 -0
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +31 -13
- package/app/page.tsx +7 -0
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftPanel.tsx +40 -2
- package/components/Hosts/AccountMachines.tsx +194 -0
- package/components/Hosts/HostRow.tsx +195 -45
- package/components/Hosts/HostsDialog.tsx +49 -37
- package/components/Hosts/IncomingLinkSheet.tsx +246 -0
- package/components/Hosts/LinkCodeSheet.tsx +186 -0
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Hosts/sheet.ts +3 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +18 -2
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +1 -0
- package/components/SessionList/SessionRowMenu.tsx +17 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/Welcome/ConnectBlock.tsx +1 -1
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/peers/index.ts +2 -0
- package/data/peers/keys.ts +7 -0
- package/data/peers/queries.ts +172 -0
- package/data/push/topics.ts +4 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +10 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/brief.ts +3 -1
- package/lib/agents/spawn.ts +4 -0
- package/lib/bus/format.ts +25 -4
- package/lib/bus/index.ts +121 -106
- package/lib/bus/resolve.ts +16 -2
- package/lib/bus/targets.ts +122 -0
- package/lib/bus/waits/kick.ts +12 -0
- package/lib/bus/waits/lines.ts +69 -0
- package/lib/bus/waits/register.ts +83 -0
- package/lib/bus/waits/store.ts +196 -0
- package/lib/bus/waits/tick.ts +192 -0
- package/lib/claims/collectLocal.ts +121 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/connect/control-api.ts +108 -2
- package/lib/connect/heartbeat.ts +11 -3
- package/lib/connect/serve.ts +2 -0
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +1 -0
- package/lib/db/types.ts +20 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +19 -1
- package/lib/orchestrator/facts.ts +22 -0
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +270 -0
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/cli.ts +24 -0
- package/lib/peer/client.ts +26 -1
- package/lib/peer/code.ts +3 -0
- package/lib/peer/hello-route.ts +32 -0
- package/lib/peer/hello.ts +346 -0
- package/lib/peer/link-cli.ts +248 -0
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +84 -0
- package/lib/peer/name.ts +32 -0
- package/lib/peer/notify.ts +32 -0
- package/lib/peer/pairing-out.ts +382 -0
- package/lib/peer/pairing.ts +432 -0
- package/lib/peer/pin.ts +62 -0
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-intake.ts +37 -8
- package/lib/peer/rpc-server.ts +72 -6
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/schedules/when.ts +1 -1
- package/lib/security/auth.ts +10 -2
- package/lib/security/demo.ts +7 -0
- package/lib/security/gate.ts +3 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/end.ts +3 -0
- package/lib/sessions/launch.ts +5 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +5 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +3 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +5 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +6 -0
- package/stores/claimsUi.ts +22 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
package/lib/orchestrator/act.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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,10 +37,24 @@ 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)}` : ""}`
|
|
43
44
|
);
|
|
45
|
+
if (f.waits?.on.length)
|
|
46
|
+
parts.push(
|
|
47
|
+
`waiting on ${f.waits.on
|
|
48
|
+
.map(
|
|
49
|
+
(w) =>
|
|
50
|
+
`${untrusted(f.name, w.name)} (${w.kind === "reply" ? "reply" : w.kind === "idle" ? "to go idle" : "to merge"}, ${Math.max(1, Math.round(w.leftMs / 60_000))}m left)`
|
|
51
|
+
)
|
|
52
|
+
.join(", ")}`
|
|
53
|
+
);
|
|
54
|
+
if (f.waits?.by.length)
|
|
55
|
+
parts.push(
|
|
56
|
+
`${f.waits.by.map((n) => untrusted(f.name, n)).join(", ")} waiting on it`
|
|
57
|
+
);
|
|
44
58
|
if (f.stack) {
|
|
45
59
|
const card = f.stack.ticket ? ` ${f.stack.ticket}` : "";
|
|
46
60
|
parts.push(
|
|
@@ -53,7 +67,8 @@ function sessionLine(f: SessionFacts): string {
|
|
|
53
67
|
// The sessions tool's answer: one line per session, busiest news first.
|
|
54
68
|
export function describeSessions(
|
|
55
69
|
workspace: string,
|
|
56
|
-
facts: SessionFacts[]
|
|
70
|
+
facts: SessionFacts[],
|
|
71
|
+
groups: readonly string[] = []
|
|
57
72
|
): string {
|
|
58
73
|
if (!facts.length) return `No sessions are running in ${workspace}.`;
|
|
59
74
|
const rank = { waiting: 0, running: 1, idle: 2, dead: 3 };
|
|
@@ -69,5 +84,8 @@ export function describeSessions(
|
|
|
69
84
|
return [
|
|
70
85
|
`${facts.length} ${noun} in ${workspace}${summary}:`,
|
|
71
86
|
...sorted.map(sessionLine),
|
|
87
|
+
...(groups.length
|
|
88
|
+
? [`Saad's groups here: ${groups.map((g) => `"${g}"`).join(", ")}.`]
|
|
89
|
+
: []),
|
|
72
90
|
].join("\n");
|
|
73
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";
|
|
@@ -20,6 +21,7 @@ import { prFor, storedPR } from "../tasks/session";
|
|
|
20
21
|
import { lastActiveOf } from "../tasks/last-active";
|
|
21
22
|
import * as taskClient from "../tasks/contract/client";
|
|
22
23
|
import { deriveTaskState } from "../tasks";
|
|
24
|
+
import { waitBadges } from "../bus/waits/store";
|
|
23
25
|
import { waitingState } from "./task-state";
|
|
24
26
|
import { commitTime, repoOf } from "./repo";
|
|
25
27
|
import { CHECKLESS_MS, checklessFor, ciSettleIn } from "./task-state";
|
|
@@ -42,8 +44,15 @@ export interface SessionFacts {
|
|
|
42
44
|
stalled?: Stall | null;
|
|
43
45
|
// Its agent is summarising the context: not stuck, not plain work.
|
|
44
46
|
compacting?: boolean;
|
|
47
|
+
// Saad's group it sits in, when it is in one.
|
|
48
|
+
group?: string | null;
|
|
45
49
|
// Last sign of life, in ms.
|
|
46
50
|
lastActive: number;
|
|
51
|
+
// Open waits (aos send --await, aos wait): who it waits on, and who waits on it.
|
|
52
|
+
waits?: {
|
|
53
|
+
on: { name: string; kind: string; leftMs: number }[];
|
|
54
|
+
by: string[];
|
|
55
|
+
} | null;
|
|
47
56
|
// Work that should end in a PR: a task, or a session on its own branch.
|
|
48
57
|
branch: string | null;
|
|
49
58
|
// A quick change the orchestrator makes itself (quick.ts): no agent.
|
|
@@ -301,6 +310,8 @@ export async function sessionFacts(
|
|
|
301
310
|
workspaceId: string
|
|
302
311
|
): Promise<SessionFacts[]> {
|
|
303
312
|
await statusDetector.refreshCache();
|
|
313
|
+
const badges = waitBadges();
|
|
314
|
+
const groups = groupNames();
|
|
304
315
|
return Promise.all(
|
|
305
316
|
workspaceSessions(workspaceId).map(async (s) => {
|
|
306
317
|
const [{ status, activity, needsInput }, task] = await Promise.all([
|
|
@@ -315,10 +326,21 @@ export async function sessionFacts(
|
|
|
315
326
|
status,
|
|
316
327
|
activity,
|
|
317
328
|
needsInput,
|
|
329
|
+
waits: badges.has(s.id)
|
|
330
|
+
? {
|
|
331
|
+
on: badges.get(s.id)!.on.map((w) => ({
|
|
332
|
+
name: w.name,
|
|
333
|
+
kind: w.kind,
|
|
334
|
+
leftMs: Math.max(0, w.deadline - Date.now()),
|
|
335
|
+
})),
|
|
336
|
+
by: badges.get(s.id)!.by.map((w) => w.name),
|
|
337
|
+
}
|
|
338
|
+
: null,
|
|
318
339
|
stalled:
|
|
319
340
|
s.view === "chat" && !hostLink(s.host_id) ? chatStall(s.id) : null,
|
|
320
341
|
compacting:
|
|
321
342
|
s.view === "chat" && !hostLink(s.host_id) && !!chatCompacting(s.id),
|
|
343
|
+
group: groups.get(s.session_group_id ?? "") ?? null,
|
|
322
344
|
lastActive: await lastActiveFor(s),
|
|
323
345
|
branch: s.branch_name,
|
|
324
346
|
quick: !!s.quick_of,
|
|
@@ -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(
|
|
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:
|
|
@@ -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(
|
|
73
|
+
conditionsFor(
|
|
74
|
+
facts,
|
|
75
|
+
stacks,
|
|
76
|
+
now,
|
|
77
|
+
recentlyMessaged(orchestratorId, now),
|
|
78
|
+
localOverlaps()
|
|
79
|
+
),
|
|
62
80
|
subjects,
|
|
63
81
|
now
|
|
64
82
|
);
|