@saadnvd1/agent-os 0.3.533 → 0.3.534

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 (83) hide show
  1. package/README.md +68 -2
  2. package/app/api/features/pieces/[id]/release/route.ts +16 -0
  3. package/app/api/features/route.ts +8 -0
  4. package/app/api/recipes/[id]/carry/clear/route.ts +20 -0
  5. package/app/api/recipes/[id]/preview/route.ts +19 -0
  6. package/app/api/recipes/[id]/route.ts +51 -0
  7. package/app/api/recipes/[id]/run/route.ts +35 -0
  8. package/app/api/recipes/[id]/runs/route.ts +23 -0
  9. package/app/api/recipes/draft/route.ts +14 -0
  10. package/app/api/recipes/route.ts +35 -0
  11. package/app/api/tasks/[id]/plan-edits/route.ts +34 -0
  12. package/components/CommandPalette/useAppCommands.ts +20 -0
  13. package/components/Features/FeaturesSection.tsx +100 -0
  14. package/components/Features/index.ts +1 -0
  15. package/components/Recipes/RecipeForm.tsx +251 -0
  16. package/components/Recipes/RecipePage.tsx +127 -0
  17. package/components/Recipes/RecipesPanel.tsx +225 -0
  18. package/components/Recipes/RunChip.tsx +16 -0
  19. package/components/Recipes/RunHistory.tsx +88 -0
  20. package/components/Recipes/RunSheet.tsx +84 -0
  21. package/components/Recipes/TimerFields.tsx +193 -0
  22. package/components/Recipes/format.ts +51 -0
  23. package/components/Recipes/index.ts +1 -0
  24. package/components/Schedules/SchedulesPanel.tsx +1 -0
  25. package/components/SessionList/RowSubtitle.tsx +16 -0
  26. package/components/SessionList/SessionRowMenu.tsx +29 -0
  27. package/components/SessionList/SidebarHeader.tsx +5 -0
  28. package/components/SessionList/WorkspaceSwitcher.tsx +20 -4
  29. package/components/Settings/SettingsDialog.tsx +9 -0
  30. package/components/Tasks/PlanReview.tsx +253 -0
  31. package/components/Tasks/TaskRow.tsx +19 -44
  32. package/components/Tasks/TasksDialog.tsx +2 -0
  33. package/data/features/index.ts +40 -0
  34. package/data/push/topics.ts +8 -0
  35. package/data/recipes/index.ts +129 -0
  36. package/data/tasks/index.ts +14 -0
  37. package/lib/away/run.ts +4 -1
  38. package/lib/db/changes.ts +8 -0
  39. package/lib/db/migrations.ts +127 -0
  40. package/lib/db/queries.ts +4 -1
  41. package/lib/db/types.ts +6 -0
  42. package/lib/models/plan-step.ts +67 -6
  43. package/lib/notify/copy.ts +28 -10
  44. package/lib/notify/push/events.ts +36 -2
  45. package/lib/notify/push/policy.ts +2 -0
  46. package/lib/notify/push-types.ts +7 -3
  47. package/lib/orchestrator/brief.ts +13 -0
  48. package/lib/orchestrator/conditions.ts +3 -1
  49. package/lib/orchestrator/deliver.ts +2 -0
  50. package/lib/orchestrator/digest.ts +27 -1
  51. package/lib/orchestrator/plan-pieces.ts +41 -0
  52. package/lib/orchestrator/recipe-tools.ts +159 -0
  53. package/lib/orchestrator/review.ts +2 -1
  54. package/lib/orchestrator/serve.ts +25 -0
  55. package/lib/orchestrator/tool-names.ts +5 -0
  56. package/lib/orchestrator/tool-schemas.ts +54 -0
  57. package/lib/orchestrator/tools.ts +10 -0
  58. package/lib/plans/features.ts +200 -0
  59. package/lib/plans/pieces.ts +366 -0
  60. package/lib/plans/release.ts +323 -0
  61. package/lib/plans/view.ts +85 -0
  62. package/lib/recipes/api.ts +19 -0
  63. package/lib/recipes/chip.ts +40 -0
  64. package/lib/recipes/compose.ts +78 -0
  65. package/lib/recipes/draft.ts +93 -0
  66. package/lib/recipes/index.ts +54 -0
  67. package/lib/recipes/result.ts +59 -0
  68. package/lib/recipes/run.ts +202 -0
  69. package/lib/recipes/settle.ts +465 -0
  70. package/lib/recipes/store.ts +378 -0
  71. package/lib/recipes/test-fixtures.ts +49 -0
  72. package/lib/recipes/timer.ts +107 -0
  73. package/lib/recipes/types.ts +116 -0
  74. package/lib/schedules/index.ts +4 -1
  75. package/lib/schedules/start.ts +33 -9
  76. package/lib/schedules/store.ts +6 -2
  77. package/lib/security/demo.ts +1 -0
  78. package/lib/settings-url.ts +1 -0
  79. package/lib/tasks/loop-types.ts +23 -0
  80. package/lib/tasks/plan-view.ts +38 -1
  81. package/package.json +1 -1
  82. package/server.ts +2 -0
  83. package/stores/recipesUi.ts +53 -0
@@ -17,6 +17,8 @@ import { notifyStatusChanged } from "../status/hub";
17
17
  import { formatRunTime } from "./cron";
18
18
  import { Braked, fromLabel, type RunDeps, type Started } from "./run";
19
19
  import { braked, BrakeRefused } from "../orchestrator/brakes";
20
+ import { startRun } from "../recipes/run";
21
+ import { hasLiveRun } from "../recipes/store";
20
22
  import { startedSessions, type Schedule, type ScheduleRun } from "./store";
21
23
 
22
24
  function projectOf(schedule: Schedule) {
@@ -220,6 +222,8 @@ export async function stillRunning(
220
222
  return schedule.target_session_id
221
223
  ? sessionBusy(schedule.target_session_id)
222
224
  : false;
225
+ case "recipe":
226
+ return schedule.recipe_id ? hasLiveRun(schedule.recipe_id) : false;
223
227
  case "orchestrator": {
224
228
  // A message: done once it's delivered. An agent's check-in, like one
225
229
  // to any session, waits while the orchestrator is still working.
@@ -279,17 +283,37 @@ async function throughBrakes(
279
283
  }
280
284
  }
281
285
 
286
+ // A recipe's timer starts a run; the recipe's own checks (one live run,
287
+ // brakes) decide the rest.
288
+ async function startRecipe(
289
+ schedule: Schedule,
290
+ onSession: OnSession,
291
+ trigger: "timer" | "catch-up"
292
+ ): Promise<string> {
293
+ if (!schedule.recipe_id) throw new Error("Its recipe is gone");
294
+ try {
295
+ const { run } = await startRun(schedule.recipe_id, { trigger });
296
+ if (run.session_id) onSession(run.session_id);
297
+ return run.session_id ?? "";
298
+ } catch (error) {
299
+ if (error instanceof BrakeRefused) throw new Braked(error.reason);
300
+ throw error;
301
+ }
302
+ }
303
+
282
304
  export const realDeps: RunDeps = {
283
305
  start: (schedule, onSession) =>
284
- schedule.kind === "task"
285
- ? throughBrakes(schedule, "task", () => startTask(schedule, onSession))
286
- : schedule.kind === "session"
287
- ? throughBrakes(schedule, "session", () =>
288
- startSession(schedule, onSession)
289
- )
290
- : schedule.kind === "message"
291
- ? messageSession(schedule, onSession)
292
- : postToOrchestrator(schedule, onSession),
306
+ schedule.kind === "recipe"
307
+ ? startRecipe(schedule, onSession, "timer")
308
+ : schedule.kind === "task"
309
+ ? throughBrakes(schedule, "task", () => startTask(schedule, onSession))
310
+ : schedule.kind === "session"
311
+ ? throughBrakes(schedule, "session", () =>
312
+ startSession(schedule, onSession)
313
+ )
314
+ : schedule.kind === "message"
315
+ ? messageSession(schedule, onSession)
316
+ : postToOrchestrator(schedule, onSession),
293
317
  stillRunning,
294
318
  notify,
295
319
  };
@@ -25,7 +25,9 @@ export const SCHEDULE_KINDS = [
25
25
  "orchestrator",
26
26
  "message",
27
27
  ] as const;
28
- export type ScheduleKind = (typeof SCHEDULE_KINDS)[number];
28
+ // "recipe" is a recipe's timer (lib/recipes/timer): it runs a recipe, and is
29
+ // made and edited through the recipe, never through the Schedules form.
30
+ export type ScheduleKind = (typeof SCHEDULE_KINDS)[number] | "recipe";
29
31
 
30
32
  export type RunTrigger = "schedule" | "catch-up" | "manual";
31
33
  // "claimed" only between taking a slot and starting its work.
@@ -44,6 +46,8 @@ export interface Schedule {
44
46
  // A "message" schedule's session. Its id is the address; its name is
45
47
  // only looked up to show.
46
48
  target_session_id: string | null;
49
+ // A recipe timer's recipe.
50
+ recipe_id?: string | null;
47
51
  // The agent session that made it (aos schedule add), or null for Saad.
48
52
  created_by_session_id: string | null;
49
53
  enabled: boolean;
@@ -223,7 +227,7 @@ function validate(
223
227
  throw new Error("A schedule name can't use quotes or [ ]");
224
228
  const prompt = input.prompt?.trim();
225
229
  if (!prompt) throw new Error("Write the prompt it runs");
226
- if (!SCHEDULE_KINDS.includes(input.kind))
230
+ if (!(SCHEDULE_KINDS as readonly string[]).includes(input.kind))
227
231
  throw new Error("Pick task, session, orchestrator or message");
228
232
  const timezone = input.timezone?.trim() || DEFAULT_TIMEZONE;
229
233
  if (!isTimezone(timezone)) throw new Error(`Unknown time zone "${timezone}"`);
@@ -57,6 +57,7 @@ export const STATIC_SIBLINGS = [
57
57
  "connect",
58
58
  "detect",
59
59
  "done-idle",
60
+ "draft",
60
61
  "held-replies",
61
62
  "import",
62
63
  "init",
@@ -10,6 +10,7 @@ export const SETTINGS_SECTIONS = [
10
10
  "models",
11
11
  "devices",
12
12
  "notifications",
13
+ "recipes",
13
14
  "schedules",
14
15
  "audits",
15
16
  "setup",
@@ -149,7 +149,30 @@ export interface TaskChecks {
149
149
 
150
150
  // -- A task's plan ---------------------------------------------------------
151
151
 
152
+ export interface PlanPieceView {
153
+ position: number;
154
+ name: string;
155
+ slice: string;
156
+ project: string;
157
+ afterPosition: number | null;
158
+ afterKind: "merged" | "deployed" | null;
159
+ reasons: string[];
160
+ owns: string[];
161
+ // Merging into the piece before would break why it was split (a).
162
+ forced: boolean;
163
+ // Saad's pending edit, applied at approval.
164
+ edit: "merge" | "drop" | null;
165
+ }
166
+
167
+ export interface PlanPiecesView {
168
+ pieces: PlanPieceView[];
169
+ folded: { name: string; into: number; why: string }[];
170
+ warnings: string[];
171
+ }
172
+
152
173
  export interface TaskPlan {
174
+ // Set once a plan waits for approval: how the checker split it.
175
+ pieces?: PlanPiecesView | null;
153
176
  // As the starter decided (null reason when unstated).
154
177
  planned: boolean;
155
178
  reason: string | null;
@@ -9,7 +9,13 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import { db, type Session } from "../db";
11
11
  import { PLAN_FILE, planSummary } from "../models/plan-step";
12
- import { PLAN_TEXT_MAX, type TaskPlan } from "./loop-types";
12
+ import { planFor } from "../plans/features";
13
+ import { parseEdits } from "../plans/pieces";
14
+ import {
15
+ PLAN_TEXT_MAX,
16
+ type PlanPiecesView,
17
+ type TaskPlan,
18
+ } from "./loop-types";
13
19
  import { expandHome } from "./session";
14
20
 
15
21
  const TITLE_MAX = 120;
@@ -61,6 +67,36 @@ function capped(text: string) {
61
67
  return { text: over ? text.slice(0, PLAN_TEXT_MAX) : text, truncated: over };
62
68
  }
63
69
 
70
+ const PIECES_READ_MAX = 400_000;
71
+
72
+ // How the plan splits, read from the whole file (the block is at its end).
73
+ function piecesView(session: Session, file: string): PlanPiecesView | null {
74
+ const text = readHead(file, PIECES_READ_MAX);
75
+ const result = text ? planFor(session, text) : null;
76
+ if (!result) return null;
77
+ const edits = parseEdits(session.plan_edits);
78
+ return {
79
+ pieces: result.pieces.map((p) => ({
80
+ position: p.position,
81
+ name: p.name,
82
+ slice: p.slice,
83
+ project: p.project.name,
84
+ afterPosition: p.afterPosition,
85
+ afterKind: p.afterKind,
86
+ reasons: p.reasons,
87
+ owns: p.owns,
88
+ forced: p.reasons.includes("a"),
89
+ edit: edits.drop?.includes(p.position)
90
+ ? "drop"
91
+ : edits.merge?.includes(p.position)
92
+ ? "merge"
93
+ : null,
94
+ })),
95
+ folded: result.folded,
96
+ warnings: result.warnings,
97
+ };
98
+ }
99
+
64
100
  export function taskPlan(session: Session): TaskPlan | null {
65
101
  const decided = session.plan_decided ?? null;
66
102
  const phase = session.plan_phase ?? null;
@@ -111,6 +147,7 @@ export function taskPlan(session: Session): TaskPlan | null {
111
147
  source: "step",
112
148
  title: planTitle(body),
113
149
  path: text !== null ? PLAN_FILE : null,
150
+ pieces: phase === "awaiting" ? piecesView(session, file) : null,
114
151
  ...capped(body),
115
152
  };
116
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.533",
3
+ "version": "0.3.534",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
package/server.ts CHANGED
@@ -13,6 +13,7 @@ import { stackStartGate } from "./lib/orchestrator/brakes";
13
13
  import { buildId } from "./lib/build";
14
14
  import { startOrchestratorWatcher } from "./lib/orchestrator/watcher";
15
15
  import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
16
+ import { startSettling } from "./lib/recipes";
16
17
  import { startUsageRollup, usageRollupEnabled } from "./lib/usage/nightly";
17
18
  import { resumePhoneOutbox } from "./lib/notify";
18
19
  import { resumePushOutbox } from "./lib/notify/push";
@@ -411,6 +412,7 @@ app.prepare().then(async () => {
411
412
  if (process.env.AGENTOS_ORCHESTRATOR !== "off") startOrchestratorWatcher();
412
413
  // Schedules tick here, once a minute, and nowhere else.
413
414
  if (schedulesEnabled(process.env)) startScheduler(realDeps);
415
+ startSettling();
414
416
  resumePhoneOutbox();
415
417
  resumePushOutbox();
416
418
  // The usage ledger's daily rollup and its ratchet alerts.
@@ -0,0 +1,53 @@
1
+ import { proxy } from "valtio";
2
+ import { settingsUiActions } from "./settingsUi";
3
+
4
+ // Settings > Recipes: one workspace's, or all of them. A recipe opened from
5
+ // elsewhere (a session's menu, the palette), or a finished session to save
6
+ // as one, lands here.
7
+ export const recipesUi = proxy<{
8
+ workspaceId: string | null;
9
+ recipeId: string | null;
10
+ fromSessionId: string | null;
11
+ runRecipeId: string | null;
12
+ }>({
13
+ workspaceId: null,
14
+ recipeId: null,
15
+ fromSessionId: null,
16
+ runRecipeId: null,
17
+ });
18
+
19
+ export const recipesUiActions = {
20
+ open: (workspaceId: string | null = null) => {
21
+ Object.assign(recipesUi, {
22
+ workspaceId,
23
+ recipeId: null,
24
+ fromSessionId: null,
25
+ runRecipeId: null,
26
+ });
27
+ settingsUiActions.open("recipes");
28
+ },
29
+ openRecipe: (recipeId: string, run = false) => {
30
+ Object.assign(recipesUi, {
31
+ recipeId,
32
+ fromSessionId: null,
33
+ runRecipeId: run ? recipeId : null,
34
+ });
35
+ settingsUiActions.open("recipes");
36
+ },
37
+ saveSessionAs: (sessionId: string, workspaceId: string | null = null) => {
38
+ Object.assign(recipesUi, {
39
+ workspaceId,
40
+ recipeId: null,
41
+ fromSessionId: sessionId,
42
+ runRecipeId: null,
43
+ });
44
+ settingsUiActions.open("recipes");
45
+ },
46
+ clear: () => {
47
+ Object.assign(recipesUi, {
48
+ recipeId: null,
49
+ fromSessionId: null,
50
+ runRecipeId: null,
51
+ });
52
+ },
53
+ };