@saadnvd1/agent-os 0.3.503 → 0.3.506

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +67 -0
  2. package/app/api/projects/[id]/session-logs/route.ts +39 -0
  3. package/app/api/session-logs/[id]/route.ts +18 -0
  4. package/app/api/workspaces/[id]/session-logs/route.ts +35 -0
  5. package/components/Pane/DesktopTabBar.tsx +4 -1
  6. package/components/Pane/MobileTabBar.tsx +12 -1
  7. package/components/Pane/PaneViewToggle.tsx +14 -2
  8. package/components/Pane/index.tsx +28 -1
  9. package/components/PrStageBadge.tsx +15 -6
  10. package/components/Projects/ProjectSettingsDialog.tsx +11 -0
  11. package/components/SessionList/LiveLine.tsx +75 -0
  12. package/components/SessionList/RowParts.tsx +1 -7
  13. package/components/SessionList/RowShell.tsx +1 -1
  14. package/components/SessionList/RowSubtitle.tsx +4 -0
  15. package/components/SessionList/SessionRow.tsx +9 -3
  16. package/components/SessionLogs/LogDocument.tsx +29 -0
  17. package/components/SessionLogs/ProjectLogsSection.tsx +140 -0
  18. package/components/Workspaces/HqFolderForm.tsx +71 -0
  19. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  20. package/contexts/PaneContext.tsx +1 -1
  21. package/data/session-logs/index.ts +110 -0
  22. package/lib/db/migrations.ts +24 -0
  23. package/lib/done/index.ts +2 -0
  24. package/lib/orchestrator/brief.ts +1 -0
  25. package/lib/orchestrator/logs.ts +81 -0
  26. package/lib/orchestrator/serve.ts +5 -0
  27. package/lib/orchestrator/tool-names.ts +1 -0
  28. package/lib/orchestrator/tool-schemas.ts +13 -0
  29. package/lib/orchestrator/tools.ts +1 -0
  30. package/lib/session-logs/build.ts +253 -0
  31. package/lib/session-logs/folder.ts +142 -0
  32. package/lib/session-logs/gather.ts +301 -0
  33. package/lib/session-logs/hq.ts +201 -0
  34. package/lib/session-logs/index.ts +193 -0
  35. package/lib/session-logs/paths.ts +37 -0
  36. package/lib/session-logs/reexport.ts +90 -0
  37. package/lib/session-logs/store.ts +105 -0
  38. package/lib/sidebar/live-status.ts +52 -0
  39. package/lib/tasks/finish.ts +3 -0
  40. package/lib/tasks/pr-stage.ts +14 -0
  41. package/lib/tasks/session.ts +4 -0
  42. package/package.json +1 -1
  43. package/scripts/backfill-session-logs.ts +101 -0
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ScrollText } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { Input } from "@/components/ui/input";
14
+ import {
15
+ useProjectLogsQuery,
16
+ useSetLogFolder,
17
+ type LogHeader,
18
+ } from "@/data/session-logs";
19
+ import { LogDocument } from "./LogDocument";
20
+
21
+ const day = (h: LogHeader) => (h.mergedAt ?? h.createdAt).slice(0, 10);
22
+
23
+ function LogRow({ log, onOpen }: { log: LogHeader; onOpen: () => void }) {
24
+ return (
25
+ <li>
26
+ <button
27
+ type="button"
28
+ onClick={onOpen}
29
+ className="hover:bg-accent/50 flex min-h-11 w-full items-center gap-3 px-3 py-2 text-left transition-colors"
30
+ >
31
+ <span className="min-w-0 flex-1">
32
+ <span className="block truncate text-sm font-medium">{log.name}</span>
33
+ <span className="text-muted-foreground block truncate text-xs">
34
+ {day(log)}
35
+ {log.prNumber ? ` · #${log.prNumber}` : ""}
36
+ {` · $${log.costUsd.toFixed(2)}`}
37
+ </span>
38
+ </span>
39
+ <ScrollText className="text-muted-foreground h-4 w-4 shrink-0" />
40
+ </button>
41
+ </li>
42
+ );
43
+ }
44
+
45
+ // A project's logs of merged tasks, newest first, and the folder their
46
+ // files go to (the default is outside every repository).
47
+ export function ProjectLogsSection({
48
+ projectId,
49
+ open,
50
+ }: {
51
+ projectId: string;
52
+ open: boolean;
53
+ }) {
54
+ const { data, isPending, isError, error } = useProjectLogsQuery(
55
+ projectId,
56
+ open
57
+ );
58
+ const save = useSetLogFolder(projectId);
59
+ const [draft, setDraft] = useState<string | null>(null);
60
+ const [viewing, setViewing] = useState<LogHeader | null>(null);
61
+ const folder = draft ?? data?.folder ?? "";
62
+
63
+ return (
64
+ <div className="space-y-2">
65
+ <h3 className="text-sm font-medium">Session logs</h3>
66
+ <p className="text-muted-foreground text-xs">
67
+ When a task merges, AgentOS writes a log of it from what it already
68
+ recorded. No model is involved.
69
+ </p>
70
+ <div className="space-y-1.5">
71
+ <label htmlFor={`log-folder-${projectId}`} className="text-sm">
72
+ Session log folder
73
+ </label>
74
+ <div className="flex gap-2">
75
+ <Input
76
+ id={`log-folder-${projectId}`}
77
+ value={folder}
78
+ onChange={(e) => setDraft(e.target.value)}
79
+ placeholder={data ? `${data.defaultFolder}/…` : "~/path/to/logs"}
80
+ className="h-11 flex-1 md:h-9"
81
+ onKeyDown={(e) => {
82
+ if (e.key !== "Enter") return;
83
+ // Not the project form's submit.
84
+ e.preventDefault();
85
+ if (draft !== null)
86
+ save.mutate(draft, { onSuccess: () => setDraft(null) });
87
+ }}
88
+ />
89
+ <Button
90
+ type="button"
91
+ variant="outline"
92
+ className="h-11 md:h-9"
93
+ disabled={save.isPending || draft === null}
94
+ onClick={() =>
95
+ save.mutate(draft ?? "", { onSuccess: () => setDraft(null) })
96
+ }
97
+ >
98
+ Save
99
+ </Button>
100
+ </div>
101
+ {save.isError && (
102
+ <p className="text-destructive text-sm">{save.error.message}</p>
103
+ )}
104
+ <p className="text-muted-foreground text-xs">
105
+ An absolute or ~ path, never inside the project&apos;s repository.
106
+ Empty uses the default.
107
+ </p>
108
+ </div>
109
+ {isPending && (
110
+ <div className="bg-muted/40 h-16 animate-pulse rounded-lg" />
111
+ )}
112
+ {isError && <p className="text-destructive text-sm">{error.message}</p>}
113
+ {data &&
114
+ (data.logs.length ? (
115
+ <ul className="bg-muted/40 divide-border max-h-64 divide-y overflow-y-auto rounded-lg">
116
+ {data.logs.map((l) => (
117
+ <LogRow key={l.id} log={l} onOpen={() => setViewing(l)} />
118
+ ))}
119
+ </ul>
120
+ ) : (
121
+ <p className="text-muted-foreground text-sm">
122
+ No merged tasks logged yet.
123
+ </p>
124
+ ))}
125
+ <Dialog open={!!viewing} onOpenChange={(o) => !o && setViewing(null)}>
126
+ <DialogContent className="max-h-[90vh] max-w-3xl overflow-hidden [&>*]:min-w-0">
127
+ <DialogHeader>
128
+ <DialogTitle className="truncate">
129
+ {viewing?.name ?? "Log"}
130
+ </DialogTitle>
131
+ <DialogDescription>Session log</DialogDescription>
132
+ </DialogHeader>
133
+ <div className="h-[70vh]">
134
+ {viewing && <LogDocument sessionId={viewing.id} />}
135
+ </div>
136
+ </DialogContent>
137
+ </Dialog>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { useSetWorkspaceHq, useWorkspaceHqQuery } from "@/data/session-logs";
8
+
9
+ // The git repo this workspace's session logs are written into and
10
+ // committed to. Blank keeps them in AgentOS's own folder.
11
+ export function HqFolderForm({ workspaceId }: { workspaceId: string }) {
12
+ const { data, isPending, isError, error } = useWorkspaceHqQuery(workspaceId);
13
+ const save = useSetWorkspaceHq(workspaceId);
14
+ const [draft, setDraft] = useState<string | null>(null);
15
+ const saved = data?.hq?.folder ?? "";
16
+ const folder = draft ?? saved;
17
+ const id = `hq-folder-${workspaceId}`;
18
+
19
+ if (isPending)
20
+ return <div className="bg-muted/60 h-11 animate-pulse rounded-md" />;
21
+ if (isError)
22
+ return <p className="text-destructive text-xs">{error.message}</p>;
23
+
24
+ return (
25
+ <form
26
+ onSubmit={(e) => {
27
+ e.preventDefault();
28
+ save.mutate(folder, {
29
+ onSuccess: () => {
30
+ setDraft(null);
31
+ toast.success(
32
+ folder.trim() ? "HQ folder saved" : "HQ folder cleared"
33
+ );
34
+ },
35
+ onError: (err) => toast.error(err.message),
36
+ });
37
+ }}
38
+ className="space-y-1.5"
39
+ >
40
+ <label htmlFor={id} className="text-sm">
41
+ HQ folder
42
+ </label>
43
+ <div className="flex gap-2">
44
+ <Input
45
+ id={id}
46
+ value={folder}
47
+ onChange={(e) => setDraft(e.target.value)}
48
+ placeholder="~/path/to/a/private/git/repo"
49
+ className="h-11 flex-1 text-base md:h-9 md:text-sm"
50
+ />
51
+ <Button
52
+ type="submit"
53
+ size="sm"
54
+ className="h-11 sm:h-9"
55
+ disabled={folder === saved || save.isPending}
56
+ >
57
+ Save
58
+ </Button>
59
+ </div>
60
+ <p className="text-muted-foreground text-xs">
61
+ Session logs go to docs/sessions/&lt;project&gt;/ in this repo and are
62
+ committed there, one file per commit.{" "}
63
+ {data.hq?.visibility === "private"
64
+ ? "The repo is private, so each commit is pushed."
65
+ : data.hq
66
+ ? "Commits stay local: the repo isn't a private GitHub repository."
67
+ : "It must be a private git repo outside the workspace's projects."}
68
+ </p>
69
+ </form>
70
+ );
71
+ }
@@ -6,6 +6,7 @@ import { TaskLimitForm } from "./TaskLimitForm";
6
6
  import { RulesForm } from "./RulesForm";
7
7
  import { FreshStartForm } from "./FreshStartForm";
8
8
  import { McpServersForm } from "./McpServersForm";
9
+ import { HqFolderForm } from "./HqFolderForm";
9
10
  import { StallForm } from "./StallForm";
10
11
  import { WorkspaceModelsForm } from "./WorkspaceModelsForm";
11
12
  import { AwayWindowForm } from "@/components/Away/AwayWindowForm";
@@ -47,6 +48,7 @@ export function WorkspaceSettings() {
47
48
  <RulesForm workspaceId={w.id} />
48
49
  <WorkspaceModelsForm workspaceId={w.id} />
49
50
  <McpServersForm workspace={w} />
51
+ <HqFolderForm workspaceId={w.id} />
50
52
  <SettingToggle
51
53
  title="Wake tasks on CI failures and reviews"
52
54
  detail="A task's agent is messaged once when a check fails on its PR's head, with the log's tail, and when someone reviews or comments on it."
@@ -26,7 +26,7 @@ import {
26
26
  } from "@/lib/panes";
27
27
  import { useViewport } from "@/hooks/useViewport";
28
28
 
29
- export type ViewMode = "terminal" | "files" | "git" | "workers";
29
+ export type ViewMode = "terminal" | "files" | "git" | "workers" | "log";
30
30
 
31
31
  export interface PaneViewState {
32
32
  viewMode: ViewMode;
@@ -0,0 +1,110 @@
1
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import type { LogHeader } from "@/lib/session-logs/store";
3
+
4
+ export type { LogHeader };
5
+
6
+ export const sessionLogKeys = {
7
+ all: ["session-logs"] as const,
8
+ task: (id: string) => [...sessionLogKeys.all, "task", id] as const,
9
+ project: (id: string) => [...sessionLogKeys.all, "project", id] as const,
10
+ };
11
+
12
+ export interface TaskLog {
13
+ header: LogHeader;
14
+ markdown: string;
15
+ }
16
+
17
+ export interface ProjectLogs {
18
+ folder: string | null;
19
+ defaultFolder: string;
20
+ logs: LogHeader[];
21
+ }
22
+
23
+ // null: this task has no log (it didn't merge, or it predates logs).
24
+ export function useTaskLogQuery(sessionId: string | undefined, enabled = true) {
25
+ return useQuery({
26
+ queryKey: sessionLogKeys.task(sessionId ?? ""),
27
+ queryFn: async (): Promise<TaskLog | null> => {
28
+ const res = await fetch(
29
+ `/api/session-logs/${encodeURIComponent(sessionId!)}`
30
+ );
31
+ if (res.status === 404) return null;
32
+ const data = await res.json();
33
+ if (!res.ok) throw new Error(data.error || "Request failed");
34
+ return data;
35
+ },
36
+ enabled: enabled && !!sessionId,
37
+ // A written log doesn't change; a task that just merged may not have
38
+ // one for a moment.
39
+ staleTime: (query) => (query.state.data ? Infinity : 10_000),
40
+ });
41
+ }
42
+
43
+ async function projectLogs(res: Response): Promise<ProjectLogs> {
44
+ const data = await res.json();
45
+ if (!res.ok) throw new Error(data.error || "Request failed");
46
+ return data;
47
+ }
48
+
49
+ export function useProjectLogsQuery(projectId: string, enabled = true) {
50
+ return useQuery({
51
+ queryKey: sessionLogKeys.project(projectId),
52
+ queryFn: async () =>
53
+ projectLogs(await fetch(`/api/projects/${projectId}/session-logs`)),
54
+ enabled,
55
+ staleTime: 30_000,
56
+ });
57
+ }
58
+
59
+ export function useSetLogFolder(projectId: string) {
60
+ const queryClient = useQueryClient();
61
+ return useMutation({
62
+ mutationFn: async (folder: string) =>
63
+ projectLogs(
64
+ await fetch(`/api/projects/${projectId}/session-logs`, {
65
+ method: "PUT",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ folder }),
68
+ })
69
+ ),
70
+ onSuccess: (data) =>
71
+ queryClient.setQueryData(sessionLogKeys.project(projectId), data),
72
+ });
73
+ }
74
+
75
+ export interface WorkspaceHq {
76
+ hq: { folder: string; visibility: "private" | "internal" | "none" } | null;
77
+ }
78
+
79
+ const hqKey = (workspaceId: string) =>
80
+ [...sessionLogKeys.all, "hq", workspaceId] as const;
81
+
82
+ async function hqState(res: Response): Promise<WorkspaceHq> {
83
+ const data = await res.json();
84
+ if (!res.ok) throw new Error(data.error || "Request failed");
85
+ return data;
86
+ }
87
+
88
+ export function useWorkspaceHqQuery(workspaceId: string) {
89
+ return useQuery({
90
+ queryKey: hqKey(workspaceId),
91
+ queryFn: async () =>
92
+ hqState(await fetch(`/api/workspaces/${workspaceId}/session-logs`)),
93
+ staleTime: 30_000,
94
+ });
95
+ }
96
+
97
+ export function useSetWorkspaceHq(workspaceId: string) {
98
+ const queryClient = useQueryClient();
99
+ return useMutation({
100
+ mutationFn: async (folder: string) =>
101
+ hqState(
102
+ await fetch(`/api/workspaces/${workspaceId}/session-logs`, {
103
+ method: "PUT",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ folder }),
106
+ })
107
+ ),
108
+ onSuccess: (data) => queryClient.setQueryData(hqKey(workspaceId), data),
109
+ });
110
+ }
@@ -1593,6 +1593,30 @@ const migrations: Migration[] = [
1593
1593
  })();
1594
1594
  },
1595
1595
  },
1596
+ {
1597
+ id: 69,
1598
+ name: "session_logs",
1599
+ up: (db) => {
1600
+ // One log per merged task, written by AgentOS from data it already
1601
+ // holds (lib/session-logs). Never deleted: the database is the record,
1602
+ // the markdown file on disk is a copy.
1603
+ db.exec(`
1604
+ CREATE TABLE IF NOT EXISTS session_logs (
1605
+ session_id TEXT PRIMARY KEY,
1606
+ project_id TEXT,
1607
+ workspace_id TEXT,
1608
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1609
+ markdown TEXT NOT NULL,
1610
+ data TEXT NOT NULL,
1611
+ file_path TEXT
1612
+ );
1613
+ CREATE INDEX IF NOT EXISTS idx_session_logs_project
1614
+ ON session_logs(project_id, created_at);
1615
+ CREATE INDEX IF NOT EXISTS idx_session_logs_workspace
1616
+ ON session_logs(workspace_id, created_at);
1617
+ `);
1618
+ },
1619
+ },
1596
1620
  ];
1597
1621
 
1598
1622
  // `upTo`: stop after this id (tests that start from an older database).
package/lib/done/index.ts CHANGED
@@ -25,6 +25,7 @@ import { sessionMergePolicy } from "../tasks/finish";
25
25
  import { releasePorts } from "../ports";
26
26
  import { dropSessionDatabase } from "../project-config/database";
27
27
  import { doneOnPeer, peerSessionLink } from "../hosts/peer-actions";
28
+ import { captureMergedTask } from "../session-logs";
28
29
 
29
30
  export interface DoneOptions {
30
31
  // Who asked: the orchestrator's gate failures count toward Saad, as its
@@ -102,6 +103,7 @@ function mark(s: Session, status: "merged" | "done", pr: TaskPR | null) {
102
103
  : `UPDATE sessions SET task_status = 'done' WHERE id = ?`
103
104
  ).run(s.id);
104
105
  syncTaskCardInBackground(s, status, pr);
106
+ if (status === "merged") void captureMergedTask(s.id, { head: pr?.head });
105
107
  }
106
108
 
107
109
  export async function doneSession(
@@ -95,6 +95,7 @@ Reading:
95
95
  - \`${TOOL_NAMES.sessions}\`: every session in the workspace with its project, view, status, what it's doing, task/PR/CI state and stack position. After them, external sessions: tmux sessions AgentOS didn't start (dispatch, a person's own terminal), on this machine or another, working in one of the workspace's repositories or a worktree of it, each with its branch's open PR. They're read-only: \`read\` works on them; \`send\`, \`stop\` and \`done\` don't. Gate their PRs with \`review\` and \`sign_off\` by number.
96
96
  - \`${TOOL_NAMES.read}\` (session, lines?): the end of a session's terminal or chat, or an external session's screen.
97
97
  - \`${TOOL_NAMES.cards}\` (board?): the cards on the workspace's LumifyHub boards.
98
+ - \`${TOOL_NAMES.logs}\` (project?, task?, n?): the logs AgentOS wrote when tasks merged. Without task, the latest headers; with it, that task's whole log (brief, plan, timeline, usage, outcome). Check it before repeating work or re-planning a job a merged task already did.
98
99
  - \`${TOOL_NAMES.stack_status}\` (id): one stack's items, PRs and progress.
99
100
  - \`${TOOL_NAMES.orchestrators}\`: every workspace, and whether it has an orchestrator.
100
101
 
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The orchestrator's `logs` tool: session logs read from the database as
3
+ * they were written. Reading only; nothing here asks a model to summarise.
4
+ */
5
+
6
+ import { formatDuration } from "../session-logs/build";
7
+ import { getLog, listLogs, type LogHeader } from "../session-logs";
8
+ import { db } from "../db";
9
+ import { workspaceProject } from "./targets";
10
+ import { untrusted } from "./untrusted";
11
+
12
+ // About 8k tokens: what one full log may return.
13
+ export const LOG_CHAR_CAP = 30000;
14
+
15
+ const headerLine = (h: LogHeader) =>
16
+ [
17
+ (h.mergedAt ?? h.createdAt).slice(0, 10),
18
+ `${h.name} [${h.id.slice(0, 8)}]`,
19
+ h.project ?? "no project",
20
+ h.prNumber ? `#${h.prNumber}` : "no PR",
21
+ h.durationMs !== null ? formatDuration(h.durationMs) : "?",
22
+ `$${h.costUsd.toFixed(2)}`,
23
+ h.hasPlan ? "plan" : h.planKept ? "no plan" : "plan not kept",
24
+ ].join(" · ");
25
+
26
+ function findOne(
27
+ workspaceId: string,
28
+ projectId: string | null,
29
+ ref: string
30
+ ): string {
31
+ const r = ref.trim().toLowerCase();
32
+ const pr = /^#?(\d+)$/.exec(r);
33
+ const rows = db
34
+ .prepare(
35
+ `SELECT session_id AS id, json_extract(data, '$.name') AS name,
36
+ json_extract(data, '$.prNumber') AS pr
37
+ FROM session_logs WHERE workspace_id = ? AND (? IS NULL OR project_id = ?)
38
+ ORDER BY json_extract(data, '$.mergedAt') DESC`
39
+ )
40
+ .all(workspaceId, projectId, projectId) as {
41
+ id: string;
42
+ name: string;
43
+ pr: number | null;
44
+ }[];
45
+ const hit = rows.filter(
46
+ (x) =>
47
+ x.id === ref ||
48
+ x.id.startsWith(r) ||
49
+ x.name.toLowerCase() === r ||
50
+ (pr !== null && x.pr === Number(pr[1]))
51
+ );
52
+ if (hit.length === 1) return hit[0].id;
53
+ if (hit.length > 1)
54
+ throw new Error(
55
+ `"${ref}" matches ${hit.length} logs (${hit.map((x) => x.id.slice(0, 8)).join(", ")}); use an id`
56
+ );
57
+ throw new Error(`No log for "${ref}" in this workspace.`);
58
+ }
59
+
60
+ export function readLogs(
61
+ workspaceId: string,
62
+ a: { project?: string; task?: string; n?: number }
63
+ ): string {
64
+ const projectId = a.project
65
+ ? workspaceProject(workspaceId, a.project).id
66
+ : null;
67
+ if (a.task) {
68
+ const row = getLog(findOne(workspaceId, projectId, a.task))!;
69
+ const md =
70
+ row.markdown.length > LOG_CHAR_CAP
71
+ ? `${row.markdown.slice(0, LOG_CHAR_CAP)}\n… (trimmed; ${row.markdown.length} characters in all)`
72
+ : row.markdown;
73
+ return untrusted("session log", md);
74
+ }
75
+ const logs = listLogs(
76
+ { workspaceId, ...(projectId ? { projectId } : {}) },
77
+ a.n ?? 10
78
+ );
79
+ if (!logs.length) return "No session logs yet.";
80
+ return untrusted("session logs", logs.map(headerLine).join("\n"));
81
+ }
@@ -12,6 +12,7 @@ import {
12
12
  stop,
13
13
  } from "./act";
14
14
  import { readCards } from "./cards";
15
+ import { readLogs } from "./logs";
15
16
  import { describeSessions } from "./describe";
16
17
  import { sessionFacts } from "./facts";
17
18
  import { describeExternal, externalSessions } from "./external-sessions";
@@ -85,6 +86,10 @@ export async function runTool(
85
86
  }
86
87
  case "cards":
87
88
  return readCards(w, p(tool, raw).board);
89
+ case "logs": {
90
+ const a = p(tool, raw);
91
+ return readLogs(w, a);
92
+ }
88
93
  case "send": {
89
94
  const a = p(tool, raw);
90
95
  return send(w, a.session, a.message, {
@@ -10,6 +10,7 @@ export const TOOL_NAMES = {
10
10
  sessions: name("sessions"),
11
11
  read: name("read"),
12
12
  cards: name("cards"),
13
+ logs: name("logs"),
13
14
  send: name("send"),
14
15
  orchestrators: name("orchestrators"),
15
16
  message_orchestrator: name("message_orchestrator"),
@@ -57,6 +57,19 @@ export const TOOL_SHAPES = {
57
57
  cards: {
58
58
  board: ref("One board, by board or project name").optional(),
59
59
  },
60
+ logs: {
61
+ project: project.optional(),
62
+ task: ref(
63
+ "One merged task's log, by task name, id (or its first characters) or PR (#12); without it, the latest headers"
64
+ ).optional(),
65
+ n: z
66
+ .number()
67
+ .int()
68
+ .min(1)
69
+ .max(50)
70
+ .optional()
71
+ .describe("How many headers, newest first (default 10)"),
72
+ },
60
73
  send: {
61
74
  session,
62
75
  message: z.string().trim().min(1).max(8000),
@@ -55,6 +55,7 @@ const DESCRIPTIONS: Record<ToolName, string> = {
55
55
  "Every session in this workspace: project, view, status, what it's doing, task/PR/CI state and stack position; then external sessions (tmux sessions AgentOS didn't start, here or on other machines, working in a workspace repository), read-only, with their branch's PR.",
56
56
  read: "The end of one session's terminal or chat, or an external session's screen, as plain text (capped at about 4k tokens).",
57
57
  cards: "Cards on this workspace's linked LumifyHub boards, by list.",
58
+ logs: "Session logs AgentOS wrote when tasks merged: the last N headers (task, PR, cost, duration) of the workspace or one project, or one log in full (brief, plan, timeline, usage, outcome). Read straight from the database; nothing is summarised.",
58
59
  send: "Message a session in this workspace over the bus. It arrives as its next prompt. When Saad changes a running task's scope, tell it with scope_change set, so its review judges the PR against the brief as amended.",
59
60
  orchestrators:
60
61
  "Every workspace, and whether it has an orchestrator you can message.",