loopable-cli 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.
Files changed (133) hide show
  1. package/.output/nitro.json +17 -0
  2. package/.output/public/apple-touch-icon.png +0 -0
  3. package/.output/public/assets/_connectorId-BWjYn5tm.js +1 -0
  4. package/.output/public/assets/_loopId-Ds4RkyEc.js +1 -0
  5. package/.output/public/assets/_taskId-BOzICXDf.js +1 -0
  6. package/.output/public/assets/alert-Bn9-Zm0l.js +1 -0
  7. package/.output/public/assets/arrow-left-BF9juV9T.js +1 -0
  8. package/.output/public/assets/arrow-right-CzqvnOY1.js +1 -0
  9. package/.output/public/assets/button-HCY6DCY-.js +1 -0
  10. package/.output/public/assets/connection-status-CYZB8kLy.js +1 -0
  11. package/.output/public/assets/connector-icon-BZ_MUJV_.js +1 -0
  12. package/.output/public/assets/connectors-BvzttfSu.js +1 -0
  13. package/.output/public/assets/connectors-DdJNwRN_.js +1 -0
  14. package/.output/public/assets/external-link-D46X-bXa.js +1 -0
  15. package/.output/public/assets/gaps-D-I6xiDc.js +1 -0
  16. package/.output/public/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  17. package/.output/public/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  18. package/.output/public/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  19. package/.output/public/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  20. package/.output/public/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  21. package/.output/public/assets/inbox-BUUu9xxE.js +1 -0
  22. package/.output/public/assets/index-ztzex0FN.js +10 -0
  23. package/.output/public/assets/loader-CcBl2u8m.js +1 -0
  24. package/.output/public/assets/loop-form-dB3CLcZb.js +1 -0
  25. package/.output/public/assets/loops-DRunQrxL.js +1 -0
  26. package/.output/public/assets/loops-oSdqMd29.js +1 -0
  27. package/.output/public/assets/manifests-DNMqf7Vi.js +7 -0
  28. package/.output/public/assets/new-uyFxVjuB.js +1 -0
  29. package/.output/public/assets/preload-helper-ORbuV86A.js +3 -0
  30. package/.output/public/assets/runners-DSU3r5Ww.js +1 -0
  31. package/.output/public/assets/setting-fields-ixkpdBb-.js +3 -0
  32. package/.output/public/assets/styles-DZL5V1o7.css +2 -0
  33. package/.output/public/assets/switch-d5ummmHY.js +1 -0
  34. package/.output/public/assets/task-list-BpOStt9v.js +1 -0
  35. package/.output/public/assets/tasks-B8MsKgic.js +1 -0
  36. package/.output/public/assets/trash-2-D6u6NpzO.js +1 -0
  37. package/.output/public/favicon.svg +9 -0
  38. package/.output/server/_chunks/ssr-renderer.mjs +26 -0
  39. package/.output/server/_connectorId-8nb5yOzG.mjs +421 -0
  40. package/.output/server/_libs/@base-ui/react+[...].mjs +12841 -0
  41. package/.output/server/_libs/@tanstack/react-router+[...].mjs +14349 -0
  42. package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
  43. package/.output/server/_libs/dijkstrajs.mjs +118 -0
  44. package/.output/server/_libs/drizzle-orm.mjs +4349 -0
  45. package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
  46. package/.output/server/_libs/lucide-react.mjs +911 -0
  47. package/.output/server/_libs/next-themes.mjs +53 -0
  48. package/.output/server/_libs/pngjs.mjs +1680 -0
  49. package/.output/server/_libs/qrcode.mjs +2583 -0
  50. package/.output/server/_libs/sonner.mjs +955 -0
  51. package/.output/server/_libs/tailwind-merge.mjs +3380 -0
  52. package/.output/server/_libs/tanstack__history.mjs +341 -0
  53. package/.output/server/_libs/ufo.mjs +64 -0
  54. package/.output/server/_loopId-BsUL1SeL.mjs +272 -0
  55. package/.output/server/_runtime.mjs +35 -0
  56. package/.output/server/_ssr/access-zIl15o8y.mjs +42 -0
  57. package/.output/server/_ssr/agents-Btql102J.mjs +657 -0
  58. package/.output/server/_ssr/agents-DAOZPgO2.mjs +76 -0
  59. package/.output/server/_ssr/alert-NEiOCTOl.mjs +37 -0
  60. package/.output/server/_ssr/callback-CUINvl7M.mjs +62 -0
  61. package/.output/server/_ssr/client-CKkOfh-s.mjs +335 -0
  62. package/.output/server/_ssr/connection-status-Bi5peUAk.mjs +31 -0
  63. package/.output/server/_ssr/connections-qnw2GVlP.mjs +2542 -0
  64. package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +37 -0
  65. package/.output/server/_ssr/connectors-BHNwuIMj.mjs +106 -0
  66. package/.output/server/_ssr/connectors-C28g0i4c.mjs +83 -0
  67. package/.output/server/_ssr/createServerRpc-DPDrnFdH.mjs +12 -0
  68. package/.output/server/_ssr/empty-plugin-adapters-BJ4m9L8g.mjs +5 -0
  69. package/.output/server/_ssr/gaps-RAFSXLZf.mjs +53 -0
  70. package/.output/server/_ssr/inbox-k5zHergU.mjs +35 -0
  71. package/.output/server/_ssr/loop-form-DlObSewZ.mjs +412 -0
  72. package/.output/server/_ssr/loops-APFcP0y6.mjs +311 -0
  73. package/.output/server/_ssr/loops-xt2Bn7_q.mjs +222 -0
  74. package/.output/server/_ssr/new-DfKyd2Wa.mjs +169 -0
  75. package/.output/server/_ssr/router-3WmpE5b_.mjs +1553 -0
  76. package/.output/server/_ssr/runners-By-mwc3F.mjs +32 -0
  77. package/.output/server/_ssr/runners-DI_NxpHC.mjs +116 -0
  78. package/.output/server/_ssr/runners-DtXs4Q8a.mjs +106 -0
  79. package/.output/server/_ssr/secrets-Hqti-niS.mjs +199 -0
  80. package/.output/server/_ssr/setting-fields-8vKFj4df.mjs +92 -0
  81. package/.output/server/_ssr/ssr.mjs +6605 -0
  82. package/.output/server/_ssr/start-Bdc2GJUI.mjs +51 -0
  83. package/.output/server/_ssr/switch-ReNglgH6.mjs +18 -0
  84. package/.output/server/_ssr/task-list-CUITIqkb.mjs +186 -0
  85. package/.output/server/_ssr/tasks-SAjNovX6.mjs +110 -0
  86. package/.output/server/_ssr/tasks-vcoodp_D.mjs +533 -0
  87. package/.output/server/_tanstack-start-manifest_v-C0cP5F0M.mjs +125 -0
  88. package/.output/server/_taskId-CXS3lod0.mjs +249 -0
  89. package/.output/server/index.mjs +541 -0
  90. package/README.md +81 -0
  91. package/dist/cli.mjs +165 -0
  92. package/dist/dispatcher.mjs +9871 -0
  93. package/dist/runner.mjs +657 -0
  94. package/drizzle/0000_cold_demogoblin.sql +29 -0
  95. package/drizzle/0001_fancy_blonde_phantom.sql +13 -0
  96. package/drizzle/0002_spooky_smiling_tiger.sql +16 -0
  97. package/drizzle/0003_small_golden_guardian.sql +25 -0
  98. package/drizzle/0004_wonderful_thunderbolts.sql +39 -0
  99. package/drizzle/0005_named_workflows.sql +7 -0
  100. package/drizzle/0006_signals.sql +18 -0
  101. package/drizzle/0007_review_comments.sql +1 -0
  102. package/drizzle/0008_held_signals.sql +1 -0
  103. package/drizzle/0009_work_item_ref.sql +58 -0
  104. package/drizzle/0010_signal_payload.sql +2 -0
  105. package/drizzle/0011_rule_poll_cursor.sql +1 -0
  106. package/drizzle/0012_loops.sql +63 -0
  107. package/drizzle/0013_loop_owns_action_and_prompt.sql +154 -0
  108. package/drizzle/0014_loop_poll_every.sql +4 -0
  109. package/drizzle/0015_machines.sql +10 -0
  110. package/drizzle/0016_task_machine.sql +1 -0
  111. package/drizzle/0017_loop_machine.sql +1 -0
  112. package/drizzle/0018_runners.sql +1 -0
  113. package/drizzle/0019_task_runner.sql +1 -0
  114. package/drizzle/0020_loop_runner.sql +1 -0
  115. package/drizzle/0021_drop_runner_kind.sql +1 -0
  116. package/drizzle/0022_drop_loop_runner.sql +1 -0
  117. package/drizzle/0023_shared_host.sql +6 -0
  118. package/drizzle/meta/0000_snapshot.json +206 -0
  119. package/drizzle/meta/0001_snapshot.json +284 -0
  120. package/drizzle/meta/0002_snapshot.json +390 -0
  121. package/drizzle/meta/0003_snapshot.json +566 -0
  122. package/drizzle/meta/0004_snapshot.json +633 -0
  123. package/drizzle/meta/0005_snapshot.json +626 -0
  124. package/drizzle/meta/0006_snapshot.json +752 -0
  125. package/drizzle/meta/0007_snapshot.json +759 -0
  126. package/drizzle/meta/0008_snapshot.json +766 -0
  127. package/drizzle/meta/0009_snapshot.json +752 -0
  128. package/drizzle/meta/0010_snapshot.json +766 -0
  129. package/drizzle/meta/0011_snapshot.json +773 -0
  130. package/drizzle/meta/0012_snapshot.json +773 -0
  131. package/drizzle/meta/0013_snapshot.json +817 -0
  132. package/drizzle/meta/_journal.json +174 -0
  133. package/package.json +88 -0
@@ -0,0 +1,533 @@
1
+ import { f as eq, u as desc } from "../_libs/drizzle-orm.mjs";
2
+ import { c as loops, d as runDir, g as tasks, s as db } from "./client-CKkOfh-s.mjs";
3
+ import { a as connectorManifest, c as credentialForConnector, i as connectorAction, o as connectorRuntime, s as connectorWorkflow } from "./connections-qnw2GVlP.mjs";
4
+ import { o as settingsFor } from "./agents-Btql102J.mjs";
5
+ import "./runners-DtXs4Q8a.mjs";
6
+ import { randomUUID } from "node:crypto";
7
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { isAbsolute, join } from "node:path";
9
+ //#region node_modules/.nitro/vite/services/ssr/assets/tasks-vcoodp_D.js
10
+ /** The agent says this when the honest answer is silence. */
11
+ var AGENT_NOTHING = "NOTHING_TO_DO";
12
+ function isAgentNothing(text) {
13
+ const compact = (value) => value.replace(/[`*_.\s]/g, "").toUpperCase();
14
+ return compact(text) === compact(AGENT_NOTHING);
15
+ }
16
+ /**
17
+ * Folder jobs name a path that only exists on Loopable's host.
18
+ * A runner on another hostname cannot see that folder.
19
+ */
20
+ function jobRequiresHost(job) {
21
+ return Boolean(job.cwd);
22
+ }
23
+ /**
24
+ * Appended to the workflow's prompt, because the shape of the answer is the
25
+ * machinery's business and the workflow should only have to describe the job.
26
+ */
27
+ var REVIEW_FORMAT = [
28
+ `Reply with one JSON object and nothing else, in this shape:`,
29
+ ``,
30
+ `{`,
31
+ ` "summary": "a few sentences on the change as a whole",`,
32
+ ` "findings": [`,
33
+ ` { "path": "src/thing.ts", "line": 42, "body": "what is wrong, and what to do" },`,
34
+ ` { "path": "src/thing.ts", "startLine": 60, "line": 64, "body": "for a point about several lines" }`,
35
+ ` ]`,
36
+ `}`,
37
+ ``,
38
+ `- "path" is the path of the file in the repository.`,
39
+ `- "line" is the line number in the new version of the file. Only comment on`,
40
+ ` lines that this pull request changed; a comment on a line that is not in`,
41
+ ` the diff will be moved into the summary instead of attached.`,
42
+ `- One finding per point, placed where a reader has to look rather than where`,
43
+ ` you happened to notice it.`,
44
+ `- "body" is markdown with no heading.`,
45
+ `- "findings" may be empty when the summary says everything worth saying.`
46
+ ].join("\n");
47
+ /**
48
+ * Everything in the reply that might be the object, best first.
49
+ *
50
+ * More than one is offered on purpose. A streamed answer can arrive twice,
51
+ * once in pieces and once whole, and a reply can carry an example alongside
52
+ * the real thing, so the first block that looks like JSON is not reliably the
53
+ * one that is. Each is tried in turn and the first that parses wins.
54
+ */
55
+ function jsonCandidates(text) {
56
+ const fences = (pattern) => [...text.matchAll(pattern)].map((match) => (match[1] ?? "").trim()).filter(Boolean);
57
+ const labelled = fences(/```json\s*\n([\s\S]*?)```/gi);
58
+ const rest = fences(/```\s*\n([\s\S]*?)```/g);
59
+ const start = text.indexOf("{");
60
+ const end = text.lastIndexOf("}");
61
+ if (start !== -1 && end > start) rest.push(text.slice(start, end + 1));
62
+ return {
63
+ labelled,
64
+ rest
65
+ };
66
+ }
67
+ function answerFrom(json) {
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(json);
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (typeof parsed !== "object" || parsed === null) return null;
75
+ const value = parsed;
76
+ const summary = typeof value.summary === "string" ? value.summary.trim() : "";
77
+ const findings = Array.isArray(value.findings) ? value.findings.flatMap((entry) => {
78
+ const finding = findingFrom(entry);
79
+ return finding ? [finding] : [];
80
+ }) : [];
81
+ return summary || findings.length > 0 ? {
82
+ summary,
83
+ findings
84
+ } : null;
85
+ }
86
+ function findingFrom(value) {
87
+ if (typeof value !== "object" || value === null) return null;
88
+ const entry = value;
89
+ const path = typeof entry.path === "string" ? entry.path.trim() : "";
90
+ const line = typeof entry.line === "number" ? Math.trunc(entry.line) : NaN;
91
+ const body = typeof entry.body === "string" ? entry.body.trim() : "";
92
+ if (!path || !body || !Number.isFinite(line) || line < 1) return null;
93
+ const start = typeof entry.startLine === "number" ? Math.trunc(entry.startLine) : void 0;
94
+ return {
95
+ path,
96
+ line,
97
+ ...start !== void 0 && start >= 1 && start < line ? { startLine: start } : {},
98
+ body
99
+ };
100
+ }
101
+ /**
102
+ * Lenient about the wrapper and strict about the contents.
103
+ *
104
+ * An agent that answered in prose is taken at its word and gets a review with
105
+ * no anchors, which is what Loopable did before it could anchor anything. An
106
+ * agent that plainly meant to answer in JSON, by fencing it as such, and
107
+ * produced nothing readable is an error: the alternative is posting the
108
+ * wreckage to somebody's pull request.
109
+ */
110
+ function parseReview(text) {
111
+ const { labelled, rest } = jsonCandidates(text);
112
+ for (const candidate of [...labelled, ...rest]) {
113
+ const answer = answerFrom(candidate);
114
+ if (answer) return answer;
115
+ }
116
+ if (labelled.length > 0) throw new Error("The agent was asked for JSON and produced something that could not be read.");
117
+ const summary = text.trim();
118
+ if (!summary) throw new Error("The agent's answer was empty.");
119
+ return {
120
+ summary,
121
+ findings: []
122
+ };
123
+ }
124
+ /**
125
+ * Splits findings into the ones that can be attached to a line and the ones
126
+ * that cannot. With nothing to check against, everything is treated as
127
+ * unanchorable: guessing would risk the whole review being refused.
128
+ */
129
+ function anchorFindings(findings, commentable) {
130
+ const comments = [];
131
+ const loose = [];
132
+ for (const finding of findings) {
133
+ const lines = commentable?.[finding.path];
134
+ const allowed = lines ? new Set(lines) : null;
135
+ (allowed !== null && allowed.has(finding.line) && (finding.startLine === void 0 || allowed.has(finding.startLine)) ? comments : loose).push(finding);
136
+ }
137
+ return {
138
+ comments,
139
+ loose
140
+ };
141
+ }
142
+ /** The review body: the overview, then anything that could not be anchored. */
143
+ function reviewBody(summary, loose) {
144
+ const parts = summary ? [summary] : [];
145
+ for (const finding of loose) {
146
+ const at = finding.startLine ? `${finding.startLine}-${finding.line}` : String(finding.line);
147
+ parts.push(`**${finding.path}:${at}**\n\n${finding.body}`);
148
+ }
149
+ return parts.join("\n\n");
150
+ }
151
+ /**
152
+ * The branch name a code-writing task uses, so a retry pushes the same branch
153
+ * the first attempt made rather than leaving a trail of near-identical ones.
154
+ */
155
+ /** Reads as a branch name and cannot collide with a person's own. */
156
+ function slug(text) {
157
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "work";
158
+ }
159
+ function branchFor(input) {
160
+ return `loopable/${slug(input.ref)}-${input.taskId.slice(0, 8)}`;
161
+ }
162
+ /**
163
+ * The agent says this when the honest answer is "nothing". Without it a loop
164
+ * that runs by itself would post filler, which is worse than silence.
165
+ */
166
+ var NOTHING = AGENT_NOTHING;
167
+ /**
168
+ * The answer is the checkout, not the reply, so the reply is asked for as the
169
+ * account somebody reads before deciding whether to look at the diff.
170
+ */
171
+ var CODE_FORMAT = [
172
+ "Then reply with the description of a pull request: a first line under about",
173
+ "seventy characters saying what it does, a blank line, and a short account of",
174
+ "what you changed and anything you were unsure about. No preamble."
175
+ ].join("\n");
176
+ function toView(row) {
177
+ return {
178
+ id: row.id,
179
+ loopId: row.loopId,
180
+ loopName: row.loopName ?? "Deleted loop",
181
+ connectorId: row.connectorId,
182
+ state: row.state,
183
+ sourceUrl: row.sourceUrl,
184
+ sourceKind: row.sourceKind,
185
+ sourceRef: row.sourceRef,
186
+ sourceTitle: row.sourceTitle,
187
+ dryRun: row.dryRun,
188
+ agentId: row.agentId,
189
+ runnerId: row.runnerId,
190
+ agentCommand: row.agentCommand,
191
+ output: row.output,
192
+ comments: row.comments ?? [],
193
+ actionConnectorId: row.actionConnectorId,
194
+ actionId: row.actionId,
195
+ resultUrl: row.resultUrl,
196
+ error: row.error,
197
+ attempts: row.attempts,
198
+ cancelRequested: row.cancelRequested,
199
+ durationMs: row.durationMs,
200
+ createdAt: row.createdAt.toISOString(),
201
+ startedAt: row.startedAt?.toISOString() ?? null,
202
+ updatedAt: row.updatedAt.toISOString()
203
+ };
204
+ }
205
+ function select() {
206
+ return db().select({
207
+ id: tasks.id,
208
+ loopId: tasks.loopId,
209
+ connectorId: tasks.connectorId,
210
+ state: tasks.state,
211
+ sourceUrl: tasks.sourceUrl,
212
+ sourceKind: tasks.sourceKind,
213
+ sourceRef: tasks.sourceRef,
214
+ sourceTitle: tasks.sourceTitle,
215
+ dryRun: tasks.dryRun,
216
+ agentId: tasks.agentId,
217
+ runnerId: tasks.runnerId,
218
+ agentCommand: tasks.agentCommand,
219
+ output: tasks.output,
220
+ comments: tasks.comments,
221
+ actionConnectorId: tasks.actionConnectorId,
222
+ actionId: tasks.actionId,
223
+ actionTarget: tasks.actionTarget,
224
+ resultUrl: tasks.resultUrl,
225
+ error: tasks.error,
226
+ logPath: tasks.logPath,
227
+ logText: tasks.logText,
228
+ attempts: tasks.attempts,
229
+ runAfter: tasks.runAfter,
230
+ leaseUntil: tasks.leaseUntil,
231
+ cancelRequested: tasks.cancelRequested,
232
+ dedupeKey: tasks.dedupeKey,
233
+ durationMs: tasks.durationMs,
234
+ createdAt: tasks.createdAt,
235
+ startedAt: tasks.startedAt,
236
+ updatedAt: tasks.updatedAt,
237
+ loopName: loops.name
238
+ }).from(tasks).leftJoin(loops, eq(tasks.loopId, loops.id));
239
+ }
240
+ async function listTasks(options = {}) {
241
+ const query = select().orderBy(desc(tasks.createdAt)).limit(options.limit ?? 100);
242
+ return (options.loopId ? await query.where(eq(tasks.loopId, options.loopId)).all() : await query.all()).map(toView);
243
+ }
244
+ async function getTask(id) {
245
+ const row = await select().where(eq(tasks.id, id)).get();
246
+ return row ? toView(row) : null;
247
+ }
248
+ async function taskRow(id) {
249
+ return await db().select().from(tasks).where(eq(tasks.id, id)).get();
250
+ }
251
+ async function updateTask(id, values) {
252
+ await db().update(tasks).set({
253
+ ...values,
254
+ updatedAt: /* @__PURE__ */ new Date()
255
+ }).where(eq(tasks.id, id)).run();
256
+ }
257
+ /** The one place a task is created, whether a person or a poll asked for it. */
258
+ async function queue(input) {
259
+ const id = randomUUID();
260
+ await db().insert(tasks).values({
261
+ id,
262
+ loopId: input.loop.id,
263
+ connectorId: input.loop.connectorId,
264
+ state: "queued",
265
+ sourceUrl: input.url.trim(),
266
+ sourceKind: input.item.kind,
267
+ sourceRef: input.item.ref,
268
+ sourcePayload: input.payload,
269
+ sourceTitle: input.title,
270
+ dryRun: input.dryRun,
271
+ actionConnectorId: input.loop.actionConnectorId,
272
+ actionId: input.loop.actionId,
273
+ actionTarget: input.loop.actionTarget,
274
+ dedupeKey: input.dedupeKey
275
+ }).run();
276
+ return await getTask(id);
277
+ }
278
+ /** Everything a loop needs before it can produce a task, or a reason it cannot. */
279
+ async function runnable(loopId) {
280
+ const loop = await db().select().from(loops).where(eq(loops.id, loopId)).get();
281
+ if (!loop) throw new Error("Loop not found");
282
+ const manifest = connectorManifest(loop.connectorId);
283
+ if (!manifest) throw new Error(`Unknown connector: ${loop.connectorId}`);
284
+ if (!connectorWorkflow(loop.connectorId, loop.workflowId)) throw new Error(`${manifest.name} no longer offers the workflow this loop was built on.`);
285
+ if (!connectorRuntime(loop.connectorId).resolveWorkItem) throw new Error(`${manifest.name} cannot run tasks yet.`);
286
+ const writer = connectorManifest(loop.actionConnectorId);
287
+ if (!writer) throw new Error(`Unknown connector: ${loop.actionConnectorId}`);
288
+ if (!connectorRuntime(loop.actionConnectorId).applyAction) throw new Error(`${writer.name} cannot write anything yet.`);
289
+ return loop;
290
+ }
291
+ /**
292
+ * Queues a run and returns at once. Identifying the link needs no network, so
293
+ * a typo still fails while the person is looking at the box, but everything
294
+ * that can be slow or can fail belongs to the dispatcher.
295
+ */
296
+ async function enqueueTask(input) {
297
+ const loop = await runnable(input.loopId);
298
+ const manifest = connectorManifest(loop.connectorId);
299
+ const runtime = connectorRuntime(loop.connectorId);
300
+ if (manifest.byHand.kind === "now" && runtime.itemForNow) return await queue({
301
+ loop,
302
+ item: runtime.itemForNow(),
303
+ url: "",
304
+ dryRun: input.dryRun
305
+ });
306
+ if (!runtime.identifyLink) throw new Error(`${manifest.name} cannot read links.`);
307
+ const item = runtime.identifyLink(input.url);
308
+ if (!item) throw new Error(`That does not look like a ${manifest.name} link Loopable can work on.`);
309
+ return await queue({
310
+ loop,
311
+ item,
312
+ url: input.url,
313
+ dryRun: input.dryRun
314
+ });
315
+ }
316
+ /**
317
+ * Queues a run for something a poll noticed. The title is already known, so
318
+ * the inbox can say what the task is about before the dispatcher has fetched
319
+ * anything.
320
+ */
321
+ async function enqueueSignal(input) {
322
+ const loop = await runnable(input.loop.id);
323
+ return await queue({
324
+ loop,
325
+ item: input.signal,
326
+ payload: input.signal.payload,
327
+ url: input.signal.url,
328
+ title: input.signal.title,
329
+ dryRun: false,
330
+ dedupeKey: `${loop.id}:${input.signal.key}`
331
+ });
332
+ }
333
+ /** The tail is what matters while a run is going; the whole thing rarely is. */
334
+ var LOG_TAIL_BYTES = 64e3;
335
+ async function readTaskLog(id) {
336
+ const task = await taskRow(id);
337
+ if (!task) return null;
338
+ let text = task.logText;
339
+ if (!text && task.logPath && existsSync(task.logPath)) text = readFileSync(task.logPath, "utf8");
340
+ if (!text) return null;
341
+ if (text.length <= LOG_TAIL_BYTES) return text;
342
+ return `[earlier output not shown]\n\n${text.slice(-64e3)}`;
343
+ }
344
+ async function requestCancel(id) {
345
+ if (!await taskRow(id)) return null;
346
+ await updateTask(id, { cancelRequested: true });
347
+ return await getTask(id);
348
+ }
349
+ var TaskCancelled = class extends Error {
350
+ constructor() {
351
+ super("Stopped before it finished.");
352
+ this.name = "Cancelled";
353
+ }
354
+ };
355
+ /**
356
+ * The directory a loop says its agent should work in. Checked here rather than
357
+ * left to the agent, which would otherwise run somewhere unexpected and answer
358
+ * confidently about the wrong code.
359
+ */
360
+ function folderFor(loop) {
361
+ const folder = typeof loop.settings.folder === "string" ? loop.settings.folder.trim() : "";
362
+ if (!folder) throw new Error("This loop has no folder set. Set one and try again.");
363
+ if (!isAbsolute(folder)) throw new Error(`The folder must be an absolute path: ${folder}`);
364
+ if (!existsSync(folder)) throw new Error(`There is no folder at ${folder}.`);
365
+ if (!statSync(folder).isDirectory()) throw new Error(`${folder} is not a folder.`);
366
+ return folder;
367
+ }
368
+ var KIND_NOUN = {
369
+ pull_request: "pull request",
370
+ issue: "issue",
371
+ message: "the message",
372
+ email: "the email",
373
+ occurrence: "the run due at"
374
+ };
375
+ function checkoutInstructions(item, branch, writesCode) {
376
+ const checkout = item.checkout;
377
+ if (!checkout) return [];
378
+ const at = checkout.sha ? `${checkout.ref} (${checkout.sha})` : checkout.ref;
379
+ if (writesCode && branch) return [
380
+ `Clone ${checkout.url} and check out ${at}.`,
381
+ `Commit your change and push it to ${branch}. Do not open a pull request; Loopable will.`,
382
+ ``
383
+ ];
384
+ return [`Clone ${checkout.url} and check out ${at}. Work in that checkout. Do not commit or push.`, ``];
385
+ }
386
+ /**
387
+ * What the agent is actually sent.
388
+ *
389
+ * The loop supplies the job and, if it has anything to add, its guidance after
390
+ * it. What shape the answer must take is not either of theirs: it is what the
391
+ * code downstream is able to read, so it is appended here and neither of them
392
+ * has to know about it.
393
+ */
394
+ function promptFor(input) {
395
+ const shape = input.workflow.answer === "review" ? [REVIEW_FORMAT] : input.workflow.answer === "code" ? [CODE_FORMAT] : [`Reply with only the text to post. No preamble, no explanation of what you are about to do.`];
396
+ return [
397
+ `You are working on ${KIND_NOUN[input.item.kind]} ${input.item.ref}.`,
398
+ ...input.files.length > 0 ? [`Read ${input.files.join(" and ")} first.`] : [],
399
+ ``,
400
+ ...checkoutInstructions(input.item, input.branch, input.workflow.answer === "code"),
401
+ `Your task:`,
402
+ input.ask,
403
+ ...input.guidance ? [
404
+ ``,
405
+ `From the person who set this up:`,
406
+ input.guidance
407
+ ] : [],
408
+ ``,
409
+ ...shape,
410
+ ``,
411
+ `If there is genuinely nothing worth posting, reply with exactly ${NOTHING}.`
412
+ ].join("\n");
413
+ }
414
+ /**
415
+ * Turns what the agent said into what will be posted. A review is parsed and
416
+ * its findings checked against the diff; anything that cannot be attached to a
417
+ * line goes into the body with its location written out, because losing the
418
+ * point is worse than losing its position.
419
+ */
420
+ function answerFor(workflow, action, item, text) {
421
+ if (workflow.answer !== "review") return {
422
+ output: text,
423
+ comments: []
424
+ };
425
+ const { summary, findings } = parseReview(text);
426
+ const { comments, loose } = action.accepts.includes("review") ? anchorFindings(findings, item.commentable) : {
427
+ comments: [],
428
+ loose: findings
429
+ };
430
+ return {
431
+ output: reviewBody(summary, loose) || "Comments are on the lines they are about.",
432
+ comments
433
+ };
434
+ }
435
+ async function loadWork(id, signal) {
436
+ const task = await taskRow(id);
437
+ if (!task) throw new Error(`Task not found: ${id}`);
438
+ if (signal?.aborted || task.cancelRequested) throw new TaskCancelled();
439
+ const loop = await db().select().from(loops).where(eq(loops.id, task.loopId)).get();
440
+ if (!loop) throw new Error("The loop behind this task has been deleted.");
441
+ const workflow = connectorWorkflow(loop.connectorId, loop.workflowId);
442
+ if (!workflow) throw new Error(`${loop.connectorId} no longer offers ${loop.workflowId}.`);
443
+ const runtime = connectorRuntime(task.connectorId);
444
+ if (!runtime.resolveWorkItem) throw new Error(`${task.connectorId} cannot run tasks.`);
445
+ const writer = connectorRuntime(task.actionConnectorId);
446
+ if (!writer.applyAction) throw new Error(`${task.actionConnectorId} cannot write anything.`);
447
+ const action = connectorAction(task.actionConnectorId, task.actionId);
448
+ if (!action) throw new Error(`${task.actionConnectorId} no longer offers ${task.actionId}.`);
449
+ const { credential } = await credentialForConnector(task.connectorId);
450
+ return {
451
+ task,
452
+ loop,
453
+ workflow,
454
+ writer,
455
+ action,
456
+ credential,
457
+ item: await runtime.resolveWorkItem({
458
+ url: task.sourceUrl,
459
+ payload: task.sourcePayload ?? null,
460
+ credential
461
+ })
462
+ };
463
+ }
464
+ async function jobForTask(id) {
465
+ const { task, loop, workflow, item } = await loadWork(id);
466
+ if (!task.agentId) throw new Error("This task has no agent yet.");
467
+ const workspace = runDir(id);
468
+ const cwd = workflow.runsIn === "folder" ? folderFor(loop) : void 0;
469
+ const branch = workflow.answer === "code" ? branchFor({
470
+ ref: item.ref,
471
+ taskId: id
472
+ }) : void 0;
473
+ const settings = { ...await settingsFor(task.agentId) };
474
+ if (item.checkout && settings.permissionMode === "read_only") settings.permissionMode = "workspace_write";
475
+ return {
476
+ taskId: id,
477
+ agentId: task.agentId,
478
+ prompt: promptFor({
479
+ workflow,
480
+ ask: loop.prompt,
481
+ guidance: loop.guidance,
482
+ item,
483
+ files: item.context.map((file) => cwd ? join(workspace, file.name) : file.name),
484
+ branch
485
+ }),
486
+ settings,
487
+ files: item.context.map((file) => ({
488
+ name: file.name,
489
+ body: file.body
490
+ })),
491
+ ...cwd ? { cwd } : {}
492
+ };
493
+ }
494
+ async function absorbAgentOutput(id, raw) {
495
+ const { task, workflow, action, item } = await loadWork(id);
496
+ const workspace = runDir(id);
497
+ const said = raw.trim();
498
+ writeFileSync(join(workspace, "reply.txt"), said);
499
+ if (!said) throw new Error("The agent produced nothing.");
500
+ if (isAgentNothing(said)) {
501
+ await finish(id, task, {
502
+ state: "skipped",
503
+ output: said
504
+ });
505
+ return await getTask(id);
506
+ }
507
+ const parsed = answerFor(workflow, action, item, said);
508
+ if (!parsed.output && parsed.comments.length === 0) throw new Error("The agent produced nothing worth posting.");
509
+ await updateTask(id, {
510
+ output: parsed.output,
511
+ comments: parsed.comments
512
+ });
513
+ if (task.dryRun) {
514
+ await finish(id, task, { state: "prepared" });
515
+ return await getTask(id);
516
+ }
517
+ await updateTask(id, {
518
+ state: "applying",
519
+ leaseUntil: null
520
+ });
521
+ return await getTask(id);
522
+ }
523
+ async function finish(id, task, values) {
524
+ const started = task.startedAt?.getTime() ?? task.createdAt.getTime();
525
+ await updateTask(id, {
526
+ error: null,
527
+ ...values,
528
+ durationMs: Date.now() - started,
529
+ leaseUntil: null
530
+ });
531
+ }
532
+ //#endregion
533
+ export { jobForTask as a, readTaskLog as c, updateTask as d, getTask as i, requestCancel as l, enqueueSignal as n, jobRequiresHost as o, enqueueTask as r, listTasks as s, absorbAgentOutput as t, taskRow as u };
@@ -0,0 +1,125 @@
1
+ //#region node_modules/.nitro/vite/services/ssr/assets/_tanstack-start-manifest_v-C0cP5F0M.js
2
+ var tsrStartManifest = () => ({ routes: {
3
+ __root__: {
4
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/__root.tsx",
5
+ children: [
6
+ "/",
7
+ "/connectors/$connectorId",
8
+ "/inbox/$taskId",
9
+ "/loops/$loopId",
10
+ "/loops/new",
11
+ "/agents/",
12
+ "/connectors/",
13
+ "/inbox/",
14
+ "/loops/",
15
+ "/machines/",
16
+ "/runners/",
17
+ "/api/runners/claim",
18
+ "/api/runners/heartbeat",
19
+ "/api/runners/hello",
20
+ "/api/connectors/$connectorId/authorize",
21
+ "/api/connectors/$connectorId/callback",
22
+ "/api/runners/jobs/$taskId/complete",
23
+ "/api/runners/jobs/$taskId/log"
24
+ ],
25
+ preloads: [
26
+ "/assets/index-ztzex0FN.js",
27
+ "/assets/preload-helper-ORbuV86A.js",
28
+ "/assets/button-HCY6DCY-.js",
29
+ "/assets/tasks-B8MsKgic.js",
30
+ "/assets/manifests-DNMqf7Vi.js",
31
+ "/assets/connectors-BvzttfSu.js",
32
+ "/assets/loops-oSdqMd29.js"
33
+ ],
34
+ scripts: [{ attrs: {
35
+ type: "module",
36
+ async: !0,
37
+ src: "/assets/index-ztzex0FN.js"
38
+ } }]
39
+ },
40
+ "/connectors/$connectorId": {
41
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/connectors/$connectorId.tsx",
42
+ children: void 0,
43
+ preloads: [
44
+ "/assets/_connectorId-BWjYn5tm.js",
45
+ "/assets/arrow-left-BF9juV9T.js",
46
+ "/assets/connector-icon-BZ_MUJV_.js",
47
+ "/assets/external-link-D46X-bXa.js",
48
+ "/assets/loader-CcBl2u8m.js",
49
+ "/assets/trash-2-D6u6NpzO.js",
50
+ "/assets/setting-fields-ixkpdBb-.js",
51
+ "/assets/connection-status-CYZB8kLy.js",
52
+ "/assets/alert-Bn9-Zm0l.js"
53
+ ]
54
+ },
55
+ "/inbox/$taskId": {
56
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/inbox/$taskId.tsx",
57
+ children: void 0,
58
+ preloads: [
59
+ "/assets/_taskId-BOzICXDf.js",
60
+ "/assets/arrow-left-BF9juV9T.js",
61
+ "/assets/task-list-BpOStt9v.js",
62
+ "/assets/external-link-D46X-bXa.js",
63
+ "/assets/loader-CcBl2u8m.js",
64
+ "/assets/alert-Bn9-Zm0l.js"
65
+ ]
66
+ },
67
+ "/loops/$loopId": {
68
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/$loopId.tsx",
69
+ children: void 0,
70
+ preloads: [
71
+ "/assets/_loopId-Ds4RkyEc.js",
72
+ "/assets/arrow-left-BF9juV9T.js",
73
+ "/assets/loop-form-dB3CLcZb.js",
74
+ "/assets/task-list-BpOStt9v.js",
75
+ "/assets/switch-d5ummmHY.js",
76
+ "/assets/alert-Bn9-Zm0l.js",
77
+ "/assets/gaps-D-I6xiDc.js"
78
+ ]
79
+ },
80
+ "/loops/new": {
81
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/new.tsx",
82
+ children: void 0,
83
+ preloads: [
84
+ "/assets/new-uyFxVjuB.js",
85
+ "/assets/arrow-left-BF9juV9T.js",
86
+ "/assets/arrow-right-CzqvnOY1.js",
87
+ "/assets/loop-form-dB3CLcZb.js",
88
+ "/assets/connector-icon-BZ_MUJV_.js"
89
+ ]
90
+ },
91
+ "/connectors/": {
92
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/connectors/index.tsx",
93
+ children: void 0,
94
+ preloads: [
95
+ "/assets/connectors-DdJNwRN_.js",
96
+ "/assets/arrow-right-CzqvnOY1.js",
97
+ "/assets/connector-icon-BZ_MUJV_.js",
98
+ "/assets/connection-status-CYZB8kLy.js"
99
+ ]
100
+ },
101
+ "/inbox/": {
102
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/inbox/index.tsx",
103
+ children: void 0,
104
+ preloads: ["/assets/inbox-BUUu9xxE.js", "/assets/task-list-BpOStt9v.js"]
105
+ },
106
+ "/loops/": {
107
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/index.tsx",
108
+ children: void 0,
109
+ preloads: [
110
+ "/assets/loops-DRunQrxL.js",
111
+ "/assets/connector-icon-BZ_MUJV_.js",
112
+ "/assets/trash-2-D6u6NpzO.js",
113
+ "/assets/switch-d5ummmHY.js",
114
+ "/assets/alert-Bn9-Zm0l.js",
115
+ "/assets/gaps-D-I6xiDc.js"
116
+ ]
117
+ },
118
+ "/runners/": {
119
+ filePath: "/Users/bytedance/codex-projects/loopable/src/routes/runners/index.tsx",
120
+ children: void 0,
121
+ preloads: ["/assets/runners-DSU3r5Ww.js", "/assets/alert-Bn9-Zm0l.js"]
122
+ }
123
+ } });
124
+ //#endregion
125
+ export { tsrStartManifest };