@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole, RoleLabel } from "../leads/roles";
|
|
1
2
|
import { db, type Session } from "../db";
|
|
2
3
|
import { lastItems } from "../chat/store";
|
|
3
4
|
import { launchSession } from "../sessions/launch";
|
|
@@ -18,7 +19,8 @@ const sessionRow = (id: string) =>
|
|
|
18
19
|
export function coderRefusal(s: Session | undefined): string | null {
|
|
19
20
|
if (!s) return "Session not found";
|
|
20
21
|
if (s.archived_at) return `${s.name} is archived`;
|
|
21
|
-
if (s.role
|
|
22
|
+
if (isStandingRole(s.role))
|
|
23
|
+
return `${RoleLabel(s.role)} can't have an analyst`;
|
|
22
24
|
if (s.view !== "chat")
|
|
23
25
|
return "Only a chat session can have an analyst: a terminal has no turns to review";
|
|
24
26
|
if ((s.host_id || "local") !== "local")
|
package/lib/away/report.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* as Away ends and kept with the period, so it reads the same later.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { sqlNoRole } from "../leads/roles";
|
|
7
8
|
import { db } from "../db";
|
|
8
9
|
import { listWorkspaces } from "../workspaces";
|
|
9
10
|
import { HARD_LINES, openAsks } from "../orchestrator/asks";
|
|
@@ -178,7 +179,7 @@ function inFlight(workspaceId: string, awayId: number): InFlightItem[] {
|
|
|
178
179
|
`SELECT s.id, s.name, p.name AS project, s.pr_number, s.pr_url,
|
|
179
180
|
s.merge_armed_at AS armed_at
|
|
180
181
|
FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
181
|
-
WHERE p.workspace_id = ? AND s.role
|
|
182
|
+
WHERE p.workspace_id = ? AND ${sqlNoRole("s.role")} AND s.archived_at IS NULL
|
|
182
183
|
AND s.task_status = 'running'
|
|
183
184
|
AND EXISTS (SELECT 1 FROM away_actions a WHERE a.away_id = ?
|
|
184
185
|
AND a.subject = s.id AND a.kind IN (${TOUCHED}))
|
package/lib/bus/format.ts
CHANGED
|
@@ -81,6 +81,8 @@ export function matchesRef(
|
|
|
81
81
|
// orchestrator: "<workspace>/<name>" and "<workspace>/orchestrator".
|
|
82
82
|
workspaceName?: string | null;
|
|
83
83
|
orchestrator?: boolean;
|
|
84
|
+
// A lead's department: "<workspace>/<kind>".
|
|
85
|
+
leadKind?: string | null;
|
|
84
86
|
}
|
|
85
87
|
): boolean {
|
|
86
88
|
const r = ref.trim().toLowerCase();
|
|
@@ -92,6 +94,7 @@ export function matchesRef(
|
|
|
92
94
|
r === name ||
|
|
93
95
|
(!!s.projectName && r === `${s.projectName.toLowerCase()}/${name}`) ||
|
|
94
96
|
(!!ws && r === `${ws}/${name}`) ||
|
|
95
|
-
(!!ws && !!s.orchestrator && r === `${ws}/orchestrator`)
|
|
97
|
+
(!!ws && !!s.orchestrator && r === `${ws}/orchestrator`) ||
|
|
98
|
+
(!!ws && !!s.leadKind && r === `${ws}/${s.leadKind}`)
|
|
96
99
|
);
|
|
97
100
|
}
|
package/lib/bus/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the recipient's tmux pane, which works for any agent CLI.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isTechRole } from "../leads/roles";
|
|
7
8
|
import { db, type Session } from "../db";
|
|
8
9
|
import { statusDetector } from "../status-detector";
|
|
9
10
|
import { chatState, sendChatConfirmed } from "../chat/runner";
|
|
@@ -63,6 +64,8 @@ export interface Peer {
|
|
|
63
64
|
orchestratorOf: string | null;
|
|
64
65
|
// The asking session's own workspace orchestrator.
|
|
65
66
|
yours?: boolean;
|
|
67
|
+
// A lead's address, "<workspace>/<kind>", once its workspace has leads.
|
|
68
|
+
lead?: string;
|
|
66
69
|
// Names of the sessions it waits on, and of those waiting on it.
|
|
67
70
|
waitingOn: string[];
|
|
68
71
|
waitedBy: string[];
|
|
@@ -93,7 +96,7 @@ async function peerState(
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
const orchestratorOf = (s: Session) =>
|
|
96
|
-
s.role
|
|
99
|
+
isTechRole(s.role) && s.workspace_id
|
|
97
100
|
? (getWorkspace(s.workspace_id)?.name ?? "a workspace")
|
|
98
101
|
: null;
|
|
99
102
|
|
|
@@ -121,6 +124,10 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
121
124
|
running: info.running,
|
|
122
125
|
doing: info.subtitle,
|
|
123
126
|
orchestratorOf: orchestratorOf(s),
|
|
127
|
+
...(named[i].leadKind &&
|
|
128
|
+
named[i].workspaceName && {
|
|
129
|
+
lead: `${named[i].workspaceName}/${named[i].leadKind}`,
|
|
130
|
+
}),
|
|
124
131
|
waitingOn: badges.get(s.id)?.on.map((w) => w.name) ?? [],
|
|
125
132
|
waitedBy: badges.get(s.id)?.by.map((w) => w.name) ?? [],
|
|
126
133
|
...(s.agent_role && { agentRole: s.agent_role }),
|
|
@@ -130,7 +137,7 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
130
137
|
group: groups.get(s.session_group_id),
|
|
131
138
|
}),
|
|
132
139
|
...(callerWorkspace &&
|
|
133
|
-
s.role
|
|
140
|
+
isTechRole(s.role) &&
|
|
134
141
|
s.workspace_id === callerWorkspace && { yours: true }),
|
|
135
142
|
};
|
|
136
143
|
})
|
package/lib/bus/read-scope.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole, roleLabel } from "../leads/roles";
|
|
1
2
|
import { db } from "../db";
|
|
2
3
|
import type { Session } from "../db";
|
|
3
4
|
import { getProject } from "../projects";
|
|
@@ -58,7 +59,7 @@ function candidates(rows: ReadableSession[]): Candidate[] {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function inScope(s: ReadableSession, scope: ReaderScope): boolean {
|
|
61
|
-
if (s.role) return false;
|
|
62
|
+
if (isStandingRole(s.role)) return false;
|
|
62
63
|
if (scope.workspaceId) {
|
|
63
64
|
const p = s.project_id ? getProject(s.project_id) : null;
|
|
64
65
|
// A session in no project is in the workspace it was started in.
|
|
@@ -113,8 +114,8 @@ export function resolveReadable(callerId: string, ref: string): Readable {
|
|
|
113
114
|
return {
|
|
114
115
|
ok: false,
|
|
115
116
|
external: false,
|
|
116
|
-
error: s.role
|
|
117
|
-
? `${s.name} is
|
|
117
|
+
error: isStandingRole(s.role)
|
|
118
|
+
? `${s.name} is ${roleLabel(s.role)}: it isn't readable. Message it with aos send`
|
|
118
119
|
: `"${ref}" isn't in your ${scope.label}; aos read reaches only sessions there`,
|
|
119
120
|
};
|
|
120
121
|
}
|
package/lib/bus/resolve.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Candidate {
|
|
|
13
13
|
previousNames: string[];
|
|
14
14
|
workspaceName?: string | null;
|
|
15
15
|
orchestrator?: boolean;
|
|
16
|
+
// The department it heads, when its workspace has leads: "<workspace>/<kind>".
|
|
17
|
+
leadKind?: string | null;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type Resolution =
|
|
@@ -23,7 +25,7 @@ export const MIN_ID_PREFIX = 4;
|
|
|
23
25
|
|
|
24
26
|
const label = (c: Candidate) =>
|
|
25
27
|
`${
|
|
26
|
-
c.orchestrator && c.workspaceName
|
|
28
|
+
(c.orchestrator || c.leadKind) && c.workspaceName
|
|
27
29
|
? `${c.workspaceName}/`
|
|
28
30
|
: c.projectName
|
|
29
31
|
? `${c.projectName}/`
|
|
@@ -46,6 +48,7 @@ const matchesOld = (r: string, c: Candidate) =>
|
|
|
46
48
|
tmuxName: "",
|
|
47
49
|
name: old,
|
|
48
50
|
orchestrator: false,
|
|
51
|
+
leadKind: null,
|
|
49
52
|
})
|
|
50
53
|
);
|
|
51
54
|
|
package/lib/bus/role-alias.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* refuses and lists them, like any other name.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isTechRole } from "../leads/roles";
|
|
8
9
|
import type { Session } from "../db";
|
|
9
10
|
import { sessionWorkspace } from "../models/resolve";
|
|
10
11
|
|
|
@@ -60,7 +61,7 @@ export function resolveRoleAlias<T extends RoleSession>(
|
|
|
60
61
|
const workspace = caller ? sessionWorkspace(caller) : null;
|
|
61
62
|
if (role === "supervisor" && workspace) {
|
|
62
63
|
const orchestrator = others.find(
|
|
63
|
-
(s) => s.role
|
|
64
|
+
(s) => isTechRole(s.role) && s.workspace_id === workspace
|
|
64
65
|
);
|
|
65
66
|
if (orchestrator) return { ok: true, session: orchestrator };
|
|
66
67
|
}
|
package/lib/bus/targets.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Which live session a name someone typed reaches, across workspaces.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isTechRole, leadKindOf } from "../leads/roles";
|
|
6
|
+
import { hasLeads } from "../leads/settings";
|
|
5
7
|
import { db, type Session } from "../db";
|
|
6
8
|
import { getProject } from "../projects";
|
|
7
9
|
import { getWorkspace } from "../workspaces";
|
|
@@ -37,7 +39,10 @@ export function candidates(sessions: Session[]): Candidate[] {
|
|
|
37
39
|
tmuxName: s.tmux_name,
|
|
38
40
|
previousNames: old.get(s.id) ?? [],
|
|
39
41
|
workspaceName: workspaceName(s),
|
|
40
|
-
orchestrator: s.role
|
|
42
|
+
orchestrator: isTechRole(s.role),
|
|
43
|
+
// The orchestrator answers to "tech" only once its workspace has leads.
|
|
44
|
+
leadKind:
|
|
45
|
+
isTechRole(s.role) && !hasLeads(s.workspace_id) ? null : leadKindOf(s),
|
|
41
46
|
}));
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -77,7 +82,7 @@ function ownOrchestrator(
|
|
|
77
82
|
const workspace = caller ? sessionWorkspace(caller) : null;
|
|
78
83
|
if (!workspace) return undefined;
|
|
79
84
|
return sessions.find(
|
|
80
|
-
(s) => s.role
|
|
85
|
+
(s) => isTechRole(s.role) && s.workspace_id === workspace
|
|
81
86
|
);
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -95,7 +100,7 @@ export function tryResolveTarget(
|
|
|
95
100
|
const r = resolveRef(ref, candidates(sessions));
|
|
96
101
|
if (!r.ok) {
|
|
97
102
|
const orchestrators = /^(my )?orchestrator$/i.test(ref.trim())
|
|
98
|
-
? sessions.filter((s) => s.role
|
|
103
|
+
? sessions.filter((s) => isTechRole(s.role))
|
|
99
104
|
: [];
|
|
100
105
|
if (orchestrators.length)
|
|
101
106
|
return {
|
package/lib/bus/waits/tick.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* reads status through statusOf, the same idle test the orchestrator uses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../../leads/roles";
|
|
8
9
|
import type { Session } from "../../db";
|
|
9
10
|
import { db } from "../../db";
|
|
10
11
|
import { statusDetector } from "../../status-detector";
|
|
@@ -128,7 +129,7 @@ async function settle(w: WaitRow, waiter: Session, now: number): Promise<void> {
|
|
|
128
129
|
timeoutLine(target, w.kind, w.deadline - w.created_at, w.request, is)
|
|
129
130
|
);
|
|
130
131
|
const workspace = sessionWorkspace(waiter);
|
|
131
|
-
if (workspace && waiter.role
|
|
132
|
+
if (workspace && !isStandingRole(waiter.role))
|
|
132
133
|
queueEventOnce(
|
|
133
134
|
workspace,
|
|
134
135
|
`wait-timeout:${w.id}`,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* deploy restarted the worker too.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../leads/roles";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import { statusPorcelain } from "../git-exec";
|
|
10
11
|
import { db, type Session } from "../db";
|
|
@@ -33,7 +34,7 @@ export async function compactContext(sessionId: string): Promise<string> {
|
|
|
33
34
|
.get(sessionId) as
|
|
34
35
|
Pick<Session, "working_directory" | "task_prompt" | "role"> | undefined;
|
|
35
36
|
// An orchestrator starts fresh instead of compacting.
|
|
36
|
-
if (!session || session.role
|
|
37
|
+
if (!session || isStandingRole(session.role)) return "";
|
|
37
38
|
const tools = lastItems(sessionId, 400, "tool").filter(
|
|
38
39
|
(i) => i.kind === "tool"
|
|
39
40
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole } from "../leads/roles";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
@@ -35,7 +36,7 @@ export function mentionRoots(sessionId: string): MentionRoot[] | null {
|
|
|
35
36
|
working_directory: string;
|
|
36
37
|
}
|
|
37
38
|
| undefined;
|
|
38
|
-
if (session?.role
|
|
39
|
+
if (!isStandingRole(session?.role)) return null;
|
|
39
40
|
|
|
40
41
|
// Never the home folder or the whole disk: a project pointed there would
|
|
41
42
|
// list everything under it, dotfiles and keys included.
|
package/lib/chat/runner.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* events to whoever is watching. Restarting the server leaves turns running.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../leads/roles";
|
|
8
9
|
import { pushPlanReady } from "../notify/push/events";
|
|
9
10
|
import {
|
|
10
11
|
handoffAfterPlan,
|
|
@@ -638,7 +639,7 @@ async function resumeCutOff(sessionId: string): Promise<void> {
|
|
|
638
639
|
const task =
|
|
639
640
|
row.workspace_id &&
|
|
640
641
|
row.task_status === "running" &&
|
|
641
|
-
row.role
|
|
642
|
+
!isStandingRole(row.role)
|
|
642
643
|
? row.workspace_id
|
|
643
644
|
: null;
|
|
644
645
|
try {
|
|
@@ -923,7 +924,7 @@ export async function freshStartChat(
|
|
|
923
924
|
id: `note-refresh-${Date.now()}`,
|
|
924
925
|
kind: "note",
|
|
925
926
|
tone: "note",
|
|
926
|
-
text: `Fresh start: the agent starts a new conversation with its brief${session.role
|
|
927
|
+
text: `Fresh start: the agent starts a new conversation with its brief${isStandingRole(session.role) ? " and the current state from AgentOS" : ""} at its next message.`,
|
|
927
928
|
createdAt: Date.now(),
|
|
928
929
|
};
|
|
929
930
|
saveItem(sessionId, item);
|
package/lib/chat/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* per-session settings that go with them: the model and the access level.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isStandingRole } from "../leads/roles";
|
|
6
7
|
import { effectiveChatAccess, noApprovalsOn } from "../approvals/no-approvals";
|
|
7
8
|
import os from "os";
|
|
8
9
|
import { agentLabel } from "../agent-options";
|
|
@@ -330,7 +331,7 @@ export async function setChatAccess(
|
|
|
330
331
|
): Promise<void> {
|
|
331
332
|
if (!CHAT_ACCESS.includes(access)) return;
|
|
332
333
|
// An orchestrator's access is fixed by its role.
|
|
333
|
-
if (getSession(sessionId).role
|
|
334
|
+
if (isStandingRole(getSession(sessionId).role)) return;
|
|
334
335
|
db.prepare(`UPDATE sessions SET chat_access = ? WHERE id = ?`).run(
|
|
335
336
|
access,
|
|
336
337
|
sessionId
|
|
@@ -341,7 +342,7 @@ export async function setChatAccess(
|
|
|
341
342
|
|
|
342
343
|
// An orchestrator's permission mode is fixed by its role.
|
|
343
344
|
const canPlan = (s: Session) =>
|
|
344
|
-
s.role
|
|
345
|
+
!isStandingRole(s.role) && chatDriverFor(s.agent_type)?.plan !== false;
|
|
345
346
|
|
|
346
347
|
// Plan mode: the agent reads and plans, and changes nothing until the plan
|
|
347
348
|
// is carried out. Now if a conversation is live, and for every next start.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLeadRole } from "../../leads/roles";
|
|
1
2
|
import type { Session } from "../../db";
|
|
2
3
|
import type { ChatStartOptions } from "../driver";
|
|
3
4
|
import { refreshSettings } from "../refresh";
|
|
@@ -40,7 +41,9 @@ export async function roleExtras(session: Session): Promise<RoleExtras> {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async function ownExtras(session: Session): Promise<RoleExtras> {
|
|
43
|
-
|
|
44
|
+
// The orchestrator and the other leads run on the orchestrator's worker,
|
|
45
|
+
// each with its own profile.
|
|
46
|
+
if (isLeadRole(session.role))
|
|
44
47
|
return (await import("../../orchestrator/worker")).orchestratorExtras(
|
|
45
48
|
session
|
|
46
49
|
);
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* events to whoever is connected.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isStandingRole } from "../../leads/roles";
|
|
7
8
|
import os from "os";
|
|
8
9
|
import { db, type Session } from "../../db";
|
|
9
10
|
import { agentEnv } from "../../agents/launch";
|
|
@@ -216,7 +217,7 @@ export class ChatHost {
|
|
|
216
217
|
this.atRest = driver.atRest !== false;
|
|
217
218
|
const cwd = session.working_directory.replace(/^~/, os.homedir());
|
|
218
219
|
const env = { ...agentEnv(session.id), ...extras.env };
|
|
219
|
-
const orchestrator = session.role
|
|
220
|
+
const orchestrator = isStandingRole(session.role);
|
|
220
221
|
// Every chat can show visuals, asked for like any tool under its access
|
|
221
222
|
// setting (they read files and reach the web). An orchestrator reads
|
|
222
223
|
// other sessions' text, so it gets a shorter brief that says what stays
|
|
@@ -289,7 +290,7 @@ export class ChatHost {
|
|
|
289
290
|
if (session.chat_refreshed)
|
|
290
291
|
this.refreshedNote = refreshedNote(
|
|
291
292
|
extras.refresh?.role ??
|
|
292
|
-
(session.role
|
|
293
|
+
(isStandingRole(session.role) ? "orchestrator" : "task"),
|
|
293
294
|
session.chat_refreshed
|
|
294
295
|
);
|
|
295
296
|
// Told only an orchestrator whose tools changed since its last worker:
|
|
@@ -964,7 +965,7 @@ export class ChatHost {
|
|
|
964
965
|
const stale = this.staleNow(cmd.compact);
|
|
965
966
|
// An orchestrator starts over at its own threshold (lib/chat/refresh);
|
|
966
967
|
// only one gone stale does it early.
|
|
967
|
-
if (!stale || (this.session.role
|
|
968
|
+
if (!stale || (isStandingRole(this.session.role) && !stale.stale))
|
|
968
969
|
return false;
|
|
969
970
|
try {
|
|
970
971
|
enqueue(this.session.id, cmd);
|
|
@@ -978,7 +979,7 @@ export class ChatHost {
|
|
|
978
979
|
// from tasks and CI without its untrusted fence. It starts a fresh
|
|
979
980
|
// conversation from its brief and the database instead, as at its
|
|
980
981
|
// threshold, and the next worker sends the message.
|
|
981
|
-
if (this.session.role
|
|
982
|
+
if (isStandingRole(this.session.role)) {
|
|
982
983
|
this.staleRefresh = stale;
|
|
983
984
|
this.refreshAt = stale.tokens;
|
|
984
985
|
this.retiring = true;
|
|
@@ -1085,7 +1086,7 @@ export class ChatHost {
|
|
|
1085
1086
|
// line saying where it started over.
|
|
1086
1087
|
private endConversation(): void {
|
|
1087
1088
|
releaseConversation(this.session.id, this.refreshAt);
|
|
1088
|
-
const brief = `its brief${this.session.role
|
|
1089
|
+
const brief = `its brief${isStandingRole(this.session.role) ? " and the current state from AgentOS" : ""}`;
|
|
1089
1090
|
const stale = this.staleRefresh;
|
|
1090
1091
|
const requested = this.refreshAt === REQUESTED_FRESH;
|
|
1091
1092
|
this.record({
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
import { createSessionRowsView } from "../session-parts/view";
|
|
3
|
+
|
|
4
|
+
const columnsOf = (db: Parameters<Migration["up"]>[0], table: string) =>
|
|
5
|
+
(db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]).map(
|
|
6
|
+
(c) => c.name
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
key: "20261102T1944_leads",
|
|
11
|
+
up: (db) => {
|
|
12
|
+
// Leads (lib/leads): a lead is a standing chat like the orchestrator,
|
|
13
|
+
// with role 'lead' and a lead_kind; the orchestrator is the 'tech' lead
|
|
14
|
+
// once its workspace adds leads, and a workspace has one 'chief'.
|
|
15
|
+
// lead_since is when it became one, so its usage counts from then.
|
|
16
|
+
// Additive: a workspace without leads has none of these rows.
|
|
17
|
+
const sessions = columnsOf(db, "sessions");
|
|
18
|
+
if (!sessions.includes("lead_kind"))
|
|
19
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN lead_kind TEXT`);
|
|
20
|
+
if (!sessions.includes("lead_since"))
|
|
21
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN lead_since TEXT`);
|
|
22
|
+
createSessionRowsView(db);
|
|
23
|
+
db.exec(
|
|
24
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_lead
|
|
25
|
+
ON sessions(workspace_id, lead_kind) WHERE role = 'lead'`
|
|
26
|
+
);
|
|
27
|
+
db.exec(
|
|
28
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_chief
|
|
29
|
+
ON sessions(workspace_id) WHERE role = 'chief'`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// A row means the workspace added leads. The chief's autonomy starts at
|
|
33
|
+
// acting only when asked; a later setting lifts it without code.
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS workspace_leads (
|
|
36
|
+
workspace_id TEXT PRIMARY KEY,
|
|
37
|
+
enabled_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
chief_autonomy TEXT NOT NULL DEFAULT 'on_request'
|
|
39
|
+
CHECK (chief_autonomy IN ('on_request', 'acts'))
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS lead_charters (
|
|
44
|
+
workspace_id TEXT NOT NULL,
|
|
45
|
+
lead_kind TEXT NOT NULL,
|
|
46
|
+
title TEXT NOT NULL,
|
|
47
|
+
area_text TEXT NOT NULL DEFAULT '',
|
|
48
|
+
charter_md TEXT NOT NULL DEFAULT '',
|
|
49
|
+
goal TEXT NOT NULL DEFAULT '',
|
|
50
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
51
|
+
PRIMARY KEY (workspace_id, lead_kind)
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS lead_areas (
|
|
56
|
+
workspace_id TEXT NOT NULL,
|
|
57
|
+
lead_kind TEXT NOT NULL,
|
|
58
|
+
kind TEXT NOT NULL CHECK (kind IN ('project', 'path', 'board')),
|
|
59
|
+
value TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (workspace_id, lead_kind, kind, value)
|
|
61
|
+
)
|
|
62
|
+
`);
|
|
63
|
+
// Pinned direction quotes a chat item the person wrote; a newer pin
|
|
64
|
+
// supersedes an older one and nothing is deleted.
|
|
65
|
+
db.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS lead_direction (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
workspace_id TEXT NOT NULL,
|
|
69
|
+
lead_kind TEXT NOT NULL,
|
|
70
|
+
session_id TEXT NOT NULL,
|
|
71
|
+
chat_item_id TEXT NOT NULL,
|
|
72
|
+
quote TEXT NOT NULL,
|
|
73
|
+
set_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
74
|
+
superseded_by INTEGER
|
|
75
|
+
)
|
|
76
|
+
`);
|
|
77
|
+
db.exec(
|
|
78
|
+
`CREATE INDEX IF NOT EXISTS idx_lead_direction
|
|
79
|
+
ON lead_direction(workspace_id, lead_kind, id)`
|
|
80
|
+
);
|
|
81
|
+
// Proposed pins (an adopted lead's, or a lead's suggestion) wait here
|
|
82
|
+
// until the person confirms or drops them.
|
|
83
|
+
db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS lead_pin_proposals (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
workspace_id TEXT NOT NULL,
|
|
87
|
+
lead_kind TEXT NOT NULL,
|
|
88
|
+
session_id TEXT NOT NULL,
|
|
89
|
+
chat_item_id TEXT NOT NULL,
|
|
90
|
+
quote TEXT NOT NULL,
|
|
91
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
92
|
+
CHECK (status IN ('open', 'pinned', 'dropped')),
|
|
93
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
94
|
+
resolved_at TEXT
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
db.exec(
|
|
98
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_lead_pin_proposals_open
|
|
99
|
+
ON lead_pin_proposals(session_id, chat_item_id) WHERE status = 'open'`
|
|
100
|
+
);
|
|
101
|
+
db.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS lead_memos (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
workspace_id TEXT NOT NULL,
|
|
105
|
+
lead_kind TEXT NOT NULL,
|
|
106
|
+
text TEXT NOT NULL,
|
|
107
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
db.exec(
|
|
111
|
+
`CREATE INDEX IF NOT EXISTS idx_lead_memos
|
|
112
|
+
ON lead_memos(workspace_id, lead_kind, id)`
|
|
113
|
+
);
|
|
114
|
+
db.exec(`
|
|
115
|
+
CREATE TABLE IF NOT EXISTS lead_messages (
|
|
116
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
117
|
+
workspace_id TEXT NOT NULL,
|
|
118
|
+
from_kind TEXT NOT NULL,
|
|
119
|
+
to_kind TEXT NOT NULL,
|
|
120
|
+
thread TEXT NOT NULL,
|
|
121
|
+
text TEXT NOT NULL,
|
|
122
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
123
|
+
read_at TEXT
|
|
124
|
+
)
|
|
125
|
+
`);
|
|
126
|
+
db.exec(
|
|
127
|
+
`CREATE INDEX IF NOT EXISTS idx_lead_messages_to
|
|
128
|
+
ON lead_messages(workspace_id, to_kind, id)`
|
|
129
|
+
);
|
|
130
|
+
db.exec(
|
|
131
|
+
`CREATE INDEX IF NOT EXISTS idx_lead_messages_thread
|
|
132
|
+
ON lead_messages(workspace_id, thread, id)`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// The decision log and asks per lead; every existing row is Tech's.
|
|
136
|
+
for (const table of ["orchestrator_notes", "orchestrator_asks"])
|
|
137
|
+
if (!columnsOf(db, table).includes("lead_kind"))
|
|
138
|
+
db.exec(
|
|
139
|
+
`ALTER TABLE ${table} ADD COLUMN lead_kind TEXT NOT NULL DEFAULT 'tech'`
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
} satisfies Migration;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261102T2300_needs_you_chat_per_session",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
// The Needs-you "waiting in chat" item was keyed per workspace
|
|
7
|
+
// (approval:<workspace id>), so one lead's waiting chat would hide
|
|
8
|
+
// another's. It is keyed per session now; a hidden one stays hidden on
|
|
9
|
+
// the workspace's orchestrator, the only chat that showed it. Safe to
|
|
10
|
+
// repeat: a renamed key no longer matches.
|
|
11
|
+
db.exec(`
|
|
12
|
+
UPDATE OR IGNORE needs_you_dismissed
|
|
13
|
+
SET key = 'approval:' || (
|
|
14
|
+
SELECT s.id FROM sessions s
|
|
15
|
+
WHERE s.role = 'orchestrator'
|
|
16
|
+
AND s.workspace_id = needs_you_dismissed.workspace_id)
|
|
17
|
+
WHERE key = 'approval:' || workspace_id
|
|
18
|
+
AND EXISTS (
|
|
19
|
+
SELECT 1 FROM sessions s
|
|
20
|
+
WHERE s.role = 'orchestrator'
|
|
21
|
+
AND s.workspace_id = needs_you_dismissed.workspace_id)
|
|
22
|
+
`);
|
|
23
|
+
},
|
|
24
|
+
} satisfies Migration;
|
package/lib/db/types.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AgentType } from "../providers";
|
|
2
2
|
import type { ChatAccess } from "../chat/events";
|
|
3
3
|
|
|
4
|
+
// The standing chats a workspace can have (lib/leads/roles.ts).
|
|
5
|
+
export type SessionRole = "orchestrator" | "lead" | "chief";
|
|
6
|
+
|
|
4
7
|
export interface Session {
|
|
5
8
|
id: string;
|
|
6
9
|
name: string;
|
|
@@ -118,8 +121,13 @@ export interface Session {
|
|
|
118
121
|
// The agent role it was started in (lib/models/agent-roles) and its crew.
|
|
119
122
|
agent_role?: string | null;
|
|
120
123
|
crew_id?: string | null;
|
|
121
|
-
// A workspace's standing
|
|
122
|
-
|
|
124
|
+
// A workspace's standing chat (and that workspace), or null: its
|
|
125
|
+
// orchestrator, and with Leads its other leads and chief (lib/leads).
|
|
126
|
+
role: SessionRole | null;
|
|
127
|
+
// A lead's department ('tech' for the orchestrator once leads are added)
|
|
128
|
+
// and when it became one.
|
|
129
|
+
lead_kind?: string | null;
|
|
130
|
+
lead_since?: string | null;
|
|
123
131
|
// An orchestrator's tool-call secret.
|
|
124
132
|
orch_token?: string | null;
|
|
125
133
|
workspace_id: string | null;
|
package/lib/done/archive.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Archived sessions: out of the sidebar, the needs-you count and the
|
|
2
2
|
// orchestrator's view, but kept with their history and chat items.
|
|
3
3
|
|
|
4
|
+
import { isStandingRole, roleNoun } from "../leads/roles";
|
|
4
5
|
import fs from "fs";
|
|
5
6
|
import { db, queries, type Session } from "../db";
|
|
6
7
|
import { statusOf } from "../orchestrator/facts";
|
|
@@ -102,9 +103,9 @@ export async function archiveChecked(
|
|
|
102
103
|
const s = queries.getSession(db).get(id) as Session | undefined;
|
|
103
104
|
if (!s) throw new Error("Session not found");
|
|
104
105
|
if (s.archived_at) throw new Error(`${s.name} is already archived`);
|
|
105
|
-
if (s.role
|
|
106
|
+
if (isStandingRole(s.role))
|
|
106
107
|
throw new Error(
|
|
107
|
-
`${s.name} is a workspace's
|
|
108
|
+
`${s.name} is a workspace's ${roleNoun(s.role)}; it can't be archived`
|
|
108
109
|
);
|
|
109
110
|
if (peerSessionLink(s))
|
|
110
111
|
throw new Error(`${s.name} runs on another machine: archive it there`);
|
package/lib/done/auto-archive.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// session, an orchestrator, one that needs input or is working, or one with
|
|
4
4
|
// an open PR. Mirrors the sidebar's Done placement (lib/sidebar/shelves).
|
|
5
5
|
|
|
6
|
+
import { sqlNoRole } from "../leads/roles";
|
|
6
7
|
import { db, type Session } from "../db";
|
|
7
8
|
import { fromSqliteTime } from "../session-meta";
|
|
8
9
|
import { statusOf } from "../orchestrator/facts";
|
|
@@ -34,7 +35,7 @@ export async function archiveStaleDone(now = Date.now()): Promise<string[]> {
|
|
|
34
35
|
const rows = db
|
|
35
36
|
.prepare(
|
|
36
37
|
`SELECT * FROM session_rows
|
|
37
|
-
WHERE archived_at IS NULL AND pinned = 0 AND
|
|
38
|
+
WHERE archived_at IS NULL AND pinned = 0 AND ${sqlNoRole()}
|
|
38
39
|
AND peer_mirror = 0 AND quick_of IS NULL
|
|
39
40
|
AND (pr_status IS NULL OR pr_status != 'open')`
|
|
40
41
|
)
|
package/lib/done/bulk.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* The preview says what will happen to each before anything does.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { sqlNoRole, isStandingRole, roleLabel } from "../leads/roles";
|
|
8
9
|
import { db, type Session } from "../db";
|
|
9
10
|
import { getProject } from "../projects";
|
|
10
11
|
import { workspaceProjects } from "../orchestrator/brief";
|
|
@@ -52,7 +53,7 @@ function scopeSessions(scope: DoneScope): Session[] {
|
|
|
52
53
|
.prepare(
|
|
53
54
|
`SELECT * FROM session_rows
|
|
54
55
|
WHERE project_id IN (${projectIds.map(() => "?").join(", ")})
|
|
55
|
-
AND
|
|
56
|
+
AND ${sqlNoRole()} AND archived_at IS NULL
|
|
56
57
|
-- A linked machine's own sessions are cleaned up there.
|
|
57
58
|
AND peer_mirror = 0
|
|
58
59
|
-- A task another machine's orchestrator moved here is its to finish.
|
|
@@ -80,9 +81,9 @@ export async function idleSessions(
|
|
|
80
81
|
export function scopeOf(session: Session): DoneScope {
|
|
81
82
|
// An orchestrator acts through its tools, which Pause holds and which are
|
|
82
83
|
// logged; aos would reach the same sessions past both.
|
|
83
|
-
if (session.role
|
|
84
|
+
if (isStandingRole(session.role))
|
|
84
85
|
throw new Error(
|
|
85
|
-
`${session.name} is
|
|
86
|
+
`${session.name} is ${roleLabel(session.role)}: it moves, finishes and stops sessions with its move, done and stop tools, not aos`
|
|
86
87
|
);
|
|
87
88
|
const project = session.project_id ? getProject(session.project_id) : null;
|
|
88
89
|
if (!project || project.is_uncategorized)
|