@saadnvd1/agent-os 0.3.723 → 0.3.731

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 (150) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/ports/route.test.ts +43 -0
  3. package/app/api/bus/ports/route.ts +35 -0
  4. package/app/api/loops/route.ts +17 -0
  5. package/app/api/sessions/[id]/export/turn/route.test.ts +65 -0
  6. package/app/api/sessions/[id]/export/turn/route.ts +33 -0
  7. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  8. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  9. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  10. package/app/api/tasks/[id]/loop/route.ts +72 -0
  11. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  12. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  13. package/app/api/tasks/[id]/mode/route.ts +29 -0
  14. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  15. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  16. package/app/api/tasks/[id]/report/route.ts +39 -0
  17. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  18. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  19. package/app/page.tsx +8 -0
  20. package/components/Analyst/PairChip.tsx +131 -0
  21. package/components/Analyst/PairSwitch.tsx +76 -0
  22. package/components/Analyst/ReviewCard.tsx +272 -0
  23. package/components/Analyst/SignalChips.tsx +39 -0
  24. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  25. package/components/Analyst/cardState.test.ts +109 -0
  26. package/components/Analyst/cardState.ts +95 -0
  27. package/components/Analyst/pair.ts +81 -0
  28. package/components/Chat/Code.tsx +3 -0
  29. package/components/Chat/Messages.tsx +8 -1
  30. package/components/Chat/TurnCopy.tsx +84 -0
  31. package/components/Chat/itemContext.ts +14 -0
  32. package/components/Draft/DraftChips.tsx +10 -0
  33. package/components/Draft/DraftPanel.tsx +7 -0
  34. package/components/Pane/DesktopTabBar.tsx +6 -0
  35. package/components/Pane/MobileTabBar.tsx +17 -0
  36. package/components/Pane/MobileViewPicker.tsx +4 -0
  37. package/components/Pane/PaneMenu.test.ts +21 -0
  38. package/components/Pane/PaneMenu.tsx +16 -0
  39. package/components/Pane/PaneViewToggle.tsx +12 -0
  40. package/components/Pane/index.tsx +19 -0
  41. package/components/Report/DraftCard.tsx +89 -0
  42. package/components/Report/EstimateBar.tsx +51 -0
  43. package/components/Report/EvidenceSheet.tsx +98 -0
  44. package/components/Report/ModeChooser.tsx +43 -0
  45. package/components/Report/ReportView.tsx +289 -0
  46. package/components/SessionList/GroupShelves.tsx +57 -22
  47. package/components/SessionList/MoveToGroupMenu.test.ts +77 -0
  48. package/components/SessionList/SessionRow.tsx +12 -1
  49. package/components/SessionList/SessionRowMenu.tsx +6 -0
  50. package/components/SessionList/SidebarContextMenu.test.ts +21 -1
  51. package/components/SessionList/SidebarContextMenu.tsx +33 -9
  52. package/components/SessionList/index.tsx +11 -2
  53. package/components/SessionList/useGroupDrag.ts +83 -0
  54. package/components/Sessions/RolePill.tsx +7 -0
  55. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  56. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  57. package/components/Tasks/Loop/LoopView.tsx +350 -0
  58. package/components/Tasks/Loop/labels.ts +17 -0
  59. package/components/Tasks/TaskRow.tsx +2 -0
  60. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  61. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  62. package/components/ui/segmented.tsx +55 -0
  63. package/components/ui/status-pill.tsx +3 -0
  64. package/contexts/PaneContext.tsx +21 -1
  65. package/data/exports/queries.ts +11 -0
  66. package/data/loops/index.ts +3 -0
  67. package/data/loops/keys.ts +4 -0
  68. package/data/loops/mutations.ts +68 -0
  69. package/data/loops/queries.ts +40 -0
  70. package/data/push/topics.ts +6 -0
  71. package/data/reports/index.ts +115 -0
  72. package/lib/agents/brief.ts +1 -1
  73. package/lib/analyst/forward.ts +1 -1
  74. package/lib/db/changes.ts +7 -0
  75. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  76. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  77. package/lib/done/index.ts +2 -0
  78. package/lib/drafts.ts +2 -0
  79. package/lib/export/markdown.ts +7 -1
  80. package/lib/export/turn.ts +48 -0
  81. package/lib/health/config.ts +2 -0
  82. package/lib/health/env/classes.ts +14 -1
  83. package/lib/health/port-wait-tick.ts +222 -0
  84. package/lib/health/port-wait.ts +160 -0
  85. package/lib/health/tick.ts +9 -2
  86. package/lib/health/types.ts +2 -0
  87. package/lib/loops/analyse.ts +46 -0
  88. package/lib/loops/analyst-prompt.ts +203 -0
  89. package/lib/loops/checkCriteria.ts +114 -0
  90. package/lib/loops/compactEvidence.ts +140 -0
  91. package/lib/loops/controls.ts +317 -0
  92. package/lib/loops/decideStop.ts +113 -0
  93. package/lib/loops/deps.ts +128 -0
  94. package/lib/loops/diffMap.ts +39 -0
  95. package/lib/loops/draftCriteria.ts +71 -0
  96. package/lib/loops/gatherEvidence.ts +265 -0
  97. package/lib/loops/hooks.ts +40 -0
  98. package/lib/loops/index.ts +7 -0
  99. package/lib/loops/route.ts +83 -0
  100. package/lib/loops/runCycle.ts +323 -0
  101. package/lib/loops/screenInstruction.ts +59 -0
  102. package/lib/loops/sendInstruction.ts +83 -0
  103. package/lib/loops/snapshot.ts +38 -0
  104. package/lib/loops/stopLoop.ts +86 -0
  105. package/lib/loops/store.ts +481 -0
  106. package/lib/loops/summary.ts +27 -0
  107. package/lib/loops/tickLoops.ts +141 -0
  108. package/lib/loops/types.ts +223 -0
  109. package/lib/loops/world.test-helpers.ts +223 -0
  110. package/lib/models/plan-step.ts +4 -1
  111. package/lib/orchestrator/digest.ts +2 -1
  112. package/lib/orchestrator/loop-guard.ts +5 -2
  113. package/lib/orchestrator/pr-wake.ts +4 -0
  114. package/lib/orchestrator/watcher.ts +8 -1
  115. package/lib/panes.ts +52 -0
  116. package/lib/plans/acceptance.ts +23 -0
  117. package/lib/{ports.ts → ports/allocatePorts.ts} +30 -7
  118. package/lib/ports/ensurePorts.ts +138 -0
  119. package/lib/ports/findHardcodedPorts.ts +116 -0
  120. package/lib/ports/index.ts +15 -0
  121. package/lib/ports/listListeners.ts +134 -0
  122. package/lib/ports/reassignPort.ts +45 -0
  123. package/lib/ports/types.ts +62 -0
  124. package/lib/ports/whoHolds.ts +155 -0
  125. package/lib/project-config/brief.ts +4 -2
  126. package/lib/project-config/session.ts +19 -3
  127. package/lib/reports/drafts.ts +87 -0
  128. package/lib/reports/end-hook.ts +17 -0
  129. package/lib/reports/estimate.ts +107 -0
  130. package/lib/reports/evidence/build.ts +193 -0
  131. package/lib/reports/evidence/gather.ts +229 -0
  132. package/lib/reports/fixtures.ts +17 -0
  133. package/lib/reports/generate.ts +159 -0
  134. package/lib/reports/hooks.ts +30 -0
  135. package/lib/reports/mode.ts +65 -0
  136. package/lib/reports/model.ts +115 -0
  137. package/lib/reports/read.ts +26 -0
  138. package/lib/reports/store.ts +180 -0
  139. package/lib/reports/template.ts +117 -0
  140. package/lib/reports/types.ts +135 -0
  141. package/lib/reports/validate.ts +142 -0
  142. package/lib/sidebar/groupDrop.ts +36 -0
  143. package/lib/sidebar/row-menu.ts +9 -0
  144. package/lib/tasks/contract/routes.ts +5 -0
  145. package/lib/tasks/contract/types.ts +3 -0
  146. package/lib/tasks/finish.ts +2 -0
  147. package/lib/tasks/move-carry.ts +1 -0
  148. package/lib/windows-ports.ts +36 -18
  149. package/package.json +2 -2
  150. package/stores/analystUi.ts +19 -0
package/README.md CHANGED
@@ -19,6 +19,7 @@ and check on all of it from your phone.
19
19
  - **Other machines in the same list.** Link another machine that runs AgentOS and start or move work there. [More](docs/reference/machines.md)
20
20
  - **Built for your phone.** The whole app works on a phone; pair it with a QR code. [More](docs/guides/phone.md)
21
21
  - **Phone to orchestrator.** A paired phone can send a shake report or a shared page or file to a workspace's orchestrator (`POST /api/orchestrators/:workspaceId/intake`, saved under `intake/` beside the database, shared content marked untrusted), and fetch names to improve dictation (`GET /api/voice/vocab?workspace=`). [More](docs/guides/phone.md)
22
+ - **An analyst beside your coding agent.** Pair a chat with a second one that reviews each turn, explains it in plain English, flags issues and drafts the next prompt, which you send, edit or dismiss. Side by side on a desktop, a switch on a phone. [More](docs/decisions/2026-10-21-analyst-sessions.md)
22
23
  - **Agents that talk to each other.** Sessions message each other with `aos`, and you see every conversation in Messages. [More](docs/guides/agent-network.md)
23
24
  - **Chat or terminal, same conversation.** Streaming replies, steps and diffs, plan mode, slash commands, `@file`, undo. [More](docs/guides/chat.md)
24
25
  - **Bookmarks.** Star any message, tool call or turn, then find it again from the chat or across every chat, with a search. [More](docs/guides/chat.md)
@@ -0,0 +1,43 @@
1
+ import { NextRequest } from "next/server";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ const ensure = vi.hoisted(() => vi.fn());
5
+ vi.mock("@/lib/ports/ensurePorts", () => ({ ensurePorts: ensure }));
6
+
7
+ const { POST } = await import("./route");
8
+
9
+ const call = (body: unknown) =>
10
+ POST(
11
+ new NextRequest("http://localhost/api/bus/ports", {
12
+ method: "POST",
13
+ body: JSON.stringify(body),
14
+ })
15
+ );
16
+
17
+ describe("POST /api/bus/ports (aos port)", () => {
18
+ it("refuses a body without a session or with a target that's neither a name nor a port", async () => {
19
+ expect((await call({})).status).toBe(400);
20
+ expect((await call({ from: "s", target: "3000; rm -rf /" })).status).toBe(
21
+ 400
22
+ );
23
+ expect(ensure).not.toHaveBeenCalled();
24
+ });
25
+
26
+ it("hands the session and target to ensurePorts and returns its report", async () => {
27
+ ensure.mockResolvedValueOnce({
28
+ text: "PORT=3101 is free.",
29
+ blocked: false,
30
+ });
31
+ const res = await call({ from: "s1", target: "PORT" });
32
+ expect(res.status).toBe(200);
33
+ expect(await res.json()).toMatchObject({ text: "PORT=3101 is free." });
34
+ expect(ensure).toHaveBeenCalledWith("s1", "PORT");
35
+ });
36
+
37
+ it("is 404 for an unknown session and 409 for anything else it refuses", async () => {
38
+ ensure.mockRejectedValueOnce(new Error("No session nope"));
39
+ expect((await call({ from: "nope" })).status).toBe(404);
40
+ ensure.mockRejectedValueOnce(new Error("No port named X"));
41
+ expect((await call({ from: "s1", target: "X" })).status).toBe(409);
42
+ });
43
+ });
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { ensurePorts } from "@/lib/ports/ensurePorts";
4
+
5
+ const body = z.object({
6
+ from: z.string().min(1),
7
+ target: z
8
+ .string()
9
+ .regex(/^(\d{1,5}|[A-Za-z_][A-Za-z0-9_]*)$/)
10
+ .optional(),
11
+ });
12
+
13
+ // POST /api/bus/ports { from, target? } - `aos port`: whether the caller's
14
+ // ports are free on this machine, who holds the ones that aren't, and a free
15
+ // one in place of each taken one.
16
+ export async function POST(request: NextRequest) {
17
+ const parsed = body.safeParse(await request.json().catch(() => null));
18
+ if (!parsed.success)
19
+ return NextResponse.json(
20
+ {
21
+ error: "Send { from: your session id, target?: a port name or number }",
22
+ },
23
+ { status: 400 }
24
+ );
25
+ const { from, target } = parsed.data;
26
+ try {
27
+ return NextResponse.json(await ensurePorts(from, target));
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ return NextResponse.json(
31
+ { error: message },
32
+ { status: /^No session /.test(message) ? 404 : 409 }
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { loopSummary } from "@/lib/loops";
4
+
5
+ // GET /api/loops - the pill for every task that has had a loop in the last
6
+ // day, by task id.
7
+ export async function GET() {
8
+ const since = Date.now() - 24 * 60 * 60 * 1000;
9
+ const ids = db
10
+ .prepare(
11
+ `SELECT DISTINCT session_id FROM task_loops WHERE state = 'running' OR COALESCE(ended_at, 0) >= ?`
12
+ )
13
+ .all(since) as { session_id: string }[];
14
+ const loops: Record<string, ReturnType<typeof loopSummary>> = {};
15
+ for (const { session_id } of ids) loops[session_id] = loopSummary(session_id);
16
+ return NextResponse.json({ loops });
17
+ }
@@ -0,0 +1,65 @@
1
+ import { randomUUID } from "crypto";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const route = await import("./route");
5
+ const { saveItem } = await import("@/lib/chat/store");
6
+ const { seedSession } = await import("@/lib/orchestrator/testing");
7
+ const { createProject } = await import("@/lib/projects");
8
+
9
+ const secret = ["gh", "p_", "d".repeat(36)].join("");
10
+ const post = (b: unknown) =>
11
+ new Request("http://localhost/x", {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ body: JSON.stringify(b),
15
+ });
16
+ const params = (id: string) => ({ params: Promise.resolve({ id }) });
17
+
18
+ function session() {
19
+ const project = createProject({
20
+ name: `p-${randomUUID().slice(0, 6)}`,
21
+ workingDirectory: "/tmp/p",
22
+ });
23
+ const id = seedSession({
24
+ projectId: project.id,
25
+ name: "Copy me",
26
+ view: "chat",
27
+ });
28
+ let t = 1;
29
+ const add = (i: Record<string, unknown>) =>
30
+ saveItem(id, { id: `c${t}`, createdAt: t++, ...i } as never);
31
+ add({ kind: "user", text: "first" });
32
+ add({ kind: "assistant", text: "one" });
33
+ add({ kind: "user", text: "second" });
34
+ add({ kind: "assistant", text: `two ${secret}` });
35
+ return id;
36
+ }
37
+
38
+ afterEach(() => vi.unstubAllEnvs());
39
+
40
+ describe("POST /api/sessions/:id/export/turn", () => {
41
+ it("returns the turn and its response, secrets masked", async () => {
42
+ const id = session();
43
+ const res = await route.POST(post({ startId: "c3" }), params(id));
44
+ expect(res.status).toBe(200);
45
+ const j = await res.json();
46
+ expect(j.response).toContain("two [secret]");
47
+ expect(j.full).toContain("two [secret]");
48
+ expect(j.full).not.toContain("one");
49
+ expect(JSON.stringify(j)).not.toContain(secret);
50
+ });
51
+
52
+ it("404s an unknown turn and 400s a missing id", async () => {
53
+ const id = session();
54
+ expect((await route.POST(post({ startId: "zz" }), params(id))).status).toBe(
55
+ 404
56
+ );
57
+ expect((await route.POST(post({}), params(id))).status).toBe(400);
58
+ });
59
+
60
+ it("refuses in the demo", async () => {
61
+ vi.stubEnv("AGENTOS_DEMO", "1");
62
+ const res = await route.POST(post({ startId: "c1" }), params(session()));
63
+ expect(res.status).toBe(403);
64
+ });
65
+ });
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { loadSession } from "@/lib/export/load";
4
+ import { copyTurn } from "@/lib/export/turn";
5
+ import { shareContext } from "@/lib/share/context";
6
+ import { readJson, refuseDemo, toResponse } from "@/lib/share/route";
7
+ import { BadShare } from "@/lib/share/service";
8
+
9
+ const Body = z.object({ startId: z.string().min(1).max(200) });
10
+
11
+ // POST /api/sessions/:id/export/turn - one turn as Markdown for the clipboard,
12
+ // secrets masked: the whole turn, and its last agent message.
13
+ export async function POST(
14
+ request: Request,
15
+ { params }: { params: Promise<{ id: string }> }
16
+ ) {
17
+ const refused = refuseDemo();
18
+ if (refused) return refused;
19
+ const read = await readJson(request);
20
+ if ("refused" in read) return read.refused;
21
+ try {
22
+ const { id } = await params;
23
+ const body = Body.safeParse(read.body);
24
+ if (!body.success) throw new BadShare("startId is required", 400);
25
+ const loaded = loadSession(id);
26
+ return NextResponse.json(
27
+ copyTurn(loaded.items, body.data.startId, shareContext(), loaded.meta),
28
+ { headers: { "Cache-Control": "no-store" } }
29
+ );
30
+ } catch (err) {
31
+ return toResponse(err);
32
+ }
33
+ }
@@ -0,0 +1,29 @@
1
+ import { readFile } from "fs/promises";
2
+ import { NextRequest } from "next/server";
3
+ import { loopLogPath } from "@/lib/loops/gatherEvidence";
4
+ import { latestLoopFor } from "@/lib/loops/store";
5
+ import { loopRoute } from "@/lib/loops/route";
6
+ import { LoopError } from "@/lib/loops/types";
7
+ import { redact } from "@/lib/models/secrets";
8
+
9
+ const MAX = 64 * 1024;
10
+
11
+ // GET /api/tasks/:id/loop/cycles/:n/log - the tail of that cycle's checks
12
+ // output, redacted and capped.
13
+ export async function GET(
14
+ request: NextRequest,
15
+ ctx: { params: Promise<{ id: string; n: string }> }
16
+ ) {
17
+ const n = Number((await ctx.params).n);
18
+ return loopRoute(request, ctx, {}, async (task) => {
19
+ if (!latestLoopFor(task.id) || !Number.isInteger(n) || n < 1)
20
+ throw new LoopError("No such cycle", 404);
21
+ let text = "";
22
+ try {
23
+ text = await readFile(loopLogPath(task.id, n), "utf8");
24
+ } catch {
25
+ throw new LoopError("No log for that cycle", 404);
26
+ }
27
+ return { log: redact(text.length > MAX ? text.slice(-MAX) : text) };
28
+ });
29
+ }
@@ -0,0 +1,17 @@
1
+ import { NextRequest } from "next/server";
2
+ import { draftCriteria } from "@/lib/loops";
3
+ import { loopRoute } from "@/lib/loops/route";
4
+ import { LoopError } from "@/lib/loops/types";
5
+
6
+ // POST /api/tasks/:id/loop/draft-criteria - a drafted checklist from the
7
+ // card or the task's text; nothing is saved.
8
+ export async function POST(
9
+ request: NextRequest,
10
+ ctx: { params: Promise<{ id: string }> }
11
+ ) {
12
+ return loopRoute(request, ctx, {}, async (task) => {
13
+ if (!task.worktree_path)
14
+ throw new LoopError("The task has no worktree", 409);
15
+ return { criteria: await draftCriteria(task, task.worktree_path) };
16
+ });
17
+ }
@@ -0,0 +1,25 @@
1
+ import { NextRequest } from "next/server";
2
+ import { z } from "zod";
3
+ import { actOnNext } from "@/lib/loops";
4
+ import { loopRoute } from "@/lib/loops/route";
5
+
6
+ const schema = z.object({
7
+ action: z.enum(["send", "hold", "skip"]),
8
+ instruction: z.string().max(8000).optional(),
9
+ });
10
+
11
+ // POST /api/tasks/:id/loop/next - what to do with the instruction in its
12
+ // countdown: send it (edited or not), hold the countdown, or skip the cycle.
13
+ export async function POST(
14
+ request: NextRequest,
15
+ ctx: { params: Promise<{ id: string }> }
16
+ ) {
17
+ return loopRoute(request, ctx, { schema }, async (task, body) => ({
18
+ view: await actOnNext(
19
+ task.id,
20
+ body.action === "send"
21
+ ? { action: "send", instruction: body.instruction }
22
+ : { action: body.action }
23
+ ),
24
+ }));
25
+ }
@@ -0,0 +1,72 @@
1
+ import { NextRequest } from "next/server";
2
+ import { z } from "zod";
3
+ import { editLoop, loopView, startLoop } from "@/lib/loops";
4
+ import { criterionSchema, limitsSchema, loopRoute } from "@/lib/loops/route";
5
+
6
+ type Ctx = { params: Promise<{ id: string }> };
7
+
8
+ // GET /api/tasks/:id/loop - the loop, its checklist and its newest cycles;
9
+ // ?before=<n> pages back.
10
+ export async function GET(request: NextRequest, ctx: Ctx) {
11
+ const before = Number(request.nextUrl.searchParams.get("before"));
12
+ return loopRoute(request, ctx, {}, (task) => ({
13
+ view: loopView(task.id, undefined, {
14
+ before: Number.isFinite(before) && before > 0 ? before : undefined,
15
+ }),
16
+ }));
17
+ }
18
+
19
+ const startSchema = z.object({
20
+ criteria: z.array(criterionSchema).max(60),
21
+ limits: limitsSchema.optional(),
22
+ });
23
+
24
+ // POST /api/tasks/:id/loop - start a loop with its criteria and limits.
25
+ export async function POST(request: NextRequest, ctx: Ctx) {
26
+ return loopRoute(
27
+ request,
28
+ ctx,
29
+ { schema: startSchema },
30
+ async (task, body) => ({
31
+ view: await startLoop(task.id, body),
32
+ })
33
+ );
34
+ }
35
+
36
+ const editSchema = z.object({
37
+ limits: limitsSchema.optional(),
38
+ extend: z
39
+ .object({
40
+ cycles: z.number().optional(),
41
+ costUsd: z.number().optional(),
42
+ minutes: z.number().optional(),
43
+ })
44
+ .optional(),
45
+ addCriteria: z.array(criterionSchema).optional(),
46
+ editCriteria: z
47
+ .array(z.object({ id: z.number(), text: z.string() }))
48
+ .optional(),
49
+ removeCriteria: z.array(z.number()).optional(),
50
+ waive: z.array(z.number()).optional(),
51
+ });
52
+
53
+ // PATCH /api/tasks/:id/loop - limits, an extension, or the checklist.
54
+ // Raising the spend and waiving a criterion need "Can approve".
55
+ export async function PATCH(request: NextRequest, ctx: Ctx) {
56
+ return loopRoute(
57
+ request,
58
+ ctx,
59
+ {
60
+ schema: editSchema,
61
+ approver: (b) =>
62
+ !!(
63
+ b.extend ||
64
+ b.waive?.length ||
65
+ (b.limits && b.limits.maxCostUsd !== undefined)
66
+ ),
67
+ },
68
+ (task, body, device) => ({
69
+ view: editLoop(task.id, body, device ? "a device" : "you"),
70
+ })
71
+ );
72
+ }
@@ -0,0 +1,12 @@
1
+ import { NextRequest } from "next/server";
2
+ import { runNow } from "@/lib/loops";
3
+ import { loopRoute } from "@/lib/loops/route";
4
+
5
+ // POST /api/tasks/:id/loop/run - a cycle at the next tick; resumes a loop
6
+ // that stopped for a reason other than done.
7
+ export async function POST(
8
+ request: NextRequest,
9
+ ctx: { params: Promise<{ id: string }> }
10
+ ) {
11
+ return loopRoute(request, ctx, {}, (task) => ({ view: runNow(task.id) }));
12
+ }
@@ -0,0 +1,12 @@
1
+ import { NextRequest } from "next/server";
2
+ import { stopByUser } from "@/lib/loops";
3
+ import { loopRoute } from "@/lib/loops/route";
4
+
5
+ // POST /api/tasks/:id/loop/stop - end the loop; the coder's turn, if one
6
+ // is running, is left alone.
7
+ export async function POST(
8
+ request: NextRequest,
9
+ ctx: { params: Promise<{ id: string }> }
10
+ ) {
11
+ return loopRoute(request, ctx, {}, (task) => ({ view: stopByUser(task.id) }));
12
+ }
@@ -0,0 +1,29 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { refuse } from "@/lib/tasks/contract/authorize";
3
+ import { modeOf, setTaskMode } from "@/lib/reports/mode";
4
+ import { isMode } from "@/lib/reports/types";
5
+
6
+ type Ctx = { params: Promise<{ id: string }> };
7
+
8
+ // GET /api/tasks/:id/mode - the operating mode in force and where it comes from.
9
+ export async function GET(request: NextRequest, { params }: Ctx) {
10
+ const { id } = await params;
11
+ const refused = refuse(request, id, "open");
12
+ if (refused) return refused;
13
+ return NextResponse.json(modeOf(id));
14
+ }
15
+
16
+ // PUT /api/tasks/:id/mode {mode} - this task's override; null follows its workspace.
17
+ export async function PUT(request: NextRequest, { params }: Ctx) {
18
+ const { id } = await params;
19
+ const refused = refuse(request, id, "plan");
20
+ if (refused) return refused;
21
+ const { mode } = await request.json().catch(() => ({}));
22
+ if (mode !== null && !isMode(mode))
23
+ return NextResponse.json(
24
+ { error: "mode is manual, assisted, autonomous or null" },
25
+ { status: 400 }
26
+ );
27
+ setTaskMode(id, mode);
28
+ return NextResponse.json(modeOf(id));
29
+ }
@@ -0,0 +1,54 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { refuse } from "@/lib/tasks/contract/authorize";
3
+ import { approveDraft, dismissDraft, DraftError } from "@/lib/reports/drafts";
4
+ import { AnalystError } from "@/lib/analyst/types";
5
+ import { readReport } from "@/lib/reports/read";
6
+
7
+ // POST /api/tasks/:id/report/draft {source, id, linkId?, action, text?} -
8
+ // send the drafted next prompt to the coding agent (text: the reader's
9
+ // edit), or dismiss it. 409 when it was already decided.
10
+ export async function POST(
11
+ request: NextRequest,
12
+ { params }: { params: Promise<{ id: string }> }
13
+ ) {
14
+ const { id: taskId } = await params;
15
+ const refused = refuse(request, taskId, "plan");
16
+ if (refused) return refused;
17
+ const b = await request.json().catch(() => ({}));
18
+ const source =
19
+ b.source === "analyst"
20
+ ? "analyst"
21
+ : b.source === "report"
22
+ ? "report"
23
+ : null;
24
+ if (
25
+ !source ||
26
+ typeof b.id !== "string" ||
27
+ !["send", "dismiss"].includes(b.action)
28
+ )
29
+ return NextResponse.json({ error: "Bad draft request" }, { status: 400 });
30
+ if (
31
+ b.text !== undefined &&
32
+ (typeof b.text !== "string" || b.text.length > 20_000)
33
+ )
34
+ return NextResponse.json({ error: "Bad text" }, { status: 400 });
35
+ const ref = {
36
+ source,
37
+ id: b.id,
38
+ linkId: typeof b.linkId === "string" ? b.linkId : undefined,
39
+ } as const;
40
+ try {
41
+ if (b.action === "send")
42
+ await approveDraft(taskId, { ...ref, text: b.text });
43
+ else dismissDraft(taskId, ref);
44
+ return NextResponse.json(readReport(taskId));
45
+ } catch (error) {
46
+ if (error instanceof DraftError || error instanceof AnalystError)
47
+ return NextResponse.json(
48
+ { error: error.message },
49
+ { status: error.status }
50
+ );
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return NextResponse.json({ error: message }, { status: 500 });
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { refuse } from "@/lib/tasks/contract/authorize";
3
+ import { latestReport } from "@/lib/reports/store";
4
+
5
+ // GET /api/tasks/:id/report/evidence/:evId - one piece of evidence behind the
6
+ // latest report, for the drill-down. Its text is untrusted: show it only.
7
+ export async function GET(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ id: string; evId: string }> }
10
+ ) {
11
+ const { id, evId } = await params;
12
+ const refused = refuse(request, id, "open");
13
+ if (refused) return refused;
14
+ const ev = latestReport(id)?.evidence.find((e) => e.id === evId);
15
+ if (!ev)
16
+ return NextResponse.json({ error: "No such evidence" }, { status: 404 });
17
+ return NextResponse.json({ evidence: ev });
18
+ }
@@ -0,0 +1,39 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { refuse } from "@/lib/tasks/contract/authorize";
3
+ import { generateReport } from "@/lib/reports/generate";
4
+ import { readReport } from "@/lib/reports/read";
5
+ import { historyOf } from "@/lib/reports/store";
6
+ import { db } from "@/lib/db";
7
+
8
+ type Ctx = { params: Promise<{ id: string }> };
9
+
10
+ const exists = (id: string) =>
11
+ !!db.prepare(`SELECT 1 FROM session_rows WHERE id = ?`).get(id);
12
+
13
+ // GET /api/tasks/:id/report - the latest plain-English health report, whether
14
+ // one is being built, and the task's operating mode. ?history=1 lists the
15
+ // earlier ones.
16
+ export async function GET(request: NextRequest, { params }: Ctx) {
17
+ const { id } = await params;
18
+ const refused = refuse(request, id, "open");
19
+ if (refused) return refused;
20
+ if (!exists(id))
21
+ return NextResponse.json({ error: "No such task" }, { status: 404 });
22
+ if (request.nextUrl.searchParams.get("history") === "1")
23
+ return NextResponse.json({ history: historyOf(id) });
24
+ return NextResponse.json(readReport(id));
25
+ }
26
+
27
+ // POST /api/tasks/:id/report - build a new report now. Answers at once; the
28
+ // client polls the GET until `generating` clears.
29
+ export async function POST(request: NextRequest, { params }: Ctx) {
30
+ const { id } = await params;
31
+ const refused = refuse(request, id, "plan");
32
+ if (refused) return refused;
33
+ if (!exists(id))
34
+ return NextResponse.json({ error: "No such task" }, { status: 404 });
35
+ generateReport(id, "on-demand").catch((error) =>
36
+ console.error(`Report ${id}:`, error)
37
+ );
38
+ return NextResponse.json({ generating: true }, { status: 202 });
39
+ }
@@ -0,0 +1,23 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { setWorkspaceMode, workspaceMode } from "@/lib/reports/mode";
3
+ import { isMode } from "@/lib/reports/types";
4
+
5
+ type Ctx = { params: Promise<{ id: string }> };
6
+
7
+ // GET /api/workspaces/:id/mode - the default operating mode of its tasks.
8
+ export async function GET(_request: NextRequest, { params }: Ctx) {
9
+ return NextResponse.json({ mode: workspaceMode((await params).id) });
10
+ }
11
+
12
+ // PUT /api/workspaces/:id/mode {mode}
13
+ export async function PUT(request: NextRequest, { params }: Ctx) {
14
+ const { id } = await params;
15
+ const { mode } = await request.json().catch(() => ({}));
16
+ if (!isMode(mode))
17
+ return NextResponse.json(
18
+ { error: "mode is manual, assisted or autonomous" },
19
+ { status: 400 }
20
+ );
21
+ setWorkspaceMode(id, mode);
22
+ return NextResponse.json({ mode });
23
+ }
@@ -0,0 +1,17 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { isFinished } from "@/lib/tasks/state";
3
+ import { latestByTask, summaryOf } from "@/lib/reports/store";
4
+
5
+ // GET /api/workspaces/:id/reports - the newest report of each task: headline,
6
+ // estimate and state. ?active=1 leaves out finished tasks.
7
+ export async function GET(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const { id } = await params;
12
+ const active = request.nextUrl.searchParams.get("active") === "1";
13
+ const reports = latestByTask(id)
14
+ .filter(({ report }) => !active || !isFinished(report.state))
15
+ .map(({ report, name }) => summaryOf(report, name));
16
+ return NextResponse.json({ reports });
17
+ }
package/app/page.tsx CHANGED
@@ -142,6 +142,13 @@ const ExportSheet = dynamic(
142
142
  () => import("@/components/Export/ExportSheet").then((m) => m.ExportSheet),
143
143
  { ssr: false }
144
144
  );
145
+ const StartAnalystSheet = dynamic(
146
+ () =>
147
+ import("@/components/Analyst/StartAnalystSheet").then(
148
+ (m) => m.StartAnalystSheet
149
+ ),
150
+ { ssr: false }
151
+ );
145
152
  const CommandPalette = dynamic(
146
153
  () =>
147
154
  import("@/components/CommandPalette/CommandPalette").then(
@@ -171,6 +178,7 @@ const AppDialogs = memo(function AppDialogs({
171
178
  <MoveDialog />
172
179
  <ShareDialog />
173
180
  <ExportSheet />
181
+ <StartAnalystSheet />
174
182
  <AwayReport />
175
183
  <IncomingLinks />
176
184
  <CommandPalette />