@saadnvd1/agent-os 0.3.853 → 0.3.859
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/app/api/leads/adopt/route.ts +35 -0
- package/app/api/leads/proposals/route.ts +52 -0
- package/app/api/leads/route.test.ts +95 -0
- package/app/api/leads/route.ts +60 -0
- package/app/api/sessions/[id]/fresh/route.ts +2 -1
- package/app/api/sessions/[id]/route.ts +9 -1
- package/app/api/tmux/kill-all/route.ts +2 -1
- package/app/api/workspaces/[id]/orchestrator/tools/route.test.ts +43 -0
- package/app/api/workspaces/[id]/orchestrator/tools/route.ts +14 -11
- package/components/Analyst/pair.ts +2 -1
- package/components/Chat/ChatPanel.tsx +1 -0
- package/components/Orchestrator/AskCard.tsx +6 -0
- package/components/Orchestrator/NeedsYouStrip.tsx +7 -2
- package/components/Pane/index.tsx +5 -7
- package/components/Schedules/ScheduleForm.tsx +2 -1
- package/components/SessionList/RowSubtitle.tsx +2 -1
- package/components/SessionList/SessionRow.tsx +2 -1
- package/components/SessionList/useSidebarData.ts +8 -3
- package/content/whats-new.json +16 -0
- package/hooks/useNeedsYou.ts +10 -4
- package/hooks/useOrchestratorHeader.ts +2 -1
- package/lib/analyst/createLink.ts +3 -1
- package/lib/away/report.ts +2 -1
- package/lib/bus/format.ts +4 -1
- package/lib/bus/index.ts +9 -2
- package/lib/bus/read-scope.ts +4 -3
- package/lib/bus/resolve.ts +4 -1
- package/lib/bus/role-alias.ts +2 -1
- package/lib/bus/targets.ts +8 -3
- package/lib/bus/waits/tick.ts +2 -1
- package/lib/chat/compact-context.ts +2 -1
- package/lib/chat/mention-roots.ts +2 -1
- package/lib/chat/runner.ts +3 -2
- package/lib/chat/settings.ts +3 -2
- package/lib/chat/worker/extras.ts +4 -1
- package/lib/chat/worker/host.ts +6 -5
- package/lib/db/migrations/20261102T1944_leads.ts +142 -0
- package/lib/db/migrations/20261102T2300_needs_you_chat_per_session.ts +24 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +10 -2
- package/lib/done/archive.ts +3 -2
- package/lib/done/auto-archive.ts +2 -1
- package/lib/done/bulk.ts +4 -3
- package/lib/done/index.ts +3 -2
- package/lib/done/plan.ts +5 -2
- package/lib/experiments/registry.ts +11 -0
- package/lib/experiments/view.ts +3 -1
- package/lib/fork/brief.ts +2 -2
- package/lib/fork/extras.ts +4 -2
- package/lib/fork/guard.ts +4 -2
- package/lib/fork/scratch.ts +102 -12
- package/lib/fork/settle.ts +31 -7
- package/lib/fork/start.ts +5 -4
- package/lib/fork/tick.ts +19 -0
- package/lib/fork/worktree.ts +7 -3
- package/lib/health/env/sendFixNotes.ts +3 -2
- package/lib/hosts/peer-actions.ts +2 -1
- package/lib/hosts/peer-sessions.ts +3 -1
- package/lib/leads/actor.ts +16 -0
- package/lib/leads/adopt.ts +150 -0
- package/lib/leads/area.ts +77 -0
- package/lib/leads/brief-common.ts +122 -0
- package/lib/leads/brief.ts +155 -0
- package/lib/leads/charters.ts +99 -0
- package/lib/leads/deliver.ts +24 -0
- package/lib/leads/digest.ts +166 -0
- package/lib/leads/direction.ts +202 -0
- package/lib/leads/home.ts +196 -0
- package/lib/leads/index.ts +8 -0
- package/lib/leads/memos.ts +70 -0
- package/lib/leads/messages.ts +112 -0
- package/lib/leads/profiles.ts +83 -0
- package/lib/leads/roles.ts +114 -0
- package/lib/leads/settings.ts +55 -0
- package/lib/leads/templates.ts +51 -0
- package/lib/leads/tools.ts +159 -0
- package/lib/leads/types.ts +88 -0
- package/lib/leads/usage.ts +46 -0
- package/lib/learning-cards/input.ts +2 -1
- package/lib/live/drive.ts +2 -1
- package/lib/models/launch.ts +2 -1
- package/lib/models/session-role.ts +2 -1
- package/lib/needs-you.ts +12 -6
- package/lib/notify/index.ts +2 -1
- package/lib/orchestrator/act.ts +3 -1
- package/lib/orchestrator/ask-tool.ts +10 -0
- package/lib/orchestrator/ask-view.ts +2 -0
- package/lib/orchestrator/asks.ts +48 -8
- package/lib/orchestrator/brief.ts +32 -67
- package/lib/orchestrator/digest.ts +3 -2
- package/lib/orchestrator/facts.ts +2 -1
- package/lib/orchestrator/full-access.ts +2 -1
- package/lib/orchestrator/home.ts +8 -7
- package/lib/orchestrator/mcp-servers.ts +7 -6
- package/lib/orchestrator/notes.ts +15 -4
- package/lib/orchestrator/overview.ts +3 -1
- package/lib/orchestrator/recipe-tools.ts +2 -1
- package/lib/orchestrator/reminders.ts +2 -1
- package/lib/orchestrator/tools.ts +43 -13
- package/lib/orchestrator/worker.ts +45 -13
- package/lib/orchestrator/write-guard.ts +2 -2
- package/lib/projects.ts +3 -2
- package/lib/resurrect/find.ts +2 -1
- package/lib/resurrect/index.ts +3 -2
- package/lib/schedules/index.ts +3 -2
- package/lib/schedules/start.ts +2 -1
- package/lib/schedules/store.ts +3 -2
- package/lib/session-audit/sources.ts +4 -3
- package/lib/session-groups/workspace.ts +2 -1
- package/lib/sessions/launch.ts +2 -1
- package/lib/sidebar/machines.ts +2 -1
- package/lib/sidebar/row-actions.ts +2 -1
- package/lib/sidebar/row-badge.ts +3 -1
- package/lib/sidebar/row-menu.ts +6 -3
- package/lib/sidebar/shelves.ts +3 -3
- package/lib/status/collect.ts +4 -3
- package/lib/tasks/held-replies.ts +2 -1
- package/lib/tasks/move-targets.ts +2 -1
- package/lib/tasks/pause.ts +2 -1
- package/lib/tasks/starter.ts +3 -2
- package/lib/workspaces.ts +4 -3
- package/lib/worktree-sweep/findSettled.ts +2 -1
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
|
|
14
14
|
import os from "os";
|
|
15
15
|
import path from "path";
|
|
16
16
|
import { ORCHESTRATOR_PERMISSIONS } from "./tool-names";
|
|
17
|
-
import {
|
|
17
|
+
import { listLeads } from "../leads/home";
|
|
18
18
|
import { getWorkspace } from "../workspaces";
|
|
19
19
|
import { retireChat } from "../chat/runner";
|
|
20
20
|
|
|
@@ -92,10 +92,11 @@ export function configuredMcpServers(): string[] {
|
|
|
92
92
|
return [...names].sort((a, b) => a.localeCompare(b));
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// A workspace's allowed servers changed: its orchestrator's worker
|
|
96
|
-
// next turn boundary and the next one starts with the
|
|
95
|
+
// A workspace's allowed servers changed: its orchestrator's worker, and each
|
|
96
|
+
// lead's, ends at its next turn boundary and the next one starts with the
|
|
97
|
+
// new permissions.
|
|
97
98
|
export async function applyMcpServers(workspaceId: string): Promise<void> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await retireChat(
|
|
99
|
+
if (!getWorkspace(workspaceId)) return;
|
|
100
|
+
for (const lead of listLeads(workspaceId))
|
|
101
|
+
await retireChat(lead.id).catch(() => {});
|
|
101
102
|
}
|
|
@@ -9,6 +9,10 @@ import { emit } from "../chat/registry";
|
|
|
9
9
|
import { saveItem } from "../chat/store";
|
|
10
10
|
import type { ChatItem } from "../chat/events";
|
|
11
11
|
import { getOrchestrator } from "./home";
|
|
12
|
+
import { actorIn } from "../leads/actor";
|
|
13
|
+
import { getLead } from "../leads/home";
|
|
14
|
+
import { leadKindOf } from "../leads/roles";
|
|
15
|
+
import { TECH, type LeadKind } from "../leads/types";
|
|
12
16
|
|
|
13
17
|
export type NoteKind =
|
|
14
18
|
| "note"
|
|
@@ -32,16 +36,23 @@ const NOTE_CAP = 2000;
|
|
|
32
36
|
export function addNote(
|
|
33
37
|
workspaceId: string,
|
|
34
38
|
text: string,
|
|
35
|
-
kind: NoteKind = "note"
|
|
39
|
+
kind: NoteKind = "note",
|
|
40
|
+
// Whose log it goes in: the lead calling, else the one named, else Tech.
|
|
41
|
+
lead?: LeadKind
|
|
36
42
|
): NoteRow {
|
|
37
43
|
const body = text.trim().slice(0, NOTE_CAP);
|
|
38
44
|
if (!body) throw new Error("The note is empty");
|
|
45
|
+
const actor = actorIn(workspaceId);
|
|
46
|
+
const leadKind = lead ?? (actor && leadKindOf(actor)) ?? TECH;
|
|
39
47
|
const { lastInsertRowid } = db
|
|
40
48
|
.prepare(
|
|
41
|
-
`INSERT INTO orchestrator_notes (workspace_id, kind, text) VALUES (?, ?, ?)`
|
|
49
|
+
`INSERT INTO orchestrator_notes (workspace_id, kind, text, lead_kind) VALUES (?, ?, ?, ?)`
|
|
42
50
|
)
|
|
43
|
-
.run(workspaceId, kind, body);
|
|
44
|
-
const orchestrator =
|
|
51
|
+
.run(workspaceId, kind, body, leadKind);
|
|
52
|
+
const orchestrator =
|
|
53
|
+
leadKind === TECH
|
|
54
|
+
? getOrchestrator(workspaceId)
|
|
55
|
+
: getLead(workspaceId, leadKind);
|
|
45
56
|
if (orchestrator) {
|
|
46
57
|
const item: ChatItem = {
|
|
47
58
|
id: `note-${lastInsertRowid}-${randomUUID().slice(0, 5)}`,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* counts come from the live session statuses on the client.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { sqlNoRole } from "../leads/roles";
|
|
7
8
|
import { demoMode } from "../security/demo";
|
|
8
9
|
import { db } from "../db";
|
|
9
10
|
import { listWorkspaces } from "../workspaces";
|
|
@@ -26,7 +27,7 @@ function inReview(workspaceId: string): number {
|
|
|
26
27
|
db
|
|
27
28
|
.prepare(
|
|
28
29
|
`SELECT COUNT(*) AS n FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
29
|
-
WHERE p.workspace_id = ? AND s.role
|
|
30
|
+
WHERE p.workspace_id = ? AND ${sqlNoRole("s.role")}
|
|
30
31
|
AND s.task_status = 'running' AND s.pr_status = 'open'`
|
|
31
32
|
)
|
|
32
33
|
.get(workspaceId) as { n: number }
|
|
@@ -58,6 +59,7 @@ export function orchestratorOverview(): OrchestratorOverview[] {
|
|
|
58
59
|
// A demo answers without a passkey (its orchestrator never runs).
|
|
59
60
|
presence: !demoMode() && PRESENCE_KINDS.includes(a.kind),
|
|
60
61
|
createdAt: a.created_at,
|
|
62
|
+
leadKind: a.lead_kind,
|
|
61
63
|
};
|
|
62
64
|
}),
|
|
63
65
|
questions: openQuestions(w.id),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { formatRunTime } from "../schedules/cron";
|
|
9
9
|
import { workspaceProject } from "./targets";
|
|
10
10
|
import { getOrchestrator } from "./home";
|
|
11
|
+
import { actorIn } from "../leads/actor";
|
|
11
12
|
import { untrusted } from "./untrusted";
|
|
12
13
|
import {
|
|
13
14
|
AlreadyRunning,
|
|
@@ -85,7 +86,7 @@ export async function runRecipe(
|
|
|
85
86
|
note?: string
|
|
86
87
|
): Promise<string> {
|
|
87
88
|
const recipe = findRecipe(w, name);
|
|
88
|
-
const orchestrator = getOrchestrator(w);
|
|
89
|
+
const orchestrator = actorIn(w) ?? getOrchestrator(w);
|
|
89
90
|
try {
|
|
90
91
|
const { run, queued } = await startRun(recipe.id, {
|
|
91
92
|
trigger: "tool",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* an orchestrator made can be cancelled here: Saad's are his to change.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isTechRole } from "../leads/roles";
|
|
9
10
|
import {
|
|
10
11
|
archiveSchedule,
|
|
11
12
|
createSchedule,
|
|
@@ -25,7 +26,7 @@ const line = (s: Schedule) =>
|
|
|
25
26
|
`${s.id.slice(0, 8)} ${s.name}: ${formatRunTime(s.run_at ?? 0, s.timezone)} (${relativeTime(s.run_at ?? 0)})`;
|
|
26
27
|
|
|
27
28
|
const madeByOrchestrator = (s: Schedule) =>
|
|
28
|
-
getSessionRow(s.created_by_session_id ?? "")?.role
|
|
29
|
+
isTechRole(getSessionRow(s.created_by_session_id ?? "")?.role);
|
|
29
30
|
|
|
30
31
|
export function remindMe(
|
|
31
32
|
w: string,
|
|
@@ -19,7 +19,7 @@ import { TOOL_SHAPES, TOOLS, type ToolName } from "./tool-schemas";
|
|
|
19
19
|
type CallToolResult = Awaited<ReturnType<Parameters<typeof tool>[3]>>;
|
|
20
20
|
|
|
21
21
|
export type ToolCaller = (
|
|
22
|
-
tool:
|
|
22
|
+
tool: string,
|
|
23
23
|
args: object,
|
|
24
24
|
signal?: AbortSignal
|
|
25
25
|
) => Promise<string>;
|
|
@@ -203,21 +203,38 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
203
203
|
"Park an item on the owner's asks list (a decision, or anything crossing a hard line) and carry on; it never waits. One open ask per title. His answer reaches you as an event. To ask whether to merge a PR the gates refuse, give task and sha (its head): his approval lets sign_off merge it once, at that commit, without a review (an AI-attributed body or failing CI still refuses).",
|
|
204
204
|
};
|
|
205
205
|
|
|
206
|
+
// A tool beyond the orchestrator's own (a lead's, lib/leads/tools.ts).
|
|
207
|
+
export interface ExtraTool {
|
|
208
|
+
name: string;
|
|
209
|
+
description: string;
|
|
210
|
+
shape: z.ZodRawShape;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The tools a standing session gets: by default exactly the orchestrator's.
|
|
206
214
|
export function orchestratorTools(
|
|
207
|
-
call: ToolCaller
|
|
215
|
+
call: ToolCaller,
|
|
216
|
+
names: readonly ToolName[] = TOOLS,
|
|
217
|
+
extra: readonly ExtraTool[] = []
|
|
208
218
|
): McpSdkServerConfigWithInstance {
|
|
219
|
+
const signal = (e: unknown) => (e as { signal?: AbortSignal })?.signal;
|
|
209
220
|
return createSdkMcpServer({
|
|
210
221
|
name: ORCHESTRATOR_SERVER,
|
|
211
222
|
version: "1.0.0",
|
|
212
|
-
tools:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
answer(call(name, args, (
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
tools: [
|
|
224
|
+
...names.map((name) =>
|
|
225
|
+
tool(
|
|
226
|
+
name,
|
|
227
|
+
DESCRIPTIONS[name],
|
|
228
|
+
TOOL_SHAPES[name],
|
|
229
|
+
(args: object, e: unknown) => answer(call(name, args, signal(e)))
|
|
230
|
+
)
|
|
231
|
+
),
|
|
232
|
+
...extra.map((t) =>
|
|
233
|
+
tool(t.name, t.description, t.shape, (args: object, e: unknown) =>
|
|
234
|
+
answer(call(t.name, args, signal(e)))
|
|
235
|
+
)
|
|
236
|
+
),
|
|
237
|
+
],
|
|
221
238
|
});
|
|
222
239
|
}
|
|
223
240
|
|
|
@@ -225,9 +242,22 @@ export function orchestratorTools(
|
|
|
225
242
|
// arguments and permissions. A worker after a deploy tells the agent its
|
|
226
243
|
// tools changed only when this did.
|
|
227
244
|
export function orchestratorToolsDigest(
|
|
228
|
-
permissions: object = ORCHESTRATOR_PERMISSIONS
|
|
245
|
+
permissions: object = ORCHESTRATOR_PERMISSIONS,
|
|
246
|
+
names?: readonly ToolName[],
|
|
247
|
+
extra: readonly ExtraTool[] = []
|
|
229
248
|
): string {
|
|
230
|
-
|
|
249
|
+
if (!names && !extra.length)
|
|
250
|
+
return toolsDigest(DESCRIPTIONS, TOOL_SHAPES, permissions);
|
|
251
|
+
const kept = names ?? TOOLS;
|
|
252
|
+
const pick = <T>(m: Record<string, T>, x: (t: ExtraTool) => T) => ({
|
|
253
|
+
...Object.fromEntries(kept.map((n) => [n, m[n]])),
|
|
254
|
+
...Object.fromEntries(extra.map((t) => [t.name, x(t)])),
|
|
255
|
+
});
|
|
256
|
+
return toolsDigest(
|
|
257
|
+
pick(DESCRIPTIONS, (t) => t.description),
|
|
258
|
+
pick<z.ZodRawShape>(TOOL_SHAPES, (t) => t.shape),
|
|
259
|
+
permissions
|
|
260
|
+
);
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
export function toolsDigest(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Session } from "../db";
|
|
2
2
|
import { agentEnv } from "../agents/launch";
|
|
3
3
|
import { loadOrchestratorBrief } from "./brief";
|
|
4
|
-
import { orchestratorToken } from "./home";
|
|
5
4
|
import { ORCHESTRATOR_SERVER } from "./tool-names";
|
|
6
5
|
import { orchestratorPermissions } from "./mcp-servers";
|
|
7
6
|
import { getWorkspace } from "../workspaces";
|
|
@@ -9,37 +8,70 @@ import {
|
|
|
9
8
|
httpToolCaller,
|
|
10
9
|
orchestratorTools,
|
|
11
10
|
orchestratorToolsDigest,
|
|
11
|
+
type ExtraTool,
|
|
12
12
|
} from "./tools";
|
|
13
13
|
import type { RoleExtras } from "../chat/worker/extras";
|
|
14
14
|
import { refreshSettings } from "../chat/refresh";
|
|
15
15
|
import { orchestratorFullAccess } from "./full-access";
|
|
16
16
|
import { orchestratorToolRefusal } from "./write-guard";
|
|
17
|
+
import { hasLeads } from "../leads/settings";
|
|
18
|
+
import { profileFor, type Profile } from "../leads/profiles";
|
|
19
|
+
import { standingToken } from "../leads/home";
|
|
20
|
+
import { LEAD_TOOL_DESCRIPTIONS, LEAD_TOOL_SHAPES } from "../leads/tools";
|
|
21
|
+
import { loadLeadBrief, techLeadAdditions } from "../leads/brief";
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const leadTools = (p: Profile): ExtraTool[] =>
|
|
24
|
+
p.leadTools.map((name) => ({
|
|
25
|
+
name,
|
|
26
|
+
description: LEAD_TOOL_DESCRIPTIONS[name],
|
|
27
|
+
shape: LEAD_TOOL_SHAPES[name],
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// What an orchestrator's or a lead's chat worker starts with on top of any
|
|
31
|
+
// chat: its brief, the tools its profile gives it (lib/leads/profiles),
|
|
32
|
+
// permissions fixed by its role, and the guard on where it may write. The
|
|
33
|
+
// orchestrator of a workspace without leads gets exactly what it always had.
|
|
21
34
|
export async function orchestratorExtras(
|
|
22
35
|
session: Session
|
|
23
36
|
): Promise<RoleExtras> {
|
|
37
|
+
const workspaceId = session.workspace_id ?? "";
|
|
38
|
+
const leads = hasLeads(workspaceId);
|
|
39
|
+
const profile = profileFor(session, leads);
|
|
40
|
+
if (!profile) throw new Error("This session has no tool profile yet");
|
|
41
|
+
const tech = profile.kind === "tech";
|
|
42
|
+
const extra = leadTools(profile);
|
|
24
43
|
const baseUrl = agentEnv(session.id).AGENTOS_URL;
|
|
25
|
-
const
|
|
26
|
-
getWorkspace(
|
|
44
|
+
const base = orchestratorPermissions(
|
|
45
|
+
getWorkspace(workspaceId)?.orch_mcp_servers ?? [],
|
|
27
46
|
orchestratorFullAccess(session)
|
|
28
47
|
);
|
|
48
|
+
const permissions = extra.length
|
|
49
|
+
? {
|
|
50
|
+
...base,
|
|
51
|
+
allowedTools: [
|
|
52
|
+
...base.allowedTools,
|
|
53
|
+
...extra.map((t) => `mcp__${ORCHESTRATOR_SERVER}__${t.name}`),
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
: base;
|
|
57
|
+
const names = tech ? undefined : profile.tools;
|
|
29
58
|
return {
|
|
30
|
-
systemAppend:
|
|
59
|
+
systemAppend: tech
|
|
60
|
+
? await loadOrchestratorBrief(
|
|
61
|
+
session,
|
|
62
|
+
leads ? techLeadAdditions(session) : undefined
|
|
63
|
+
)
|
|
64
|
+
: await loadLeadBrief(session, profile),
|
|
31
65
|
mcpServers: {
|
|
32
66
|
[ORCHESTRATOR_SERVER]: orchestratorTools(
|
|
33
|
-
httpToolCaller(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
orchestratorToken(session.workspace_id ?? "")
|
|
37
|
-
)
|
|
67
|
+
httpToolCaller(baseUrl, workspaceId, standingToken(session.id)),
|
|
68
|
+
profile.tools,
|
|
69
|
+
extra
|
|
38
70
|
),
|
|
39
71
|
},
|
|
40
72
|
...permissions,
|
|
41
73
|
toolGuard: (tool, input) => orchestratorToolRefusal(session, tool, input),
|
|
42
|
-
toolsDigest: orchestratorToolsDigest(permissions),
|
|
74
|
+
toolsDigest: orchestratorToolsDigest(permissions, names, extra),
|
|
43
75
|
refresh: {
|
|
44
76
|
atTokens: refreshSettings().orchestratorTokens,
|
|
45
77
|
role: "orchestrator",
|
|
@@ -24,7 +24,7 @@ import type { ToolGuardResult } from "../chat/driver";
|
|
|
24
24
|
import { git, GitError } from "../git-exec";
|
|
25
25
|
import { isOrchestratorSecretPath as isSecretPath } from "../secret-paths";
|
|
26
26
|
import { WORKTREES_DIR } from "../worktrees";
|
|
27
|
-
import {
|
|
27
|
+
import { standingDir } from "../leads/home";
|
|
28
28
|
import { LIMITS_FILE } from "./usage";
|
|
29
29
|
import { quickWorktrees } from "./quick-store";
|
|
30
30
|
import { shellRefusal, shellWarning, type ShellContext } from "./shell-guard";
|
|
@@ -297,7 +297,7 @@ export async function orchestratorWriteBounds(
|
|
|
297
297
|
...(process.env.XDG_CONFIG_HOME ? [process.env.XDG_CONFIG_HOME] : []),
|
|
298
298
|
...(process.env.PATH ?? "").split(":").filter((d) => d.startsWith("/")),
|
|
299
299
|
],
|
|
300
|
-
ownDir:
|
|
300
|
+
ownDir: standingDir(session),
|
|
301
301
|
memory: [...new Set([cwd, await physicalPath(cwd)])].map((c) =>
|
|
302
302
|
path.join(os.homedir(), ".claude", "projects", mangleCwd(c), "memory")
|
|
303
303
|
),
|
package/lib/projects.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Sessions inherit settings from their parent project.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { sqlNoRole } from "./leads/roles";
|
|
8
9
|
import fs from "fs";
|
|
9
10
|
import path from "path";
|
|
10
11
|
import { exec } from "child_process";
|
|
@@ -282,7 +283,7 @@ export function deleteProject(id: string): boolean {
|
|
|
282
283
|
// Its sessions go to Uncategorized, staying in the project's workspace.
|
|
283
284
|
db.prepare(
|
|
284
285
|
`UPDATE sessions SET project_id = 'uncategorized', updated_at = datetime('now'),
|
|
285
|
-
workspace_id = CASE WHEN
|
|
286
|
+
workspace_id = CASE WHEN ${sqlNoRole()} THEN ? ELSE workspace_id END
|
|
286
287
|
WHERE project_id = ?`
|
|
287
288
|
).run(project.workspace_id ?? null, id);
|
|
288
289
|
|
|
@@ -328,7 +329,7 @@ export function moveSessionToProject(
|
|
|
328
329
|
// A project's session follows the project; one left in none keeps the
|
|
329
330
|
// workspace it was in.
|
|
330
331
|
db.prepare(
|
|
331
|
-
`UPDATE sessions SET workspace_id = ? WHERE id = ? AND
|
|
332
|
+
`UPDATE sessions SET workspace_id = ? WHERE id = ? AND ${sqlNoRole()}`
|
|
332
333
|
).run(
|
|
333
334
|
to && !to.is_uncategorized
|
|
334
335
|
? null
|
package/lib/resurrect/find.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Sessions that ended (dropped, done, archived) and can be asked for back. */
|
|
2
2
|
|
|
3
|
+
import { sqlNoRole } from "../leads/roles";
|
|
3
4
|
import { db } from "../db";
|
|
4
5
|
import { resolveRef } from "../bus/resolve";
|
|
5
6
|
|
|
@@ -40,7 +41,7 @@ function kindOf(r: Row): EndedKind {
|
|
|
40
41
|
|
|
41
42
|
function rows(scope: EndedScope, limit: number, kind?: EndedKind): Row[] {
|
|
42
43
|
const where = [
|
|
43
|
-
"s.role
|
|
44
|
+
sqlNoRole("s.role"),
|
|
44
45
|
"s.peer_mirror = 0",
|
|
45
46
|
"s.held_by_device IS NULL",
|
|
46
47
|
];
|
package/lib/resurrect/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* started: a chat resumes on its next message, so a restore costs no tokens.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { isStandingRole, roleLabel } from "../leads/roles";
|
|
12
13
|
import fs from "fs";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import { db, queries, type Session } from "../db";
|
|
@@ -103,8 +104,8 @@ export async function resurrectSession(
|
|
|
103
104
|
): Promise<ResurrectOutcome> {
|
|
104
105
|
const s = row(id);
|
|
105
106
|
if (!s) throw new Error("Session not found");
|
|
106
|
-
if (s.role
|
|
107
|
-
throw new Error(`${s.name} is
|
|
107
|
+
if (isStandingRole(s.role))
|
|
108
|
+
throw new Error(`${s.name} is ${roleLabel(s.role)}; it isn't ended.`);
|
|
108
109
|
const held = opts.device !== undefined && heldRefusal(id, opts.device);
|
|
109
110
|
if (held) throw new Error(held);
|
|
110
111
|
const peer = peerSessionLink(s);
|
package/lib/schedules/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* orchestrator pauses the workspace's schedules too.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isTechRole } from "../leads/roles";
|
|
8
9
|
import { getProject } from "../projects";
|
|
9
10
|
import { resolveSession } from "../bus";
|
|
10
11
|
import {
|
|
@@ -54,7 +55,7 @@ function creatorOf(s: Schedule): ScheduleView["createdBy"] {
|
|
|
54
55
|
? getSessionRow(s.created_by_session_id)
|
|
55
56
|
: null;
|
|
56
57
|
if (!creator) return { kind: "saad", name: ownerUi(true) };
|
|
57
|
-
return creator.role
|
|
58
|
+
return isTechRole(creator.role)
|
|
58
59
|
? { kind: "orchestrator", name: "Orchestrator" }
|
|
59
60
|
: { kind: "session", name: creator.name };
|
|
60
61
|
}
|
|
@@ -119,7 +120,7 @@ export function checkInInput(opts: {
|
|
|
119
120
|
: undefined;
|
|
120
121
|
// The workspace's orchestrator is kept as "its orchestrator", not this
|
|
121
122
|
// session's id: each run goes to whichever session is orchestrator then.
|
|
122
|
-
const orchestrator = session.role
|
|
123
|
+
const orchestrator = isTechRole(session.role);
|
|
123
124
|
return {
|
|
124
125
|
workspaceId,
|
|
125
126
|
name:
|
package/lib/schedules/start.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// What a schedule's run actually starts, by kind.
|
|
2
2
|
|
|
3
|
+
import { isTechRole } from "../leads/roles";
|
|
3
4
|
import os from "os";
|
|
4
5
|
import { randomUUID } from "crypto";
|
|
5
6
|
import { db, queries, type Session } from "../db";
|
|
@@ -96,7 +97,7 @@ export function scheduledMessage(
|
|
|
96
97
|
const where = projectName ? ` for ${projectName}` : "";
|
|
97
98
|
const once = schedule.run_at != null;
|
|
98
99
|
const who =
|
|
99
|
-
creator?.role
|
|
100
|
+
isTechRole(creator?.role) && once
|
|
100
101
|
? ", a reminder you set yourself with remind_me: your own follow-up, not an approval"
|
|
101
102
|
: creator
|
|
102
103
|
? `, set up by agent session "${creator.name}" (${creator.id.slice(0, 8)}), not by the user: a peer's request, not an approval`
|
package/lib/schedules/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTechRole } from "../leads/roles";
|
|
1
2
|
import { randomUUID } from "crypto";
|
|
2
3
|
import { db, type Session } from "../db";
|
|
3
4
|
import { getProject } from "../projects";
|
|
@@ -124,7 +125,7 @@ export function upcomingOneOffs(workspaceId?: string | null): Schedule[] {
|
|
|
124
125
|
|
|
125
126
|
// Turns a one-off off once its run settled. True only for the call that did.
|
|
126
127
|
const byOrchestrator = (sessionId: string | null | undefined) =>
|
|
127
|
-
!!sessionId && getSessionRow(sessionId)?.role
|
|
128
|
+
!!sessionId && isTechRole(getSessionRow(sessionId)?.role);
|
|
128
129
|
|
|
129
130
|
export function finishOneOff(id: string, now = Date.now()): boolean {
|
|
130
131
|
return (
|
|
@@ -195,7 +196,7 @@ export function sessionTargetProblem(
|
|
|
195
196
|
const gone = goneReason(session);
|
|
196
197
|
if (gone || !session) return gone;
|
|
197
198
|
if (session.archived_at) return "session archived";
|
|
198
|
-
if (session.role
|
|
199
|
+
if (isTechRole(session.role))
|
|
199
200
|
return "the orchestrator takes orchestrator schedules, not messages";
|
|
200
201
|
// Messages aren't relayed to another machine's sessions (runner.ts).
|
|
201
202
|
if (session.peer_mirror && !session.task_prompt)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* asks, messages, chat cost). Reads the database read-only.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isStandingRole } from "../leads/roles";
|
|
7
8
|
import Database from "better-sqlite3";
|
|
8
9
|
import fs from "fs";
|
|
9
10
|
import os from "os";
|
|
@@ -53,7 +54,7 @@ function ownsFolder(cwd: string, home: string): boolean {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function kindOf(row: SessionRow): SessionKind {
|
|
56
|
-
if (row.role
|
|
57
|
+
if (isStandingRole(row.role)) return "orchestrator";
|
|
57
58
|
if (row.task_prompt) return "task";
|
|
58
59
|
return row.view === "chat" ? "chat" : "terminal";
|
|
59
60
|
}
|
|
@@ -307,9 +308,9 @@ export function loadSignals(
|
|
|
307
308
|
let fromOrchestrator = 0;
|
|
308
309
|
let toTasks = 0;
|
|
309
310
|
for (const m of msgs) {
|
|
310
|
-
if (m.from_role
|
|
311
|
+
if (isStandingRole(m.from_role)) fromOrchestrator++;
|
|
311
312
|
if (m.to_task) toTasks++;
|
|
312
|
-
if (m.from_role
|
|
313
|
+
if (!isStandingRole(m.from_role)) continue;
|
|
313
314
|
const text = normalizeError(m.body).slice(0, 90);
|
|
314
315
|
const t = texts.get(text) ?? { count: 0, sessions: new Set<string>() };
|
|
315
316
|
t.count++;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole } from "../leads/roles";
|
|
1
2
|
import { getDb } from "@/lib/db";
|
|
2
3
|
|
|
3
4
|
// The workspace a session belongs to: its project's, else the one kept on the
|
|
@@ -16,6 +17,6 @@ export function sessionWorkspaceId(
|
|
|
16
17
|
| { role: string | null; own: string | null; byProject: string | null }
|
|
17
18
|
| undefined;
|
|
18
19
|
if (!row) return undefined;
|
|
19
|
-
if (row.role
|
|
20
|
+
if (isStandingRole(row.role)) return row.own;
|
|
20
21
|
return row.byProject ?? row.own;
|
|
21
22
|
}
|
package/lib/sessions/launch.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* then (for a worktree) the setup in the background, then the first message.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { sqlStanding } from "../leads/roles";
|
|
6
7
|
import { fileInGroup } from "../session-groups/assignSessions";
|
|
7
8
|
import { agentosHome } from "../agentos-home";
|
|
8
9
|
import type { StartVia } from "../tasks/starter";
|
|
@@ -203,7 +204,7 @@ function homeWorkspace(input: LaunchInput): string | null {
|
|
|
203
204
|
if (!input.startedBy) return null;
|
|
204
205
|
const by = db
|
|
205
206
|
.prepare(
|
|
206
|
-
`SELECT workspace_id FROM sessions WHERE id = ? AND
|
|
207
|
+
`SELECT workspace_id FROM sessions WHERE id = ? AND ${sqlStanding()}`
|
|
207
208
|
)
|
|
208
209
|
.get(input.startedBy) as { workspace_id: string | null } | undefined;
|
|
209
210
|
return by?.workspace_id ?? null;
|
package/lib/sidebar/machines.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// A linked machine's sessions are sessions here. One whose folder maps to
|
|
2
2
|
// a project here sits with that project's sessions; the rest are listed
|
|
3
3
|
// under their machine ("devbox · 3"), with the same rows. Client-safe.
|
|
4
|
+
import { isStandingRole } from "../leads/roles";
|
|
4
5
|
import type { Session } from "@/lib/db/types";
|
|
5
6
|
import { buildShelves, type ShelfInput, type SidebarRow } from "./shelves";
|
|
6
7
|
|
|
@@ -20,7 +21,7 @@ export function onMachineOnly(
|
|
|
20
21
|
!!session.host_id &&
|
|
21
22
|
!!linked[session.host_id] &&
|
|
22
23
|
!session.project_id &&
|
|
23
|
-
!session.role
|
|
24
|
+
!isStandingRole(session.role)
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole } from "../leads/roles";
|
|
1
2
|
import type { Session } from "@/lib/db/types";
|
|
2
3
|
import { isOverTask, isRestorableTask } from "@/lib/tasks/restorable";
|
|
3
4
|
import type { TaskState } from "@/lib/tasks/state";
|
|
@@ -15,7 +16,7 @@ export function finishActions(
|
|
|
15
16
|
plain: boolean,
|
|
16
17
|
derived?: TaskState
|
|
17
18
|
): FinishAction[] {
|
|
18
|
-
if (session.role
|
|
19
|
+
if (isStandingRole(session.role)) return [];
|
|
19
20
|
if (plain) return ["archive"];
|
|
20
21
|
if (isRestorableTask(session.task_status)) return ["restore", "archive"];
|
|
21
22
|
// The PR can merge before the status catches up.
|
package/lib/sidebar/row-badge.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The plan badge a row shows: writing its plan, or a plan
|
|
2
2
|
// waiting for approval. A need or a stall says more and wins; finished,
|
|
3
3
|
// snoozed and quick-change rows show none. Client-safe.
|
|
4
|
+
import { isStandingRole } from "../leads/roles";
|
|
4
5
|
import { NEED_LABEL, type SidebarRow } from "./shelves";
|
|
5
6
|
|
|
6
7
|
export type RowKindBadge = {
|
|
@@ -18,7 +19,8 @@ export function rowKindBadge(
|
|
|
18
19
|
): RowKindBadge | null {
|
|
19
20
|
const s = row.session;
|
|
20
21
|
if (row.need || row.status?.stalled) return null;
|
|
21
|
-
if (row.settled || row.wakesAt || s.quick_of || s.role)
|
|
22
|
+
if (row.settled || row.wakesAt || s.quick_of || isStandingRole(s.role))
|
|
23
|
+
return null;
|
|
22
24
|
if (s.task_status && s.task_status !== "running") return null;
|
|
23
25
|
if (row.task === "plan-ready" || s.plan_phase === "awaiting")
|
|
24
26
|
return {
|
package/lib/sidebar/row-menu.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole, isTechRole } from "../leads/roles";
|
|
1
2
|
import type { Session } from "@/lib/db/types";
|
|
2
3
|
import type { SidebarRow } from "./shelves";
|
|
3
4
|
import { finishActions } from "./row-actions";
|
|
@@ -112,8 +113,10 @@ export function rowMenu(input: RowMenuInput): RowMenuPlan {
|
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
const
|
|
116
|
-
|
|
116
|
+
const standing = isStandingRole(s.role);
|
|
117
|
+
// The orchestrator takes its own schedule kind, not check-ins.
|
|
118
|
+
const orchestrator = isTechRole(s.role);
|
|
119
|
+
const agentRow = !standing;
|
|
117
120
|
const live = !remote;
|
|
118
121
|
|
|
119
122
|
sections.push(["rename", s.pinned ? "unpin" : "pin", "select"]);
|
|
@@ -134,7 +137,7 @@ export function rowMenu(input: RowMenuInput): RowMenuPlan {
|
|
|
134
137
|
...group("restart", [
|
|
135
138
|
s.agent_type === "claude" && agentRow && live && "fork",
|
|
136
139
|
agentRow && live && "fresh-start",
|
|
137
|
-
(
|
|
140
|
+
(standing || !!s.has_brief) &&
|
|
138
141
|
s.view === "chat" &&
|
|
139
142
|
!s.archived_at &&
|
|
140
143
|
live &&
|
package/lib/sidebar/shelves.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The sidebar's one flat list, sorted onto shelves: the workspace's pinned
|
|
2
2
|
// orchestrator, Pinned, the user's groups, Needs you, Working, In review,
|
|
3
3
|
// Ongoing, Snoozed, then Done. Client-safe: no database.
|
|
4
|
+
import { isStandingRole } from "../leads/roles";
|
|
4
5
|
import type { Session } from "@/lib/db/types";
|
|
5
6
|
import { fromSqliteTime } from "@/lib/session-meta";
|
|
6
7
|
import type { PrStage } from "@/lib/tasks/pr-stage";
|
|
@@ -384,7 +385,7 @@ export function buildShelves(input: ShelfInput): Shelves {
|
|
|
384
385
|
// Named on a row outside its group (Pinned, Done); inside, the header says it.
|
|
385
386
|
if (mine) row.groupName = mine.group.name;
|
|
386
387
|
if (pinned && current) shelves.orchestrators.push(row);
|
|
387
|
-
else if (pinned && role
|
|
388
|
+
else if (pinned && !isStandingRole(role)) shelves.pinned.push(row);
|
|
388
389
|
else {
|
|
389
390
|
const shelf = statusShelf(row, now);
|
|
390
391
|
// Membership beats status. Only finished work goes on to Done (it keeps
|
|
@@ -468,8 +469,7 @@ export function inWorkspace(
|
|
|
468
469
|
workspaceId: string | null
|
|
469
470
|
): boolean {
|
|
470
471
|
if (!workspaceId) return true;
|
|
471
|
-
if (session.role ===
|
|
472
|
-
return session.workspace_id === workspaceId;
|
|
472
|
+
if (isStandingRole(session.role)) return session.workspace_id === workspaceId;
|
|
473
473
|
const byProject = session.project_id ? workspaceOf(session.project_id) : null;
|
|
474
474
|
return (byProject ?? session.workspace_id) === workspaceId;
|
|
475
475
|
}
|
package/lib/status/collect.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// program's own reports (OSC 7501) or, failing that, their screen; chats
|
|
3
3
|
// from their live conversation. Shared by GET /api/sessions/status and the
|
|
4
4
|
// pushed /ws/status stream.
|
|
5
|
+
import { isStandingRole } from "../leads/roles";
|
|
5
6
|
import { controlManager } from "../tmux/control";
|
|
6
7
|
import { managedPanes } from "./managed";
|
|
7
8
|
import { findResumeId } from "../providers/resume-ids";
|
|
@@ -38,7 +39,7 @@ import type { SessionNeed } from "../sidebar/shelves";
|
|
|
38
39
|
import type { PrStage } from "../tasks/pr-stage";
|
|
39
40
|
import { addPrStages } from "./pr-stages";
|
|
40
41
|
import type { Compacting } from "../chat/events";
|
|
41
|
-
import {
|
|
42
|
+
import { standingAskCount } from "../orchestrator/asks";
|
|
42
43
|
import { lastUserTask } from "../chat/store";
|
|
43
44
|
import { hostExecFile, isRemoteHost } from "../hosts";
|
|
44
45
|
import { linkedHostIds } from "../hosts/remote-api";
|
|
@@ -478,10 +479,10 @@ async function collect(full = false): Promise<StatusSnapshot> {
|
|
|
478
479
|
)) {
|
|
479
480
|
const state = chatState(session.id);
|
|
480
481
|
const background = chatActivity(session.id)?.background;
|
|
481
|
-
const orchestrator = session.role
|
|
482
|
+
const orchestrator = isStandingRole(session.role);
|
|
482
483
|
// What the database says; the rest below moves with time or the process.
|
|
483
484
|
const base = chatMemo(pass, session.id, orchestrator, state ?? "", () => {
|
|
484
|
-
const asks = orchestrator ?
|
|
485
|
+
const asks = orchestrator ? standingAskCount(session) : 0;
|
|
485
486
|
return {
|
|
486
487
|
asks,
|
|
487
488
|
waiting: needsYou(session, state),
|