@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
@@ -0,0 +1,265 @@
1
+ import { spawn } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { agentosHome } from "../agentos-home";
5
+ import { git } from "../git-exec";
6
+ import { adviseHeavy } from "../load/advise";
7
+ import { checkEnv } from "../orchestrator/claude-cli";
8
+ import { compactCommand, signatureOf } from "./compactEvidence";
9
+ import type { CommandResult, EvidenceSummary } from "./types";
10
+
11
+ export const EVIDENCE_TIMEOUT_MS = 10 * 60_000;
12
+ const OUTPUT_KEEP = 2_000_000;
13
+
14
+ export interface EvidenceCommands {
15
+ typecheck: string | null;
16
+ test: string | null;
17
+ }
18
+
19
+ const TYPECHECK = /\btypecheck\b|\btsc\b/;
20
+ const PLACEHOLDER = /<[^<>]*>/;
21
+ const TEST_FILE =
22
+ /\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|\/)test_[^/]+\.py$|_test\.(?:go|py|rb)$/;
23
+
24
+ // The yardstick is the BASE branch's agentos.json (or .dispatch.json): the
25
+ // coder can't make a check pass by editing the command that runs it.
26
+ export async function commandsFromBase(
27
+ repo: string,
28
+ base: string
29
+ ): Promise<EvidenceCommands> {
30
+ for (const file of ["agentos.json", ".dispatch.json"]) {
31
+ let text: string;
32
+ try {
33
+ text = await git(repo, ["show", `${base}:${file}`]);
34
+ } catch {
35
+ continue;
36
+ }
37
+ try {
38
+ const test = (JSON.parse(text) as { test?: unknown }).test;
39
+ if (typeof test !== "string" || !test.trim()) continue;
40
+ const parts = test
41
+ .split("&&")
42
+ .map((s) => s.trim())
43
+ .filter(Boolean);
44
+ const type = parts.filter((p) => TYPECHECK.test(p));
45
+ const rest = parts.filter((p) => !TYPECHECK.test(p));
46
+ return {
47
+ typecheck: type.length ? type.join(" && ") : null,
48
+ test: rest.length ? rest.join(" && ") : null,
49
+ };
50
+ } catch {
51
+ continue;
52
+ }
53
+ }
54
+ return { typecheck: null, test: null };
55
+ }
56
+
57
+ // The test files in the diff, and the ones beside changed source files.
58
+ export async function testFilesFor(
59
+ dir: string,
60
+ base: string,
61
+ sha: string
62
+ ): Promise<string[]> {
63
+ const out = await git(dir, [
64
+ "diff",
65
+ "--name-only",
66
+ "--no-renames",
67
+ `${base}...${sha}`,
68
+ ]).catch(() => "");
69
+ const changed = out
70
+ .split("\n")
71
+ .map((s) => s.trim())
72
+ .filter(Boolean);
73
+ const found = new Set<string>();
74
+ for (const f of changed) {
75
+ if (TEST_FILE.test(f)) {
76
+ if (fs.existsSync(path.join(dir, f))) found.add(f);
77
+ continue;
78
+ }
79
+ const ext = path.extname(f);
80
+ const stem = f.slice(0, f.length - ext.length);
81
+ for (const suffix of [".test", ".spec"])
82
+ for (const e of [ext, ".ts", ".tsx"]) {
83
+ const candidate = `${stem}${suffix}${e}`;
84
+ if (fs.existsSync(path.join(dir, candidate))) found.add(candidate);
85
+ }
86
+ }
87
+ return [...found].sort();
88
+ }
89
+
90
+ // "<the test files you touched>" and its like become the files; with none the
91
+ // command is skipped. A command with no placeholder is the whole suite.
92
+ export function fillPlaceholder(
93
+ command: string,
94
+ files: string[]
95
+ ): { command: string | null; why?: string } {
96
+ if (!PLACEHOLDER.test(command)) return { command };
97
+ if (!files.length)
98
+ return { command: null, why: "no test files in the diff to run" };
99
+ return {
100
+ command: command.replace(
101
+ PLACEHOLDER,
102
+ files.map((f) => JSON.stringify(f)).join(" ")
103
+ ),
104
+ };
105
+ }
106
+
107
+ export interface RunOutput {
108
+ exitCode: number | null;
109
+ output: string;
110
+ durationMs: number;
111
+ }
112
+ export type Runner = (
113
+ command: string,
114
+ cwd: string,
115
+ timeoutMs: number
116
+ ) => Promise<RunOutput>;
117
+
118
+ export const runShell: Runner = (command, cwd, timeoutMs) =>
119
+ new Promise((resolve) => {
120
+ const started = Date.now();
121
+ const child = spawn(command, {
122
+ cwd,
123
+ shell: true,
124
+ env: { ...checkEnv(), CI: "1", FORCE_COLOR: "0" },
125
+ stdio: ["ignore", "pipe", "pipe"],
126
+ detached: true,
127
+ });
128
+ if (child.pid)
129
+ try {
130
+ adviseHeavy({
131
+ key: `pid:${child.pid}`,
132
+ sessionId: null,
133
+ label: "loop checks",
134
+ pid: child.pid,
135
+ });
136
+ } catch {}
137
+ let output = "";
138
+ const take = (d: Buffer) => {
139
+ output += d;
140
+ if (output.length > OUTPUT_KEEP) output = output.slice(-OUTPUT_KEEP);
141
+ };
142
+ child.stdout.on("data", take);
143
+ child.stderr.on("data", take);
144
+ const timer = setTimeout(() => {
145
+ try {
146
+ process.kill(-child.pid!, "SIGKILL");
147
+ } catch {}
148
+ output += `\n[timed out after ${Math.round(timeoutMs / 1000)}s]`;
149
+ }, timeoutMs);
150
+ child.on("close", (code) => {
151
+ clearTimeout(timer);
152
+ resolve({ exitCode: code, output, durationMs: Date.now() - started });
153
+ });
154
+ child.on("error", (e) => {
155
+ clearTimeout(timer);
156
+ resolve({
157
+ exitCode: 127,
158
+ output: String(e),
159
+ durationMs: Date.now() - started,
160
+ });
161
+ });
162
+ });
163
+
164
+ // One evidence run at a time on the machine, so loops don't stack suites.
165
+ let chain: Promise<unknown> = Promise.resolve();
166
+ const alone = <T>(fn: () => Promise<T>): Promise<T> => {
167
+ const next = chain.then(fn, fn);
168
+ chain = next.catch(() => {});
169
+ return next;
170
+ };
171
+
172
+ export const loopLogPath = (taskId: string, n: number) =>
173
+ agentosHome("loops", taskId, `${n}.log`);
174
+
175
+ export interface Gathered {
176
+ summary: EvidenceSummary;
177
+ signature: string | null;
178
+ logPath: string | null;
179
+ }
180
+
181
+ // Typecheck, then the tests, in the task's worktree. Output goes to a log
182
+ // file; what comes back is compact.
183
+ export async function gatherEvidence(input: {
184
+ taskId: string;
185
+ n: number;
186
+ dir: string;
187
+ commands: EvidenceCommands;
188
+ testFiles: string[];
189
+ run?: Runner;
190
+ timeoutMs?: number;
191
+ }): Promise<Gathered> {
192
+ const run = input.run ?? runShell;
193
+ const results: CommandResult[] = [];
194
+ const outputs = new Map<string, string>();
195
+ const log: string[] = [];
196
+ const todo: { name: CommandResult["name"]; raw: string | null }[] = [
197
+ { name: "typecheck", raw: input.commands.typecheck },
198
+ { name: "test", raw: input.commands.test },
199
+ ];
200
+ await alone(async () => {
201
+ for (const { name, raw } of todo) {
202
+ if (!raw) continue;
203
+ const filled = fillPlaceholder(raw, input.testFiles);
204
+ if (!filled.command) {
205
+ results.push({
206
+ name,
207
+ command: raw,
208
+ exitCode: null,
209
+ durationMs: 0,
210
+ passed: [],
211
+ failed: [],
212
+ errors: [],
213
+ excerpt: "",
214
+ skipped: filled.why,
215
+ });
216
+ continue;
217
+ }
218
+ const r = await run(
219
+ filled.command,
220
+ input.dir,
221
+ input.timeoutMs ?? EVIDENCE_TIMEOUT_MS
222
+ );
223
+ log.push(`$ ${filled.command}\n${r.output}\n[exit ${r.exitCode}]\n`);
224
+ outputs.set(name, r.output);
225
+ results.push(
226
+ compactCommand({
227
+ name,
228
+ command: filled.command,
229
+ exitCode: r.exitCode,
230
+ durationMs: r.durationMs,
231
+ output: r.output,
232
+ })
233
+ );
234
+ }
235
+ });
236
+ const ran = results.filter((r) => !r.skipped);
237
+ const green = ran.length > 0 && ran.every((r) => r.exitCode === 0);
238
+ const note = !ran.length
239
+ ? "no check could run: " +
240
+ (results
241
+ .map((r) => r.skipped)
242
+ .filter(Boolean)
243
+ .join("; ") || "no test command on the base branch")
244
+ : results.some((r) => r.skipped)
245
+ ? results
246
+ .filter((r) => r.skipped)
247
+ .map((r) => `${r.name} skipped: ${r.skipped}`)
248
+ .join("; ")
249
+ : undefined;
250
+ let logPath: string | null = null;
251
+ if (log.length) {
252
+ try {
253
+ logPath = loopLogPath(input.taskId, input.n);
254
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
255
+ fs.writeFileSync(logPath, log.join("\n"));
256
+ } catch {
257
+ logPath = null;
258
+ }
259
+ }
260
+ return {
261
+ summary: { green, commands: results, ci: [], ...(note ? { note } : {}) },
262
+ signature: signatureOf(results, outputs),
263
+ logPath,
264
+ };
265
+ }
@@ -0,0 +1,40 @@
1
+ import type { Stop } from "./decideStop";
2
+ import type { LoopRow } from "./types";
3
+
4
+ // Where other features plug into the loop without the loop importing them
5
+ // (the plain-English health reports and operating modes, BR02-19, set these
6
+ // once they land; until then they are empty).
7
+ export interface LoopHooks {
8
+ // A cycle's checks and analysis are stored: ask for a report, fire and
9
+ // forget. The summary is the analyst's own words, not evidence.
10
+ onCycleSettled?: (input: {
11
+ taskId: string;
12
+ cycleId: number;
13
+ analystSummary: string | null;
14
+ }) => void;
15
+ // The loop began or ended on a task: the operating mode follows.
16
+ onStart?: (loop: LoopRow) => void;
17
+ onStop?: (
18
+ loop: LoopRow,
19
+ stop: Stop,
20
+ pendingInstruction: string | null
21
+ ) => void;
22
+ }
23
+
24
+ let hooks: LoopHooks = {};
25
+
26
+ export const setLoopHooks = (next: LoopHooks) => {
27
+ hooks = next;
28
+ };
29
+
30
+ // A hook never breaks the loop.
31
+ export function callHook<K extends keyof LoopHooks>(
32
+ name: K,
33
+ ...args: Parameters<NonNullable<LoopHooks[K]>>
34
+ ): void {
35
+ try {
36
+ (hooks[name] as ((...a: unknown[]) => void) | undefined)?.(...args);
37
+ } catch (error) {
38
+ console.error(`Loop hook ${name}:`, error);
39
+ }
40
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./controls";
2
+ export { tickLoops } from "./tickLoops";
3
+ export { setLoopHooks, type LoopHooks } from "./hooks";
4
+ export { draftCriteria } from "./draftCriteria";
5
+ export { liveLoopFor, latestLoopFor } from "./store";
6
+ export type * from "./types";
7
+ export { loopSummary, loopDigestNote } from "./summary";
@@ -0,0 +1,83 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { refuse } from "../tasks/contract/authorize";
4
+ import { requireApprover } from "../security/approver";
5
+ import { db, type Session } from "../db";
6
+ import { LoopError } from "./types";
7
+
8
+ type Ctx = { params: Promise<{ id: string }> };
9
+
10
+ export const criterionSchema = z.object({
11
+ text: z.string().max(2000),
12
+ source: z.enum(["card", "brief", "plan", "user"]).optional(),
13
+ evidenceKind: z
14
+ .enum(["test", "diff", "typecheck", "ci"])
15
+ .nullable()
16
+ .optional(),
17
+ });
18
+
19
+ export const limitsSchema = z
20
+ .object({
21
+ maxCycles: z.number(),
22
+ maxCostUsd: z.number(),
23
+ maxMinutes: z.number(),
24
+ graceSeconds: z.number(),
25
+ stallCycles: z.number(),
26
+ repeatCycles: z.number(),
27
+ })
28
+ .partial();
29
+
30
+ // A task's loop route: the same device rule as the task's other routes, a
31
+ // zod-checked body, and the loop's own errors as their status codes.
32
+ export async function loopRoute<B = undefined>(
33
+ request: NextRequest,
34
+ { params }: Ctx,
35
+ opts: {
36
+ schema?: z.ZodType<B>;
37
+ // Raises what the loop may spend or skips a check: a device that can't
38
+ // answer asks may not.
39
+ approver?: (body: B) => boolean;
40
+ },
41
+ run: (
42
+ task: Session,
43
+ body: B,
44
+ device: string | null
45
+ ) => Promise<object> | object
46
+ ): Promise<NextResponse> {
47
+ const id = (await params).id;
48
+ const refused = refuse(request, id, "open");
49
+ if (refused) return refused;
50
+ const task = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
51
+ Session | undefined;
52
+ if (!task)
53
+ return NextResponse.json({ error: "No such task" }, { status: 404 });
54
+ let body = undefined as B;
55
+ if (opts.schema) {
56
+ const parsed = opts.schema.safeParse(
57
+ await request.json().catch(() => ({}))
58
+ );
59
+ if (!parsed.success)
60
+ return NextResponse.json(
61
+ { error: parsed.error.issues[0]?.message ?? "Bad request" },
62
+ { status: 400 }
63
+ );
64
+ body = parsed.data;
65
+ }
66
+ let device: string | null = null;
67
+ if (opts.approver?.(body)) {
68
+ const gate = requireApprover(request);
69
+ if (!gate.ok) return gate.response;
70
+ device = gate.approver.deviceId;
71
+ }
72
+ try {
73
+ return NextResponse.json(await run(task, body, device));
74
+ } catch (error) {
75
+ if (error instanceof LoopError)
76
+ return NextResponse.json(
77
+ { error: error.message },
78
+ { status: error.status }
79
+ );
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ return NextResponse.json({ error: message }, { status: 500 });
82
+ }
83
+ }