@saadnvd1/agent-os 0.3.532 → 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.
- package/README.md +71 -3
- package/app/api/devices/connect/route.ts +79 -0
- package/app/api/features/pieces/[id]/release/route.ts +16 -0
- package/app/api/features/route.ts +8 -0
- package/app/api/recipes/[id]/carry/clear/route.ts +20 -0
- package/app/api/recipes/[id]/preview/route.ts +19 -0
- package/app/api/recipes/[id]/route.ts +51 -0
- package/app/api/recipes/[id]/run/route.ts +35 -0
- package/app/api/recipes/[id]/runs/route.ts +23 -0
- package/app/api/recipes/draft/route.ts +14 -0
- package/app/api/recipes/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +34 -0
- package/components/CommandPalette/useAppCommands.ts +20 -0
- package/components/Devices/ConnectCard.tsx +2 -15
- package/components/Features/FeaturesSection.tsx +100 -0
- package/components/Features/index.ts +1 -0
- package/components/Recipes/RecipeForm.tsx +251 -0
- package/components/Recipes/RecipePage.tsx +127 -0
- package/components/Recipes/RecipesPanel.tsx +225 -0
- package/components/Recipes/RunChip.tsx +16 -0
- package/components/Recipes/RunHistory.tsx +88 -0
- package/components/Recipes/RunSheet.tsx +84 -0
- package/components/Recipes/TimerFields.tsx +193 -0
- package/components/Recipes/format.ts +51 -0
- package/components/Recipes/index.ts +1 -0
- package/components/Schedules/SchedulesPanel.tsx +1 -0
- package/components/SessionList/RowSubtitle.tsx +16 -0
- package/components/SessionList/SessionRowMenu.tsx +29 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +20 -4
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Tasks/PlanReview.tsx +253 -0
- package/components/Tasks/TaskRow.tsx +19 -44
- package/components/Tasks/TasksDialog.tsx +2 -0
- package/components/Welcome/Block.tsx +26 -0
- package/components/Welcome/ConnectBlock.tsx +169 -0
- package/components/Welcome/DevicesStep.tsx +4 -26
- package/data/devices/queries.ts +21 -1
- package/data/features/index.ts +40 -0
- package/data/push/topics.ts +8 -0
- package/data/recipes/index.ts +129 -0
- package/data/tasks/index.ts +14 -0
- package/lib/away/run.ts +4 -1
- package/lib/connect/cli.ts +6 -18
- package/lib/connect/link.ts +35 -0
- package/lib/connect/serve.ts +12 -1
- package/lib/connect/view.ts +28 -0
- package/lib/db/changes.ts +8 -0
- package/lib/db/migrations.ts +127 -0
- package/lib/db/queries.ts +4 -1
- package/lib/db/types.ts +6 -0
- package/lib/models/plan-step.ts +67 -6
- package/lib/notify/copy.ts +28 -10
- package/lib/notify/push/events.ts +36 -2
- package/lib/notify/push/policy.ts +2 -0
- package/lib/notify/push-types.ts +7 -3
- package/lib/orchestrator/brief.ts +13 -0
- package/lib/orchestrator/conditions.ts +3 -1
- package/lib/orchestrator/deliver.ts +2 -0
- package/lib/orchestrator/digest.ts +27 -1
- package/lib/orchestrator/plan-pieces.ts +41 -0
- package/lib/orchestrator/recipe-tools.ts +159 -0
- package/lib/orchestrator/review.ts +2 -1
- package/lib/orchestrator/serve.ts +25 -0
- package/lib/orchestrator/tool-names.ts +5 -0
- package/lib/orchestrator/tool-schemas.ts +54 -0
- package/lib/orchestrator/tools.ts +10 -0
- package/lib/plans/features.ts +200 -0
- package/lib/plans/pieces.ts +366 -0
- package/lib/plans/release.ts +323 -0
- package/lib/plans/view.ts +85 -0
- package/lib/recipes/api.ts +19 -0
- package/lib/recipes/chip.ts +40 -0
- package/lib/recipes/compose.ts +78 -0
- package/lib/recipes/draft.ts +93 -0
- package/lib/recipes/index.ts +54 -0
- package/lib/recipes/result.ts +59 -0
- package/lib/recipes/run.ts +202 -0
- package/lib/recipes/settle.ts +465 -0
- package/lib/recipes/store.ts +378 -0
- package/lib/recipes/test-fixtures.ts +49 -0
- package/lib/recipes/timer.ts +107 -0
- package/lib/recipes/types.ts +116 -0
- package/lib/schedules/index.ts +4 -1
- package/lib/schedules/start.ts +33 -9
- package/lib/schedules/store.ts +6 -2
- package/lib/security/demo.ts +2 -0
- package/lib/settings-url.ts +1 -0
- package/lib/tasks/loop-types.ts +23 -0
- package/lib/tasks/plan-view.ts +38 -1
- package/package.json +1 -1
- package/server.ts +2 -0
- package/stores/recipesUi.ts +53 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { FeatureView } from "@/lib/plans/view";
|
|
3
|
+
import { usePollWhenOffline } from "../push/connection";
|
|
4
|
+
|
|
5
|
+
export const featureKeys = {
|
|
6
|
+
all: ["features"] as const,
|
|
7
|
+
list: (workspaceId: string | null) =>
|
|
8
|
+
[...featureKeys.all, "list", workspaceId ?? "all"] as const,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function useFeaturesQuery(workspaceId: string | null = null) {
|
|
12
|
+
return useQuery({
|
|
13
|
+
queryKey: featureKeys.list(workspaceId),
|
|
14
|
+
queryFn: async () => {
|
|
15
|
+
const res = await fetch(
|
|
16
|
+
`/api/features${workspaceId ? `?workspace=${workspaceId}` : ""}`
|
|
17
|
+
);
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
20
|
+
return data.features as FeatureView[];
|
|
21
|
+
},
|
|
22
|
+
// Pushed when features or their pieces change.
|
|
23
|
+
refetchInterval: usePollWhenOffline(20000),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useReleasePiece() {
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
return useMutation({
|
|
30
|
+
mutationFn: async (id: number) => {
|
|
31
|
+
const res = await fetch(`/api/features/pieces/${id}/release`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
});
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
36
|
+
},
|
|
37
|
+
onSettled: () =>
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: featureKeys.all }),
|
|
39
|
+
});
|
|
40
|
+
}
|
package/data/push/topics.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { orchestratorKeys } from "../orchestrators";
|
|
|
12
12
|
import { ruleKeys } from "../orchestrators/rules";
|
|
13
13
|
import { workspaceKeys } from "../workspaces";
|
|
14
14
|
import { scheduleKeys } from "../schedules";
|
|
15
|
+
import { recipeKeys } from "../recipes";
|
|
16
|
+
import { featureKeys } from "../features";
|
|
15
17
|
import { archivedKeys } from "../done";
|
|
16
18
|
import { awayKeys } from "../away";
|
|
17
19
|
|
|
@@ -54,6 +56,12 @@ function keysFor(topic: string): QueryKey[] {
|
|
|
54
56
|
case "schedules":
|
|
55
57
|
case "schedule_runs":
|
|
56
58
|
return [scheduleKeys.all];
|
|
59
|
+
case "recipes":
|
|
60
|
+
case "recipe_runs":
|
|
61
|
+
return [recipeKeys.all];
|
|
62
|
+
case "features":
|
|
63
|
+
case "feature_pieces":
|
|
64
|
+
return [featureKeys.all, taskKeys.all, sessionKeys.all];
|
|
57
65
|
case "orchestrator_asks":
|
|
58
66
|
case "orchestrator_questions":
|
|
59
67
|
case "needs_you_dismissed":
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { RecipeInput, RecipeRun, RecipeView } from "@/lib/recipes/types";
|
|
3
|
+
import type { RecipeDraft } from "@/lib/recipes/draft";
|
|
4
|
+
import type { TimerInput } from "@/lib/recipes/timer";
|
|
5
|
+
import { sessionKeys } from "../sessions/keys";
|
|
6
|
+
import { taskKeys } from "../tasks";
|
|
7
|
+
import { usePollWhenOffline } from "../push/connection";
|
|
8
|
+
|
|
9
|
+
export const recipeKeys = {
|
|
10
|
+
all: ["recipes"] as const,
|
|
11
|
+
list: (workspaceId: string | null) =>
|
|
12
|
+
[...recipeKeys.all, "list", workspaceId ?? "all"] as const,
|
|
13
|
+
detail: (id: string) => [...recipeKeys.all, "detail", id] as const,
|
|
14
|
+
preview: (id: string, note: string) =>
|
|
15
|
+
[...recipeKeys.all, "preview", id, note] as const,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function call<T>(
|
|
19
|
+
url: string,
|
|
20
|
+
method = "GET",
|
|
21
|
+
body?: unknown
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method,
|
|
25
|
+
headers:
|
|
26
|
+
body === undefined ? undefined : { "Content-Type": "application/json" },
|
|
27
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useRecipesQuery(workspaceId: string | null, enabled = true) {
|
|
35
|
+
return useQuery({
|
|
36
|
+
queryKey: recipeKeys.list(workspaceId),
|
|
37
|
+
queryFn: async () =>
|
|
38
|
+
(
|
|
39
|
+
await call<{ recipes: RecipeView[] }>(
|
|
40
|
+
`/api/recipes${workspaceId ? `?workspace=${workspaceId}` : ""}`
|
|
41
|
+
)
|
|
42
|
+
).recipes,
|
|
43
|
+
enabled,
|
|
44
|
+
// Pushed when recipes or their runs change.
|
|
45
|
+
refetchInterval: usePollWhenOffline(15000),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useRecipeQuery(id: string | null) {
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: recipeKeys.detail(id ?? ""),
|
|
52
|
+
queryFn: () =>
|
|
53
|
+
call<{ recipe: RecipeView; runs: RecipeRun[] }>(`/api/recipes/${id}`),
|
|
54
|
+
enabled: !!id,
|
|
55
|
+
refetchInterval: usePollWhenOffline(10000),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// What a run started now would send, for the Run sheet.
|
|
60
|
+
export function usePreviewQuery(id: string, note: string, enabled: boolean) {
|
|
61
|
+
return useQuery({
|
|
62
|
+
queryKey: recipeKeys.preview(id, note),
|
|
63
|
+
queryFn: () =>
|
|
64
|
+
call<{ prompt: string }>(
|
|
65
|
+
`/api/recipes/${id}/preview${note ? `?note=${encodeURIComponent(note)}` : ""}`
|
|
66
|
+
),
|
|
67
|
+
enabled,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function useRecipeMutation<V, R>(fn: (v: V) => Promise<R>) {
|
|
72
|
+
const queryClient = useQueryClient();
|
|
73
|
+
return useMutation({
|
|
74
|
+
mutationFn: fn,
|
|
75
|
+
onSettled: () => {
|
|
76
|
+
queryClient.invalidateQueries({ queryKey: recipeKeys.all });
|
|
77
|
+
queryClient.invalidateQueries({ queryKey: taskKeys.all });
|
|
78
|
+
queryClient.invalidateQueries({ queryKey: sessionKeys.all });
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type RecipeFormInput = RecipeInput & { timer?: TimerInput | null };
|
|
84
|
+
|
|
85
|
+
export function useCreateRecipe() {
|
|
86
|
+
return useRecipeMutation((input: RecipeFormInput) =>
|
|
87
|
+
call<{ recipe: RecipeView }>("/api/recipes", "POST", input)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useUpdateRecipe() {
|
|
92
|
+
return useRecipeMutation(
|
|
93
|
+
({
|
|
94
|
+
id,
|
|
95
|
+
...patch
|
|
96
|
+
}: Partial<Omit<RecipeFormInput, "workspaceId">> & { id: string }) =>
|
|
97
|
+
call<{ recipe: RecipeView }>(`/api/recipes/${id}`, "PATCH", patch)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useArchiveRecipe() {
|
|
102
|
+
return useRecipeMutation((id: string) =>
|
|
103
|
+
call(`/api/recipes/${id}`, "DELETE")
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function useRunRecipe() {
|
|
108
|
+
return useRecipeMutation(
|
|
109
|
+
(input: { id: string; note?: string; requestId: string }) =>
|
|
110
|
+
call<{ run: RecipeRun; queued: boolean }>(
|
|
111
|
+
`/api/recipes/${input.id}/run`,
|
|
112
|
+
"POST",
|
|
113
|
+
{ note: input.note, requestId: input.requestId }
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useClearCarry() {
|
|
119
|
+
return useRecipeMutation((id: string) =>
|
|
120
|
+
call(`/api/recipes/${id}/carry/clear`, "POST", {})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function useRecipeDraft() {
|
|
125
|
+
return useMutation({
|
|
126
|
+
mutationFn: (sessionId: string) =>
|
|
127
|
+
call<RecipeDraft>("/api/recipes/draft", "POST", { sessionId }),
|
|
128
|
+
});
|
|
129
|
+
}
|
package/data/tasks/index.ts
CHANGED
|
@@ -97,6 +97,20 @@ export function useApprovePlan() {
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Which waiting pieces of a plan to fold into the one before, or leave out.
|
|
101
|
+
export function usePlanEdits() {
|
|
102
|
+
return useTaskMutation(
|
|
103
|
+
async (input: { id: string; merge: number[]; drop: number[] }) =>
|
|
104
|
+
json(
|
|
105
|
+
await fetch(`/api/tasks/${input.id}/plan-edits`, {
|
|
106
|
+
method: "PUT",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({ merge: input.merge, drop: input.drop }),
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
export function useResumeTask() {
|
|
101
115
|
return useTaskMutation(async (input: { id: string; force?: boolean }) =>
|
|
102
116
|
json(
|
package/lib/away/run.ts
CHANGED
|
@@ -286,7 +286,10 @@ async function rerunReview(
|
|
|
286
286
|
|
|
287
287
|
const SHA = /^[0-9a-f]{40}$/;
|
|
288
288
|
|
|
289
|
-
async function mergeCommit(
|
|
289
|
+
export async function mergeCommit(
|
|
290
|
+
repo: string,
|
|
291
|
+
pr: number
|
|
292
|
+
): Promise<string | null> {
|
|
290
293
|
const out = await run(
|
|
291
294
|
"gh",
|
|
292
295
|
[
|
package/lib/connect/cli.ts
CHANGED
|
@@ -21,13 +21,8 @@ import {
|
|
|
21
21
|
serverPort,
|
|
22
22
|
setConnectEnabled,
|
|
23
23
|
} from "./config";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
checkApiUrl,
|
|
27
|
-
DEFAULT_API,
|
|
28
|
-
registerWithCode,
|
|
29
|
-
requestTunnel,
|
|
30
|
-
} from "./control-api";
|
|
24
|
+
import { linkWithCode } from "./link";
|
|
25
|
+
import { checkApiUrl, requestTunnel } from "./control-api";
|
|
31
26
|
|
|
32
27
|
const arg = (flag: string) => {
|
|
33
28
|
const i = process.argv.indexOf(flag);
|
|
@@ -83,23 +78,16 @@ async function main() {
|
|
|
83
78
|
config = { ...config, hostname };
|
|
84
79
|
saveConnect(config, dir);
|
|
85
80
|
} else {
|
|
86
|
-
const apiUrl = checkApiUrl(
|
|
87
|
-
process.env.CONNECT_API_URL || DEFAULT_API,
|
|
88
|
-
operator
|
|
89
|
-
);
|
|
90
81
|
const code = await linkCode();
|
|
91
82
|
if (!code)
|
|
92
83
|
throw new Error(
|
|
93
84
|
"get a code from runagentos.com (Connect a machine), then run agent-os connect and paste it"
|
|
94
85
|
);
|
|
95
|
-
config = await
|
|
86
|
+
config = await linkWithCode({
|
|
96
87
|
dir,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
arg("--name") || os.hostname(),
|
|
101
|
-
port
|
|
102
|
-
),
|
|
88
|
+
code,
|
|
89
|
+
name: arg("--name") || os.hostname(),
|
|
90
|
+
port,
|
|
103
91
|
});
|
|
104
92
|
}
|
|
105
93
|
console.log(`\n Machine ${config.machineId}`);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linking this machine to the Connect service with a code from
|
|
3
|
+
* runagentos.com. Shared by `agent-os connect` and the setup's link form.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type ConnectConfig } from "./config";
|
|
7
|
+
import { DEFAULT_API, checkApiUrl, registerWithCode } from "./control-api";
|
|
8
|
+
import { enrol } from "./enrol";
|
|
9
|
+
|
|
10
|
+
/** The code as typed, or null when it can't be one. */
|
|
11
|
+
export function cleanLinkCode(raw: unknown): string | null {
|
|
12
|
+
if (typeof raw !== "string") return null;
|
|
13
|
+
const code = raw.trim();
|
|
14
|
+
if (!code || code.length > 128 || /[\s\u0000-\u001f\u007f-\u009f]/.test(code))
|
|
15
|
+
return null;
|
|
16
|
+
return code;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function linkWithCode(opts: {
|
|
20
|
+
code: string;
|
|
21
|
+
name: string;
|
|
22
|
+
port: number;
|
|
23
|
+
dir?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
}): Promise<ConnectConfig> {
|
|
26
|
+
const env = opts.env ?? process.env;
|
|
27
|
+
const apiUrl = checkApiUrl(
|
|
28
|
+
env.CONNECT_API_URL || DEFAULT_API,
|
|
29
|
+
env.AGENTOS_CONNECT_OPERATOR === "1"
|
|
30
|
+
);
|
|
31
|
+
return enrol({
|
|
32
|
+
dir: opts.dir,
|
|
33
|
+
register: registerWithCode(apiUrl, opts.code, opts.name, opts.port),
|
|
34
|
+
});
|
|
35
|
+
}
|
package/lib/connect/serve.ts
CHANGED
|
@@ -195,7 +195,18 @@ export function startConnect(
|
|
|
195
195
|
? "AGENTOS_CONNECT_PORT isn't a usable port"
|
|
196
196
|
: decision.reason;
|
|
197
197
|
if (readConnect(dir).ok) log(`connect: not starting: ${reason}`);
|
|
198
|
-
|
|
198
|
+
// Still says whether the machine is linked, read when asked.
|
|
199
|
+
g.__agentosConnect = () => {
|
|
200
|
+
const r = readConnect(dir);
|
|
201
|
+
return {
|
|
202
|
+
state: "refused",
|
|
203
|
+
reason,
|
|
204
|
+
...(r.ok && {
|
|
205
|
+
enrolled: true,
|
|
206
|
+
hostname: r.files.config.hostname || undefined,
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
209
|
+
};
|
|
199
210
|
return { hostname: () => null as string | null, close: () => {} };
|
|
200
211
|
}
|
|
201
212
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConnectStatus } from "./serve";
|
|
2
|
+
|
|
3
|
+
export const CONNECT_SITE = "https://runagentos.com";
|
|
4
|
+
|
|
5
|
+
export const CONNECT_STATE: Record<ConnectStatus["state"], string> = {
|
|
6
|
+
up: "Connected",
|
|
7
|
+
connecting: "Connecting…",
|
|
8
|
+
denied: "Refused by Connect",
|
|
9
|
+
off: "Off",
|
|
10
|
+
refused: "Not running here",
|
|
11
|
+
error: "Problem",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** What the UI needs to know about Connect on this machine. */
|
|
15
|
+
export function connectView(s: ConnectStatus) {
|
|
16
|
+
const linked = !!s.enrolled || !!s.hostname;
|
|
17
|
+
const on =
|
|
18
|
+
s.state === "up" ||
|
|
19
|
+
s.state === "connecting" ||
|
|
20
|
+
s.state === "denied" ||
|
|
21
|
+
(s.state === "error" && !!s.enrolled);
|
|
22
|
+
return {
|
|
23
|
+
linked,
|
|
24
|
+
on,
|
|
25
|
+
canToggle: linked && s.state !== "refused",
|
|
26
|
+
hostname: s.hostname,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/lib/db/changes.ts
CHANGED
|
@@ -34,6 +34,13 @@ export const TABLES_WATCHED_78 = [
|
|
|
34
34
|
"needs_you_dismissed",
|
|
35
35
|
] as const;
|
|
36
36
|
|
|
37
|
+
export const TABLES_WATCHED_80 = [
|
|
38
|
+
"recipes",
|
|
39
|
+
"recipe_runs",
|
|
40
|
+
"features",
|
|
41
|
+
"feature_pieces",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
37
44
|
// Every table any migration watches, for the browser's topic map to cover.
|
|
38
45
|
export const WATCHED_TABLES: readonly string[] = [
|
|
39
46
|
...TABLES_WATCHED_45,
|
|
@@ -41,6 +48,7 @@ export const WATCHED_TABLES: readonly string[] = [
|
|
|
41
48
|
...TABLES_WATCHED_56,
|
|
42
49
|
...TABLES_WATCHED_58,
|
|
43
50
|
...TABLES_WATCHED_78,
|
|
51
|
+
...TABLES_WATCHED_80,
|
|
44
52
|
];
|
|
45
53
|
|
|
46
54
|
export function installChangeTriggers(
|
package/lib/db/migrations.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TABLES_WATCHED_56,
|
|
6
6
|
TABLES_WATCHED_58,
|
|
7
7
|
TABLES_WATCHED_78,
|
|
8
|
+
TABLES_WATCHED_80,
|
|
8
9
|
} from "./changes";
|
|
9
10
|
import type Database from "better-sqlite3";
|
|
10
11
|
|
|
@@ -1830,6 +1831,132 @@ const migrations: Migration[] = [
|
|
|
1830
1831
|
if (!has) db.exec(`ALTER TABLE task_queue ADD COLUMN agent_type TEXT`);
|
|
1831
1832
|
},
|
|
1832
1833
|
},
|
|
1834
|
+
{
|
|
1835
|
+
id: 80,
|
|
1836
|
+
name: "recipes",
|
|
1837
|
+
up: (db) => {
|
|
1838
|
+
// Recipes: a saved brief that starts a fresh task or session each run
|
|
1839
|
+
// (lib/recipes), and the features a plan splits into pieces
|
|
1840
|
+
// (lib/plans). All additive. A run or piece finds its session by
|
|
1841
|
+
// session_id, so sessions gains no column.
|
|
1842
|
+
db.exec(`
|
|
1843
|
+
CREATE TABLE IF NOT EXISTS recipes (
|
|
1844
|
+
id TEXT PRIMARY KEY,
|
|
1845
|
+
workspace_id TEXT NOT NULL,
|
|
1846
|
+
project_id TEXT NOT NULL,
|
|
1847
|
+
name TEXT NOT NULL,
|
|
1848
|
+
kind TEXT NOT NULL,
|
|
1849
|
+
brief TEXT NOT NULL,
|
|
1850
|
+
model TEXT,
|
|
1851
|
+
view TEXT NOT NULL DEFAULT 'chat',
|
|
1852
|
+
auto_done INTEGER NOT NULL DEFAULT 1,
|
|
1853
|
+
max_minutes INTEGER,
|
|
1854
|
+
carry_cleared_at INTEGER,
|
|
1855
|
+
source_session_id TEXT,
|
|
1856
|
+
created_by_session_id TEXT,
|
|
1857
|
+
updated_by_session_id TEXT,
|
|
1858
|
+
archived_at TEXT,
|
|
1859
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1860
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1861
|
+
);
|
|
1862
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_recipes_name
|
|
1863
|
+
ON recipes(workspace_id, name COLLATE NOCASE) WHERE archived_at IS NULL;
|
|
1864
|
+
|
|
1865
|
+
CREATE TABLE IF NOT EXISTS recipe_runs (
|
|
1866
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1867
|
+
recipe_id TEXT NOT NULL,
|
|
1868
|
+
seq INTEGER NOT NULL,
|
|
1869
|
+
trigger TEXT NOT NULL,
|
|
1870
|
+
triggered_by_session_id TEXT,
|
|
1871
|
+
schedule_run_id INTEGER,
|
|
1872
|
+
request_id TEXT,
|
|
1873
|
+
note TEXT,
|
|
1874
|
+
prompt TEXT NOT NULL,
|
|
1875
|
+
revision_id INTEGER,
|
|
1876
|
+
status TEXT NOT NULL,
|
|
1877
|
+
session_id TEXT,
|
|
1878
|
+
pr_number INTEGER,
|
|
1879
|
+
pr_url TEXT,
|
|
1880
|
+
summary TEXT,
|
|
1881
|
+
outputs TEXT,
|
|
1882
|
+
summary_source TEXT,
|
|
1883
|
+
error TEXT,
|
|
1884
|
+
cost_usd REAL,
|
|
1885
|
+
duration_ms INTEGER,
|
|
1886
|
+
started_at TEXT,
|
|
1887
|
+
ended_at TEXT,
|
|
1888
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1889
|
+
UNIQUE (recipe_id, seq)
|
|
1890
|
+
);
|
|
1891
|
+
CREATE INDEX IF NOT EXISTS idx_recipe_runs_recipe
|
|
1892
|
+
ON recipe_runs(recipe_id, id DESC);
|
|
1893
|
+
CREATE INDEX IF NOT EXISTS idx_recipe_runs_session
|
|
1894
|
+
ON recipe_runs(session_id);
|
|
1895
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_recipe_runs_one_live
|
|
1896
|
+
ON recipe_runs(recipe_id)
|
|
1897
|
+
WHERE status IN ('queued','starting','running');
|
|
1898
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_recipe_runs_request
|
|
1899
|
+
ON recipe_runs(request_id) WHERE request_id IS NOT NULL;
|
|
1900
|
+
|
|
1901
|
+
CREATE TABLE IF NOT EXISTS recipe_revisions (
|
|
1902
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1903
|
+
recipe_id TEXT NOT NULL,
|
|
1904
|
+
brief TEXT NOT NULL,
|
|
1905
|
+
settings TEXT NOT NULL,
|
|
1906
|
+
edited_by_session_id TEXT,
|
|
1907
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1908
|
+
);
|
|
1909
|
+
|
|
1910
|
+
CREATE TABLE IF NOT EXISTS features (
|
|
1911
|
+
id TEXT PRIMARY KEY,
|
|
1912
|
+
workspace_id TEXT NOT NULL,
|
|
1913
|
+
name TEXT NOT NULL,
|
|
1914
|
+
plan_session_id TEXT NOT NULL,
|
|
1915
|
+
plan_text TEXT NOT NULL,
|
|
1916
|
+
status TEXT NOT NULL,
|
|
1917
|
+
done_at TEXT,
|
|
1918
|
+
cost_usd REAL,
|
|
1919
|
+
duration_ms INTEGER,
|
|
1920
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1921
|
+
);
|
|
1922
|
+
CREATE INDEX IF NOT EXISTS idx_features_plan
|
|
1923
|
+
ON features(plan_session_id);
|
|
1924
|
+
|
|
1925
|
+
CREATE TABLE IF NOT EXISTS feature_pieces (
|
|
1926
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1927
|
+
feature_id TEXT NOT NULL,
|
|
1928
|
+
position INTEGER NOT NULL,
|
|
1929
|
+
name TEXT NOT NULL,
|
|
1930
|
+
slice TEXT NOT NULL,
|
|
1931
|
+
project_id TEXT NOT NULL,
|
|
1932
|
+
after_position INTEGER,
|
|
1933
|
+
after_kind TEXT,
|
|
1934
|
+
reasons TEXT,
|
|
1935
|
+
owns TEXT,
|
|
1936
|
+
status TEXT NOT NULL,
|
|
1937
|
+
session_id TEXT,
|
|
1938
|
+
note TEXT,
|
|
1939
|
+
UNIQUE (feature_id, position)
|
|
1940
|
+
);
|
|
1941
|
+
CREATE INDEX IF NOT EXISTS idx_feature_pieces_session
|
|
1942
|
+
ON feature_pieces(session_id);
|
|
1943
|
+
`);
|
|
1944
|
+
const cols = (
|
|
1945
|
+
db.prepare(`PRAGMA table_info(schedules)`).all() as { name: string }[]
|
|
1946
|
+
).map((c) => c.name);
|
|
1947
|
+
if (!cols.includes("recipe_id"))
|
|
1948
|
+
db.exec(`ALTER TABLE schedules ADD COLUMN recipe_id TEXT`);
|
|
1949
|
+
if (!cols.includes("migrated_from_kind"))
|
|
1950
|
+
db.exec(`ALTER TABLE schedules ADD COLUMN migrated_from_kind TEXT`);
|
|
1951
|
+
const plan = (
|
|
1952
|
+
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
1953
|
+
).map((c) => c.name);
|
|
1954
|
+
// Edits Saad made to a plan's pieces before approving it (merge, drop).
|
|
1955
|
+
if (!plan.includes("plan_edits"))
|
|
1956
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN plan_edits TEXT`);
|
|
1957
|
+
installChangeTriggers(db, TABLES_WATCHED_80);
|
|
1958
|
+
},
|
|
1959
|
+
},
|
|
1833
1960
|
];
|
|
1834
1961
|
|
|
1835
1962
|
// `upTo`: stop after this id (tests that start from an older database).
|
package/lib/db/queries.ts
CHANGED
|
@@ -32,7 +32,10 @@ export function sessionListColumns(db: Database.Database, alias = ""): string {
|
|
|
32
32
|
.split(",")
|
|
33
33
|
.map((c) => `${prefix}${c}`)
|
|
34
34
|
.join(", ");
|
|
35
|
-
|
|
35
|
+
// The recipe run or feature piece a session is, as JSON, for its row's chip.
|
|
36
|
+
const recipe = `(SELECT json_object('id', r.id, 'name', r.name, 'seq', rr.seq) FROM recipe_runs rr JOIN recipes r ON r.id = rr.recipe_id WHERE rr.session_id = ${alias || "sessions"}.id)`;
|
|
37
|
+
const piece = `(SELECT json_object('feature', f.id, 'name', f.name, 'position', fp.position, 'total', (SELECT COUNT(*) FROM feature_pieces WHERE feature_id = f.id AND status != 'dropped')) FROM feature_pieces fp JOIN features f ON f.id = fp.feature_id WHERE fp.session_id = ${alias || "sessions"}.id)`;
|
|
38
|
+
return `${list}, (${prefix}task_brief IS NOT NULL AND ${prefix}task_brief != '') AS has_brief, ${recipe} AS recipe_run, ${piece} AS feature_piece`;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export const queries = {
|
package/lib/db/types.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface Session {
|
|
|
26
26
|
started_by?: string | null;
|
|
27
27
|
// 1 planned, 0 no plan, null not stated; why, in the starter's words.
|
|
28
28
|
plan_decided?: number | null;
|
|
29
|
+
// JSON: the pieces Saad merged or dropped before approving the plan.
|
|
30
|
+
plan_edits?: string | null;
|
|
29
31
|
plan_reason?: string | null;
|
|
30
32
|
system_prompt: string | null;
|
|
31
33
|
group_path: string; // Deprecated - use project_id
|
|
@@ -55,6 +57,10 @@ export interface Session {
|
|
|
55
57
|
task_brief?: string | null;
|
|
56
58
|
// Lists carry this instead of the brief itself (0/1).
|
|
57
59
|
has_brief?: number;
|
|
60
|
+
// JSON, in lists: the recipe run this session is ({id, name, seq}), and the
|
|
61
|
+
// feature piece it builds ({feature, name, position, total}).
|
|
62
|
+
recipe_run?: string | null;
|
|
63
|
+
feature_piece?: string | null;
|
|
58
64
|
// How the session is shown: a chat conversation or a terminal.
|
|
59
65
|
view: "chat" | "terminal";
|
|
60
66
|
chat_access: ChatAccess;
|