@saadnvd1/agent-os 0.3.819 → 0.3.821
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/client-config/route.test.ts +3 -1
- package/app/api/owner/route.test.ts +27 -0
- package/app/api/owner/route.ts +17 -0
- package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +2 -2
- package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +1 -1
- package/app/api/workspaces/[id]/orchestrator/rules/route.ts +2 -2
- package/app/api/workspaces/[id]/orchestrator/tools/route.ts +6 -1
- package/app/api/worktree-sweep/route.ts +2 -2
- package/components/Chat/EventRow.tsx +5 -2
- package/components/Chat/Messages.tsx +9 -2
- package/components/SessionList/statusDot.test.ts +1 -1
- package/components/Settings/OwnerNameRow.tsx +55 -0
- package/components/Settings/SetupSection.tsx +2 -0
- package/content/whats-new.json +8 -0
- package/data/client-config/queries.ts +3 -0
- package/data/owner/mutations.ts +19 -0
- package/lib/approvals/enable.ts +2 -1
- package/lib/artifacts/tools.ts +2 -1
- package/lib/away/brief.ts +2 -1
- package/lib/away/run.ts +5 -4
- package/lib/away/sweep.ts +2 -1
- package/lib/chat/runner.ts +2 -1
- package/lib/chat/turns.ts +1 -1
- package/lib/config/client-config-types.ts +2 -0
- package/lib/config/client-config.ts +2 -0
- package/lib/learning-cards/analyze.ts +2 -1
- package/lib/models/plan-step.ts +2 -1
- package/lib/orchestrator/approval-carry.ts +2 -1
- package/lib/orchestrator/armed.ts +9 -8
- package/lib/orchestrator/ask-heads.ts +2 -1
- package/lib/orchestrator/ask-tool.ts +6 -5
- package/lib/orchestrator/asks.ts +6 -4
- package/lib/orchestrator/brakes.ts +3 -2
- package/lib/orchestrator/brief.ts +61 -50
- package/lib/orchestrator/cross.ts +2 -1
- package/lib/orchestrator/describe.ts +5 -2
- package/lib/orchestrator/digest.ts +4 -3
- package/lib/orchestrator/escalate.ts +7 -6
- package/lib/orchestrator/external-gates.ts +5 -4
- package/lib/orchestrator/held-asks.ts +2 -2
- package/lib/orchestrator/held.ts +3 -2
- package/lib/orchestrator/mcp-servers.ts +1 -1
- package/lib/orchestrator/pause.ts +4 -3
- package/lib/orchestrator/questions.ts +2 -1
- package/lib/orchestrator/recipe-tools.ts +5 -4
- package/lib/orchestrator/review.ts +4 -3
- package/lib/orchestrator/rules.ts +2 -1
- package/lib/orchestrator/serve.ts +1 -1
- package/lib/orchestrator/shell-guard.ts +4 -4
- package/lib/orchestrator/signoff.test-helpers.ts +1 -1
- package/lib/orchestrator/signoff.ts +10 -7
- package/lib/orchestrator/sweep-arm.ts +2 -1
- package/lib/orchestrator/tool-names.ts +1 -1
- package/lib/orchestrator/tool-schemas.ts +13 -13
- package/lib/orchestrator/tools.ts +21 -21
- package/lib/owner-label.ts +25 -0
- package/lib/owner.ts +21 -0
- package/lib/project-add/index.ts +2 -1
- package/lib/schedules/index.ts +2 -1
- package/lib/session-logs/gather.ts +3 -2
- package/lib/tasks/queue.ts +3 -1
- package/lib/tasks/start.ts +3 -1
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ describe("GET /api/client-config", () => {
|
|
|
6
6
|
const res = GET();
|
|
7
7
|
expect(res.status).toBe(200);
|
|
8
8
|
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
9
|
-
|
|
9
|
+
const body = await res.json();
|
|
10
|
+
expect(body.v).toBe(1);
|
|
11
|
+
expect(body).toHaveProperty("ownerName");
|
|
10
12
|
});
|
|
11
13
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GET, PUT } from "./route";
|
|
3
|
+
|
|
4
|
+
const put = (body: unknown) =>
|
|
5
|
+
PUT(
|
|
6
|
+
new Request("http://localhost/api/owner", {
|
|
7
|
+
method: "PUT",
|
|
8
|
+
body: JSON.stringify(body),
|
|
9
|
+
})
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe("/api/owner", () => {
|
|
13
|
+
it("sets, reads and clears the owner's name", async () => {
|
|
14
|
+
expect(await (await GET()).json()).toEqual({ name: null });
|
|
15
|
+
expect(await (await put({ name: " Alex " })).json()).toEqual({
|
|
16
|
+
name: "Alex",
|
|
17
|
+
});
|
|
18
|
+
expect(await (await GET()).json()).toEqual({ name: "Alex" });
|
|
19
|
+
expect(await (await put({ name: null })).json()).toEqual({ name: null });
|
|
20
|
+
expect(await (await GET()).json()).toEqual({ name: null });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("refuses a name that isn't text", async () => {
|
|
24
|
+
expect((await put({ name: 5 })).status).toBe(400);
|
|
25
|
+
expect((await put({})).status).toBe(400);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ownerName, setOwnerName } from "@/lib/owner";
|
|
3
|
+
|
|
4
|
+
// GET /api/owner - the name the owner gave, or null.
|
|
5
|
+
export function GET() {
|
|
6
|
+
return NextResponse.json({ name: ownerName() });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// PUT /api/owner { name } - set it; blank or null clears it.
|
|
10
|
+
export async function PUT(request: Request) {
|
|
11
|
+
const body = (await request.json().catch(() => null)) as {
|
|
12
|
+
name?: unknown;
|
|
13
|
+
} | null;
|
|
14
|
+
if (!body || (body.name !== null && typeof body.name !== "string"))
|
|
15
|
+
return NextResponse.json({ error: "name must be text" }, { status: 400 });
|
|
16
|
+
return NextResponse.json({ name: setOwnerName(body.name) });
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import type { PresenceProof } from "@/lib/security/presence-http";
|
|
3
3
|
import { removeRule, updateRule } from "@/lib/orchestrator/rules";
|
|
4
|
-
import {
|
|
4
|
+
import { provenAsOwner } from "../presence";
|
|
5
5
|
|
|
6
6
|
type RouteParams = { params: Promise<{ id: string; ruleId: string }> };
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@ async function target(
|
|
|
21
21
|
response: NextResponse.json({ error: "Bad rule id" }, { status: 400 }),
|
|
22
22
|
};
|
|
23
23
|
const body = (await request.json().catch(() => ({}))) as Body;
|
|
24
|
-
const refused = await
|
|
24
|
+
const refused = await provenAsOwner(request, id, body);
|
|
25
25
|
if (refused) return { response: refused };
|
|
26
26
|
return { workspaceId: id, ruleId: n, body };
|
|
27
27
|
}
|
|
@@ -10,7 +10,7 @@ import { rulesPresence } from "@/lib/orchestrator/presence-binding";
|
|
|
10
10
|
// A rule written here is Saad's, shown to the orchestrator as his word and
|
|
11
11
|
// removable only by him: so it takes his passkey, not just a trusted place
|
|
12
12
|
// (any process on this machine reaches the route).
|
|
13
|
-
export async function
|
|
13
|
+
export async function provenAsOwner(
|
|
14
14
|
request: NextRequest,
|
|
15
15
|
workspaceId: string,
|
|
16
16
|
proof: PresenceProof | undefined
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import type { PresenceProof } from "@/lib/security/presence-http";
|
|
3
3
|
import { getWorkspace } from "@/lib/workspaces";
|
|
4
4
|
import { addRule, listRules } from "@/lib/orchestrator/rules";
|
|
5
|
-
import {
|
|
5
|
+
import { provenAsOwner } from "./presence";
|
|
6
6
|
|
|
7
7
|
type RouteParams = { params: Promise<{ id: string }> };
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
|
25
25
|
const { text } = body;
|
|
26
26
|
if (typeof text !== "string")
|
|
27
27
|
return NextResponse.json({ error: "text is required" }, { status: 400 });
|
|
28
|
-
const refused = await
|
|
28
|
+
const refused = await provenAsOwner(request, id, body);
|
|
29
29
|
if (refused) return refused;
|
|
30
30
|
try {
|
|
31
31
|
return NextResponse.json({ rule: addRule(id, text, "saad") });
|
|
@@ -5,6 +5,8 @@ import { TOKEN_HEADER } from "@/lib/orchestrator/tools";
|
|
|
5
5
|
|
|
6
6
|
type RouteParams = { params: Promise<{ id: string }> };
|
|
7
7
|
|
|
8
|
+
const LEGACY_ASK = "ask_saad";
|
|
9
|
+
|
|
8
10
|
// The orchestrator chat's tools call here from its worker: plain text back.
|
|
9
11
|
// Only that workspace's orchestrator worker holds the secret: other local
|
|
10
12
|
// sessions are trusted by the network, not to act as the orchestrator.
|
|
@@ -12,7 +14,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
|
12
14
|
const { id } = await params;
|
|
13
15
|
if (!isOrchestratorToken(id, request.headers.get(TOKEN_HEADER)))
|
|
14
16
|
return new NextResponse("Not the orchestrator", { status: 403 });
|
|
15
|
-
const
|
|
17
|
+
const body = await request.json().catch(() => ({}));
|
|
18
|
+
const { args } = body;
|
|
19
|
+
// A worker started before the rename still calls ask_saad.
|
|
20
|
+
const tool = body.tool === LEGACY_ASK ? "ask_owner" : body.tool;
|
|
16
21
|
if (!isToolName(tool))
|
|
17
22
|
return NextResponse.json({ error: "Unknown tool" }, { status: 400 });
|
|
18
23
|
try {
|
|
@@ -10,6 +10,7 @@ import { requireLocalTrust } from "@/lib/security/route-guard";
|
|
|
10
10
|
import { db, type Session } from "@/lib/db";
|
|
11
11
|
import { orchestratorFullAccess } from "@/lib/orchestrator/full-access";
|
|
12
12
|
import { sweepState } from "@/lib/worktree-sweep/state";
|
|
13
|
+
import { owner } from "@/lib/owner";
|
|
13
14
|
|
|
14
15
|
const body = z.union([
|
|
15
16
|
z.object({
|
|
@@ -53,8 +54,7 @@ export async function PUT(request: NextRequest) {
|
|
|
53
54
|
if (!s || !orchestratorFullAccess(s))
|
|
54
55
|
return NextResponse.json(
|
|
55
56
|
{
|
|
56
|
-
error:
|
|
57
|
-
"a session can't turn the full clean-up on: Saad does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to",
|
|
57
|
+
error: `a session can't turn the full clean-up on: ${owner()} does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to`,
|
|
58
58
|
},
|
|
59
59
|
{ status: 403 }
|
|
60
60
|
);
|
|
@@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
|
|
|
18
18
|
import { UserMessage } from "./Messages";
|
|
19
19
|
import { openSessionLink as openSession } from "./session-link";
|
|
20
20
|
import { useRowState } from "./rowState";
|
|
21
|
+
import { useOwnerName } from "@/data/client-config/queries";
|
|
22
|
+
import { ownerInUi } from "@/lib/owner-label";
|
|
21
23
|
|
|
22
24
|
type UserItem = Extract<ChatItem, { kind: "user" }>;
|
|
23
25
|
|
|
@@ -158,6 +160,7 @@ export function EventRow({
|
|
|
158
160
|
origin: ChatOrigin;
|
|
159
161
|
}) {
|
|
160
162
|
const { body, decided } = eventParts(item, origin);
|
|
163
|
+
const decidedLabel = `${ownerInUi(useOwnerName(), true)} decided`;
|
|
161
164
|
const decision = origin.kind === "decision";
|
|
162
165
|
return (
|
|
163
166
|
<div className="flex flex-col gap-1">
|
|
@@ -167,7 +170,7 @@ export function EventRow({
|
|
|
167
170
|
id={`${item.id}:${i}`}
|
|
168
171
|
kind="decision"
|
|
169
172
|
tone="decision"
|
|
170
|
-
head={<span className="truncate font-medium">
|
|
173
|
+
head={<span className="truncate font-medium">{decidedLabel}</span>}
|
|
171
174
|
body={d}
|
|
172
175
|
at={item.createdAt}
|
|
173
176
|
/>
|
|
@@ -179,7 +182,7 @@ export function EventRow({
|
|
|
179
182
|
tone={decision ? "decision" : undefined}
|
|
180
183
|
head={
|
|
181
184
|
decision ? (
|
|
182
|
-
<span className="truncate font-medium">
|
|
185
|
+
<span className="truncate font-medium">{decidedLabel}</span>
|
|
183
186
|
) : origin.kind === "guest" ? (
|
|
184
187
|
<span className="truncate font-medium">
|
|
185
188
|
Suggested by {origin.label}
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
TurnBookmark,
|
|
40
40
|
BookmarkPress,
|
|
41
41
|
} from "@/components/Bookmarks/BookmarkButton";
|
|
42
|
+
import { useOwnerName } from "@/data/client-config/queries";
|
|
43
|
+
import { ownerVerb } from "@/lib/owner-label";
|
|
42
44
|
|
|
43
45
|
type Of<K extends ChatItem["kind"]> = Extract<ChatItem, { kind: K }>;
|
|
44
46
|
|
|
@@ -473,13 +475,18 @@ export function CommandOutput({ item }: { item: Of<"command_output"> }) {
|
|
|
473
475
|
const NOTE_LABEL = {
|
|
474
476
|
note: "Note",
|
|
475
477
|
brake: "Brake",
|
|
476
|
-
escalation: "
|
|
478
|
+
escalation: "",
|
|
477
479
|
ask: "Ask",
|
|
478
480
|
pause: "Pause",
|
|
479
481
|
away: "Away",
|
|
480
482
|
} as const;
|
|
481
483
|
|
|
482
484
|
export function NoteLine({ item }: { item: Of<"note"> }) {
|
|
485
|
+
const name = useOwnerName();
|
|
486
|
+
const label =
|
|
487
|
+
item.tone === "escalation"
|
|
488
|
+
? ownerVerb(name, "decides", "decide")
|
|
489
|
+
: NOTE_LABEL[item.tone];
|
|
483
490
|
return (
|
|
484
491
|
<div
|
|
485
492
|
className={cn(
|
|
@@ -490,7 +497,7 @@ export function NoteLine({ item }: { item: Of<"note"> }) {
|
|
|
490
497
|
)}
|
|
491
498
|
>
|
|
492
499
|
<span className="text-muted-foreground mr-2 font-mono text-[11px] tracking-wide uppercase">
|
|
493
|
-
{
|
|
500
|
+
{label}
|
|
494
501
|
</span>
|
|
495
502
|
{item.text}
|
|
496
503
|
</div>
|
|
@@ -35,7 +35,7 @@ describe("statusDot", () => {
|
|
|
35
35
|
expect(d.title).toContain("Load 4.2 on 10 cores");
|
|
36
36
|
expect(d.opensHealth).toBe(true);
|
|
37
37
|
});
|
|
38
|
-
it("goes red when a health issue needs
|
|
38
|
+
it("goes red when a health issue needs the owner", () => {
|
|
39
39
|
const d = statusDot(load("green"), { open: 2, needsYou: 2 })!;
|
|
40
40
|
expect(d.level).toBe("red");
|
|
41
41
|
expect(d.label).toBe("Load normal · 2 health issues need you");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { useClientConfig } from "@/data/client-config/queries";
|
|
7
|
+
import { useSetOwnerName } from "@/data/owner/mutations";
|
|
8
|
+
|
|
9
|
+
// The name AgentOS and the orchestrators use when they refer to you.
|
|
10
|
+
export function OwnerNameRow() {
|
|
11
|
+
const saved = useClientConfig().data?.ownerName ?? "";
|
|
12
|
+
// null: show what is saved.
|
|
13
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
14
|
+
const value = draft ?? saved;
|
|
15
|
+
const save = useSetOwnerName();
|
|
16
|
+
return (
|
|
17
|
+
<form
|
|
18
|
+
className="bg-muted/40 flex flex-col gap-2 rounded-lg px-3 py-2.5 sm:flex-row sm:items-center"
|
|
19
|
+
onSubmit={(e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
save.mutate(value.trim() || null, { onSuccess: () => setDraft(null) });
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<div className="min-w-0 flex-1">
|
|
25
|
+
<label htmlFor="owner-name" className="text-sm font-medium">
|
|
26
|
+
Your name
|
|
27
|
+
</label>
|
|
28
|
+
<p className="text-muted-foreground text-xs">
|
|
29
|
+
Used where AgentOS and your orchestrators refer to you. Orchestrators
|
|
30
|
+
pick it up when they next start.
|
|
31
|
+
</p>
|
|
32
|
+
{save.error && (
|
|
33
|
+
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
<Input
|
|
37
|
+
id="owner-name"
|
|
38
|
+
value={value}
|
|
39
|
+
maxLength={40}
|
|
40
|
+
placeholder="Not set"
|
|
41
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
42
|
+
className="min-h-11 sm:min-h-8 sm:w-44"
|
|
43
|
+
/>
|
|
44
|
+
<Button
|
|
45
|
+
type="submit"
|
|
46
|
+
size="sm"
|
|
47
|
+
variant="outline"
|
|
48
|
+
className="min-h-11 sm:min-h-8"
|
|
49
|
+
disabled={save.isPending || value.trim() === saved}
|
|
50
|
+
>
|
|
51
|
+
Save
|
|
52
|
+
</Button>
|
|
53
|
+
</form>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
|
|
|
4
4
|
import { HealthList } from "@/components/Welcome/HealthList";
|
|
5
5
|
import { AgentList } from "@/components/Welcome/AgentList";
|
|
6
6
|
import { ReachRow } from "@/components/Reach/ReachRow";
|
|
7
|
+
import { OwnerNameRow } from "./OwnerNameRow";
|
|
7
8
|
import { settingsUiActions } from "@/stores/settingsUi";
|
|
8
9
|
import { welcomeUiActions } from "@/stores/welcomeUi";
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ import { welcomeUiActions } from "@/stores/welcomeUi";
|
|
|
11
12
|
export function SetupSection() {
|
|
12
13
|
return (
|
|
13
14
|
<div className="flex flex-col gap-4">
|
|
15
|
+
<OwnerNameRow />
|
|
14
16
|
<div className="bg-muted/40 flex items-center gap-3 rounded-lg px-3 py-2.5">
|
|
15
17
|
<div className="min-w-0 flex-1">
|
|
16
18
|
<p className="text-sm font-medium">First-run setup</p>
|
package/content/whats-new.json
CHANGED
|
@@ -1913,6 +1913,14 @@
|
|
|
1913
1913
|
"area": "AgentOS",
|
|
1914
1914
|
"text": "Files shared into the app are now checked against the same rules as chat attachments.",
|
|
1915
1915
|
"url": "https://github.com/saadnvd1/agent-os/pull/549"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
"id": 550,
|
|
1919
|
+
"date": "2026-11-01",
|
|
1920
|
+
"kind": "new",
|
|
1921
|
+
"area": "AgentOS",
|
|
1922
|
+
"text": "You can set your name in Settings, and AgentOS uses it in chats and notes instead of a fixed name.",
|
|
1923
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/550"
|
|
1916
1924
|
}
|
|
1917
1925
|
]
|
|
1918
1926
|
}
|
|
@@ -16,3 +16,6 @@ export const clientConfigQuery = queryOptions({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export const useClientConfig = () => useQuery(clientConfigQuery);
|
|
19
|
+
|
|
20
|
+
/** The owner's own name, or null until they set one (see lib/owner-label). */
|
|
21
|
+
export const useOwnerName = () => useClientConfig().data?.ownerName ?? null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { clientConfigKeys } from "@/data/client-config/queries";
|
|
3
|
+
|
|
4
|
+
export function useSetOwnerName() {
|
|
5
|
+
const qc = useQueryClient();
|
|
6
|
+
return useMutation({
|
|
7
|
+
mutationFn: async (name: string | null) => {
|
|
8
|
+
const res = await fetch("/api/owner", {
|
|
9
|
+
method: "PUT",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify({ name }),
|
|
12
|
+
});
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
if (!res.ok) throw new Error(data.error || "Couldn't save the name");
|
|
15
|
+
return data.name as string | null;
|
|
16
|
+
},
|
|
17
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: clientConfigKeys.all }),
|
|
18
|
+
});
|
|
19
|
+
}
|
package/lib/approvals/enable.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
recordAutoApproval,
|
|
12
12
|
setNoApprovals,
|
|
13
13
|
} from "./no-approvals";
|
|
14
|
+
import { ownerUi } from "../owner";
|
|
14
15
|
|
|
15
16
|
// Everything already waiting when the mode goes on is approved: open asks,
|
|
16
17
|
// tool cards in live chats.
|
|
@@ -42,7 +43,7 @@ export async function switchNoApprovals(on: boolean): Promise<void> {
|
|
|
42
43
|
o.workspace_id,
|
|
43
44
|
on
|
|
44
45
|
? "No approvals mode is on: every ask is approved as it's raised, and agents run without permission prompts."
|
|
45
|
-
:
|
|
46
|
+
: `No approvals mode is off: asks wait for ${ownerUi()} again.`,
|
|
46
47
|
"ask"
|
|
47
48
|
);
|
|
48
49
|
}
|
package/lib/artifacts/tools.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./store";
|
|
22
22
|
import { followLatest } from "../share/follow";
|
|
23
23
|
import { previewHtml } from "./preview";
|
|
24
|
+
import { owner } from "../owner";
|
|
24
25
|
|
|
25
26
|
export const VISUALS_SERVER = "visuals";
|
|
26
27
|
|
|
@@ -218,4 +219,4 @@ When a chart, table, diagram or mockup would say more than prose, build a self-c
|
|
|
218
219
|
// is told what to keep out of a page.
|
|
219
220
|
export const ORCHESTRATOR_VISUALS_BRIEF = `## Pages
|
|
220
221
|
|
|
221
|
-
\`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for
|
|
222
|
+
\`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for ${owner()} to read or share; check it with \`html_preview\` first. Put only what you wrote in it: no secrets, and nothing from a session's text loaded from a URL.`;
|
package/lib/away/brief.ts
CHANGED
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
} from "./state";
|
|
13
13
|
import { SWEEP_EVERY_MS } from "./sweep";
|
|
14
14
|
import { awayWindowMinutes } from "./activity";
|
|
15
|
+
import { owner, Owner } from "../owner";
|
|
15
16
|
|
|
16
17
|
export function awayOnLine(row: AwayRow, now = Date.now()): string {
|
|
17
|
-
return
|
|
18
|
+
return `${Owner()} is away (Away mode, since ${clockTime(fromSql(row.started_at), now)}, ${untilText(row, now)}). Follow work through rather than only reacting to events: keep each task moving to merged and deployed, sign off what passes its gates, and pick up what stalled. Every ${SWEEP_EVERY_MS / 60000} minutes AgentOS sweeps this workspace without messaging any session: it runs lost reviews again, checks the deploy after each merge, and sends you one digest of what's new (stalled tasks, idle ones with uncommitted work and no PR, ${owner()}'s own sessions idle with uncommitted work). You decide what needs work and act with your tools. Only sessions with real activity in the last ${awayWindowMinutes()} minutes are listed: leave the rest alone, since a message wakes an agent and re-reads its whole context. Interactive sessions (not tasks) are ${owner()}'s: ask a listed one at most once to wrap up, never drive them further. Hard lines still go to ask_owner; while he's away his asks reach his phone in one batch per sweep, so raise them as usual. \`aos notify\` is held for that batch too, so don't send one per item.`;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// For the digest: the same line whenever Away is on, messaged or not, so an
|
package/lib/away/run.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { checkDeploy } from "./deploy";
|
|
|
56
56
|
import { buildAwayReport, phoneSummary, reportMarkdown } from "./report";
|
|
57
57
|
import { awayOnLine } from "./brief";
|
|
58
58
|
import { shutFinishedTaskSimulators } from "../simulator";
|
|
59
|
+
import { owner, Owner, ownerUi } from "../owner";
|
|
59
60
|
|
|
60
61
|
const PHONE_SOURCE = "away";
|
|
61
62
|
const PHONE_LINES = 8;
|
|
@@ -116,7 +117,7 @@ function tellAway(
|
|
|
116
117
|
).run(away.id, workspaceId, orchestratorId, toldKey(workspaceId));
|
|
117
118
|
addNote(
|
|
118
119
|
workspaceId,
|
|
119
|
-
|
|
120
|
+
`${ownerUi(true)} turned Away on, ${untilText(away, now)}.`,
|
|
120
121
|
"away"
|
|
121
122
|
);
|
|
122
123
|
}
|
|
@@ -181,13 +182,13 @@ export function turnAwayOff(
|
|
|
181
182
|
}
|
|
182
183
|
const line =
|
|
183
184
|
by === "expired"
|
|
184
|
-
?
|
|
185
|
-
:
|
|
185
|
+
? `Away mode ran out: ${owner()} may be back. Go back to reacting to events; don't drive interactive sessions.`
|
|
186
|
+
: `${Owner()} is back: Away mode is off. Go back to reacting to events.`;
|
|
186
187
|
for (const workspaceId of reachedWorkspaces(ended.id)) {
|
|
187
188
|
queueEvent(workspaceId, `away-back:${ended.id}`, null, line, now);
|
|
188
189
|
addNote(
|
|
189
190
|
workspaceId,
|
|
190
|
-
by === "expired" ? "Away ran out." :
|
|
191
|
+
by === "expired" ? "Away ran out." : `${ownerUi(true)} turned Away off.`,
|
|
191
192
|
"away"
|
|
192
193
|
);
|
|
193
194
|
}
|
package/lib/away/sweep.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { isFinished } from "../tasks/state";
|
|
23
23
|
import type { SessionFacts } from "../orchestrator/facts";
|
|
24
|
+
import { owner } from "../owner";
|
|
24
25
|
|
|
25
26
|
export const SWEEP_EVERY_MS = 10 * 60 * 1000;
|
|
26
27
|
export const STALL_MS = 30 * 60 * 1000;
|
|
@@ -199,7 +200,7 @@ function wrapupCandidate(f: SessionFacts, v: SweepView): Candidate | null {
|
|
|
199
200
|
kind: "wrapup",
|
|
200
201
|
sessionId: f.id,
|
|
201
202
|
key,
|
|
202
|
-
line: `${f.name} (${f.id}):
|
|
203
|
+
line: `${f.name} (${f.id}): ${owner()}'s own session, idle ${minutes(v.now - turnOf(f, v))}m with uncommitted work.`,
|
|
203
204
|
};
|
|
204
205
|
}
|
|
205
206
|
|
package/lib/chat/runner.ts
CHANGED
|
@@ -86,6 +86,7 @@ import { DEMO_REFUSAL, demoMode } from "../security/demo";
|
|
|
86
86
|
import { chatHold, chatRefusal } from "./hold";
|
|
87
87
|
import { getHost } from "../hosts";
|
|
88
88
|
import { hostLink } from "../hosts/remote-api";
|
|
89
|
+
import { ownerUi } from "../owner";
|
|
89
90
|
|
|
90
91
|
export {
|
|
91
92
|
refreshCapabilities,
|
|
@@ -119,7 +120,7 @@ export async function carryOutPlan(
|
|
|
119
120
|
{
|
|
120
121
|
id: `user-carry-${planId}`,
|
|
121
122
|
text: CARRY_OUT,
|
|
122
|
-
origin: { kind: "decision", label:
|
|
123
|
+
origin: { kind: "decision", label: ownerUi(true) },
|
|
123
124
|
host: true,
|
|
124
125
|
},
|
|
125
126
|
timeoutMs
|
package/lib/chat/turns.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const isQuestionTool = (item: ChatItem): boolean =>
|
|
|
38
38
|
item.kind === "tool" && /(^|__)question$/.test(item.name);
|
|
39
39
|
|
|
40
40
|
export const isAskTool = (item: ChatItem): boolean =>
|
|
41
|
-
item.kind === "tool" && /(^|__)
|
|
41
|
+
item.kind === "tool" && /(^|__)ask_(owner|saad)$/.test(item.name);
|
|
42
42
|
|
|
43
43
|
// The assistant message a question was asked in (its turn's last words
|
|
44
44
|
// before the call), else the call itself.
|
|
@@ -30,6 +30,8 @@ export interface ClientConfig {
|
|
|
30
30
|
intake: IntakeLimits;
|
|
31
31
|
attachments: AttachmentConfig;
|
|
32
32
|
voice: VoiceLimits;
|
|
33
|
+
/** The owner's own name for text that refers to them; null until set. */
|
|
34
|
+
ownerName: string | null;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// What a chat accepts, served from lib/attachments/policy.ts. Sizes in bytes.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownerName } from "../owner";
|
|
1
2
|
import { ATTACHMENT_LIMITS } from "../attachments/policy";
|
|
2
3
|
import { ACCESS_LEVELS, DEFAULT_NEW_CHAT_ACCESS } from "../chat/access-levels";
|
|
3
4
|
import { CHAT_ACCESS } from "../chat/events";
|
|
@@ -42,5 +43,6 @@ export function clientConfig(): ClientConfig {
|
|
|
42
43
|
intake: INTAKE_LIMITS,
|
|
43
44
|
attachments: ATTACHMENT_LIMITS,
|
|
44
45
|
voice: VOICE_LIMITS,
|
|
46
|
+
ownerName: ownerName(),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type Pair,
|
|
25
25
|
type SessionInput,
|
|
26
26
|
} from "./types";
|
|
27
|
+
import { owner } from "../owner";
|
|
27
28
|
|
|
28
29
|
const ID = "learning-cards" as const;
|
|
29
30
|
const LESSON_MAX = 200;
|
|
@@ -227,7 +228,7 @@ export async function analyzeSession(
|
|
|
227
228
|
input.workspaceId,
|
|
228
229
|
`learning-card:${card.id}`,
|
|
229
230
|
sessionId,
|
|
230
|
-
`Learning card on "${input.name.replace(/\s+/g, " ")}", waiting on
|
|
231
|
+
`Learning card on "${input.name.replace(/\s+/g, " ")}", waiting on ${owner()}'s Needs-you strip: ${untrusted("learning card", cardTitle(card))}`
|
|
231
232
|
);
|
|
232
233
|
} finally {
|
|
233
234
|
inFlight.delete(sessionId);
|
package/lib/models/plan-step.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
splitOf,
|
|
36
36
|
} from "../plans/features";
|
|
37
37
|
import { advancePlan, setPlan } from "../db/session-parts";
|
|
38
|
+
import { Owner } from "../owner";
|
|
38
39
|
|
|
39
40
|
export const PLAN_FILE = ".agentos/plan.md";
|
|
40
41
|
|
|
@@ -533,7 +534,7 @@ export async function recheckPlanning(
|
|
|
533
534
|
export type ApprovedBy = "saad" | "orchestrator" | "no-approvals";
|
|
534
535
|
|
|
535
536
|
const APPROVAL_TEXT: Record<ApprovedBy, string> = {
|
|
536
|
-
saad:
|
|
537
|
+
saad: `${Owner()} approved the plan; implement it as planned.`,
|
|
537
538
|
orchestrator: "The orchestrator approved the plan; implement it as planned.",
|
|
538
539
|
"no-approvals":
|
|
539
540
|
"The plan was approved automatically (no-approvals mode); implement it as planned.",
|
|
@@ -11,6 +11,7 @@ import { addNote } from "./notes";
|
|
|
11
11
|
import { comparePatches } from "./patch";
|
|
12
12
|
import { fetchRefs } from "./repo";
|
|
13
13
|
import type { Session } from "../db";
|
|
14
|
+
import { Owner } from "../owner";
|
|
14
15
|
|
|
15
16
|
const short = (sha: string) => sha.slice(0, 7);
|
|
16
17
|
|
|
@@ -41,7 +42,7 @@ export async function carryApproval(
|
|
|
41
42
|
if (rebindApproval(approval.id, approval.sha, head, line))
|
|
42
43
|
addNote(
|
|
43
44
|
workspaceId,
|
|
44
|
-
|
|
45
|
+
`${Owner()}'s approval of ${work.name} at ${short(approval.sha)} carries to ${short(head)}: same patch, rebased onto ${base}.`
|
|
45
46
|
);
|
|
46
47
|
const now = getAsk(workspaceId, approval.id);
|
|
47
48
|
return { approval: now?.sha === head && !now.used_at ? now : null, note };
|
|
@@ -31,6 +31,7 @@ import { workspaceTask } from "./targets";
|
|
|
31
31
|
import { markStatusDirty } from "../status/dirty";
|
|
32
32
|
import { setPr } from "../db/session-parts";
|
|
33
33
|
import { noteAutoArmed } from "./fast-lane-state";
|
|
34
|
+
import { owner } from "../owner";
|
|
34
35
|
|
|
35
36
|
// Judged again at least this often, for what the fingerprint can't see.
|
|
36
37
|
export const ARMED_RECHECK_MS = 5 * 60 * 1000;
|
|
@@ -94,7 +95,7 @@ export async function arm(workspaceId: string, ref: string): Promise<string> {
|
|
|
94
95
|
const task = workspaceTask(workspaceId, ref);
|
|
95
96
|
armTask(task);
|
|
96
97
|
if (isFast(task)) return armedNow(workspaceId, task);
|
|
97
|
-
return `Armed ${task.name}: AgentOS merges its PR the moment every sign_off gate passes at its head, and tells you with one event. A gate failure also comes as one event; it stays armed for the fix, unless the failure goes to
|
|
98
|
+
return `Armed ${task.name}: AgentOS merges its PR the moment every sign_off gate passes at its head, and tells you with one event. A gate failure also comes as one event; it stays armed for the fix, unless the failure goes to ${owner()}. No need to call sign_off.`;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// Arms a task, or says why it can't be: only a running task, in a
|
|
@@ -108,11 +109,11 @@ export function armTask(task: Session): void {
|
|
|
108
109
|
const held = approved ? [] : holdingEscalations(task.id);
|
|
109
110
|
if (held.length)
|
|
110
111
|
throw new Error(
|
|
111
|
-
`${task.name} is with
|
|
112
|
+
`${task.name} is with ${owner()} (${held.map((h) => h.gate).join(", ")}); only he merges it`
|
|
112
113
|
);
|
|
113
114
|
if (w && !approved && openBySubject(w, workSubject(task.id)))
|
|
114
115
|
throw new Error(
|
|
115
|
-
`${task.name} has an open ask with
|
|
116
|
+
`${task.name} has an open ask with ${owner()}; it merges on his answer`
|
|
116
117
|
);
|
|
117
118
|
if (task.peer_mirror)
|
|
118
119
|
throw new Error(`${task.name} runs on a linked machine; arm it there`);
|
|
@@ -255,7 +256,7 @@ async function mergeOne(
|
|
|
255
256
|
event(
|
|
256
257
|
workspaceId,
|
|
257
258
|
task,
|
|
258
|
-
`task ${task.name}: disarmed, it's with
|
|
259
|
+
`task ${task.name}: disarmed, it's with ${owner()} (${held.join(", ")})`
|
|
259
260
|
);
|
|
260
261
|
return;
|
|
261
262
|
}
|
|
@@ -280,14 +281,14 @@ async function mergeOne(
|
|
|
280
281
|
if (!stillArmed(workspaceId, task.id)) return;
|
|
281
282
|
const counted = await deps.judge(workspaceId, task);
|
|
282
283
|
if (counted.ok) return merge(workspaceId, task, counted, deps);
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
284
|
+
const toOwner = holdingEscalations(task.id).length > 0;
|
|
285
|
+
if (toOwner) setArmed(task.id, false);
|
|
285
286
|
else setPr(db, task.id, { merge_armed_seen: seen });
|
|
286
287
|
event(
|
|
287
288
|
workspaceId,
|
|
288
289
|
task,
|
|
289
|
-
|
|
290
|
-
? `task ${task.name}: armed merge stopped and disarmed, it went to
|
|
290
|
+
toOwner
|
|
291
|
+
? `task ${task.name}: armed merge stopped and disarmed, it went to ${owner()}.\n${counted.text}`
|
|
291
292
|
: `task ${task.name}: armed, not merged yet; it stays armed and merges once this is fixed.\n${counted.text}`
|
|
292
293
|
);
|
|
293
294
|
}
|
|
@@ -15,6 +15,7 @@ import type { SessionFacts } from "./facts";
|
|
|
15
15
|
import { addNote } from "./notes";
|
|
16
16
|
import { comparePatches } from "./patch";
|
|
17
17
|
import { fetchRefs, repoOf } from "./repo";
|
|
18
|
+
import { Owner } from "../owner";
|
|
18
19
|
|
|
19
20
|
const short = (sha: string) => sha.slice(0, 7);
|
|
20
21
|
|
|
@@ -49,7 +50,7 @@ export async function followAskHeads(
|
|
|
49
50
|
if (moveAskHead(ask.id, ask.sha, pr.head, line))
|
|
50
51
|
addNote(
|
|
51
52
|
workspaceId,
|
|
52
|
-
|
|
53
|
+
`${Owner()}'s ask on ${task.name} now names ${short(pr.head)} (${same ? "same patch" : "patch changed"}).`,
|
|
53
54
|
"ask"
|
|
54
55
|
);
|
|
55
56
|
} catch (error) {
|