@bpinternal/overwatch 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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +451 -0
  3. package/dist/actuators/actuator.d.ts +13 -0
  4. package/dist/actuators/actuator.js +30 -0
  5. package/dist/actuators/actuator.js.map +7 -0
  6. package/dist/actuators/agent-pr.d.ts +25 -0
  7. package/dist/actuators/agent-pr.js +119 -0
  8. package/dist/actuators/agent-pr.js.map +7 -0
  9. package/dist/actuators/comments.d.ts +3 -0
  10. package/dist/actuators/comments.js +42 -0
  11. package/dist/actuators/comments.js.map +7 -0
  12. package/dist/actuators/index.d.ts +54 -0
  13. package/dist/actuators/index.js +40 -0
  14. package/dist/actuators/index.js.map +7 -0
  15. package/dist/actuators/instructions.d.ts +9 -0
  16. package/dist/actuators/instructions.js +62 -0
  17. package/dist/actuators/instructions.js.map +7 -0
  18. package/dist/actuators.d.ts +72 -0
  19. package/dist/actuators.js +173 -0
  20. package/dist/actuators.js.map +7 -0
  21. package/dist/agents.d.ts +53 -0
  22. package/dist/agents.js +107 -0
  23. package/dist/agents.js.map +7 -0
  24. package/dist/claims.d.ts +16 -0
  25. package/dist/claims.js +51 -0
  26. package/dist/claims.js.map +7 -0
  27. package/dist/cli.d.ts +13 -0
  28. package/dist/cli.js +94 -0
  29. package/dist/cli.js.map +7 -0
  30. package/dist/control-loop.d.ts +50 -0
  31. package/dist/control-loop.js +285 -0
  32. package/dist/control-loop.js.map +7 -0
  33. package/dist/github.d.ts +98 -0
  34. package/dist/github.js +258 -0
  35. package/dist/github.js.map +7 -0
  36. package/dist/index.d.ts +11 -0
  37. package/dist/index.js +69 -0
  38. package/dist/index.js.map +7 -0
  39. package/dist/log.d.ts +22 -0
  40. package/dist/log.js +87 -0
  41. package/dist/log.js.map +7 -0
  42. package/dist/pickers/busiest-file.d.ts +9 -0
  43. package/dist/pickers/busiest-file.js +48 -0
  44. package/dist/pickers/busiest-file.js.map +7 -0
  45. package/dist/pickers/count.d.ts +6 -0
  46. package/dist/pickers/count.js +29 -0
  47. package/dist/pickers/count.js.map +7 -0
  48. package/dist/pickers/index.d.ts +12 -0
  49. package/dist/pickers/index.js +41 -0
  50. package/dist/pickers/index.js.map +7 -0
  51. package/dist/pickers.d.ts +21 -0
  52. package/dist/pickers.js +61 -0
  53. package/dist/pickers.js.map +7 -0
  54. package/dist/repo-handle.d.ts +35 -0
  55. package/dist/repo-handle.js +75 -0
  56. package/dist/repo-handle.js.map +7 -0
  57. package/dist/sandbox.d.ts +33 -0
  58. package/dist/sandbox.js +91 -0
  59. package/dist/sandbox.js.map +7 -0
  60. package/dist/sensors/ast-grep.d.ts +41 -0
  61. package/dist/sensors/ast-grep.js +65 -0
  62. package/dist/sensors/ast-grep.js.map +7 -0
  63. package/dist/sensors/grep.d.ts +31 -0
  64. package/dist/sensors/grep.js +39 -0
  65. package/dist/sensors/grep.js.map +7 -0
  66. package/dist/sensors/index.d.ts +27 -0
  67. package/dist/sensors/index.js +38 -0
  68. package/dist/sensors/index.js.map +7 -0
  69. package/dist/sensors/react-doctor.d.ts +43 -0
  70. package/dist/sensors/react-doctor.js +71 -0
  71. package/dist/sensors/react-doctor.js.map +7 -0
  72. package/dist/sensors/script.d.ts +7 -0
  73. package/dist/sensors/script.js +32 -0
  74. package/dist/sensors/script.js.map +7 -0
  75. package/dist/sensors.d.ts +117 -0
  76. package/dist/sensors.js +126 -0
  77. package/dist/sensors.js.map +7 -0
  78. package/dist/types.d.ts +119 -0
  79. package/dist/types.js +17 -0
  80. package/dist/types.js.map +7 -0
  81. package/package.json +64 -0
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var control_loop_exports = {};
30
+ __export(control_loop_exports, {
31
+ ControlLoop: () => ControlLoop
32
+ });
33
+ module.exports = __toCommonJS(control_loop_exports);
34
+ var import_sdk = require("@daytona/sdk");
35
+ var import_repo_handle = require("./repo-handle");
36
+ var import_log = require("./log");
37
+ var actuators = __toESM(require("./actuators"));
38
+ var pickers = __toESM(require("./pickers"));
39
+ var import_cli = require("./cli");
40
+ var import_claims = require("./claims");
41
+ var import_sandbox = require("./sandbox");
42
+ const BOT_NAME = "control-loop[bot]";
43
+ const DEFAULT_BOT_EMAIL = "control-loop@users.noreply.github.com";
44
+ class ControlLoop {
45
+ constructor(options) {
46
+ this.options = options;
47
+ }
48
+ /**
49
+ * Executes one full cycle: sense -> pick -> run actuator -> verify -> PR.
50
+ * Invoke this from whatever scheduler you use (cron, CI); the loop itself
51
+ * does not stay resident.
52
+ */
53
+ async run() {
54
+ const { config, sensor, picker } = this.options;
55
+ const label = slugify(this.options.label);
56
+ const log = new import_log.RunLog(this.options.label);
57
+ const maxOpenPrCount = config.maxOpenPrCount;
58
+ if (maxOpenPrCount !== void 0) {
59
+ const openPrCount = await config.git.countOpenPrs(label);
60
+ if (openPrCount >= maxOpenPrCount) {
61
+ const reason = `${openPrCount} open PR(s) labeled "${label}" (max ${maxOpenPrCount})`;
62
+ log.skip(reason);
63
+ return { status: "skipped", reason };
64
+ }
65
+ }
66
+ return this.withProvisionedSandbox(log, async (sandbox, ctx) => {
67
+ const sense = makeSensorRunner(sensor, sandbox, ctx);
68
+ const sensed = await log.step("sensing for signals", sense);
69
+ if (sensed.length === 0) {
70
+ log.finish("clean \u2014 no signals detected");
71
+ return { status: "clean" };
72
+ }
73
+ log.info(`sensed ${sensed.length} signal(s)`);
74
+ const claimed = new Set(
75
+ (await config.git.listOpenPrBodies(label)).flatMap(import_claims.parseClaimedKeys)
76
+ );
77
+ const signals = sensed.filter((signal) => !claimed.has((0, import_claims.signalKey)(signal)));
78
+ if (signals.length < sensed.length) {
79
+ log.info(`${sensed.length - signals.length} signal(s) already claimed by open PRs`);
80
+ }
81
+ if (signals.length === 0) {
82
+ log.finish("all sensed signals are already claimed by open PRs");
83
+ return { status: "skipped", reason: "all sensed signals are claimed by open PRs" };
84
+ }
85
+ const picked = await (picker ?? pickers.count(1))(signals);
86
+ if (picked.length === 0) {
87
+ log.finish("clean \u2014 picker selected nothing");
88
+ return { status: "clean" };
89
+ }
90
+ log.info(`picked ${picked.length} of ${signals.length} signal(s) to act on`);
91
+ return this.options.actuator.act({
92
+ signals: picked,
93
+ sandbox,
94
+ ctx,
95
+ sense,
96
+ config,
97
+ label,
98
+ displayLabel: this.options.label,
99
+ log
100
+ });
101
+ });
102
+ }
103
+ /**
104
+ * Creates a sandbox, clones `branch` (default: the base branch), configures git, and
105
+ * runs the `setup` hook — the provisioning every entry point shares — then invokes `fn`
106
+ * with the ready sandbox and its {@link AgentContext}. The sandbox is always torn down
107
+ * afterward, including when provisioning or `fn` throws.
108
+ */
109
+ async withProvisionedSandbox(log, fn, branch) {
110
+ const { config } = this.options;
111
+ const sandbox = await log.step("spinning up sandbox", () => createSandbox(config));
112
+ log.attach(sandbox.id);
113
+ try {
114
+ await log.step(`cloning ${branch ?? config.git.branch} into ${import_sandbox.REPO_PATH}`, async () => {
115
+ await config.git.cloneInto(sandbox, import_sandbox.REPO_PATH, branch);
116
+ await sandbox.git.configureUser(BOT_NAME, config.git.email ?? DEFAULT_BOT_EMAIL, "local", import_sandbox.REPO_PATH);
117
+ });
118
+ const ctx = makeAgentContext(sandbox);
119
+ await ctx.exec(`mkdir -p ${import_sandbox.SCRATCH_DIR}`);
120
+ await (0, import_sandbox.runConfiguredCommand)("setup", config, ctx, log);
121
+ return await fn(sandbox, ctx);
122
+ } finally {
123
+ await log.step("tearing down sandbox", () => sandbox.delete());
124
+ }
125
+ }
126
+ /**
127
+ * Provisions a sandbox the way {@link run} does up through the `setup` hook — clone,
128
+ * configure git, run `config.hooks.setup` — then captures its filesystem as a Daytona
129
+ * snapshot named `name` and returns, without sensing, touching the agent, or opening a
130
+ * PR. Pass the same `name` as `config.sandbox.snapshot` on later runs so they start from
131
+ * the baked-in toolchain instead of re-running `setup` every time.
132
+ *
133
+ * Call this instead of `run` (e.g. as a one-off provisioning step); `name` is required.
134
+ */
135
+ async saveSnapshot(name) {
136
+ const log = new import_log.RunLog(this.options.label);
137
+ await this.withProvisionedSandbox(log, async (sandbox, ctx) => {
138
+ await ctx.exec(`rm -rf ${import_sandbox.SCRATCH_DIR}`);
139
+ await log.step(`saving snapshot ${name}`, () => sandbox._experimental_createSnapshot(name, 0));
140
+ log.finish(`snapshot saved \u2192 ${name}`);
141
+ });
142
+ }
143
+ /**
144
+ * Handler for PR comment events: reads the comments left on the PR since its last
145
+ * commit, has the agent apply them on the PR's branch, and pushes — updating the PR
146
+ * in place. Wire this to your webhook (or poller) for PR comment activity.
147
+ *
148
+ * Comments older than the PR's head commit are treated as already addressed, which
149
+ * makes repeated triggers for the same comment thread safe.
150
+ */
151
+ async applyPrComments(prNumber) {
152
+ const { config, commentActuator } = this.options;
153
+ const label = slugify(this.options.label);
154
+ const log = new import_log.RunLog(`${this.options.label} \xB7 PR #${prNumber}`);
155
+ const pr = await config.git.getPr(prNumber);
156
+ const newComments = (await config.git.listPrComments(prNumber)).filter(
157
+ (comment) => new Date(comment.createdAt) > new Date(pr.headCommittedAt)
158
+ );
159
+ if (newComments.length === 0) {
160
+ log.skip("no comments newer than the PR head");
161
+ return { status: "no-new-comments" };
162
+ }
163
+ const comments = newComments.map(
164
+ (comment) => isFeedback(comment) ? { ...comment, body: stripFeedbackPrefix(comment.body) } : comment
165
+ );
166
+ const feedback = comments.filter((_, index) => isFeedback(newComments[index]));
167
+ log.info(`applying ${comments.length} new comment(s)`);
168
+ return this.withProvisionedSandbox(
169
+ log,
170
+ async (sandbox, ctx) => {
171
+ await log.step("preparing agent", () => config.agent.prepare(ctx));
172
+ const memory = await (0, import_sandbox.readMemory)(ctx, label);
173
+ if (feedback.length > 0) {
174
+ await (0, import_sandbox.appendMemory)(ctx, label, memory, feedback);
175
+ }
176
+ const { instructions } = await (commentActuator ?? actuators.prComments)(comments);
177
+ await log.step(
178
+ "agent applying comments",
179
+ () => config.agent.executeAgent(instructions + (0, import_sandbox.memoryNote)(memory), ctx)
180
+ );
181
+ await ctx.exec(`rm -rf ${import_sandbox.SCRATCH_DIR}`);
182
+ await (0, import_sandbox.runConfiguredCommand)("preCommit", config, ctx, log);
183
+ const status = await ctx.exec("git status --porcelain");
184
+ if (status.output.trim() === "") {
185
+ log.finish("no changes \u2014 agent left the tree untouched");
186
+ return { status: "no-changes", comments };
187
+ }
188
+ await log.step("committing & pushing", async () => {
189
+ await (0, import_sandbox.commitAll)(ctx, "fix: address PR review comments");
190
+ await config.git.push(sandbox, import_sandbox.REPO_PATH, pr.branch);
191
+ });
192
+ log.finish(`comments applied to ${pr.branch}`);
193
+ return { status: "comments-applied", branch: pr.branch, comments };
194
+ },
195
+ pr.branch
196
+ );
197
+ }
198
+ /**
199
+ * Ready-made CLI for driving this loop from CI (or a shell). Declare the loop, then call
200
+ * this once at the end of the file:
201
+ *
202
+ * const loop = new ControlLoop({ ... });
203
+ * loop.cli();
204
+ *
205
+ * Two subcommands: `run` (one full cycle → {@link run}) and `apply-comments <pr>`
206
+ * (apply new PR review comments → {@link applyPrComments}). Run with `--help` for usage.
207
+ * Parses `process.argv` by default; the process exits non-zero on error or an unresolved
208
+ * fix, so CI can gate on it.
209
+ */
210
+ cli(argv = process.argv) {
211
+ return (0, import_cli.runCli)(this, { name: slugify(this.options.label), description: this.options.label }, argv);
212
+ }
213
+ }
214
+ const HIGH_RESOURCES = { cpu: 4, memory: 8, disk: 10 };
215
+ async function createSandbox(config) {
216
+ const daytona = new import_sdk.Daytona();
217
+ if (config.sandbox?.snapshot) {
218
+ return daytona.create({ snapshot: config.sandbox.snapshot, envVars: config.env });
219
+ }
220
+ return daytona.create(
221
+ { image: "daytonaio/sandbox:latest", resources: HIGH_RESOURCES, envVars: config.env },
222
+ { timeout: 300 }
223
+ );
224
+ }
225
+ function makeAgentContext(sandbox) {
226
+ return {
227
+ exec: async (command, options) => {
228
+ const response = await sandbox.process.executeCommand(
229
+ command,
230
+ import_sandbox.REPO_PATH,
231
+ options?.env,
232
+ options?.timeoutSec ?? 300
233
+ );
234
+ return { exitCode: response.exitCode, output: response.result };
235
+ },
236
+ writeFile: async (path, content) => {
237
+ await sandbox.fs.uploadFile(Buffer.from(content), `${import_sandbox.REPO_PATH}/${path}`);
238
+ }
239
+ };
240
+ }
241
+ function makeSensorRunner(sensor, sandbox, ctx) {
242
+ if (typeof sensor === "function") {
243
+ const repo = new import_repo_handle.RepoHandle(sandbox, import_sandbox.REPO_PATH);
244
+ return () => sensor(repo);
245
+ }
246
+ const extension = sensor.localPath.endsWith(".js") ? ".js" : ".ts";
247
+ const scriptPath = `${import_sandbox.SCRATCH_DIR}/sensor${extension}`;
248
+ const signalsPath = `${import_sandbox.SCRATCH_DIR}/signals.json`;
249
+ let prepared = false;
250
+ return async () => {
251
+ if (!prepared) {
252
+ await sandbox.fs.uploadFile(sensor.localPath, `${import_sandbox.REPO_PATH}/${scriptPath}`);
253
+ await ctx.writeFile(
254
+ `${import_sandbox.SCRATCH_DIR}/run-sensor.ts`,
255
+ `import { writeFileSync } from "node:fs";
256
+ const mod = await import("./sensor${extension}");
257
+ const signals = await mod.default();
258
+ writeFileSync("${signalsPath}", JSON.stringify(signals));
259
+ `
260
+ );
261
+ const bun = await ctx.exec("command -v bun || npm install -g bun", { timeoutSec: 600 });
262
+ if (bun.exitCode !== 0)
263
+ throw new Error(`failed to install bun for the sensor script: ${bun.output}`);
264
+ prepared = true;
265
+ }
266
+ const run = await ctx.exec(`bun ${import_sandbox.SCRATCH_DIR}/run-sensor.ts`, { timeoutSec: 600 });
267
+ if (run.exitCode !== 0) throw new Error(`sensor script exited ${run.exitCode}: ${run.output}`);
268
+ const raw = await sandbox.fs.downloadFile(`${import_sandbox.REPO_PATH}/${signalsPath}`);
269
+ return JSON.parse(raw.toString("utf-8"));
270
+ };
271
+ }
272
+ function isFeedback(comment) {
273
+ return comment.body.trimStart().startsWith("/feedback");
274
+ }
275
+ function stripFeedbackPrefix(body) {
276
+ return body.trimStart().replace(/^\/feedback\s*/, "");
277
+ }
278
+ function slugify(label) {
279
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
280
+ }
281
+ // Annotate the CommonJS export names for ESM import in node:
282
+ 0 && (module.exports = {
283
+ ControlLoop
284
+ });
285
+ //# sourceMappingURL=control-loop.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/control-loop.ts"],
4
+ "sourcesContent": ["import { Daytona, type Sandbox } from \"@daytona/sdk\";\nimport type { AgentContext } from \"./agents\";\nimport type { PrComment } from \"./github\";\nimport { RepoHandle } from \"./repo-handle\";\nimport { RunLog } from \"./log\";\nimport * as actuators from \"./actuators\";\nimport * as pickers from \"./pickers\";\nimport { runCli } from \"./cli\";\nimport { parseClaimedKeys, signalKey } from \"./claims\";\nimport {\n REPO_PATH,\n SCRATCH_DIR,\n appendMemory,\n commitAll,\n memoryNote,\n readMemory,\n runConfiguredCommand,\n} from \"./sandbox\";\nimport type { Sensor } from \"./sensors\";\nimport type {\n ApplyCommentsResult,\n ControlLoopConfig,\n ControlLoopOptions,\n ControlLoopRunResult,\n Signal,\n} from \"./types\";\n\nconst BOT_NAME = \"control-loop[bot]\";\n/** Committer email for the loop's commits when the git source doesn't specify one. */\nconst DEFAULT_BOT_EMAIL = \"control-loop@users.noreply.github.com\";\n\nexport class ControlLoop<TData = unknown> {\n constructor(private readonly options: ControlLoopOptions<TData>) {}\n\n /**\n * Executes one full cycle: sense -> pick -> run actuator -> verify -> PR.\n * Invoke this from whatever scheduler you use (cron, CI); the loop itself\n * does not stay resident.\n */\n async run(): Promise<ControlLoopRunResult<TData>> {\n const { config, sensor, picker } = this.options;\n const label = slugify(this.options.label);\n const log = new RunLog(this.options.label);\n\n const maxOpenPrCount = config.maxOpenPrCount;\n if (maxOpenPrCount !== undefined) {\n const openPrCount = await config.git.countOpenPrs(label);\n if (openPrCount >= maxOpenPrCount) {\n const reason = `${openPrCount} open PR(s) labeled \"${label}\" (max ${maxOpenPrCount})`;\n log.skip(reason);\n return { status: \"skipped\", reason };\n }\n }\n\n return this.withProvisionedSandbox(log, async (sandbox, ctx) => {\n const sense = makeSensorRunner(sensor, sandbox, ctx);\n\n const sensed = await log.step(\"sensing for signals\", sense);\n if (sensed.length === 0) {\n log.finish(\"clean \u2014 no signals detected\");\n return { status: \"clean\" };\n }\n log.info(`sensed ${sensed.length} signal(s)`);\n\n // Signals already claimed by an open PR of this loop are excluded, so concurrent\n // runs don't fix the same thing twice while earlier PRs await merge.\n const claimed = new Set(\n (await config.git.listOpenPrBodies(label)).flatMap(parseClaimedKeys),\n );\n const signals = sensed.filter((signal) => !claimed.has(signalKey(signal)));\n if (signals.length < sensed.length) {\n log.info(`${sensed.length - signals.length} signal(s) already claimed by open PRs`);\n }\n if (signals.length === 0) {\n log.finish(\"all sensed signals are already claimed by open PRs\");\n return { status: \"skipped\", reason: \"all sensed signals are claimed by open PRs\" };\n }\n\n const picked = await (picker ?? pickers.count<TData>(1))(signals);\n if (picked.length === 0) {\n log.finish(\"clean \u2014 picker selected nothing\");\n return { status: \"clean\" };\n }\n log.info(`picked ${picked.length} of ${signals.length} signal(s) to act on`);\n\n // The actuator owns everything past this point \u2014 what to do with the picked\n // signals and how to report the outcome. The default `AgentPrActuator` runs the\n // agent per signal and opens a PR; other actuators may do neither.\n return this.options.actuator.act({\n signals: picked,\n sandbox,\n ctx,\n sense,\n config,\n label,\n displayLabel: this.options.label,\n log,\n });\n });\n }\n\n /**\n * Creates a sandbox, clones `branch` (default: the base branch), configures git, and\n * runs the `setup` hook \u2014 the provisioning every entry point shares \u2014 then invokes `fn`\n * with the ready sandbox and its {@link AgentContext}. The sandbox is always torn down\n * afterward, including when provisioning or `fn` throws.\n */\n private async withProvisionedSandbox<T>(\n log: RunLog,\n fn: (sandbox: Sandbox, ctx: AgentContext) => Promise<T>,\n branch?: string,\n ): Promise<T> {\n const { config } = this.options;\n const sandbox = await log.step(\"spinning up sandbox\", () => createSandbox(config));\n log.attach(sandbox.id);\n\n try {\n await log.step(`cloning ${branch ?? config.git.branch} into ${REPO_PATH}`, async () => {\n await config.git.cloneInto(sandbox, REPO_PATH, branch);\n await sandbox.git.configureUser(BOT_NAME, config.git.email ?? DEFAULT_BOT_EMAIL, \"local\", REPO_PATH);\n });\n\n const ctx = makeAgentContext(sandbox);\n await ctx.exec(`mkdir -p ${SCRATCH_DIR}`);\n\n await runConfiguredCommand(\"setup\", config, ctx, log);\n\n return await fn(sandbox, ctx);\n } finally {\n await log.step(\"tearing down sandbox\", () => sandbox.delete());\n }\n }\n\n /**\n * Provisions a sandbox the way {@link run} does up through the `setup` hook \u2014 clone,\n * configure git, run `config.hooks.setup` \u2014 then captures its filesystem as a Daytona\n * snapshot named `name` and returns, without sensing, touching the agent, or opening a\n * PR. Pass the same `name` as `config.sandbox.snapshot` on later runs so they start from\n * the baked-in toolchain instead of re-running `setup` every time.\n *\n * Call this instead of `run` (e.g. as a one-off provisioning step); `name` is required.\n */\n async saveSnapshot(name: string): Promise<void> {\n const log = new RunLog(this.options.label);\n\n await this.withProvisionedSandbox(log, async (sandbox, ctx) => {\n // The scratch dir is per-run bookkeeping (prompts, sensor scripts); keep it out of\n // the snapshot so it doesn't leak into runs created from it.\n await ctx.exec(`rm -rf ${SCRATCH_DIR}`);\n\n // 0 = no timeout: baking a real repo's toolchain can take well past the SDK's\n // 60-second default.\n await log.step(`saving snapshot ${name}`, () => sandbox._experimental_createSnapshot(name, 0));\n log.finish(`snapshot saved \u2192 ${name}`);\n });\n }\n\n /**\n * Handler for PR comment events: reads the comments left on the PR since its last\n * commit, has the agent apply them on the PR's branch, and pushes \u2014 updating the PR\n * in place. Wire this to your webhook (or poller) for PR comment activity.\n *\n * Comments older than the PR's head commit are treated as already addressed, which\n * makes repeated triggers for the same comment thread safe.\n */\n async applyPrComments(prNumber: number): Promise<ApplyCommentsResult> {\n const { config, commentActuator } = this.options;\n const label = slugify(this.options.label);\n const log = new RunLog(`${this.options.label} \u00B7 PR #${prNumber}`);\n\n const pr = await config.git.getPr(prNumber);\n const newComments = (await config.git.listPrComments(prNumber)).filter(\n (comment) => new Date(comment.createdAt) > new Date(pr.headCommittedAt),\n );\n if (newComments.length === 0) {\n log.skip(\"no comments newer than the PR head\");\n return { status: \"no-new-comments\" };\n }\n\n // \"/feedback ...\" comments are applied to the PR like any other, but their text is\n // also persisted to the loop's memory file so every future run learns from them.\n // The prefix is stripped so the agent sees plain instructions.\n const comments = newComments.map((comment) =>\n isFeedback(comment) ? { ...comment, body: stripFeedbackPrefix(comment.body) } : comment,\n );\n const feedback = comments.filter((_, index) => isFeedback(newComments[index]!));\n log.info(`applying ${comments.length} new comment(s)`);\n\n return this.withProvisionedSandbox(\n log,\n async (sandbox, ctx) => {\n await log.step(\"preparing agent\", () => config.agent.prepare(ctx));\n\n // Memory is read before appending, so the agent gets past feedback while the new\n // feedback reaches it through the comments themselves.\n const memory = await readMemory(ctx, label);\n if (feedback.length > 0) {\n await appendMemory(ctx, label, memory, feedback);\n }\n\n const { instructions } = await (commentActuator ?? actuators.prComments)(comments);\n await log.step(\"agent applying comments\", () =>\n config.agent.executeAgent(instructions + memoryNote(memory), ctx),\n );\n\n await ctx.exec(`rm -rf ${SCRATCH_DIR}`);\n await runConfiguredCommand(\"preCommit\", config, ctx, log);\n const status = await ctx.exec(\"git status --porcelain\");\n if (status.output.trim() === \"\") {\n log.finish(\"no changes \u2014 agent left the tree untouched\");\n return { status: \"no-changes\", comments };\n }\n\n await log.step(\"committing & pushing\", async () => {\n await commitAll(ctx, \"fix: address PR review comments\");\n await config.git.push(sandbox, REPO_PATH, pr.branch);\n });\n log.finish(`comments applied to ${pr.branch}`);\n return { status: \"comments-applied\", branch: pr.branch, comments };\n },\n pr.branch,\n );\n }\n\n /**\n * Ready-made CLI for driving this loop from CI (or a shell). Declare the loop, then call\n * this once at the end of the file:\n *\n * const loop = new ControlLoop({ ... });\n * loop.cli();\n *\n * Two subcommands: `run` (one full cycle \u2192 {@link run}) and `apply-comments <pr>`\n * (apply new PR review comments \u2192 {@link applyPrComments}). Run with `--help` for usage.\n * Parses `process.argv` by default; the process exits non-zero on error or an unresolved\n * fix, so CI can gate on it.\n */\n cli(argv: string[] = process.argv): Promise<void> {\n return runCli(this, { name: slugify(this.options.label), description: this.options.label }, argv);\n }\n}\n\n/**\n * Every sandbox runs on a \"high\" machine: Daytona's default tier (1 vCPU / 1 GiB RAM\n * / 3 GiB disk) OOM-kills dependency installs on real repos (`setup` exiting 137),\n * and monorepo installs + an agent benefit from the extra headroom.\n */\nconst HIGH_RESOURCES = { cpu: 4, memory: 8, disk: 10 };\n\nasync function createSandbox(config: ControlLoopConfig): Promise<Sandbox> {\n // Reads DAYTONA_API_KEY / DAYTONA_API_URL / DAYTONA_TARGET from the environment.\n const daytona = new Daytona();\n\n if (config.sandbox?.snapshot) {\n // Daytona rejects resources when creating from a snapshot (\"Cannot specify Sandbox\n // resources when using a snapshot\") \u2014 a snapshot's resources are baked in, so give\n // yours a medium-or-bigger tier when creating it.\n return daytona.create({ snapshot: config.sandbox.snapshot, envVars: config.env });\n }\n\n // Resources can only be requested for image-based creation. daytonaio/sandbox is the\n // image behind Daytona's default snapshot (bun/node/git included); the built snapshot\n // is cached by Daytona, so this is only slow the very first time.\n return daytona.create(\n { image: \"daytonaio/sandbox:latest\", resources: HIGH_RESOURCES, envVars: config.env },\n { timeout: 300 },\n );\n}\n\nfunction makeAgentContext(sandbox: Sandbox): AgentContext {\n return {\n exec: async (command, options) => {\n const response = await sandbox.process.executeCommand(\n command,\n REPO_PATH,\n options?.env,\n options?.timeoutSec ?? 300,\n );\n return { exitCode: response.exitCode, output: response.result };\n },\n writeFile: async (path, content) => {\n await sandbox.fs.uploadFile(Buffer.from(content), `${REPO_PATH}/${path}`);\n },\n };\n}\n\n/**\n * Normalizes both sensor flavors into a re-runnable `() => Promise<Signal[]>`.\n *\n * Script sensors are uploaded into the repo's scratch dir and executed with bun, with\n * the repo root as cwd. The generated runner writes the signals to a JSON file rather\n * than stdout, so the user's script is free to log without corrupting the result.\n */\nfunction makeSensorRunner<TData>(\n sensor: Sensor<TData>,\n sandbox: Sandbox,\n ctx: AgentContext,\n): () => Promise<Signal<TData>[]> {\n if (typeof sensor === \"function\") {\n const repo = new RepoHandle(sandbox, REPO_PATH);\n return () => sensor(repo);\n }\n\n const extension = sensor.localPath.endsWith(\".js\") ? \".js\" : \".ts\";\n const scriptPath = `${SCRATCH_DIR}/sensor${extension}`;\n const signalsPath = `${SCRATCH_DIR}/signals.json`;\n let prepared = false;\n\n return async () => {\n if (!prepared) {\n await sandbox.fs.uploadFile(sensor.localPath, `${REPO_PATH}/${scriptPath}`);\n await ctx.writeFile(\n `${SCRATCH_DIR}/run-sensor.ts`,\n `import { writeFileSync } from \"node:fs\";\\n` +\n `const mod = await import(\"./sensor${extension}\");\\n` +\n `const signals = await mod.default();\\n` +\n `writeFileSync(\"${signalsPath}\", JSON.stringify(signals));\\n`,\n );\n const bun = await ctx.exec(\"command -v bun || npm install -g bun\", { timeoutSec: 600 });\n if (bun.exitCode !== 0)\n throw new Error(`failed to install bun for the sensor script: ${bun.output}`);\n prepared = true;\n }\n\n const run = await ctx.exec(`bun ${SCRATCH_DIR}/run-sensor.ts`, { timeoutSec: 600 });\n if (run.exitCode !== 0) throw new Error(`sensor script exited ${run.exitCode}: ${run.output}`);\n const raw = await sandbox.fs.downloadFile(`${REPO_PATH}/${signalsPath}`);\n return JSON.parse(raw.toString(\"utf-8\")) as Signal<TData>[];\n };\n}\n\nfunction isFeedback(comment: PrComment): boolean {\n return comment.body.trimStart().startsWith(\"/feedback\");\n}\n\nfunction stripFeedbackPrefix(body: string): string {\n return body.trimStart().replace(/^\\/feedback\\s*/, \"\");\n}\n\nfunction slugify(label: string): string {\n return label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,98 @@
1
+ import type { Sandbox } from "@daytona/sdk";
2
+ /** A human comment on a PR. `file`/`line` are present for inline review comments only. */
3
+ export interface PrComment {
4
+ id: number;
5
+ author: string;
6
+ body: string;
7
+ file?: string;
8
+ line?: number;
9
+ createdAt: string;
10
+ }
11
+ /**
12
+ * Where the code comes from and where the PR goes. Implemented by {@link Github};
13
+ * the interface exists so other forges (GitLab, ...) can slot in later.
14
+ */
15
+ export interface GitSource {
16
+ /** Base branch the loop clones from and targets with its PR. */
17
+ readonly branch: string;
18
+ /**
19
+ * Committer email stamped on the loop's commits (the ones that make up the PR).
20
+ * Optional — the loop falls back to a default no-reply address when unset.
21
+ */
22
+ readonly email?: string;
23
+ countOpenPrs(label: string): Promise<number>;
24
+ /** Bodies of the open PRs carrying the loop's label; used to skip already-claimed signals. */
25
+ listOpenPrBodies(label: string): Promise<string[]>;
26
+ /** Clones `branch` (defaults to the base branch) into the sandbox at `path`. */
27
+ cloneInto(sandbox: Sandbox, path: string, branch?: string): Promise<void>;
28
+ push(sandbox: Sandbox, path: string, branch: string): Promise<void>;
29
+ openPr(params: {
30
+ branch: string;
31
+ title: string;
32
+ body: string;
33
+ label: string;
34
+ }): Promise<string>;
35
+ /** Head branch of the PR and the commit date of its tip, used to skip already-addressed comments. */
36
+ getPr(prNumber: number): Promise<{
37
+ branch: string;
38
+ headCommittedAt: string;
39
+ }>;
40
+ /**
41
+ * Comments on the PR — inline review comments and PR-level comments — oldest first.
42
+ * Inline comments whose review thread has been resolved are excluded.
43
+ */
44
+ listPrComments(prNumber: number): Promise<PrComment[]>;
45
+ }
46
+ export type GithubProps = {
47
+ /** e.g. "https://github.com/botpress/some-repo.git" */
48
+ repo: string;
49
+ branch: string;
50
+ /**
51
+ * GitHub token. Optional for cloning/counting PRs on public repos, but required to
52
+ * push and open PRs — a run that reaches the PR stage without one fails there.
53
+ */
54
+ key?: string;
55
+ /**
56
+ * Committer email stamped on the loop's commits. Optional; set it to attribute the
57
+ * PR's commits to a specific bot/user account instead of the default no-reply address.
58
+ */
59
+ email?: string;
60
+ };
61
+ export declare class Github implements GitSource {
62
+ readonly branch: string;
63
+ readonly email?: string;
64
+ private readonly repoUrl;
65
+ private readonly owner;
66
+ private readonly name;
67
+ private readonly key?;
68
+ private readonly octokit;
69
+ constructor(props: GithubProps);
70
+ countOpenPrs(label: string): Promise<number>;
71
+ listOpenPrBodies(label: string): Promise<string[]>;
72
+ private openPrIssues;
73
+ cloneInto(sandbox: Sandbox, path: string, branch?: string): Promise<void>;
74
+ getPr(prNumber: number): Promise<{
75
+ branch: string;
76
+ headCommittedAt: string;
77
+ }>;
78
+ listPrComments(prNumber: number): Promise<PrComment[]>;
79
+ push(sandbox: Sandbox, path: string, branch: string): Promise<void>;
80
+ openPr(params: {
81
+ branch: string;
82
+ title: string;
83
+ body: string;
84
+ label: string;
85
+ }): Promise<string>;
86
+ /** Retries a request a few times; the label call must not be abandoned over a transient error. */
87
+ private retry;
88
+ /** Best-effort PR close used to roll back a partially-created PR; a failure here is swallowed. */
89
+ private closePrQuietly;
90
+ private ensureLabel;
91
+ /**
92
+ * IDs of inline review comments whose thread has been resolved. Thread resolution
93
+ * is only exposed by GitHub's GraphQL API, which always requires auth — without a
94
+ * key, nothing is filtered.
95
+ */
96
+ private resolvedReviewCommentIds;
97
+ private requireKey;
98
+ }