@usepipr/runtime 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/{commands-BC2fTZsi.mjs → commands-Cj8p5IQF.mjs} +1299 -1213
- package/dist/commands-Cj8p5IQF.mjs.map +1 -0
- package/dist/{commands-C7OX2Jnd.d.mts → commands-Zzy2vNDo.d.mts} +49 -127
- package/dist/commands-Zzy2vNDo.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/internal/testing.d.mts +2 -0
- package/dist/internal/testing.mjs +2 -0
- package/dist/pi/runtime-tools-extension.d.mts.map +1 -1
- package/dist/pi/runtime-tools-extension.mjs.map +1 -1
- package/package.json +12 -17
- package/dist/commands-BC2fTZsi.mjs.map +0 -1
- package/dist/commands-C7OX2Jnd.d.mts.map +0 -1
- package/dist/contract-BsL98-bI.mjs +0 -74
- package/dist/contract-BsL98-bI.mjs.map +0 -1
- package/dist/contract-D-zqnrkd.d.mts +0 -10
- package/dist/contract-D-zqnrkd.d.mts.map +0 -1
- package/dist/e2e/action-fixture.d.mts +0 -2
- package/dist/e2e/action-fixture.mjs +0 -2
- package/dist/e2e/pi-contract.d.mts +0 -2
- package/dist/e2e/pi-contract.mjs +0 -2
|
@@ -1,134 +1,55 @@
|
|
|
1
|
-
import { i as piReadOnlyToolNames, r as piProviderProfileSchema, t as parsePiProviderInvocation } from "./contract-BsL98-bI.mjs";
|
|
2
1
|
import { c as resolveReadAtRefRequest, l as unavailableReadAtRefResult, n as boundedLineSlice, r as parseManifestPath, u as createDiffRangeIndex } from "./runtime-tools-core-CW1xenzy.mjs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import { chmod, cp, lstat, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { parseReviewResult, reviewFindingSchema, reviewResultSchema, reviewSchemaExample } from "@usepipr/sdk";
|
|
7
|
+
import { buildPiprPlan, commandPatternParts, embeddedSdkDeclaration, isBuiltinReadOnlyTool, isCommandCaptureToken, isCommandRestCaptureToken, isOptionalCommandPatternPart, isPiprConfigFactory, readSdkDeclarationSourceWithChunk, renderPromptValue, reviewOutputSchemaId, tokenizeCommandPattern, unsupportedCommandRestCaptureError } from "@usepipr/sdk/internal";
|
|
9
8
|
import os from "node:os";
|
|
10
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
12
10
|
import { compact, isPlainObject, uniq, uniqBy } from "lodash-es";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import pino from "pino";
|
|
15
11
|
import picomatch from "picomatch";
|
|
12
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
16
14
|
import { Octokit } from "@octokit/rest";
|
|
17
15
|
import { existsSync, mkdirSync } from "node:fs";
|
|
18
|
-
//#region src/
|
|
19
|
-
const
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (isOptionalPart(part)) {
|
|
38
|
-
const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
|
|
39
|
-
if (nextIndex !== void 0) index = nextIndex;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (isRestCaptureToken(part)) {
|
|
43
|
-
const value = lineTokens.slice(index).join(" ");
|
|
44
|
-
if (!value) return {
|
|
45
|
-
ok: false,
|
|
46
|
-
error: `Expected '${part}'`
|
|
47
|
-
};
|
|
48
|
-
captures[part.slice(1, -4)] = value;
|
|
49
|
-
index = lineTokens.length;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const nextIndex = parsePatternToken(part, lineTokens, index, captures);
|
|
53
|
-
if (nextIndex === void 0) return {
|
|
54
|
-
ok: false,
|
|
55
|
-
error: `Expected '${part}'`
|
|
56
|
-
};
|
|
57
|
-
index = nextIndex;
|
|
58
|
-
}
|
|
59
|
-
if (index !== lineTokens.length) return {
|
|
60
|
-
ok: false,
|
|
61
|
-
error: `Unexpected argument '${lineTokens[index]}'`
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
ok: true,
|
|
65
|
-
value: captures
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function commandPatternPrefixMatches(pattern, line) {
|
|
69
|
-
const patternTokens = patternPartsFor(pattern);
|
|
70
|
-
if (unsupportedRestCaptureError(patternTokens)) return false;
|
|
71
|
-
const lineTokens = tokenize(line);
|
|
72
|
-
let index = 0;
|
|
73
|
-
for (const token of patternTokens) {
|
|
74
|
-
if (isCaptureToken(token) || isOptionalPart(token)) return true;
|
|
75
|
-
if (lineTokens[index] !== token) return false;
|
|
76
|
-
index += 1;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
function parseOptionalPatternPart(pattern, lineTokens, startIndex, captures) {
|
|
81
|
-
const patternTokens = tokenize(pattern);
|
|
82
|
-
if (patternTokens.length === 0 || lineTokens[startIndex] !== patternTokens[0]) return;
|
|
83
|
-
const snapshot = { ...captures };
|
|
84
|
-
let index = startIndex;
|
|
85
|
-
for (const token of patternTokens) {
|
|
86
|
-
const nextIndex = parsePatternToken(token, lineTokens, index, captures);
|
|
87
|
-
if (nextIndex === void 0) {
|
|
88
|
-
for (const key of Object.keys(captures)) delete captures[key];
|
|
89
|
-
Object.assign(captures, snapshot);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
index = nextIndex;
|
|
93
|
-
}
|
|
94
|
-
return index;
|
|
95
|
-
}
|
|
96
|
-
function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
97
|
-
if (isCaptureToken(patternToken)) {
|
|
98
|
-
const value = lineTokens[index];
|
|
99
|
-
if (!value) return;
|
|
100
|
-
captures[patternToken.slice(1, -1)] = value;
|
|
101
|
-
return index + 1;
|
|
102
|
-
}
|
|
103
|
-
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
16
|
+
//#region src/config/config-deps.ts
|
|
17
|
+
const runtimeProvidedPackages = new Set(["@usepipr/sdk", "@types/bun"]);
|
|
18
|
+
async function installConfigDependencies(configDir, options = {}) {
|
|
19
|
+
const packageJsonPath = path.join(configDir, "package.json");
|
|
20
|
+
if (!await fileExists$1(packageJsonPath)) return;
|
|
21
|
+
if (shouldSkipConfigInstall(JSON.parse(await Bun.file(packageJsonPath).text()))) return;
|
|
22
|
+
const bunLockPath = path.join(configDir, "bun.lock");
|
|
23
|
+
if (options.frozen && !await fileExists$1(bunLockPath)) throw new Error(`${configDir}: bun.lock is required when .pipr/package.json declares dependencies. Run \`bun install\` in .pipr/ and commit bun.lock.`);
|
|
24
|
+
await assertBunAvailable();
|
|
25
|
+
const args = ["install", "--ignore-scripts"];
|
|
26
|
+
if (options.frozen) args.push("--frozen-lockfile");
|
|
27
|
+
const proc = Bun.spawn(["bun", ...args], {
|
|
28
|
+
cwd: configDir,
|
|
29
|
+
env: process.env,
|
|
30
|
+
stdout: "pipe",
|
|
31
|
+
stderr: "pipe"
|
|
32
|
+
});
|
|
33
|
+
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
34
|
+
if (exitCode !== 0) throw new Error(`${configDir}: bun install failed (exit ${exitCode}).` + (stderr.trim().length > 0 ? `\n${stderr.trim()}` : ""));
|
|
104
35
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
36
|
+
function shouldSkipConfigInstall(manifest) {
|
|
37
|
+
const packages = [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})];
|
|
38
|
+
return packages.length === 0 || packages.every((name) => runtimeProvidedPackages.has(name));
|
|
107
39
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
40
|
+
async function assertBunAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
if (await Bun.spawn(["bun", "--version"], {
|
|
43
|
+
env: process.env,
|
|
44
|
+
stdout: "pipe",
|
|
45
|
+
stderr: "pipe"
|
|
46
|
+
}).exited !== 0) throw new Error("bun is required on PATH to install .pipr/package.json dependencies. Install Bun from https://bun.sh");
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error("bun is required on PATH to install .pipr/package.json dependencies. Install Bun from https://bun.sh");
|
|
116
49
|
}
|
|
117
50
|
}
|
|
118
|
-
function
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
function isOptionalPart(value) {
|
|
122
|
-
return value.startsWith("[") && value.endsWith("]");
|
|
123
|
-
}
|
|
124
|
-
function isCaptureToken(value) {
|
|
125
|
-
return /^<[a-z0-9-]+(\.\.\.)?>$/.test(value);
|
|
126
|
-
}
|
|
127
|
-
function isRestCaptureToken(value) {
|
|
128
|
-
return /^<[a-z0-9-]+\.\.\.>$/.test(value);
|
|
129
|
-
}
|
|
130
|
-
function tokenize(value) {
|
|
131
|
-
return value.trim().split(/\s+/).filter(Boolean);
|
|
51
|
+
async function fileExists$1(filePath) {
|
|
52
|
+
return await Bun.file(filePath).exists();
|
|
132
53
|
}
|
|
133
54
|
//#endregion
|
|
134
55
|
//#region src/config/paths.ts
|
|
@@ -153,46 +74,86 @@ function toGitPath(filePath) {
|
|
|
153
74
|
return filePath === "." ? "." : filePath.split(path.sep).join("/");
|
|
154
75
|
}
|
|
155
76
|
//#endregion
|
|
156
|
-
//#region src/
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
77
|
+
//#region src/pi/contract.ts
|
|
78
|
+
const piThinkingLevels = [
|
|
79
|
+
"off",
|
|
80
|
+
"minimal",
|
|
81
|
+
"low",
|
|
82
|
+
"medium",
|
|
83
|
+
"high",
|
|
84
|
+
"xhigh"
|
|
85
|
+
];
|
|
86
|
+
const piBuiltinToolNames = [
|
|
87
|
+
"read",
|
|
88
|
+
"bash",
|
|
89
|
+
"edit",
|
|
90
|
+
"write",
|
|
91
|
+
"grep",
|
|
92
|
+
"find",
|
|
93
|
+
"ls"
|
|
94
|
+
];
|
|
95
|
+
const piReadOnlyToolNames = [
|
|
96
|
+
"read",
|
|
97
|
+
"grep",
|
|
98
|
+
"find",
|
|
99
|
+
"ls"
|
|
100
|
+
];
|
|
101
|
+
const piRequiredCliFlags = [
|
|
102
|
+
"--provider",
|
|
103
|
+
"--model",
|
|
104
|
+
"--system-prompt",
|
|
105
|
+
"--mode",
|
|
106
|
+
"--print",
|
|
107
|
+
"--no-session",
|
|
108
|
+
"--session-dir",
|
|
109
|
+
"--tools",
|
|
110
|
+
"--extension",
|
|
111
|
+
"--no-context-files",
|
|
112
|
+
"--no-approve",
|
|
113
|
+
"--no-extensions",
|
|
114
|
+
"--no-skills",
|
|
115
|
+
"--no-prompt-templates",
|
|
116
|
+
"--no-themes",
|
|
117
|
+
"--thinking"
|
|
118
|
+
];
|
|
119
|
+
const nonEmptyStringSchema$1 = z.string().min(1);
|
|
120
|
+
const piProviderIdSchema = z.string().regex(/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/);
|
|
121
|
+
const piApiKeyEnvNameSchema = z.string().regex(/^[A-Z_][A-Z0-9_]*$/);
|
|
122
|
+
const piThinkingLevelSchema = z.enum(piThinkingLevels);
|
|
123
|
+
const piReadOnlyToolNamesSchema = z.tuple([
|
|
124
|
+
z.literal("read"),
|
|
125
|
+
z.literal("grep"),
|
|
126
|
+
z.literal("find"),
|
|
127
|
+
z.literal("ls")
|
|
128
|
+
]);
|
|
129
|
+
const piProviderProfileSchema = z.strictObject({
|
|
130
|
+
id: piProviderIdSchema,
|
|
131
|
+
provider: nonEmptyStringSchema$1,
|
|
132
|
+
model: nonEmptyStringSchema$1,
|
|
133
|
+
apiKeyEnv: piApiKeyEnvNameSchema,
|
|
134
|
+
thinking: piThinkingLevelSchema.optional()
|
|
135
|
+
});
|
|
136
|
+
const piProviderInvocationSchema = z.strictObject({
|
|
137
|
+
provider: nonEmptyStringSchema$1,
|
|
138
|
+
model: nonEmptyStringSchema$1,
|
|
139
|
+
apiKeyEnv: piApiKeyEnvNameSchema,
|
|
140
|
+
thinking: piThinkingLevelSchema,
|
|
141
|
+
tools: piReadOnlyToolNamesSchema
|
|
142
|
+
});
|
|
143
|
+
function parsePiProviderInvocation(value) {
|
|
144
|
+
return piProviderInvocationSchema.parse(value);
|
|
162
145
|
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/review/contract.ts
|
|
148
|
+
const reviewResultSchemaId = reviewOutputSchemaId;
|
|
149
|
+
z.toJSONSchema(reviewResultSchema);
|
|
163
150
|
function reviewSchemaExample$1() {
|
|
164
|
-
return
|
|
151
|
+
return parseReviewResult(reviewSchemaExample());
|
|
165
152
|
}
|
|
166
153
|
//#endregion
|
|
167
154
|
//#region src/types.ts
|
|
168
155
|
const nonEmptyStringSchema = z.string().min(1);
|
|
169
156
|
const providerConfigSchema = piProviderProfileSchema;
|
|
170
|
-
const pathGlobPatternSchema = z.string().min(1).superRefine((pattern, context) => {
|
|
171
|
-
if (pattern.includes("\0")) context.addIssue({
|
|
172
|
-
code: "custom",
|
|
173
|
-
message: "must not contain NUL bytes"
|
|
174
|
-
});
|
|
175
|
-
if (pattern.includes("\\")) context.addIssue({
|
|
176
|
-
code: "custom",
|
|
177
|
-
message: "must use POSIX '/' separators"
|
|
178
|
-
});
|
|
179
|
-
if (pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern)) context.addIssue({
|
|
180
|
-
code: "custom",
|
|
181
|
-
message: "must be repo-relative"
|
|
182
|
-
});
|
|
183
|
-
if (pattern.startsWith("!")) context.addIssue({
|
|
184
|
-
code: "custom",
|
|
185
|
-
message: "must use paths.exclude instead of negation"
|
|
186
|
-
});
|
|
187
|
-
if (pattern.split("/").includes("..")) context.addIssue({
|
|
188
|
-
code: "custom",
|
|
189
|
-
message: "must not contain '..' segments"
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
z.strictObject({
|
|
193
|
-
include: z.array(pathGlobPatternSchema).min(1).optional(),
|
|
194
|
-
exclude: z.array(pathGlobPatternSchema).min(1).optional()
|
|
195
|
-
});
|
|
196
157
|
const diffManifestLimitsConfigSchema = z.strictObject({
|
|
197
158
|
fullMaxBytes: z.number().int().positive().optional(),
|
|
198
159
|
fullMaxEstimatedTokens: z.number().int().positive().optional(),
|
|
@@ -330,7 +291,7 @@ const droppedFindingSchema = z.strictObject({
|
|
|
330
291
|
reason: nonEmptyStringSchema
|
|
331
292
|
});
|
|
332
293
|
const validatedReviewSchema = z.strictObject({
|
|
333
|
-
review:
|
|
294
|
+
review: reviewResultSchema,
|
|
334
295
|
validFindings: z.array(reviewFindingSchema),
|
|
335
296
|
droppedFindings: z.array(droppedFindingSchema)
|
|
336
297
|
});
|
|
@@ -368,64 +329,64 @@ function embeddedSdkAssets() {
|
|
|
368
329
|
};
|
|
369
330
|
}
|
|
370
331
|
//#endregion
|
|
371
|
-
//#region src/config/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"moduleResolution": "Bundler"
|
|
379
|
-
},
|
|
380
|
-
"include": ["./**/*.ts"]
|
|
332
|
+
//#region src/config/sdk-module.ts
|
|
333
|
+
function resolvedSdkModulePath() {
|
|
334
|
+
try {
|
|
335
|
+
return fileURLToPath(import.meta.resolve("@usepipr/sdk"));
|
|
336
|
+
} catch {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
381
339
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const files = [];
|
|
385
|
-
if (options.tsconfig !== false) files.push({
|
|
386
|
-
relativePath: path.join(relativeConfigDir, "tsconfig.json"),
|
|
387
|
-
contents: starterTsconfig
|
|
388
|
-
});
|
|
389
|
-
files.push({
|
|
390
|
-
relativePath: path.join(relativeConfigDir, "types", "pipr-sdk.d.ts"),
|
|
391
|
-
contents: await sdkDeclaration()
|
|
392
|
-
});
|
|
393
|
-
return files;
|
|
340
|
+
function resolvedSdkPackageRoot() {
|
|
341
|
+
return sdkPackageRootFromResolvedModule(resolvedSdkModulePath());
|
|
394
342
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
343
|
+
function sdkPackageRootFromResolvedModule(modulePath) {
|
|
344
|
+
if (!modulePath) return;
|
|
345
|
+
const moduleDir = path.dirname(modulePath);
|
|
346
|
+
return path.basename(moduleDir) === "src" || path.basename(moduleDir) === "dist" ? path.dirname(moduleDir) : moduleDir;
|
|
347
|
+
}
|
|
348
|
+
function sdkModuleStubSource(modulePath, embeddedModule) {
|
|
349
|
+
if (modulePath) return `export * from ${JSON.stringify(pathToFileURL(modulePath).href)};\n`;
|
|
350
|
+
if (embeddedModule) return embeddedModule;
|
|
351
|
+
throw new Error("Unable to locate @usepipr/sdk runtime module");
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/config/sdk-stub.ts
|
|
355
|
+
const sdkDeclarationModules = [{
|
|
356
|
+
moduleName: "@usepipr/sdk",
|
|
357
|
+
fileName: "index.d.mts"
|
|
358
|
+
}];
|
|
359
|
+
async function installTypedSdkStub(configDir) {
|
|
360
|
+
const sdkRoot = path.join(configDir, "node_modules", "@usepipr", "sdk");
|
|
361
|
+
await rm(sdkRoot, {
|
|
362
|
+
recursive: true,
|
|
363
|
+
force: true
|
|
364
|
+
});
|
|
365
|
+
await mkdir(sdkRoot, { recursive: true });
|
|
366
|
+
await Bun.write(path.join(sdkRoot, "package.json"), JSON.stringify({
|
|
367
|
+
type: "module",
|
|
368
|
+
types: "./index.d.ts",
|
|
369
|
+
exports: { ".": {
|
|
370
|
+
types: "./index.d.ts",
|
|
371
|
+
default: "./index.mjs"
|
|
372
|
+
} }
|
|
373
|
+
}));
|
|
374
|
+
await Bun.write(path.join(sdkRoot, "index.mjs"), sdkModuleStubSource(resolvedSdkModulePath(), embeddedSdkAssets().module));
|
|
375
|
+
await Bun.write(path.join(sdkRoot, "index.d.ts"), await sdkStubDeclaration());
|
|
401
376
|
}
|
|
402
|
-
async function
|
|
377
|
+
async function sdkStubDeclaration() {
|
|
403
378
|
const embedded = embeddedSdkAssets().declaration;
|
|
404
379
|
if (embedded?.includes("declare module \"@usepipr/sdk\"")) {
|
|
405
380
|
assertStandaloneSdkDeclaration(embedded);
|
|
406
|
-
return embedded
|
|
381
|
+
return embedded.endsWith("\n") ? embedded : `${embedded}\n`;
|
|
407
382
|
}
|
|
408
383
|
const declaration = embeddedSdkDeclaration(await rawSdkDeclarations());
|
|
409
384
|
assertStandaloneSdkDeclaration(declaration);
|
|
410
|
-
return declaration
|
|
385
|
+
return declaration.endsWith("\n") ? declaration : `${declaration}\n`;
|
|
411
386
|
}
|
|
412
387
|
function assertStandaloneSdkDeclaration(declaration) {
|
|
413
388
|
if (declaration.includes("from \"zod\"") || declaration.includes("z.ZodType")) throw new Error("generated SDK declaration must be standalone and must not import zod");
|
|
414
389
|
}
|
|
415
|
-
const sdkDeclarationModules = [
|
|
416
|
-
{
|
|
417
|
-
moduleName: "@usepipr/sdk",
|
|
418
|
-
fileName: "index.d.mts"
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
moduleName: "@usepipr/sdk/review",
|
|
422
|
-
fileName: "review.d.mts"
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
moduleName: "@usepipr/sdk/tools",
|
|
426
|
-
fileName: "tools.d.mts"
|
|
427
|
-
}
|
|
428
|
-
];
|
|
429
390
|
async function rawSdkDeclarations() {
|
|
430
391
|
const declarations = await Promise.all(sdkDeclarationModules.map(async (module) => {
|
|
431
392
|
const declarationPath = await sdkDeclarationPath(module.fileName);
|
|
@@ -440,13 +401,28 @@ async function rawSdkDeclarations() {
|
|
|
440
401
|
moduleName: sdkDeclarationModules[0].moduleName,
|
|
441
402
|
source: embedded
|
|
442
403
|
}];
|
|
443
|
-
throw new Error("Unable to locate @usepipr/sdk declaration file. Build @usepipr/sdk before
|
|
404
|
+
throw new Error("Unable to locate @usepipr/sdk declaration file. Build @usepipr/sdk before loading config.");
|
|
444
405
|
}
|
|
445
406
|
async function sdkDeclarationPath(fileName) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
407
|
+
const sdkRoot = resolvedSdkPackageRoot();
|
|
408
|
+
if (!sdkRoot) return;
|
|
409
|
+
const candidate = path.join(sdkRoot, "dist", fileName);
|
|
410
|
+
return await Bun.file(candidate).exists() ? candidate : void 0;
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/config/starter-tsconfig.ts
|
|
414
|
+
const starterTsconfig = `{
|
|
415
|
+
"compilerOptions": {
|
|
416
|
+
"strict": true,
|
|
417
|
+
"noEmit": true,
|
|
418
|
+
"skipLibCheck": true,
|
|
419
|
+
"target": "ES2022",
|
|
420
|
+
"module": "ESNext",
|
|
421
|
+
"moduleResolution": "Bundler"
|
|
422
|
+
},
|
|
423
|
+
"include": ["./**/*.ts"]
|
|
449
424
|
}
|
|
425
|
+
`;
|
|
450
426
|
//#endregion
|
|
451
427
|
//#region src/config/ts-loader.ts
|
|
452
428
|
async function loadTypescriptConfig(options) {
|
|
@@ -457,13 +433,8 @@ async function loadTypescriptConfig(options) {
|
|
|
457
433
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-config-"));
|
|
458
434
|
try {
|
|
459
435
|
const tempConfigDir = path.join(tempRoot, relativeConfigDir);
|
|
460
|
-
await
|
|
461
|
-
|
|
462
|
-
errorOnExist: false,
|
|
463
|
-
force: true,
|
|
464
|
-
filter: (source) => !isIgnoredConfigCopyPath(source, projectDir)
|
|
465
|
-
});
|
|
466
|
-
await installSdkStub(tempConfigDir);
|
|
436
|
+
await copyConfigDirectory(projectDir, tempConfigDir);
|
|
437
|
+
await prepareConfigDirectory(tempConfigDir, { frozen: true });
|
|
467
438
|
const factory = (await import(`${pathToFileURL(path.join(tempConfigDir, "config.ts")).href}?pipr=${Date.now()}`)).default;
|
|
468
439
|
if (!isPiprConfigFactory(factory)) throw new Error(`${sourceConfigPath}: default export must be created by definePipr()`);
|
|
469
440
|
return {
|
|
@@ -478,27 +449,15 @@ async function loadTypescriptConfig(options) {
|
|
|
478
449
|
});
|
|
479
450
|
}
|
|
480
451
|
}
|
|
481
|
-
async function
|
|
482
|
-
|
|
483
|
-
await
|
|
484
|
-
await Bun.write(path.join(sdkRoot, "package.json"), JSON.stringify({
|
|
485
|
-
type: "module",
|
|
486
|
-
exports: {
|
|
487
|
-
".": "./index.mjs",
|
|
488
|
-
"./review": "./review.mjs",
|
|
489
|
-
"./tools": "./tools.mjs"
|
|
490
|
-
}
|
|
491
|
-
}));
|
|
492
|
-
await Bun.write(path.join(sdkRoot, "index.mjs"), await sdkStubSource());
|
|
493
|
-
await Bun.write(path.join(sdkRoot, "review.mjs"), "export * from \"./index.mjs\";\n");
|
|
494
|
-
await Bun.write(path.join(sdkRoot, "tools.mjs"), "export * from \"./index.mjs\";\n");
|
|
452
|
+
async function prepareConfigDirectory(configDir, options = {}) {
|
|
453
|
+
await installConfigDependencies(configDir, options);
|
|
454
|
+
await installTypedSdkStub(configDir);
|
|
495
455
|
}
|
|
496
456
|
async function typecheckTypescriptConfig(rootDir, relativeConfigDir) {
|
|
497
457
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-config-check-"));
|
|
498
458
|
try {
|
|
499
459
|
const tempProjectDir = path.join(tempRoot, "project");
|
|
500
460
|
const tempConfigDir = path.join(tempProjectDir, relativeConfigDir);
|
|
501
|
-
const configTypesPath = path.join(relativeConfigDir, "types");
|
|
502
461
|
await cp(rootDir, tempProjectDir, {
|
|
503
462
|
recursive: true,
|
|
504
463
|
errorOnExist: false,
|
|
@@ -506,11 +465,15 @@ async function typecheckTypescriptConfig(rootDir, relativeConfigDir) {
|
|
|
506
465
|
filter: (source) => {
|
|
507
466
|
const relative = path.relative(rootDir, source);
|
|
508
467
|
const first = relative.split(path.sep)[0] ?? "";
|
|
509
|
-
return !ignoredTypecheckRootEntries.has(first) && relative !== "bun.lock"
|
|
468
|
+
return !ignoredTypecheckRootEntries.has(first) && relative !== "bun.lock";
|
|
510
469
|
}
|
|
511
470
|
});
|
|
471
|
+
await prepareConfigDirectory(tempConfigDir, { frozen: true });
|
|
512
472
|
const tsconfigPath = path.join(tempConfigDir, "tsconfig.json");
|
|
513
|
-
|
|
473
|
+
if (!await fileExists(tsconfigPath)) {
|
|
474
|
+
await mkdir(tempConfigDir, { recursive: true });
|
|
475
|
+
await Bun.write(tsconfigPath, starterTsconfig);
|
|
476
|
+
}
|
|
514
477
|
await typecheckTypescriptConfigWithApi(tempConfigDir, tsconfigPath);
|
|
515
478
|
} finally {
|
|
516
479
|
await rm(tempRoot, {
|
|
@@ -524,28 +487,17 @@ async function typecheckTypescriptConfigWithApi(configDir, tsconfigPath) {
|
|
|
524
487
|
const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
525
488
|
if (config.error) throw new Error(formatTypeScriptDiagnostics(ts, [config.error], configDir));
|
|
526
489
|
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, configDir);
|
|
527
|
-
const typeRoot = path.join(configDir, "types");
|
|
528
490
|
const bundledTypeRoots = [];
|
|
529
|
-
try {
|
|
491
|
+
if (!await fileExists(path.join(configDir, "node_modules", "@types", "bun", "package.json"))) try {
|
|
530
492
|
const require = createRequire(import.meta.url);
|
|
531
493
|
bundledTypeRoots.push(path.dirname(path.dirname(require.resolve("@types/bun/package.json"))));
|
|
532
494
|
} catch {}
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
return relative.startsWith("..") || path.isAbsolute(relative);
|
|
536
|
-
})];
|
|
537
|
-
const program = ts.createProgram(fileNames, {
|
|
495
|
+
const configPath = path.join(configDir, "config.ts");
|
|
496
|
+
const program = ts.createProgram([configPath, ...parsed.fileNames], {
|
|
538
497
|
...parsed.options,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
...parsed.options.typeRoots ?? []
|
|
543
|
-
])],
|
|
544
|
-
types: [...new Set([
|
|
545
|
-
...parsed.options.types ?? [],
|
|
546
|
-
"pipr-sdk",
|
|
547
|
-
...bundledTypeRoots.length ? ["bun"] : []
|
|
548
|
-
])]
|
|
498
|
+
skipLibCheck: true,
|
|
499
|
+
typeRoots: [...new Set([...parsed.options.typeRoots ?? [], ...bundledTypeRoots])],
|
|
500
|
+
types: [...new Set([...parsed.options.types ?? [], ...bundledTypeRoots.length ? ["bun"] : []])]
|
|
549
501
|
});
|
|
550
502
|
const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
|
|
551
503
|
if (diagnostics.length > 0) throw new Error(`TypeScript config check failed for ${path.join(configDir, "config.ts")}:\n` + formatTypeScriptDiagnostics(ts, diagnostics, configDir));
|
|
@@ -557,26 +509,17 @@ function formatTypeScriptDiagnostics(ts, diagnostics, configDir) {
|
|
|
557
509
|
getNewLine: () => "\n"
|
|
558
510
|
});
|
|
559
511
|
}
|
|
560
|
-
async function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
async function sdkSourcePath() {
|
|
568
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
569
|
-
const candidates = [
|
|
570
|
-
path.resolve(moduleDir, "../../../sdk/src/index.ts"),
|
|
571
|
-
path.resolve(moduleDir, "../../sdk/src/index.ts"),
|
|
572
|
-
path.resolve(moduleDir, "../../../sdk/dist/index.mjs"),
|
|
573
|
-
path.resolve(moduleDir, "../../sdk/dist/index.mjs")
|
|
574
|
-
];
|
|
575
|
-
for (const candidate of candidates) if (await fileExists(candidate)) return candidate;
|
|
512
|
+
async function copyConfigDirectory(sourceDir, targetDir) {
|
|
513
|
+
await cp(sourceDir, targetDir, {
|
|
514
|
+
recursive: true,
|
|
515
|
+
errorOnExist: false,
|
|
516
|
+
force: true,
|
|
517
|
+
filter: (source) => !isIgnoredConfigCopyPath(source, sourceDir)
|
|
518
|
+
});
|
|
576
519
|
}
|
|
577
520
|
function isIgnoredConfigCopyPath(source, configDir) {
|
|
578
521
|
const relative = path.relative(configDir, source);
|
|
579
|
-
return relative === "node_modules" || relative.startsWith(`node_modules${path.sep}`)
|
|
522
|
+
return relative === "node_modules" || relative.startsWith(`node_modules${path.sep}`);
|
|
580
523
|
}
|
|
581
524
|
const ignoredTypecheckRootEntries = new Set([
|
|
582
525
|
".fallow",
|
|
@@ -747,6 +690,8 @@ export default definePipr((pipr) => {
|
|
|
747
690
|
options: { thinking: "medium" },
|
|
748
691
|
});
|
|
749
692
|
|
|
693
|
+
pipr.config({ publication: { maxInlineComments: 8 } });
|
|
694
|
+
|
|
750
695
|
const reviewer = pipr.reviewer({
|
|
751
696
|
name: "bug-hunter",
|
|
752
697
|
model: primary,
|
|
@@ -765,7 +710,6 @@ export default definePipr((pipr) => {
|
|
|
765
710
|
paths: {
|
|
766
711
|
exclude: ["docs/**", "**/*.md"],
|
|
767
712
|
},
|
|
768
|
-
inlineComments: { max: 8 },
|
|
769
713
|
timeout: "7m",
|
|
770
714
|
entrypoints: {
|
|
771
715
|
changeRequest: ["opened", "updated", "reopened", "ready"],
|
|
@@ -913,6 +857,8 @@ export default definePipr((pipr) => {
|
|
|
913
857
|
options: { thinking: "high" },
|
|
914
858
|
});
|
|
915
859
|
|
|
860
|
+
pipr.config({ publication: { maxInlineComments: 5 } });
|
|
861
|
+
|
|
916
862
|
pipr.review({
|
|
917
863
|
id: "review",
|
|
918
864
|
model,
|
|
@@ -921,8 +867,7 @@ export default definePipr((pipr) => {
|
|
|
921
867
|
maintainability, and test coverage.
|
|
922
868
|
Return only actionable findings that target valid diff ranges.
|
|
923
869
|
\`,
|
|
924
|
-
|
|
925
|
-
timeout: "5m",
|
|
870
|
+
timeout: "10m",
|
|
926
871
|
comment: (result, context) => ({
|
|
927
872
|
main:
|
|
928
873
|
context.platform.id === "local"
|
|
@@ -1547,6 +1492,8 @@ export default definePipr((pipr) => {
|
|
|
1547
1492
|
options: { thinking: "medium" },
|
|
1548
1493
|
});
|
|
1549
1494
|
|
|
1495
|
+
pipr.config({ publication: { maxInlineComments: 0 } });
|
|
1496
|
+
|
|
1550
1497
|
pipr.review({
|
|
1551
1498
|
id: "pr-briefing",
|
|
1552
1499
|
model,
|
|
@@ -1555,7 +1502,6 @@ export default definePipr((pipr) => {
|
|
|
1555
1502
|
classify the PR type, explain review risk, and include a concise file walkthrough.
|
|
1556
1503
|
Return no inline findings unless there is a concrete blocker.
|
|
1557
1504
|
\`,
|
|
1558
|
-
inlineComments: false,
|
|
1559
1505
|
comment: (result, context) => [
|
|
1560
1506
|
"## PR Briefing",
|
|
1561
1507
|
"",
|
|
@@ -1646,18 +1592,6 @@ export default definePipr((pipr) => {
|
|
|
1646
1592
|
options: { thinking: "high" },
|
|
1647
1593
|
});
|
|
1648
1594
|
|
|
1649
|
-
pipr.checks({
|
|
1650
|
-
aggregate: { enabled: true, name: "pipr quality gate" },
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
pipr.limits({
|
|
1654
|
-
timeoutSeconds: 420,
|
|
1655
|
-
diffManifest: {
|
|
1656
|
-
fullMaxEstimatedTokens: 32000,
|
|
1657
|
-
condensedMaxEstimatedTokens: 64000,
|
|
1658
|
-
},
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
1595
|
pipr.config({
|
|
1662
1596
|
publication: {
|
|
1663
1597
|
maxInlineComments: 6,
|
|
@@ -1669,6 +1603,16 @@ export default definePipr((pipr) => {
|
|
|
1669
1603
|
userReplies: { enabled: true, allowedActors: "write" },
|
|
1670
1604
|
},
|
|
1671
1605
|
},
|
|
1606
|
+
checks: {
|
|
1607
|
+
aggregate: { enabled: true, name: "pipr quality gate" },
|
|
1608
|
+
},
|
|
1609
|
+
limits: {
|
|
1610
|
+
timeoutSeconds: 420,
|
|
1611
|
+
diffManifest: {
|
|
1612
|
+
fullMaxEstimatedTokens: 32000,
|
|
1613
|
+
condensedMaxEstimatedTokens: 64000,
|
|
1614
|
+
},
|
|
1615
|
+
},
|
|
1672
1616
|
});
|
|
1673
1617
|
|
|
1674
1618
|
pipr.review({
|
|
@@ -1680,7 +1624,6 @@ export default definePipr((pipr) => {
|
|
|
1680
1624
|
If no blocking issue exists, state that no blocking issue exists.
|
|
1681
1625
|
\`,
|
|
1682
1626
|
check: { enabled: true, name: "quality gate", required: true },
|
|
1683
|
-
inlineComments: { max: 6 },
|
|
1684
1627
|
comment: (result) => ({
|
|
1685
1628
|
main: \`## Quality Gate\\n\\n\${result.summary.body}\`,
|
|
1686
1629
|
inlineFindings: result.inlineFindings,
|
|
@@ -1860,7 +1803,9 @@ function isOfficialInitRecipeId(recipe) {
|
|
|
1860
1803
|
//#endregion
|
|
1861
1804
|
//#region src/config/init.ts
|
|
1862
1805
|
const supportedOfficialInitAdapters = ["github"];
|
|
1863
|
-
const defaultWorkflowActionRef = "somus/pipr@v0.1
|
|
1806
|
+
const defaultWorkflowActionRef = "somus/pipr@v0.2.1";
|
|
1807
|
+
const defaultSdkVersion = "0.2.1";
|
|
1808
|
+
const defaultTypesBunVersion = "1.3.14";
|
|
1864
1809
|
function resolveOfficialInitAdapters(adapters) {
|
|
1865
1810
|
if (adapters === void 0) return [...supportedOfficialInitAdapters];
|
|
1866
1811
|
if (adapters.length === 0) return [];
|
|
@@ -1880,25 +1825,38 @@ function unsupportedAdapterError(adapter) {
|
|
|
1880
1825
|
return /* @__PURE__ */ new Error(`Unsupported pipr init adapter '${adapter}'. Supported adapters: ${supportedOfficialInitAdapters.join(", ")}; use 'none' to skip adapter files.`);
|
|
1881
1826
|
}
|
|
1882
1827
|
async function initOfficialMinimalProject(options) {
|
|
1883
|
-
const { configDir, relativeConfigDir } = resolveContainedConfigDir(options);
|
|
1828
|
+
const { configDir, relativeConfigDir, projectDir } = resolveContainedConfigDir(options);
|
|
1884
1829
|
const adapters = resolveOfficialInitAdapters(options.adapters);
|
|
1885
1830
|
const rootDir = path.resolve(options.rootDir);
|
|
1886
|
-
const
|
|
1887
|
-
const targets = (
|
|
1831
|
+
const minimal = options.minimal === true;
|
|
1832
|
+
const targets = (await starterFiles(relativeConfigDir, adapters, options.recipe, minimal)).map((file) => ({
|
|
1888
1833
|
...file,
|
|
1889
1834
|
absolutePath: path.join(rootDir, file.relativePath)
|
|
1890
1835
|
}));
|
|
1891
1836
|
await assertSafeTargetAncestors(targets, rootDir);
|
|
1892
1837
|
const existing = await findExistingTargets(targets);
|
|
1893
|
-
if (existing.length > 0 && !options.force) {
|
|
1894
|
-
if (typeSupport === "only") return {
|
|
1895
|
-
configDir,
|
|
1896
|
-
...await writeTargets(targets, existing, { skipExisting: true })
|
|
1897
|
-
};
|
|
1898
|
-
throw new Error(`Project already contains pipr files: ${existing.join(", ")}. Use --force to replace existing .pipr files.`);
|
|
1899
|
-
}
|
|
1838
|
+
if (existing.length > 0 && !options.force) throw new Error(`Project already contains pipr files: ${existing.join(", ")}. Use --force to replace existing .pipr files.`);
|
|
1900
1839
|
const result = await writeTargets(targets, existing, { skipExisting: false });
|
|
1901
|
-
if (
|
|
1840
|
+
if (!minimal) {
|
|
1841
|
+
await assertBunAvailable();
|
|
1842
|
+
const install = Bun.spawn([
|
|
1843
|
+
"bun",
|
|
1844
|
+
"install",
|
|
1845
|
+
"--ignore-scripts"
|
|
1846
|
+
], {
|
|
1847
|
+
cwd: projectDir,
|
|
1848
|
+
env: process.env,
|
|
1849
|
+
stdout: "pipe",
|
|
1850
|
+
stderr: "pipe"
|
|
1851
|
+
});
|
|
1852
|
+
const [exitCode, stderr] = await Promise.all([install.exited, new Response(install.stderr).text()]);
|
|
1853
|
+
if (exitCode !== 0) throw new Error(`${configDir}: bun install failed (exit ${exitCode}).` + (stderr.trim().length > 0 ? `\n${stderr.trim()}` : ""));
|
|
1854
|
+
if (await Bun.file(path.join(projectDir, "bun.lock")).exists()) {
|
|
1855
|
+
const lockRelative = path.join(relativeConfigDir, "bun.lock");
|
|
1856
|
+
if (!existing.includes(lockRelative) && !result.created.includes(lockRelative)) result.created.push(lockRelative);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
await loadRuntimeProject({
|
|
1902
1860
|
rootDir: options.rootDir,
|
|
1903
1861
|
configDir
|
|
1904
1862
|
});
|
|
@@ -1907,7 +1865,7 @@ async function initOfficialMinimalProject(options) {
|
|
|
1907
1865
|
...result
|
|
1908
1866
|
};
|
|
1909
1867
|
}
|
|
1910
|
-
async function starterFiles(relativeConfigDir, adapters, recipe,
|
|
1868
|
+
async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false) {
|
|
1911
1869
|
const files = [{
|
|
1912
1870
|
relativePath: path.join(relativeConfigDir, "config.ts"),
|
|
1913
1871
|
contents: officialInitRecipeConfigTs(recipe)
|
|
@@ -1915,13 +1873,29 @@ async function starterFiles(relativeConfigDir, adapters, recipe, includeTypeSupp
|
|
|
1915
1873
|
relativePath: path.join(relativeConfigDir, file.relativePath),
|
|
1916
1874
|
contents: file.contents
|
|
1917
1875
|
}))];
|
|
1918
|
-
if (
|
|
1876
|
+
if (!minimal) files.push({
|
|
1877
|
+
relativePath: path.join(relativeConfigDir, "package.json"),
|
|
1878
|
+
contents: starterPackageJson()
|
|
1879
|
+
}, {
|
|
1880
|
+
relativePath: path.join(relativeConfigDir, "tsconfig.json"),
|
|
1881
|
+
contents: starterTsconfig
|
|
1882
|
+
}, {
|
|
1883
|
+
relativePath: path.join(relativeConfigDir, ".gitignore"),
|
|
1884
|
+
contents: "node_modules\n"
|
|
1885
|
+
});
|
|
1919
1886
|
if (adapters.includes("github")) files.push({
|
|
1920
1887
|
relativePath: path.join(".github", "workflows", "pipr.yml"),
|
|
1921
|
-
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe)
|
|
1888
|
+
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe, minimal)
|
|
1922
1889
|
});
|
|
1923
1890
|
return files;
|
|
1924
1891
|
}
|
|
1892
|
+
function starterPackageJson() {
|
|
1893
|
+
return `${JSON.stringify({
|
|
1894
|
+
private: true,
|
|
1895
|
+
dependencies: { "@usepipr/sdk": process.env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
|
|
1896
|
+
devDependencies: { "@types/bun": defaultTypesBunVersion }
|
|
1897
|
+
}, null, 2)}\n`;
|
|
1898
|
+
}
|
|
1925
1899
|
async function writeTargets(targets, existing, options) {
|
|
1926
1900
|
const created = [];
|
|
1927
1901
|
const overwritten = [];
|
|
@@ -1975,7 +1949,7 @@ async function maybeLstat(filePath) {
|
|
|
1975
1949
|
return;
|
|
1976
1950
|
}
|
|
1977
1951
|
}
|
|
1978
|
-
function starterWorkflow(relativeConfigDir, recipe) {
|
|
1952
|
+
function starterWorkflow(relativeConfigDir, recipe, minimal = false) {
|
|
1979
1953
|
const lines = [
|
|
1980
1954
|
"name: pipr",
|
|
1981
1955
|
"",
|
|
@@ -1998,20 +1972,25 @@ function starterWorkflow(relativeConfigDir, recipe) {
|
|
|
1998
1972
|
" steps:",
|
|
1999
1973
|
" - uses: actions/checkout@v6",
|
|
2000
1974
|
" with:",
|
|
2001
|
-
" fetch-depth: 0"
|
|
2002
|
-
` - uses: ${defaultWorkflowActionRef}`,
|
|
2003
|
-
" env:",
|
|
2004
|
-
` DEEPSEEK_API_KEY: $${[
|
|
2005
|
-
"{{ ",
|
|
2006
|
-
"secrets.DEEPSEEK_API_KEY",
|
|
2007
|
-
" }}"
|
|
2008
|
-
].join("")}`,
|
|
2009
|
-
` GITHUB_TOKEN: $${[
|
|
2010
|
-
"{{ ",
|
|
2011
|
-
"github.token",
|
|
2012
|
-
" }}"
|
|
2013
|
-
].join("")}`
|
|
1975
|
+
" fetch-depth: 0"
|
|
2014
1976
|
];
|
|
1977
|
+
if (!minimal) lines.push(" - uses: actions/cache@v4", " with:", " path: /home/runner/work/_temp/_github_home/.bun/install/cache", ` key: pipr-bun-${[
|
|
1978
|
+
"{{ ",
|
|
1979
|
+
`hashFiles('${relativeConfigDir}/bun.lock')`,
|
|
1980
|
+
" }}"
|
|
1981
|
+
].join("")}`);
|
|
1982
|
+
lines.push(` - uses: ${defaultWorkflowActionRef}`);
|
|
1983
|
+
lines.push(" env:");
|
|
1984
|
+
lines.push(` DEEPSEEK_API_KEY: $${[
|
|
1985
|
+
"{{ ",
|
|
1986
|
+
"secrets.DEEPSEEK_API_KEY",
|
|
1987
|
+
" }}"
|
|
1988
|
+
].join("")}`);
|
|
1989
|
+
lines.push(` GITHUB_TOKEN: $${[
|
|
1990
|
+
"{{ ",
|
|
1991
|
+
"github.token",
|
|
1992
|
+
" }}"
|
|
1993
|
+
].join("")}`);
|
|
2015
1994
|
for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(` ${secret.env}: $${[
|
|
2016
1995
|
"{{ ",
|
|
2017
1996
|
`secrets.${secret.secret}`,
|
|
@@ -2386,44 +2365,452 @@ function createLocalChangeRequestEvent(options) {
|
|
|
2386
2365
|
});
|
|
2387
2366
|
}
|
|
2388
2367
|
//#endregion
|
|
2389
|
-
//#region src/
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
const baseRoot = path.join(toolRoot, "base");
|
|
2394
|
-
await mkdir(baseRoot, { recursive: true });
|
|
2395
|
-
const baseRanges = await materializeBaseRangeSnapshots({
|
|
2396
|
-
baseRoot,
|
|
2397
|
-
manifest: options.request.manifest,
|
|
2398
|
-
sourceWorkspace: options.sourceWorkspace,
|
|
2399
|
-
maxBytes: options.request.toolResponseMaxBytes
|
|
2400
|
-
});
|
|
2401
|
-
const data = {
|
|
2402
|
-
manifest: options.request.manifest,
|
|
2403
|
-
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
2404
|
-
baseRanges
|
|
2405
|
-
};
|
|
2406
|
-
const dataPath = path.join(toolRoot, "data.json");
|
|
2407
|
-
await Bun.write(dataPath, JSON.stringify(data));
|
|
2408
|
-
return {
|
|
2409
|
-
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
2410
|
-
dataPath,
|
|
2411
|
-
toolNames: piRuntimeReadToolNames
|
|
2412
|
-
};
|
|
2368
|
+
//#region src/commands/grammar.ts
|
|
2369
|
+
const piprCommandPrefix = "@pipr";
|
|
2370
|
+
function firstNonEmptyLine(value) {
|
|
2371
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
2413
2372
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
const candidates = [
|
|
2417
|
-
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
2418
|
-
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
2419
|
-
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
2420
|
-
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
2421
|
-
];
|
|
2422
|
-
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
2423
|
-
throw new Error("Unable to locate pipr runtime tools extension");
|
|
2373
|
+
function isPiprCommandLine(line) {
|
|
2374
|
+
return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
|
|
2424
2375
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2376
|
+
function parseCommandPattern(pattern, line) {
|
|
2377
|
+
const patternParts = commandPatternParts(pattern);
|
|
2378
|
+
const validationError = unsupportedCommandRestCaptureError(pattern);
|
|
2379
|
+
if (validationError) return {
|
|
2380
|
+
ok: false,
|
|
2381
|
+
error: validationError
|
|
2382
|
+
};
|
|
2383
|
+
const lineTokens = tokenizeCommandPattern(line);
|
|
2384
|
+
const captures = {};
|
|
2385
|
+
let index = 0;
|
|
2386
|
+
for (const part of patternParts) {
|
|
2387
|
+
if (isOptionalCommandPatternPart(part)) {
|
|
2388
|
+
const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
|
|
2389
|
+
if (nextIndex !== void 0) index = nextIndex;
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
if (isCommandRestCaptureToken(part)) {
|
|
2393
|
+
const value = lineTokens.slice(index).join(" ");
|
|
2394
|
+
if (!value) return {
|
|
2395
|
+
ok: false,
|
|
2396
|
+
error: `Expected '${part}'`
|
|
2397
|
+
};
|
|
2398
|
+
captures[part.slice(1, -4)] = value;
|
|
2399
|
+
index = lineTokens.length;
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
const nextIndex = parsePatternToken(part, lineTokens, index, captures);
|
|
2403
|
+
if (nextIndex === void 0) return {
|
|
2404
|
+
ok: false,
|
|
2405
|
+
error: `Expected '${part}'`
|
|
2406
|
+
};
|
|
2407
|
+
index = nextIndex;
|
|
2408
|
+
}
|
|
2409
|
+
if (index !== lineTokens.length) return {
|
|
2410
|
+
ok: false,
|
|
2411
|
+
error: `Unexpected argument '${lineTokens[index]}'`
|
|
2412
|
+
};
|
|
2413
|
+
return {
|
|
2414
|
+
ok: true,
|
|
2415
|
+
value: captures
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function commandPatternPrefixMatches(pattern, line) {
|
|
2419
|
+
const patternTokens = commandPatternParts(pattern);
|
|
2420
|
+
if (unsupportedCommandRestCaptureError(pattern)) return false;
|
|
2421
|
+
const lineTokens = tokenizeCommandPattern(line);
|
|
2422
|
+
let index = 0;
|
|
2423
|
+
for (const token of patternTokens) {
|
|
2424
|
+
if (isCommandCaptureToken(token) || isOptionalCommandPatternPart(token)) return true;
|
|
2425
|
+
if (lineTokens[index] !== token) return false;
|
|
2426
|
+
index += 1;
|
|
2427
|
+
}
|
|
2428
|
+
return true;
|
|
2429
|
+
}
|
|
2430
|
+
function parseOptionalPatternPart(pattern, lineTokens, startIndex, captures) {
|
|
2431
|
+
const patternTokens = tokenizeCommandPattern(pattern);
|
|
2432
|
+
if (patternTokens.length === 0 || lineTokens[startIndex] !== patternTokens[0]) return;
|
|
2433
|
+
const snapshot = { ...captures };
|
|
2434
|
+
let index = startIndex;
|
|
2435
|
+
for (const token of patternTokens) {
|
|
2436
|
+
const nextIndex = parsePatternToken(token, lineTokens, index, captures);
|
|
2437
|
+
if (nextIndex === void 0) {
|
|
2438
|
+
for (const key of Object.keys(captures)) delete captures[key];
|
|
2439
|
+
Object.assign(captures, snapshot);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
index = nextIndex;
|
|
2443
|
+
}
|
|
2444
|
+
return index;
|
|
2445
|
+
}
|
|
2446
|
+
function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
2447
|
+
if (isCommandCaptureToken(patternToken)) {
|
|
2448
|
+
const value = lineTokens[index];
|
|
2449
|
+
if (!value) return;
|
|
2450
|
+
captures[patternToken.slice(1, -1)] = value;
|
|
2451
|
+
return index + 1;
|
|
2452
|
+
}
|
|
2453
|
+
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
2454
|
+
}
|
|
2455
|
+
//#endregion
|
|
2456
|
+
//#region src/action/entry-dispatch.ts
|
|
2457
|
+
const permissionOrder = [
|
|
2458
|
+
"read",
|
|
2459
|
+
"triage",
|
|
2460
|
+
"write",
|
|
2461
|
+
"maintain",
|
|
2462
|
+
"admin"
|
|
2463
|
+
];
|
|
2464
|
+
const changeRequestActions = [
|
|
2465
|
+
"opened",
|
|
2466
|
+
"updated",
|
|
2467
|
+
"reopened",
|
|
2468
|
+
"ready",
|
|
2469
|
+
"closed"
|
|
2470
|
+
];
|
|
2471
|
+
function dispatchRuntimeEntry(options) {
|
|
2472
|
+
if (options.kind === "change-request") return {
|
|
2473
|
+
kind: "change-request",
|
|
2474
|
+
tasks: selectRuntimeTasks({
|
|
2475
|
+
plan: options.plan,
|
|
2476
|
+
event: options.event,
|
|
2477
|
+
taskName: options.taskName
|
|
2478
|
+
}),
|
|
2479
|
+
taskName: options.taskName
|
|
2480
|
+
};
|
|
2481
|
+
return resolvePlanCommand(options.plan, options.line);
|
|
2482
|
+
}
|
|
2483
|
+
function selectRuntimeTasks(options) {
|
|
2484
|
+
if (options.taskName) return options.plan.tasks.filter((task) => task.name === options.taskName);
|
|
2485
|
+
return selectChangeRequestTasks(options.plan, options.event);
|
|
2486
|
+
}
|
|
2487
|
+
function selectLocalReviewTasks(plan) {
|
|
2488
|
+
return uniqBy(plan.changeRequestTriggers.map((trigger) => trigger.task), (task) => task.name).filter((task) => task.local !== false);
|
|
2489
|
+
}
|
|
2490
|
+
function selectChangeRequestTasks(plan, event) {
|
|
2491
|
+
if (!changeRequestActions.includes(event.action)) return [];
|
|
2492
|
+
const action = event.action;
|
|
2493
|
+
return uniqBy(plan.changeRequestTriggers.filter((trigger) => trigger.actions.includes(action)).map((trigger) => trigger.task), (task) => task.name);
|
|
2494
|
+
}
|
|
2495
|
+
function selectPlanCommand(plan, line) {
|
|
2496
|
+
let firstInvalid;
|
|
2497
|
+
for (const command of plan.commands) {
|
|
2498
|
+
const parsed = parseCommandPattern(command.pattern, line);
|
|
2499
|
+
if (!parsed.ok) {
|
|
2500
|
+
if (commandPatternPrefixMatches(command.pattern, line) && !firstInvalid) firstInvalid = {
|
|
2501
|
+
kind: "invalid",
|
|
2502
|
+
command,
|
|
2503
|
+
error: parsed.error
|
|
2504
|
+
};
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
return {
|
|
2508
|
+
kind: "matched",
|
|
2509
|
+
command,
|
|
2510
|
+
commandName: command.pattern.replace(/^@pipr\s+/, "").split(/\s+/)[0] ?? command.pattern,
|
|
2511
|
+
line,
|
|
2512
|
+
arguments: parsed.value
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
return firstInvalid;
|
|
2516
|
+
}
|
|
2517
|
+
function resolvePlanCommand(plan, line) {
|
|
2518
|
+
if (!line) return {
|
|
2519
|
+
kind: "ignored",
|
|
2520
|
+
reason: "comment did not contain a command line"
|
|
2521
|
+
};
|
|
2522
|
+
if (!isPiprCommandLine(line)) return {
|
|
2523
|
+
kind: "ignored",
|
|
2524
|
+
reason: "comment did not target pipr"
|
|
2525
|
+
};
|
|
2526
|
+
const selected = selectPlanCommand(plan, line);
|
|
2527
|
+
if (selected?.kind === "matched") return {
|
|
2528
|
+
kind: "matched",
|
|
2529
|
+
invocation: {
|
|
2530
|
+
taskName: selected.command.task.name,
|
|
2531
|
+
commandName: selected.commandName,
|
|
2532
|
+
requiredPermission: selected.command.permission,
|
|
2533
|
+
line: selected.line,
|
|
2534
|
+
pattern: selected.command.pattern,
|
|
2535
|
+
arguments: selected.arguments
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
if (selected?.kind === "invalid") return {
|
|
2539
|
+
kind: "invalid",
|
|
2540
|
+
reason: selected.error,
|
|
2541
|
+
requiredPermission: selected.command.permission,
|
|
2542
|
+
body: renderPlanCommandHelp(plan, selected.error)
|
|
2543
|
+
};
|
|
2544
|
+
return {
|
|
2545
|
+
kind: "help",
|
|
2546
|
+
reason: `unknown pipr command '${line}'`,
|
|
2547
|
+
requiredPermission: "read",
|
|
2548
|
+
body: renderPlanCommandHelp(plan, `Unknown command: ${line}`)
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
function parsePlanCommandInputs(plan, invocation) {
|
|
2552
|
+
const matchingCommand = plan.commands.find((candidate) => candidate.task.name === invocation.taskName && candidate.pattern === invocation.pattern) ?? plan.commands.find((candidate) => candidate.task.name === invocation.taskName);
|
|
2553
|
+
if (!matchingCommand) return {
|
|
2554
|
+
kind: "invalid",
|
|
2555
|
+
reason: `No command registered for task '${invocation.taskName}'`,
|
|
2556
|
+
requiredPermission: invocation.requiredPermission,
|
|
2557
|
+
body: renderPlanCommandHelp(plan)
|
|
2558
|
+
};
|
|
2559
|
+
try {
|
|
2560
|
+
return {
|
|
2561
|
+
kind: "matched",
|
|
2562
|
+
invocation: {
|
|
2563
|
+
...invocation,
|
|
2564
|
+
inputs: matchingCommand.parse ? matchingCommand.parse(invocation.arguments) : invocation.arguments
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2569
|
+
return {
|
|
2570
|
+
kind: "invalid",
|
|
2571
|
+
reason,
|
|
2572
|
+
requiredPermission: invocation.requiredPermission,
|
|
2573
|
+
body: renderPlanCommandHelp(plan, reason)
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function hasRequiredRepositoryPermission(actual, required) {
|
|
2578
|
+
if (actual === "none") return false;
|
|
2579
|
+
return permissionOrder.indexOf(actual) >= permissionOrder.indexOf(required);
|
|
2580
|
+
}
|
|
2581
|
+
function permissionDeniedHelp(plan, required) {
|
|
2582
|
+
return renderPlanCommandHelp(plan, `Permission denied: requires ${required}.`);
|
|
2583
|
+
}
|
|
2584
|
+
function renderPlanCommandHelp(plan, reason) {
|
|
2585
|
+
const lines = ["# pipr commands", ""];
|
|
2586
|
+
if (reason) lines.push(reason, "");
|
|
2587
|
+
for (const command of plan.commands) lines.push(`- ${command.pattern} (${command.permission})`);
|
|
2588
|
+
return lines.join("\n");
|
|
2589
|
+
}
|
|
2590
|
+
//#endregion
|
|
2591
|
+
//#region src/diff/path-filter.ts
|
|
2592
|
+
const matcherOptions = {
|
|
2593
|
+
dot: true,
|
|
2594
|
+
nonegate: true,
|
|
2595
|
+
windows: false
|
|
2596
|
+
};
|
|
2597
|
+
const compiledFilters = /* @__PURE__ */ new WeakMap();
|
|
2598
|
+
function filterDiffManifestByPaths(manifest, filter) {
|
|
2599
|
+
if (!filter) return manifest;
|
|
2600
|
+
return {
|
|
2601
|
+
...manifest,
|
|
2602
|
+
files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function diffFileMatchesPathFilter(file, filter) {
|
|
2606
|
+
if (!filter) return true;
|
|
2607
|
+
const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
|
|
2608
|
+
const compiled = readCompiledFilter(filter);
|
|
2609
|
+
const include = compiled.include;
|
|
2610
|
+
const exclude = compiled.exclude;
|
|
2611
|
+
const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
|
|
2612
|
+
const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
|
|
2613
|
+
return included && !excluded;
|
|
2614
|
+
}
|
|
2615
|
+
function pathMatchesFilter(filePath, filter) {
|
|
2616
|
+
if (!filter) return true;
|
|
2617
|
+
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
2618
|
+
const compiled = readCompiledFilter(filter);
|
|
2619
|
+
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
2620
|
+
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
2621
|
+
}
|
|
2622
|
+
function readCompiledFilter(filter) {
|
|
2623
|
+
const existing = compiledFilters.get(filter);
|
|
2624
|
+
if (existing) return existing;
|
|
2625
|
+
const compiled = {
|
|
2626
|
+
include: filter.include?.map(compilePattern),
|
|
2627
|
+
exclude: filter.exclude?.map(compilePattern)
|
|
2628
|
+
};
|
|
2629
|
+
compiledFilters.set(filter, compiled);
|
|
2630
|
+
return compiled;
|
|
2631
|
+
}
|
|
2632
|
+
function compilePattern(pattern) {
|
|
2633
|
+
return {
|
|
2634
|
+
matches: picomatch(pattern, matcherOptions),
|
|
2635
|
+
matchBasename: !pattern.includes("/")
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
//#endregion
|
|
2639
|
+
//#region src/diff/manifest-projection.ts
|
|
2640
|
+
const defaultDiffManifestPromptLimits = {
|
|
2641
|
+
fullMaxBytes: 128 * 1024,
|
|
2642
|
+
fullMaxEstimatedTokens: 32e3,
|
|
2643
|
+
condensedMaxBytes: 256 * 1024,
|
|
2644
|
+
condensedMaxEstimatedTokens: 64e3,
|
|
2645
|
+
toolResponseMaxBytes: 64 * 1024
|
|
2646
|
+
};
|
|
2647
|
+
function projectDiffManifest(manifest, options) {
|
|
2648
|
+
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
2649
|
+
const manifestOptions = options ?? {};
|
|
2650
|
+
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
2651
|
+
return parseDiffManifest({
|
|
2652
|
+
...scopedManifest,
|
|
2653
|
+
files: scopedManifest.files.map((file) => ({
|
|
2654
|
+
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
2655
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
2656
|
+
...rangeFieldsForOptions(range, manifestOptions),
|
|
2657
|
+
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
2658
|
+
}))
|
|
2659
|
+
}))
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
function cloneDiffManifest(manifest) {
|
|
2663
|
+
return parseDiffManifest(structuredClone(manifest));
|
|
2664
|
+
}
|
|
2665
|
+
function prepareDiffManifestPrompt(manifest, config) {
|
|
2666
|
+
const limits = resolveDiffManifestPromptLimits(config);
|
|
2667
|
+
const full = measureDiffManifestPrompt(manifest);
|
|
2668
|
+
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
2669
|
+
mode: "full",
|
|
2670
|
+
manifest,
|
|
2671
|
+
metrics: {
|
|
2672
|
+
full,
|
|
2673
|
+
selected: full
|
|
2674
|
+
},
|
|
2675
|
+
limits
|
|
2676
|
+
};
|
|
2677
|
+
const condensedManifest = condenseDiffManifest(manifest);
|
|
2678
|
+
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
2679
|
+
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
2680
|
+
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
2681
|
+
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
2682
|
+
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
2683
|
+
].join("; "));
|
|
2684
|
+
return {
|
|
2685
|
+
mode: "condensed",
|
|
2686
|
+
manifest: condensedManifest,
|
|
2687
|
+
metrics: {
|
|
2688
|
+
full,
|
|
2689
|
+
selected: condensed
|
|
2690
|
+
},
|
|
2691
|
+
limits
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
function condenseDiffManifest(manifest) {
|
|
2695
|
+
return {
|
|
2696
|
+
baseSha: manifest.baseSha,
|
|
2697
|
+
headSha: manifest.headSha,
|
|
2698
|
+
mergeBaseSha: manifest.mergeBaseSha,
|
|
2699
|
+
files: manifest.files.map(condenseDiffManifestFile)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function measureDiffManifestPrompt(manifest) {
|
|
2703
|
+
const json = JSON.stringify(manifest, null, 2);
|
|
2704
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
2705
|
+
return {
|
|
2706
|
+
bytes,
|
|
2707
|
+
estimatedTokens: Math.ceil(bytes / 4)
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
function resolveDiffManifestPromptLimits(config) {
|
|
2711
|
+
return {
|
|
2712
|
+
...defaultDiffManifestPromptLimits,
|
|
2713
|
+
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function manifestOptionsHaveEffect(options) {
|
|
2717
|
+
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
2718
|
+
}
|
|
2719
|
+
function withoutCompressedFileFields(file, compressed) {
|
|
2720
|
+
if (!compressed) return file;
|
|
2721
|
+
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
2722
|
+
return rest;
|
|
2723
|
+
}
|
|
2724
|
+
function withoutCompressedRangeFields(range, compressed) {
|
|
2725
|
+
if (!compressed) return range;
|
|
2726
|
+
const { summary: _summary, ...rest } = range;
|
|
2727
|
+
return rest;
|
|
2728
|
+
}
|
|
2729
|
+
function rangeFieldsForOptions(range, options) {
|
|
2730
|
+
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
2731
|
+
if (options.includePreviews === false) {
|
|
2732
|
+
const { preview: _preview, ...rest } = fields;
|
|
2733
|
+
return rest;
|
|
2734
|
+
}
|
|
2735
|
+
return fields;
|
|
2736
|
+
}
|
|
2737
|
+
function truncatePreview(preview, maxLines) {
|
|
2738
|
+
if (preview === void 0 || maxLines === void 0) return preview;
|
|
2739
|
+
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
2740
|
+
}
|
|
2741
|
+
function condenseDiffManifestFile(file) {
|
|
2742
|
+
return {
|
|
2743
|
+
path: file.path,
|
|
2744
|
+
previousPath: file.previousPath,
|
|
2745
|
+
status: file.status,
|
|
2746
|
+
language: file.language,
|
|
2747
|
+
additions: file.additions,
|
|
2748
|
+
deletions: file.deletions,
|
|
2749
|
+
hunks: file.hunks.map((hunk) => ({
|
|
2750
|
+
hunkIndex: hunk.hunkIndex,
|
|
2751
|
+
header: hunk.header,
|
|
2752
|
+
oldStart: hunk.oldStart,
|
|
2753
|
+
oldLines: hunk.oldLines,
|
|
2754
|
+
newStart: hunk.newStart,
|
|
2755
|
+
newLines: hunk.newLines,
|
|
2756
|
+
contentHash: hunk.contentHash
|
|
2757
|
+
})),
|
|
2758
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
2759
|
+
id: range.id,
|
|
2760
|
+
path: range.path,
|
|
2761
|
+
side: range.side,
|
|
2762
|
+
startLine: range.startLine,
|
|
2763
|
+
endLine: range.endLine,
|
|
2764
|
+
kind: range.kind,
|
|
2765
|
+
hunkIndex: range.hunkIndex,
|
|
2766
|
+
hunkHeader: range.hunkHeader,
|
|
2767
|
+
hunkContentHash: range.hunkContentHash
|
|
2768
|
+
})),
|
|
2769
|
+
excludedReason: file.excludedReason
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
2773
|
+
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
2774
|
+
}
|
|
2775
|
+
//#endregion
|
|
2776
|
+
//#region src/pi/runtime-tools.ts
|
|
2777
|
+
const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
|
|
2778
|
+
async function preparePiRuntimeReadTools(options) {
|
|
2779
|
+
const toolRoot = path.join(options.root, "runtime-tools");
|
|
2780
|
+
const baseRoot = path.join(toolRoot, "base");
|
|
2781
|
+
await mkdir(baseRoot, { recursive: true });
|
|
2782
|
+
const baseRanges = await materializeBaseRangeSnapshots({
|
|
2783
|
+
baseRoot,
|
|
2784
|
+
manifest: options.request.manifest,
|
|
2785
|
+
sourceWorkspace: options.sourceWorkspace,
|
|
2786
|
+
maxBytes: options.request.toolResponseMaxBytes
|
|
2787
|
+
});
|
|
2788
|
+
const data = {
|
|
2789
|
+
manifest: options.request.manifest,
|
|
2790
|
+
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
2791
|
+
baseRanges
|
|
2792
|
+
};
|
|
2793
|
+
const dataPath = path.join(toolRoot, "data.json");
|
|
2794
|
+
await Bun.write(dataPath, JSON.stringify(data));
|
|
2795
|
+
return {
|
|
2796
|
+
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
2797
|
+
dataPath,
|
|
2798
|
+
toolNames: piRuntimeReadToolNames
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async function piRuntimeToolsExtensionPath() {
|
|
2802
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2803
|
+
const candidates = [
|
|
2804
|
+
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
2805
|
+
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
2806
|
+
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
2807
|
+
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
2808
|
+
];
|
|
2809
|
+
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
2810
|
+
throw new Error("Unable to locate pipr runtime tools extension");
|
|
2811
|
+
}
|
|
2812
|
+
async function pathExists(filePath) {
|
|
2813
|
+
return await Bun.file(filePath).exists();
|
|
2427
2814
|
}
|
|
2428
2815
|
async function materializeBaseRangeSnapshots(options) {
|
|
2429
2816
|
const ranges = {};
|
|
@@ -2853,51 +3240,31 @@ function createRuntimeActionLog(options) {
|
|
|
2853
3240
|
for (const [key, value] of Object.entries(options.env ?? process.env)) if (sensitiveEnvNamePattern.test(key)) addSecret(secrets, value);
|
|
2854
3241
|
const debugEnabled = (options.env ?? process.env).ACTIONS_STEP_DEBUG === "true" || (options.env ?? process.env).PIPR_LOG_LEVEL === "debug";
|
|
2855
3242
|
const sink = options.logSink ?? noopActionLogSink;
|
|
2856
|
-
const writer = structuredLogWriter(sink, secrets);
|
|
2857
|
-
const logger = pino({
|
|
2858
|
-
base: void 0,
|
|
2859
|
-
level: debugEnabled ? "debug" : "info",
|
|
2860
|
-
timestamp: false,
|
|
2861
|
-
messageKey: "event",
|
|
2862
|
-
customLevels: { notice: 35 },
|
|
2863
|
-
formatters: { level(label) {
|
|
2864
|
-
return { level: label };
|
|
2865
|
-
} }
|
|
2866
|
-
}, writer.stream);
|
|
2867
3243
|
return {
|
|
2868
3244
|
debugEnabled,
|
|
2869
3245
|
writesToSink: options.logSink !== void 0,
|
|
2870
3246
|
info(event, fields) {
|
|
2871
|
-
|
|
2872
|
-
logger.info(compactFields(fields, secrets), redact(event, secrets));
|
|
3247
|
+
emitRecord(sink, secrets, "info", event, fields);
|
|
2873
3248
|
},
|
|
2874
3249
|
notice(event, fields) {
|
|
2875
|
-
|
|
2876
|
-
logger.notice(compactFields(fields, secrets), redact(event, secrets));
|
|
3250
|
+
emitRecord(sink, secrets, "notice", event, fields);
|
|
2877
3251
|
},
|
|
2878
3252
|
warning(event, fields) {
|
|
2879
|
-
|
|
2880
|
-
logger.warn(compactFields(fields, secrets), redact(event, secrets));
|
|
3253
|
+
emitRecord(sink, secrets, "warning", event, fields);
|
|
2881
3254
|
},
|
|
2882
3255
|
error(event, fields) {
|
|
2883
|
-
|
|
2884
|
-
logger.error(compactFields(fields, secrets), redact(event, secrets));
|
|
3256
|
+
emitRecord(sink, secrets, "error", event, fields);
|
|
2885
3257
|
},
|
|
2886
3258
|
debug(event, fields) {
|
|
2887
|
-
if (debugEnabled)
|
|
2888
|
-
writer.level = "debug";
|
|
2889
|
-
logger.debug(compactFields(fields, secrets), redact(event, secrets));
|
|
2890
|
-
}
|
|
3259
|
+
if (debugEnabled) emitRecord(sink, secrets, "debug", event, fields);
|
|
2891
3260
|
},
|
|
2892
3261
|
text(level, event, text) {
|
|
2893
3262
|
if (level === "debug" && !debugEnabled) return;
|
|
2894
|
-
|
|
2895
|
-
sinkForLevel(sink, level)(message);
|
|
3263
|
+
emitRecord(sink, secrets, level, event, void 0, redact(text, secrets));
|
|
2896
3264
|
},
|
|
2897
3265
|
textSnippet(level, event, text, snippetOptions) {
|
|
2898
3266
|
if (level === "debug" && !debugEnabled) return;
|
|
2899
|
-
|
|
2900
|
-
sinkForLevel(sink, level)(message);
|
|
3267
|
+
emitRecord(sink, secrets, level, event, void 0, formatTextSnippet(text, secrets, snippetOptions));
|
|
2901
3268
|
},
|
|
2902
3269
|
formatTextSnippet(text, snippetOptions) {
|
|
2903
3270
|
return formatTextSnippet(text, secrets, snippetOptions);
|
|
@@ -2941,93 +3308,21 @@ function compactFields(fields, secrets) {
|
|
|
2941
3308
|
function formatTextSnippet(text, secrets, options) {
|
|
2942
3309
|
return boundedLogSnippet(redact(text, secrets), options);
|
|
2943
3310
|
}
|
|
2944
|
-
function
|
|
2945
|
-
|
|
2946
|
-
level
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
};
|
|
2951
|
-
return writer;
|
|
2952
|
-
}
|
|
2953
|
-
function structuredLine(logger, writer, level, event) {
|
|
2954
|
-
let line = "";
|
|
2955
|
-
const previousStream = writer.stream.write;
|
|
2956
|
-
writer.stream.write = (value) => {
|
|
2957
|
-
line = value.trimEnd();
|
|
2958
|
-
};
|
|
2959
|
-
writer.level = level;
|
|
2960
|
-
logger[logMethod(level)]({}, event);
|
|
2961
|
-
writer.stream.write = previousStream;
|
|
2962
|
-
return line;
|
|
2963
|
-
}
|
|
2964
|
-
function logMethod(level) {
|
|
2965
|
-
if (level === "warning") return "warn";
|
|
2966
|
-
return level;
|
|
2967
|
-
}
|
|
2968
|
-
function sinkForLevel(sink, level) {
|
|
2969
|
-
if (level === "warning") return sink.warning.bind(sink);
|
|
2970
|
-
return sink[level].bind(sink);
|
|
3311
|
+
function emitRecord(sink, secrets, level, event, fields, text) {
|
|
3312
|
+
sink.log({
|
|
3313
|
+
level,
|
|
3314
|
+
event: redact(event, secrets),
|
|
3315
|
+
fields: compactFields(fields, secrets),
|
|
3316
|
+
text
|
|
3317
|
+
});
|
|
2971
3318
|
}
|
|
2972
3319
|
const noopActionLogSink = {
|
|
2973
|
-
|
|
2974
|
-
notice() {},
|
|
2975
|
-
warning() {},
|
|
2976
|
-
error() {},
|
|
2977
|
-
debug() {},
|
|
3320
|
+
log() {},
|
|
2978
3321
|
async group(_name, run) {
|
|
2979
3322
|
return await run();
|
|
2980
3323
|
}
|
|
2981
3324
|
};
|
|
2982
3325
|
//#endregion
|
|
2983
|
-
//#region src/diff/path-filter.ts
|
|
2984
|
-
const matcherOptions = {
|
|
2985
|
-
dot: true,
|
|
2986
|
-
nonegate: true,
|
|
2987
|
-
windows: false
|
|
2988
|
-
};
|
|
2989
|
-
const compiledFilters = /* @__PURE__ */ new WeakMap();
|
|
2990
|
-
function filterDiffManifestByPaths(manifest, filter) {
|
|
2991
|
-
if (!filter) return manifest;
|
|
2992
|
-
return {
|
|
2993
|
-
...manifest,
|
|
2994
|
-
files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
function diffFileMatchesPathFilter(file, filter) {
|
|
2998
|
-
if (!filter) return true;
|
|
2999
|
-
const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
|
|
3000
|
-
const compiled = readCompiledFilter(filter);
|
|
3001
|
-
const include = compiled.include;
|
|
3002
|
-
const exclude = compiled.exclude;
|
|
3003
|
-
const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
|
|
3004
|
-
const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
|
|
3005
|
-
return included && !excluded;
|
|
3006
|
-
}
|
|
3007
|
-
function pathMatchesFilter(filePath, filter) {
|
|
3008
|
-
if (!filter) return true;
|
|
3009
|
-
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
3010
|
-
const compiled = readCompiledFilter(filter);
|
|
3011
|
-
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
3012
|
-
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
3013
|
-
}
|
|
3014
|
-
function readCompiledFilter(filter) {
|
|
3015
|
-
const existing = compiledFilters.get(filter);
|
|
3016
|
-
if (existing) return existing;
|
|
3017
|
-
const compiled = {
|
|
3018
|
-
include: filter.include?.map(compilePattern),
|
|
3019
|
-
exclude: filter.exclude?.map(compilePattern)
|
|
3020
|
-
};
|
|
3021
|
-
compiledFilters.set(filter, compiled);
|
|
3022
|
-
return compiled;
|
|
3023
|
-
}
|
|
3024
|
-
function compilePattern(pattern) {
|
|
3025
|
-
return {
|
|
3026
|
-
matches: picomatch(pattern, matcherOptions),
|
|
3027
|
-
matchBasename: !pattern.includes("/")
|
|
3028
|
-
};
|
|
3029
|
-
}
|
|
3030
|
-
//#endregion
|
|
3031
3326
|
//#region src/review/range-validation.ts
|
|
3032
3327
|
function assertFindingMatchesRange(finding, range) {
|
|
3033
3328
|
const reason = findingRangeMismatchReason(finding, range);
|
|
@@ -3043,7 +3338,7 @@ function findingRangeMismatchReason(finding, range) {
|
|
|
3043
3338
|
}
|
|
3044
3339
|
//#endregion
|
|
3045
3340
|
//#region src/review/review.ts
|
|
3046
|
-
function
|
|
3341
|
+
function validateReviewResult(review, manifest, options) {
|
|
3047
3342
|
if (options.expectedHeadSha && manifest.headSha !== options.expectedHeadSha) throw new Error(`Diff Manifest head SHA '${manifest.headSha}' does not match expected head SHA '${options.expectedHeadSha}'`);
|
|
3048
3343
|
const ranges = createDiffRangeIndex(manifest);
|
|
3049
3344
|
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
@@ -3111,7 +3406,7 @@ function findingFingerprint(finding) {
|
|
|
3111
3406
|
//#endregion
|
|
3112
3407
|
//#region src/review/agent/agent-prompt.ts
|
|
3113
3408
|
async function renderAgentPrompt(options) {
|
|
3114
|
-
const prompt = await options.agent
|
|
3409
|
+
const prompt = await renderAgentDefinitionPrompt(options.agent, options.input, options.agentRunContext.prompt);
|
|
3115
3410
|
const toolMode = options.toolMode ?? "read-only";
|
|
3116
3411
|
return compact([
|
|
3117
3412
|
promptSection("Role", "You are pipr's read-only change request agent."),
|
|
@@ -3126,6 +3421,9 @@ async function renderAgentPrompt(options) {
|
|
|
3126
3421
|
promptSection("Prompt", renderPromptValue(prompt))
|
|
3127
3422
|
]).join("\n\n");
|
|
3128
3423
|
}
|
|
3424
|
+
function renderAgentDefinitionPrompt(agent, input, context) {
|
|
3425
|
+
return agent.definition.prompt(input, { ...context });
|
|
3426
|
+
}
|
|
3129
3427
|
function promptSection(title, body) {
|
|
3130
3428
|
if (!body?.trim()) return;
|
|
3131
3429
|
return `${title}:\n${body}`;
|
|
@@ -3142,12 +3440,12 @@ function outputPrompt(schema) {
|
|
|
3142
3440
|
return compact([
|
|
3143
3441
|
`Schema ID: ${schema.id}.`,
|
|
3144
3442
|
schema.jsonSchema ? `JSON Schema:\n${JSON.stringify(schema.jsonSchema, null, 2)}` : void 0,
|
|
3145
|
-
schema.id ===
|
|
3146
|
-
schema.id ===
|
|
3443
|
+
schema.id === reviewResultSchemaId ? `Example:\n${JSON.stringify(reviewSchemaExample$1(), null, 2)}` : void 0,
|
|
3444
|
+
schema.id === reviewResultSchemaId ? "`suggestedFix` is exact replacement code for the selected range. Do not include Markdown fences, prose, or labels in `suggestedFix`." : void 0,
|
|
3147
3445
|
"Return exactly one JSON value matching the schema.",
|
|
3148
3446
|
"The first non-whitespace character must be { or [ and the last non-whitespace character must be } or ].",
|
|
3149
3447
|
"Do not include Markdown, code fences, prose, explanations, or leading/trailing text.",
|
|
3150
|
-
schema.id ===
|
|
3448
|
+
schema.id === reviewResultSchemaId ? "For inlineFindings, use only fields shown in the schema and only exact Diff Manifest commentable ranges. If no exact range applies, omit the finding." : void 0
|
|
3151
3449
|
]).join("\n\n");
|
|
3152
3450
|
}
|
|
3153
3451
|
function pathScopePrompt(paths) {
|
|
@@ -3184,143 +3482,6 @@ function customToolPrompt(agentTools) {
|
|
|
3184
3482
|
return ["Custom plugin tools:", ...agentTools.customTools.map((tool) => `${tool.name}: ${tool.description ?? "No description."}`)].join("\n");
|
|
3185
3483
|
}
|
|
3186
3484
|
//#endregion
|
|
3187
|
-
//#region src/diff/manifest-projection.ts
|
|
3188
|
-
const defaultDiffManifestPromptLimits = {
|
|
3189
|
-
fullMaxBytes: 128 * 1024,
|
|
3190
|
-
fullMaxEstimatedTokens: 32e3,
|
|
3191
|
-
condensedMaxBytes: 256 * 1024,
|
|
3192
|
-
condensedMaxEstimatedTokens: 64e3,
|
|
3193
|
-
toolResponseMaxBytes: 64 * 1024
|
|
3194
|
-
};
|
|
3195
|
-
function projectDiffManifest(manifest, options) {
|
|
3196
|
-
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
3197
|
-
const manifestOptions = options ?? {};
|
|
3198
|
-
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
3199
|
-
return parseDiffManifest({
|
|
3200
|
-
...scopedManifest,
|
|
3201
|
-
files: scopedManifest.files.map((file) => ({
|
|
3202
|
-
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
3203
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
3204
|
-
...rangeFieldsForOptions(range, manifestOptions),
|
|
3205
|
-
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
3206
|
-
}))
|
|
3207
|
-
}))
|
|
3208
|
-
});
|
|
3209
|
-
}
|
|
3210
|
-
function cloneDiffManifest(manifest) {
|
|
3211
|
-
return parseDiffManifest(structuredClone(manifest));
|
|
3212
|
-
}
|
|
3213
|
-
function prepareDiffManifestPrompt(manifest, config) {
|
|
3214
|
-
const limits = resolveDiffManifestPromptLimits(config);
|
|
3215
|
-
const full = measureDiffManifestPrompt(manifest);
|
|
3216
|
-
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
3217
|
-
mode: "full",
|
|
3218
|
-
manifest,
|
|
3219
|
-
metrics: {
|
|
3220
|
-
full,
|
|
3221
|
-
selected: full
|
|
3222
|
-
},
|
|
3223
|
-
limits
|
|
3224
|
-
};
|
|
3225
|
-
const condensedManifest = condenseDiffManifest(manifest);
|
|
3226
|
-
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
3227
|
-
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
3228
|
-
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
3229
|
-
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
3230
|
-
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
3231
|
-
].join("; "));
|
|
3232
|
-
return {
|
|
3233
|
-
mode: "condensed",
|
|
3234
|
-
manifest: condensedManifest,
|
|
3235
|
-
metrics: {
|
|
3236
|
-
full,
|
|
3237
|
-
selected: condensed
|
|
3238
|
-
},
|
|
3239
|
-
limits
|
|
3240
|
-
};
|
|
3241
|
-
}
|
|
3242
|
-
function condenseDiffManifest(manifest) {
|
|
3243
|
-
return {
|
|
3244
|
-
baseSha: manifest.baseSha,
|
|
3245
|
-
headSha: manifest.headSha,
|
|
3246
|
-
mergeBaseSha: manifest.mergeBaseSha,
|
|
3247
|
-
files: manifest.files.map(condenseDiffManifestFile)
|
|
3248
|
-
};
|
|
3249
|
-
}
|
|
3250
|
-
function measureDiffManifestPrompt(manifest) {
|
|
3251
|
-
const json = JSON.stringify(manifest, null, 2);
|
|
3252
|
-
const bytes = Buffer.byteLength(json, "utf8");
|
|
3253
|
-
return {
|
|
3254
|
-
bytes,
|
|
3255
|
-
estimatedTokens: Math.ceil(bytes / 4)
|
|
3256
|
-
};
|
|
3257
|
-
}
|
|
3258
|
-
function resolveDiffManifestPromptLimits(config) {
|
|
3259
|
-
return {
|
|
3260
|
-
...defaultDiffManifestPromptLimits,
|
|
3261
|
-
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
function manifestOptionsHaveEffect(options) {
|
|
3265
|
-
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
3266
|
-
}
|
|
3267
|
-
function withoutCompressedFileFields(file, compressed) {
|
|
3268
|
-
if (!compressed) return file;
|
|
3269
|
-
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
3270
|
-
return rest;
|
|
3271
|
-
}
|
|
3272
|
-
function withoutCompressedRangeFields(range, compressed) {
|
|
3273
|
-
if (!compressed) return range;
|
|
3274
|
-
const { summary: _summary, ...rest } = range;
|
|
3275
|
-
return rest;
|
|
3276
|
-
}
|
|
3277
|
-
function rangeFieldsForOptions(range, options) {
|
|
3278
|
-
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
3279
|
-
if (options.includePreviews === false) {
|
|
3280
|
-
const { preview: _preview, ...rest } = fields;
|
|
3281
|
-
return rest;
|
|
3282
|
-
}
|
|
3283
|
-
return fields;
|
|
3284
|
-
}
|
|
3285
|
-
function truncatePreview(preview, maxLines) {
|
|
3286
|
-
if (preview === void 0 || maxLines === void 0) return preview;
|
|
3287
|
-
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
3288
|
-
}
|
|
3289
|
-
function condenseDiffManifestFile(file) {
|
|
3290
|
-
return {
|
|
3291
|
-
path: file.path,
|
|
3292
|
-
previousPath: file.previousPath,
|
|
3293
|
-
status: file.status,
|
|
3294
|
-
language: file.language,
|
|
3295
|
-
additions: file.additions,
|
|
3296
|
-
deletions: file.deletions,
|
|
3297
|
-
hunks: file.hunks.map((hunk) => ({
|
|
3298
|
-
hunkIndex: hunk.hunkIndex,
|
|
3299
|
-
header: hunk.header,
|
|
3300
|
-
oldStart: hunk.oldStart,
|
|
3301
|
-
oldLines: hunk.oldLines,
|
|
3302
|
-
newStart: hunk.newStart,
|
|
3303
|
-
newLines: hunk.newLines,
|
|
3304
|
-
contentHash: hunk.contentHash
|
|
3305
|
-
})),
|
|
3306
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
3307
|
-
id: range.id,
|
|
3308
|
-
path: range.path,
|
|
3309
|
-
side: range.side,
|
|
3310
|
-
startLine: range.startLine,
|
|
3311
|
-
endLine: range.endLine,
|
|
3312
|
-
kind: range.kind,
|
|
3313
|
-
hunkIndex: range.hunkIndex,
|
|
3314
|
-
hunkHeader: range.hunkHeader,
|
|
3315
|
-
hunkContentHash: range.hunkContentHash
|
|
3316
|
-
})),
|
|
3317
|
-
excludedReason: file.excludedReason
|
|
3318
|
-
};
|
|
3319
|
-
}
|
|
3320
|
-
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
3321
|
-
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
3322
|
-
}
|
|
3323
|
-
//#endregion
|
|
3324
3485
|
//#region src/review/agent/diff-manifest-context.ts
|
|
3325
3486
|
function prepareDiffManifestContext(options) {
|
|
3326
3487
|
const manifest = readReservedInputManifest(options.input);
|
|
@@ -3419,7 +3580,8 @@ function resolveProvider(config, providerId) {
|
|
|
3419
3580
|
return provider;
|
|
3420
3581
|
}
|
|
3421
3582
|
function createAgentRunContext(runtime) {
|
|
3422
|
-
const runId =
|
|
3583
|
+
const runId = runtime.runId ?? runtime.taskContext?.run.id;
|
|
3584
|
+
if (!runId) throw new Error("runId is required for stable review run identity");
|
|
3423
3585
|
const repositorySlugParts = runtime.event.repository.slug.split("/");
|
|
3424
3586
|
const repository = {
|
|
3425
3587
|
root: runtime.workspace,
|
|
@@ -3652,9 +3814,9 @@ function parseAgentOutput(output, agent) {
|
|
|
3652
3814
|
let lastError = "";
|
|
3653
3815
|
for (const payload of jsonPayloadCandidates(output)) try {
|
|
3654
3816
|
const json = JSON.parse(payload);
|
|
3655
|
-
if (agent.definition.output.id ===
|
|
3817
|
+
if (agent.definition.output.id === reviewResultSchemaId) return {
|
|
3656
3818
|
ok: true,
|
|
3657
|
-
value:
|
|
3819
|
+
value: parseReviewResult(json),
|
|
3658
3820
|
repairAttempted: false
|
|
3659
3821
|
};
|
|
3660
3822
|
return {
|
|
@@ -3690,6 +3852,9 @@ function buildRepairPrompt(options) {
|
|
|
3690
3852
|
].join("\n\n");
|
|
3691
3853
|
}
|
|
3692
3854
|
//#endregion
|
|
3855
|
+
//#region package.json
|
|
3856
|
+
var version = "0.2.1";
|
|
3857
|
+
//#endregion
|
|
3693
3858
|
//#region src/review/prior-state.ts
|
|
3694
3859
|
const mainCommentMarker = "pipr:main-comment";
|
|
3695
3860
|
const inlineFindingMarkerPrefix = "pipr:finding";
|
|
@@ -3732,7 +3897,7 @@ function buildPriorReviewState(options) {
|
|
|
3732
3897
|
const prior = priorFindings.get(id);
|
|
3733
3898
|
if (prior) usedPriorIds.add(prior.id);
|
|
3734
3899
|
currentFindingIds.add(id);
|
|
3735
|
-
|
|
3900
|
+
const record = {
|
|
3736
3901
|
id,
|
|
3737
3902
|
status: "open",
|
|
3738
3903
|
path: finding.path,
|
|
@@ -3742,29 +3907,30 @@ function buildPriorReviewState(options) {
|
|
|
3742
3907
|
endLine: finding.endLine,
|
|
3743
3908
|
firstSeenHeadSha: prior?.firstSeenHeadSha ?? options.reviewedHeadSha,
|
|
3744
3909
|
lastSeenHeadSha: options.reviewedHeadSha,
|
|
3745
|
-
lastCommentedHeadSha: prior
|
|
3746
|
-
}
|
|
3910
|
+
...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
|
|
3911
|
+
};
|
|
3912
|
+
nextFindings.set(id, record);
|
|
3747
3913
|
}
|
|
3748
3914
|
for (const prior of priorFindings.values()) {
|
|
3749
3915
|
if (nextFindings.has(prior.id)) continue;
|
|
3750
|
-
nextFindings.set(prior.id,
|
|
3916
|
+
nextFindings.set(prior.id, prior);
|
|
3751
3917
|
}
|
|
3752
|
-
return {
|
|
3918
|
+
return {
|
|
3753
3919
|
version: 1,
|
|
3754
3920
|
reviewedHeadSha: options.reviewedHeadSha,
|
|
3755
3921
|
selectedTasks: options.selectedTasks,
|
|
3756
3922
|
findings: cappedFindings([...nextFindings.values()], currentFindingIds)
|
|
3757
|
-
}
|
|
3923
|
+
};
|
|
3758
3924
|
}
|
|
3759
3925
|
function resolvePriorFindings(state, findingIds) {
|
|
3760
3926
|
const resolved = new Set(findingIds);
|
|
3761
|
-
return
|
|
3927
|
+
return {
|
|
3762
3928
|
...state,
|
|
3763
3929
|
findings: state.findings.map((finding) => ({
|
|
3764
3930
|
...finding,
|
|
3765
3931
|
status: resolved.has(finding.id) ? "resolved" : finding.status
|
|
3766
3932
|
}))
|
|
3767
|
-
}
|
|
3933
|
+
};
|
|
3768
3934
|
}
|
|
3769
3935
|
function priorReviewStateForSelectedTasks(state, selectedTasks) {
|
|
3770
3936
|
if (!state || state.selectedTasks.length !== selectedTasks.length || !state.selectedTasks.every((taskName, index) => taskName === selectedTasks[index])) return;
|
|
@@ -3818,26 +3984,26 @@ function renderVerifierResponseMarker(findingId, responseKey) {
|
|
|
3818
3984
|
return `<!-- ${verifierResponseMarkerPrefix} id=${findingId} key=${responseKey} -->`;
|
|
3819
3985
|
}
|
|
3820
3986
|
function extractInlineFindingMarkerRecords(commentBodies) {
|
|
3821
|
-
return commentBodies
|
|
3987
|
+
return extractMarkerRecords(commentBodies, inlineFindingMarkerPrefix);
|
|
3822
3988
|
}
|
|
3823
3989
|
function extractInlineFindingMarkers(commentBodies) {
|
|
3824
3990
|
return new Set(extractInlineFindingMarkerRecords(commentBodies).map((record) => record.marker));
|
|
3825
3991
|
}
|
|
3826
3992
|
function extractResolvedFindingMarkerRecords(commentBodies) {
|
|
3827
|
-
return commentBodies
|
|
3993
|
+
return extractMarkerRecords(commentBodies, resolvedFindingMarkerPrefix);
|
|
3828
3994
|
}
|
|
3829
3995
|
function applyResolvedFindingMarkers(state, commentBodies) {
|
|
3830
3996
|
const resolvedMarkers = new Set(extractResolvedFindingMarkerRecords(commentBodies).map((record) => `${record.id}:${record.head}`));
|
|
3831
|
-
return
|
|
3997
|
+
return {
|
|
3832
3998
|
...state,
|
|
3833
3999
|
findings: state.findings.map((finding) => ({
|
|
3834
4000
|
...finding,
|
|
3835
4001
|
status: finding.lastCommentedHeadSha && resolvedMarkers.has(`${finding.id}:${finding.lastCommentedHeadSha}`) ? "resolved" : finding.status
|
|
3836
4002
|
}))
|
|
3837
|
-
}
|
|
4003
|
+
};
|
|
3838
4004
|
}
|
|
3839
4005
|
function extractVerifierResponseMarkers(commentBodies) {
|
|
3840
|
-
return new Set(commentBodies
|
|
4006
|
+
return new Set(extractMarkerRecords(commentBodies, verifierResponseMarkerPrefix).map((record) => record.marker));
|
|
3841
4007
|
}
|
|
3842
4008
|
function isPiprThreadActionReplyBody(body) {
|
|
3843
4009
|
const parsed = parsePiprMarker(body ? firstNonEmptyLine(body) : void 0);
|
|
@@ -3849,13 +4015,17 @@ function applyInlineFindingMarkers(state, commentBodies) {
|
|
|
3849
4015
|
const [, , findingId, headSha] = marker.split(":");
|
|
3850
4016
|
if (findingId && headSha) markerById.set(findingId, headSha);
|
|
3851
4017
|
}
|
|
3852
|
-
return
|
|
4018
|
+
return {
|
|
3853
4019
|
...state,
|
|
3854
|
-
findings: state.findings.map((finding) =>
|
|
3855
|
-
|
|
3856
|
-
lastCommentedHeadSha:
|
|
3857
|
-
|
|
3858
|
-
|
|
4020
|
+
findings: state.findings.map((finding) => {
|
|
4021
|
+
const headSha = markerById.get(finding.id);
|
|
4022
|
+
const { lastCommentedHeadSha: _lastCommentedHeadSha, ...rest } = finding;
|
|
4023
|
+
return headSha ? {
|
|
4024
|
+
...rest,
|
|
4025
|
+
lastCommentedHeadSha: headSha
|
|
4026
|
+
} : rest;
|
|
4027
|
+
})
|
|
4028
|
+
};
|
|
3859
4029
|
}
|
|
3860
4030
|
function findingIdFor(finding, state) {
|
|
3861
4031
|
return (state ? matchFindingRecord(state, finding) : void 0)?.id ?? newFindingId(finding);
|
|
@@ -3904,8 +4074,11 @@ function parseFindingHeadMarker(comment, prefix) {
|
|
|
3904
4074
|
marker: prefix === inlineFindingMarkerPrefix ? inlineFindingMarker(id, head) : prefix === resolvedFindingMarkerPrefix ? `${resolvedFindingMarkerPrefix}:${id}:${head}` : `${verifierResponseMarkerPrefix}:${id}:${head}`
|
|
3905
4075
|
};
|
|
3906
4076
|
}
|
|
4077
|
+
function extractMarkerRecords(commentBodies, prefix) {
|
|
4078
|
+
return commentBodies.flatMap((body) => [parseFindingHeadMarker(firstNonEmptyLine(body), prefix)].filter((marker) => marker !== void 0));
|
|
4079
|
+
}
|
|
3907
4080
|
function encodeReviewState(state) {
|
|
3908
|
-
return Buffer$1.from(JSON.stringify(
|
|
4081
|
+
return Buffer$1.from(JSON.stringify(state)).toString("base64url");
|
|
3909
4082
|
}
|
|
3910
4083
|
function decodeReviewState(value) {
|
|
3911
4084
|
if (!value) return;
|
|
@@ -3939,143 +4112,8 @@ function hashParts(parts) {
|
|
|
3939
4112
|
return new Bun.CryptoHasher("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
|
|
3940
4113
|
}
|
|
3941
4114
|
//#endregion
|
|
3942
|
-
//#region src/action/entry-dispatch.ts
|
|
3943
|
-
const permissionOrder = [
|
|
3944
|
-
"read",
|
|
3945
|
-
"triage",
|
|
3946
|
-
"write",
|
|
3947
|
-
"maintain",
|
|
3948
|
-
"admin"
|
|
3949
|
-
];
|
|
3950
|
-
const changeRequestActions = [
|
|
3951
|
-
"opened",
|
|
3952
|
-
"updated",
|
|
3953
|
-
"reopened",
|
|
3954
|
-
"ready",
|
|
3955
|
-
"closed"
|
|
3956
|
-
];
|
|
3957
|
-
function dispatchRuntimeEntry(options) {
|
|
3958
|
-
if (options.kind === "change-request") return {
|
|
3959
|
-
kind: "change-request",
|
|
3960
|
-
tasks: selectRuntimeTasks({
|
|
3961
|
-
plan: options.plan,
|
|
3962
|
-
event: options.event,
|
|
3963
|
-
taskName: options.taskName
|
|
3964
|
-
}),
|
|
3965
|
-
taskName: options.taskName
|
|
3966
|
-
};
|
|
3967
|
-
return resolvePlanCommand(options.plan, options.line);
|
|
3968
|
-
}
|
|
3969
|
-
function selectRuntimeTasks(options) {
|
|
3970
|
-
if (options.taskName) return options.plan.tasks.filter((task) => task.name === options.taskName);
|
|
3971
|
-
return selectChangeRequestTasks(options.plan, options.event);
|
|
3972
|
-
}
|
|
3973
|
-
function selectLocalReviewTasks(plan) {
|
|
3974
|
-
return uniqBy(plan.changeRequestTriggers.map((trigger) => trigger.task), (task) => task.name).filter((task) => task.local !== false);
|
|
3975
|
-
}
|
|
3976
|
-
function selectChangeRequestTasks(plan, event) {
|
|
3977
|
-
if (!changeRequestActions.includes(event.action)) return [];
|
|
3978
|
-
const action = event.action;
|
|
3979
|
-
return uniqBy(plan.changeRequestTriggers.filter((trigger) => trigger.actions.includes(action)).map((trigger) => trigger.task), (task) => task.name);
|
|
3980
|
-
}
|
|
3981
|
-
function selectPlanCommand(plan, line) {
|
|
3982
|
-
let firstInvalid;
|
|
3983
|
-
for (const command of plan.commands) {
|
|
3984
|
-
const parsed = parseCommandPattern(command.pattern, line);
|
|
3985
|
-
if (!parsed.ok) {
|
|
3986
|
-
if (commandPatternPrefixMatches(command.pattern, line) && !firstInvalid) firstInvalid = {
|
|
3987
|
-
kind: "invalid",
|
|
3988
|
-
command,
|
|
3989
|
-
error: parsed.error
|
|
3990
|
-
};
|
|
3991
|
-
continue;
|
|
3992
|
-
}
|
|
3993
|
-
return {
|
|
3994
|
-
kind: "matched",
|
|
3995
|
-
command,
|
|
3996
|
-
commandName: command.pattern.replace(/^@pipr\s+/, "").split(/\s+/)[0] ?? command.pattern,
|
|
3997
|
-
line,
|
|
3998
|
-
arguments: parsed.value
|
|
3999
|
-
};
|
|
4000
|
-
}
|
|
4001
|
-
return firstInvalid;
|
|
4002
|
-
}
|
|
4003
|
-
function resolvePlanCommand(plan, line) {
|
|
4004
|
-
if (!line) return {
|
|
4005
|
-
kind: "ignored",
|
|
4006
|
-
reason: "comment did not contain a command line"
|
|
4007
|
-
};
|
|
4008
|
-
if (!isPiprCommandLine(line)) return {
|
|
4009
|
-
kind: "ignored",
|
|
4010
|
-
reason: "comment did not target pipr"
|
|
4011
|
-
};
|
|
4012
|
-
const selected = selectPlanCommand(plan, line);
|
|
4013
|
-
if (selected?.kind === "matched") return {
|
|
4014
|
-
kind: "matched",
|
|
4015
|
-
invocation: {
|
|
4016
|
-
taskName: selected.command.task.name,
|
|
4017
|
-
commandName: selected.commandName,
|
|
4018
|
-
requiredPermission: selected.command.permission,
|
|
4019
|
-
line: selected.line,
|
|
4020
|
-
pattern: selected.command.pattern,
|
|
4021
|
-
arguments: selected.arguments
|
|
4022
|
-
}
|
|
4023
|
-
};
|
|
4024
|
-
if (selected?.kind === "invalid") return {
|
|
4025
|
-
kind: "invalid",
|
|
4026
|
-
reason: selected.error,
|
|
4027
|
-
requiredPermission: selected.command.permission,
|
|
4028
|
-
body: renderPlanCommandHelp(plan, selected.error)
|
|
4029
|
-
};
|
|
4030
|
-
return {
|
|
4031
|
-
kind: "help",
|
|
4032
|
-
reason: `unknown pipr command '${line}'`,
|
|
4033
|
-
requiredPermission: "read",
|
|
4034
|
-
body: renderPlanCommandHelp(plan, `Unknown command: ${line}`)
|
|
4035
|
-
};
|
|
4036
|
-
}
|
|
4037
|
-
function parsePlanCommandInputs(plan, invocation) {
|
|
4038
|
-
const matchingCommand = plan.commands.find((candidate) => candidate.task.name === invocation.taskName && candidate.pattern === invocation.pattern) ?? plan.commands.find((candidate) => candidate.task.name === invocation.taskName);
|
|
4039
|
-
if (!matchingCommand) return {
|
|
4040
|
-
kind: "invalid",
|
|
4041
|
-
reason: `No command registered for task '${invocation.taskName}'`,
|
|
4042
|
-
requiredPermission: invocation.requiredPermission,
|
|
4043
|
-
body: renderPlanCommandHelp(plan)
|
|
4044
|
-
};
|
|
4045
|
-
try {
|
|
4046
|
-
return {
|
|
4047
|
-
kind: "matched",
|
|
4048
|
-
invocation: {
|
|
4049
|
-
...invocation,
|
|
4050
|
-
inputs: matchingCommand.parse ? matchingCommand.parse(invocation.arguments) : invocation.arguments
|
|
4051
|
-
}
|
|
4052
|
-
};
|
|
4053
|
-
} catch (error) {
|
|
4054
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4055
|
-
return {
|
|
4056
|
-
kind: "invalid",
|
|
4057
|
-
reason,
|
|
4058
|
-
requiredPermission: invocation.requiredPermission,
|
|
4059
|
-
body: renderPlanCommandHelp(plan, reason)
|
|
4060
|
-
};
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
function hasRequiredRepositoryPermission(actual, required) {
|
|
4064
|
-
if (actual === "none") return false;
|
|
4065
|
-
return permissionOrder.indexOf(actual) >= permissionOrder.indexOf(required);
|
|
4066
|
-
}
|
|
4067
|
-
function permissionDeniedHelp(plan, required) {
|
|
4068
|
-
return renderPlanCommandHelp(plan, `Permission denied: requires ${required}.`);
|
|
4069
|
-
}
|
|
4070
|
-
function renderPlanCommandHelp(plan, reason) {
|
|
4071
|
-
const lines = ["# pipr commands", ""];
|
|
4072
|
-
if (reason) lines.push(reason, "");
|
|
4073
|
-
for (const command of plan.commands) lines.push(`- ${command.pattern} (${command.permission})`);
|
|
4074
|
-
return lines.join("\n");
|
|
4075
|
-
}
|
|
4076
|
-
//#endregion
|
|
4077
4115
|
//#region src/review/comment.ts
|
|
4078
|
-
const runtimeVersion =
|
|
4116
|
+
const runtimeVersion = version;
|
|
4079
4117
|
const inlinePublicationItemSchema = z.strictObject({
|
|
4080
4118
|
finding: reviewFindingSchema,
|
|
4081
4119
|
range: commentableRangeSchema,
|
|
@@ -4146,7 +4184,7 @@ function buildPublicationPlan(options) {
|
|
|
4146
4184
|
findings: options.inlineItems.map((item) => item.finding),
|
|
4147
4185
|
reviewedHeadSha: options.metadata.reviewedHeadSha,
|
|
4148
4186
|
selectedTasks: options.metadata.selectedTasks
|
|
4149
|
-
})
|
|
4187
|
+
});
|
|
4150
4188
|
const cappedInlineItems = options.maxInlineComments === void 0 ? options.inlineItems : options.inlineItems.slice(0, options.maxInlineComments);
|
|
4151
4189
|
const metadata = publicationMetadataSchema.parse({
|
|
4152
4190
|
...options.metadata,
|
|
@@ -4227,7 +4265,7 @@ function buildCommentPublishingPlan(options) {
|
|
|
4227
4265
|
findings: options.validated.validFindings,
|
|
4228
4266
|
reviewedHeadSha: options.event.change.head.sha,
|
|
4229
4267
|
selectedTasks: options.metadata.selectedTasks
|
|
4230
|
-
})
|
|
4268
|
+
});
|
|
4231
4269
|
const inlineCommentDrafts = prepareInlinePublicationItems({
|
|
4232
4270
|
validated: options.validated,
|
|
4233
4271
|
manifest: options.manifest,
|
|
@@ -4249,6 +4287,30 @@ function buildCommentPublishingPlan(options) {
|
|
|
4249
4287
|
};
|
|
4250
4288
|
}
|
|
4251
4289
|
//#endregion
|
|
4290
|
+
//#region src/review/run-identity.ts
|
|
4291
|
+
function stableReviewRunId(options) {
|
|
4292
|
+
return `pipr-${new Bun.CryptoHasher("sha256").update(JSON.stringify({
|
|
4293
|
+
platform: options.event.platform.id,
|
|
4294
|
+
repository: options.event.repository.slug,
|
|
4295
|
+
changeNumber: options.event.change.number,
|
|
4296
|
+
baseSha: options.event.change.base.sha,
|
|
4297
|
+
headSha: options.event.change.head.sha,
|
|
4298
|
+
trustedConfigHash: options.trustedConfigHash,
|
|
4299
|
+
trustedConfigSha: options.trustedConfigSha,
|
|
4300
|
+
selectedTasks: options.selectedTasks,
|
|
4301
|
+
command: options.commandInvocation ? {
|
|
4302
|
+
name: options.commandInvocation.name,
|
|
4303
|
+
line: options.commandInvocation.line,
|
|
4304
|
+
arguments: sortedCommandArguments(options.commandInvocation.arguments),
|
|
4305
|
+
sourceCommentId: options.commandInvocation.sourceCommentId
|
|
4306
|
+
} : void 0,
|
|
4307
|
+
verifier: options.verifierInvocation
|
|
4308
|
+
})).digest("hex").slice(0, 24)}`;
|
|
4309
|
+
}
|
|
4310
|
+
function sortedCommandArguments(arguments_) {
|
|
4311
|
+
return Object.fromEntries(Object.entries(arguments_).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
4312
|
+
}
|
|
4313
|
+
//#endregion
|
|
4252
4314
|
//#region src/review/verifier.ts
|
|
4253
4315
|
const verifierFindingSchema = z.strictObject({
|
|
4254
4316
|
id: z.string().min(1),
|
|
@@ -4313,6 +4375,7 @@ async function runInternalVerifier(options) {
|
|
|
4313
4375
|
env: options.env,
|
|
4314
4376
|
piExecutable: options.piExecutable,
|
|
4315
4377
|
piRunner: options.piRunner,
|
|
4378
|
+
runId: options.runId,
|
|
4316
4379
|
log: options.log
|
|
4317
4380
|
}
|
|
4318
4381
|
});
|
|
@@ -4329,6 +4392,7 @@ async function runInternalVerifier(options) {
|
|
|
4329
4392
|
function verifierInput(options, prior, candidates) {
|
|
4330
4393
|
return {
|
|
4331
4394
|
manifest: options.diffManifest,
|
|
4395
|
+
runId: options.runId,
|
|
4332
4396
|
mode: options.mode.kind,
|
|
4333
4397
|
reviewedHeadSha: prior.reviewedHeadSha,
|
|
4334
4398
|
currentHeadSha: options.event.change.head.sha,
|
|
@@ -4469,7 +4533,7 @@ function boundedVerifierText(value) {
|
|
|
4469
4533
|
}
|
|
4470
4534
|
//#endregion
|
|
4471
4535
|
//#region src/review/task/task-output.ts
|
|
4472
|
-
const
|
|
4536
|
+
const agentInlineFindingsOutputSchema = z.custom((value) => z.looseObject({ inlineFindings: z.array(reviewFindingSchema) }).safeParse(value).success);
|
|
4473
4537
|
function createOutputState() {
|
|
4474
4538
|
return {
|
|
4475
4539
|
findings: [],
|
|
@@ -4491,16 +4555,19 @@ function mergeTaskOutputs(results) {
|
|
|
4491
4555
|
}
|
|
4492
4556
|
function mergeCommentContribution(merged, comment) {
|
|
4493
4557
|
if (!comment) return;
|
|
4494
|
-
|
|
4495
|
-
if (merged.commandResponse) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4558
|
+
assertOutputContributionAllowed(merged, "comment", comment.taskName, (existing, next) => `ctx.comment(...) may be called once per selected run; received comments from '${existing}' and '${next}'`);
|
|
4496
4559
|
merged.comment = comment;
|
|
4497
4560
|
}
|
|
4498
4561
|
function mergeCommandResponseContribution(merged, commandResponse) {
|
|
4499
4562
|
if (!commandResponse) return;
|
|
4500
|
-
|
|
4501
|
-
if (merged.comment) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4563
|
+
assertOutputContributionAllowed(merged, "commandResponse", commandResponse.taskName, (existing, next) => `ctx.command.reply(...) may be called once per selected run; received replies from '${existing}' and '${next}'`);
|
|
4502
4564
|
merged.commandResponse = commandResponse;
|
|
4503
4565
|
}
|
|
4566
|
+
function assertOutputContributionAllowed(state, kind, taskName, duplicateMessage) {
|
|
4567
|
+
const existing = kind === "comment" ? state.comment : state.commandResponse;
|
|
4568
|
+
if (existing) throw new Error(duplicateMessage(existing.taskName, taskName));
|
|
4569
|
+
if (kind === "comment" ? state.commandResponse : state.comment) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4570
|
+
}
|
|
4504
4571
|
function createCheckHandle(state) {
|
|
4505
4572
|
return {
|
|
4506
4573
|
pass(summary) {
|
|
@@ -4532,8 +4599,7 @@ function runtimeTaskCheckResult(taskName, check) {
|
|
|
4532
4599
|
};
|
|
4533
4600
|
}
|
|
4534
4601
|
function collectComment(state, value, taskName) {
|
|
4535
|
-
|
|
4536
|
-
if (state.comment) throw new Error(`ctx.comment(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4602
|
+
assertOutputContributionAllowed(state, "comment", taskName, () => `ctx.comment(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4537
4603
|
state.comment = {
|
|
4538
4604
|
taskName,
|
|
4539
4605
|
value
|
|
@@ -4542,8 +4608,7 @@ function collectComment(state, value, taskName) {
|
|
|
4542
4608
|
collectInlineFindings(state, value.inlineFindings);
|
|
4543
4609
|
}
|
|
4544
4610
|
function collectCommandResponse(state, value, taskName) {
|
|
4545
|
-
|
|
4546
|
-
if (state.commandResponse) throw new Error(`ctx.command.reply(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4611
|
+
assertOutputContributionAllowed(state, "commandResponse", taskName, () => `ctx.command.reply(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4547
4612
|
state.commandResponse = {
|
|
4548
4613
|
taskName,
|
|
4549
4614
|
value
|
|
@@ -4576,30 +4641,13 @@ function collectInlineFindings(state, findings) {
|
|
|
4576
4641
|
const arrayScope = state.findingScopes.get(findings);
|
|
4577
4642
|
state.findings.push(...findings.map((finding) => ({
|
|
4578
4643
|
finding,
|
|
4579
|
-
paths:
|
|
4644
|
+
paths: arrayScope
|
|
4580
4645
|
})));
|
|
4581
4646
|
}
|
|
4582
4647
|
function trackResultFindingScope(state, value, paths) {
|
|
4583
|
-
if (!hasInlineFindings(value)) return;
|
|
4584
4648
|
if (!paths) return;
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
if (!isReviewFindingLike(finding)) continue;
|
|
4588
|
-
markFindingScope(finding, paths);
|
|
4589
|
-
}
|
|
4590
|
-
}
|
|
4591
|
-
function markFindingScope(finding, paths) {
|
|
4592
|
-
Object.defineProperty(finding, findingScopeMarker, {
|
|
4593
|
-
value: paths,
|
|
4594
|
-
enumerable: true,
|
|
4595
|
-
configurable: true
|
|
4596
|
-
});
|
|
4597
|
-
}
|
|
4598
|
-
function hasInlineFindings(value) {
|
|
4599
|
-
return typeof value === "object" && value !== null && Array.isArray(value.inlineFindings);
|
|
4600
|
-
}
|
|
4601
|
-
function isReviewFindingLike(value) {
|
|
4602
|
-
return typeof value === "object" && value !== null && typeof value.body === "string" && typeof value.path === "string" && typeof value.rangeId === "string";
|
|
4649
|
+
const parsed = agentInlineFindingsOutputSchema.safeParse(value);
|
|
4650
|
+
if (parsed.success) state.findingScopes.set(parsed.data.inlineFindings, paths);
|
|
4603
4651
|
}
|
|
4604
4652
|
function collectedReview(output) {
|
|
4605
4653
|
return {
|
|
@@ -4652,13 +4700,21 @@ async function runTaskRuntime(options) {
|
|
|
4652
4700
|
selectedTasks,
|
|
4653
4701
|
taskCount: tasks.length
|
|
4654
4702
|
});
|
|
4703
|
+
const runId = stableReviewRunId({
|
|
4704
|
+
event: options.event,
|
|
4705
|
+
selectedTasks,
|
|
4706
|
+
trustedConfigSha: options.trustedConfigSha,
|
|
4707
|
+
trustedConfigHash: options.trustedConfigHash,
|
|
4708
|
+
commandInvocation: options.commandInvocation
|
|
4709
|
+
});
|
|
4655
4710
|
const loadedPriorReviewState = options.priorReviewState ?? await options.loadPriorReviewState?.();
|
|
4656
4711
|
const priorMainComment = options.priorMainComment ?? await options.loadPriorMainComment?.();
|
|
4657
4712
|
const priorReviewState = priorReviewStateForSelectedTasks(loadedPriorReviewState, selectedTasks);
|
|
4658
4713
|
const runtimeOptions = {
|
|
4659
4714
|
...options,
|
|
4660
4715
|
priorReviewState,
|
|
4661
|
-
priorMainComment
|
|
4716
|
+
priorMainComment,
|
|
4717
|
+
runId
|
|
4662
4718
|
};
|
|
4663
4719
|
const manifestCache = /* @__PURE__ */ new Map();
|
|
4664
4720
|
const taskResults = await Promise.all(tasks.map(async (task, taskOrder) => {
|
|
@@ -4731,7 +4787,7 @@ async function runTaskRuntime(options) {
|
|
|
4731
4787
|
});
|
|
4732
4788
|
if (commandResponse) return commandResponse;
|
|
4733
4789
|
assertReviewCommentOutput(output, options.commandInvocation !== void 0);
|
|
4734
|
-
const validated =
|
|
4790
|
+
const validated = validateReviewResult(collectedReview(output), diffManifest, {
|
|
4735
4791
|
expectedHeadSha: options.event.change.head.sha,
|
|
4736
4792
|
pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
|
|
4737
4793
|
});
|
|
@@ -4740,7 +4796,8 @@ async function runTaskRuntime(options) {
|
|
|
4740
4796
|
config,
|
|
4741
4797
|
provider,
|
|
4742
4798
|
diffManifest,
|
|
4743
|
-
priorReviewState
|
|
4799
|
+
priorReviewState,
|
|
4800
|
+
runId
|
|
4744
4801
|
});
|
|
4745
4802
|
const publishing = buildCommentPublishingPlan({
|
|
4746
4803
|
event: options.event,
|
|
@@ -4817,14 +4874,15 @@ async function runSynchronizeVerifier(options) {
|
|
|
4817
4874
|
diffManifest: options.diffManifest,
|
|
4818
4875
|
priorReviewState: options.priorReviewState,
|
|
4819
4876
|
threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
|
|
4820
|
-
mode: { kind: "synchronize" }
|
|
4877
|
+
mode: { kind: "synchronize" },
|
|
4878
|
+
runId: options.runId
|
|
4821
4879
|
});
|
|
4822
4880
|
}
|
|
4823
4881
|
function createTaskContext(options) {
|
|
4824
4882
|
const repositorySlugParts = options.event.repository.slug.split("/");
|
|
4825
4883
|
let taskContext;
|
|
4826
4884
|
taskContext = {
|
|
4827
|
-
run: { id:
|
|
4885
|
+
run: { id: options.runId },
|
|
4828
4886
|
repository: {
|
|
4829
4887
|
root: options.workspace,
|
|
4830
4888
|
owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
|
|
@@ -4877,13 +4935,14 @@ function createTaskContext(options) {
|
|
|
4877
4935
|
runOptions,
|
|
4878
4936
|
runtime: {
|
|
4879
4937
|
...options,
|
|
4880
|
-
taskContext
|
|
4938
|
+
taskContext,
|
|
4939
|
+
runId: options.runId
|
|
4881
4940
|
}
|
|
4882
4941
|
});
|
|
4883
4942
|
options.output.providerModels.push(...result.providerModels);
|
|
4884
4943
|
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
4885
4944
|
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
4886
|
-
return result.value;
|
|
4945
|
+
return agentOutputForTaskContext(agent, result.value);
|
|
4887
4946
|
} },
|
|
4888
4947
|
review: { async prior() {
|
|
4889
4948
|
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
@@ -4896,6 +4955,9 @@ function createTaskContext(options) {
|
|
|
4896
4955
|
};
|
|
4897
4956
|
return taskContext;
|
|
4898
4957
|
}
|
|
4958
|
+
function agentOutputForTaskContext(_agent, value) {
|
|
4959
|
+
return value;
|
|
4960
|
+
}
|
|
4899
4961
|
function resolveTaskSecret(secret, options) {
|
|
4900
4962
|
if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
|
|
4901
4963
|
const value = (options.env ?? process.env)[secret.name];
|
|
@@ -6248,100 +6310,26 @@ function logEventContext(log, event) {
|
|
|
6248
6310
|
platform: event.platform.id,
|
|
6249
6311
|
eventName: event.eventName,
|
|
6250
6312
|
action: event.action,
|
|
6251
|
-
rawAction: event.rawAction,
|
|
6252
|
-
repo: event.repository.slug,
|
|
6253
|
-
change: event.change.number,
|
|
6254
|
-
base: shortSha(event.change.base.sha),
|
|
6255
|
-
head: shortSha(event.change.head.sha),
|
|
6256
|
-
fork: event.change.isFork
|
|
6257
|
-
});
|
|
6258
|
-
}
|
|
6259
|
-
function logTrustedRuntime(log, runtime) {
|
|
6260
|
-
log.notice("trusted config", {
|
|
6261
|
-
source: runtime.settings.source,
|
|
6262
|
-
trustedConfigSha: shortSha(runtime.trustedConfigSha),
|
|
6263
|
-
trustedConfigHash: runtime.trustedConfigHash.slice(0, 12),
|
|
6264
|
-
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6265
|
-
tasks: runtime.plan.tasks.length,
|
|
6266
|
-
commands: runtime.plan.commands.length
|
|
6267
|
-
});
|
|
6268
|
-
}
|
|
6269
|
-
function addProviderSecrets(log, config, env) {
|
|
6270
|
-
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
6271
|
-
}
|
|
6272
|
-
//#endregion
|
|
6273
|
-
//#region src/action/git-project.ts
|
|
6274
|
-
async function loadRuntimeProjectFromGitCommit(options) {
|
|
6275
|
-
const configDir = resolveContainedConfigDir(options);
|
|
6276
|
-
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
6277
|
-
if (files.length === 0) throw new Error(`${configDir.configDir}/config.ts is required at base commit ${options.commitSha}`);
|
|
6278
|
-
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-base-config-"));
|
|
6279
|
-
try {
|
|
6280
|
-
const hash = new Bun.CryptoHasher("sha256");
|
|
6281
|
-
for (const file of files) {
|
|
6282
|
-
assertRegularFile(file, options.commitSha);
|
|
6283
|
-
const relativePath = relativeGitPath(configDir.gitPath, file.path);
|
|
6284
|
-
const targetPath = path.join(tempRoot, configDir.relativeConfigDir, ...relativePath.split("/"));
|
|
6285
|
-
const contents = showFileAtCommit(options.rootDir, options.commitSha, file.path);
|
|
6286
|
-
hash.update(relativePath);
|
|
6287
|
-
hash.update("\0");
|
|
6288
|
-
hash.update(contents);
|
|
6289
|
-
hash.update("\0");
|
|
6290
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
6291
|
-
await Bun.write(targetPath, contents);
|
|
6292
|
-
}
|
|
6293
|
-
return {
|
|
6294
|
-
...await loadRuntimeProject({
|
|
6295
|
-
rootDir: tempRoot,
|
|
6296
|
-
configDir: configDir.relativeConfigDir,
|
|
6297
|
-
env: options.env,
|
|
6298
|
-
requireProviderEnv: false
|
|
6299
|
-
}),
|
|
6300
|
-
trustedConfigSha: options.commitSha,
|
|
6301
|
-
trustedConfigHash: hash.digest("hex")
|
|
6302
|
-
};
|
|
6303
|
-
} finally {
|
|
6304
|
-
await rm(tempRoot, {
|
|
6305
|
-
recursive: true,
|
|
6306
|
-
force: true
|
|
6307
|
-
});
|
|
6308
|
-
}
|
|
6309
|
-
}
|
|
6310
|
-
function listConfigFilesAtCommit(rootDir, commitSha, gitPath) {
|
|
6311
|
-
return runGit$1([
|
|
6312
|
-
"ls-tree",
|
|
6313
|
-
"-r",
|
|
6314
|
-
"-z",
|
|
6315
|
-
commitSha,
|
|
6316
|
-
"--",
|
|
6317
|
-
gitPath
|
|
6318
|
-
], rootDir).split("\0").filter(Boolean).map((entry) => parseGitTreeEntry(entry, commitSha));
|
|
6319
|
-
}
|
|
6320
|
-
function parseGitTreeEntry(entry, commitSha) {
|
|
6321
|
-
const separatorIndex = entry.indexOf(" ");
|
|
6322
|
-
if (separatorIndex === -1) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6323
|
-
const metadata = entry.slice(0, separatorIndex);
|
|
6324
|
-
const filePath = entry.slice(separatorIndex + 1);
|
|
6325
|
-
const [mode = ""] = metadata.split(" ");
|
|
6326
|
-
if (!mode || !filePath) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6327
|
-
return {
|
|
6328
|
-
mode,
|
|
6329
|
-
path: filePath
|
|
6330
|
-
};
|
|
6331
|
-
}
|
|
6332
|
-
function assertRegularFile(file, commitSha) {
|
|
6333
|
-
if (file.mode !== "100644" && file.mode !== "100755") throw new Error(`${file.path}: only regular config files are supported at ${commitSha}`);
|
|
6334
|
-
}
|
|
6335
|
-
function relativeGitPath(root, filePath) {
|
|
6336
|
-
const relative = root === "." ? filePath : path.posix.relative(root, filePath);
|
|
6337
|
-
assertRelativeGitPath(root, filePath, relative);
|
|
6338
|
-
return relative;
|
|
6313
|
+
rawAction: event.rawAction,
|
|
6314
|
+
repo: event.repository.slug,
|
|
6315
|
+
change: event.change.number,
|
|
6316
|
+
base: shortSha(event.change.base.sha),
|
|
6317
|
+
head: shortSha(event.change.head.sha),
|
|
6318
|
+
fork: event.change.isFork
|
|
6319
|
+
});
|
|
6339
6320
|
}
|
|
6340
|
-
function
|
|
6341
|
-
|
|
6321
|
+
function logTrustedRuntime(log, runtime) {
|
|
6322
|
+
log.notice("trusted config", {
|
|
6323
|
+
source: runtime.settings.source,
|
|
6324
|
+
trustedConfigSha: shortSha(runtime.trustedConfigSha),
|
|
6325
|
+
trustedConfigHash: runtime.trustedConfigHash.slice(0, 12),
|
|
6326
|
+
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6327
|
+
tasks: runtime.plan.tasks.length,
|
|
6328
|
+
commands: runtime.plan.commands.length
|
|
6329
|
+
});
|
|
6342
6330
|
}
|
|
6343
|
-
function
|
|
6344
|
-
|
|
6331
|
+
function addProviderSecrets(log, config, env) {
|
|
6332
|
+
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
6345
6333
|
}
|
|
6346
6334
|
//#endregion
|
|
6347
6335
|
//#region src/action/runtime-checks.ts
|
|
@@ -6601,146 +6589,287 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
6601
6589
|
}
|
|
6602
6590
|
}
|
|
6603
6591
|
//#endregion
|
|
6604
|
-
//#region src/action/
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6592
|
+
//#region src/action/git-project.ts
|
|
6593
|
+
async function loadRuntimeProjectFromGitCommit(options) {
|
|
6594
|
+
const configDir = resolveContainedConfigDir(options);
|
|
6595
|
+
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
6596
|
+
if (files.length === 0) throw new Error(`${configDir.configDir}/config.ts is required at base commit ${options.commitSha}`);
|
|
6597
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-base-config-"));
|
|
6598
|
+
try {
|
|
6599
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
6600
|
+
for (const file of files) {
|
|
6601
|
+
assertRegularFile(file, options.commitSha);
|
|
6602
|
+
const relativePath = relativeGitPath(configDir.gitPath, file.path);
|
|
6603
|
+
const targetPath = path.join(tempRoot, configDir.relativeConfigDir, ...relativePath.split("/"));
|
|
6604
|
+
const contents = showFileAtCommit(options.rootDir, options.commitSha, file.path);
|
|
6605
|
+
hash.update(relativePath);
|
|
6606
|
+
hash.update("\0");
|
|
6607
|
+
hash.update(contents);
|
|
6608
|
+
hash.update("\0");
|
|
6609
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
6610
|
+
await Bun.write(targetPath, contents);
|
|
6611
|
+
}
|
|
6612
|
+
return {
|
|
6613
|
+
...await loadRuntimeProject({
|
|
6614
|
+
rootDir: tempRoot,
|
|
6615
|
+
configDir: configDir.relativeConfigDir,
|
|
6616
|
+
env: options.env,
|
|
6617
|
+
requireProviderEnv: false
|
|
6618
|
+
}),
|
|
6619
|
+
trustedConfigSha: options.commitSha,
|
|
6620
|
+
trustedConfigHash: hash.digest("hex")
|
|
6621
|
+
};
|
|
6622
|
+
} finally {
|
|
6623
|
+
await rm(tempRoot, {
|
|
6624
|
+
recursive: true,
|
|
6625
|
+
force: true
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
function listConfigFilesAtCommit(rootDir, commitSha, gitPath) {
|
|
6630
|
+
return runGit$1([
|
|
6631
|
+
"ls-tree",
|
|
6632
|
+
"-r",
|
|
6633
|
+
"-z",
|
|
6634
|
+
commitSha,
|
|
6635
|
+
"--",
|
|
6636
|
+
gitPath
|
|
6637
|
+
], rootDir).split("\0").filter(Boolean).map((entry) => parseGitTreeEntry(entry, commitSha)).filter((file) => !isIgnoredGitConfigPath(gitPath, file.path));
|
|
6638
|
+
}
|
|
6639
|
+
function isIgnoredGitConfigPath(gitPath, filePath) {
|
|
6640
|
+
const relative = gitPath === "." ? filePath : path.posix.relative(gitPath, filePath);
|
|
6641
|
+
return relative === "node_modules" || relative.startsWith("node_modules/");
|
|
6642
|
+
}
|
|
6643
|
+
function parseGitTreeEntry(entry, commitSha) {
|
|
6644
|
+
const separatorIndex = entry.indexOf(" ");
|
|
6645
|
+
if (separatorIndex === -1) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6646
|
+
const metadata = entry.slice(0, separatorIndex);
|
|
6647
|
+
const filePath = entry.slice(separatorIndex + 1);
|
|
6648
|
+
const [mode = ""] = metadata.split(" ");
|
|
6649
|
+
if (!mode || !filePath) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6650
|
+
return {
|
|
6651
|
+
mode,
|
|
6652
|
+
path: filePath
|
|
6653
|
+
};
|
|
6654
|
+
}
|
|
6655
|
+
function assertRegularFile(file, commitSha) {
|
|
6656
|
+
if (file.mode !== "100644" && file.mode !== "100755") throw new Error(`${file.path}: only regular config files are supported at ${commitSha}`);
|
|
6657
|
+
}
|
|
6658
|
+
function relativeGitPath(root, filePath) {
|
|
6659
|
+
const relative = root === "." ? filePath : path.posix.relative(root, filePath);
|
|
6660
|
+
assertRelativeGitPath(root, filePath, relative);
|
|
6661
|
+
return relative;
|
|
6662
|
+
}
|
|
6663
|
+
function assertRelativeGitPath(root, filePath, relative) {
|
|
6664
|
+
if (!relative || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`${filePath}: git path escaped configDir ${root}`);
|
|
6665
|
+
}
|
|
6666
|
+
function showFileAtCommit(rootDir, commitSha, filePath) {
|
|
6667
|
+
return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
|
|
6668
|
+
}
|
|
6669
|
+
//#endregion
|
|
6670
|
+
//#region src/action/trusted-runtime.ts
|
|
6671
|
+
async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
6672
|
+
const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
|
|
6608
6673
|
rootDir: options.rootDir,
|
|
6609
6674
|
configDir: options.configDir,
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6675
|
+
commitSha: event.change.base.sha,
|
|
6676
|
+
env: options.env
|
|
6677
|
+
}));
|
|
6678
|
+
logTrustedRuntime(log, trustedRuntime);
|
|
6679
|
+
return trustedRuntime;
|
|
6680
|
+
}
|
|
6681
|
+
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
6682
|
+
addProviderSecrets(log, config, options.env);
|
|
6683
|
+
assertTrustedActionProviderEnv(options, config);
|
|
6684
|
+
await logPhase(log, "checkout head", async () => {
|
|
6685
|
+
adapter.workspace.ensureHeadCheckout({
|
|
6686
|
+
rootDir: options.rootDir,
|
|
6687
|
+
change: event
|
|
6688
|
+
});
|
|
6614
6689
|
});
|
|
6615
6690
|
}
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6691
|
+
//#endregion
|
|
6692
|
+
//#region src/action/command-entry.ts
|
|
6693
|
+
async function runIssueCommentActionCommand(options, adapter, log) {
|
|
6694
|
+
const prepared = await prepareIssueCommentCommand(options, adapter, log);
|
|
6695
|
+
if (prepared.kind === "ignored") {
|
|
6696
|
+
log.notice("action ignored", { reason: prepared.reason });
|
|
6697
|
+
return prepared;
|
|
6698
|
+
}
|
|
6699
|
+
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
6622
6700
|
}
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6701
|
+
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
6702
|
+
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
6703
|
+
eventPath: options.eventPath,
|
|
6704
|
+
env: options.env ?? process.env,
|
|
6705
|
+
workspace: options.rootDir
|
|
6706
|
+
}));
|
|
6707
|
+
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
6708
|
+
if (runnable.kind === "ignored") return runnable;
|
|
6709
|
+
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
6710
|
+
repository: comment.repository,
|
|
6711
|
+
changeNumber: comment.changeNumber,
|
|
6712
|
+
workspace: comment.workspace,
|
|
6713
|
+
eventName: comment.eventName,
|
|
6714
|
+
action: comment.action,
|
|
6715
|
+
rawAction: comment.rawAction
|
|
6716
|
+
}));
|
|
6717
|
+
const event = parseChangeRequestEventContext({
|
|
6718
|
+
eventName: loaded.eventName ?? comment.eventName,
|
|
6719
|
+
action: loaded.action ?? comment.action,
|
|
6720
|
+
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
6721
|
+
platform: { id: adapter.id },
|
|
6722
|
+
repository: loaded.repository,
|
|
6723
|
+
change: loaded.change,
|
|
6724
|
+
workspace: loaded.workspace ?? comment.workspace
|
|
6725
|
+
});
|
|
6726
|
+
logEventContext(log, event);
|
|
6727
|
+
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
6728
|
+
const resolution = resolvePlanCommand(trustedRuntime.plan, runnable.line);
|
|
6729
|
+
if (resolution.kind === "ignored") return {
|
|
6730
|
+
kind: "ignored",
|
|
6731
|
+
reason: resolution.reason
|
|
6732
|
+
};
|
|
6733
|
+
return {
|
|
6734
|
+
kind: "prepared",
|
|
6735
|
+
comment,
|
|
6736
|
+
line: runnable.line,
|
|
6737
|
+
event,
|
|
6738
|
+
trustedRuntime,
|
|
6739
|
+
resolution
|
|
6740
|
+
};
|
|
6741
|
+
}
|
|
6742
|
+
function runnableIssueCommentCommand(comment, dryRun) {
|
|
6743
|
+
if (!comment.isChangeRequest) return {
|
|
6744
|
+
kind: "ignored",
|
|
6745
|
+
reason: "issue_comment did not target a pull request"
|
|
6746
|
+
};
|
|
6747
|
+
if (comment.action !== "created") return {
|
|
6748
|
+
kind: "ignored",
|
|
6749
|
+
reason: `issue_comment action '${comment.action}' is not supported`
|
|
6750
|
+
};
|
|
6751
|
+
const line = firstNonEmptyLine(comment.body);
|
|
6752
|
+
if (!line || !isPiprCommandLine(line)) return {
|
|
6753
|
+
kind: "ignored",
|
|
6754
|
+
reason: "issue_comment did not target pipr"
|
|
6755
|
+
};
|
|
6756
|
+
return dryRun ? {
|
|
6757
|
+
kind: "ignored",
|
|
6758
|
+
reason: "PIPR_DRY_RUN=1; command dispatch skipped"
|
|
6759
|
+
} : {
|
|
6760
|
+
kind: "runnable",
|
|
6761
|
+
line
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
|
|
6765
|
+
const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
|
|
6766
|
+
const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
|
|
6767
|
+
repository: prepared.comment.repository,
|
|
6768
|
+
actor: prepared.comment.actor
|
|
6769
|
+
}));
|
|
6770
|
+
log.notice("command dispatch", {
|
|
6771
|
+
resolution: prepared.resolution.kind,
|
|
6772
|
+
requiredPermission,
|
|
6773
|
+
actualPermission: permission
|
|
6774
|
+
});
|
|
6775
|
+
if (!hasRequiredRepositoryPermission(permission, requiredPermission)) return {
|
|
6776
|
+
kind: "command-help",
|
|
6777
|
+
event: prepared.event,
|
|
6778
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6779
|
+
body: permissionDeniedHelp(prepared.trustedRuntime.plan, requiredPermission),
|
|
6780
|
+
reason: `permission denied for '${prepared.line}'`
|
|
6781
|
+
};
|
|
6782
|
+
if (prepared.resolution.kind === "help" || prepared.resolution.kind === "invalid") return {
|
|
6783
|
+
kind: "command-help",
|
|
6784
|
+
event: prepared.event,
|
|
6785
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6786
|
+
body: prepared.resolution.body,
|
|
6787
|
+
reason: prepared.resolution.reason
|
|
6788
|
+
};
|
|
6789
|
+
const parsedResolution = parsePlanCommandInputs(prepared.trustedRuntime.plan, prepared.resolution.invocation);
|
|
6790
|
+
if (parsedResolution.kind === "invalid") return {
|
|
6791
|
+
kind: "command-help",
|
|
6792
|
+
event: prepared.event,
|
|
6793
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6794
|
+
body: parsedResolution.body,
|
|
6795
|
+
reason: parsedResolution.reason
|
|
6796
|
+
};
|
|
6797
|
+
if (parsedResolution.kind !== "matched") return {
|
|
6798
|
+
kind: "ignored",
|
|
6799
|
+
reason: "command dispatch did not resolve to a runnable task"
|
|
6800
|
+
};
|
|
6801
|
+
await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
|
|
6802
|
+
const dispatch = dispatchRuntimeEntry({
|
|
6803
|
+
kind: "change-request",
|
|
6804
|
+
plan: prepared.trustedRuntime.plan,
|
|
6805
|
+
event: prepared.event,
|
|
6806
|
+
taskName: parsedResolution.invocation.taskName
|
|
6807
|
+
});
|
|
6808
|
+
return await issueCommentCommandResult({
|
|
6809
|
+
adapter,
|
|
6810
|
+
completed: await runTrustedReviewAndPublish({
|
|
6811
|
+
options,
|
|
6812
|
+
adapter,
|
|
6813
|
+
trustedRuntime: prepared.trustedRuntime,
|
|
6814
|
+
event: prepared.event,
|
|
6815
|
+
taskName: parsedResolution.invocation.taskName,
|
|
6816
|
+
taskInput: parsedResolution.invocation.inputs,
|
|
6817
|
+
selectedTasks: dispatch.kind === "change-request" ? dispatch.tasks : [],
|
|
6818
|
+
commandInvocation: {
|
|
6819
|
+
name: parsedResolution.invocation.commandName,
|
|
6820
|
+
line: parsedResolution.invocation.line,
|
|
6821
|
+
arguments: parsedResolution.invocation.arguments,
|
|
6822
|
+
sourceCommentId: prepared.comment.commentId
|
|
6823
|
+
},
|
|
6824
|
+
log
|
|
6825
|
+
}),
|
|
6826
|
+
event: prepared.event,
|
|
6827
|
+
commandName: parsedResolution.invocation.commandName,
|
|
6828
|
+
sourceCommentId: prepared.comment.commentId,
|
|
6829
|
+
configSource: prepared.trustedRuntime.settings.source
|
|
6628
6830
|
});
|
|
6629
|
-
return inspectRuntimePlan(runtime.plan, runtime.settings.source);
|
|
6630
6831
|
}
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
GITHUB_EVENT_NAME: "pull_request"
|
|
6643
|
-
},
|
|
6644
|
-
workspace: options.rootDir
|
|
6832
|
+
async function issueCommentCommandResult(options) {
|
|
6833
|
+
if (options.completed.kind === "skipped") return {
|
|
6834
|
+
kind: "ignored",
|
|
6835
|
+
reason: options.completed.reason
|
|
6836
|
+
};
|
|
6837
|
+
if (options.completed.kind === "command-response") return await publishCommandResponseActionResult({
|
|
6838
|
+
adapter: options.adapter,
|
|
6839
|
+
completed: options.completed,
|
|
6840
|
+
event: options.event,
|
|
6841
|
+
sourceCommentId: options.sourceCommentId,
|
|
6842
|
+
configSource: options.configSource
|
|
6645
6843
|
});
|
|
6646
6844
|
return {
|
|
6647
|
-
|
|
6648
|
-
event
|
|
6845
|
+
kind: "review",
|
|
6846
|
+
event: options.event,
|
|
6847
|
+
command: options.commandName,
|
|
6848
|
+
configSource: options.configSource,
|
|
6849
|
+
review: options.completed.review,
|
|
6850
|
+
publication: options.completed.publication
|
|
6649
6851
|
};
|
|
6650
6852
|
}
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
configDir: options.configDir,
|
|
6660
|
-
base: options.baseSha.slice(0, 12),
|
|
6661
|
-
head: options.headSha?.slice(0, 12)
|
|
6662
|
-
});
|
|
6663
|
-
const runtime = await loadRuntimeProject({
|
|
6664
|
-
...options,
|
|
6665
|
-
requireProviderEnv: true
|
|
6666
|
-
});
|
|
6667
|
-
log?.notice("local config loaded", {
|
|
6668
|
-
source: runtime.settings.source,
|
|
6669
|
-
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6670
|
-
tasks: runtime.plan.tasks.length,
|
|
6671
|
-
commands: runtime.plan.commands.length
|
|
6672
|
-
});
|
|
6673
|
-
const selectedTasks = selectLocalReviewTasks(runtime.plan);
|
|
6674
|
-
const includeWorkingTree = options.headSha === void 0;
|
|
6675
|
-
const headSha = options.headSha ?? runGit$1(["rev-parse", "HEAD"], options.rootDir).trim();
|
|
6676
|
-
const event = parseChangeRequestEventContext({ ...createLocalChangeRequestEvent({
|
|
6677
|
-
rootDir: options.rootDir,
|
|
6678
|
-
baseSha: options.baseSha,
|
|
6679
|
-
headSha
|
|
6680
|
-
}) });
|
|
6681
|
-
if (log) {
|
|
6682
|
-
logEventContext(log, event);
|
|
6683
|
-
log.notice("local dispatch", {
|
|
6684
|
-
selectedTasks: selectedTasks.map((task) => task.name),
|
|
6685
|
-
skippedLocalTasks: runtime.plan.tasks.filter((task) => task.local === false).map((task) => task.name),
|
|
6686
|
-
diffTarget: includeWorkingTree ? "working-tree" : "head-ref"
|
|
6687
|
-
});
|
|
6688
|
-
}
|
|
6689
|
-
const result = await runTaskRuntime({
|
|
6690
|
-
workspace: options.rootDir,
|
|
6691
|
-
config: runtime.settings.config,
|
|
6692
|
-
event,
|
|
6693
|
-
env: options.env,
|
|
6694
|
-
plan: runtime.plan,
|
|
6695
|
-
selectedTasks,
|
|
6696
|
-
emptyTasksReason: "No change-request tasks are configured for local review",
|
|
6697
|
-
piExecutable: options.piExecutable,
|
|
6698
|
-
diffManifestBuilder: includeWorkingTree ? (diffOptions) => buildDiffManifest({
|
|
6699
|
-
...diffOptions,
|
|
6700
|
-
includeWorkingTree: true
|
|
6701
|
-
}) : void 0,
|
|
6702
|
-
log,
|
|
6703
|
-
taskLog: options.taskLog
|
|
6704
|
-
});
|
|
6705
|
-
if (result.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
6706
|
-
log?.notice("local review complete", {
|
|
6707
|
-
kind: result.kind,
|
|
6708
|
-
taskChecks: result.taskChecks.length,
|
|
6709
|
-
validFindings: result.kind === "review" ? result.validated.validFindings.length : void 0,
|
|
6710
|
-
droppedFindings: result.kind === "review" ? result.validated.droppedFindings.length : void 0,
|
|
6711
|
-
inlineDrafts: result.kind === "review" ? result.inlineCommentDrafts.length : void 0
|
|
6712
|
-
});
|
|
6713
|
-
return result;
|
|
6714
|
-
}
|
|
6715
|
-
/** Runs the GitHub Action workflow for pull request and issue-comment events. */
|
|
6716
|
-
async function runActionCommand(options) {
|
|
6717
|
-
return await runActionCommandWithDependencies(options);
|
|
6718
|
-
}
|
|
6719
|
-
async function runActionCommandWithDependencies(options) {
|
|
6720
|
-
const log = createRuntimeActionLog({
|
|
6721
|
-
logSink: options.logSink,
|
|
6722
|
-
env: options.env
|
|
6723
|
-
});
|
|
6724
|
-
return await log.group("pipr action", async () => {
|
|
6725
|
-
const eventName = (options.env ?? process.env).GITHUB_EVENT_NAME ?? "pull_request";
|
|
6726
|
-
log.notice("action start", {
|
|
6727
|
-
eventName,
|
|
6728
|
-
dryRun: options.dryRun,
|
|
6729
|
-
root: options.rootDir,
|
|
6730
|
-
configDir: options.configDir
|
|
6731
|
-
});
|
|
6732
|
-
const adapter = createActionHostAdapter(options);
|
|
6733
|
-
await logPhase(log, "workspace", async () => {
|
|
6734
|
-
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
6735
|
-
rootDir: options.rootDir,
|
|
6736
|
-
env: options.env
|
|
6737
|
-
});
|
|
6738
|
-
});
|
|
6739
|
-
if (eventName === "issue_comment") return await runIssueCommentActionCommand(options, adapter, log);
|
|
6740
|
-
if (eventName === "pull_request_review_comment") return await runReviewCommentReplyActionCommand(options, adapter, log);
|
|
6741
|
-
return await runPullRequestActionCommand(options, adapter, log);
|
|
6853
|
+
async function publishCommandResponseActionResult(options) {
|
|
6854
|
+
const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
|
|
6855
|
+
if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
|
|
6856
|
+
const publication = await publishCommandResponse({
|
|
6857
|
+
change: options.event,
|
|
6858
|
+
sourceCommentId: options.sourceCommentId,
|
|
6859
|
+
commandName: options.completed.response.commandName,
|
|
6860
|
+
body: options.completed.response.body
|
|
6742
6861
|
});
|
|
6862
|
+
return {
|
|
6863
|
+
kind: "command-response",
|
|
6864
|
+
event: options.event,
|
|
6865
|
+
command: options.completed.response.commandName,
|
|
6866
|
+
configSource: options.configSource,
|
|
6867
|
+
response: { body: options.completed.response.body },
|
|
6868
|
+
publication
|
|
6869
|
+
};
|
|
6743
6870
|
}
|
|
6871
|
+
//#endregion
|
|
6872
|
+
//#region src/action/pull-request-entry.ts
|
|
6744
6873
|
async function runPullRequestActionCommand(options, adapter, log) {
|
|
6745
6874
|
const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
|
|
6746
6875
|
eventPath: options.eventPath,
|
|
@@ -6789,34 +6918,8 @@ async function runPullRequestActionCommand(options, adapter, log) {
|
|
|
6789
6918
|
publication: completed.publication
|
|
6790
6919
|
};
|
|
6791
6920
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
rootDir: options.rootDir,
|
|
6795
|
-
configDir: options.configDir,
|
|
6796
|
-
commitSha: event.change.base.sha,
|
|
6797
|
-
env: options.env
|
|
6798
|
-
}));
|
|
6799
|
-
logTrustedRuntime(log, trustedRuntime);
|
|
6800
|
-
return trustedRuntime;
|
|
6801
|
-
}
|
|
6802
|
-
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
6803
|
-
addProviderSecrets(log, config, options.env);
|
|
6804
|
-
assertTrustedActionProviderEnv(options, config);
|
|
6805
|
-
await logPhase(log, "checkout head", async () => {
|
|
6806
|
-
adapter.workspace.ensureHeadCheckout({
|
|
6807
|
-
rootDir: options.rootDir,
|
|
6808
|
-
change: event
|
|
6809
|
-
});
|
|
6810
|
-
});
|
|
6811
|
-
}
|
|
6812
|
-
async function runIssueCommentActionCommand(options, adapter, log) {
|
|
6813
|
-
const prepared = await prepareIssueCommentCommand(options, adapter, log);
|
|
6814
|
-
if (prepared.kind === "ignored") {
|
|
6815
|
-
log.notice("action ignored", { reason: prepared.reason });
|
|
6816
|
-
return prepared;
|
|
6817
|
-
}
|
|
6818
|
-
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
6819
|
-
}
|
|
6921
|
+
//#endregion
|
|
6922
|
+
//#region src/action/verifier-entry.ts
|
|
6820
6923
|
async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
6821
6924
|
const capabilities = reviewCommentReplyDispatchCapabilities(options, adapter);
|
|
6822
6925
|
if (capabilities.kind === "ignored") {
|
|
@@ -6960,207 +7063,190 @@ async function runReviewCommentVerifier(options, adapter, prepared, log) {
|
|
|
6960
7063
|
actor: reply.actor
|
|
6961
7064
|
},
|
|
6962
7065
|
respondWhenStillValid: config.publication.autoResolve.userReplies.respondWhenStillValid
|
|
6963
|
-
}
|
|
7066
|
+
},
|
|
7067
|
+
runId: stableReviewRunId({
|
|
7068
|
+
event,
|
|
7069
|
+
selectedTasks: ["pipr-internal-verifier"],
|
|
7070
|
+
trustedConfigSha: trustedRuntime.trustedConfigSha,
|
|
7071
|
+
trustedConfigHash: trustedRuntime.trustedConfigHash,
|
|
7072
|
+
verifierInvocation: {
|
|
7073
|
+
mode: "user-reply",
|
|
7074
|
+
commentId: reply.commentId,
|
|
7075
|
+
parentCommentId: reply.parentCommentId
|
|
7076
|
+
}
|
|
7077
|
+
})
|
|
6964
7078
|
});
|
|
6965
7079
|
}
|
|
6966
7080
|
function runnableReviewCommentReply(reply) {
|
|
6967
|
-
if (reply.action !== "created") return {
|
|
6968
|
-
kind: "ignored",
|
|
6969
|
-
reason: `review comment action '${reply.action}' is not supported`
|
|
6970
|
-
};
|
|
6971
|
-
if (!reply.parentCommentId) return {
|
|
6972
|
-
kind: "ignored",
|
|
6973
|
-
reason: "review comment was not a reply"
|
|
6974
|
-
};
|
|
6975
|
-
if (reply.actor === "github-actions[bot]") return {
|
|
6976
|
-
kind: "ignored",
|
|
6977
|
-
reason: "review comment reply was authored by pipr"
|
|
6978
|
-
};
|
|
6979
|
-
if (isPiprThreadActionReplyBody(reply.body)) return {
|
|
6980
|
-
kind: "ignored",
|
|
6981
|
-
reason: "review comment reply was authored by pipr"
|
|
6982
|
-
};
|
|
6983
|
-
return { kind: "runnable" };
|
|
6984
|
-
}
|
|
6985
|
-
async function verifierActorAllowed(adapter, event, reply, config) {
|
|
6986
|
-
const allowed = config.publication.autoResolve.userReplies.allowedActors;
|
|
6987
|
-
if (allowed === "any") return true;
|
|
6988
|
-
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
6989
|
-
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
6990
|
-
repository: event.repository,
|
|
6991
|
-
actor: reply.actor
|
|
6992
|
-
}), "write");
|
|
6993
|
-
}
|
|
6994
|
-
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
6995
|
-
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
6996
|
-
eventPath: options.eventPath,
|
|
6997
|
-
env: options.env ?? process.env,
|
|
6998
|
-
workspace: options.rootDir
|
|
6999
|
-
}));
|
|
7000
|
-
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
7001
|
-
if (runnable.kind === "ignored") return runnable;
|
|
7002
|
-
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
7003
|
-
repository: comment.repository,
|
|
7004
|
-
changeNumber: comment.changeNumber,
|
|
7005
|
-
workspace: comment.workspace,
|
|
7006
|
-
eventName: comment.eventName,
|
|
7007
|
-
action: comment.action,
|
|
7008
|
-
rawAction: comment.rawAction
|
|
7009
|
-
}));
|
|
7010
|
-
const event = parseChangeRequestEventContext({
|
|
7011
|
-
eventName: loaded.eventName ?? comment.eventName,
|
|
7012
|
-
action: loaded.action ?? comment.action,
|
|
7013
|
-
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
7014
|
-
platform: { id: adapter.id },
|
|
7015
|
-
repository: loaded.repository,
|
|
7016
|
-
change: loaded.change,
|
|
7017
|
-
workspace: loaded.workspace ?? comment.workspace
|
|
7018
|
-
});
|
|
7019
|
-
logEventContext(log, event);
|
|
7020
|
-
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
7021
|
-
const resolution = resolvePlanCommand(trustedRuntime.plan, runnable.line);
|
|
7022
|
-
if (resolution.kind === "ignored") return {
|
|
7023
|
-
kind: "ignored",
|
|
7024
|
-
reason: resolution.reason
|
|
7025
|
-
};
|
|
7026
|
-
return {
|
|
7027
|
-
kind: "prepared",
|
|
7028
|
-
comment,
|
|
7029
|
-
line: runnable.line,
|
|
7030
|
-
event,
|
|
7031
|
-
trustedRuntime,
|
|
7032
|
-
resolution
|
|
7033
|
-
};
|
|
7034
|
-
}
|
|
7035
|
-
function runnableIssueCommentCommand(comment, dryRun) {
|
|
7036
|
-
if (!comment.isChangeRequest) return {
|
|
7081
|
+
if (reply.action !== "created") return {
|
|
7037
7082
|
kind: "ignored",
|
|
7038
|
-
reason:
|
|
7083
|
+
reason: `review comment action '${reply.action}' is not supported`
|
|
7039
7084
|
};
|
|
7040
|
-
if (
|
|
7085
|
+
if (!reply.parentCommentId) return {
|
|
7041
7086
|
kind: "ignored",
|
|
7042
|
-
reason:
|
|
7087
|
+
reason: "review comment was not a reply"
|
|
7043
7088
|
};
|
|
7044
|
-
|
|
7045
|
-
if (!line || !isPiprCommandLine(line)) return {
|
|
7089
|
+
if (reply.actor === "github-actions[bot]") return {
|
|
7046
7090
|
kind: "ignored",
|
|
7047
|
-
reason: "
|
|
7091
|
+
reason: "review comment reply was authored by pipr"
|
|
7048
7092
|
};
|
|
7049
|
-
|
|
7093
|
+
if (isPiprThreadActionReplyBody(reply.body)) return {
|
|
7050
7094
|
kind: "ignored",
|
|
7051
|
-
reason: "
|
|
7052
|
-
} : {
|
|
7053
|
-
kind: "runnable",
|
|
7054
|
-
line
|
|
7095
|
+
reason: "review comment reply was authored by pipr"
|
|
7055
7096
|
};
|
|
7097
|
+
return { kind: "runnable" };
|
|
7056
7098
|
}
|
|
7057
|
-
async function
|
|
7058
|
-
const
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
event: prepared.event,
|
|
7078
|
-
configSource: prepared.trustedRuntime.settings.source,
|
|
7079
|
-
body: prepared.resolution.body,
|
|
7080
|
-
reason: prepared.resolution.reason
|
|
7081
|
-
};
|
|
7082
|
-
const parsedResolution = parsePlanCommandInputs(prepared.trustedRuntime.plan, prepared.resolution.invocation);
|
|
7083
|
-
if (parsedResolution.kind === "invalid") return {
|
|
7084
|
-
kind: "command-help",
|
|
7085
|
-
event: prepared.event,
|
|
7086
|
-
configSource: prepared.trustedRuntime.settings.source,
|
|
7087
|
-
body: parsedResolution.body,
|
|
7088
|
-
reason: parsedResolution.reason
|
|
7089
|
-
};
|
|
7090
|
-
if (parsedResolution.kind !== "matched") return {
|
|
7091
|
-
kind: "ignored",
|
|
7092
|
-
reason: "command dispatch did not resolve to a runnable task"
|
|
7093
|
-
};
|
|
7094
|
-
await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
|
|
7095
|
-
const dispatch = dispatchRuntimeEntry({
|
|
7096
|
-
kind: "change-request",
|
|
7097
|
-
plan: prepared.trustedRuntime.plan,
|
|
7098
|
-
event: prepared.event,
|
|
7099
|
-
taskName: parsedResolution.invocation.taskName
|
|
7099
|
+
async function verifierActorAllowed(adapter, event, reply, config) {
|
|
7100
|
+
const allowed = config.publication.autoResolve.userReplies.allowedActors;
|
|
7101
|
+
if (allowed === "any") return true;
|
|
7102
|
+
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
7103
|
+
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
7104
|
+
repository: event.repository,
|
|
7105
|
+
actor: reply.actor
|
|
7106
|
+
}), "write");
|
|
7107
|
+
}
|
|
7108
|
+
//#endregion
|
|
7109
|
+
//#region src/action/commands.ts
|
|
7110
|
+
/** Initializes the official minimal `.pipr` project files. */
|
|
7111
|
+
async function runInitCommand(options) {
|
|
7112
|
+
return await initOfficialMinimalProject({
|
|
7113
|
+
rootDir: options.rootDir,
|
|
7114
|
+
configDir: options.configDir,
|
|
7115
|
+
force: options.force,
|
|
7116
|
+
adapters: options.adapters,
|
|
7117
|
+
recipe: options.recipe,
|
|
7118
|
+
minimal: options.minimal
|
|
7100
7119
|
});
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
arguments: parsedResolution.invocation.arguments
|
|
7115
|
-
},
|
|
7116
|
-
log
|
|
7117
|
-
}),
|
|
7118
|
-
event: prepared.event,
|
|
7119
|
-
commandName: parsedResolution.invocation.commandName,
|
|
7120
|
-
sourceCommentId: prepared.comment.commentId,
|
|
7121
|
-
configSource: prepared.trustedRuntime.settings.source
|
|
7120
|
+
}
|
|
7121
|
+
/** Loads and validates the runtime project configuration. */
|
|
7122
|
+
async function runValidateCommand(options) {
|
|
7123
|
+
return (await validateProject({
|
|
7124
|
+
...options,
|
|
7125
|
+
requireProviderEnv: options.requireProviderEnv ?? false
|
|
7126
|
+
})).settings;
|
|
7127
|
+
}
|
|
7128
|
+
/** Returns an inspectable summary of the configured runtime plan. */
|
|
7129
|
+
async function runInspectCommand(options) {
|
|
7130
|
+
const runtime = await loadRuntimeProject({
|
|
7131
|
+
...options,
|
|
7132
|
+
requireProviderEnv: false
|
|
7122
7133
|
});
|
|
7134
|
+
return inspectRuntimePlan(runtime.plan, runtime.settings.source);
|
|
7123
7135
|
}
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7136
|
+
/** Loads the runtime config and pull request event without running review publication. */
|
|
7137
|
+
async function runDryRunCommand(options) {
|
|
7138
|
+
const runtime = await loadRuntimeProject({
|
|
7139
|
+
...options,
|
|
7140
|
+
requireProviderEnv: false
|
|
7141
|
+
});
|
|
7142
|
+
const event = await createActionHostAdapter(options).events.parseEvent({
|
|
7143
|
+
eventPath: options.eventPath,
|
|
7144
|
+
env: {
|
|
7145
|
+
...options.env,
|
|
7146
|
+
GITHUB_WORKSPACE: options.rootDir,
|
|
7147
|
+
GITHUB_EVENT_NAME: "pull_request"
|
|
7148
|
+
},
|
|
7149
|
+
workspace: options.rootDir
|
|
7135
7150
|
});
|
|
7136
7151
|
return {
|
|
7137
|
-
|
|
7138
|
-
event
|
|
7139
|
-
command: options.commandName,
|
|
7140
|
-
configSource: options.configSource,
|
|
7141
|
-
review: options.completed.review,
|
|
7142
|
-
publication: options.completed.publication
|
|
7152
|
+
configSource: runtime.settings.source,
|
|
7153
|
+
event
|
|
7143
7154
|
};
|
|
7144
7155
|
}
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7156
|
+
/** Runs configured change-request tasks against local Git base and head revisions. */
|
|
7157
|
+
async function runLocalReviewCommand(options) {
|
|
7158
|
+
const log = options.logSink ? createRuntimeActionLog({
|
|
7159
|
+
logSink: options.logSink,
|
|
7160
|
+
env: options.env
|
|
7161
|
+
}) : void 0;
|
|
7162
|
+
log?.notice("local review start", {
|
|
7163
|
+
root: options.rootDir,
|
|
7164
|
+
configDir: options.configDir,
|
|
7165
|
+
base: options.baseSha.slice(0, 12),
|
|
7166
|
+
head: options.headSha?.slice(0, 12)
|
|
7167
|
+
});
|
|
7168
|
+
const runtime = await loadRuntimeProject({
|
|
7169
|
+
...options,
|
|
7170
|
+
requireProviderEnv: true
|
|
7171
|
+
});
|
|
7172
|
+
log?.notice("local config loaded", {
|
|
7173
|
+
source: runtime.settings.source,
|
|
7174
|
+
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
7175
|
+
tasks: runtime.plan.tasks.length,
|
|
7176
|
+
commands: runtime.plan.commands.length
|
|
7177
|
+
});
|
|
7178
|
+
const selectedTasks = selectLocalReviewTasks(runtime.plan);
|
|
7179
|
+
const includeWorkingTree = options.headSha === void 0;
|
|
7180
|
+
const headSha = options.headSha ?? runGit$1(["rev-parse", "HEAD"], options.rootDir).trim();
|
|
7181
|
+
const event = parseChangeRequestEventContext({ ...createLocalChangeRequestEvent({
|
|
7182
|
+
rootDir: options.rootDir,
|
|
7183
|
+
baseSha: options.baseSha,
|
|
7184
|
+
headSha
|
|
7185
|
+
}) });
|
|
7186
|
+
if (log) {
|
|
7187
|
+
logEventContext(log, event);
|
|
7188
|
+
log.notice("local dispatch", {
|
|
7189
|
+
selectedTasks: selectedTasks.map((task) => task.name),
|
|
7190
|
+
skippedLocalTasks: runtime.plan.tasks.filter((task) => task.local === false).map((task) => task.name),
|
|
7191
|
+
diffTarget: includeWorkingTree ? "working-tree" : "head-ref"
|
|
7192
|
+
});
|
|
7193
|
+
}
|
|
7194
|
+
const result = await runTaskRuntime({
|
|
7195
|
+
workspace: options.rootDir,
|
|
7196
|
+
config: runtime.settings.config,
|
|
7197
|
+
event,
|
|
7198
|
+
env: options.env,
|
|
7199
|
+
plan: runtime.plan,
|
|
7200
|
+
selectedTasks,
|
|
7201
|
+
emptyTasksReason: "No change-request tasks are configured for local review",
|
|
7202
|
+
piExecutable: options.piExecutable,
|
|
7203
|
+
diffManifestBuilder: includeWorkingTree ? (diffOptions) => buildDiffManifest({
|
|
7204
|
+
...diffOptions,
|
|
7205
|
+
includeWorkingTree: true
|
|
7206
|
+
}) : void 0,
|
|
7207
|
+
log,
|
|
7208
|
+
taskLog: options.taskLog
|
|
7209
|
+
});
|
|
7210
|
+
if (result.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
7211
|
+
log?.notice("local review complete", {
|
|
7212
|
+
kind: result.kind,
|
|
7213
|
+
taskChecks: result.taskChecks.length,
|
|
7214
|
+
validFindings: result.kind === "review" ? result.validated.validFindings.length : void 0,
|
|
7215
|
+
droppedFindings: result.kind === "review" ? result.validated.droppedFindings.length : void 0,
|
|
7216
|
+
inlineDrafts: result.kind === "review" ? result.inlineCommentDrafts.length : void 0
|
|
7217
|
+
});
|
|
7218
|
+
return result;
|
|
7219
|
+
}
|
|
7220
|
+
/** Runs the GitHub Action workflow for pull request and issue-comment events. */
|
|
7221
|
+
async function runActionCommand(options) {
|
|
7222
|
+
return await runActionCommandWithDependencies(options);
|
|
7223
|
+
}
|
|
7224
|
+
async function runActionCommandWithDependencies(options) {
|
|
7225
|
+
const log = createRuntimeActionLog({
|
|
7226
|
+
logSink: options.logSink,
|
|
7227
|
+
env: options.env
|
|
7228
|
+
});
|
|
7229
|
+
return await log.group("pipr action", async () => {
|
|
7230
|
+
const eventName = (options.env ?? process.env).GITHUB_EVENT_NAME ?? "pull_request";
|
|
7231
|
+
log.notice("action start", {
|
|
7232
|
+
eventName,
|
|
7233
|
+
dryRun: options.dryRun,
|
|
7234
|
+
root: options.rootDir,
|
|
7235
|
+
configDir: options.configDir
|
|
7236
|
+
});
|
|
7237
|
+
const adapter = createActionHostAdapter(options);
|
|
7238
|
+
await logPhase(log, "workspace", async () => {
|
|
7239
|
+
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
7240
|
+
rootDir: options.rootDir,
|
|
7241
|
+
env: options.env
|
|
7242
|
+
});
|
|
7243
|
+
});
|
|
7244
|
+
if (eventName === "issue_comment") return await runIssueCommentActionCommand(options, adapter, log);
|
|
7245
|
+
if (eventName === "pull_request_review_comment") return await runReviewCommentReplyActionCommand(options, adapter, log);
|
|
7246
|
+
return await runPullRequestActionCommand(options, adapter, log);
|
|
7153
7247
|
});
|
|
7154
|
-
return {
|
|
7155
|
-
kind: "command-response",
|
|
7156
|
-
event: options.event,
|
|
7157
|
-
command: options.completed.response.commandName,
|
|
7158
|
-
configSource: options.configSource,
|
|
7159
|
-
response: { body: options.completed.response.body },
|
|
7160
|
-
publication
|
|
7161
|
-
};
|
|
7162
7248
|
}
|
|
7163
7249
|
//#endregion
|
|
7164
|
-
export { runInspectCommand as a, PublicationError as c, supportedOfficialInitRecipes as d, runInitCommand as i, supportedOfficialInitAdapters as l, runActionCommandWithDependencies as n, runLocalReviewCommand as o, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, listOfficialInitRecipes as u };
|
|
7250
|
+
export { runInspectCommand as a, PublicationError as c, supportedOfficialInitRecipes as d, piBuiltinToolNames as f, piThinkingLevels as h, runInitCommand as i, supportedOfficialInitAdapters as l, piRequiredCliFlags as m, runActionCommandWithDependencies as n, runLocalReviewCommand as o, piReadOnlyToolNames as p, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, listOfficialInitRecipes as u };
|
|
7165
7251
|
|
|
7166
|
-
//# sourceMappingURL=commands-
|
|
7252
|
+
//# sourceMappingURL=commands-Cj8p5IQF.mjs.map
|