@webgrow/skillhub 0.1.0
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 +114 -0
- package/bridge/.env.example +14 -0
- package/bridge/README.md +74 -0
- package/bridge/adapters.mjs +209 -0
- package/bridge/convex-functions.mjs +13 -0
- package/bridge/doctor.mjs +448 -0
- package/bridge/harness.mjs +217 -0
- package/convex/_generated/ai/ai-files.state.json +6 -0
- package/convex/_generated/ai/guidelines.md +368 -0
- package/convex/_generated/api.d.ts +59 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/bridge.ts +709 -0
- package/convex/convex.config.ts +8 -0
- package/convex/http.ts +37 -0
- package/convex/loops.ts +546 -0
- package/convex/runs.ts +183 -0
- package/convex/schema.ts +220 -0
- package/convex/skills.ts +413 -0
- package/convex/tsconfig.json +25 -0
- package/dashboard/.env.example +1 -0
- package/dashboard/index.html +12 -0
- package/dashboard/src/App.jsx +1743 -0
- package/dashboard/src/convexRefs.js +32 -0
- package/dashboard/src/main.jsx +46 -0
- package/dashboard/src/styles.css +982 -0
- package/dashboard/tsconfig.json +17 -0
- package/dashboard/vite.config.mjs +23 -0
- package/package.json +48 -0
- package/src/bridge-command.mjs +101 -0
- package/src/cli.mjs +246 -0
- package/src/cloud-catalog.mjs +588 -0
- package/src/config.mjs +150 -0
- package/src/connect.mjs +410 -0
package/convex/http.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { httpRouter } from "convex/server";
|
|
2
|
+
import { api } from "./_generated/api";
|
|
3
|
+
import { httpAction } from "./_generated/server";
|
|
4
|
+
|
|
5
|
+
const http = httpRouter();
|
|
6
|
+
|
|
7
|
+
function json(data: unknown, status = 200) {
|
|
8
|
+
return new Response(JSON.stringify(data), {
|
|
9
|
+
status,
|
|
10
|
+
headers: {
|
|
11
|
+
"content-type": "application/json; charset=utf-8",
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
http.route({
|
|
17
|
+
path: "/bridge/health",
|
|
18
|
+
method: "GET",
|
|
19
|
+
handler: httpAction(async () =>
|
|
20
|
+
json({
|
|
21
|
+
ok: true,
|
|
22
|
+
service: "skillhub-convex-bridge",
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
http.route({
|
|
28
|
+
path: "/bridge/log",
|
|
29
|
+
method: "POST",
|
|
30
|
+
handler: httpAction(async (ctx, request) => {
|
|
31
|
+
const body = await request.json();
|
|
32
|
+
const logId = await ctx.runMutation(api.bridge.emitLog, body);
|
|
33
|
+
return json({ ok: true, logId });
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export default http;
|
package/convex/loops.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
const defaultSteps = [
|
|
5
|
+
{
|
|
6
|
+
id: "step-1",
|
|
7
|
+
label: "Plan the work",
|
|
8
|
+
actionType: "codex.cli",
|
|
9
|
+
skillSlug: "",
|
|
10
|
+
instructions: "Read the objective, identify risks, and produce a short plan.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "step-2",
|
|
14
|
+
label: "Execute the work",
|
|
15
|
+
actionType: "codex.cli",
|
|
16
|
+
skillSlug: "",
|
|
17
|
+
instructions: "Carry out the plan, report changes, verification, and blockers.",
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const list = query({
|
|
22
|
+
args: {
|
|
23
|
+
limit: v.optional(v.number()),
|
|
24
|
+
},
|
|
25
|
+
handler: async (ctx, args) => {
|
|
26
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
27
|
+
const loops = await ctx.db
|
|
28
|
+
.query("loops")
|
|
29
|
+
.withIndex("by_updatedAt")
|
|
30
|
+
.order("desc")
|
|
31
|
+
.take(limit);
|
|
32
|
+
|
|
33
|
+
return await Promise.all(
|
|
34
|
+
loops.map(async (loop) => ({
|
|
35
|
+
loop,
|
|
36
|
+
latestVersion: loop.latestVersionId
|
|
37
|
+
? await ctx.db.get(loop.latestVersionId)
|
|
38
|
+
: null,
|
|
39
|
+
draftVersion: loop.draftVersionId
|
|
40
|
+
? await ctx.db.get(loop.draftVersionId)
|
|
41
|
+
: null,
|
|
42
|
+
})),
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const get = query({
|
|
48
|
+
args: {
|
|
49
|
+
loopId: v.id("loops"),
|
|
50
|
+
},
|
|
51
|
+
handler: async (ctx, args) => {
|
|
52
|
+
const loop = await ctx.db.get(args.loopId);
|
|
53
|
+
if (!loop) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const versions = await ctx.db
|
|
58
|
+
.query("loopVersions")
|
|
59
|
+
.withIndex("by_loop_version", (q) => q.eq("loopId", args.loopId))
|
|
60
|
+
.order("desc")
|
|
61
|
+
.take(30);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
loop,
|
|
65
|
+
versions,
|
|
66
|
+
latestVersion: loop.latestVersionId
|
|
67
|
+
? await ctx.db.get(loop.latestVersionId)
|
|
68
|
+
: null,
|
|
69
|
+
draftVersion: loop.draftVersionId
|
|
70
|
+
? await ctx.db.get(loop.draftVersionId)
|
|
71
|
+
: null,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const createDraft = mutation({
|
|
77
|
+
args: {
|
|
78
|
+
name: v.string(),
|
|
79
|
+
description: v.optional(v.string()),
|
|
80
|
+
objective: v.optional(v.string()),
|
|
81
|
+
trigger: v.optional(v.string()),
|
|
82
|
+
tags: v.optional(v.array(v.string())),
|
|
83
|
+
primarySkill: v.optional(v.string()),
|
|
84
|
+
steps: v.optional(v.array(v.any())),
|
|
85
|
+
},
|
|
86
|
+
handler: async (ctx, args) => {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const name = requireText(args.name, "Loop name");
|
|
89
|
+
const slug = await uniqueSlug(ctx, slugify(name));
|
|
90
|
+
const steps = normalizeSteps(args.steps ?? defaultSteps);
|
|
91
|
+
const manifest = buildManifest({
|
|
92
|
+
name,
|
|
93
|
+
description: cleanOptional(args.description),
|
|
94
|
+
objective: cleanOptional(args.objective) || `Run ${name}`,
|
|
95
|
+
trigger: cleanOptional(args.trigger) || "manual",
|
|
96
|
+
primarySkill: cleanOptional(args.primarySkill),
|
|
97
|
+
tags: normalizeTags(args.tags ?? []),
|
|
98
|
+
steps,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const loopId = await ctx.db.insert("loops", {
|
|
102
|
+
slug,
|
|
103
|
+
name,
|
|
104
|
+
description: cleanOptional(args.description),
|
|
105
|
+
primarySkill: cleanOptional(args.primarySkill),
|
|
106
|
+
tags: normalizeTags(args.tags ?? []),
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const draftVersionId = await ctx.db.insert("loopVersions", {
|
|
112
|
+
loopId,
|
|
113
|
+
version: "draft",
|
|
114
|
+
status: "draft",
|
|
115
|
+
manifest,
|
|
116
|
+
contentHash: contentHash(manifest),
|
|
117
|
+
createdAt: now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await ctx.db.patch(loopId, {
|
|
122
|
+
draftVersionId,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return loopId;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const saveDraft = mutation({
|
|
131
|
+
args: {
|
|
132
|
+
loopId: v.id("loops"),
|
|
133
|
+
name: v.string(),
|
|
134
|
+
description: v.optional(v.string()),
|
|
135
|
+
objective: v.string(),
|
|
136
|
+
trigger: v.optional(v.string()),
|
|
137
|
+
tags: v.optional(v.array(v.string())),
|
|
138
|
+
primarySkill: v.optional(v.string()),
|
|
139
|
+
steps: v.array(v.any()),
|
|
140
|
+
},
|
|
141
|
+
handler: async (ctx, args) => {
|
|
142
|
+
const loop = await requireLoop(ctx, args.loopId);
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const name = requireText(args.name, "Loop name");
|
|
145
|
+
const steps = normalizeSteps(args.steps);
|
|
146
|
+
const tags = normalizeTags(args.tags ?? []);
|
|
147
|
+
const manifest = buildManifest({
|
|
148
|
+
name,
|
|
149
|
+
description: cleanOptional(args.description),
|
|
150
|
+
objective: requireText(args.objective, "Objective"),
|
|
151
|
+
trigger: cleanOptional(args.trigger) || "manual",
|
|
152
|
+
primarySkill: cleanOptional(args.primarySkill),
|
|
153
|
+
tags,
|
|
154
|
+
steps,
|
|
155
|
+
});
|
|
156
|
+
const draftVersionId = await ensureDraftVersion(ctx, loop, now);
|
|
157
|
+
const loopPatch: any = {
|
|
158
|
+
name,
|
|
159
|
+
description: cleanOptional(args.description),
|
|
160
|
+
primarySkill: cleanOptional(args.primarySkill),
|
|
161
|
+
tags,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!loop.latestVersionId || loop.slug.startsWith("untitled-loop")) {
|
|
166
|
+
loopPatch.slug = await uniqueSlugForLoop(ctx, slugify(name), args.loopId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await ctx.db.patch(args.loopId, loopPatch);
|
|
170
|
+
await ctx.db.patch(draftVersionId, {
|
|
171
|
+
manifest,
|
|
172
|
+
contentHash: contentHash(manifest),
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return draftVersionId;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const publishDraft = mutation({
|
|
181
|
+
args: {
|
|
182
|
+
loopId: v.id("loops"),
|
|
183
|
+
},
|
|
184
|
+
handler: async (ctx, args) => {
|
|
185
|
+
const loop = await requireLoop(ctx, args.loopId);
|
|
186
|
+
if (!loop.draftVersionId) {
|
|
187
|
+
throw new Error("No draft exists to publish.");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const draft = await ctx.db.get(loop.draftVersionId);
|
|
191
|
+
if (!draft) {
|
|
192
|
+
throw new Error("Draft version not found.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
validateManifest(draft.manifest);
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const version = await nextPublishedVersion(ctx, args.loopId);
|
|
198
|
+
|
|
199
|
+
await ctx.db.patch(draft._id, {
|
|
200
|
+
version,
|
|
201
|
+
status: "published",
|
|
202
|
+
publishedAt: now,
|
|
203
|
+
updatedAt: now,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await ctx.db.patch(args.loopId, {
|
|
207
|
+
latestVersionId: draft._id,
|
|
208
|
+
draftVersionId: undefined,
|
|
209
|
+
updatedAt: now,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return draft._id;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export const run = mutation({
|
|
217
|
+
args: {
|
|
218
|
+
loopId: v.id("loops"),
|
|
219
|
+
promptOverride: v.optional(v.string()),
|
|
220
|
+
},
|
|
221
|
+
handler: async (ctx, args) => {
|
|
222
|
+
const loop = await requireLoop(ctx, args.loopId);
|
|
223
|
+
const versionId = loop.latestVersionId ?? loop.draftVersionId;
|
|
224
|
+
if (!versionId) {
|
|
225
|
+
throw new Error("Save a loop draft before running it.");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const version = await ctx.db.get(versionId);
|
|
229
|
+
if (!version) {
|
|
230
|
+
throw new Error("Loop version not found.");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
validateManifest(version.manifest);
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
const steps = version.manifest.steps;
|
|
236
|
+
const firstStep = steps[0];
|
|
237
|
+
const prompt = cleanOptional(args.promptOverride) || version.manifest.objective;
|
|
238
|
+
|
|
239
|
+
const runId = await ctx.db.insert("loopRuns", {
|
|
240
|
+
loopSlug: loop.slug,
|
|
241
|
+
loopVersionId: version._id,
|
|
242
|
+
status: "running",
|
|
243
|
+
activeNodeId: firstStep.id,
|
|
244
|
+
prompt,
|
|
245
|
+
graph: version.manifest.graph,
|
|
246
|
+
controls: {
|
|
247
|
+
source: "loop-authoring",
|
|
248
|
+
mode: "loop",
|
|
249
|
+
loopId: loop._id,
|
|
250
|
+
loopVersionId: version._id,
|
|
251
|
+
},
|
|
252
|
+
context: {
|
|
253
|
+
loop: {
|
|
254
|
+
name: loop.name,
|
|
255
|
+
slug: loop.slug,
|
|
256
|
+
version: version.version,
|
|
257
|
+
status: version.status,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
transitionCount: 0,
|
|
261
|
+
startedAt: now,
|
|
262
|
+
updatedAt: now,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await ctx.db.insert("runEvents", {
|
|
266
|
+
runId,
|
|
267
|
+
sequence: 1,
|
|
268
|
+
eventType: "loop.started",
|
|
269
|
+
level: "info",
|
|
270
|
+
nodeId: firstStep.id,
|
|
271
|
+
message: `Loop started: ${loop.name}`,
|
|
272
|
+
payload: {
|
|
273
|
+
loopId: loop._id,
|
|
274
|
+
loopVersionId: version._id,
|
|
275
|
+
steps: steps.length,
|
|
276
|
+
prompt,
|
|
277
|
+
},
|
|
278
|
+
createdAt: now,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const actionId = await enqueueLoopStep(ctx, {
|
|
282
|
+
runId,
|
|
283
|
+
loop,
|
|
284
|
+
version,
|
|
285
|
+
prompt,
|
|
286
|
+
steps,
|
|
287
|
+
stepIndex: 0,
|
|
288
|
+
now,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return { runId, actionId };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
async function enqueueLoopStep(ctx: any, { runId, loop, version, prompt, steps, stepIndex, now }: any) {
|
|
296
|
+
const step = steps[stepIndex];
|
|
297
|
+
const nextStep = steps[stepIndex + 1] ?? null;
|
|
298
|
+
const actionId = await ctx.db.insert("hostActions", {
|
|
299
|
+
runId,
|
|
300
|
+
loopSlug: loop.slug,
|
|
301
|
+
nodeId: step.id,
|
|
302
|
+
actionType: step.actionType || "codex.cli",
|
|
303
|
+
title: `${loop.name}: ${step.label}`,
|
|
304
|
+
status: "pending",
|
|
305
|
+
idempotencyKey: `loop-step:${runId}:${step.id}`,
|
|
306
|
+
payload: {
|
|
307
|
+
prompt: renderStepPrompt({ loop, version, prompt, step, stepIndex, steps }),
|
|
308
|
+
loopStep: {
|
|
309
|
+
loopId: loop._id,
|
|
310
|
+
loopVersionId: version._id,
|
|
311
|
+
stepIndex,
|
|
312
|
+
totalSteps: steps.length,
|
|
313
|
+
steps,
|
|
314
|
+
nextStep,
|
|
315
|
+
},
|
|
316
|
+
loop: {
|
|
317
|
+
name: loop.name,
|
|
318
|
+
slug: loop.slug,
|
|
319
|
+
version: version.version,
|
|
320
|
+
},
|
|
321
|
+
step,
|
|
322
|
+
},
|
|
323
|
+
codexTool: {
|
|
324
|
+
adapter: "codex-cli",
|
|
325
|
+
safeDefault: "dry-run",
|
|
326
|
+
},
|
|
327
|
+
createdAt: now,
|
|
328
|
+
updatedAt: now,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await ctx.db.insert("runEvents", {
|
|
332
|
+
runId,
|
|
333
|
+
sequence: stepIndex + 2,
|
|
334
|
+
eventType: "loop.step.enqueued",
|
|
335
|
+
level: "info",
|
|
336
|
+
nodeId: step.id,
|
|
337
|
+
message: `Queued step ${stepIndex + 1}: ${step.label}`,
|
|
338
|
+
payload: {
|
|
339
|
+
actionId,
|
|
340
|
+
stepIndex,
|
|
341
|
+
totalSteps: steps.length,
|
|
342
|
+
},
|
|
343
|
+
createdAt: now,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return actionId;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function requireLoop(ctx: any, loopId: any) {
|
|
350
|
+
const loop = await ctx.db.get(loopId);
|
|
351
|
+
if (!loop) {
|
|
352
|
+
throw new Error(`Loop not found: ${loopId}`);
|
|
353
|
+
}
|
|
354
|
+
return loop;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function ensureDraftVersion(ctx: any, loop: any, now: number) {
|
|
358
|
+
if (loop.draftVersionId) {
|
|
359
|
+
return loop.draftVersionId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const latest = loop.latestVersionId ? await ctx.db.get(loop.latestVersionId) : null;
|
|
363
|
+
const manifest = latest?.manifest ?? buildManifest({
|
|
364
|
+
name: loop.name,
|
|
365
|
+
description: loop.description,
|
|
366
|
+
objective: `Run ${loop.name}`,
|
|
367
|
+
trigger: "manual",
|
|
368
|
+
primarySkill: loop.primarySkill,
|
|
369
|
+
tags: loop.tags,
|
|
370
|
+
steps: defaultSteps,
|
|
371
|
+
});
|
|
372
|
+
const draftVersionId = await ctx.db.insert("loopVersions", {
|
|
373
|
+
loopId: loop._id,
|
|
374
|
+
version: "draft",
|
|
375
|
+
status: "draft",
|
|
376
|
+
manifest,
|
|
377
|
+
contentHash: contentHash(manifest),
|
|
378
|
+
createdAt: now,
|
|
379
|
+
updatedAt: now,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await ctx.db.patch(loop._id, {
|
|
383
|
+
draftVersionId,
|
|
384
|
+
updatedAt: now,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return draftVersionId;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function nextPublishedVersion(ctx: any, loopId: any) {
|
|
391
|
+
const published = await ctx.db
|
|
392
|
+
.query("loopVersions")
|
|
393
|
+
.withIndex("by_loop_status", (q: any) =>
|
|
394
|
+
q.eq("loopId", loopId).eq("status", "published"),
|
|
395
|
+
)
|
|
396
|
+
.collect();
|
|
397
|
+
|
|
398
|
+
return `v${published.length + 1}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function uniqueSlug(ctx: any, base: string) {
|
|
402
|
+
const existing = await ctx.db
|
|
403
|
+
.query("loops")
|
|
404
|
+
.withIndex("by_slug", (q: any) => q.eq("slug", base))
|
|
405
|
+
.unique();
|
|
406
|
+
|
|
407
|
+
if (!existing) {
|
|
408
|
+
return base;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return `${base}-${Date.now().toString(36)}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function uniqueSlugForLoop(ctx: any, base: string, loopId: any) {
|
|
415
|
+
const existing = await ctx.db
|
|
416
|
+
.query("loops")
|
|
417
|
+
.withIndex("by_slug", (q: any) => q.eq("slug", base))
|
|
418
|
+
.unique();
|
|
419
|
+
|
|
420
|
+
if (!existing || existing._id === loopId) {
|
|
421
|
+
return base;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return `${base}-${Date.now().toString(36)}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function buildManifest({ name, description, objective, trigger, primarySkill, tags, steps }: any) {
|
|
428
|
+
const normalizedSteps = normalizeSteps(steps);
|
|
429
|
+
return {
|
|
430
|
+
name,
|
|
431
|
+
description,
|
|
432
|
+
objective,
|
|
433
|
+
trigger,
|
|
434
|
+
primarySkill,
|
|
435
|
+
tags,
|
|
436
|
+
steps: normalizedSteps,
|
|
437
|
+
graph: linearGraphFromSteps(normalizedSteps),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function validateManifest(manifest: any) {
|
|
442
|
+
if (!manifest?.steps?.length) {
|
|
443
|
+
throw new Error("Loop needs at least one step.");
|
|
444
|
+
}
|
|
445
|
+
if (!cleanOptional(manifest.objective)) {
|
|
446
|
+
throw new Error("Loop objective is required.");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function linearGraphFromSteps(steps: any[]) {
|
|
451
|
+
return {
|
|
452
|
+
layout: "linear",
|
|
453
|
+
entryNodeId: steps[0]?.id ?? null,
|
|
454
|
+
nodes: steps.map((step, index) => ({
|
|
455
|
+
id: step.id,
|
|
456
|
+
type: step.actionType || "codex.cli",
|
|
457
|
+
label: step.label,
|
|
458
|
+
prompt: step.instructions,
|
|
459
|
+
skillRefs: step.skillSlug ? [step.skillSlug] : [],
|
|
460
|
+
x: 120 + index * 260,
|
|
461
|
+
y: 120,
|
|
462
|
+
})),
|
|
463
|
+
edges: steps.slice(0, -1).map((step, index) => ({
|
|
464
|
+
id: `edge-${index + 1}`,
|
|
465
|
+
from: step.id,
|
|
466
|
+
to: steps[index + 1].id,
|
|
467
|
+
condition: "completed",
|
|
468
|
+
})),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function normalizeSteps(steps: any[]) {
|
|
473
|
+
const normalized = steps
|
|
474
|
+
.map((step, index) => ({
|
|
475
|
+
id: cleanOptional(step.id) || `step-${index + 1}`,
|
|
476
|
+
label: cleanOptional(step.label) || `Step ${index + 1}`,
|
|
477
|
+
actionType: cleanOptional(step.actionType) || "codex.cli",
|
|
478
|
+
skillSlug: cleanOptional(step.skillSlug) || "",
|
|
479
|
+
instructions: cleanOptional(step.instructions) || `Complete step ${index + 1}.`,
|
|
480
|
+
}))
|
|
481
|
+
.slice(0, 12);
|
|
482
|
+
|
|
483
|
+
if (!normalized.length) {
|
|
484
|
+
throw new Error("Loop needs at least one step.");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return normalized.map((step, index) => ({
|
|
488
|
+
...step,
|
|
489
|
+
id: slugify(step.id) || `step-${index + 1}`,
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function renderStepPrompt({ loop, version, prompt, step, stepIndex, steps }: any) {
|
|
494
|
+
return [
|
|
495
|
+
`Loop: ${loop.name}`,
|
|
496
|
+
`Version: ${version.version}`,
|
|
497
|
+
`Step ${stepIndex + 1} of ${steps.length}: ${step.label}`,
|
|
498
|
+
step.skillSlug ? `Skill reference: ${step.skillSlug}` : null,
|
|
499
|
+
"",
|
|
500
|
+
"Loop objective:",
|
|
501
|
+
prompt,
|
|
502
|
+
"",
|
|
503
|
+
"Step instructions:",
|
|
504
|
+
step.instructions,
|
|
505
|
+
"",
|
|
506
|
+
"All loop steps:",
|
|
507
|
+
...steps.map((item: any, index: number) => `${index + 1}. ${item.label}`),
|
|
508
|
+
]
|
|
509
|
+
.filter(Boolean)
|
|
510
|
+
.join("\n");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function contentHash(manifest: any) {
|
|
514
|
+
const text = JSON.stringify(manifest);
|
|
515
|
+
let hash = 0;
|
|
516
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
517
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
518
|
+
}
|
|
519
|
+
return hash.toString(36);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function slugify(value: string) {
|
|
523
|
+
const slug = value
|
|
524
|
+
.toLowerCase()
|
|
525
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
526
|
+
.replace(/^-+|-+$/g, "");
|
|
527
|
+
|
|
528
|
+
return slug || "loop";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function normalizeTags(tags: string[]) {
|
|
532
|
+
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))].slice(0, 12);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function cleanOptional(value?: string) {
|
|
536
|
+
const cleaned = value?.trim();
|
|
537
|
+
return cleaned ? cleaned : undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function requireText(value: string | undefined, label: string) {
|
|
541
|
+
const cleaned = cleanOptional(value);
|
|
542
|
+
if (!cleaned) {
|
|
543
|
+
throw new Error(`${label} is required.`);
|
|
544
|
+
}
|
|
545
|
+
return cleaned;
|
|
546
|
+
}
|