@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.
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/dist/actuators/actuator.d.ts +13 -0
- package/dist/actuators/actuator.js +30 -0
- package/dist/actuators/actuator.js.map +7 -0
- package/dist/actuators/agent-pr.d.ts +25 -0
- package/dist/actuators/agent-pr.js +119 -0
- package/dist/actuators/agent-pr.js.map +7 -0
- package/dist/actuators/comments.d.ts +3 -0
- package/dist/actuators/comments.js +42 -0
- package/dist/actuators/comments.js.map +7 -0
- package/dist/actuators/index.d.ts +54 -0
- package/dist/actuators/index.js +40 -0
- package/dist/actuators/index.js.map +7 -0
- package/dist/actuators/instructions.d.ts +9 -0
- package/dist/actuators/instructions.js +62 -0
- package/dist/actuators/instructions.js.map +7 -0
- package/dist/actuators.d.ts +72 -0
- package/dist/actuators.js +173 -0
- package/dist/actuators.js.map +7 -0
- package/dist/agents.d.ts +53 -0
- package/dist/agents.js +107 -0
- package/dist/agents.js.map +7 -0
- package/dist/claims.d.ts +16 -0
- package/dist/claims.js +51 -0
- package/dist/claims.js.map +7 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +94 -0
- package/dist/cli.js.map +7 -0
- package/dist/control-loop.d.ts +50 -0
- package/dist/control-loop.js +285 -0
- package/dist/control-loop.js.map +7 -0
- package/dist/github.d.ts +98 -0
- package/dist/github.js +258 -0
- package/dist/github.js.map +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +7 -0
- package/dist/log.d.ts +22 -0
- package/dist/log.js +87 -0
- package/dist/log.js.map +7 -0
- package/dist/pickers/busiest-file.d.ts +9 -0
- package/dist/pickers/busiest-file.js +48 -0
- package/dist/pickers/busiest-file.js.map +7 -0
- package/dist/pickers/count.d.ts +6 -0
- package/dist/pickers/count.js +29 -0
- package/dist/pickers/count.js.map +7 -0
- package/dist/pickers/index.d.ts +12 -0
- package/dist/pickers/index.js +41 -0
- package/dist/pickers/index.js.map +7 -0
- package/dist/pickers.d.ts +21 -0
- package/dist/pickers.js +61 -0
- package/dist/pickers.js.map +7 -0
- package/dist/repo-handle.d.ts +35 -0
- package/dist/repo-handle.js +75 -0
- package/dist/repo-handle.js.map +7 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.js +91 -0
- package/dist/sandbox.js.map +7 -0
- package/dist/sensors/ast-grep.d.ts +41 -0
- package/dist/sensors/ast-grep.js +65 -0
- package/dist/sensors/ast-grep.js.map +7 -0
- package/dist/sensors/grep.d.ts +31 -0
- package/dist/sensors/grep.js +39 -0
- package/dist/sensors/grep.js.map +7 -0
- package/dist/sensors/index.d.ts +27 -0
- package/dist/sensors/index.js +38 -0
- package/dist/sensors/index.js.map +7 -0
- package/dist/sensors/react-doctor.d.ts +43 -0
- package/dist/sensors/react-doctor.js +71 -0
- package/dist/sensors/react-doctor.js.map +7 -0
- package/dist/sensors/script.d.ts +7 -0
- package/dist/sensors/script.js +32 -0
- package/dist/sensors/script.js.map +7 -0
- package/dist/sensors.d.ts +117 -0
- package/dist/sensors.js +126 -0
- package/dist/sensors.js.map +7 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +7 -0
- package/package.json +64 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pickers_exports = {};
|
|
20
|
+
__export(pickers_exports, {
|
|
21
|
+
busiestFile: () => import_busiest_file.busiestFile,
|
|
22
|
+
chain: () => chain,
|
|
23
|
+
count: () => import_count.count
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(pickers_exports);
|
|
26
|
+
var import_count = require("./count");
|
|
27
|
+
var import_busiest_file = require("./busiest-file");
|
|
28
|
+
const chain = (...steps) => async (signals) => {
|
|
29
|
+
let current = signals;
|
|
30
|
+
for (const step of steps) {
|
|
31
|
+
current = await step(current);
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
36
|
+
0 && (module.exports = {
|
|
37
|
+
busiestFile,
|
|
38
|
+
chain,
|
|
39
|
+
count
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/pickers/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Signal } from \"../types\";\n\n/** Chooses which of the sensed signals to act on this run. */\nexport type Picker<TData = unknown> = (\n signals: Signal<TData>[],\n) => Promise<Signal<TData>[]> | Signal<TData>[];\n\nexport { count } from \"./count\";\nexport { busiestFile } from \"./busiest-file\";\n\n/**\n * Runs pickers left-to-right, each narrowing the output of the previous \u2014 e.g.\n * `chain(busiestFile(), count(3))` focuses one file, then caps it at 3 signals.\n * Order matters: every step only sees what the one before it kept. With no steps\n * it passes the signals through untouched.\n */\nexport const chain =\n <TData = unknown>(...steps: Picker<TData>[]): Picker<TData> =>\n async (signals: Signal<TData>[]) => {\n let current = signals;\n for (const step of steps) {\n current = await step(current);\n }\n return current;\n };\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,mBAAsB;AACtB,0BAA4B;AAQrB,MAAM,QACX,IAAqB,UACrB,OAAO,YAA6B;AAClC,MAAI,UAAU;AACd,aAAW,QAAQ,OAAO;AACxB,cAAU,MAAM,KAAK,OAAO;AAAA,EAC9B;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Picker } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Default picker: takes the first `count` signals in sensor order.
|
|
4
|
+
* Sort inside your sensor (or write your own picker) to prioritize differently.
|
|
5
|
+
*/
|
|
6
|
+
export declare const count: <TData = unknown>(count: number) => Picker<TData>;
|
|
7
|
+
/**
|
|
8
|
+
* Picker: takes every signal belonging to the single busiest file — the file with
|
|
9
|
+
* the most signals this run. Focuses one run on one file so all its fixes land in a
|
|
10
|
+
* single, coherent PR. Ties are broken toward the file whose first signal appears
|
|
11
|
+
* earliest in sensor order. Signals without a `location` have no file to group by and
|
|
12
|
+
* are ignored (they can never be the busiest file).
|
|
13
|
+
*/
|
|
14
|
+
export declare const busiestFile: <TData = unknown>() => Picker<TData>;
|
|
15
|
+
/**
|
|
16
|
+
* Runs pickers left-to-right, each narrowing the output of the previous — e.g.
|
|
17
|
+
* `chain(busiestFile(), count(3))` focuses one file, then caps it at 3 signals.
|
|
18
|
+
* Order matters: every step only sees what the one before it kept. With no steps
|
|
19
|
+
* it passes the signals through untouched.
|
|
20
|
+
*/
|
|
21
|
+
export declare const chain: <TData = unknown>(...steps: Picker<TData>[]) => Picker<TData>;
|
package/dist/pickers.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pickers_exports = {};
|
|
20
|
+
__export(pickers_exports, {
|
|
21
|
+
busiestFile: () => busiestFile,
|
|
22
|
+
chain: () => chain,
|
|
23
|
+
count: () => count
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(pickers_exports);
|
|
26
|
+
var import_types = require("./types");
|
|
27
|
+
const count = (count2) => (signals) => signals.slice(0, count2);
|
|
28
|
+
const busiestFile = () => (signals) => {
|
|
29
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const signal of signals) {
|
|
31
|
+
const file = signal.location?.file;
|
|
32
|
+
if (file === void 0) continue;
|
|
33
|
+
const group = byFile.get(file);
|
|
34
|
+
if (group) {
|
|
35
|
+
group.push(signal);
|
|
36
|
+
} else {
|
|
37
|
+
byFile.set(file, [signal]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let winner = [];
|
|
41
|
+
for (const group of byFile.values()) {
|
|
42
|
+
if (group.length > winner.length) {
|
|
43
|
+
winner = group;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return winner;
|
|
47
|
+
};
|
|
48
|
+
const chain = (...steps) => async (signals) => {
|
|
49
|
+
let current = signals;
|
|
50
|
+
for (const step of steps) {
|
|
51
|
+
current = await step(current);
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
};
|
|
55
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
56
|
+
0 && (module.exports = {
|
|
57
|
+
busiestFile,
|
|
58
|
+
chain,
|
|
59
|
+
count
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=pickers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/pickers.ts"],
|
|
4
|
+
"sourcesContent": ["import { type Signal, type Picker } from \"./types\";\n\n/**\n * Default picker: takes the first `count` signals in sensor order.\n * Sort inside your sensor (or write your own picker) to prioritize differently.\n */\nexport const count =\n <TData = unknown>(count: number): Picker<TData> =>\n (signals: Signal<TData>[]) =>\n signals.slice(0, count);\n\n/**\n * Picker: takes every signal belonging to the single busiest file \u2014 the file with\n * the most signals this run. Focuses one run on one file so all its fixes land in a\n * single, coherent PR. Ties are broken toward the file whose first signal appears\n * earliest in sensor order. Signals without a `location` have no file to group by and\n * are ignored (they can never be the busiest file).\n */\nexport const busiestFile =\n <TData = unknown>(): Picker<TData> =>\n (signals: Signal<TData>[]) => {\n const byFile = new Map<string, Signal<TData>[]>();\n for (const signal of signals) {\n const file = signal.location?.file;\n if (file === undefined) continue;\n const group = byFile.get(file);\n if (group) {\n group.push(signal);\n } else {\n byFile.set(file, [signal]);\n }\n }\n\n let winner: Signal<TData>[] = [];\n for (const group of byFile.values()) {\n if (group.length > winner.length) {\n winner = group;\n }\n }\n return winner;\n};\n\n/**\n * Runs pickers left-to-right, each narrowing the output of the previous \u2014 e.g.\n * `chain(busiestFile(), count(3))` focuses one file, then caps it at 3 signals.\n * Order matters: every step only sees what the one before it kept. With no steps\n * it passes the signals through untouched.\n */\nexport const chain =\n <TData = unknown>(...steps: Picker<TData>[]): Picker<TData> =>\n async (signals: Signal<TData>[]) => {\n let current = signals;\n for (const step of steps) {\n current = await step(current);\n }\n return current;\n };\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyC;AAMlC,MAAM,QACX,CAAkBA,WAClB,CAAC,YACC,QAAQ,MAAM,GAAGA,MAAK;AASnB,MAAM,cACX,MACA,CAAC,YAA6B;AAC9B,QAAM,SAAS,oBAAI,IAA6B;AAChD,aAAW,UAAU,SAAS;AAC5B,UAAM,OAAO,OAAO,UAAU;AAC9B,QAAI,SAAS,OAAW;AACxB,UAAM,QAAQ,OAAO,IAAI,IAAI;AAC7B,QAAI,OAAO;AACT,YAAM,KAAK,MAAM;AAAA,IACnB,OAAO;AACL,aAAO,IAAI,MAAM,CAAC,MAAM,CAAC;AAAA,IAC3B;AAAA,EACF;AAEA,MAAI,SAA0B,CAAC;AAC/B,aAAW,SAAS,OAAO,OAAO,GAAG;AACnC,QAAI,MAAM,SAAS,OAAO,QAAQ;AAChC,eAAS;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAQO,MAAM,QACX,IAAqB,UACrB,OAAO,YAA6B;AAClC,MAAI,UAAU;AACd,aAAW,QAAQ,OAAO;AACxB,cAAU,MAAM,KAAK,OAAO;AAAA,EAC9B;AACA,SAAO;AACT;",
|
|
6
|
+
"names": ["count"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Sandbox } from "@daytona/sdk";
|
|
2
|
+
export interface ExecResult {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
output: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ExecOptions {
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
timeoutSec?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface GrepMatch {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read-oriented view of the cloned repo inside the sandbox, handed to host-side sensors.
|
|
17
|
+
* Every method proxies to the sandbox; paths are relative to the repo root.
|
|
18
|
+
*/
|
|
19
|
+
export declare class RepoHandle {
|
|
20
|
+
private readonly sandbox;
|
|
21
|
+
private readonly repoPath;
|
|
22
|
+
constructor(sandbox: Sandbox, repoPath: string);
|
|
23
|
+
/** Runs a shell command with the repo root as cwd. */
|
|
24
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
25
|
+
readFile(path: string): Promise<string>;
|
|
26
|
+
/** Glob for files, e.g. `glob("*.ts")`. Returned paths are relative to the repo root. */
|
|
27
|
+
glob(pattern: string): Promise<string[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Search file contents, e.g. `grep("TODO:")`. File paths are relative to the repo
|
|
30
|
+
* root. Pass `paths` to restrict the search to those folders (also repo-relative).
|
|
31
|
+
*/
|
|
32
|
+
grep(pattern: string, paths?: string[]): Promise<GrepMatch[]>;
|
|
33
|
+
private resolve;
|
|
34
|
+
private relative;
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var repo_handle_exports = {};
|
|
20
|
+
__export(repo_handle_exports, {
|
|
21
|
+
RepoHandle: () => RepoHandle
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(repo_handle_exports);
|
|
24
|
+
class RepoHandle {
|
|
25
|
+
constructor(sandbox, repoPath) {
|
|
26
|
+
this.sandbox = sandbox;
|
|
27
|
+
this.repoPath = repoPath;
|
|
28
|
+
}
|
|
29
|
+
/** Runs a shell command with the repo root as cwd. */
|
|
30
|
+
async exec(command, options) {
|
|
31
|
+
const response = await this.sandbox.process.executeCommand(
|
|
32
|
+
command,
|
|
33
|
+
this.repoPath,
|
|
34
|
+
options?.env,
|
|
35
|
+
options?.timeoutSec ?? 300
|
|
36
|
+
);
|
|
37
|
+
return { exitCode: response.exitCode, output: response.result };
|
|
38
|
+
}
|
|
39
|
+
async readFile(path) {
|
|
40
|
+
const buffer = await this.sandbox.fs.downloadFile(`${this.repoPath}/${path}`);
|
|
41
|
+
return buffer.toString("utf-8");
|
|
42
|
+
}
|
|
43
|
+
/** Glob for files, e.g. `glob("*.ts")`. Returned paths are relative to the repo root. */
|
|
44
|
+
async glob(pattern) {
|
|
45
|
+
const response = await this.sandbox.fs.searchFiles(this.repoPath, pattern);
|
|
46
|
+
return response.files.map((file) => this.relative(file));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Search file contents, e.g. `grep("TODO:")`. File paths are relative to the repo
|
|
50
|
+
* root. Pass `paths` to restrict the search to those folders (also repo-relative).
|
|
51
|
+
*/
|
|
52
|
+
async grep(pattern, paths) {
|
|
53
|
+
const roots = paths?.length ? paths : ["."];
|
|
54
|
+
const results = await Promise.all(roots.map((root) => this.sandbox.fs.findFiles(this.resolve(root), pattern)));
|
|
55
|
+
return results.flat().map((match) => ({
|
|
56
|
+
file: this.relative(match.file),
|
|
57
|
+
line: match.line,
|
|
58
|
+
content: match.content
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
resolve(path) {
|
|
62
|
+
const trimmed = path.replace(/^\.\/?/, "").replace(/\/+$/, "");
|
|
63
|
+
return trimmed === "" ? this.repoPath : `${this.repoPath}/${trimmed}`;
|
|
64
|
+
}
|
|
65
|
+
relative(file) {
|
|
66
|
+
const marker = `${this.repoPath}/`;
|
|
67
|
+
const index = file.indexOf(marker);
|
|
68
|
+
return index === -1 ? file : file.slice(index + marker.length);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
72
|
+
0 && (module.exports = {
|
|
73
|
+
RepoHandle
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=repo-handle.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/repo-handle.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Sandbox } from \"@daytona/sdk\";\n\nexport interface ExecResult {\n exitCode: number;\n output: string;\n}\n\nexport interface ExecOptions {\n env?: Record<string, string>;\n timeoutSec?: number;\n}\n\nexport interface GrepMatch {\n file: string;\n line: number;\n content: string;\n}\n\n/**\n * Read-oriented view of the cloned repo inside the sandbox, handed to host-side sensors.\n * Every method proxies to the sandbox; paths are relative to the repo root.\n */\nexport class RepoHandle {\n constructor(\n private readonly sandbox: Sandbox,\n private readonly repoPath: string,\n ) {}\n\n /** Runs a shell command with the repo root as cwd. */\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n const response = await this.sandbox.process.executeCommand(\n command,\n this.repoPath,\n options?.env,\n options?.timeoutSec ?? 300,\n );\n return { exitCode: response.exitCode, output: response.result };\n }\n\n async readFile(path: string): Promise<string> {\n const buffer = await this.sandbox.fs.downloadFile(`${this.repoPath}/${path}`);\n return buffer.toString(\"utf-8\");\n }\n\n /** Glob for files, e.g. `glob(\"*.ts\")`. Returned paths are relative to the repo root. */\n async glob(pattern: string): Promise<string[]> {\n const response = await this.sandbox.fs.searchFiles(this.repoPath, pattern);\n return response.files.map((file) => this.relative(file));\n }\n\n /**\n * Search file contents, e.g. `grep(\"TODO:\")`. File paths are relative to the repo\n * root. Pass `paths` to restrict the search to those folders (also repo-relative).\n */\n async grep(pattern: string, paths?: string[]): Promise<GrepMatch[]> {\n const roots = paths?.length ? paths : [\".\"];\n const results = await Promise.all(roots.map((root) => this.sandbox.fs.findFiles(this.resolve(root), pattern)));\n return results.flat().map((match) => ({\n file: this.relative(match.file),\n line: match.line,\n content: match.content,\n }));\n }\n\n private resolve(path: string): string {\n const trimmed = path.replace(/^\\.\\/?/, \"\").replace(/\\/+$/, \"\");\n return trimmed === \"\" ? this.repoPath : `${this.repoPath}/${trimmed}`;\n }\n\n private relative(file: string): string {\n const marker = `${this.repoPath}/`;\n const index = file.indexOf(marker);\n return index === -1 ? file : file.slice(index + marker.length);\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBO,MAAM,WAAW;AAAA,EACtB,YACmB,SACA,UACjB;AAFiB;AACA;AAAA,EAChB;AAAA;AAAA,EAGH,MAAM,KAAK,SAAiB,SAA4C;AACtE,UAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ;AAAA,MAC1C;AAAA,MACA,KAAK;AAAA,MACL,SAAS;AAAA,MACT,SAAS,cAAc;AAAA,IACzB;AACA,WAAO,EAAE,UAAU,SAAS,UAAU,QAAQ,SAAS,OAAO;AAAA,EAChE;AAAA,EAEA,MAAM,SAAS,MAA+B;AAC5C,UAAM,SAAS,MAAM,KAAK,QAAQ,GAAG,aAAa,GAAG,KAAK,QAAQ,IAAI,IAAI,EAAE;AAC5E,WAAO,OAAO,SAAS,OAAO;AAAA,EAChC;AAAA;AAAA,EAGA,MAAM,KAAK,SAAoC;AAC7C,UAAM,WAAW,MAAM,KAAK,QAAQ,GAAG,YAAY,KAAK,UAAU,OAAO;AACzE,WAAO,SAAS,MAAM,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAK,SAAiB,OAAwC;AAClE,UAAM,QAAQ,OAAO,SAAS,QAAQ,CAAC,GAAG;AAC1C,UAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,QAAQ,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,OAAO,CAAC,CAAC;AAC7G,WAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MACpC,MAAM,KAAK,SAAS,MAAM,IAAI;AAAA,MAC9B,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,IACjB,EAAE;AAAA,EACJ;AAAA,EAEQ,QAAQ,MAAsB;AACpC,UAAM,UAAU,KAAK,QAAQ,UAAU,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAC7D,WAAO,YAAY,KAAK,KAAK,WAAW,GAAG,KAAK,QAAQ,IAAI,OAAO;AAAA,EACrE;AAAA,EAEQ,SAAS,MAAsB;AACrC,UAAM,SAAS,GAAG,KAAK,QAAQ;AAC/B,UAAM,QAAQ,KAAK,QAAQ,MAAM;AACjC,WAAO,UAAU,KAAK,OAAO,KAAK,MAAM,QAAQ,OAAO,MAAM;AAAA,EAC/D;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentContext } from "./agents";
|
|
2
|
+
import type { PrComment } from "./github";
|
|
3
|
+
import type { ControlLoopConfig } from "./types";
|
|
4
|
+
import type { RunLog } from "./log";
|
|
5
|
+
/** Where every entry point clones the repo inside the sandbox. */
|
|
6
|
+
export declare const REPO_PATH = "workspace/repo";
|
|
7
|
+
/** Per-run bookkeeping dir (prompts, sensor scripts); never committed to a PR. */
|
|
8
|
+
export declare const SCRATCH_DIR = ".control-loop";
|
|
9
|
+
/**
|
|
10
|
+
* Stages and commits everything with the real git binary. Daytona's toolbox git (go-git)
|
|
11
|
+
* 500s on pathological filenames an agent can leave behind (e.g. a file named `\`);
|
|
12
|
+
* real git handles them.
|
|
13
|
+
*
|
|
14
|
+
* `--no-verify` skips repo git hooks: they target interactive developers (a check-only
|
|
15
|
+
* hook like `oxfmt --check` can never be satisfied by a bot that doesn't know the fix
|
|
16
|
+
* command), and the PR's CI is the authoritative gate for the loop's changes. Repo
|
|
17
|
+
* conventions are applied through `config.hooks.preCommit` instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare function commitAll(ctx: AgentContext, message: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Runs one of the configured lifecycle hooks (`config.hooks`) from the repo root,
|
|
22
|
+
* as a logged step. A missing hook is a no-op; a non-zero exit aborts the run.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runConfiguredCommand(name: keyof NonNullable<ControlLoopConfig["hooks"]>, config: ControlLoopConfig, ctx: AgentContext, log: RunLog): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* The loop's memory lives in the repo itself, so it ships with the PR that recorded it
|
|
27
|
+
* and is available to every future clone once merged.
|
|
28
|
+
*/
|
|
29
|
+
export declare function memoryPath(label: string): string;
|
|
30
|
+
export declare function readMemory(ctx: AgentContext, label: string): Promise<string | undefined>;
|
|
31
|
+
export declare function appendMemory(ctx: AgentContext, label: string, existing: string | undefined, feedback: PrComment[]): Promise<void>;
|
|
32
|
+
/** Appended to the agent's instructions whenever the loop has accumulated memory. */
|
|
33
|
+
export declare function memoryNote(memory: string | undefined): string;
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var sandbox_exports = {};
|
|
20
|
+
__export(sandbox_exports, {
|
|
21
|
+
REPO_PATH: () => REPO_PATH,
|
|
22
|
+
SCRATCH_DIR: () => SCRATCH_DIR,
|
|
23
|
+
appendMemory: () => appendMemory,
|
|
24
|
+
commitAll: () => commitAll,
|
|
25
|
+
memoryNote: () => memoryNote,
|
|
26
|
+
memoryPath: () => memoryPath,
|
|
27
|
+
readMemory: () => readMemory,
|
|
28
|
+
runConfiguredCommand: () => runConfiguredCommand
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(sandbox_exports);
|
|
31
|
+
const REPO_PATH = "workspace/repo";
|
|
32
|
+
const SCRATCH_DIR = ".control-loop";
|
|
33
|
+
async function commitAll(ctx, message) {
|
|
34
|
+
const add = await ctx.exec("git add -A");
|
|
35
|
+
if (add.exitCode !== 0) throw new Error(`git add failed: ${add.output}`);
|
|
36
|
+
const commit = await ctx.exec(`git commit --no-verify -m ${shellQuote(message)}`);
|
|
37
|
+
if (commit.exitCode !== 0) throw new Error(`git commit failed: ${commit.output}`);
|
|
38
|
+
}
|
|
39
|
+
function shellQuote(value) {
|
|
40
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
41
|
+
}
|
|
42
|
+
async function runConfiguredCommand(name, config, ctx, log) {
|
|
43
|
+
const command = config.hooks?.[name];
|
|
44
|
+
if (!command) return;
|
|
45
|
+
await log.step(`running ${name} \`${command}\``, async () => {
|
|
46
|
+
const result = await ctx.exec(command, { timeoutSec: 900 });
|
|
47
|
+
if (result.exitCode !== 0) {
|
|
48
|
+
throw new Error(`${name} command "${command}" exited ${result.exitCode}: ${result.output}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function memoryPath(label) {
|
|
53
|
+
return `.github/control-loop/memory-${label}.md`;
|
|
54
|
+
}
|
|
55
|
+
async function readMemory(ctx, label) {
|
|
56
|
+
const result = await ctx.exec(`cat ${memoryPath(label)}`);
|
|
57
|
+
return result.exitCode === 0 ? result.output : void 0;
|
|
58
|
+
}
|
|
59
|
+
async function appendMemory(ctx, label, existing, feedback) {
|
|
60
|
+
const header = `# Control loop memory \u2014 ${label}
|
|
61
|
+
|
|
62
|
+
Improvements collected from \`/feedback\` PR comments. Every entry is included in the agent's instructions on all future runs of this loop.
|
|
63
|
+
`;
|
|
64
|
+
const entries = feedback.map(
|
|
65
|
+
(comment) => `- ${comment.createdAt.slice(0, 10)} @${comment.author}: ${comment.body.trim()}`
|
|
66
|
+
);
|
|
67
|
+
const content = `${(existing ?? header).trimEnd()}
|
|
68
|
+
${entries.join("\n")}
|
|
69
|
+
`;
|
|
70
|
+
await ctx.exec(`mkdir -p ${memoryPath(label).split("/").slice(0, -1).join("/")}`);
|
|
71
|
+
await ctx.writeFile(memoryPath(label), content);
|
|
72
|
+
}
|
|
73
|
+
function memoryNote(memory) {
|
|
74
|
+
if (!memory) return "";
|
|
75
|
+
return `
|
|
76
|
+
|
|
77
|
+
Control loop memory \u2014 standing guidance accumulated from human feedback on past PRs. Follow it:
|
|
78
|
+
${memory}`;
|
|
79
|
+
}
|
|
80
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
81
|
+
0 && (module.exports = {
|
|
82
|
+
REPO_PATH,
|
|
83
|
+
SCRATCH_DIR,
|
|
84
|
+
appendMemory,
|
|
85
|
+
commitAll,
|
|
86
|
+
memoryNote,
|
|
87
|
+
memoryPath,
|
|
88
|
+
readMemory,
|
|
89
|
+
runConfiguredCommand
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sandbox.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AgentContext } from \"./agents\";\nimport type { PrComment } from \"./github\";\nimport type { ControlLoopConfig } from \"./types\";\nimport type { RunLog } from \"./log\";\n\n/** Where every entry point clones the repo inside the sandbox. */\nexport const REPO_PATH = \"workspace/repo\";\n/** Per-run bookkeeping dir (prompts, sensor scripts); never committed to a PR. */\nexport const SCRATCH_DIR = \".control-loop\";\n\n/**\n * Stages and commits everything with the real git binary. Daytona's toolbox git (go-git)\n * 500s on pathological filenames an agent can leave behind (e.g. a file named `\\`);\n * real git handles them.\n *\n * `--no-verify` skips repo git hooks: they target interactive developers (a check-only\n * hook like `oxfmt --check` can never be satisfied by a bot that doesn't know the fix\n * command), and the PR's CI is the authoritative gate for the loop's changes. Repo\n * conventions are applied through `config.hooks.preCommit` instead.\n */\nexport async function commitAll(ctx: AgentContext, message: string): Promise<void> {\n const add = await ctx.exec(\"git add -A\");\n if (add.exitCode !== 0) throw new Error(`git add failed: ${add.output}`);\n const commit = await ctx.exec(`git commit --no-verify -m ${shellQuote(message)}`);\n if (commit.exitCode !== 0) throw new Error(`git commit failed: ${commit.output}`);\n}\n\n/** Single-quote for the shell; commit messages carry arbitrary sensor/signal text. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Runs one of the configured lifecycle hooks (`config.hooks`) from the repo root,\n * as a logged step. A missing hook is a no-op; a non-zero exit aborts the run.\n */\nexport async function runConfiguredCommand(\n name: keyof NonNullable<ControlLoopConfig[\"hooks\"]>,\n config: ControlLoopConfig,\n ctx: AgentContext,\n log: RunLog,\n): Promise<void> {\n const command = config.hooks?.[name];\n if (!command) return;\n await log.step(`running ${name} \\`${command}\\``, async () => {\n const result = await ctx.exec(command, { timeoutSec: 900 });\n if (result.exitCode !== 0) {\n throw new Error(`${name} command \"${command}\" exited ${result.exitCode}: ${result.output}`);\n }\n });\n}\n\n/**\n * The loop's memory lives in the repo itself, so it ships with the PR that recorded it\n * and is available to every future clone once merged.\n */\nexport function memoryPath(label: string): string {\n return `.github/control-loop/memory-${label}.md`;\n}\n\nexport async function readMemory(ctx: AgentContext, label: string): Promise<string | undefined> {\n const result = await ctx.exec(`cat ${memoryPath(label)}`);\n return result.exitCode === 0 ? result.output : undefined;\n}\n\nexport async function appendMemory(\n ctx: AgentContext,\n label: string,\n existing: string | undefined,\n feedback: PrComment[],\n): Promise<void> {\n const header =\n `# Control loop memory \u2014 ${label}\\n\\n` +\n \"Improvements collected from `/feedback` PR comments. \" +\n \"Every entry is included in the agent's instructions on all future runs of this loop.\\n\";\n const entries = feedback.map(\n (comment) => `- ${comment.createdAt.slice(0, 10)} @${comment.author}: ${comment.body.trim()}`,\n );\n const content = `${(existing ?? header).trimEnd()}\\n${entries.join(\"\\n\")}\\n`;\n await ctx.exec(`mkdir -p ${memoryPath(label).split(\"/\").slice(0, -1).join(\"/\")}`);\n await ctx.writeFile(memoryPath(label), content);\n}\n\n/** Appended to the agent's instructions whenever the loop has accumulated memory. */\nexport function memoryNote(memory: string | undefined): string {\n if (!memory) return \"\";\n return `\\n\\nControl loop memory \u2014 standing guidance accumulated from human feedback on past PRs. Follow it:\\n${memory}`;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMO,MAAM,YAAY;AAElB,MAAM,cAAc;AAY3B,eAAsB,UAAU,KAAmB,SAAgC;AACjF,QAAM,MAAM,MAAM,IAAI,KAAK,YAAY;AACvC,MAAI,IAAI,aAAa,EAAG,OAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,EAAE;AACvE,QAAM,SAAS,MAAM,IAAI,KAAK,6BAA6B,WAAW,OAAO,CAAC,EAAE;AAChF,MAAI,OAAO,aAAa,EAAG,OAAM,IAAI,MAAM,sBAAsB,OAAO,MAAM,EAAE;AAClF;AAGA,SAAS,WAAW,OAAuB;AACzC,SAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,CAAC;AACzC;AAMA,eAAsB,qBACpB,MACA,QACA,KACA,KACe;AACf,QAAM,UAAU,OAAO,QAAQ,IAAI;AACnC,MAAI,CAAC,QAAS;AACd,QAAM,IAAI,KAAK,WAAW,IAAI,MAAM,OAAO,MAAM,YAAY;AAC3D,UAAM,SAAS,MAAM,IAAI,KAAK,SAAS,EAAE,YAAY,IAAI,CAAC;AAC1D,QAAI,OAAO,aAAa,GAAG;AACzB,YAAM,IAAI,MAAM,GAAG,IAAI,aAAa,OAAO,YAAY,OAAO,QAAQ,KAAK,OAAO,MAAM,EAAE;AAAA,IAC5F;AAAA,EACF,CAAC;AACH;AAMO,SAAS,WAAW,OAAuB;AAChD,SAAO,+BAA+B,KAAK;AAC7C;AAEA,eAAsB,WAAW,KAAmB,OAA4C;AAC9F,QAAM,SAAS,MAAM,IAAI,KAAK,OAAO,WAAW,KAAK,CAAC,EAAE;AACxD,SAAO,OAAO,aAAa,IAAI,OAAO,SAAS;AACjD;AAEA,eAAsB,aACpB,KACA,OACA,UACA,UACe;AACf,QAAM,SACJ,gCAA2B,KAAK;AAAA;AAAA;AAAA;AAGlC,QAAM,UAAU,SAAS;AAAA,IACvB,CAAC,YAAY,KAAK,QAAQ,UAAU,MAAM,GAAG,EAAE,CAAC,KAAK,QAAQ,MAAM,KAAK,QAAQ,KAAK,KAAK,CAAC;AAAA,EAC7F;AACA,QAAM,UAAU,IAAI,YAAY,QAAQ,QAAQ,CAAC;AAAA,EAAK,QAAQ,KAAK,IAAI,CAAC;AAAA;AACxE,QAAM,IAAI,KAAK,YAAY,WAAW,KAAK,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC,EAAE;AAChF,QAAM,IAAI,UAAU,WAAW,KAAK,GAAG,OAAO;AAChD;AAGO,SAAS,WAAW,QAAoC;AAC7D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO;AAAA;AAAA;AAAA,EAAwG,MAAM;AACvH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SignalPriority } from "../types";
|
|
2
|
+
import type { SensorFn } from "./index";
|
|
3
|
+
export interface AstGrepMatch {
|
|
4
|
+
file: string;
|
|
5
|
+
/** 1-based, like Signal.location.line. */
|
|
6
|
+
line: number;
|
|
7
|
+
/** Source text of the matched node. */
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AstGrepOptions {
|
|
11
|
+
/** ast-grep pattern, e.g. "console.log($$$ARGS)". */
|
|
12
|
+
pattern: string;
|
|
13
|
+
/** Language to parse, e.g. "typescript" or "tsx". Omit to let ast-grep infer it per file. */
|
|
14
|
+
language?: string;
|
|
15
|
+
/** Applied to every signal. Default "medium". */
|
|
16
|
+
priority?: SignalPriority;
|
|
17
|
+
/**
|
|
18
|
+
* Builds the signal message for a match. Signals are matched across runs by
|
|
19
|
+
* file + message, so keep it stable for a given anomaly. The default includes the
|
|
20
|
+
* matched text, which naturally disappears once the anomaly is fixed.
|
|
21
|
+
*/
|
|
22
|
+
message?: (match: AstGrepMatch) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Paths (relative to the repo root) to scan. Required — scoping the scan keeps the
|
|
25
|
+
* signal set intentional; pass ["."] to explicitly scan the whole repo.
|
|
26
|
+
*/
|
|
27
|
+
paths: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sensor that runs [ast-grep](https://ast-grep.github.io) inside the sandbox and turns
|
|
31
|
+
* every structural match into a signal. The CLI is installed on first use if the
|
|
32
|
+
* sandbox image doesn't have it (pre-bake `@ast-grep/cli` in a snapshot to skip that).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* sensor: sensors.astGrep({
|
|
36
|
+
* pattern: "throw new Error($$$)",
|
|
37
|
+
* language: "ts",
|
|
38
|
+
* paths: ["packages/backend/src/trpc/routers/"],
|
|
39
|
+
* })
|
|
40
|
+
*/
|
|
41
|
+
export declare const astGrep: (options: AstGrepOptions) => SensorFn<AstGrepMatch>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var ast_grep_exports = {};
|
|
20
|
+
__export(ast_grep_exports, {
|
|
21
|
+
astGrep: () => astGrep
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(ast_grep_exports);
|
|
24
|
+
const astGrep = (options) => {
|
|
25
|
+
return async (repo) => {
|
|
26
|
+
const install = await repo.exec("command -v ast-grep || npm install -g @ast-grep/cli", { timeoutSec: 600 });
|
|
27
|
+
if (install.exitCode !== 0) {
|
|
28
|
+
throw new Error(`failed to install ast-grep: ${install.output}`);
|
|
29
|
+
}
|
|
30
|
+
const lang = options.language ? ` --lang ${options.language}` : "";
|
|
31
|
+
const paths = ` ${options.paths.map(shellQuote).join(" ")}`;
|
|
32
|
+
const run = await repo.exec(`ast-grep run --pattern ${shellQuote(options.pattern)}${lang} --json${paths}`, {
|
|
33
|
+
timeoutSec: 600
|
|
34
|
+
});
|
|
35
|
+
if (run.exitCode !== 0) {
|
|
36
|
+
throw new Error(`ast-grep exited ${run.exitCode}: ${run.output}`);
|
|
37
|
+
}
|
|
38
|
+
const results = JSON.parse(run.output);
|
|
39
|
+
return results.map((result) => {
|
|
40
|
+
const match = {
|
|
41
|
+
file: result.file,
|
|
42
|
+
line: result.range.start.line + 1,
|
|
43
|
+
text: result.text
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
location: { file: match.file, line: match.line },
|
|
47
|
+
message: options.message?.(match) ?? defaultMessage(options.pattern, match),
|
|
48
|
+
priority: options.priority ?? "medium",
|
|
49
|
+
data: match
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
function defaultMessage(pattern, match) {
|
|
55
|
+
const text = match.text.split("\n")[0].slice(0, 120);
|
|
56
|
+
return `matches pattern "${pattern}": ${text}`;
|
|
57
|
+
}
|
|
58
|
+
function shellQuote(value) {
|
|
59
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
60
|
+
}
|
|
61
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
62
|
+
0 && (module.exports = {
|
|
63
|
+
astGrep
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=ast-grep.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sensors/ast-grep.ts"],
|
|
4
|
+
"sourcesContent": ["import type { SignalPriority } from \"../types\";\nimport type { SensorFn } from \"./index\";\n\nexport interface AstGrepMatch {\n file: string;\n /** 1-based, like Signal.location.line. */\n line: number;\n /** Source text of the matched node. */\n text: string;\n}\n\nexport interface AstGrepOptions {\n /** ast-grep pattern, e.g. \"console.log($$$ARGS)\". */\n pattern: string;\n /** Language to parse, e.g. \"typescript\" or \"tsx\". Omit to let ast-grep infer it per file. */\n language?: string;\n /** Applied to every signal. Default \"medium\". */\n priority?: SignalPriority;\n /**\n * Builds the signal message for a match. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default includes the\n * matched text, which naturally disappears once the anomaly is fixed.\n */\n message?: (match: AstGrepMatch) => string;\n /**\n * Paths (relative to the repo root) to scan. Required \u2014 scoping the scan keeps the\n * signal set intentional; pass [\".\"] to explicitly scan the whole repo.\n */\n paths: string[];\n}\n\n/**\n * Sensor that runs [ast-grep](https://ast-grep.github.io) inside the sandbox and turns\n * every structural match into a signal. The CLI is installed on first use if the\n * sandbox image doesn't have it (pre-bake `@ast-grep/cli` in a snapshot to skip that).\n *\n * @example\n * sensor: sensors.astGrep({\n * pattern: \"throw new Error($$$)\",\n * language: \"ts\",\n * paths: [\"packages/backend/src/trpc/routers/\"],\n * })\n */\nexport const astGrep = (options: AstGrepOptions): SensorFn<AstGrepMatch> => {\n return async (repo) => {\n const install = await repo.exec(\"command -v ast-grep || npm install -g @ast-grep/cli\", { timeoutSec: 600 });\n if (install.exitCode !== 0) {\n throw new Error(`failed to install ast-grep: ${install.output}`);\n }\n\n const lang = options.language ? ` --lang ${options.language}` : \"\";\n const paths = ` ${options.paths.map(shellQuote).join(\" \")}`;\n const run = await repo.exec(`ast-grep run --pattern ${shellQuote(options.pattern)}${lang} --json${paths}`, {\n timeoutSec: 600,\n });\n if (run.exitCode !== 0) {\n throw new Error(`ast-grep exited ${run.exitCode}: ${run.output}`);\n }\n\n const results = JSON.parse(run.output) as Array<{\n file: string;\n text: string;\n range: { start: { line: number } };\n }>;\n\n return results.map((result) => {\n const match: AstGrepMatch = {\n file: result.file,\n line: result.range.start.line + 1,\n text: result.text,\n };\n return {\n location: { file: match.file, line: match.line },\n message: options.message?.(match) ?? defaultMessage(options.pattern, match),\n priority: options.priority ?? \"medium\",\n data: match,\n };\n });\n };\n};\n\nfunction defaultMessage(pattern: string, match: AstGrepMatch): string {\n const text = match.text.split(\"\\n\")[0]!.slice(0, 120);\n return `matches pattern \"${pattern}\": ${text}`;\n}\n\n/** Single-quote for the shell \u2014 ast-grep patterns are full of `$VAR`s the shell must not expand. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CO,MAAM,UAAU,CAAC,YAAoD;AAC1E,SAAO,OAAO,SAAS;AACrB,UAAM,UAAU,MAAM,KAAK,KAAK,uDAAuD,EAAE,YAAY,IAAI,CAAC;AAC1G,QAAI,QAAQ,aAAa,GAAG;AAC1B,YAAM,IAAI,MAAM,+BAA+B,QAAQ,MAAM,EAAE;AAAA,IACjE;AAEA,UAAM,OAAO,QAAQ,WAAW,WAAW,QAAQ,QAAQ,KAAK;AAChE,UAAM,QAAQ,IAAI,QAAQ,MAAM,IAAI,UAAU,EAAE,KAAK,GAAG,CAAC;AACzD,UAAM,MAAM,MAAM,KAAK,KAAK,0BAA0B,WAAW,QAAQ,OAAO,CAAC,GAAG,IAAI,UAAU,KAAK,IAAI;AAAA,MACzG,YAAY;AAAA,IACd,CAAC;AACD,QAAI,IAAI,aAAa,GAAG;AACtB,YAAM,IAAI,MAAM,mBAAmB,IAAI,QAAQ,KAAK,IAAI,MAAM,EAAE;AAAA,IAClE;AAEA,UAAM,UAAU,KAAK,MAAM,IAAI,MAAM;AAMrC,WAAO,QAAQ,IAAI,CAAC,WAAW;AAC7B,YAAM,QAAsB;AAAA,QAC1B,MAAM,OAAO;AAAA,QACb,MAAM,OAAO,MAAM,MAAM,OAAO;AAAA,QAChC,MAAM,OAAO;AAAA,MACf;AACA,aAAO;AAAA,QACL,UAAU,EAAE,MAAM,MAAM,MAAM,MAAM,MAAM,KAAK;AAAA,QAC/C,SAAS,QAAQ,UAAU,KAAK,KAAK,eAAe,QAAQ,SAAS,KAAK;AAAA,QAC1E,UAAU,QAAQ,YAAY;AAAA,QAC9B,MAAM;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,eAAe,SAAiB,OAA6B;AACpE,QAAM,OAAO,MAAM,KAAK,MAAM,IAAI,EAAE,CAAC,EAAG,MAAM,GAAG,GAAG;AACpD,SAAO,oBAAoB,OAAO,MAAM,IAAI;AAC9C;AAGA,SAAS,WAAW,OAAuB;AACzC,SAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,CAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GrepMatch } from "../repo-handle";
|
|
2
|
+
import type { SignalPriority } from "../types";
|
|
3
|
+
import type { SensorFn } from "./index";
|
|
4
|
+
export interface GrepOptions {
|
|
5
|
+
/** Text/regex pattern searched in file contents. */
|
|
6
|
+
pattern: string;
|
|
7
|
+
/**
|
|
8
|
+
* Paths (relative to the repo root) to search. Required — scoping the search keeps
|
|
9
|
+
* the signal set intentional; pass ["."] to explicitly search the whole repo.
|
|
10
|
+
*/
|
|
11
|
+
paths: string[];
|
|
12
|
+
/** Applied to every signal. Default "medium". */
|
|
13
|
+
priority?: SignalPriority;
|
|
14
|
+
/**
|
|
15
|
+
* Builds the signal message for a match. Signals are matched across runs by
|
|
16
|
+
* file + message, so keep it stable for a given anomaly. The default includes the
|
|
17
|
+
* matched line, which naturally disappears once the anomaly is fixed.
|
|
18
|
+
*/
|
|
19
|
+
message?: (match: GrepMatch) => string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sensor that searches file contents and turns every matching line into a signal.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* sensor: sensors.grep({
|
|
26
|
+
* pattern: "TODO:",
|
|
27
|
+
* paths: ["src/"],
|
|
28
|
+
* message: (match) => `unresolved TODO: ${match.content.trim()}`,
|
|
29
|
+
* })
|
|
30
|
+
*/
|
|
31
|
+
export declare const grep: (options: GrepOptions) => SensorFn<GrepMatch>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var grep_exports = {};
|
|
20
|
+
__export(grep_exports, {
|
|
21
|
+
grep: () => grep
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(grep_exports);
|
|
24
|
+
const grep = (options) => {
|
|
25
|
+
return async (repo) => {
|
|
26
|
+
const matches = await repo.grep(options.pattern, options.paths);
|
|
27
|
+
return matches.map((match) => ({
|
|
28
|
+
location: { file: match.file, line: match.line },
|
|
29
|
+
message: options.message?.(match) ?? `matches "${options.pattern}": ${match.content.trim().slice(0, 120)}`,
|
|
30
|
+
priority: options.priority ?? "medium",
|
|
31
|
+
data: match
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
36
|
+
0 && (module.exports = {
|
|
37
|
+
grep
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=grep.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sensors/grep.ts"],
|
|
4
|
+
"sourcesContent": ["import type { GrepMatch } from \"../repo-handle\";\nimport type { SignalPriority } from \"../types\";\nimport type { SensorFn } from \"./index\";\n\nexport interface GrepOptions {\n /** Text/regex pattern searched in file contents. */\n pattern: string;\n /**\n * Paths (relative to the repo root) to search. Required \u2014 scoping the search keeps\n * the signal set intentional; pass [\".\"] to explicitly search the whole repo.\n */\n paths: string[];\n /** Applied to every signal. Default \"medium\". */\n priority?: SignalPriority;\n /**\n * Builds the signal message for a match. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default includes the\n * matched line, which naturally disappears once the anomaly is fixed.\n */\n message?: (match: GrepMatch) => string;\n}\n\n/**\n * Sensor that searches file contents and turns every matching line into a signal.\n *\n * @example\n * sensor: sensors.grep({\n * pattern: \"TODO:\",\n * paths: [\"src/\"],\n * message: (match) => `unresolved TODO: ${match.content.trim()}`,\n * })\n */\nexport const grep = (options: GrepOptions): SensorFn<GrepMatch> => {\n return async (repo) => {\n const matches = await repo.grep(options.pattern, options.paths);\n return matches.map((match) => ({\n location: { file: match.file, line: match.line },\n message: options.message?.(match) ?? `matches \"${options.pattern}\": ${match.content.trim().slice(0, 120)}`,\n priority: options.priority ?? \"medium\",\n data: match,\n }));\n };\n};\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCO,MAAM,OAAO,CAAC,YAA8C;AACjE,SAAO,OAAO,SAAS;AACrB,UAAM,UAAU,MAAM,KAAK,KAAK,QAAQ,SAAS,QAAQ,KAAK;AAC9D,WAAO,QAAQ,IAAI,CAAC,WAAW;AAAA,MAC7B,UAAU,EAAE,MAAM,MAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC/C,SAAS,QAAQ,UAAU,KAAK,KAAK,YAAY,QAAQ,OAAO,MAAM,MAAM,QAAQ,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;AAAA,MACxG,UAAU,QAAQ,YAAY;AAAA,MAC9B,MAAM;AAAA,IACR,EAAE;AAAA,EACJ;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|