@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.
@@ -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 { buildPiprPlan, embeddedSdkDeclaration, isBuiltinReadOnlyTool, isPiprConfigFactory, readSdkDeclarationSourceWithChunk, renderPromptValue, reviewOutputSchemaId } from "@usepipr/sdk/internal";
8
- import { parseReviewResult, reviewFindingSchema, reviewResultSchema, reviewSchemaExample } from "@usepipr/sdk/review";
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/commands/grammar.ts
19
- const piprCommandPrefix = "@pipr";
20
- function firstNonEmptyLine(value) {
21
- return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
22
- }
23
- function isPiprCommandLine(line) {
24
- return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
25
- }
26
- function parseCommandPattern(pattern, line) {
27
- const patternParts = patternPartsFor(pattern);
28
- const validationError = unsupportedRestCaptureError(patternParts);
29
- if (validationError) return {
30
- ok: false,
31
- error: validationError
32
- };
33
- const lineTokens = tokenize(line);
34
- const captures = {};
35
- let index = 0;
36
- for (const part of patternParts) {
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 patternPartsFor(pattern) {
106
- return pattern.match(/\[[^\]]+\]|[^\s]+/g) ?? [];
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 unsupportedRestCaptureError(parts) {
109
- for (const [index, part] of parts.entries()) {
110
- if (isOptionalPart(part)) {
111
- const optionalRest = tokenize(part.slice(1, -1)).find(isRestCaptureToken);
112
- if (optionalRest) return finalRequiredRestCaptureError(optionalRest);
113
- continue;
114
- }
115
- if (isRestCaptureToken(part) && index !== parts.length - 1) return finalRequiredRestCaptureError(part);
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 finalRequiredRestCaptureError(token) {
119
- return `Rest capture '${token}' must be the final required command pattern token`;
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/review/contract.ts
157
- const prReviewSchemaId = reviewOutputSchemaId;
158
- const prReviewSchema = reviewResultSchema;
159
- z.toJSONSchema(prReviewSchema);
160
- function parsePrReview(value) {
161
- return prReviewSchema.parse(parseReviewResult(value));
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 parsePrReview(reviewSchemaExample());
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: prReviewSchema,
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/type-support.ts
372
- const starterTsconfig = `{
373
- "compilerOptions": {
374
- "strict": true,
375
- "noEmit": true,
376
- "target": "ES2022",
377
- "module": "ESNext",
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
- async function generatedTypeSupportFiles(relativeConfigDir, options = {}) {
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
- async function writeGeneratedTypeSupport(configDir, options = {}) {
396
- for (const file of await generatedTypeSupportFiles("", options)) {
397
- const target = path.join(configDir, file.relativePath);
398
- await mkdir(path.dirname(target), { recursive: true });
399
- await Bun.write(target, file.contents);
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 sdkDeclaration() {
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 pipr init.");
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 moduleDir = path.dirname(fileURLToPath(import.meta.url));
447
- const candidates = [path.resolve(moduleDir, "../../sdk/dist", fileName), path.resolve(moduleDir, "../../../sdk/dist", fileName)];
448
- for (const candidate of candidates) if (await Bun.file(candidate).exists()) return candidate;
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 cp(projectDir, tempConfigDir, {
461
- recursive: true,
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 installSdkStub(configDir) {
482
- const sdkRoot = path.join(configDir, "node_modules", "@usepipr", "sdk");
483
- await mkdir(sdkRoot, { recursive: true });
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" && relative !== configTypesPath && !relative.startsWith(`${configTypesPath}${path.sep}`);
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
- await writeGeneratedTypeSupport(tempConfigDir, { tsconfig: !await fileExists(tsconfigPath) });
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 fileNames = [path.join(configDir, "config.ts"), ...parsed.fileNames.filter((fileName) => {
534
- const relative = path.relative(typeRoot, fileName);
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
- typeRoots: [...new Set([
540
- typeRoot,
541
- ...bundledTypeRoots,
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 sdkStubSource() {
561
- const sourcePath = await sdkSourcePath();
562
- if (sourcePath) return `export * from ${JSON.stringify(pathToFileURL(sourcePath).href)};\n`;
563
- const embedded = embeddedSdkAssets().module;
564
- if (embedded) return embedded;
565
- throw new Error("Unable to locate @usepipr/sdk runtime module");
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}`) || relative === "bun.lock";
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
- inlineComments: { max: 5 },
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.3";
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 typeSupport = options.typeSupport ?? "include";
1887
- const targets = (typeSupport === "only" ? await generatedTypeSupportFiles(relativeConfigDir) : await starterFiles(relativeConfigDir, adapters, options.recipe, typeSupport !== "skip")).map((file) => ({
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 (typeSupport !== "only") await loadRuntimeProject({
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, includeTypeSupport = true) {
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 (includeTypeSupport) files.push(...await generatedTypeSupportFiles(relativeConfigDir));
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/pi/runtime-tools.ts
2390
- const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
2391
- async function preparePiRuntimeReadTools(options) {
2392
- const toolRoot = path.join(options.root, "runtime-tools");
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
- async function piRuntimeToolsExtensionPath() {
2415
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
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
- async function pathExists(filePath) {
2426
- return await Bun.file(filePath).exists();
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
- writer.level = "info";
2872
- logger.info(compactFields(fields, secrets), redact(event, secrets));
3247
+ emitRecord(sink, secrets, "info", event, fields);
2873
3248
  },
2874
3249
  notice(event, fields) {
2875
- writer.level = "notice";
2876
- logger.notice(compactFields(fields, secrets), redact(event, secrets));
3250
+ emitRecord(sink, secrets, "notice", event, fields);
2877
3251
  },
2878
3252
  warning(event, fields) {
2879
- writer.level = "warning";
2880
- logger.warn(compactFields(fields, secrets), redact(event, secrets));
3253
+ emitRecord(sink, secrets, "warning", event, fields);
2881
3254
  },
2882
3255
  error(event, fields) {
2883
- writer.level = "error";
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
- const message = `${structuredLine(logger, writer, level, redact(event, secrets))}\n${redact(text, secrets)}`;
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
- const message = `${structuredLine(logger, writer, level, redact(event, secrets))}\n${formatTextSnippet(text, secrets, snippetOptions)}`;
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 structuredLogWriter(sink, secrets) {
2945
- const writer = {
2946
- level: "info",
2947
- stream: { write(line) {
2948
- sinkForLevel(sink, writer.level)(redact(line.trimEnd(), secrets));
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
- info() {},
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 validatePrReview(review, manifest, options) {
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.definition.prompt(options.input, { ...options.agentRunContext.prompt });
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 === prReviewSchemaId ? `Example:\n${JSON.stringify(reviewSchemaExample$1(), null, 2)}` : void 0,
3146
- schema.id === prReviewSchemaId ? "`suggestedFix` is exact replacement code for the selected range. Do not include Markdown fences, prose, or labels in `suggestedFix`." : void 0,
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 === prReviewSchemaId ? "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
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 = crypto.randomUUID();
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 === prReviewSchemaId) return {
3817
+ if (agent.definition.output.id === reviewResultSchemaId) return {
3656
3818
  ok: true,
3657
- value: parsePrReview(json),
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
- nextFindings.set(id, priorFindingRecordSchema.parse({
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?.lastCommentedHeadSha
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, priorFindingRecordSchema.parse(prior));
3916
+ nextFindings.set(prior.id, prior);
3751
3917
  }
3752
- return { state: priorReviewStateSchema.parse({
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 priorReviewStateSchema.parse({
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.flatMap((body) => [parseFindingHeadMarker(firstNonEmptyLine(body), inlineFindingMarkerPrefix)].filter((marker) => marker !== void 0));
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.flatMap((body) => [parseFindingHeadMarker(firstNonEmptyLine(body), resolvedFindingMarkerPrefix)].filter((marker) => marker !== void 0));
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 priorReviewStateSchema.parse({
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.flatMap((body) => [parseFindingHeadMarker(firstNonEmptyLine(body), verifierResponseMarkerPrefix)].filter((marker) => marker !== void 0)).map((record) => record.marker));
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 priorReviewStateSchema.parse({
4018
+ return {
3853
4019
  ...state,
3854
- findings: state.findings.map((finding) => ({
3855
- ...finding,
3856
- lastCommentedHeadSha: markerById.get(finding.id)
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(priorReviewStateSchema.parse(state))).toString("base64url");
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 = "0.1.3";
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
- }).state;
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
- }).state;
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 findingScopeMarker = Symbol("pipr.findingScope");
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
- if (merged.comment) throw new Error(`ctx.comment(...) may be called once per selected run; received comments from '${merged.comment.taskName}' and '${comment.taskName}'`);
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
- if (merged.commandResponse) throw new Error(`ctx.command.reply(...) may be called once per selected run; received replies from '${merged.commandResponse.taskName}' and '${commandResponse.taskName}'`);
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
- if (state.commandResponse) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
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
- if (state.comment) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
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: finding[findingScopeMarker] ?? arrayScope
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
- state.findingScopes.set(value.inlineFindings, paths);
4586
- for (const finding of value.inlineFindings) {
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 = validatePrReview(collectedReview(output), diffManifest, {
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: crypto.randomUUID() },
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 assertRelativeGitPath(root, filePath, relative) {
6341
- if (!relative || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`${filePath}: git path escaped configDir ${root}`);
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 showFileAtCommit(rootDir, commitSha, filePath) {
6344
- return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
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/commands.ts
6605
- /** Initializes the official minimal `.pipr` project files. */
6606
- async function runInitCommand(options) {
6607
- return await initOfficialMinimalProject({
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
- force: options.force,
6611
- adapters: options.adapters,
6612
- recipe: options.recipe,
6613
- typeSupport: options.typeSupport
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
- /** Loads and validates the runtime project configuration. */
6617
- async function runValidateCommand(options) {
6618
- return (await validateProject({
6619
- ...options,
6620
- requireProviderEnv: options.requireProviderEnv ?? false
6621
- })).settings;
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
- /** Returns an inspectable summary of the configured runtime plan. */
6624
- async function runInspectCommand(options) {
6625
- const runtime = await loadRuntimeProject({
6626
- ...options,
6627
- requireProviderEnv: false
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
- /** Loads the runtime config and pull request event without running review publication. */
6632
- async function runDryRunCommand(options) {
6633
- const runtime = await loadRuntimeProject({
6634
- ...options,
6635
- requireProviderEnv: false
6636
- });
6637
- const event = await createActionHostAdapter(options).events.parseEvent({
6638
- eventPath: options.eventPath,
6639
- env: {
6640
- ...options.env,
6641
- GITHUB_WORKSPACE: options.rootDir,
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
- configSource: runtime.settings.source,
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
- /** Runs configured change-request tasks against local Git base and head revisions. */
6652
- async function runLocalReviewCommand(options) {
6653
- const log = options.logSink ? createRuntimeActionLog({
6654
- logSink: options.logSink,
6655
- env: options.env
6656
- }) : void 0;
6657
- log?.notice("local review start", {
6658
- root: options.rootDir,
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
- async function loadTrustedRuntimeForEvent(options, event, log) {
6793
- const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
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: "issue_comment did not target a pull request"
7083
+ reason: `review comment action '${reply.action}' is not supported`
7039
7084
  };
7040
- if (comment.action !== "created") return {
7085
+ if (!reply.parentCommentId) return {
7041
7086
  kind: "ignored",
7042
- reason: `issue_comment action '${comment.action}' is not supported`
7087
+ reason: "review comment was not a reply"
7043
7088
  };
7044
- const line = firstNonEmptyLine(comment.body);
7045
- if (!line || !isPiprCommandLine(line)) return {
7089
+ if (reply.actor === "github-actions[bot]") return {
7046
7090
  kind: "ignored",
7047
- reason: "issue_comment did not target pipr"
7091
+ reason: "review comment reply was authored by pipr"
7048
7092
  };
7049
- return dryRun ? {
7093
+ if (isPiprThreadActionReplyBody(reply.body)) return {
7050
7094
  kind: "ignored",
7051
- reason: "PIPR_DRY_RUN=1; command dispatch skipped"
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 dispatchIssueCommentCommand(options, adapter, prepared, log) {
7058
- const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
7059
- const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
7060
- repository: prepared.comment.repository,
7061
- actor: prepared.comment.actor
7062
- }));
7063
- log.notice("command dispatch", {
7064
- resolution: prepared.resolution.kind,
7065
- requiredPermission,
7066
- actualPermission: permission
7067
- });
7068
- if (!hasRequiredRepositoryPermission(permission, requiredPermission)) return {
7069
- kind: "command-help",
7070
- event: prepared.event,
7071
- configSource: prepared.trustedRuntime.settings.source,
7072
- body: permissionDeniedHelp(prepared.trustedRuntime.plan, requiredPermission),
7073
- reason: `permission denied for '${prepared.line}'`
7074
- };
7075
- if (prepared.resolution.kind === "help" || prepared.resolution.kind === "invalid") return {
7076
- kind: "command-help",
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
- return await issueCommentCommandResult({
7102
- adapter,
7103
- completed: await runTrustedReviewAndPublish({
7104
- options,
7105
- adapter,
7106
- trustedRuntime: prepared.trustedRuntime,
7107
- event: prepared.event,
7108
- taskName: parsedResolution.invocation.taskName,
7109
- taskInput: parsedResolution.invocation.inputs,
7110
- selectedTasks: dispatch.kind === "change-request" ? dispatch.tasks : [],
7111
- commandInvocation: {
7112
- name: parsedResolution.invocation.commandName,
7113
- line: parsedResolution.invocation.line,
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
- async function issueCommentCommandResult(options) {
7125
- if (options.completed.kind === "skipped") return {
7126
- kind: "ignored",
7127
- reason: options.completed.reason
7128
- };
7129
- if (options.completed.kind === "command-response") return await publishCommandResponseActionResult({
7130
- adapter: options.adapter,
7131
- completed: options.completed,
7132
- event: options.event,
7133
- sourceCommentId: options.sourceCommentId,
7134
- configSource: options.configSource
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
- kind: "review",
7138
- event: options.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
- async function publishCommandResponseActionResult(options) {
7146
- const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
7147
- if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
7148
- const publication = await publishCommandResponse({
7149
- change: options.event,
7150
- sourceCommentId: options.sourceCommentId,
7151
- commandName: options.completed.response.commandName,
7152
- body: options.completed.response.body
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-BC2fTZsi.mjs.map
7252
+ //# sourceMappingURL=commands-Cj8p5IQF.mjs.map