@usepipr/runtime 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -272,7 +272,10 @@ const piprConfigSchema = z.strictObject({
272
272
  providers: z.array(providerConfigSchema).min(1),
273
273
  publication: z.strictObject({
274
274
  maxInlineComments: z.number().int().min(0).max(50).optional(),
275
- autoResolve: autoResolveConfigSchema
275
+ autoResolve: autoResolveConfigSchema,
276
+ showHeader: z.boolean().default(true),
277
+ showFooter: z.boolean().default(true),
278
+ showStats: z.boolean().default(true)
276
279
  }),
277
280
  limits: z.strictObject({
278
281
  timeoutSeconds: z.number().int().positive().max(3600).optional(),
@@ -292,6 +295,31 @@ const repositoryRefSchema = z.strictObject({
292
295
  slug: nonEmptyStringSchema,
293
296
  url: nonEmptyStringSchema.optional()
294
297
  });
298
+ const codeHostCoordinatesSchema = z.discriminatedUnion("provider", [
299
+ z.strictObject({
300
+ provider: z.literal("github"),
301
+ owner: nonEmptyStringSchema,
302
+ repository: nonEmptyStringSchema
303
+ }),
304
+ z.strictObject({
305
+ provider: z.literal("gitlab"),
306
+ projectId: nonEmptyStringSchema,
307
+ projectPath: nonEmptyStringSchema
308
+ }),
309
+ z.strictObject({
310
+ provider: z.literal("bitbucket"),
311
+ workspace: nonEmptyStringSchema,
312
+ repository: nonEmptyStringSchema,
313
+ repositoryUuid: nonEmptyStringSchema.optional()
314
+ }),
315
+ z.strictObject({
316
+ provider: z.literal("azure-devops"),
317
+ organization: nonEmptyStringSchema,
318
+ project: nonEmptyStringSchema,
319
+ projectId: nonEmptyStringSchema.optional(),
320
+ repositoryId: nonEmptyStringSchema
321
+ })
322
+ ]);
295
323
  const changeEndpointSchema = z.strictObject({
296
324
  sha: nonEmptyStringSchema,
297
325
  ref: nonEmptyStringSchema.optional(),
@@ -307,7 +335,8 @@ const changeRequestRefSchema = z.strictObject({
307
335
  author: z.strictObject({ login: nonEmptyStringSchema }).optional(),
308
336
  base: changeEndpointSchema,
309
337
  head: changeEndpointSchema,
310
- isFork: z.boolean().optional()
338
+ isFork: z.boolean().optional(),
339
+ isDraft: z.boolean().optional()
311
340
  });
312
341
  const changeRequestEventContextSchema = z.strictObject({
313
342
  eventName: nonEmptyStringSchema,
@@ -315,6 +344,7 @@ const changeRequestEventContextSchema = z.strictObject({
315
344
  rawAction: nonEmptyStringSchema.optional(),
316
345
  platform: platformInfoSchema,
317
346
  repository: repositoryRefSchema,
347
+ coordinates: codeHostCoordinatesSchema.optional(),
318
348
  change: changeRequestRefSchema,
319
349
  workspace: nonEmptyStringSchema
320
350
  });
@@ -526,7 +556,7 @@ function compareStableSemver(left, right) {
526
556
  }
527
557
  //#endregion
528
558
  //#region src/shared/version.ts
529
- const runtimeVersion = "0.3.7";
559
+ const runtimeVersion = "0.4.0";
530
560
  //#endregion
531
561
  //#region src/config/version-compat.ts
532
562
  async function resolveConfigVersionCompatibility(options) {
@@ -776,7 +806,10 @@ function planToRuntimeSettings(plan, options) {
776
806
  providers,
777
807
  publication: {
778
808
  maxInlineComments: plan.publication.maxInlineComments,
779
- autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id)
809
+ autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id),
810
+ showHeader: plan.publication.showHeader ?? true,
811
+ showFooter: plan.publication.showFooter ?? true,
812
+ showStats: plan.publication.showStats ?? true
780
813
  },
781
814
  limits: plan.limits
782
815
  },
@@ -3543,11 +3576,17 @@ function isOfficialInitRecipeId(recipe) {
3543
3576
  }
3544
3577
  //#endregion
3545
3578
  //#region src/config/init.ts
3546
- const supportedOfficialInitAdapters = ["github"];
3547
- const defaultWorkflowActionRef = "somus/pipr@v0.3.7";
3548
- const defaultSdkVersion = "0.3.7";
3579
+ const supportedOfficialInitAdapters = [
3580
+ "github",
3581
+ "gitlab",
3582
+ "azure-devops",
3583
+ "bitbucket"
3584
+ ];
3585
+ const defaultWorkflowActionRef = "somus/pipr@v0.4.0";
3586
+ const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.0";
3587
+ const defaultSdkVersion = "0.4.0";
3549
3588
  function resolveOfficialInitAdapters(adapters) {
3550
- if (adapters === void 0) return [...supportedOfficialInitAdapters];
3589
+ if (adapters === void 0) return ["github"];
3551
3590
  if (adapters.length === 0) return [];
3552
3591
  const selected = /* @__PURE__ */ new Set();
3553
3592
  for (const adapter of adapters) {
@@ -3556,7 +3595,7 @@ function resolveOfficialInitAdapters(adapters) {
3556
3595
  if (adapters.length > 1) throw new Error("Adapter 'none' cannot be mixed with other init adapters.");
3557
3596
  return [];
3558
3597
  }
3559
- if (adapter !== "github") throw unsupportedAdapterError(adapter);
3598
+ if (!supportedOfficialInitAdapters.includes(adapter)) throw unsupportedAdapterError(adapter);
3560
3599
  selected.add(adapter);
3561
3600
  }
3562
3601
  return [...selected];
@@ -3627,8 +3666,66 @@ async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false
3627
3666
  relativePath: path.join(".github", "workflows", "pipr.yml"),
3628
3667
  contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe, minimal)
3629
3668
  });
3669
+ if (adapters.includes("gitlab")) files.push({
3670
+ relativePath: ".gitlab-ci.yml",
3671
+ contents: starterGitLabPipeline(relativeConfigDir.split(path.sep).join("/"), recipe)
3672
+ });
3673
+ if (adapters.includes("azure-devops")) files.push({
3674
+ relativePath: "azure-devops.pipr.env.example",
3675
+ contents: starterAzureDevOpsWebhookEnvironment(recipe)
3676
+ });
3677
+ if (adapters.includes("bitbucket")) files.push({
3678
+ relativePath: "bitbucket.pipr.env.example",
3679
+ contents: starterBitbucketWebhookEnvironment(recipe)
3680
+ });
3630
3681
  return files;
3631
3682
  }
3683
+ function starterGitLabPipeline(relativeConfigDir, recipe) {
3684
+ const lines = [
3685
+ "pipr:",
3686
+ " image:",
3687
+ ` name: ${defaultGitLabImageRef}`,
3688
+ " entrypoint: [\"\"]",
3689
+ " rules:",
3690
+ " - if: '$CI_PIPELINE_SOURCE == \"merge_request_event\"'",
3691
+ " variables:",
3692
+ " GIT_DEPTH: \"0\"",
3693
+ " PIPR_CODE_HOST: \"gitlab\"",
3694
+ " script:",
3695
+ ` - pipr host-run --host gitlab --config-dir ${relativeConfigDir}`
3696
+ ];
3697
+ for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(` # Configure ${secret.env} as a masked GitLab CI/CD variable.`);
3698
+ lines.push("");
3699
+ return lines.join("\n");
3700
+ }
3701
+ function starterAzureDevOpsWebhookEnvironment(recipe) {
3702
+ const lines = [
3703
+ "# Copy these names into the trusted webhook runner's secret store.",
3704
+ "AZURE_DEVOPS_ORGANIZATION=",
3705
+ "AZURE_DEVOPS_PROJECT=",
3706
+ "AZURE_DEVOPS_BEARER_TOKEN=",
3707
+ "PIPR_AZURE_SUBSCRIPTION_ID=",
3708
+ "PIPR_WEBHOOK_SECRET="
3709
+ ];
3710
+ for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(`${secret.env}=`);
3711
+ lines.push("");
3712
+ return lines.join("\n");
3713
+ }
3714
+ function starterBitbucketWebhookEnvironment(recipe) {
3715
+ const lines = [
3716
+ "# Copy these names into the trusted webhook runner's secret store.",
3717
+ "BITBUCKET_WORKSPACE=",
3718
+ "BITBUCKET_REPO_SLUG=",
3719
+ "BITBUCKET_EMAIL=",
3720
+ "BITBUCKET_API_TOKEN=",
3721
+ "BITBUCKET_PERMISSION_EMAIL=",
3722
+ "BITBUCKET_PERMISSION_API_TOKEN=",
3723
+ "PIPR_WEBHOOK_SECRET="
3724
+ ];
3725
+ for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(`${secret.env}=`);
3726
+ lines.push("");
3727
+ return lines.join("\n");
3728
+ }
3632
3729
  function starterPackageJson() {
3633
3730
  return `${JSON.stringify({
3634
3731
  private: true,
@@ -3747,6 +3844,797 @@ function starterWorkflow(relativeConfigDir, recipe, minimal = false) {
3747
3844
  return lines.join("\n");
3748
3845
  }
3749
3846
  //#endregion
3847
+ //#region src/shared/redaction.ts
3848
+ const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
3849
+ function redactPotentialSecrets(value) {
3850
+ return value.replace(secretLikeTokenPattern, "[redacted secret]");
3851
+ }
3852
+ //#endregion
3853
+ //#region src/hosts/http.ts
3854
+ var CodeHostHttpError = class extends Error {
3855
+ status;
3856
+ constructor(message, status) {
3857
+ super(message);
3858
+ this.name = "CodeHostHttpError";
3859
+ this.status = status;
3860
+ }
3861
+ };
3862
+ function createCodeHostHttpClient(options) {
3863
+ const fetchRequest = options.fetch ?? fetch;
3864
+ const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
3865
+ const maxRetries = options.maxRetries ?? 2;
3866
+ const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
3867
+ const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
3868
+ let delayBeforeNextRequest = 0;
3869
+ return { async json(path, schema, init = {}) {
3870
+ if (delayBeforeNextRequest > 0) {
3871
+ await sleep(delayBeforeNextRequest);
3872
+ delayBeforeNextRequest = 0;
3873
+ }
3874
+ const method = init.method?.toUpperCase() ?? "GET";
3875
+ for (let attempt = 0;; attempt += 1) {
3876
+ const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
3877
+ const response = await fetchRequest(new URL(path, options.baseUrl), {
3878
+ ...init,
3879
+ headers: {
3880
+ ...Object.fromEntries(new Headers(options.headers)),
3881
+ ...Object.fromEntries(new Headers(init.headers))
3882
+ },
3883
+ signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
3884
+ });
3885
+ const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
3886
+ if (response.ok) {
3887
+ delayBeforeNextRequest = retryAfter;
3888
+ return schema.parse(await response.json());
3889
+ }
3890
+ if (shouldRetryCodeHostRequest({
3891
+ method,
3892
+ attempt,
3893
+ maxRetries,
3894
+ response,
3895
+ retryStatuses: options.retryNonIdempotentStatuses
3896
+ })) {
3897
+ await sleep(retryAfter || 250 * 2 ** attempt);
3898
+ continue;
3899
+ }
3900
+ const body = (await response.text()).slice(0, 1024);
3901
+ throw new CodeHostHttpError(redact$1(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
3902
+ }
3903
+ } };
3904
+ }
3905
+ function shouldRetryCodeHostRequest(options) {
3906
+ return (options.method === "GET" || options.method === "HEAD" || options.retryStatuses?.includes(options.response.status) === true) && options.attempt < options.maxRetries && (options.response.status === 429 || options.response.status >= 500 || options.retryStatuses?.includes(options.response.status) === true);
3907
+ }
3908
+ function retryAfterMilliseconds(value) {
3909
+ if (!value) return 0;
3910
+ const seconds = Number(value);
3911
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
3912
+ const date = Date.parse(value);
3913
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
3914
+ }
3915
+ function redact$1(value, secrets) {
3916
+ let redacted = value;
3917
+ for (const secret of secrets) redacted = redacted.split(secret).join("***");
3918
+ return redactPotentialSecrets(redacted);
3919
+ }
3920
+ //#endregion
3921
+ //#region src/hosts/azure-devops/coordinates.ts
3922
+ function azureOrganizationFromUrl(value) {
3923
+ const url = new URL(value);
3924
+ return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
3925
+ }
3926
+ //#endregion
3927
+ //#region src/hosts/azure-devops/client.ts
3928
+ const identitySchema = z.looseObject({
3929
+ displayName: z.string().optional(),
3930
+ providerDisplayName: z.string().optional(),
3931
+ uniqueName: z.string().optional(),
3932
+ id: z.string().optional(),
3933
+ properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
3934
+ }).transform((identity) => {
3935
+ const displayName = identity.displayName ?? identity.providerDisplayName;
3936
+ const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
3937
+ return {
3938
+ ...identity,
3939
+ ...displayName ? { displayName } : {},
3940
+ ...uniqueName ? { uniqueName } : {}
3941
+ };
3942
+ });
3943
+ const commitSchema = z.looseObject({ commitId: z.string().min(1) });
3944
+ const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
3945
+ const pullRequestSchema$1 = z.looseObject({
3946
+ pullRequestId: z.number().int().positive(),
3947
+ isDraft: z.boolean().optional(),
3948
+ title: z.string(),
3949
+ description: z.string().nullish(),
3950
+ url: z.string().optional(),
3951
+ sourceRefName: z.string().min(1),
3952
+ targetRefName: z.string().min(1),
3953
+ createdBy: identitySchema.optional(),
3954
+ lastMergeSourceCommit: commitSchema,
3955
+ lastMergeTargetCommit: commitSchema,
3956
+ forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
3957
+ repository: z.looseObject({
3958
+ id: z.string().min(1),
3959
+ name: z.string().min(1),
3960
+ url: z.string().optional(),
3961
+ project: z.looseObject({
3962
+ id: z.string().min(1),
3963
+ name: z.string().min(1)
3964
+ })
3965
+ })
3966
+ });
3967
+ const repositorySchema$1 = z.looseObject({
3968
+ id: z.string().min(1),
3969
+ name: z.string().min(1),
3970
+ project: z.looseObject({
3971
+ id: z.string().min(1),
3972
+ name: z.string().min(1)
3973
+ })
3974
+ });
3975
+ const iterationSchema = z.looseObject({
3976
+ id: z.number().int().positive(),
3977
+ sourceRefCommit: commitSchema
3978
+ });
3979
+ const iterationChangeSchema = z.looseObject({
3980
+ changeTrackingId: z.number().int(),
3981
+ changeType: z.string(),
3982
+ item: z.looseObject({
3983
+ path: z.string().min(1),
3984
+ originalPath: z.string().min(1).optional()
3985
+ })
3986
+ });
3987
+ const threadCommentSchema = z.looseObject({
3988
+ id: z.union([z.number(), z.string()]).transform(String),
3989
+ parentCommentId: z.number().int().optional(),
3990
+ content: z.string().default(""),
3991
+ author: identitySchema.optional(),
3992
+ isDeleted: z.boolean().optional()
3993
+ });
3994
+ const pointSchema = z.looseObject({
3995
+ line: z.number().int(),
3996
+ offset: z.number().int()
3997
+ });
3998
+ const threadSchema = z.looseObject({
3999
+ id: z.union([z.number(), z.string()]).transform(String),
4000
+ status: z.string().optional(),
4001
+ comments: z.array(threadCommentSchema).default([]),
4002
+ threadContext: z.looseObject({
4003
+ filePath: z.string().optional(),
4004
+ leftFileStart: pointSchema.nullish(),
4005
+ leftFileEnd: pointSchema.nullish(),
4006
+ rightFileStart: pointSchema.nullish(),
4007
+ rightFileEnd: pointSchema.nullish()
4008
+ }).nullish(),
4009
+ pullRequestThreadContext: z.looseObject({
4010
+ changeTrackingId: z.number().int().optional(),
4011
+ iterationContext: z.looseObject({
4012
+ firstComparingIteration: z.number().int(),
4013
+ secondComparingIteration: z.number().int()
4014
+ }).optional()
4015
+ }).nullish()
4016
+ });
4017
+ const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
4018
+ const aclSchema = collectionSchema(z.looseObject({
4019
+ inheritPermissions: z.boolean().default(true),
4020
+ acesDictionary: z.record(z.string(), z.looseObject({
4021
+ allow: z.number().int().default(0),
4022
+ deny: z.number().int().default(0),
4023
+ extendedInfo: z.looseObject({
4024
+ effectiveAllow: z.number().int().default(0),
4025
+ effectiveDeny: z.number().int().default(0)
4026
+ }).optional()
4027
+ }))
4028
+ }));
4029
+ function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch) {
4030
+ const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
4031
+ const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
4032
+ const pat = env.AZURE_DEVOPS_TOKEN;
4033
+ const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
4034
+ if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
4035
+ if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
4036
+ if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
4037
+ const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
4038
+ const api = createCodeHostHttpClient({
4039
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
4040
+ headers,
4041
+ fetch
4042
+ });
4043
+ const organizationApi = createCodeHostHttpClient({
4044
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
4045
+ headers,
4046
+ fetch
4047
+ });
4048
+ const identityApi = createCodeHostHttpClient({
4049
+ baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
4050
+ headers,
4051
+ fetch
4052
+ });
4053
+ const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
4054
+ return {
4055
+ organization,
4056
+ project,
4057
+ async currentUser() {
4058
+ return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
4059
+ },
4060
+ async getRepository(repository) {
4061
+ const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
4062
+ return {
4063
+ id: value.id,
4064
+ name: value.name,
4065
+ projectId: value.project.id,
4066
+ project: value.project.name
4067
+ };
4068
+ },
4069
+ async getRepositoryPermission(actor, projectId, repositoryId) {
4070
+ const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
4071
+ descriptor: z.string().min(1),
4072
+ isActive: z.boolean().optional(),
4073
+ isContainer: z.boolean().optional(),
4074
+ memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
4075
+ })))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
4076
+ if (candidates.length !== 1) return "none";
4077
+ const identity = candidates[0];
4078
+ if (!identity) return "none";
4079
+ const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
4080
+ const loadAcls = (token) => {
4081
+ const query = new URLSearchParams({
4082
+ token,
4083
+ descriptors: descriptors.join(","),
4084
+ includeExtendedInfo: "true",
4085
+ recurse: "false",
4086
+ "api-version": "7.1"
4087
+ });
4088
+ return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
4089
+ };
4090
+ const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
4091
+ return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
4092
+ },
4093
+ getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
4094
+ async listIterations(repositoryId, changeNumber) {
4095
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
4096
+ id: iteration.id,
4097
+ headSha: iteration.sourceRefCommit.commitId
4098
+ }));
4099
+ },
4100
+ async loadChange(options) {
4101
+ if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
4102
+ const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
4103
+ const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
4104
+ const headSha = pullRequest.lastMergeSourceCommit.commitId;
4105
+ const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
4106
+ if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
4107
+ const sourceRef = branchName(pullRequest.sourceRefName);
4108
+ const targetRef = branchName(pullRequest.targetRefName);
4109
+ return {
4110
+ repository: {
4111
+ slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
4112
+ url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
4113
+ },
4114
+ coordinates: {
4115
+ provider: "azure-devops",
4116
+ organization,
4117
+ project: pullRequest.repository.project.name,
4118
+ projectId: pullRequest.repository.project.id,
4119
+ repositoryId: pullRequest.repository.id
4120
+ },
4121
+ change: {
4122
+ number: pullRequest.pullRequestId,
4123
+ isDraft: pullRequest.isDraft,
4124
+ title: pullRequest.title,
4125
+ description: pullRequest.description ?? "",
4126
+ url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
4127
+ author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
4128
+ base: {
4129
+ sha: pullRequest.lastMergeTargetCommit.commitId,
4130
+ ref: targetRef
4131
+ },
4132
+ head: {
4133
+ sha: headSha,
4134
+ ref: sourceRef,
4135
+ ...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
4136
+ },
4137
+ ...pullRequest.forkSource ? { isFork: true } : {}
4138
+ },
4139
+ iterationId: iteration.id
4140
+ };
4141
+ },
4142
+ async listIterationChanges(repositoryId, changeNumber, iterationId) {
4143
+ const changes = [];
4144
+ let skip = 0;
4145
+ let top = 2e3;
4146
+ for (;;) {
4147
+ const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
4148
+ const page = await api.json(withApiVersion(path), z.looseObject({
4149
+ changeEntries: z.array(iterationChangeSchema),
4150
+ nextSkip: z.number().int().nonnegative().optional(),
4151
+ nextTop: z.number().int().nonnegative().max(2e3).optional()
4152
+ }));
4153
+ changes.push(...page.changeEntries.map((change) => ({
4154
+ changeTrackingId: change.changeTrackingId,
4155
+ changeType: change.changeType,
4156
+ path: trimLeadingSlash(change.item.path),
4157
+ ...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
4158
+ })));
4159
+ if (!page.nextSkip) return changes;
4160
+ skip = page.nextSkip;
4161
+ if (page.nextTop) top = page.nextTop;
4162
+ }
4163
+ },
4164
+ async listThreads(repositoryId, changeNumber) {
4165
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
4166
+ },
4167
+ createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
4168
+ updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
4169
+ createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
4170
+ updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
4171
+ async createStatus(repositoryId, changeNumber, body) {
4172
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
4173
+ }
4174
+ };
4175
+ }
4176
+ function azureDevOpsStatusState(state) {
4177
+ switch (state) {
4178
+ case "pending": return "pending";
4179
+ case "success": return "succeeded";
4180
+ case "failure": return "failed";
4181
+ case "neutral": return "notApplicable";
4182
+ }
4183
+ }
4184
+ function azureRepositoryPermission(bits) {
4185
+ if ((bits & 2) === 0) return "none";
4186
+ if ((bits & 8192) !== 0) return "admin";
4187
+ if ((bits & 2048) !== 0) return "maintain";
4188
+ if ((bits & 4) !== 0) return "write";
4189
+ if ((bits & 16384) !== 0) return "triage";
4190
+ return "read";
4191
+ }
4192
+ function azurePermissionFromAcls(acls) {
4193
+ let allow = 0;
4194
+ let deny = 0;
4195
+ for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
4196
+ allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
4197
+ deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
4198
+ }
4199
+ return azureRepositoryPermission(allow & ~deny);
4200
+ }
4201
+ function collectionSchema(item) {
4202
+ return z.looseObject({
4203
+ count: z.number().int().nonnegative(),
4204
+ value: z.array(item)
4205
+ });
4206
+ }
4207
+ function withApiVersion(path) {
4208
+ return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
4209
+ }
4210
+ function jsonRequest$2(method, body) {
4211
+ return {
4212
+ method,
4213
+ headers: { "Content-Type": "application/json" },
4214
+ body: JSON.stringify(body)
4215
+ };
4216
+ }
4217
+ function organizationFromCollectionUri$1(value) {
4218
+ return value ? azureOrganizationFromUrl(value) : void 0;
4219
+ }
4220
+ function branchName(ref) {
4221
+ return ref.replace(/^refs\/heads\//, "");
4222
+ }
4223
+ function repositoryWebUrl(organization, project, repository) {
4224
+ return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
4225
+ }
4226
+ function trimLeadingSlash(value) {
4227
+ return value.replace(/^\/+/, "");
4228
+ }
4229
+ //#endregion
4230
+ //#region src/hosts/bitbucket/schema.ts
4231
+ const bitbucketRepositorySchema = z.looseObject({
4232
+ uuid: z.string().min(1),
4233
+ name: z.string().min(1),
4234
+ full_name: z.string().min(1),
4235
+ slug: z.string().min(1).optional(),
4236
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
4237
+ }).transform((repository) => ({
4238
+ ...repository,
4239
+ slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
4240
+ }));
4241
+ //#endregion
4242
+ //#region src/hosts/bitbucket/client.ts
4243
+ const userSchema$1 = z.looseObject({
4244
+ uuid: z.string().optional(),
4245
+ nickname: z.string().optional(),
4246
+ display_name: z.string().optional()
4247
+ });
4248
+ const endpointSchema = z.looseObject({
4249
+ branch: z.looseObject({ name: z.string().min(1) }),
4250
+ commit: z.looseObject({ hash: z.string().min(1) }),
4251
+ repository: bitbucketRepositorySchema
4252
+ });
4253
+ const pullRequestSchema = z.looseObject({
4254
+ id: z.number().int().positive(),
4255
+ draft: z.boolean().optional(),
4256
+ title: z.string(),
4257
+ description: z.string().default(""),
4258
+ author: userSchema$1.optional(),
4259
+ source: endpointSchema,
4260
+ destination: endpointSchema,
4261
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
4262
+ });
4263
+ const inlineSchema = z.looseObject({
4264
+ path: z.string().optional(),
4265
+ from: z.number().int().nullable().optional(),
4266
+ to: z.number().int().nullable().optional(),
4267
+ start_from: z.number().int().nullable().optional(),
4268
+ start_to: z.number().int().nullable().optional()
4269
+ });
4270
+ const commentSchema = z.looseObject({
4271
+ id: z.union([z.number(), z.string()]).transform(String),
4272
+ content: z.looseObject({ raw: z.string().default("") }),
4273
+ user: userSchema$1.optional(),
4274
+ parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
4275
+ inline: inlineSchema.optional(),
4276
+ deleted: z.boolean().optional(),
4277
+ resolution: z.looseObject({}).optional()
4278
+ });
4279
+ function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
4280
+ const workspace = env.BITBUCKET_WORKSPACE;
4281
+ const repository = env.BITBUCKET_REPO_SLUG;
4282
+ const token = env.BITBUCKET_API_TOKEN;
4283
+ const email = env.BITBUCKET_EMAIL;
4284
+ if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
4285
+ if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
4286
+ if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
4287
+ const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
4288
+ const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
4289
+ const api = createCodeHostHttpClient({
4290
+ baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
4291
+ headers: { Authorization: authorization },
4292
+ fetch
4293
+ });
4294
+ const rootApi = createCodeHostHttpClient({
4295
+ baseUrl: "https://api.bitbucket.org/2.0/",
4296
+ headers: { Authorization: authorization },
4297
+ fetch
4298
+ });
4299
+ const prPath = (id) => `pullrequests/${id}`;
4300
+ return {
4301
+ workspace,
4302
+ repository,
4303
+ async currentUser() {
4304
+ const value = await rootApi.json("user", userSchema$1);
4305
+ return {
4306
+ uuid: value.uuid,
4307
+ nickname: value.nickname,
4308
+ displayName: value.display_name
4309
+ };
4310
+ },
4311
+ async getRepository() {
4312
+ const value = await api.json("", bitbucketRepositorySchema);
4313
+ return {
4314
+ uuid: value.uuid,
4315
+ slug: value.slug,
4316
+ fullName: value.full_name,
4317
+ url: value.links.html.href
4318
+ };
4319
+ },
4320
+ async getRepositoryPermission(actor, repositoryUuid) {
4321
+ const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
4322
+ const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
4323
+ if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
4324
+ const permissionApi = createCodeHostHttpClient({
4325
+ baseUrl: "https://api.bitbucket.org/2.0/",
4326
+ headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
4327
+ fetch
4328
+ });
4329
+ const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
4330
+ return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
4331
+ permission: z.enum([
4332
+ "read",
4333
+ "write",
4334
+ "admin"
4335
+ ]),
4336
+ user: userSchema$1
4337
+ })))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
4338
+ },
4339
+ getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
4340
+ async loadChange(options) {
4341
+ if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
4342
+ const pullRequest = await this.getPullRequest(options.changeNumber);
4343
+ return {
4344
+ repository: {
4345
+ slug: pullRequest.destination.repository.full_name,
4346
+ url: pullRequest.destination.repository.links.html.href
4347
+ },
4348
+ coordinates: {
4349
+ provider: "bitbucket",
4350
+ workspace,
4351
+ repository,
4352
+ repositoryUuid: pullRequest.destination.repository.uuid
4353
+ },
4354
+ change: {
4355
+ number: pullRequest.id,
4356
+ isDraft: pullRequest.draft,
4357
+ title: pullRequest.title,
4358
+ description: pullRequest.description,
4359
+ url: pullRequest.links.html.href,
4360
+ author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
4361
+ base: {
4362
+ sha: pullRequest.destination.commit.hash,
4363
+ ref: pullRequest.destination.branch.name,
4364
+ url: pullRequest.destination.repository.links.html.href
4365
+ },
4366
+ head: {
4367
+ sha: pullRequest.source.commit.hash,
4368
+ ref: pullRequest.source.branch.name,
4369
+ url: pullRequest.source.repository.links.html.href
4370
+ },
4371
+ isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
4372
+ }
4373
+ };
4374
+ },
4375
+ async listComments(id) {
4376
+ return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
4377
+ },
4378
+ createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
4379
+ updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
4380
+ async replyToComment(id, commentId, content) {
4381
+ const parentId = positiveCommentId(commentId);
4382
+ return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
4383
+ content: { raw: content },
4384
+ parent: { id: parentId }
4385
+ }));
4386
+ },
4387
+ async resolveComment(id, commentId) {
4388
+ positiveCommentId(commentId);
4389
+ await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
4390
+ },
4391
+ async setStatus(sha, key, body) {
4392
+ return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
4393
+ ...body,
4394
+ key
4395
+ }))).key;
4396
+ }
4397
+ };
4398
+ }
4399
+ function positiveCommentId(value) {
4400
+ const id = Number(value);
4401
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
4402
+ return id;
4403
+ }
4404
+ function bitbucketStatusState(state) {
4405
+ if (state === "pending") return "INPROGRESS";
4406
+ if (state === "failure") return "FAILED";
4407
+ if (state === "neutral") return "STOPPED";
4408
+ return "SUCCESSFUL";
4409
+ }
4410
+ function escapeBitbucketQueryValue(value) {
4411
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
4412
+ }
4413
+ function pagedSchema(item) {
4414
+ return z.looseObject({
4415
+ values: z.array(item),
4416
+ next: z.string().url().optional()
4417
+ });
4418
+ }
4419
+ async function listAll(api, path, schema, allowedPathPrefix) {
4420
+ const values = [];
4421
+ let next = path;
4422
+ while (next) {
4423
+ const page = await api.json(next, pagedSchema(schema));
4424
+ values.push(...page.values);
4425
+ if (page.next) {
4426
+ const url = new URL(page.next);
4427
+ if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
4428
+ }
4429
+ next = page.next;
4430
+ }
4431
+ return values;
4432
+ }
4433
+ function jsonRequest$1(method, body) {
4434
+ return {
4435
+ method,
4436
+ headers: { "Content-Type": "application/json" },
4437
+ body: JSON.stringify(body)
4438
+ };
4439
+ }
4440
+ //#endregion
4441
+ //#region src/hosts/gitlab/client.ts
4442
+ const userSchema = z.looseObject({
4443
+ id: z.number().int().positive(),
4444
+ username: z.string().min(1)
4445
+ });
4446
+ const noteSchema = z.looseObject({
4447
+ id: z.union([z.number(), z.string()]).transform(String),
4448
+ body: z.string(),
4449
+ author: userSchema.optional()
4450
+ });
4451
+ const positionSchema = z.looseObject({
4452
+ new_path: z.string().optional(),
4453
+ old_path: z.string().optional(),
4454
+ new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
4455
+ old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0)
4456
+ });
4457
+ const discussionNoteSchema = noteSchema.extend({
4458
+ resolvable: z.boolean().optional(),
4459
+ resolved: z.boolean().optional(),
4460
+ position: positionSchema.nullable().optional()
4461
+ });
4462
+ const discussionSchema = z.looseObject({
4463
+ id: z.string().min(1),
4464
+ individual_note: z.boolean().optional(),
4465
+ notes: z.array(discussionNoteSchema)
4466
+ });
4467
+ const diffRefsSchema = z.looseObject({
4468
+ base_sha: z.string().min(1),
4469
+ start_sha: z.string().min(1),
4470
+ head_sha: z.string().min(1)
4471
+ });
4472
+ const mergeRequestResponseSchema = z.looseObject({
4473
+ iid: z.number().int().positive(),
4474
+ title: z.string().default(""),
4475
+ description: z.string().nullable().optional(),
4476
+ web_url: z.string().url().optional(),
4477
+ source_branch: z.string().min(1),
4478
+ target_branch: z.string().min(1),
4479
+ source_project_id: z.number().int().positive(),
4480
+ target_project_id: z.number().int().positive(),
4481
+ sha: z.string().min(1),
4482
+ draft: z.boolean().optional(),
4483
+ author: userSchema.optional(),
4484
+ references: z.looseObject({ full: z.string().optional() }).optional(),
4485
+ diff_refs: diffRefsSchema.nullable().optional()
4486
+ });
4487
+ const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
4488
+ const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
4489
+ const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
4490
+ function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
4491
+ const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
4492
+ if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
4493
+ const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
4494
+ const clientOptions = {
4495
+ baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
4496
+ headers,
4497
+ fetch,
4498
+ sleep: async (milliseconds) => void await sleep(milliseconds)
4499
+ };
4500
+ const api = createCodeHostHttpClient(clientOptions);
4501
+ const statusApi = createCodeHostHttpClient({
4502
+ ...clientOptions,
4503
+ retryNonIdempotentStatuses: [409]
4504
+ });
4505
+ return {
4506
+ async getProject(project) {
4507
+ const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
4508
+ id: z.union([z.number(), z.string()]).transform(String),
4509
+ path_with_namespace: z.string().min(1)
4510
+ }));
4511
+ return {
4512
+ id: value.id,
4513
+ path: value.path_with_namespace
4514
+ };
4515
+ },
4516
+ currentUser: () => api.json("user", userSchema),
4517
+ async loadChange(options) {
4518
+ const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
4519
+ return {
4520
+ repository: {
4521
+ slug: options.projectPath,
4522
+ url: projectWebUrl(mergeRequest.web_url)
4523
+ },
4524
+ coordinates: {
4525
+ provider: "gitlab",
4526
+ projectId: String(mergeRequest.target_project_id),
4527
+ projectPath: options.projectPath
4528
+ },
4529
+ change: {
4530
+ number: mergeRequest.iid,
4531
+ title: mergeRequest.title,
4532
+ description: mergeRequest.description ?? "",
4533
+ url: mergeRequest.web_url,
4534
+ author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
4535
+ base: {
4536
+ sha: mergeRequest.diff_refs.start_sha,
4537
+ ref: mergeRequest.target_branch
4538
+ },
4539
+ head: {
4540
+ sha: mergeRequest.diff_refs.head_sha,
4541
+ ref: mergeRequest.source_branch
4542
+ },
4543
+ isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
4544
+ isDraft: mergeRequest.draft
4545
+ }
4546
+ };
4547
+ },
4548
+ async getMergeRequest(projectId, changeNumber) {
4549
+ const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
4550
+ for (let attempt = 0;; attempt += 1) {
4551
+ const response = await api.json(requestPath, mergeRequestResponseSchema);
4552
+ const prepared = mergeRequestSchema.safeParse(response);
4553
+ if (prepared.success) return prepared.data;
4554
+ if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
4555
+ await sleep(250 * 2 ** attempt);
4556
+ }
4557
+ },
4558
+ async getRepositoryPermission(projectId, username) {
4559
+ const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
4560
+ if (!user) return "none";
4561
+ try {
4562
+ return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
4563
+ } catch (error) {
4564
+ if (error instanceof CodeHostHttpError && error.status === 404) return "none";
4565
+ throw error;
4566
+ }
4567
+ },
4568
+ listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
4569
+ createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
4570
+ updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
4571
+ listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
4572
+ getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
4573
+ async findReplyParent(projectId, changeNumber, noteId, discussionId) {
4574
+ if (!discussionId) return void 0;
4575
+ const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
4576
+ if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
4577
+ return discussion.notes[0]?.id;
4578
+ },
4579
+ createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
4580
+ body,
4581
+ position
4582
+ })),
4583
+ replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
4584
+ async resolveDiscussion(projectId, changeNumber, discussionId) {
4585
+ await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
4586
+ },
4587
+ async setStatus(projectId, sha, name, state, description) {
4588
+ const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
4589
+ state: gitLabStatusState(state),
4590
+ name,
4591
+ description: boundedStatusDescription(description)
4592
+ }));
4593
+ return String(status.id ?? name);
4594
+ }
4595
+ };
4596
+ }
4597
+ const MAX_PAGINATION_PAGES = 100;
4598
+ async function paginated(client, path, schema) {
4599
+ const values = [];
4600
+ for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
4601
+ const separator = path.includes("?") ? "&" : "?";
4602
+ const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
4603
+ values.push(...batch);
4604
+ if (batch.length < 100) return values;
4605
+ }
4606
+ throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
4607
+ }
4608
+ function jsonRequest(method, body) {
4609
+ return {
4610
+ method,
4611
+ headers: { "Content-Type": "application/json" },
4612
+ body: JSON.stringify(body)
4613
+ };
4614
+ }
4615
+ function accessLevelPermission(level) {
4616
+ if (level >= 50) return "admin";
4617
+ if (level >= 40) return "maintain";
4618
+ if (level >= 30) return "write";
4619
+ if (level >= 15) return "triage";
4620
+ if (level >= 10) return "read";
4621
+ return "none";
4622
+ }
4623
+ function boundedStatusDescription(description) {
4624
+ return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
4625
+ }
4626
+ function gitLabStatusState(state) {
4627
+ switch (state) {
4628
+ case "pending": return "pending";
4629
+ case "success": return "success";
4630
+ case "failure": return "failed";
4631
+ case "neutral": return "skipped";
4632
+ }
4633
+ }
4634
+ function projectWebUrl(mergeRequestUrl) {
4635
+ return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
4636
+ }
4637
+ //#endregion
3750
4638
  //#region src/diff/git.ts
3751
4639
  function runGit$1(args, cwd, maxBuffer) {
3752
4640
  const result = Bun.spawnSync(["git", ...args], {
@@ -4108,32 +4996,217 @@ function createLocalChangeRequestEvent(options) {
4108
4996
  });
4109
4997
  }
4110
4998
  //#endregion
4111
- //#region src/commands/grammar.ts
4112
- const piprCommandPrefix = "@pipr";
4113
- function firstNonEmptyLine(value) {
4114
- return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
4999
+ //#region src/diff/path-filter.ts
5000
+ const matcherOptions = {
5001
+ dot: true,
5002
+ nonegate: true,
5003
+ windows: false
5004
+ };
5005
+ const compiledFilters = /* @__PURE__ */ new WeakMap();
5006
+ function filterDiffManifestByPaths(manifest, filter) {
5007
+ if (!filter) return manifest;
5008
+ return {
5009
+ ...manifest,
5010
+ files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
5011
+ };
4115
5012
  }
4116
- function isPiprCommandLine(line) {
4117
- return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
5013
+ function diffFileMatchesPathFilter(file, filter) {
5014
+ if (!filter) return true;
5015
+ const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
5016
+ const compiled = readCompiledFilter(filter);
5017
+ const include = compiled.include;
5018
+ const exclude = compiled.exclude;
5019
+ const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
5020
+ const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
5021
+ return included && !excluded;
4118
5022
  }
4119
- function parseCommandPattern(pattern, line) {
4120
- const patternParts = commandPatternParts(pattern);
4121
- const validationError = unsupportedCommandRestCaptureError(pattern);
4122
- if (validationError) return {
4123
- ok: false,
4124
- error: validationError
4125
- };
4126
- const lineTokens = tokenizeCommandPattern(line);
4127
- const captures = {};
4128
- let index = 0;
4129
- for (const part of patternParts) {
4130
- if (isOptionalCommandPatternPart(part)) {
4131
- const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
4132
- if (nextIndex !== void 0) index = nextIndex;
4133
- continue;
4134
- }
4135
- if (isCommandRestCaptureToken(part)) {
4136
- const value = lineTokens.slice(index).join(" ");
5023
+ function pathMatchesFilter(filePath, filter) {
5024
+ if (!filter) return true;
5025
+ const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
5026
+ const compiled = readCompiledFilter(filter);
5027
+ if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
5028
+ return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
5029
+ }
5030
+ function readCompiledFilter(filter) {
5031
+ const existing = compiledFilters.get(filter);
5032
+ if (existing) return existing;
5033
+ const compiled = {
5034
+ include: filter.include?.map(compilePattern),
5035
+ exclude: filter.exclude?.map(compilePattern)
5036
+ };
5037
+ compiledFilters.set(filter, compiled);
5038
+ return compiled;
5039
+ }
5040
+ function compilePattern(pattern) {
5041
+ return {
5042
+ matches: picomatch(pattern, matcherOptions),
5043
+ matchBasename: !pattern.includes("/")
5044
+ };
5045
+ }
5046
+ //#endregion
5047
+ //#region src/diff/manifest-projection.ts
5048
+ const defaultDiffManifestPromptLimits = {
5049
+ fullMaxBytes: 128 * 1024,
5050
+ fullMaxEstimatedTokens: 32e3,
5051
+ condensedMaxBytes: 256 * 1024,
5052
+ condensedMaxEstimatedTokens: 64e3,
5053
+ toolResponseMaxBytes: 64 * 1024
5054
+ };
5055
+ function projectDiffManifest(manifest, options) {
5056
+ if (!manifestOptionsHaveEffect(options)) return manifest;
5057
+ const manifestOptions = options ?? {};
5058
+ const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
5059
+ return parseDiffManifest({
5060
+ ...scopedManifest,
5061
+ files: scopedManifest.files.map((file) => ({
5062
+ ...withoutCompressedFileFields(file, manifestOptions.compressed === true),
5063
+ commentableRanges: file.commentableRanges.map((range) => ({
5064
+ ...rangeFieldsForOptions(range, manifestOptions),
5065
+ ...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
5066
+ }))
5067
+ }))
5068
+ });
5069
+ }
5070
+ function cloneDiffManifest(manifest) {
5071
+ return parseDiffManifest(structuredClone(manifest));
5072
+ }
5073
+ function prepareDiffManifestPrompt(manifest, config) {
5074
+ const limits = resolveDiffManifestPromptLimits(config);
5075
+ const full = measureDiffManifestPrompt(manifest);
5076
+ if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
5077
+ mode: "full",
5078
+ manifest,
5079
+ metrics: {
5080
+ full,
5081
+ selected: full
5082
+ },
5083
+ limits
5084
+ };
5085
+ const condensedManifest = condenseDiffManifest(manifest);
5086
+ const condensed = measureDiffManifestPrompt(condensedManifest);
5087
+ if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
5088
+ "Diff Manifest payload exceeds condensed limit before Pi execution",
5089
+ `selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
5090
+ `limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
5091
+ ].join("; "));
5092
+ return {
5093
+ mode: "condensed",
5094
+ manifest: condensedManifest,
5095
+ metrics: {
5096
+ full,
5097
+ selected: condensed
5098
+ },
5099
+ limits
5100
+ };
5101
+ }
5102
+ function condenseDiffManifest(manifest) {
5103
+ return {
5104
+ baseSha: manifest.baseSha,
5105
+ headSha: manifest.headSha,
5106
+ mergeBaseSha: manifest.mergeBaseSha,
5107
+ files: manifest.files.map(condenseDiffManifestFile)
5108
+ };
5109
+ }
5110
+ function measureDiffManifestPrompt(manifest) {
5111
+ const json = JSON.stringify(manifest, null, 2);
5112
+ const bytes = Buffer.byteLength(json, "utf8");
5113
+ return {
5114
+ bytes,
5115
+ estimatedTokens: Math.ceil(bytes / 4)
5116
+ };
5117
+ }
5118
+ function resolveDiffManifestPromptLimits(config) {
5119
+ return {
5120
+ ...defaultDiffManifestPromptLimits,
5121
+ ...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
5122
+ };
5123
+ }
5124
+ function manifestOptionsHaveEffect(options) {
5125
+ return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
5126
+ }
5127
+ function withoutCompressedFileFields(file, compressed) {
5128
+ if (!compressed) return file;
5129
+ const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
5130
+ return rest;
5131
+ }
5132
+ function withoutCompressedRangeFields(range, compressed) {
5133
+ if (!compressed) return range;
5134
+ const { summary: _summary, ...rest } = range;
5135
+ return rest;
5136
+ }
5137
+ function rangeFieldsForOptions(range, options) {
5138
+ const fields = withoutCompressedRangeFields(range, options.compressed === true);
5139
+ if (options.includePreviews === false) {
5140
+ const { preview: _preview, ...rest } = fields;
5141
+ return rest;
5142
+ }
5143
+ return fields;
5144
+ }
5145
+ function truncatePreview(preview, maxLines) {
5146
+ if (preview === void 0 || maxLines === void 0) return preview;
5147
+ return preview.split("\n").slice(0, maxLines).join("\n");
5148
+ }
5149
+ function condenseDiffManifestFile(file) {
5150
+ return {
5151
+ path: file.path,
5152
+ previousPath: file.previousPath,
5153
+ status: file.status,
5154
+ language: file.language,
5155
+ additions: file.additions,
5156
+ deletions: file.deletions,
5157
+ hunks: file.hunks.map((hunk) => ({
5158
+ hunkIndex: hunk.hunkIndex,
5159
+ header: hunk.header,
5160
+ oldStart: hunk.oldStart,
5161
+ oldLines: hunk.oldLines,
5162
+ newStart: hunk.newStart,
5163
+ newLines: hunk.newLines,
5164
+ contentHash: hunk.contentHash
5165
+ })),
5166
+ commentableRanges: file.commentableRanges.map((range) => ({
5167
+ id: range.id,
5168
+ path: range.path,
5169
+ side: range.side,
5170
+ startLine: range.startLine,
5171
+ endLine: range.endLine,
5172
+ kind: range.kind,
5173
+ hunkIndex: range.hunkIndex,
5174
+ hunkHeader: range.hunkHeader,
5175
+ hunkContentHash: range.hunkContentHash
5176
+ })),
5177
+ excludedReason: file.excludedReason
5178
+ };
5179
+ }
5180
+ function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
5181
+ return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
5182
+ }
5183
+ //#endregion
5184
+ //#region src/commands/grammar.ts
5185
+ const piprCommandPrefix = "@pipr";
5186
+ function firstNonEmptyLine(value) {
5187
+ return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
5188
+ }
5189
+ function isPiprCommandLine(line) {
5190
+ return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
5191
+ }
5192
+ function parseCommandPattern(pattern, line) {
5193
+ const patternParts = commandPatternParts(pattern);
5194
+ const validationError = unsupportedCommandRestCaptureError(pattern);
5195
+ if (validationError) return {
5196
+ ok: false,
5197
+ error: validationError
5198
+ };
5199
+ const lineTokens = tokenizeCommandPattern(line);
5200
+ const captures = {};
5201
+ let index = 0;
5202
+ for (const part of patternParts) {
5203
+ if (isOptionalCommandPatternPart(part)) {
5204
+ const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
5205
+ if (nextIndex !== void 0) index = nextIndex;
5206
+ continue;
5207
+ }
5208
+ if (isCommandRestCaptureToken(part)) {
5209
+ const value = lineTokens.slice(index).join(" ");
4137
5210
  if (!value) return {
4138
5211
  ok: false,
4139
5212
  error: `Expected '${part}'`
@@ -4196,7 +5269,7 @@ function parsePatternToken(patternToken, lineTokens, index, captures) {
4196
5269
  return lineTokens[index] === patternToken ? index + 1 : void 0;
4197
5270
  }
4198
5271
  //#endregion
4199
- //#region src/action/entry-dispatch.ts
5272
+ //#region src/host-run/entry-dispatch.ts
4200
5273
  const permissionOrder = [
4201
5274
  "read",
4202
5275
  "triage",
@@ -4331,229 +5404,44 @@ function renderPlanCommandHelp(plan, reason) {
4331
5404
  return lines.join("\n");
4332
5405
  }
4333
5406
  //#endregion
4334
- //#region src/diff/path-filter.ts
4335
- const matcherOptions = {
4336
- dot: true,
4337
- nonegate: true,
4338
- windows: false
4339
- };
4340
- const compiledFilters = /* @__PURE__ */ new WeakMap();
4341
- function filterDiffManifestByPaths(manifest, filter) {
4342
- if (!filter) return manifest;
5407
+ //#region src/pi/runtime-tools.ts
5408
+ const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
5409
+ async function preparePiRuntimeReadTools(options) {
5410
+ const toolRoot = path.join(options.root, "runtime-tools");
5411
+ const baseRoot = path.join(toolRoot, "base");
5412
+ await mkdir(baseRoot, { recursive: true });
5413
+ const baseRanges = await materializeBaseRangeSnapshots({
5414
+ baseRoot,
5415
+ manifest: options.request.manifest,
5416
+ sourceWorkspace: options.sourceWorkspace,
5417
+ maxBytes: options.request.toolResponseMaxBytes
5418
+ });
5419
+ const data = {
5420
+ manifest: options.request.manifest,
5421
+ toolResponseMaxBytes: options.request.toolResponseMaxBytes,
5422
+ baseRanges
5423
+ };
5424
+ const dataPath = path.join(toolRoot, "data.json");
5425
+ await Bun.write(dataPath, JSON.stringify(data));
4343
5426
  return {
4344
- ...manifest,
4345
- files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
5427
+ extensionPath: await piRuntimeToolsExtensionPath(),
5428
+ dataPath,
5429
+ toolNames: piRuntimeReadToolNames
4346
5430
  };
4347
5431
  }
4348
- function diffFileMatchesPathFilter(file, filter) {
4349
- if (!filter) return true;
4350
- const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
4351
- const compiled = readCompiledFilter(filter);
4352
- const include = compiled.include;
4353
- const exclude = compiled.exclude;
4354
- const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
4355
- const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
4356
- return included && !excluded;
5432
+ async function piRuntimeToolsExtensionPath() {
5433
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
5434
+ const candidates = [
5435
+ path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
5436
+ path.join(moduleDir, "runtime-tools-extension.mjs"),
5437
+ path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
5438
+ path.join(moduleDir, "runtime-tools-extension.ts")
5439
+ ];
5440
+ for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
5441
+ throw new Error("Unable to locate pipr runtime tools extension");
4357
5442
  }
4358
- function pathMatchesFilter(filePath, filter) {
4359
- if (!filter) return true;
4360
- const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
4361
- const compiled = readCompiledFilter(filter);
4362
- if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
4363
- return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
4364
- }
4365
- function readCompiledFilter(filter) {
4366
- const existing = compiledFilters.get(filter);
4367
- if (existing) return existing;
4368
- const compiled = {
4369
- include: filter.include?.map(compilePattern),
4370
- exclude: filter.exclude?.map(compilePattern)
4371
- };
4372
- compiledFilters.set(filter, compiled);
4373
- return compiled;
4374
- }
4375
- function compilePattern(pattern) {
4376
- return {
4377
- matches: picomatch(pattern, matcherOptions),
4378
- matchBasename: !pattern.includes("/")
4379
- };
4380
- }
4381
- //#endregion
4382
- //#region src/diff/manifest-projection.ts
4383
- const defaultDiffManifestPromptLimits = {
4384
- fullMaxBytes: 128 * 1024,
4385
- fullMaxEstimatedTokens: 32e3,
4386
- condensedMaxBytes: 256 * 1024,
4387
- condensedMaxEstimatedTokens: 64e3,
4388
- toolResponseMaxBytes: 64 * 1024
4389
- };
4390
- function projectDiffManifest(manifest, options) {
4391
- if (!manifestOptionsHaveEffect(options)) return manifest;
4392
- const manifestOptions = options ?? {};
4393
- const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
4394
- return parseDiffManifest({
4395
- ...scopedManifest,
4396
- files: scopedManifest.files.map((file) => ({
4397
- ...withoutCompressedFileFields(file, manifestOptions.compressed === true),
4398
- commentableRanges: file.commentableRanges.map((range) => ({
4399
- ...rangeFieldsForOptions(range, manifestOptions),
4400
- ...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
4401
- }))
4402
- }))
4403
- });
4404
- }
4405
- function cloneDiffManifest(manifest) {
4406
- return parseDiffManifest(structuredClone(manifest));
4407
- }
4408
- function prepareDiffManifestPrompt(manifest, config) {
4409
- const limits = resolveDiffManifestPromptLimits(config);
4410
- const full = measureDiffManifestPrompt(manifest);
4411
- if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
4412
- mode: "full",
4413
- manifest,
4414
- metrics: {
4415
- full,
4416
- selected: full
4417
- },
4418
- limits
4419
- };
4420
- const condensedManifest = condenseDiffManifest(manifest);
4421
- const condensed = measureDiffManifestPrompt(condensedManifest);
4422
- if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
4423
- "Diff Manifest payload exceeds condensed limit before Pi execution",
4424
- `selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
4425
- `limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
4426
- ].join("; "));
4427
- return {
4428
- mode: "condensed",
4429
- manifest: condensedManifest,
4430
- metrics: {
4431
- full,
4432
- selected: condensed
4433
- },
4434
- limits
4435
- };
4436
- }
4437
- function condenseDiffManifest(manifest) {
4438
- return {
4439
- baseSha: manifest.baseSha,
4440
- headSha: manifest.headSha,
4441
- mergeBaseSha: manifest.mergeBaseSha,
4442
- files: manifest.files.map(condenseDiffManifestFile)
4443
- };
4444
- }
4445
- function measureDiffManifestPrompt(manifest) {
4446
- const json = JSON.stringify(manifest, null, 2);
4447
- const bytes = Buffer.byteLength(json, "utf8");
4448
- return {
4449
- bytes,
4450
- estimatedTokens: Math.ceil(bytes / 4)
4451
- };
4452
- }
4453
- function resolveDiffManifestPromptLimits(config) {
4454
- return {
4455
- ...defaultDiffManifestPromptLimits,
4456
- ...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
4457
- };
4458
- }
4459
- function manifestOptionsHaveEffect(options) {
4460
- return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
4461
- }
4462
- function withoutCompressedFileFields(file, compressed) {
4463
- if (!compressed) return file;
4464
- const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
4465
- return rest;
4466
- }
4467
- function withoutCompressedRangeFields(range, compressed) {
4468
- if (!compressed) return range;
4469
- const { summary: _summary, ...rest } = range;
4470
- return rest;
4471
- }
4472
- function rangeFieldsForOptions(range, options) {
4473
- const fields = withoutCompressedRangeFields(range, options.compressed === true);
4474
- if (options.includePreviews === false) {
4475
- const { preview: _preview, ...rest } = fields;
4476
- return rest;
4477
- }
4478
- return fields;
4479
- }
4480
- function truncatePreview(preview, maxLines) {
4481
- if (preview === void 0 || maxLines === void 0) return preview;
4482
- return preview.split("\n").slice(0, maxLines).join("\n");
4483
- }
4484
- function condenseDiffManifestFile(file) {
4485
- return {
4486
- path: file.path,
4487
- previousPath: file.previousPath,
4488
- status: file.status,
4489
- language: file.language,
4490
- additions: file.additions,
4491
- deletions: file.deletions,
4492
- hunks: file.hunks.map((hunk) => ({
4493
- hunkIndex: hunk.hunkIndex,
4494
- header: hunk.header,
4495
- oldStart: hunk.oldStart,
4496
- oldLines: hunk.oldLines,
4497
- newStart: hunk.newStart,
4498
- newLines: hunk.newLines,
4499
- contentHash: hunk.contentHash
4500
- })),
4501
- commentableRanges: file.commentableRanges.map((range) => ({
4502
- id: range.id,
4503
- path: range.path,
4504
- side: range.side,
4505
- startLine: range.startLine,
4506
- endLine: range.endLine,
4507
- kind: range.kind,
4508
- hunkIndex: range.hunkIndex,
4509
- hunkHeader: range.hunkHeader,
4510
- hunkContentHash: range.hunkContentHash
4511
- })),
4512
- excludedReason: file.excludedReason
4513
- };
4514
- }
4515
- function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
4516
- return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
4517
- }
4518
- //#endregion
4519
- //#region src/pi/runtime-tools.ts
4520
- const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
4521
- async function preparePiRuntimeReadTools(options) {
4522
- const toolRoot = path.join(options.root, "runtime-tools");
4523
- const baseRoot = path.join(toolRoot, "base");
4524
- await mkdir(baseRoot, { recursive: true });
4525
- const baseRanges = await materializeBaseRangeSnapshots({
4526
- baseRoot,
4527
- manifest: options.request.manifest,
4528
- sourceWorkspace: options.sourceWorkspace,
4529
- maxBytes: options.request.toolResponseMaxBytes
4530
- });
4531
- const data = {
4532
- manifest: options.request.manifest,
4533
- toolResponseMaxBytes: options.request.toolResponseMaxBytes,
4534
- baseRanges
4535
- };
4536
- const dataPath = path.join(toolRoot, "data.json");
4537
- await Bun.write(dataPath, JSON.stringify(data));
4538
- return {
4539
- extensionPath: await piRuntimeToolsExtensionPath(),
4540
- dataPath,
4541
- toolNames: piRuntimeReadToolNames
4542
- };
4543
- }
4544
- async function piRuntimeToolsExtensionPath() {
4545
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
4546
- const candidates = [
4547
- path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
4548
- path.join(moduleDir, "runtime-tools-extension.mjs"),
4549
- path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
4550
- path.join(moduleDir, "runtime-tools-extension.ts")
4551
- ];
4552
- for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
4553
- throw new Error("Unable to locate pipr runtime tools extension");
4554
- }
4555
- async function pathExists(filePath) {
4556
- return await Bun.file(filePath).exists();
5443
+ async function pathExists(filePath) {
5444
+ return await Bun.file(filePath).exists();
4557
5445
  }
4558
5446
  async function materializeBaseRangeSnapshots(options) {
4559
5447
  const ranges = {};
@@ -4746,7 +5634,27 @@ const ignoredWorkspacePaths = /* @__PURE__ */ new Set([
4746
5634
  ".fallow",
4747
5635
  "coverage"
4748
5636
  ]);
4749
- const eventRecordSchema = z.record(z.string(), z.unknown());
5637
+ const typedEventSchema = z.looseObject({ type: z.string() });
5638
+ const piJsonEventTypes = /* @__PURE__ */ new Set([
5639
+ "agent_start",
5640
+ "agent_end",
5641
+ "auto_retry_start",
5642
+ "auto_retry_end",
5643
+ "compaction_start",
5644
+ "compaction_end",
5645
+ "message_start",
5646
+ "message_update",
5647
+ "message_end",
5648
+ "queue_update",
5649
+ "session",
5650
+ "session_info_changed",
5651
+ "thinking_level_changed",
5652
+ "tool_execution_start",
5653
+ "tool_execution_update",
5654
+ "tool_execution_end",
5655
+ "turn_start",
5656
+ "turn_end"
5657
+ ]);
4750
5658
  const tokenCountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4751
5659
  const assistantMessageEventSchema = z.looseObject({
4752
5660
  type: z.literal("message_end"),
@@ -4764,6 +5672,14 @@ const assistantUsageMessageSchema = z.looseObject({
4764
5672
  cost: z.looseObject({ total: z.number().nonnegative() })
4765
5673
  })
4766
5674
  });
5675
+ const defaultPiStreamLimits = {
5676
+ maxJsonEventBytes: 16 * 1024 * 1024,
5677
+ maxRawStdoutBytes: 16 * 1024 * 1024,
5678
+ maxStderrBytes: 16 * 1024 * 1024
5679
+ };
5680
+ const maxRetainedModelCount = 64;
5681
+ const maxRetainedModelBytes = 64 * 1024;
5682
+ const processTerminationGraceMs = 250;
4767
5683
  async function runPi(options) {
4768
5684
  const started = Date.now();
4769
5685
  const sandbox = await createPiRunSandbox(options.workspace);
@@ -4780,19 +5696,13 @@ async function runPi(options) {
4780
5696
  const promptPath = path.join(sandbox.root, "prompt.md");
4781
5697
  await Bun.write(promptPath, options.prompt);
4782
5698
  const args = buildPiArgs(options.provider, `@${promptPath}`, sandbox.sessionDir, preparedTools, options.builtinTools);
4783
- const result = await runProcess(options.piExecutable ?? "pi", args, {
5699
+ return await runProcess(options.piExecutable ?? "pi", args, {
4784
5700
  cwd: sandbox.workspace,
4785
5701
  env: buildPiEnv(options.provider, sandbox, options.env, preparedTools),
4786
5702
  started,
4787
- timeoutSeconds: options.timeoutSeconds
5703
+ timeoutSeconds: options.timeoutSeconds,
5704
+ streamLimits: options.streamLimits ?? defaultPiStreamLimits
4788
5705
  });
4789
- const events = parsePiJsonEvents(result.stdout);
4790
- return {
4791
- ...result,
4792
- ...events?.models.length ? { models: events.models } : {},
4793
- ...events?.usage ? { usage: events.usage } : {},
4794
- stdout: result.exitCode === 0 ? events?.assistantText ?? result.stdout : result.stdout
4795
- };
4796
5706
  } finally {
4797
5707
  await preparedTools?.custom?.close();
4798
5708
  await chmodRecursive(sandbox.root, 493);
@@ -4914,69 +5824,195 @@ async function chmodRecursive(target, mode) {
4914
5824
  const entries = await readdir(target, { withFileTypes: true });
4915
5825
  for (const entry of entries) await chmodRecursive(path.join(target, entry.name), mode);
4916
5826
  }
4917
- function parsePiJsonEvents(stdout) {
4918
- const events = [];
4919
- for (const line of stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)) try {
4920
- const value = JSON.parse(line);
4921
- const parsed = eventRecordSchema.safeParse(value);
4922
- if (!parsed.success) return;
4923
- events.push(parsed.data);
4924
- } catch {
4925
- return;
5827
+ var PiOutputCollector = class {
5828
+ limits;
5829
+ mode = "undetermined";
5830
+ pending = "";
5831
+ pendingBytes = 0;
5832
+ rawOutput = "";
5833
+ rawOutputBytes = 0;
5834
+ failureReason;
5835
+ assistantText;
5836
+ models = [];
5837
+ modelSet = /* @__PURE__ */ new Set();
5838
+ modelBytes = 0;
5839
+ assistantMessageCount = 0;
5840
+ usageMessageCount = 0;
5841
+ inputTokens = 0;
5842
+ outputTokens = 0;
5843
+ costUsd = 0;
5844
+ usagePartial = false;
5845
+ stream = {
5846
+ rawStdoutBytes: 0,
5847
+ jsonEventCount: 0,
5848
+ largestEventBytes: 0,
5849
+ peakBufferedBytes: 0
5850
+ };
5851
+ constructor(limits) {
5852
+ this.limits = limits;
4926
5853
  }
4927
- if (!events.some((event) => typeof event.type === "string")) return;
4928
- let assistantText;
4929
- for (const event of events) assistantText = assistantTextFromEvent(event) ?? assistantText;
4930
- const assistantMessages = assistantMessagesFromEvents(events);
4931
- const models = assistantModels(assistantMessages);
4932
- const usage = assistantUsage(assistantMessages);
4933
- return {
4934
- ...assistantText !== void 0 ? { assistantText } : {},
4935
- models,
4936
- ...usage ? { usage } : {}
4937
- };
4938
- }
4939
- function assistantMessagesFromEvents(events) {
4940
- return events.flatMap((event) => {
5854
+ push(chunk) {
5855
+ this.stream.rawStdoutBytes += Buffer$1.byteLength(chunk, "utf8");
5856
+ if (this.failureReason) return this.failureReason;
5857
+ let offset = 0;
5858
+ while (offset < chunk.length && !this.failureReason) {
5859
+ if (this.mode === "raw") {
5860
+ this.appendRaw(chunk.slice(offset));
5861
+ break;
5862
+ }
5863
+ const newline = chunk.indexOf("\n", offset);
5864
+ const end = newline < 0 ? chunk.length : newline;
5865
+ this.appendPending(chunk.slice(offset, end));
5866
+ if (newline < 0 || this.failureReason) break;
5867
+ this.consumePending(true);
5868
+ offset = newline + 1;
5869
+ }
5870
+ return this.failureReason;
5871
+ }
5872
+ finish() {
5873
+ if (!this.failureReason && this.pending.length > 0) this.consumePending(false);
5874
+ if (this.failureReason) return {
5875
+ stdout: "",
5876
+ stream: this.stream
5877
+ };
5878
+ if (this.mode !== "json") return {
5879
+ stdout: this.rawOutput,
5880
+ stream: this.stream
5881
+ };
5882
+ return {
5883
+ stdout: this.assistantText ?? "",
5884
+ ...this.models.length > 0 ? { models: this.models } : {},
5885
+ ...this.usageMessageCount > 0 ? { usage: this.usage() } : {},
5886
+ stream: this.stream
5887
+ };
5888
+ }
5889
+ failure() {
5890
+ return this.failureReason;
5891
+ }
5892
+ appendPending(fragment) {
5893
+ const fragmentBytes = Buffer$1.byteLength(fragment, "utf8");
5894
+ const nextBytes = this.pendingBytes + fragmentBytes;
5895
+ if (nextBytes > (this.mode === "json" ? this.limits.maxJsonEventBytes : Math.max(this.limits.maxJsonEventBytes, this.limits.maxRawStdoutBytes))) {
5896
+ this.fail(this.mode === "json" ? "Pi JSON event exceeded the output limit" : "Pi stdout exceeded the output limit");
5897
+ return;
5898
+ }
5899
+ this.pending += fragment;
5900
+ this.pendingBytes = nextBytes;
5901
+ this.recordPeak(this.pendingBytes);
5902
+ }
5903
+ consumePending(terminated) {
5904
+ const source = `${this.pending}${terminated ? "\n" : ""}`;
5905
+ const line = this.pending.trim();
5906
+ const eventBytes = Buffer$1.byteLength(line, "utf8");
5907
+ this.pending = "";
5908
+ this.pendingBytes = 0;
5909
+ if (!line) {
5910
+ if (this.mode === "undetermined") this.appendRaw(source);
5911
+ return;
5912
+ }
5913
+ const event = parsePiEvent(line);
5914
+ if (this.mode === "undetermined") {
5915
+ if (!event || !piJsonEventTypes.has(event.type)) {
5916
+ this.mode = "raw";
5917
+ this.appendRaw(source);
5918
+ return;
5919
+ }
5920
+ this.mode = "json";
5921
+ this.rawOutput = "";
5922
+ this.rawOutputBytes = 0;
5923
+ }
5924
+ if (!event) {
5925
+ this.fail("Pi JSON output was malformed");
5926
+ return;
5927
+ }
5928
+ if (eventBytes > this.limits.maxJsonEventBytes) {
5929
+ this.fail("Pi JSON event exceeded the output limit");
5930
+ return;
5931
+ }
5932
+ this.stream.jsonEventCount += 1;
5933
+ this.stream.largestEventBytes = Math.max(this.stream.largestEventBytes, eventBytes);
5934
+ this.consumeEvent(event);
5935
+ }
5936
+ appendRaw(value) {
5937
+ const valueBytes = Buffer$1.byteLength(value, "utf8");
5938
+ const nextBytes = this.rawOutputBytes + valueBytes;
5939
+ if (nextBytes > this.limits.maxRawStdoutBytes) {
5940
+ this.fail("Pi raw stdout exceeded the output limit");
5941
+ return;
5942
+ }
5943
+ this.rawOutput += value;
5944
+ this.rawOutputBytes = nextBytes;
5945
+ this.recordPeak(this.rawOutputBytes);
5946
+ }
5947
+ fail(reason) {
5948
+ this.failureReason = reason;
5949
+ this.pending = "";
5950
+ this.pendingBytes = 0;
5951
+ this.rawOutput = "";
5952
+ this.rawOutputBytes = 0;
5953
+ this.assistantText = void 0;
5954
+ this.models.length = 0;
5955
+ this.modelSet.clear();
5956
+ this.modelBytes = 0;
5957
+ }
5958
+ consumeEvent(event) {
5959
+ this.assistantText = assistantTextFromEvent(event) ?? this.assistantText;
4941
5960
  const parsed = assistantMessageEventSchema.safeParse(event);
4942
- return parsed.success ? [parsed.data.message] : [];
4943
- });
4944
- }
4945
- function assistantModels(messages) {
4946
- const models = [];
4947
- for (const message of messages) {
5961
+ if (!parsed.success) return;
5962
+ const message = parsed.data.message;
5963
+ this.assistantMessageCount += 1;
4948
5964
  const model = message.responseModel ?? message.model;
4949
- if (model && !models.includes(model)) models.push(model);
5965
+ if (model) {
5966
+ this.addModel(model);
5967
+ if (this.failureReason) return;
5968
+ }
5969
+ const usage = assistantUsageMessageSchema.safeParse(message);
5970
+ if (!usage.success) return;
5971
+ this.usageMessageCount += 1;
5972
+ this.addUsage(usage.data);
4950
5973
  }
4951
- return models;
4952
- }
4953
- function assistantUsage(messages) {
4954
- const usageMessages = messages.flatMap((message) => {
4955
- const parsed = assistantUsageMessageSchema.safeParse(message);
4956
- return parsed.success ? [parsed.data] : [];
4957
- });
4958
- if (usageMessages.length === 0) return;
4959
- let inputTokens = 0;
4960
- let outputTokens = 0;
4961
- let costUsd = 0;
4962
- let partial = usageMessages.length !== messages.length;
4963
- for (const message of usageMessages) {
4964
- const nextInputTokens = inputTokens + message.usage.input;
4965
- if (Number.isSafeInteger(nextInputTokens)) inputTokens = nextInputTokens;
4966
- else partial = true;
4967
- const nextOutputTokens = outputTokens + message.usage.output;
4968
- if (Number.isSafeInteger(nextOutputTokens)) outputTokens = nextOutputTokens;
4969
- else partial = true;
4970
- const nextCostUsd = costUsd + message.usage.cost.total;
4971
- if (Number.isFinite(nextCostUsd)) costUsd = nextCostUsd;
4972
- else partial = true;
5974
+ addModel(model) {
5975
+ if (this.modelSet.has(model)) return;
5976
+ const modelBytes = Buffer$1.byteLength(model, "utf8");
5977
+ if (this.modelSet.size >= maxRetainedModelCount || this.modelBytes + modelBytes > maxRetainedModelBytes) {
5978
+ this.fail("Pi model metadata exceeded the output limit");
5979
+ return;
5980
+ }
5981
+ this.modelSet.add(model);
5982
+ this.models.push(model);
5983
+ this.modelBytes += modelBytes;
5984
+ }
5985
+ addUsage(message) {
5986
+ const nextInputTokens = this.inputTokens + message.usage.input;
5987
+ if (Number.isSafeInteger(nextInputTokens)) this.inputTokens = nextInputTokens;
5988
+ else this.usagePartial = true;
5989
+ const nextOutputTokens = this.outputTokens + message.usage.output;
5990
+ if (Number.isSafeInteger(nextOutputTokens)) this.outputTokens = nextOutputTokens;
5991
+ else this.usagePartial = true;
5992
+ const nextCostUsd = this.costUsd + message.usage.cost.total;
5993
+ if (Number.isFinite(nextCostUsd)) this.costUsd = nextCostUsd;
5994
+ else this.usagePartial = true;
5995
+ }
5996
+ usage() {
5997
+ return {
5998
+ status: this.usagePartial || this.usageMessageCount !== this.assistantMessageCount ? "partial" : "complete",
5999
+ inputTokens: this.inputTokens,
6000
+ outputTokens: this.outputTokens,
6001
+ costUsd: this.costUsd
6002
+ };
6003
+ }
6004
+ recordPeak(bytes) {
6005
+ this.stream.peakBufferedBytes = Math.max(this.stream.peakBufferedBytes, bytes);
6006
+ }
6007
+ };
6008
+ function parsePiEvent(line) {
6009
+ try {
6010
+ const parsed = typedEventSchema.safeParse(JSON.parse(line));
6011
+ if (!parsed.success) return;
6012
+ return parsed.data;
6013
+ } catch {
6014
+ return;
4973
6015
  }
4974
- return {
4975
- status: partial ? "partial" : "complete",
4976
- inputTokens,
4977
- outputTokens,
4978
- costUsd
4979
- };
4980
6016
  }
4981
6017
  function assistantTextFromEvent(event) {
4982
6018
  if (event.type === "message_end" || event.type === "turn_end") return assistantMessageText(event.message);
@@ -5006,7 +6042,9 @@ function textContent(content) {
5006
6042
  function runProcess(command, args, options) {
5007
6043
  return new Promise((resolve, reject) => {
5008
6044
  let timedOut = false;
6045
+ let streamFailure;
5009
6046
  let timeout;
6047
+ let terminationTimeout;
5010
6048
  const detached = process.platform !== "win32";
5011
6049
  const child = spawn(command, args, {
5012
6050
  cwd: options.cwd,
@@ -5018,34 +6056,85 @@ function runProcess(command, args, options) {
5018
6056
  "pipe"
5019
6057
  ]
5020
6058
  });
5021
- let stdout = "";
6059
+ const stdout = new PiOutputCollector(options.streamLimits);
5022
6060
  let stderr = "";
6061
+ let stderrBytes = 0;
5023
6062
  child.stdout.setEncoding("utf8");
5024
6063
  child.stderr.setEncoding("utf8");
5025
6064
  child.stdout.on("data", (chunk) => {
5026
- stdout += chunk;
6065
+ const failure = stdout.push(chunk);
6066
+ if (failure) failStream(failure);
5027
6067
  });
5028
6068
  child.stderr.on("data", (chunk) => {
6069
+ if (streamFailure || timedOut) return;
6070
+ const nextBytes = stderrBytes + Buffer$1.byteLength(chunk, "utf8");
6071
+ if (nextBytes > options.streamLimits.maxStderrBytes) {
6072
+ failStream("Pi stderr exceeded the output limit");
6073
+ return;
6074
+ }
5029
6075
  stderr += chunk;
6076
+ stderrBytes = nextBytes;
5030
6077
  });
6078
+ const failStream = (reason) => {
6079
+ if (streamFailure || timedOut) return;
6080
+ streamFailure = reason;
6081
+ stderr = "";
6082
+ stderrBytes = 0;
6083
+ if (timeout) {
6084
+ clearTimeout(timeout);
6085
+ timeout = void 0;
6086
+ }
6087
+ terminateProcessGroup();
6088
+ };
6089
+ const terminateProcessGroup = () => {
6090
+ killProcessGroup(child, "SIGTERM");
6091
+ terminationTimeout ??= setTimeout(() => {
6092
+ killProcessGroup(child, "SIGKILL");
6093
+ }, processTerminationGraceMs);
6094
+ };
5031
6095
  if (options.timeoutSeconds !== void 0) timeout = setTimeout(() => {
5032
6096
  timedOut = true;
5033
- killProcessGroup(child, "SIGTERM");
6097
+ terminateProcessGroup();
5034
6098
  }, options.timeoutSeconds * 1e3);
5035
6099
  child.on("error", reject);
5036
6100
  child.on("close", (exitCode) => {
5037
6101
  if (timeout) clearTimeout(timeout);
5038
- if (timedOut) stderr += `${stderr ? "\n" : ""}Pi timed out after ${options.timeoutSeconds}s`;
5039
- resolve({
5040
- stdout,
6102
+ resolve(finalizeProcessResult({
6103
+ collector: stdout,
5041
6104
  stderr,
5042
- exitCode: timedOut ? 124 : exitCode ?? 1,
6105
+ exitCode,
6106
+ timedOut,
6107
+ streamFailure,
6108
+ timeoutSeconds: options.timeoutSeconds,
5043
6109
  durationMs: Date.now() - options.started
5044
- });
6110
+ }));
5045
6111
  });
5046
6112
  });
5047
6113
  }
5048
- function killProcessGroup(child, signal) {
6114
+ function finalizeProcessResult(options) {
6115
+ const collected = options.collector.finish();
6116
+ const streamFailure = options.streamFailure ?? options.collector.failure();
6117
+ if (options.timedOut) return {
6118
+ ...collected,
6119
+ stderr: `${options.stderr ? `${options.stderr}\n` : ""}Pi timed out after ${options.timeoutSeconds}s`,
6120
+ exitCode: 124,
6121
+ durationMs: options.durationMs
6122
+ };
6123
+ if (streamFailure) return {
6124
+ stdout: "",
6125
+ stderr: streamFailure,
6126
+ exitCode: 1,
6127
+ durationMs: options.durationMs,
6128
+ ...collected.stream ? { stream: collected.stream } : {}
6129
+ };
6130
+ return {
6131
+ ...collected,
6132
+ stderr: options.stderr,
6133
+ exitCode: options.exitCode ?? 1,
6134
+ durationMs: options.durationMs
6135
+ };
6136
+ }
6137
+ function killProcessGroup(child, signal) {
5049
6138
  try {
5050
6139
  if (process.platform !== "win32" && child.pid) {
5051
6140
  process.kill(-child.pid, signal);
@@ -5057,19 +6146,13 @@ function killProcessGroup(child, signal) {
5057
6146
  }
5058
6147
  }
5059
6148
  //#endregion
5060
- //#region src/shared/redaction.ts
5061
- const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
5062
- function redactPotentialSecrets(value) {
5063
- return value.replace(secretLikeTokenPattern, "[redacted secret]");
5064
- }
5065
- //#endregion
5066
6149
  //#region src/shared/logging.ts
5067
6150
  const sensitiveEnvNamePattern = /(TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL|COOKIE)/i;
5068
- function createRuntimeActionLog(options) {
6151
+ function createRuntimeLog(options) {
5069
6152
  const secrets = /* @__PURE__ */ new Set();
5070
6153
  for (const [key, value] of Object.entries(options.env ?? process.env)) if (sensitiveEnvNamePattern.test(key)) addSecret(secrets, value);
5071
6154
  const debugEnabled = (options.env ?? process.env).ACTIONS_STEP_DEBUG === "true" || (options.env ?? process.env).PIPR_LOG_LEVEL === "debug";
5072
- const sink = options.logSink ?? noopActionLogSink;
6155
+ const sink = options.logSink ?? noopRuntimeLogSink;
5073
6156
  return {
5074
6157
  debugEnabled,
5075
6158
  writesToSink: options.logSink !== void 0,
@@ -5146,7 +6229,7 @@ function emitRecord(sink, secrets, level, event, fields, text) {
5146
6229
  text
5147
6230
  });
5148
6231
  }
5149
- const noopActionLogSink = {
6232
+ const noopRuntimeLogSink = {
5150
6233
  log() {},
5151
6234
  async group(_name, run) {
5152
6235
  return await run();
@@ -5671,7 +6754,8 @@ function logPiResult(options, provider, result, timeoutSeconds) {
5671
6754
  durationMs: result.durationMs,
5672
6755
  stdoutBytes: result.stdout.length,
5673
6756
  stderrBytes: result.stderr.length,
5674
- timeoutSeconds
6757
+ timeoutSeconds,
6758
+ ...result.stream ?? {}
5675
6759
  });
5676
6760
  }
5677
6761
  function builtinToolsForPrompt(toolMode) {
@@ -5756,10 +6840,63 @@ const mainCommentTitles = /* @__PURE__ */ new Set([
5756
6840
  "# Pipr Review",
5757
6841
  mainCommentTitle
5758
6842
  ]);
6843
+ const mainCommentHeaderHiddenMarker = "<!-- pipr:header:hidden -->";
6844
+ const mainCommentFooterHiddenMarker = "<!-- pipr:footer:hidden -->";
5759
6845
  const reviewStatsStartMarker = "<!-- pipr:stats:start -->";
5760
6846
  const reviewStatsEndMarker = "<!-- pipr:stats:end -->";
6847
+ const reviewStatsHiddenMarker = "<!-- pipr:stats:hidden -->";
5761
6848
  const escapedRepositoryUrl = piprRepositoryUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5762
6849
  const mainCommentAttributionPattern = new RegExp(`^<sub>Review generated by \\[Pipr\\]\\(${escapedRepositoryUrl}\\) for commit \`[^\`]+\`\\.(?: Config SDK \\d+\\.\\d+\\.\\d+ is behind (?:Pipr \\d+\\.\\d+\\.\\d+|\\[Pipr \\d+\\.\\d+\\.\\d+\\]\\(${escapedRepositoryUrl}/releases/tag/v\\d+\\.\\d+\\.\\d+\\))\\.)?</sub>$`);
6850
+ const maxReviewStatsModelLength = 200;
6851
+ const credentialLikeModelPattern = /(?:(?:AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{16,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})/i;
6852
+ const highEntropyModelSegmentPattern = /[A-Za-z0-9]{24,}/;
6853
+ const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
6854
+ const reviewStatsSchema = z.strictObject({
6855
+ models: z.array(reviewStatsModelSchema).min(1).max(20),
6856
+ agentRuns: z.number().int().positive(),
6857
+ durationMs: z.number().int().nonnegative(),
6858
+ inputTokens: z.number().int().nonnegative(),
6859
+ outputTokens: z.number().int().nonnegative(),
6860
+ costUsd: z.number().nonnegative(),
6861
+ usageStatus: z.enum([
6862
+ "complete",
6863
+ "partial",
6864
+ "unavailable"
6865
+ ])
6866
+ });
6867
+ function accumulateReviewStats(prior, current) {
6868
+ if (!prior) return current;
6869
+ if (!current) return prior;
6870
+ const inputTokens = addUsageTotal(prior.inputTokens, current.inputTokens, Number.isSafeInteger);
6871
+ const outputTokens = addUsageTotal(prior.outputTokens, current.outputTokens, Number.isSafeInteger);
6872
+ const costUsd = addUsageTotal(prior.costUsd, current.costUsd, Number.isFinite);
6873
+ const usageStatus = inputTokens.complete && outputTokens.complete && costUsd.complete && prior.usageStatus === current.usageStatus ? prior.usageStatus : "partial";
6874
+ return {
6875
+ models: [.../* @__PURE__ */ new Set([...prior.models, ...current.models])].slice(0, 20),
6876
+ agentRuns: Math.min(Number.MAX_SAFE_INTEGER, prior.agentRuns + current.agentRuns),
6877
+ durationMs: Math.min(Number.MAX_SAFE_INTEGER, prior.durationMs + current.durationMs),
6878
+ inputTokens: inputTokens.total,
6879
+ outputTokens: outputTokens.total,
6880
+ costUsd: costUsd.total,
6881
+ usageStatus
6882
+ };
6883
+ }
6884
+ function addUsageTotal(prior, current, isValid) {
6885
+ const total = prior + current;
6886
+ return isValid(total) && total >= 0 ? {
6887
+ total,
6888
+ complete: true
6889
+ } : {
6890
+ total: prior,
6891
+ complete: false
6892
+ };
6893
+ }
6894
+ function sanitizeReviewStatsModel(model) {
6895
+ const normalized = model.replace(/\s+/g, " ").trim();
6896
+ if (credentialLikeModelPattern.test(normalized) || highEntropyModelSegmentPattern.test(normalized)) return "[redacted credential]";
6897
+ const sanitized = redactPotentialSecrets(normalized);
6898
+ return sanitized ? sanitized.slice(0, maxReviewStatsModelLength) : void 0;
6899
+ }
5763
6900
  //#endregion
5764
6901
  //#region src/review/prior-state.ts
5765
6902
  const mainCommentMarker = "pipr:main-comment";
@@ -5785,7 +6922,8 @@ const priorReviewStateSchema = z.strictObject({
5785
6922
  version: z.literal(1),
5786
6923
  reviewedHeadSha: z.string().min(1),
5787
6924
  selectedTasks: z.array(z.string().min(1)),
5788
- findings: z.array(priorFindingRecordSchema)
6925
+ findings: z.array(priorFindingRecordSchema),
6926
+ stats: reviewStatsSchema.optional()
5789
6927
  });
5790
6928
  function buildPriorReviewState(options) {
5791
6929
  const scopedPriorState = priorReviewStateForSelectedTasks(options.priorState, options.selectedTasks);
@@ -5793,6 +6931,7 @@ function buildPriorReviewState(options) {
5793
6931
  const nextFindings = /* @__PURE__ */ new Map();
5794
6932
  const currentFindingIds = /* @__PURE__ */ new Set();
5795
6933
  const usedPriorIds = /* @__PURE__ */ new Set();
6934
+ const stats = accumulateReviewStats(scopedPriorState?.stats, options.stats);
5796
6935
  for (const finding of options.findings) {
5797
6936
  const id = selectFindingId({
5798
6937
  finding,
@@ -5825,7 +6964,8 @@ function buildPriorReviewState(options) {
5825
6964
  version: 1,
5826
6965
  reviewedHeadSha: options.reviewedHeadSha,
5827
6966
  selectedTasks: options.selectedTasks,
5828
- findings: cappedFindings([...nextFindings.values()], currentFindingIds)
6967
+ findings: cappedFindings([...nextFindings.values()], currentFindingIds),
6968
+ ...stats ? { stats } : {}
5829
6969
  };
5830
6970
  }
5831
6971
  function resolvePriorFindings(state, findingIds) {
@@ -6017,35 +7157,13 @@ function parseAttrs(input) {
6017
7157
  function hashParts(parts) {
6018
7158
  return new Bun.CryptoHasher("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
6019
7159
  }
6020
- const maxReviewStatsModelLength = 200;
6021
- const credentialLikeModelPattern = /(?:(?:AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{16,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})/i;
6022
- const highEntropyModelSegmentPattern = /[A-Za-z0-9]{24,}/;
6023
- const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
6024
- const reviewStatsSchema = z.strictObject({
6025
- models: z.array(reviewStatsModelSchema).min(1).max(20),
6026
- agentRuns: z.number().int().positive(),
6027
- durationMs: z.number().int().nonnegative(),
6028
- inputTokens: z.number().int().nonnegative(),
6029
- outputTokens: z.number().int().nonnegative(),
6030
- costUsd: z.number().nonnegative(),
6031
- usageStatus: z.enum([
6032
- "complete",
6033
- "partial",
6034
- "unavailable"
6035
- ])
6036
- });
6037
- function sanitizeReviewStatsModel(model) {
6038
- const normalized = model.replace(/\s+/g, " ").trim();
6039
- if (credentialLikeModelPattern.test(normalized) || highEntropyModelSegmentPattern.test(normalized)) return "[redacted credential]";
6040
- const sanitized = redactPotentialSecrets(normalized);
6041
- return sanitized ? sanitized.slice(0, maxReviewStatsModelLength) : void 0;
6042
- }
6043
7160
  //#endregion
6044
7161
  //#region src/review/comment.ts
6045
7162
  const inlinePublicationItemSchema = z.strictObject({
6046
7163
  finding: reviewFindingSchema,
6047
7164
  range: commentableRangeSchema,
6048
7165
  path: z.string().min(1),
7166
+ previousPath: z.string().min(1).optional(),
6049
7167
  side: reviewSideSchema,
6050
7168
  startLine: z.number().int().positive(),
6051
7169
  endLine: z.number().int().positive(),
@@ -6080,7 +7198,7 @@ const threadActionSchema = z.strictObject({
6080
7198
  kind: z.enum(["resolve", "reply"]),
6081
7199
  findingId: findingIdSchema,
6082
7200
  findingHeadSha: z.string().min(1),
6083
- commentId: z.number().int().positive(),
7201
+ commentId: z.string().min(1),
6084
7202
  threadId: z.string().min(1).optional(),
6085
7203
  body: z.string().min(1),
6086
7204
  responseKey: z.string().min(1)
@@ -6109,6 +7227,20 @@ const publicationPlanSchema = z.strictObject({
6109
7227
  reviewState: priorReviewStateSchema,
6110
7228
  threadActions: threadActionsSchema
6111
7229
  });
7230
+ function publicationPlanForHostCapabilities(plan, capabilities) {
7231
+ return {
7232
+ ...plan,
7233
+ inlineItems: plan.inlineItems.filter((item) => capabilities.multilineInlineComments || item.startLine === item.endLine).map((item) => {
7234
+ if (capabilities.suggestedChanges || !item.finding.suggestedFix) return item;
7235
+ const finding = withoutSuggestedFix(item.finding);
7236
+ return {
7237
+ ...item,
7238
+ finding,
7239
+ body: renderInlineBody(finding, item.findingId, item.reviewedHeadSha)
7240
+ };
7241
+ })
7242
+ };
7243
+ }
6112
7244
  function buildPublicationPlan(options) {
6113
7245
  const reviewState = options.reviewState ?? buildPriorReviewState({
6114
7246
  findings: options.inlineItems.map((item) => item.finding),
@@ -6125,7 +7257,10 @@ function buildPublicationPlan(options) {
6125
7257
  event: options.event,
6126
7258
  reviewState,
6127
7259
  main: options.main,
6128
- metadata
7260
+ metadata,
7261
+ showHeader: options.showHeader ?? true,
7262
+ showFooter: options.showFooter ?? true,
7263
+ showStats: options.showStats ?? true
6129
7264
  }),
6130
7265
  mainMarker: mainCommentMarker,
6131
7266
  changeNumber: options.event.change.number,
@@ -6138,19 +7273,21 @@ function buildPublicationPlan(options) {
6138
7273
  function preparePublishableInlineFindings(options) {
6139
7274
  const ranges = createDiffRangeIndex(options.manifest);
6140
7275
  return options.validated.validFindings.flatMap((finding) => {
6141
- const range = ranges.rangeById(finding.rangeId);
6142
- if (!range) throw new Error(`Validated finding range '${finding.rangeId}' is missing from Diff Manifest`);
7276
+ const match = ranges.findRange(finding.rangeId);
7277
+ if (!match) throw new Error(`Validated finding range '${finding.rangeId}' is missing from Diff Manifest`);
7278
+ const { file, range } = match;
6143
7279
  const findingWithBody = findingWithPublishableBody(finding);
6144
7280
  if (!findingWithBody) return [];
6145
7281
  return [{
6146
7282
  finding: findingWithPublishableSuggestedFix(findingWithBody, range),
6147
- range
7283
+ range,
7284
+ previousPath: file.previousPath
6148
7285
  }];
6149
7286
  });
6150
7287
  }
6151
7288
  function prepareInlinePublicationItemsForPublishableFindings(options) {
6152
7289
  const seenFindingIds = /* @__PURE__ */ new Set();
6153
- return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range }) => {
7290
+ return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath }) => {
6154
7291
  const findingId = findingIdFor(publishableFinding, options.reviewState);
6155
7292
  const stateRecord = options.reviewState ? matchFindingRecord(options.reviewState, publishableFinding) : void 0;
6156
7293
  if (seenFindingIds.has(findingId) || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
@@ -6160,6 +7297,7 @@ function prepareInlinePublicationItemsForPublishableFindings(options) {
6160
7297
  finding: publishableFinding,
6161
7298
  range,
6162
7299
  path: publishableFinding.path,
7300
+ previousPath,
6163
7301
  side: publishableFinding.side,
6164
7302
  startLine: publishableFinding.startLine,
6165
7303
  endLine: publishableFinding.endLine,
@@ -6209,14 +7347,14 @@ function renderMainComment(options) {
6209
7347
  reviewState: options.reviewState
6210
7348
  }),
6211
7349
  "",
6212
- mainCommentTitle,
6213
- "",
7350
+ ...!options.showHeader ? [mainCommentHeaderHiddenMarker, ""] : [],
7351
+ ...options.showHeader ? [mainCommentTitle, ""] : [],
6214
7352
  ...options.metadata.validFindings > 0 ? [`**Findings:** ${options.metadata.validFindings}`, ""] : [],
6215
7353
  redactPotentialSecrets(options.main),
6216
7354
  "",
6217
- ...options.metadata.stats ? [renderReviewStats(options.metadata.stats), ""] : [],
6218
- renderMainCommentAttribution(options.metadata),
6219
- ""
7355
+ ...!options.showStats || !options.metadata.stats ? [reviewStatsHiddenMarker, ""] : [],
7356
+ ...options.showStats && options.metadata.stats ? [renderReviewStats(options.metadata.stats), ""] : [],
7357
+ ...options.showFooter ? [renderMainCommentAttribution(options.metadata), ""] : [mainCommentFooterHiddenMarker, ""]
6220
7358
  ].join("\n");
6221
7359
  }
6222
7360
  function renderReviewStats(stats) {
@@ -6307,7 +7445,8 @@ function buildCommentPublishingPlan(options) {
6307
7445
  priorState: options.priorReviewState,
6308
7446
  findings: publishableInlineFindings.map((item) => item.finding),
6309
7447
  reviewedHeadSha: options.event.change.head.sha,
6310
- selectedTasks: options.metadata.selectedTasks
7448
+ selectedTasks: options.metadata.selectedTasks,
7449
+ stats: options.metadata.stats
6311
7450
  });
6312
7451
  const inlineCommentDrafts = prepareInlinePublicationItemsForPublishableFindings({
6313
7452
  publishableFindings: publishableInlineFindings,
@@ -6319,7 +7458,13 @@ function buildCommentPublishingPlan(options) {
6319
7458
  main: options.main,
6320
7459
  inlineItems: inlineCommentDrafts,
6321
7460
  maxInlineComments: options.maxInlineComments,
6322
- metadata: options.metadata,
7461
+ showHeader: options.showHeader,
7462
+ showFooter: options.showFooter,
7463
+ showStats: options.showStats,
7464
+ metadata: {
7465
+ ...options.metadata,
7466
+ ...reviewState.stats ? { stats: reviewState.stats } : {}
7467
+ },
6323
7468
  reviewState,
6324
7469
  threadActions: options.threadActions
6325
7470
  });
@@ -6555,8 +7700,12 @@ function internalVerifierAgent(provider, config) {
6555
7700
  "Do not repeat user-supplied text unless needed to explain the verdict.",
6556
7701
  config.publication.autoResolve.instructions
6557
7702
  ].filter(Boolean).join("\n"),
6558
- prompt: (input) => JSON.stringify(input, null, 2),
7703
+ prompt: (input) => {
7704
+ const { manifest: _manifest, ...verifierPromptInput } = input;
7705
+ return JSON.stringify(verifierPromptInput, null, 2);
7706
+ },
6559
7707
  tools: [],
7708
+ timeout: "2m",
6560
7709
  retry: {
6561
7710
  invalidOutput: 1,
6562
7711
  transientFailure: 0
@@ -6762,19 +7911,34 @@ function priorReviewForTask(priorMainComment, priorReviewState) {
6762
7911
  }
6763
7912
  function visibleMainComment(body) {
6764
7913
  const sourceLines = body.split("\n");
6765
- const statsRange = generatedReviewStatsRange(sourceLines);
6766
- const lines = sourceLines.filter((line, index) => {
6767
- return !(statsRange && index >= statsRange.start && index <= statsRange.end) && !line.startsWith("<!-- pipr:main-comment ") && !mainCommentAttributionPattern.test(line);
7914
+ const envelope = parseGeneratedMainCommentEnvelope(sourceLines);
7915
+ const lines = sourceLines.filter((_line, index) => {
7916
+ return !(envelope.statsRange && index >= envelope.statsRange.start && index <= envelope.statsRange.end) && index !== envelope.mainMarkerIndex && index !== envelope.headerMarkerIndex && index !== envelope.statsMarkerIndex && index !== envelope.footerIndex;
6768
7917
  });
6769
7918
  while (lines[0] === "") lines.shift();
6770
- if (lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
7919
+ if (envelope.headerMarkerIndex < 0 && lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
6771
7920
  while (lines[0] === "") lines.shift();
6772
7921
  return lines.join("\n").trim();
6773
7922
  }
6774
- function generatedReviewStatsRange(lines) {
6775
- const attributionIndex = lines.findLastIndex((line) => mainCommentAttributionPattern.test(line));
6776
- if (attributionIndex < 0) return;
6777
- const end = lines.slice(0, attributionIndex).findLastIndex((line) => line !== "");
7923
+ function parseGeneratedMainCommentEnvelope(lines) {
7924
+ const mainMarkerIndex = lines.findIndex((line) => line.startsWith("<!-- pipr:main-comment "));
7925
+ const headerCandidateOffset = lines.slice(mainMarkerIndex + 1).findIndex((line) => line !== "");
7926
+ const headerCandidateIndex = mainMarkerIndex + 1 + headerCandidateOffset;
7927
+ const headerMarkerIndex = mainMarkerIndex >= 0 && headerCandidateOffset >= 0 && lines[headerCandidateIndex] === "<!-- pipr:header:hidden -->" ? headerCandidateIndex : -1;
7928
+ const lastLineIndex = lines.findLastIndex((line) => line !== "");
7929
+ const lastLine = lines[lastLineIndex] ?? "";
7930
+ const footerIndex = lastLine === "<!-- pipr:footer:hidden -->" || mainCommentAttributionPattern.test(lastLine) ? lastLineIndex : -1;
7931
+ const lastContentIndex = lines.slice(0, footerIndex < 0 ? lines.length : footerIndex).findLastIndex((line) => line !== "");
7932
+ return {
7933
+ mainMarkerIndex,
7934
+ headerMarkerIndex,
7935
+ statsMarkerIndex: lines[lastContentIndex] === "<!-- pipr:stats:hidden -->" ? lastContentIndex : -1,
7936
+ statsRange: generatedReviewStatsRange(lines, footerIndex),
7937
+ footerIndex
7938
+ };
7939
+ }
7940
+ function generatedReviewStatsRange(lines, generatedFooterIndex) {
7941
+ const end = lines.slice(0, generatedFooterIndex < 0 ? lines.length : generatedFooterIndex).findLastIndex((line) => line !== "");
6778
7942
  if (end < 0) return;
6779
7943
  if (lines[end] !== "<!-- pipr:stats:end -->") return;
6780
7944
  if (lines[end - 1] !== "</details>") return;
@@ -6970,6 +8134,9 @@ async function runTaskRuntime(options) {
6970
8134
  validated,
6971
8135
  manifest: diffManifest,
6972
8136
  maxInlineComments: config.publication.maxInlineComments,
8137
+ showHeader: config.publication.showHeader,
8138
+ showFooter: config.publication.showFooter,
8139
+ showStats: config.publication.showStats,
6973
8140
  priorReviewState: verifier.priorReviewState,
6974
8141
  threadActions: verifier.threadActions,
6975
8142
  metadata: {
@@ -7092,107 +8259,1105 @@ function createTaskContext(options) {
7092
8259
  async reply(markdown) {
7093
8260
  collectCommandResponse(options.output, markdown, options.taskName);
7094
8261
  }
7095
- } : void 0,
7096
- secret(secret) {
7097
- return resolveTaskSecret(secret, options);
8262
+ } : void 0,
8263
+ secret(secret) {
8264
+ return resolveTaskSecret(secret, options);
8265
+ },
8266
+ pi: { async run(agent, input, runOptions) {
8267
+ const result = await runReviewAgent({
8268
+ agent,
8269
+ input,
8270
+ runOptions,
8271
+ runtime: {
8272
+ ...options,
8273
+ taskContext,
8274
+ runId: options.runId,
8275
+ piRunSink: options.piRunSink
8276
+ }
8277
+ });
8278
+ options.output.providerModels.push(...result.providerModels);
8279
+ if (result.repairAttempted) options.output.repairAttempted = true;
8280
+ trackResultFindingScope(options.output, result.value, runOptions?.paths);
8281
+ return agentOutputForTaskContext(agent, result.value);
8282
+ } },
8283
+ review: { async prior() {
8284
+ return priorReviewForTask(options.priorMainComment, options.priorReviewState);
8285
+ } },
8286
+ check: createCheckHandle(options.output),
8287
+ async comment(value) {
8288
+ collectComment(options.output, value, options.taskName);
8289
+ },
8290
+ log: options.taskLog ?? console
8291
+ };
8292
+ return taskContext;
8293
+ }
8294
+ function agentOutputForTaskContext(_agent, value) {
8295
+ return value;
8296
+ }
8297
+ function resolveTaskSecret(secret, options) {
8298
+ if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
8299
+ const value = (options.env ?? process.env)[secret.name];
8300
+ if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
8301
+ options.log?.addSecret(value);
8302
+ return value;
8303
+ }
8304
+ function commandResponseRuntimeResult(options) {
8305
+ return {
8306
+ kind: "command-response",
8307
+ provider: options.provider,
8308
+ diffManifest: options.diffManifest,
8309
+ taskChecks: options.taskChecks,
8310
+ repairAttempted: options.output.repairAttempted,
8311
+ commandResponse: {
8312
+ commandName: options.commandInvocation.name,
8313
+ line: options.commandInvocation.line,
8314
+ arguments: options.commandInvocation.arguments,
8315
+ body: options.commandResponse.value
8316
+ }
8317
+ };
8318
+ }
8319
+ function skippedTaskRuntimeResult(options) {
8320
+ const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
8321
+ const review = {
8322
+ summary: { body: reason },
8323
+ inlineFindings: []
8324
+ };
8325
+ const validated = {
8326
+ review,
8327
+ validFindings: [],
8328
+ droppedFindings: []
8329
+ };
8330
+ const publicationPlan = buildCommentPublishingPlan({
8331
+ event: options.event,
8332
+ main: reason,
8333
+ validated,
8334
+ manifest: options.diffManifest,
8335
+ maxInlineComments: options.config.publication.maxInlineComments,
8336
+ showHeader: options.config.publication.showHeader,
8337
+ showFooter: options.config.publication.showFooter,
8338
+ showStats: options.config.publication.showStats,
8339
+ metadata: {
8340
+ runtimeVersion,
8341
+ configVersion: options.versionCompatibility?.configVersion,
8342
+ trustedConfigSha: options.trustedConfigSha,
8343
+ trustedConfigHash: options.trustedConfigHash,
8344
+ reviewedHeadSha: options.event.change.head.sha,
8345
+ providerModels: [options.provider.model],
8346
+ selectedTasks: [],
8347
+ failedTasks: [],
8348
+ validFindings: 0,
8349
+ droppedFindings: 0
8350
+ }
8351
+ }).publicationPlan;
8352
+ return {
8353
+ kind: "skipped",
8354
+ skipReason: reason,
8355
+ provider: options.provider,
8356
+ diffManifest: options.diffManifest,
8357
+ review,
8358
+ validated,
8359
+ publicationPlan,
8360
+ mainComment: publicationPlan.mainComment,
8361
+ inlineCommentDrafts: [],
8362
+ taskChecks: [],
8363
+ repairAttempted: false
8364
+ };
8365
+ }
8366
+ //#endregion
8367
+ //#region src/hosts/env.ts
8368
+ function requiredHostEnv(env, name, host) {
8369
+ const value = env[name];
8370
+ if (!value) throw new Error(`${name} is required for ${host} events`);
8371
+ return value;
8372
+ }
8373
+ function positiveIntegerHostEnv(env, name, host) {
8374
+ const value = Number(requiredHostEnv(env, name, host));
8375
+ if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
8376
+ return value;
8377
+ }
8378
+ //#endregion
8379
+ //#region src/hosts/azure-devops/event.ts
8380
+ const repositorySchema = z.looseObject({
8381
+ id: z.string().min(1),
8382
+ name: z.string().min(1).optional(),
8383
+ project: z.looseObject({
8384
+ id: z.string().min(1),
8385
+ name: z.string().min(1)
8386
+ })
8387
+ });
8388
+ const serviceHookSchema = z.looseObject({
8389
+ id: z.string().min(1),
8390
+ eventType: z.string().min(1),
8391
+ resource: z.unknown(),
8392
+ resourceContainers: z.looseObject({
8393
+ account: z.looseObject({ baseUrl: z.string().url() }),
8394
+ project: z.looseObject({
8395
+ id: z.string().min(1),
8396
+ baseUrl: z.string().url()
8397
+ }).optional()
8398
+ })
8399
+ });
8400
+ const pullRequestResourceSchema = z.looseObject({
8401
+ pullRequestId: z.number().int().positive(),
8402
+ isDraft: z.boolean().optional(),
8403
+ repository: repositorySchema
8404
+ });
8405
+ const commentResourceSchema = z.looseObject({
8406
+ comment: z.looseObject({
8407
+ id: z.union([z.number(), z.string()]).transform(String),
8408
+ parentCommentId: z.number().int().nonnegative().default(0),
8409
+ content: z.string(),
8410
+ author: z.looseObject({ uniqueName: z.string().min(1) })
8411
+ }),
8412
+ pullRequest: pullRequestResourceSchema
8413
+ });
8414
+ async function parseAzureDevOpsEvent(options) {
8415
+ return options.eventPath ? await serviceHookEvent(options) : await pipelineEvent$2(options);
8416
+ }
8417
+ async function pipelineEvent$2(options) {
8418
+ const organization = organizationFromCollectionUri(requiredHostEnv(options.env, "SYSTEM_COLLECTIONURI", "Azure DevOps pipeline"));
8419
+ const loaded = await loadChange$1(options, {
8420
+ organization,
8421
+ project: requiredHostEnv(options.env, "SYSTEM_TEAMPROJECT", "Azure DevOps pipeline"),
8422
+ repositoryId: requiredHostEnv(options.env, "BUILD_REPOSITORY_ID", "Azure DevOps pipeline"),
8423
+ changeNumber: positiveIntegerHostEnv(options.env, "SYSTEM_PULLREQUEST_PULLREQUESTID", "Azure DevOps pipeline")
8424
+ });
8425
+ if (loaded.change.isDraft) return draftEvent$1();
8426
+ return changeRequestEvent(loaded, {
8427
+ eventName: "azure_pipeline",
8428
+ action: options.env.PIPR_CHANGE_ACTION ?? "updated",
8429
+ rawAction: options.env.PIPR_CHANGE_ACTION,
8430
+ host: `https://dev.azure.com/${organization}`,
8431
+ workspace: options.workspace
8432
+ });
8433
+ }
8434
+ async function serviceHookEvent(options) {
8435
+ const payload = await Bun.file(options.eventPath ?? "").json();
8436
+ const hook = serviceHookSchema.parse(payload);
8437
+ const organization = organizationFromCollectionUri(hook.resourceContainers.account.baseUrl);
8438
+ if (hook.eventType === "ms.vss-code.git-pullrequest-comment-event") {
8439
+ const resource = commentResourceSchema.parse(hook.resource);
8440
+ const repository = resource.pullRequest.repository;
8441
+ const common = {
8442
+ eventName: hook.eventType,
8443
+ action: "created",
8444
+ rawAction: hook.eventType,
8445
+ repository: {
8446
+ slug: `${organization}/${repository.project.name}/${repository.name ?? repository.id}`,
8447
+ url: azureRepositoryUrl(organization, repository.project.name, repository.name ?? repository.id)
8448
+ },
8449
+ changeNumber: resource.pullRequest.pullRequestId,
8450
+ commentId: resource.comment.id,
8451
+ body: resource.comment.content,
8452
+ actor: resource.comment.author.uniqueName,
8453
+ workspace: options.workspace
8454
+ };
8455
+ return resource.comment.parentCommentId > 0 ? {
8456
+ kind: "review-comment-reply",
8457
+ reply: {
8458
+ ...common,
8459
+ parentCommentId: String(resource.comment.parentCommentId)
8460
+ }
8461
+ } : {
8462
+ kind: "command-comment",
8463
+ comment: {
8464
+ ...common,
8465
+ isChangeRequest: true
8466
+ }
8467
+ };
8468
+ }
8469
+ if (hook.eventType === "git.pullrequest.created" || hook.eventType === "git.pullrequest.updated") {
8470
+ const resource = pullRequestResourceSchema.parse(hook.resource);
8471
+ return pullRequestHookEvent(options, organization, hook.eventType, resource);
8472
+ }
8473
+ throw new Error(`Unsupported Azure DevOps event '${hook.eventType}'`);
8474
+ }
8475
+ async function pullRequestHookEvent(options, organization, eventType, resource) {
8476
+ if (resource.isDraft) return draftEvent$1();
8477
+ return changeRequestEvent(await loadChange$1(options, {
8478
+ organization,
8479
+ project: resource.repository.project.name,
8480
+ repositoryId: resource.repository.id,
8481
+ changeNumber: resource.pullRequestId
8482
+ }), {
8483
+ eventName: eventType,
8484
+ action: eventType === "git.pullrequest.created" ? "opened" : "updated",
8485
+ rawAction: eventType,
8486
+ host: `https://dev.azure.com/${organization}`,
8487
+ workspace: options.workspace
8488
+ });
8489
+ }
8490
+ function draftEvent$1() {
8491
+ return {
8492
+ kind: "ignored",
8493
+ reason: "pull request is a draft"
8494
+ };
8495
+ }
8496
+ function changeRequestEvent(loaded, native) {
8497
+ return {
8498
+ kind: "change-request",
8499
+ change: parseChangeRequestEventContext({
8500
+ eventName: native.eventName,
8501
+ action: native.action,
8502
+ rawAction: native.rawAction,
8503
+ platform: {
8504
+ id: "azure-devops",
8505
+ host: native.host
8506
+ },
8507
+ repository: loaded.repository,
8508
+ coordinates: loaded.coordinates,
8509
+ change: loaded.change,
8510
+ workspace: native.workspace
8511
+ })
8512
+ };
8513
+ }
8514
+ async function loadChange$1(options, ref) {
8515
+ if (!options.loadChangeRequest) throw new Error("Azure DevOps pull request events require an API-backed change loader");
8516
+ return await options.loadChangeRequest(ref);
8517
+ }
8518
+ function organizationFromCollectionUri(value) {
8519
+ const organization = azureOrganizationFromUrl(value);
8520
+ if (!organization) throw new Error("Azure DevOps collection URI did not contain an organization");
8521
+ return organization;
8522
+ }
8523
+ function azureRepositoryUrl(organization, project, repository) {
8524
+ return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
8525
+ }
8526
+ //#endregion
8527
+ //#region src/review/publication-result.ts
8528
+ /** Error thrown when publication fails after producing partial result metadata. */
8529
+ var PublicationError = class extends Error {
8530
+ result;
8531
+ constructor(message, result) {
8532
+ super(message);
8533
+ this.result = result;
8534
+ }
8535
+ };
8536
+ //#endregion
8537
+ //#region src/hosts/publication.ts
8538
+ async function publishUnseenInlineItems(options) {
8539
+ const existing = new Set(extractInlineFindingMarkerRecords(options.existingBodies).map((record) => `${record.id}:${record.head}`));
8540
+ const errors = [];
8541
+ let posted = 0;
8542
+ let skipped = 0;
8543
+ for (const item of options.items) {
8544
+ if (existing.has(`${item.findingId}:${item.reviewedHeadSha}`)) {
8545
+ skipped += 1;
8546
+ continue;
8547
+ }
8548
+ try {
8549
+ await options.publish(item);
8550
+ posted += 1;
8551
+ } catch (error) {
8552
+ errors.push(error instanceof Error ? error.message : String(error));
8553
+ }
8554
+ }
8555
+ return {
8556
+ posted,
8557
+ skipped,
8558
+ errors
8559
+ };
8560
+ }
8561
+ function commandResponseBody(options) {
8562
+ const marker = `<!-- pipr:command-response change=${options.changeNumber} source=${options.sourceCommentId} command=${options.commandName} -->`;
8563
+ return {
8564
+ marker,
8565
+ body: [
8566
+ marker,
8567
+ "",
8568
+ options.body,
8569
+ ""
8570
+ ].join("\n")
8571
+ };
8572
+ }
8573
+ function completeHostPublication(options) {
8574
+ const partial = {
8575
+ inlineComments: {
8576
+ posted: options.inline.posted,
8577
+ skipped: options.inline.skipped,
8578
+ failed: options.inline.errors.length
8579
+ },
8580
+ metadata: {
8581
+ ...options.metadata,
8582
+ inlinePublicationErrors: options.inline.errors,
8583
+ inlineResolutionErrors: options.resolutionErrors
8584
+ }
8585
+ };
8586
+ if (options.inline.errors.length > 0) throw new PublicationError(`${options.provider} inline comment publication failed`, partial);
8587
+ return {
8588
+ mainComment: {
8589
+ action: options.mainAction,
8590
+ id: options.mainId
8591
+ },
8592
+ ...partial
8593
+ };
8594
+ }
8595
+ //#endregion
8596
+ //#region src/hosts/azure-devops/publication.ts
8597
+ async function publishAzureDevOpsPlan(options) {
8598
+ const coordinates = azureCoordinates$1(options.change);
8599
+ const native = await currentNativeChange(options.client, options.change);
8600
+ const owner = await options.client.currentUser();
8601
+ const threads = await options.client.listThreads(coordinates.repositoryId, options.change.change.number);
8602
+ const existingMain = ownedRootThread(threads, owner.uniqueName, mainMarker$2(options.change.change.number));
8603
+ const main = existingMain ? await options.client.updateComment(coordinates.repositoryId, options.change.change.number, existingMain.id, existingMain.comments[0]?.id ?? "", options.plan.mainComment) : (await options.client.createThread(coordinates.repositoryId, options.change.change.number, unpositionedThread(options.plan.mainComment))).comments[0];
8604
+ if (!main) throw new Error("Azure DevOps did not return the Main Review Comment");
8605
+ const markerBodies = ownedThreadComments(threads, owner.uniqueName).map((comment) => comment.content);
8606
+ const changes = await options.client.listIterationChanges(coordinates.repositoryId, options.change.change.number, native.iterationId);
8607
+ const inline = await publishUnseenInlineItems({
8608
+ items: options.plan.inlineItems,
8609
+ existingBodies: markerBodies,
8610
+ publish: async (item) => {
8611
+ await options.client.createThread(coordinates.repositoryId, options.change.change.number, await inlineThread(options.change, item, changes, native.iterationId));
8612
+ }
8613
+ });
8614
+ const resolution = await publishAzureDevOpsThreadActions({
8615
+ client: options.client,
8616
+ change: options.change,
8617
+ actions: options.plan.threadActions,
8618
+ reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
8619
+ threads
8620
+ });
8621
+ return completeHostPublication({
8622
+ provider: "Azure DevOps",
8623
+ mainAction: existingMain ? "updated" : "created",
8624
+ mainId: main.id,
8625
+ inline,
8626
+ resolutionErrors: resolution.errors,
8627
+ metadata: options.plan.metadata
8628
+ });
8629
+ }
8630
+ async function publishAzureDevOpsCommandResponse(options) {
8631
+ const coordinates = azureCoordinates$1(options.change);
8632
+ const response = commandResponseBody({
8633
+ changeNumber: options.change.change.number,
8634
+ sourceCommentId: options.sourceCommentId,
8635
+ commandName: options.commandName,
8636
+ body: options.body
8637
+ });
8638
+ await currentPullRequest(options.client, options.change);
8639
+ const owner = await options.client.currentUser();
8640
+ const existing = ownedRootThread(await options.client.listThreads(coordinates.repositoryId, options.change.change.number), owner.uniqueName, response.marker);
8641
+ const comment = existing ? await options.client.updateComment(coordinates.repositoryId, options.change.change.number, existing.id, existing.comments[0]?.id ?? "", response.body) : (await options.client.createThread(coordinates.repositoryId, options.change.change.number, unpositionedThread(response.body))).comments[0];
8642
+ if (!comment) throw new Error("Azure DevOps did not return the command response comment");
8643
+ return {
8644
+ action: existing ? "updated" : "created",
8645
+ id: comment.id
8646
+ };
8647
+ }
8648
+ async function loadAzureDevOpsPriorReviewState(options) {
8649
+ const state = extractPriorReviewState(await loadAzureDevOpsPriorMainComment(options), options.change.change.number);
8650
+ if (!state) return void 0;
8651
+ const owner = await options.client.currentUser();
8652
+ const bodies = ownedThreadComments(await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number), owner.uniqueName).map((comment) => comment.content);
8653
+ return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
8654
+ }
8655
+ async function loadAzureDevOpsPriorMainComment(options) {
8656
+ const owner = await options.client.currentUser();
8657
+ return ownedRootThread(await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number), owner.uniqueName, mainMarker$2(options.change.change.number))?.comments[0]?.content;
8658
+ }
8659
+ async function loadAzureDevOpsInlineThreadContexts(options) {
8660
+ const owner = await options.client.currentUser();
8661
+ return (await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number)).flatMap((thread) => {
8662
+ const root = thread.comments[0];
8663
+ const marker = root ? extractInlineFindingMarkerRecords([root.content])[0] : void 0;
8664
+ if (!root || !marker || root.author?.uniqueName !== owner.uniqueName) return [];
8665
+ return [{
8666
+ findingId: marker.id,
8667
+ findingHeadSha: marker.head,
8668
+ parentCommentId: root.id,
8669
+ parentBody: root.content,
8670
+ threadId: thread.id,
8671
+ threadResolved: isResolved(thread),
8672
+ comments: thread.comments.map((comment) => ({
8673
+ id: comment.id,
8674
+ body: comment.content,
8675
+ authorLogin: comment.author?.uniqueName
8676
+ }))
8677
+ }];
8678
+ });
8679
+ }
8680
+ async function publishAzureDevOpsThreadActions(options) {
8681
+ if (options.actions.length === 0) return { errors: [] };
8682
+ const coordinates = azureCoordinates$1(options.change);
8683
+ await currentNativeChange(options.client, options.change, options.reviewedHeadSha);
8684
+ const threads = options.threads ?? await options.client.listThreads(coordinates.repositoryId, options.change.change.number);
8685
+ const byComment = new Map(threads.flatMap((thread) => thread.comments.map((comment) => [comment.id, thread])));
8686
+ const errors = [];
8687
+ for (const action of options.actions) {
8688
+ const thread = action.threadId ? threads.find((candidate) => candidate.id === action.threadId) : byComment.get(action.commentId);
8689
+ const error = await publishAzureDevOpsThreadAction({
8690
+ client: options.client,
8691
+ repositoryId: coordinates.repositoryId,
8692
+ changeNumber: options.change.change.number,
8693
+ action,
8694
+ thread
8695
+ });
8696
+ if (error) errors.push(error);
8697
+ }
8698
+ return { errors };
8699
+ }
8700
+ async function publishAzureDevOpsThreadAction(options) {
8701
+ if (!options.thread) return `Azure DevOps thread not found for comment ${options.action.commentId}`;
8702
+ try {
8703
+ if (!options.thread.comments.some((comment) => comment.content.includes(options.action.responseKey))) await options.client.createThreadComment(options.repositoryId, options.changeNumber, options.thread.id, {
8704
+ parentCommentId: Number(options.thread.comments[0]?.id ?? 0),
8705
+ content: options.action.body,
8706
+ commentType: 1
8707
+ });
8708
+ if (options.action.kind === "resolve" && !isResolved(options.thread)) await options.client.updateThreadStatus(options.repositoryId, options.changeNumber, options.thread.id, "fixed");
8709
+ } catch (error) {
8710
+ return error instanceof Error ? error.message : String(error);
8711
+ }
8712
+ }
8713
+ async function inlineThread(change, item, changes, iterationId) {
8714
+ const selectedPath = item.side === "LEFT" ? item.previousPath ?? item.path : item.path;
8715
+ const nativeChange = changes.find((candidate) => {
8716
+ if (!(candidate.path === selectedPath || candidate.originalPath === selectedPath)) return false;
8717
+ const changeType = candidate.changeType.toLowerCase();
8718
+ return item.side === "LEFT" ? changeType !== "add" : changeType !== "delete";
8719
+ });
8720
+ if (!nativeChange) throw new Error(`Azure DevOps changeTrackingId not found for ${selectedPath}`);
8721
+ const start = {
8722
+ line: item.startLine,
8723
+ offset: 1
8724
+ };
8725
+ const end = {
8726
+ line: item.endLine,
8727
+ offset: await lineEndOffset(change, selectedPath, item.endLine, item.side)
8728
+ };
8729
+ return {
8730
+ comments: [{
8731
+ parentCommentId: 0,
8732
+ content: item.body,
8733
+ commentType: 1
8734
+ }],
8735
+ status: "active",
8736
+ threadContext: {
8737
+ filePath: `/${selectedPath.replace(/^\/+/, "")}`,
8738
+ ...item.side === "RIGHT" ? {
8739
+ rightFileStart: start,
8740
+ rightFileEnd: end
8741
+ } : {
8742
+ leftFileStart: start,
8743
+ leftFileEnd: end
8744
+ }
8745
+ },
8746
+ pullRequestThreadContext: {
8747
+ changeTrackingId: nativeChange.changeTrackingId,
8748
+ iterationContext: {
8749
+ firstComparingIteration: 1,
8750
+ secondComparingIteration: iterationId
8751
+ }
8752
+ }
8753
+ };
8754
+ }
8755
+ async function lineEndOffset(change, filePath, line, side) {
8756
+ const root = path.resolve(change.workspace);
8757
+ const sha = side === "RIGHT" ? change.change.head.sha : change.change.base.sha;
8758
+ try {
8759
+ const result = Bun.spawnSync([
8760
+ "git",
8761
+ "show",
8762
+ `${sha}:${filePath}`
8763
+ ], { cwd: root });
8764
+ if (result.exitCode !== 0) return 1;
8765
+ const content = result.stdout.toString().split(/\r?\n/)[line - 1];
8766
+ return content === void 0 ? 1 : content.length + 1;
8767
+ } catch {
8768
+ return 1;
8769
+ }
8770
+ }
8771
+ async function currentNativeChange(client, change, reviewedHeadSha = change.change.head.sha) {
8772
+ const pullRequest = await currentPullRequest(client, change, reviewedHeadSha);
8773
+ const coordinates = azureCoordinates$1(change);
8774
+ const iteration = (await client.listIterations(coordinates.repositoryId, change.change.number)).findLast((candidate) => candidate.headSha === reviewedHeadSha);
8775
+ if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${reviewedHeadSha}`);
8776
+ return {
8777
+ pullRequest,
8778
+ iterationId: iteration.id
8779
+ };
8780
+ }
8781
+ async function currentPullRequest(client, change, reviewedHeadSha = change.change.head.sha) {
8782
+ const coordinates = azureCoordinates$1(change);
8783
+ const pullRequest = await client.getPullRequest(coordinates.repositoryId, change.change.number);
8784
+ if (pullRequest.lastMergeSourceCommit.commitId !== reviewedHeadSha) throw new Error(`Azure DevOps pull request head changed from ${reviewedHeadSha} to ${pullRequest.lastMergeSourceCommit.commitId}`);
8785
+ if (pullRequest.lastMergeTargetCommit.commitId !== change.change.base.sha) throw new Error(`Azure DevOps pull request base changed from ${change.change.base.sha} to ${pullRequest.lastMergeTargetCommit.commitId}`);
8786
+ return pullRequest;
8787
+ }
8788
+ function azureCoordinates$1(change) {
8789
+ if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
8790
+ return change.coordinates;
8791
+ }
8792
+ function ownedRootThread(threads, uniqueName, marker) {
8793
+ return threads.find((thread) => {
8794
+ const root = thread.comments[0];
8795
+ return !thread.threadContext?.filePath && root?.author?.uniqueName === uniqueName && root.content.trimStart().startsWith(marker);
8796
+ });
8797
+ }
8798
+ function ownedThreadComments(threads, uniqueName) {
8799
+ return threads.flatMap((thread) => thread.comments.filter((comment) => comment.author?.uniqueName === uniqueName));
8800
+ }
8801
+ function unpositionedThread(content) {
8802
+ return {
8803
+ comments: [{
8804
+ parentCommentId: 0,
8805
+ content,
8806
+ commentType: 1
8807
+ }],
8808
+ status: "active"
8809
+ };
8810
+ }
8811
+ function isResolved(thread) {
8812
+ return thread.status === "fixed" || thread.status === "closed" || thread.status === "wontFix" || thread.status === "byDesign";
8813
+ }
8814
+ function mainMarker$2(changeNumber) {
8815
+ return `<!-- pipr:main-comment change=${changeNumber} `;
8816
+ }
8817
+ //#endregion
8818
+ //#region src/hosts/git.ts
8819
+ async function ensureCodeHostHeadCheckout(options) {
8820
+ if (!await hasGitCommit(options.rootDir, options.headSha)) {
8821
+ const remote = options.fetchRemote ?? "origin";
8822
+ try {
8823
+ await fetchGit(options.rootDir, remote, options.headSha, options.fetchEnv);
8824
+ } catch {
8825
+ await fetchGit(options.rootDir, remote, options.fetchRef, options.fetchEnv);
8826
+ }
8827
+ if (!await hasGitCommit(options.rootDir, options.headSha)) throw new Error(`Code host did not provide reviewed commit ${options.headSha} from ${options.fetchRef}`);
8828
+ }
8829
+ if ((await runGit(options.rootDir, ["rev-parse", "HEAD"])).trim() !== options.headSha) await runGit(options.rootDir, [
8830
+ "checkout",
8831
+ "--detach",
8832
+ options.headSha
8833
+ ]);
8834
+ }
8835
+ async function fetchGit(rootDir, remote, ref, env) {
8836
+ await runGit(rootDir, [
8837
+ "fetch",
8838
+ "--no-tags",
8839
+ "--depth=1",
8840
+ remote,
8841
+ ref
8842
+ ], env);
8843
+ }
8844
+ async function hasGitCommit(rootDir, sha) {
8845
+ try {
8846
+ await runGit(rootDir, [
8847
+ "cat-file",
8848
+ "-e",
8849
+ `${sha}^{commit}`
8850
+ ]);
8851
+ return true;
8852
+ } catch {
8853
+ return false;
8854
+ }
8855
+ }
8856
+ async function runGit(rootDir, args, env) {
8857
+ const child = Bun.spawn(["git", ...args], {
8858
+ cwd: rootDir,
8859
+ env: {
8860
+ ...process.env,
8861
+ ...env
8862
+ },
8863
+ stderr: "pipe",
8864
+ stdout: "pipe"
8865
+ });
8866
+ const [exitCode, stdout, stderr] = await Promise.all([
8867
+ child.exited,
8868
+ new Response(child.stdout).text(),
8869
+ new Response(child.stderr).text()
8870
+ ]);
8871
+ if (exitCode !== 0) throw new Error(stderr.trim() || `git ${args.join(" ")} failed`);
8872
+ return stdout;
8873
+ }
8874
+ //#endregion
8875
+ //#region src/hosts/azure-devops/workspace.ts
8876
+ function ensureAzureDevOpsHeadCheckout(options) {
8877
+ const headRef = options.change.change.head.ref;
8878
+ if (!headRef) throw new Error("Azure DevOps pull request source ref is required for checkout");
8879
+ return ensureCodeHostHeadCheckout({
8880
+ rootDir: options.rootDir,
8881
+ headSha: options.change.change.head.sha,
8882
+ fetchRef: headRef.startsWith("refs/") ? headRef : `refs/heads/${headRef}`,
8883
+ fetchRemote: options.change.change.isFork ? options.change.change.head.url : void 0
8884
+ });
8885
+ }
8886
+ //#endregion
8887
+ //#region src/hosts/azure-devops/adapter.ts
8888
+ function createAzureDevOpsHostAdapter(options = {}) {
8889
+ const client = options.client ?? createAzureDevOpsClient(options.env);
8890
+ return {
8891
+ id: "azure-devops",
8892
+ capabilities: {
8893
+ commandComments: true,
8894
+ reviewCommentReplies: true,
8895
+ threadResolution: true,
8896
+ multilineInlineComments: true,
8897
+ suggestedChanges: true,
8898
+ statuses: true
8899
+ },
8900
+ events: {
8901
+ parseEvent(parseOptions) {
8902
+ return parseAzureDevOpsEvent({
8903
+ ...parseOptions,
8904
+ loadChangeRequest: (ref) => client.loadChange(ref)
8905
+ });
8906
+ },
8907
+ loadChangeRequest(ref) {
8908
+ const coordinates = azureCoordinatesFromRepository(client, ref.repository.slug);
8909
+ return client.loadChange({
8910
+ ...coordinates,
8911
+ changeNumber: ref.changeNumber
8912
+ }).then((loaded) => ({
8913
+ ...loaded,
8914
+ eventName: ref.eventName,
8915
+ action: ref.action,
8916
+ rawAction: ref.rawAction,
8917
+ workspace: ref.workspace
8918
+ }));
8919
+ }
7098
8920
  },
7099
- pi: { async run(agent, input, runOptions) {
7100
- const result = await runReviewAgent({
7101
- agent,
7102
- input,
7103
- runOptions,
7104
- runtime: {
7105
- ...options,
7106
- taskContext,
7107
- runId: options.runId,
7108
- piRunSink: options.piRunSink
7109
- }
7110
- });
7111
- options.output.providerModels.push(...result.providerModels);
7112
- if (result.repairAttempted) options.output.repairAttempted = true;
7113
- trackResultFindingScope(options.output, result.value, runOptions?.paths);
7114
- return agentOutputForTaskContext(agent, result.value);
7115
- } },
7116
- review: { async prior() {
7117
- return priorReviewForTask(options.priorMainComment, options.priorReviewState);
8921
+ workspace: { ensureHeadCheckout: ensureAzureDevOpsHeadCheckout },
8922
+ permissions: { getRepositoryPermission({ change, actor }) {
8923
+ const coordinates = azureCoordinates(change);
8924
+ if (!coordinates.projectId) throw new Error("Azure DevOps projectId is required for permission checks");
8925
+ return client.getRepositoryPermission(actor, coordinates.projectId, coordinates.repositoryId);
7118
8926
  } },
7119
- check: createCheckHandle(options.output),
7120
- async comment(value) {
7121
- collectComment(options.output, value, options.taskName);
8927
+ publication: {
8928
+ publish: ({ plan, change }) => publishAzureDevOpsPlan({
8929
+ client,
8930
+ plan,
8931
+ change
8932
+ }),
8933
+ publishCommandResponse: (args) => publishAzureDevOpsCommandResponse({
8934
+ client,
8935
+ ...args
8936
+ }),
8937
+ publishThreadActions: (args) => publishAzureDevOpsThreadActions({
8938
+ client,
8939
+ ...args
8940
+ })
7122
8941
  },
7123
- log: options.taskLog ?? console
8942
+ comments: {
8943
+ loadPriorReviewState: ({ change }) => loadAzureDevOpsPriorReviewState({
8944
+ client,
8945
+ change
8946
+ }),
8947
+ loadPriorMainComment: ({ change }) => loadAzureDevOpsPriorMainComment({
8948
+ client,
8949
+ change
8950
+ }),
8951
+ loadInlineThreadContexts: ({ change }) => loadAzureDevOpsInlineThreadContexts({
8952
+ client,
8953
+ change
8954
+ })
8955
+ },
8956
+ statuses: {
8957
+ isAvailable: () => true,
8958
+ async upsert({ change, name, state, summary, status }) {
8959
+ const coordinates = azureCoordinates(change);
8960
+ const pullRequest = await client.getPullRequest(coordinates.repositoryId, change.change.number);
8961
+ if (pullRequest.lastMergeSourceCommit.commitId !== change.change.head.sha || pullRequest.lastMergeTargetCommit.commitId !== change.change.base.sha) throw new Error("Azure DevOps pull request endpoints changed before status publication");
8962
+ const iteration = (await client.listIterations(coordinates.repositoryId, change.change.number)).findLast((candidate) => candidate.headSha === change.change.head.sha);
8963
+ if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${change.change.head.sha}`);
8964
+ const id = await client.createStatus(coordinates.repositoryId, change.change.number, {
8965
+ state: azureDevOpsStatusState(state),
8966
+ description: summary?.slice(0, 1e3),
8967
+ context: {
8968
+ genre: "pipr",
8969
+ name: `pipr/${name}`
8970
+ },
8971
+ iterationId: iteration.id
8972
+ });
8973
+ return status ?? {
8974
+ id,
8975
+ name
8976
+ };
8977
+ }
8978
+ }
7124
8979
  };
7125
- return taskContext;
7126
8980
  }
7127
- function agentOutputForTaskContext(_agent, value) {
7128
- return value;
8981
+ function azureCoordinates(change) {
8982
+ if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
8983
+ return change.coordinates;
7129
8984
  }
7130
- function resolveTaskSecret(secret, options) {
7131
- if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
7132
- const value = (options.env ?? process.env)[secret.name];
7133
- if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
7134
- options.log?.addSecret(value);
7135
- return value;
8985
+ function azureCoordinatesFromRepository(client, slug) {
8986
+ const repositoryId = slug.split("/").at(-1);
8987
+ if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
8988
+ return {
8989
+ organization: client.organization,
8990
+ project: client.project,
8991
+ repositoryId
8992
+ };
7136
8993
  }
7137
- function commandResponseRuntimeResult(options) {
8994
+ //#endregion
8995
+ //#region src/hosts/bitbucket/event.ts
8996
+ const webhookSchema = z.looseObject({
8997
+ actor: z.looseObject({ nickname: z.string().min(1) }),
8998
+ repository: bitbucketRepositorySchema,
8999
+ pullrequest: z.looseObject({
9000
+ id: z.number().int().positive(),
9001
+ draft: z.boolean().optional()
9002
+ }),
9003
+ comment: z.looseObject({
9004
+ id: z.union([z.number(), z.string()]).transform(String),
9005
+ content: z.looseObject({ raw: z.string().default("") }),
9006
+ parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
9007
+ inline: z.unknown().optional()
9008
+ }).optional()
9009
+ });
9010
+ async function parseBitbucketEvent(options) {
9011
+ return options.eventPath ? await webhookEvent(options) : await pipelineEvent$1(options);
9012
+ }
9013
+ async function pipelineEvent$1(options) {
9014
+ const workspace = requiredHostEnv(options.env, "BITBUCKET_WORKSPACE", "Bitbucket");
9015
+ const repository = requiredHostEnv(options.env, "BITBUCKET_REPO_SLUG", "Bitbucket");
9016
+ const changeNumber = positiveIntegerHostEnv(options.env, "BITBUCKET_PR_ID", "Bitbucket");
9017
+ const loaded = await options.loadChangeRequest({
9018
+ workspace,
9019
+ repository,
9020
+ changeNumber
9021
+ });
9022
+ if (loaded.change.isDraft) return draftEvent();
7138
9023
  return {
7139
- kind: "command-response",
7140
- provider: options.provider,
7141
- diffManifest: options.diffManifest,
7142
- taskChecks: options.taskChecks,
7143
- repairAttempted: options.output.repairAttempted,
7144
- commandResponse: {
7145
- commandName: options.commandInvocation.name,
7146
- line: options.commandInvocation.line,
7147
- arguments: options.commandInvocation.arguments,
7148
- body: options.commandResponse.value
9024
+ kind: "change-request",
9025
+ change: {
9026
+ eventName: "bitbucket_pipeline",
9027
+ action: options.env.PIPR_CHANGE_ACTION ?? "updated",
9028
+ rawAction: options.env.PIPR_CHANGE_ACTION,
9029
+ platform: {
9030
+ id: "bitbucket",
9031
+ host: "https://bitbucket.org"
9032
+ },
9033
+ repository: loaded.repository,
9034
+ coordinates: loaded.coordinates,
9035
+ change: loaded.change,
9036
+ workspace: options.workspace
7149
9037
  }
7150
9038
  };
7151
9039
  }
7152
- function skippedTaskRuntimeResult(options) {
7153
- const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
7154
- const review = {
7155
- summary: { body: reason },
7156
- inlineFindings: []
9040
+ async function webhookEvent(options) {
9041
+ const hook = webhookSchema.parse(await Bun.file(options.eventPath ?? "").json());
9042
+ const eventKey = requiredHostEnv(options.env, "BITBUCKET_EVENT_KEY", "Bitbucket");
9043
+ if (eventKey === "pullrequest:comment_created") {
9044
+ if (!hook.comment) throw new Error("Bitbucket comment event payload is missing comment");
9045
+ return commentEvent(hook, hook.comment, eventKey, options.workspace);
9046
+ }
9047
+ return await pullRequestEvent(options, hook, eventKey);
9048
+ }
9049
+ function commentEvent(hook, comment, eventKey, workspace) {
9050
+ const common = {
9051
+ eventName: eventKey,
9052
+ action: "created",
9053
+ rawAction: eventKey,
9054
+ repository: {
9055
+ slug: hook.repository.full_name,
9056
+ url: hook.repository.links.html.href
9057
+ },
9058
+ changeNumber: hook.pullrequest.id,
9059
+ commentId: comment.id,
9060
+ body: comment.content.raw,
9061
+ actor: hook.actor.nickname,
9062
+ workspace
7157
9063
  };
7158
- const validated = {
7159
- review,
7160
- validFindings: [],
7161
- droppedFindings: []
9064
+ return comment.parent ? {
9065
+ kind: "review-comment-reply",
9066
+ reply: {
9067
+ ...common,
9068
+ parentCommentId: comment.parent.id
9069
+ }
9070
+ } : {
9071
+ kind: "command-comment",
9072
+ comment: {
9073
+ ...common,
9074
+ isChangeRequest: true
9075
+ }
7162
9076
  };
7163
- const publicationPlan = buildCommentPublishingPlan({
7164
- event: options.event,
7165
- main: reason,
7166
- validated,
7167
- manifest: options.diffManifest,
7168
- maxInlineComments: options.config.publication.maxInlineComments,
7169
- metadata: {
7170
- runtimeVersion,
7171
- configVersion: options.versionCompatibility?.configVersion,
7172
- trustedConfigSha: options.trustedConfigSha,
7173
- trustedConfigHash: options.trustedConfigHash,
7174
- reviewedHeadSha: options.event.change.head.sha,
7175
- providerModels: [options.provider.model],
7176
- selectedTasks: [],
7177
- failedTasks: [],
7178
- validFindings: 0,
7179
- droppedFindings: 0
9077
+ }
9078
+ async function pullRequestEvent(options, hook, eventKey) {
9079
+ const action = pullRequestAction(eventKey);
9080
+ if (hook.pullrequest.draft) return draftEvent();
9081
+ const loaded = await options.loadChangeRequest({
9082
+ workspace: hook.repository.full_name.split("/")[0] ?? "",
9083
+ repository: hook.repository.slug,
9084
+ changeNumber: hook.pullrequest.id
9085
+ });
9086
+ return {
9087
+ kind: "change-request",
9088
+ change: {
9089
+ eventName: eventKey,
9090
+ action,
9091
+ rawAction: eventKey,
9092
+ platform: {
9093
+ id: "bitbucket",
9094
+ host: "https://bitbucket.org"
9095
+ },
9096
+ repository: loaded.repository,
9097
+ coordinates: loaded.coordinates,
9098
+ change: loaded.change,
9099
+ workspace: options.workspace
7180
9100
  }
7181
- }).publicationPlan;
9101
+ };
9102
+ }
9103
+ function pullRequestAction(eventKey) {
9104
+ if (eventKey === "pullrequest:created") return "opened";
9105
+ if (eventKey === "pullrequest:updated") return "updated";
9106
+ if ([
9107
+ "pullrequest:fulfilled",
9108
+ "pullrequest:rejected",
9109
+ "pullrequest:superseded"
9110
+ ].includes(eventKey)) return "closed";
9111
+ throw new Error(`Unsupported Bitbucket event: ${eventKey}`);
9112
+ }
9113
+ function draftEvent() {
7182
9114
  return {
7183
- kind: "skipped",
7184
- skipReason: reason,
7185
- provider: options.provider,
7186
- diffManifest: options.diffManifest,
7187
- review,
7188
- validated,
7189
- publicationPlan,
7190
- mainComment: publicationPlan.mainComment,
7191
- inlineCommentDrafts: [],
7192
- taskChecks: [],
7193
- repairAttempted: false
9115
+ kind: "ignored",
9116
+ reason: "pull request is a draft"
9117
+ };
9118
+ }
9119
+ //#endregion
9120
+ //#region src/hosts/bitbucket/publication.ts
9121
+ async function publishBitbucketPlan(options) {
9122
+ await assertCurrentEndpoints(options.client, options.change);
9123
+ const owner = await options.client.currentUser();
9124
+ const comments = await options.client.listComments(options.change.change.number);
9125
+ const owned = comments.filter((comment) => comment.user?.uuid === owner.uuid);
9126
+ const existingMain = owned.find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)));
9127
+ const main = existingMain ? await options.client.updateComment(options.change.change.number, existingMain.id, options.plan.mainComment) : await options.client.createComment(options.change.change.number, { content: { raw: options.plan.mainComment } });
9128
+ const inline = await publishUnseenInlineItems({
9129
+ items: options.plan.inlineItems,
9130
+ existingBodies: owned.map((comment) => comment.content.raw),
9131
+ publish: (item) => options.client.createComment(options.change.change.number, {
9132
+ content: { raw: item.body },
9133
+ inline: bitbucketInline(item)
9134
+ })
9135
+ });
9136
+ const resolution = await publishBitbucketThreadActions({
9137
+ client: options.client,
9138
+ change: options.change,
9139
+ actions: options.plan.threadActions,
9140
+ reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
9141
+ comments
9142
+ });
9143
+ return completeHostPublication({
9144
+ provider: "Bitbucket",
9145
+ mainAction: existingMain ? "updated" : "created",
9146
+ mainId: main.id,
9147
+ inline,
9148
+ resolutionErrors: resolution.errors,
9149
+ metadata: options.plan.metadata
9150
+ });
9151
+ }
9152
+ async function publishBitbucketCommandResponse(options) {
9153
+ const response = commandResponseBody({
9154
+ changeNumber: options.change.change.number,
9155
+ sourceCommentId: options.sourceCommentId,
9156
+ commandName: options.commandName,
9157
+ body: options.body
9158
+ });
9159
+ await assertCurrentEndpoints(options.client, options.change);
9160
+ const owner = await options.client.currentUser();
9161
+ const existing = (await options.client.listComments(options.change.change.number)).find((comment) => comment.user?.uuid === owner.uuid && comment.content.raw.includes(response.marker));
9162
+ const comment = existing ? await options.client.updateComment(options.change.change.number, existing.id, response.body) : await options.client.createComment(options.change.change.number, { content: { raw: response.body } });
9163
+ return {
9164
+ action: existing ? "updated" : "created",
9165
+ id: comment.id
9166
+ };
9167
+ }
9168
+ async function loadBitbucketPriorReviewState(options) {
9169
+ const comments = await loadBitbucketOwnedComments(options);
9170
+ const body = comments.find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)))?.content.raw;
9171
+ const state = extractPriorReviewState(body, options.change.change.number);
9172
+ if (!state) return void 0;
9173
+ const bodies = comments.map((comment) => comment.content.raw);
9174
+ return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
9175
+ }
9176
+ async function loadBitbucketPriorMainComment(options) {
9177
+ return (await loadBitbucketOwnedComments(options)).find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)))?.content.raw;
9178
+ }
9179
+ async function loadBitbucketOwnedComments(options) {
9180
+ const owner = await options.client.currentUser();
9181
+ return (await options.client.listComments(options.change.change.number)).filter((comment) => comment.user?.uuid === owner.uuid);
9182
+ }
9183
+ async function loadBitbucketInlineThreadContexts(options) {
9184
+ const owner = await options.client.currentUser();
9185
+ const comments = await options.client.listComments(options.change.change.number);
9186
+ return comments.flatMap((root) => {
9187
+ const marker = extractInlineFindingMarkerRecords([root.content.raw])[0];
9188
+ if (!marker || root.user?.uuid !== owner.uuid || root.parent) return [];
9189
+ const replies = comments.filter((comment) => comment.parent?.id === root.id);
9190
+ return [{
9191
+ findingId: marker.id,
9192
+ findingHeadSha: marker.head,
9193
+ parentCommentId: root.id,
9194
+ parentBody: root.content.raw,
9195
+ threadId: root.id,
9196
+ threadResolved: root.resolution !== void 0,
9197
+ comments: [root, ...replies].map((comment) => ({
9198
+ id: comment.id,
9199
+ body: comment.content.raw,
9200
+ authorLogin: comment.user?.nickname
9201
+ }))
9202
+ }];
9203
+ });
9204
+ }
9205
+ async function publishBitbucketThreadActions(options) {
9206
+ if (options.actions.length === 0) return { errors: [] };
9207
+ await assertCurrentEndpoints(options.client, options.change, options.reviewedHeadSha);
9208
+ const comments = options.comments ?? await options.client.listComments(options.change.change.number);
9209
+ const errors = [];
9210
+ for (const action of options.actions) {
9211
+ const root = comments.find((comment) => comment.id === (action.threadId ?? action.commentId) && !comment.parent);
9212
+ if (!root) {
9213
+ errors.push(`Bitbucket comment not found for ${action.commentId}`);
9214
+ continue;
9215
+ }
9216
+ try {
9217
+ if (!comments.filter((comment) => comment.parent?.id === root.id).some((comment) => comment.content.raw.includes(action.responseKey))) await options.client.replyToComment(options.change.change.number, root.id, action.body);
9218
+ if (action.kind === "resolve" && root.resolution === void 0) await options.client.resolveComment(options.change.change.number, root.id);
9219
+ } catch (error) {
9220
+ errors.push(error instanceof Error ? error.message : String(error));
9221
+ }
9222
+ }
9223
+ return { errors };
9224
+ }
9225
+ function bitbucketInline(item) {
9226
+ return item.side === "RIGHT" ? {
9227
+ path: item.path,
9228
+ to: item.endLine,
9229
+ ...item.startLine !== item.endLine ? { start_to: item.startLine } : {}
9230
+ } : {
9231
+ path: item.previousPath ?? item.path,
9232
+ from: item.endLine,
9233
+ ...item.startLine !== item.endLine ? { start_from: item.startLine } : {}
9234
+ };
9235
+ }
9236
+ async function assertCurrentEndpoints(client, change, reviewedHeadSha = change.change.head.sha) {
9237
+ const pullRequest = await client.getPullRequest(change.change.number);
9238
+ if (pullRequest.source.commit.hash !== reviewedHeadSha || pullRequest.destination.commit.hash !== change.change.base.sha) throw new Error("Bitbucket pull request endpoints changed before publication");
9239
+ }
9240
+ function mainMarker$1(changeNumber) {
9241
+ return `<!-- pipr:main-comment change=${changeNumber} `;
9242
+ }
9243
+ //#endregion
9244
+ //#region src/hosts/bitbucket/workspace.ts
9245
+ function ensureBitbucketHeadCheckout(options) {
9246
+ const ref = options.change.change.head.ref;
9247
+ if (!ref) throw new Error("Bitbucket pull request source ref is required for checkout");
9248
+ const token = options.env?.BITBUCKET_API_TOKEN ?? process.env.BITBUCKET_API_TOKEN;
9249
+ const fetchEnv = options.change.change.isFork && token ? {
9250
+ ...options.env,
9251
+ GIT_CONFIG_COUNT: "1",
9252
+ GIT_CONFIG_KEY_0: "http.extraHeader",
9253
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(`x-bitbucket-api-token-auth:${token}`).toString("base64")}`
9254
+ } : options.env;
9255
+ return ensureCodeHostHeadCheckout({
9256
+ rootDir: options.rootDir,
9257
+ headSha: options.change.change.head.sha,
9258
+ fetchRef: ref.startsWith("refs/") ? ref : `refs/heads/${ref}`,
9259
+ fetchRemote: options.change.change.isFork ? options.change.change.head.url : void 0,
9260
+ fetchEnv
9261
+ });
9262
+ }
9263
+ //#endregion
9264
+ //#region src/hosts/bitbucket/adapter.ts
9265
+ function createBitbucketHostAdapter(options = {}) {
9266
+ const client = options.client ?? createBitbucketClient(options.env);
9267
+ return {
9268
+ id: "bitbucket",
9269
+ capabilities: {
9270
+ commandComments: true,
9271
+ reviewCommentReplies: true,
9272
+ threadResolution: true,
9273
+ multilineInlineComments: true,
9274
+ suggestedChanges: false,
9275
+ statuses: true
9276
+ },
9277
+ events: {
9278
+ parseEvent: (parseOptions) => parseBitbucketEvent({
9279
+ ...parseOptions,
9280
+ loadChangeRequest: (ref) => client.loadChange(ref)
9281
+ }),
9282
+ loadChangeRequest(ref) {
9283
+ const parts = ref.repository.slug.split("/");
9284
+ return client.loadChange({
9285
+ workspace: parts.at(-2) ?? client.workspace,
9286
+ repository: parts.at(-1) ?? client.repository,
9287
+ changeNumber: ref.changeNumber
9288
+ }).then((loaded) => ({
9289
+ ...loaded,
9290
+ eventName: ref.eventName,
9291
+ action: ref.action,
9292
+ rawAction: ref.rawAction,
9293
+ workspace: ref.workspace
9294
+ }));
9295
+ }
9296
+ },
9297
+ workspace: { ensureHeadCheckout: (args) => ensureBitbucketHeadCheckout({
9298
+ ...args,
9299
+ env: options.env
9300
+ }) },
9301
+ permissions: { getRepositoryPermission({ change, actor }) {
9302
+ const coordinates = bitbucketCoordinates(change);
9303
+ if (!coordinates.repositoryUuid) throw new Error("Bitbucket repository UUID is required for permission checks");
9304
+ return client.getRepositoryPermission(actor, coordinates.repositoryUuid);
9305
+ } },
9306
+ publication: {
9307
+ publish: ({ change, plan }) => publishBitbucketPlan({
9308
+ client,
9309
+ change,
9310
+ plan
9311
+ }),
9312
+ publishCommandResponse: (args) => publishBitbucketCommandResponse({
9313
+ client,
9314
+ ...args
9315
+ }),
9316
+ publishThreadActions: (args) => publishBitbucketThreadActions({
9317
+ client,
9318
+ ...args
9319
+ })
9320
+ },
9321
+ comments: {
9322
+ loadPriorReviewState: ({ change }) => loadBitbucketPriorReviewState({
9323
+ client,
9324
+ change
9325
+ }),
9326
+ loadPriorMainComment: ({ change }) => loadBitbucketPriorMainComment({
9327
+ client,
9328
+ change
9329
+ }),
9330
+ loadInlineThreadContexts: ({ change }) => loadBitbucketInlineThreadContexts({
9331
+ client,
9332
+ change
9333
+ })
9334
+ },
9335
+ statuses: {
9336
+ isAvailable: () => true,
9337
+ async upsert({ change, name, state, summary, status }) {
9338
+ const pullRequest = await client.getPullRequest(change.change.number);
9339
+ if (pullRequest.source.commit.hash !== change.change.head.sha || pullRequest.destination.commit.hash !== change.change.base.sha) throw new Error("Bitbucket pull request endpoints changed before status publication");
9340
+ const key = `pipr-${name}`.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
9341
+ const id = await client.setStatus(change.change.head.sha, key, {
9342
+ state: bitbucketStatusState(state),
9343
+ key,
9344
+ name: `Pipr: ${name}`.slice(0, 255),
9345
+ description: summary?.slice(0, 255),
9346
+ refname: change.change.head.ref,
9347
+ url: change.change.url
9348
+ });
9349
+ return status ?? {
9350
+ id,
9351
+ name
9352
+ };
9353
+ }
9354
+ }
7194
9355
  };
7195
9356
  }
9357
+ function bitbucketCoordinates(change) {
9358
+ if (change.coordinates?.provider !== "bitbucket") throw new Error("Bitbucket adapter requires Bitbucket coordinates");
9359
+ return change.coordinates;
9360
+ }
7196
9361
  //#endregion
7197
9362
  //#region src/shared/github.ts
7198
9363
  const githubApiVersion = "2026-03-10";
@@ -7207,12 +9372,21 @@ function parseRepoSlug(value) {
7207
9372
  repo
7208
9373
  };
7209
9374
  }
9375
+ function githubCoordinates(value) {
9376
+ const { owner, repo } = parseRepoSlug(value);
9377
+ return {
9378
+ provider: "github",
9379
+ owner,
9380
+ repository: repo
9381
+ };
9382
+ }
7210
9383
  //#endregion
7211
9384
  //#region src/hosts/github/command.ts
7212
9385
  const pullRequestDetailsSchema = z.looseObject({
7213
9386
  title: z.string().optional(),
7214
9387
  body: z.string().nullable().optional(),
7215
9388
  html_url: z.string().optional(),
9389
+ draft: z.boolean().optional(),
7216
9390
  user: z.looseObject({ login: z.string().min(1).optional() }).optional(),
7217
9391
  base: z.looseObject({
7218
9392
  sha: z.string().min(1),
@@ -7235,7 +9409,7 @@ const pullRequestDetailsSchema = z.looseObject({
7235
9409
  });
7236
9410
  const githubRepositoryPermissionResponseSchema = z.looseObject({
7237
9411
  permission: z.string().min(1),
7238
- role_name: z.string().min(1).optional()
9412
+ role_name: z.string().optional()
7239
9413
  });
7240
9414
  const permissionLevels = /* @__PURE__ */ new Set([
7241
9415
  "read",
@@ -7305,7 +9479,8 @@ function githubPullRequestDetails(options) {
7305
9479
  author: githubActor(options.json.head.user?.login),
7306
9480
  fork: options.json.head.repo?.fork
7307
9481
  },
7308
- isFork: githubHeadIsFork(options.json, repository.slug)
9482
+ isFork: githubHeadIsFork(options.json, repository.slug),
9483
+ isDraft: options.json.draft
7309
9484
  }
7310
9485
  };
7311
9486
  }
@@ -7332,6 +9507,7 @@ const githubPullRequestPayloadSchema = z.looseObject({
7332
9507
  title: z.string().optional(),
7333
9508
  body: z.string().nullable().optional(),
7334
9509
  html_url: z.string().optional(),
9510
+ draft: z.boolean().optional(),
7335
9511
  user: z.looseObject({ login: z.string().optional() }).optional(),
7336
9512
  base: z.looseObject({
7337
9513
  sha: z.string().optional(),
@@ -7392,7 +9568,7 @@ const githubIssueCommentEventContextSchema = z.strictObject({
7392
9568
  url: z.string().min(1).optional()
7393
9569
  }),
7394
9570
  changeNumber: z.number({ error: "GitHub issue comment event is missing issue number" }).int().positive(),
7395
- commentId: z.number({ error: "GitHub issue comment event is missing comment id" }).int().positive(),
9571
+ commentId: z.number({ error: "GitHub issue comment event is missing comment id" }).int().positive().transform(String),
7396
9572
  isChangeRequest: z.boolean(),
7397
9573
  body: z.string(),
7398
9574
  actor: z.string({ error: "GitHub issue comment event is missing comment user" }).min(1),
@@ -7407,8 +9583,8 @@ const githubReviewCommentReplyEventSchema = z.strictObject({
7407
9583
  url: z.string().min(1).optional()
7408
9584
  }),
7409
9585
  changeNumber: z.number({ error: "GitHub review comment event is missing pull request number" }).int().positive(),
7410
- commentId: z.number({ error: "GitHub review comment event is missing comment id" }).int().positive(),
7411
- parentCommentId: z.number().int().positive().optional(),
9586
+ commentId: z.number({ error: "GitHub review comment event is missing comment id" }).int().positive().transform(String),
9587
+ parentCommentId: z.number().int().positive().transform(String).optional(),
7412
9588
  body: z.string(),
7413
9589
  actor: z.string({ error: "GitHub review comment event is missing comment user" }).min(1),
7414
9590
  workspace: z.string().min(1)
@@ -7427,18 +9603,21 @@ function githubPullRequestEventInput(payload, options) {
7427
9603
  host: options.env.GITHUB_SERVER_URL
7428
9604
  },
7429
9605
  repository,
9606
+ coordinates: githubCoordinates(repository.slug),
7430
9607
  change: githubEventChange(payload, repository.slug),
7431
9608
  workspace: options.workspace
7432
9609
  };
7433
9610
  }
7434
9611
  function githubEventRepository(payload, env) {
7435
9612
  const baseRepo = payload.pull_request.base?.repo;
9613
+ const slug = [
9614
+ payload.repository?.full_name,
9615
+ baseRepo?.full_name,
9616
+ env.GITHUB_REPOSITORY
9617
+ ].find((value) => value !== void 0 && value.length > 0);
9618
+ if (!slug) throw new Error("GitHub pull request event is missing repository coordinates");
7436
9619
  return {
7437
- slug: [
7438
- payload.repository?.full_name,
7439
- baseRepo?.full_name,
7440
- env.GITHUB_REPOSITORY
7441
- ].find((value) => value !== void 0 && value.length > 0),
9620
+ slug,
7442
9621
  url: payload.repository?.html_url ?? baseRepo?.html_url
7443
9622
  };
7444
9623
  }
@@ -7452,7 +9631,8 @@ function githubEventChange(payload, repositorySlug) {
7452
9631
  author: githubActor(pullRequest.user?.login),
7453
9632
  base: githubBaseEndpoint(pullRequest),
7454
9633
  head: githubHeadEndpoint(pullRequest),
7455
- isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug)
9634
+ isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug),
9635
+ isDraft: pullRequest.draft
7456
9636
  };
7457
9637
  }
7458
9638
  function githubBaseEndpoint(pullRequest) {
@@ -7750,16 +9930,6 @@ const githubResolveReviewThreadMutation = `
7750
9930
  }
7751
9931
  `;
7752
9932
  //#endregion
7753
- //#region src/review/publication-result.ts
7754
- /** Error thrown when publication fails after producing partial result metadata. */
7755
- var PublicationError = class extends Error {
7756
- result;
7757
- constructor(message, result) {
7758
- super(message);
7759
- this.result = result;
7760
- }
7761
- };
7762
- //#endregion
7763
9933
  //#region src/hosts/github/publication-shared.ts
7764
9934
  async function assertCurrentHeadSha(client, change, reviewedHeadSha) {
7765
9935
  const headMismatch = await currentHeadShaMismatch(client, change, reviewedHeadSha);
@@ -7816,21 +9986,25 @@ async function publishGitHubCommandResponse(options) {
7816
9986
  repo: options.change.repository.slug,
7817
9987
  issueNumber: options.change.change.number
7818
9988
  }), marker, ownerLogin);
7819
- if (existing) return {
7820
- action: "updated",
7821
- id: (await options.client.updateIssueComment({
9989
+ if (existing) {
9990
+ const updated = await options.client.updateIssueComment({
7822
9991
  repo: options.change.repository.slug,
7823
9992
  commentId: existing.id,
7824
9993
  body
7825
- })).id
7826
- };
9994
+ });
9995
+ return {
9996
+ action: "updated",
9997
+ id: String(updated.id)
9998
+ };
9999
+ }
10000
+ const created = await options.client.createIssueComment({
10001
+ repo: options.change.repository.slug,
10002
+ issueNumber: options.change.change.number,
10003
+ body
10004
+ });
7827
10005
  return {
7828
10006
  action: "created",
7829
- id: (await options.client.createIssueComment({
7830
- repo: options.change.repository.slug,
7831
- issueNumber: options.change.change.number,
7832
- body
7833
- })).id
10007
+ id: String(created.id)
7834
10008
  };
7835
10009
  }
7836
10010
  function findCommandResponseComment(comments, marker, ownerLogin) {
@@ -7873,14 +10047,14 @@ async function loadGitHubInlineThreadContexts(options) {
7873
10047
  return [{
7874
10048
  findingId: marker.id,
7875
10049
  findingHeadSha: marker.head,
7876
- parentCommentId: comment.id,
10050
+ parentCommentId: String(comment.id),
7877
10051
  parentBody: comment.body ?? "",
7878
10052
  threadId: thread?.id,
7879
10053
  threadResolved: thread?.isResolved ?? false,
7880
10054
  comments: thread?.commentIds.flatMap((id) => {
7881
10055
  const item = commentById.get(id);
7882
10056
  return item ? [{
7883
- id: item.id,
10057
+ id: String(item.id),
7884
10058
  body: item.body ?? "",
7885
10059
  authorLogin: item.authorLogin
7886
10060
  }] : [];
@@ -8001,7 +10175,7 @@ async function postThreadActionReplyIfNeeded(context, action) {
8001
10175
  await context.client.createReviewCommentReply({
8002
10176
  repo: context.change.repository.slug,
8003
10177
  pullRequestNumber: context.change.change.number,
8004
- commentId: action.commentId,
10178
+ commentId: Number(action.commentId),
8005
10179
  body: [
8006
10180
  marker.body,
8007
10181
  "",
@@ -8073,7 +10247,7 @@ async function resolveReviewThread(context, action) {
8073
10247
  }
8074
10248
  function threadForAction(context, action) {
8075
10249
  if (action.kind !== "resolve") return;
8076
- return (action.threadId ? context.threadById.get(action.threadId) : void 0) ?? context.threadByCommentId.get(action.commentId);
10250
+ return (action.threadId ? context.threadById.get(action.threadId) : void 0) ?? context.threadByCommentId.get(Number(action.commentId));
8077
10251
  }
8078
10252
  //#endregion
8079
10253
  //#region src/hosts/github/publication-review.ts
@@ -8121,21 +10295,25 @@ async function upsertMainComment(options) {
8121
10295
  repo: options.change.repository.slug,
8122
10296
  issueNumber: options.change.change.number
8123
10297
  }), options.plan.mainMarker, options.change.change.number, options.ownerLogin);
8124
- if (existing) return {
8125
- action: "updated",
8126
- id: (await options.client.updateIssueComment({
10298
+ if (existing) {
10299
+ const updated = await options.client.updateIssueComment({
8127
10300
  repo: options.change.repository.slug,
8128
10301
  commentId: existing.id,
8129
10302
  body: options.plan.mainComment
8130
- })).id
8131
- };
10303
+ });
10304
+ return {
10305
+ action: "updated",
10306
+ id: String(updated.id)
10307
+ };
10308
+ }
10309
+ const created = await options.client.createIssueComment({
10310
+ repo: options.change.repository.slug,
10311
+ issueNumber: options.change.change.number,
10312
+ body: options.plan.mainComment
10313
+ });
8132
10314
  return {
8133
10315
  action: "created",
8134
- id: (await options.client.createIssueComment({
8135
- repo: options.change.repository.slug,
8136
- issueNumber: options.change.change.number,
8137
- body: options.plan.mainComment
8138
- })).id
10316
+ id: String(created.id)
8139
10317
  };
8140
10318
  }
8141
10319
  async function publishInlineComments(options) {
@@ -8291,48 +10469,18 @@ function gitConfigEnvCount(env) {
8291
10469
  const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? process.env.GIT_CONFIG_COUNT ?? Bun.env.GIT_CONFIG_COUNT ?? "0", 10);
8292
10470
  return Number.isSafeInteger(count) && count >= 0 ? count : 0;
8293
10471
  }
8294
- function gitGlobalConfigHome(env) {
8295
- if (existsSync("/home/bun")) return "/home/bun";
8296
- const root = env.RUNNER_TEMP ?? env.TMPDIR ?? os.tmpdir();
8297
- mkdirSync(root, { recursive: true });
8298
- return root;
8299
- }
8300
- function ensureGitHubHeadCheckout(options) {
8301
- const headSha = options.change.change.head.sha;
8302
- if (!hasGitCommit(options.rootDir, headSha)) runGit(options.rootDir, [
8303
- "fetch",
8304
- "--no-tags",
8305
- "--depth=1",
8306
- "origin",
8307
- `refs/pull/${options.change.change.number}/head`
8308
- ]);
8309
- if (runGit(options.rootDir, ["rev-parse", "HEAD"]).trim() !== headSha) runGit(options.rootDir, [
8310
- "checkout",
8311
- "--detach",
8312
- headSha
8313
- ]);
8314
- }
8315
- function hasGitCommit(rootDir, sha) {
8316
- try {
8317
- runGit(rootDir, [
8318
- "cat-file",
8319
- "-e",
8320
- `${sha}^{commit}`
8321
- ]);
8322
- return true;
8323
- } catch {
8324
- return false;
8325
- }
8326
- }
8327
- function runGit(rootDir, args) {
8328
- const result = Bun.spawnSync(["git", ...args], {
8329
- cwd: rootDir,
8330
- env: process.env,
8331
- stderr: "pipe",
8332
- stdout: "pipe"
10472
+ function gitGlobalConfigHome(env) {
10473
+ if (existsSync("/home/bun")) return "/home/bun";
10474
+ const root = env.RUNNER_TEMP ?? env.TMPDIR ?? os.tmpdir();
10475
+ mkdirSync(root, { recursive: true });
10476
+ return root;
10477
+ }
10478
+ function ensureGitHubHeadCheckout(options) {
10479
+ return ensureCodeHostHeadCheckout({
10480
+ rootDir: options.rootDir,
10481
+ headSha: options.change.change.head.sha,
10482
+ fetchRef: `refs/pull/${options.change.change.number}/head`
8333
10483
  });
8334
- if (result.exitCode !== 0) throw new Error(result.stderr.toString().trim() || `git ${args.join(" ")} failed`);
8335
- return result.stdout.toString();
8336
10484
  }
8337
10485
  //#endregion
8338
10486
  //#region src/hosts/github/adapter.ts
@@ -8342,35 +10490,63 @@ function createGitHubHostAdapter(options = {}) {
8342
10490
  const publicationClient = options.publicationClient ?? createGitHubPublicationClient(env);
8343
10491
  return {
8344
10492
  id: "github",
10493
+ capabilities: {
10494
+ commandComments: true,
10495
+ reviewCommentReplies: true,
10496
+ threadResolution: true,
10497
+ multilineInlineComments: true,
10498
+ suggestedChanges: true,
10499
+ statuses: true
10500
+ },
8345
10501
  events: {
8346
- parseEvent(parseOptions) {
8347
- return loadGitHubPullRequestEventContext(parseOptions);
10502
+ async parseEvent(parseOptions) {
10503
+ const eventPath = parseOptions.eventPath;
10504
+ if (!eventPath) throw new Error("GITHUB_EVENT_PATH is required for GitHub events");
10505
+ const eventOptions = {
10506
+ ...parseOptions,
10507
+ eventPath
10508
+ };
10509
+ if (parseOptions.env.GITHUB_EVENT_NAME === "issue_comment") return {
10510
+ kind: "command-comment",
10511
+ comment: await loadGitHubIssueCommentEventContext(eventOptions)
10512
+ };
10513
+ if (parseOptions.env.GITHUB_EVENT_NAME === "pull_request_review_comment") return {
10514
+ kind: "review-comment-reply",
10515
+ reply: await loadGitHubReviewCommentReplyEvent(eventOptions)
10516
+ };
10517
+ const change = await loadGitHubPullRequestEventContext(eventOptions);
10518
+ return change.change.isDraft ? {
10519
+ kind: "ignored",
10520
+ reason: "pull request is a draft"
10521
+ } : {
10522
+ kind: "change-request",
10523
+ change
10524
+ };
8348
10525
  },
8349
10526
  async loadChangeRequest(ref) {
10527
+ const loaded = await commandClient.getPullRequest({
10528
+ repository: ref.repository,
10529
+ changeNumber: ref.changeNumber
10530
+ });
8350
10531
  return {
8351
- ...await commandClient.getPullRequest({
8352
- repository: ref.repository,
8353
- changeNumber: ref.changeNumber
8354
- }),
10532
+ ...loaded,
10533
+ coordinates: githubCoordinates(loaded.repository.slug),
8355
10534
  eventName: ref.eventName,
8356
10535
  action: ref.action,
8357
10536
  rawAction: ref.rawAction,
8358
10537
  workspace: ref.workspace
8359
10538
  };
8360
- },
8361
- resolveCommandComment(parseOptions) {
8362
- return loadGitHubIssueCommentEventContext(parseOptions);
8363
- },
8364
- resolveReviewCommentReply(parseOptions) {
8365
- return loadGitHubReviewCommentReplyEvent(parseOptions);
8366
10539
  }
8367
10540
  },
8368
10541
  workspace: {
8369
10542
  ensureHeadCheckout: ensureGitHubHeadCheckout,
8370
10543
  ensureWorkspaceSafeDirectory: ensureGitHubWorkspaceSafeDirectory
8371
10544
  },
8372
- permissions: { getRepositoryPermission(options) {
8373
- return commandClient.getRepositoryPermission(options);
10545
+ permissions: { getRepositoryPermission({ change, actor }) {
10546
+ return commandClient.getRepositoryPermission({
10547
+ repository: change.repository,
10548
+ actor
10549
+ });
8374
10550
  } },
8375
10551
  publication: {
8376
10552
  publish(options) {
@@ -8384,7 +10560,7 @@ function createGitHubHostAdapter(options = {}) {
8384
10560
  return publishGitHubCommandResponse({
8385
10561
  client: publicationClient,
8386
10562
  change: options.change,
8387
- sourceCommentId: options.sourceCommentId,
10563
+ sourceCommentId: Number(options.sourceCommentId),
8388
10564
  commandName: options.commandName,
8389
10565
  body: options.body
8390
10566
  });
@@ -8418,44 +10594,509 @@ function createGitHubHostAdapter(options = {}) {
8418
10594
  });
8419
10595
  }
8420
10596
  },
8421
- checks: {
8422
- createCheckRun(options) {
8423
- return publicationClient.createCheckRun({
10597
+ statuses: {
10598
+ isAvailable(change) {
10599
+ return change.eventName === "pull_request";
10600
+ },
10601
+ async upsert(options) {
10602
+ if (!options.status) {
10603
+ const checkRun = await publicationClient.createCheckRun({
10604
+ repo: options.change.repository.slug,
10605
+ name: options.name,
10606
+ headSha: options.change.change.head.sha,
10607
+ summary: options.summary
10608
+ });
10609
+ return {
10610
+ id: String(checkRun.id),
10611
+ name: checkRun.name
10612
+ };
10613
+ }
10614
+ if (options.state === "pending") return options.status;
10615
+ await publicationClient.updateCheckRun({
8424
10616
  repo: options.change.repository.slug,
8425
- name: options.name,
8426
- headSha: options.change.change.head.sha,
10617
+ checkRunId: Number(options.status.id),
10618
+ name: options.status.name,
10619
+ conclusion: options.state,
8427
10620
  summary: options.summary
8428
10621
  });
10622
+ return options.status;
10623
+ }
10624
+ }
10625
+ };
10626
+ }
10627
+ //#endregion
10628
+ //#region src/hosts/gitlab/event.ts
10629
+ const projectSchema = z.looseObject({
10630
+ id: z.union([z.number(), z.string()]),
10631
+ path_with_namespace: z.string().min(1),
10632
+ web_url: z.string().url().optional()
10633
+ });
10634
+ const mergeRequestHookSchema = z.looseObject({
10635
+ object_kind: z.literal("merge_request"),
10636
+ project: projectSchema,
10637
+ object_attributes: z.looseObject({
10638
+ iid: z.number().int().positive(),
10639
+ action: z.string().optional(),
10640
+ draft: z.boolean().optional()
10641
+ }),
10642
+ changes: z.looseObject({ draft: z.looseObject({
10643
+ previous: z.boolean(),
10644
+ current: z.boolean()
10645
+ }).optional() }).optional()
10646
+ });
10647
+ const noteHookSchema = z.looseObject({
10648
+ object_kind: z.literal("note"),
10649
+ project: projectSchema,
10650
+ merge_request: z.looseObject({ iid: z.number().int().positive() }),
10651
+ user: z.looseObject({ username: z.string().min(1) }),
10652
+ object_attributes: z.looseObject({
10653
+ id: z.union([z.number(), z.string()]),
10654
+ note: z.string(),
10655
+ action: z.string().optional(),
10656
+ noteable_type: z.string(),
10657
+ discussion_id: z.string().min(1).optional()
10658
+ })
10659
+ });
10660
+ async function parseGitLabEvent(options) {
10661
+ if (!options.eventPath) return await pipelineEvent(options);
10662
+ const payload = await Bun.file(options.eventPath).json();
10663
+ if (isObjectKind(payload, "merge_request")) return await mergeRequestEvent(payload, options);
10664
+ if (isObjectKind(payload, "note")) return await noteEvent(payload, options);
10665
+ throw new Error("Unsupported GitLab event payload");
10666
+ }
10667
+ async function pipelineEvent(options) {
10668
+ const loaded = await loadChange(options, {
10669
+ projectId: requiredHostEnv(options.env, "CI_PROJECT_ID", "GitLab pipeline"),
10670
+ projectPath: requiredHostEnv(options.env, "CI_PROJECT_PATH", "GitLab pipeline"),
10671
+ changeNumber: positiveIntegerHostEnv(options.env, "CI_MERGE_REQUEST_IID", "GitLab pipeline")
10672
+ });
10673
+ if (loaded.change.isDraft) return {
10674
+ kind: "ignored",
10675
+ reason: "merge request is a draft"
10676
+ };
10677
+ return {
10678
+ kind: "change-request",
10679
+ change: parseChangeRequestEventContext({
10680
+ eventName: "gitlab_pipeline",
10681
+ action: options.env.PIPR_CHANGE_ACTION ?? "updated",
10682
+ rawAction: options.env.PIPR_CHANGE_ACTION,
10683
+ platform: {
10684
+ id: "gitlab",
10685
+ host: options.env.CI_SERVER_URL ?? "https://gitlab.com"
8429
10686
  },
8430
- updateCheckRun(options) {
8431
- return publicationClient.updateCheckRun({
8432
- repo: options.change.repository.slug,
8433
- checkRunId: Number(options.checkRun.id),
8434
- name: options.checkRun.name,
8435
- conclusion: options.conclusion,
8436
- summary: options.summary
10687
+ repository: loaded.repository,
10688
+ coordinates: loaded.coordinates,
10689
+ change: loaded.change,
10690
+ workspace: options.workspace
10691
+ })
10692
+ };
10693
+ }
10694
+ async function mergeRequestEvent(payload, options) {
10695
+ const hook = mergeRequestHookSchema.parse(payload);
10696
+ const becameReady = hook.changes?.draft?.previous === true && hook.changes.draft.current === false;
10697
+ if (hook.object_attributes.draft === true && !becameReady) return {
10698
+ kind: "ignored",
10699
+ reason: "merge request is a draft"
10700
+ };
10701
+ const loaded = await loadChange(options, {
10702
+ projectId: String(hook.project.id),
10703
+ projectPath: hook.project.path_with_namespace,
10704
+ changeNumber: hook.object_attributes.iid
10705
+ });
10706
+ return {
10707
+ kind: "change-request",
10708
+ change: parseChangeRequestEventContext({
10709
+ eventName: "merge_request",
10710
+ action: normalizeMergeRequestAction(hook.object_attributes.action, hook.changes?.draft),
10711
+ rawAction: hook.object_attributes.action,
10712
+ platform: {
10713
+ id: "gitlab",
10714
+ host: gitLabHost(hook.project.web_url)
10715
+ },
10716
+ repository: loaded.repository,
10717
+ coordinates: loaded.coordinates,
10718
+ change: loaded.change,
10719
+ workspace: options.workspace
10720
+ })
10721
+ };
10722
+ }
10723
+ async function noteEvent(payload, options) {
10724
+ const hook = noteHookSchema.parse(payload);
10725
+ if (hook.object_attributes.noteable_type !== "MergeRequest") throw new Error("GitLab note did not target a merge request");
10726
+ const common = {
10727
+ eventName: "note",
10728
+ action: hook.object_attributes.action === "create" ? "created" : hook.object_attributes.action,
10729
+ rawAction: hook.object_attributes.action,
10730
+ repository: {
10731
+ slug: hook.project.path_with_namespace,
10732
+ url: hook.project.web_url
10733
+ },
10734
+ changeNumber: hook.merge_request.iid,
10735
+ commentId: String(hook.object_attributes.id),
10736
+ body: hook.object_attributes.note,
10737
+ actor: hook.user.username,
10738
+ workspace: options.workspace
10739
+ };
10740
+ const parentCommentId = await options.resolveReplyParent?.({
10741
+ projectId: String(hook.project.id),
10742
+ changeNumber: hook.merge_request.iid,
10743
+ noteId: String(hook.object_attributes.id),
10744
+ discussionId: hook.object_attributes.discussion_id
10745
+ });
10746
+ return parentCommentId ? {
10747
+ kind: "review-comment-reply",
10748
+ reply: {
10749
+ ...common,
10750
+ parentCommentId
10751
+ }
10752
+ } : {
10753
+ kind: "command-comment",
10754
+ comment: {
10755
+ ...common,
10756
+ isChangeRequest: true
10757
+ }
10758
+ };
10759
+ }
10760
+ async function loadChange(options, ref) {
10761
+ if (!options.loadChangeRequest) throw new Error("GitLab merge request events require an API-backed change loader");
10762
+ return await options.loadChangeRequest(ref);
10763
+ }
10764
+ function normalizeMergeRequestAction(action, draftChange) {
10765
+ if (action === "update" && draftChange?.previous === true && draftChange.current === false) return "ready";
10766
+ return action ? gitLabMergeRequestActions[action] ?? action : void 0;
10767
+ }
10768
+ const gitLabMergeRequestActions = {
10769
+ open: "opened",
10770
+ reopen: "reopened",
10771
+ update: "updated",
10772
+ approved: "updated",
10773
+ unapproved: "updated",
10774
+ close: "closed",
10775
+ merge: "closed"
10776
+ };
10777
+ function isObjectKind(value, kind) {
10778
+ return typeof value === "object" && value !== null && "object_kind" in value && value.object_kind === kind;
10779
+ }
10780
+ function gitLabHost(projectUrl) {
10781
+ return projectUrl ? new URL(projectUrl).origin : "https://gitlab.com";
10782
+ }
10783
+ //#endregion
10784
+ //#region src/hosts/gitlab/publication.ts
10785
+ async function publishGitLabPlan(options) {
10786
+ const { projectId } = gitLabCoordinates$1(options.change);
10787
+ const owner = await options.client.currentUser();
10788
+ const existingMain = ownedNote(await options.client.listNotes(projectId, options.change.change.number), owner.username, mainMarker(options.change.change.number));
10789
+ const discussions = await options.client.listDiscussions(projectId, options.change.change.number);
10790
+ const ownedBodies = discussionNotes(discussions).filter((note) => note.author?.username === owner.username).map((note) => note.body);
10791
+ const mergeRequest = await assertCurrentHead(options.client, projectId, options.change);
10792
+ const main = existingMain ? await options.client.updateNote(projectId, options.change.change.number, existingMain.id, options.plan.mainComment) : await options.client.createNote(projectId, options.change.change.number, options.plan.mainComment);
10793
+ const inline = await publishUnseenInlineItems({
10794
+ items: options.plan.inlineItems,
10795
+ existingBodies: ownedBodies,
10796
+ publish: async (item) => {
10797
+ await options.client.createDiscussion(projectId, options.change.change.number, gitLabInlineBody(item), gitLabPosition(item, mergeRequest.diff_refs));
10798
+ }
10799
+ });
10800
+ const resolution = await publishGitLabThreadActions({
10801
+ client: options.client,
10802
+ change: options.change,
10803
+ actions: options.plan.threadActions,
10804
+ reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
10805
+ discussions
10806
+ });
10807
+ return completeHostPublication({
10808
+ provider: "GitLab",
10809
+ mainAction: existingMain ? "updated" : "created",
10810
+ mainId: main.id,
10811
+ inline,
10812
+ resolutionErrors: resolution.errors,
10813
+ metadata: options.plan.metadata
10814
+ });
10815
+ }
10816
+ async function publishGitLabCommandResponse(options) {
10817
+ const { projectId } = gitLabCoordinates$1(options.change);
10818
+ const owner = await options.client.currentUser();
10819
+ const response = commandResponseBody({
10820
+ changeNumber: options.change.change.number,
10821
+ sourceCommentId: options.sourceCommentId,
10822
+ commandName: options.commandName,
10823
+ body: options.body
10824
+ });
10825
+ const existing = ownedNote(await options.client.listNotes(projectId, options.change.change.number), owner.username, response.marker);
10826
+ await assertCurrentHead(options.client, projectId, options.change);
10827
+ const note = existing ? await options.client.updateNote(projectId, options.change.change.number, existing.id, response.body) : await options.client.createNote(projectId, options.change.change.number, response.body);
10828
+ return {
10829
+ action: existing ? "updated" : "created",
10830
+ id: note.id
10831
+ };
10832
+ }
10833
+ async function loadGitLabPriorReviewState(options) {
10834
+ const state = extractPriorReviewState(await loadGitLabPriorMainComment(options), options.change.change.number);
10835
+ if (!state) return void 0;
10836
+ const owner = await options.client.currentUser();
10837
+ const bodies = discussionNotes(await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number)).filter((note) => note.author?.username === owner.username).map((note) => note.body);
10838
+ return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
10839
+ }
10840
+ async function loadGitLabPriorMainComment(options) {
10841
+ const owner = await options.client.currentUser();
10842
+ return ownedNote(await options.client.listNotes(gitLabCoordinates$1(options.change).projectId, options.change.change.number), owner.username, mainMarker(options.change.change.number))?.body;
10843
+ }
10844
+ async function loadGitLabInlineThreadContexts(options) {
10845
+ const owner = await options.client.currentUser();
10846
+ return (await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number)).flatMap((discussion) => {
10847
+ const root = discussion.notes[0];
10848
+ const marker = root ? extractInlineFindingMarkerRecords([root.body])[0] : void 0;
10849
+ if (!root || !marker || root.author?.username !== owner.username) return [];
10850
+ return [{
10851
+ findingId: marker.id,
10852
+ findingHeadSha: marker.head,
10853
+ parentCommentId: root.id,
10854
+ parentBody: root.body,
10855
+ threadId: discussion.id,
10856
+ threadResolved: root.resolved ?? false,
10857
+ comments: discussion.notes.map((note) => ({
10858
+ id: note.id,
10859
+ body: note.body,
10860
+ authorLogin: note.author?.username
10861
+ }))
10862
+ }];
10863
+ });
10864
+ }
10865
+ async function publishGitLabThreadActions(options) {
10866
+ if (options.actions.length === 0) return { errors: [] };
10867
+ const { projectId } = gitLabCoordinates$1(options.change);
10868
+ await assertCurrentHead(options.client, projectId, options.change, options.reviewedHeadSha);
10869
+ const discussions = options.discussions ?? await options.client.listDiscussions(projectId, options.change.change.number);
10870
+ const byNote = new Map(discussions.flatMap((discussion) => discussion.notes.map((note) => [note.id, discussion])));
10871
+ const errors = [];
10872
+ for (const action of options.actions) {
10873
+ const error = await publishGitLabThreadAction({
10874
+ client: options.client,
10875
+ projectId,
10876
+ changeNumber: options.change.change.number,
10877
+ action,
10878
+ discussion: action.threadId ? discussions.find((candidate) => candidate.id === action.threadId) : byNote.get(action.commentId)
10879
+ });
10880
+ if (error) errors.push(error);
10881
+ }
10882
+ return { errors };
10883
+ }
10884
+ async function publishGitLabThreadAction(options) {
10885
+ if (!options.discussion) return `GitLab discussion not found for comment ${options.action.commentId}`;
10886
+ try {
10887
+ if (!options.discussion.notes.some((note) => note.body.includes(options.action.responseKey))) await options.client.replyDiscussion(options.projectId, options.changeNumber, options.discussion.id, options.action.body);
10888
+ if (options.action.kind === "resolve" && !options.discussion.notes[0]?.resolved) await options.client.resolveDiscussion(options.projectId, options.changeNumber, options.discussion.id);
10889
+ } catch (error) {
10890
+ return error instanceof Error ? error.message : String(error);
10891
+ }
10892
+ }
10893
+ function gitLabPosition(item, refs) {
10894
+ const oldPath = item.previousPath ?? item.path;
10895
+ const position = {
10896
+ position_type: "text",
10897
+ base_sha: refs.base_sha,
10898
+ start_sha: refs.start_sha,
10899
+ head_sha: refs.head_sha,
10900
+ old_path: oldPath,
10901
+ new_path: item.path,
10902
+ ...item.side === "RIGHT" ? { new_line: item.endLine } : { old_line: item.endLine }
10903
+ };
10904
+ if (item.startLine !== item.endLine) {
10905
+ const type = item.side === "RIGHT" ? "new" : "old";
10906
+ const linePath = type === "old" ? oldPath : item.path;
10907
+ position.line_range = {
10908
+ start: lineRangePoint(linePath, type, item.startLine),
10909
+ end: lineRangePoint(linePath, type, item.endLine)
10910
+ };
10911
+ }
10912
+ return position;
10913
+ }
10914
+ function lineRangePoint(path, type, line) {
10915
+ return {
10916
+ line_code: `${new Bun.CryptoHasher("sha1").update(path).digest("hex")}_${type === "old" ? line : 0}_${type === "new" ? line : 0}`,
10917
+ type,
10918
+ ...type === "old" ? { old_line: line } : { new_line: line }
10919
+ };
10920
+ }
10921
+ function gitLabInlineBody(item) {
10922
+ const offset = item.endLine - item.startLine;
10923
+ return item.body.replaceAll(/(`{3,})suggestion(\r?\n)/g, `$1suggestion:-${offset}+0$2`);
10924
+ }
10925
+ async function assertCurrentHead(client, projectId, change, reviewedHeadSha = change.change.head.sha) {
10926
+ const current = await client.getMergeRequest(projectId, change.change.number);
10927
+ if (current.diff_refs.head_sha !== reviewedHeadSha) throw new Error(`GitLab merge request head changed from ${reviewedHeadSha} to ${current.diff_refs.head_sha}`);
10928
+ return current;
10929
+ }
10930
+ function gitLabCoordinates$1(change) {
10931
+ if (change.coordinates?.provider !== "gitlab") throw new Error("GitLab adapter requires GitLab coordinates");
10932
+ return change.coordinates;
10933
+ }
10934
+ function ownedNote(notes, username, marker) {
10935
+ return notes.find((note) => note.author?.username === username && note.body.trimStart().startsWith(marker));
10936
+ }
10937
+ function discussionNotes(discussions) {
10938
+ return discussions.flatMap((discussion) => discussion.notes);
10939
+ }
10940
+ function mainMarker(changeNumber) {
10941
+ return `<!-- pipr:main-comment change=${changeNumber} `;
10942
+ }
10943
+ //#endregion
10944
+ //#region src/hosts/gitlab/workspace.ts
10945
+ function ensureGitLabHeadCheckout(options) {
10946
+ return ensureCodeHostHeadCheckout({
10947
+ rootDir: options.rootDir,
10948
+ headSha: options.change.change.head.sha,
10949
+ fetchRef: `refs/merge-requests/${options.change.change.number}/head`
10950
+ });
10951
+ }
10952
+ //#endregion
10953
+ //#region src/hosts/gitlab/adapter.ts
10954
+ function createGitLabHostAdapter(options = {}) {
10955
+ const client = options.client ?? createGitLabClient(options.env);
10956
+ return {
10957
+ id: "gitlab",
10958
+ capabilities: {
10959
+ commandComments: true,
10960
+ reviewCommentReplies: true,
10961
+ threadResolution: true,
10962
+ multilineInlineComments: true,
10963
+ suggestedChanges: true,
10964
+ statuses: true
10965
+ },
10966
+ events: {
10967
+ parseEvent(parseOptions) {
10968
+ return parseGitLabEvent({
10969
+ ...parseOptions,
10970
+ loadChangeRequest: (ref) => client.loadChange(ref),
10971
+ resolveReplyParent: ({ projectId, changeNumber, noteId, discussionId }) => client.findReplyParent(projectId, changeNumber, noteId, discussionId)
8437
10972
  });
10973
+ },
10974
+ loadChangeRequest(ref) {
10975
+ const coordinates = gitLabCoordinates(ref.repository.slug, ref.repository.url);
10976
+ return client.loadChange({
10977
+ projectId: coordinates.projectId,
10978
+ projectPath: coordinates.projectPath,
10979
+ changeNumber: ref.changeNumber
10980
+ }).then((loaded) => ({
10981
+ ...loaded,
10982
+ eventName: ref.eventName,
10983
+ action: ref.action,
10984
+ rawAction: ref.rawAction,
10985
+ workspace: ref.workspace
10986
+ }));
10987
+ }
10988
+ },
10989
+ workspace: { ensureHeadCheckout: ensureGitLabHeadCheckout },
10990
+ permissions: { getRepositoryPermission({ change, actor }) {
10991
+ return client.getRepositoryPermission(gitLabCoordinates(change.repository.slug, change.repository.url).projectId, actor);
10992
+ } },
10993
+ publication: {
10994
+ publish: ({ plan, change }) => publishGitLabPlan({
10995
+ client,
10996
+ plan,
10997
+ change
10998
+ }),
10999
+ publishCommandResponse: (args) => publishGitLabCommandResponse({
11000
+ client,
11001
+ ...args
11002
+ }),
11003
+ publishThreadActions: (args) => publishGitLabThreadActions({
11004
+ client,
11005
+ ...args
11006
+ })
11007
+ },
11008
+ comments: {
11009
+ loadPriorReviewState: ({ change }) => loadGitLabPriorReviewState({
11010
+ client,
11011
+ change
11012
+ }),
11013
+ loadPriorMainComment: ({ change }) => loadGitLabPriorMainComment({
11014
+ client,
11015
+ change
11016
+ }),
11017
+ loadInlineThreadContexts: ({ change }) => loadGitLabInlineThreadContexts({
11018
+ client,
11019
+ change
11020
+ })
11021
+ },
11022
+ statuses: {
11023
+ isAvailable: () => true,
11024
+ async upsert({ change, name, state, summary, status }) {
11025
+ const id = await client.setStatus(gitLabChangeCoordinates(change).projectId, change.change.head.sha, name, state, summary);
11026
+ return status ?? {
11027
+ id,
11028
+ name
11029
+ };
8438
11030
  }
8439
11031
  }
8440
11032
  };
8441
11033
  }
11034
+ function gitLabChangeCoordinates(change) {
11035
+ if (change.coordinates?.provider !== "gitlab") throw new Error("GitLab adapter requires GitLab coordinates");
11036
+ return change.coordinates;
11037
+ }
11038
+ function gitLabCoordinates(slug, url) {
11039
+ return {
11040
+ projectId: url?.match(/\/projects\/(\d+)/)?.[1] ?? slug,
11041
+ projectPath: slug
11042
+ };
11043
+ }
11044
+ //#endregion
11045
+ //#region src/hosts/selection.ts
11046
+ const codeHostIds = [
11047
+ "github",
11048
+ "gitlab",
11049
+ "azure-devops",
11050
+ "bitbucket"
11051
+ ];
11052
+ function resolveCodeHostId(options) {
11053
+ const selected = options.explicitHost ?? options.env.PIPR_CODE_HOST;
11054
+ if (selected) return parseCodeHostId(selected);
11055
+ const detected = [];
11056
+ if (options.env.GITHUB_ACTIONS !== void 0) detected.push("github");
11057
+ if (options.env.GITLAB_CI !== void 0) detected.push("gitlab");
11058
+ if (options.env.TF_BUILD !== void 0) detected.push("azure-devops");
11059
+ if (options.env.BITBUCKET_BUILD_NUMBER !== void 0) detected.push("bitbucket");
11060
+ if (detected.length === 1) {
11061
+ const host = detected[0];
11062
+ if (host) return host;
11063
+ }
11064
+ if (detected.length > 1) throw new Error(`Multiple code hosts detected: ${detected.join(", ")}`);
11065
+ throw new Error("A code host must be selected");
11066
+ }
11067
+ function parseCodeHostId(value) {
11068
+ switch (value) {
11069
+ case "github":
11070
+ case "gitlab":
11071
+ case "azure-devops":
11072
+ case "bitbucket": return value;
11073
+ default: throw new Error(`Unsupported code host '${value}'. Supported hosts: ${codeHostIds.join(", ")}`);
11074
+ }
11075
+ }
8442
11076
  //#endregion
8443
- //#region src/action/action-host.ts
8444
- function assertTrustedActionProviderEnv(options, trustedConfig) {
11077
+ //#region src/host-run/adapter.ts
11078
+ function assertTrustedHostRunProviderEnv(options, trustedConfig) {
8445
11079
  const env = options.env ?? process.env;
8446
11080
  const missing = [];
8447
11081
  for (const provider of trustedConfig.providers) if (!env[provider.apiKeyEnv]) missing.push(provider.apiKeyEnv);
8448
11082
  if (missing.length > 0) throw new Error(`Missing provider env vars: ${missing.join(", ")}`);
8449
11083
  }
8450
- function createActionHostAdapter(options) {
8451
- return options.hostAdapter ?? createGitHubHostAdapter({
8452
- env: options.env,
8453
- commandClient: options.githubClient,
8454
- publicationClient: options.githubPublicationClient
11084
+ function createHostRunAdapter(options) {
11085
+ if (options.hostAdapter) return options.hostAdapter;
11086
+ const host = resolveCodeHostId({
11087
+ explicitHost: options.host,
11088
+ env: options.env ?? process.env
8455
11089
  });
11090
+ if (host !== "github") {
11091
+ if (host === "azure-devops") return createAzureDevOpsHostAdapter({ env: options.env });
11092
+ if (host === "gitlab") return createGitLabHostAdapter({ env: options.env });
11093
+ if (host === "bitbucket") return createBitbucketHostAdapter({ env: options.env });
11094
+ throw new Error(`Code host adapter '${host}' is not available in this build`);
11095
+ }
11096
+ return createGitHubHostAdapter({ env: options.env });
8456
11097
  }
8457
11098
  //#endregion
8458
- //#region src/action/action-logging.ts
11099
+ //#region src/host-run/logging.ts
8459
11100
  async function logPhase(log, name, run) {
8460
11101
  const started = Date.now();
8461
11102
  log.info(`${name} start`);
@@ -8504,8 +11145,8 @@ function addProviderSecrets(log, config, env) {
8504
11145
  for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
8505
11146
  }
8506
11147
  //#endregion
8507
- //#region src/action/runtime-checks.ts
8508
- const genericCheckFailureSummary = "pipr failed; see Action logs for details.";
11148
+ //#region src/host-run/runtime-checks.ts
11149
+ const genericCheckFailureSummary = "pipr failed; see runner logs for details.";
8509
11150
  async function startRuntimeChecks(options) {
8510
11151
  if (!canStartRuntimeChecks(options)) return;
8511
11152
  const tasks = options.selectedTasks;
@@ -8528,8 +11169,8 @@ async function startRuntimeChecks(options) {
8528
11169
  try {
8529
11170
  await startTaskCheckRuns(started);
8530
11171
  if (aggregateName) {
8531
- started.aggregate = await createCheckRunOrThrow(started, aggregateName, "pipr review is running.");
8532
- started.log?.info("check run created", {
11172
+ started.aggregate = await createStatusOrThrow(started, aggregateName, "pipr review is running.");
11173
+ started.log?.info("status created", {
8533
11174
  name: aggregateName,
8534
11175
  kind: "aggregate"
8535
11176
  });
@@ -8541,7 +11182,7 @@ async function startRuntimeChecks(options) {
8541
11182
  return started;
8542
11183
  }
8543
11184
  async function finalizeRuntimeChecks(checks, options) {
8544
- if (!checks?.adapter.checks?.updateCheckRun) return;
11185
+ if (!checks?.adapter.statuses) return;
8545
11186
  const taskResults = checks.tasks.map((task) => taskCheckResultForFinalization(task, checks.outcomes.get(task.name), options));
8546
11187
  const taskError = await updateTaskCheckRuns(checks, taskResults);
8547
11188
  const aggregateError = await updateAggregateCheckRun(checks, taskResults, options);
@@ -8549,37 +11190,38 @@ async function finalizeRuntimeChecks(checks, options) {
8549
11190
  if (firstError !== void 0) throw firstError instanceof Error ? firstError : new Error(String(firstError));
8550
11191
  }
8551
11192
  function canStartRuntimeChecks(options) {
8552
- return options.event.eventName === "pull_request" && options.taskName === void 0 && Boolean(options.adapter.checks?.createCheckRun) && Boolean(options.adapter.checks?.updateCheckRun);
11193
+ return options.taskName === void 0 && options.adapter.capabilities.statuses && Boolean(options.adapter.statuses?.isAvailable(options.event));
8553
11194
  }
8554
11195
  async function startTaskCheckRuns(started) {
8555
11196
  for (const task of started.tasks) {
8556
11197
  const settings = taskCheckSettings(task);
8557
11198
  if (!settings.individual) continue;
8558
- const run = await createCheckRunOrThrow(started, settings.name, "pipr task is running.");
11199
+ const run = await createStatusOrThrow(started, settings.name, "pipr task is running.");
8559
11200
  started.taskRuns.set(task.name, run);
8560
- started.log?.info("check run created", {
11201
+ started.log?.info("status created", {
8561
11202
  name: settings.name,
8562
11203
  kind: "task",
8563
11204
  task: task.name
8564
11205
  });
8565
11206
  }
8566
11207
  }
8567
- async function createCheckRunOrThrow(checks, name, summary) {
11208
+ async function createStatusOrThrow(checks, name, summary) {
8568
11209
  try {
8569
- const createCheckRun = checks.adapter.checks?.createCheckRun;
8570
- if (!createCheckRun) throw new Error("check run creation is not available");
8571
- return await createCheckRun({
11210
+ const statuses = checks.adapter.statuses;
11211
+ if (!statuses) throw new Error("status publication is not available");
11212
+ return await statuses.upsert({
8572
11213
  change: checks.event,
8573
11214
  name,
11215
+ state: "pending",
8574
11216
  summary
8575
11217
  });
8576
11218
  } catch (error) {
8577
- throw checkRunPermissionError(error);
11219
+ throw statusPermissionError(error);
8578
11220
  }
8579
11221
  }
8580
- function checkRunPermissionError(error) {
11222
+ function statusPermissionError(error) {
8581
11223
  const message = error instanceof Error ? error.message : String(error);
8582
- return /* @__PURE__ */ new Error(`Unable to create GitHub check run. Ensure the workflow grants 'checks: write'. ${message}`);
11224
+ return /* @__PURE__ */ new Error(`Unable to publish code host status. Check the adapter credential scopes. ${message}`);
8583
11225
  }
8584
11226
  async function updateTaskCheckRuns(checks, taskResults) {
8585
11227
  let firstError;
@@ -8597,20 +11239,21 @@ async function updateAggregateCheckRun(checks, taskResults, options) {
8597
11239
  }
8598
11240
  async function updateCheckRunOrError(checks, checkRun, result) {
8599
11241
  try {
8600
- await checks.adapter.checks?.updateCheckRun?.({
11242
+ await checks.adapter.statuses?.upsert({
8601
11243
  change: checks.event,
8602
- checkRun,
8603
- conclusion: result.conclusion,
11244
+ name: checkRun.name,
11245
+ status: checkRun,
11246
+ state: result.conclusion,
8604
11247
  summary: result.summary
8605
11248
  });
8606
- checks.log?.info("check run finalized", {
11249
+ checks.log?.info("status finalized", {
8607
11250
  name: checkRun.name,
8608
11251
  conclusion: result.conclusion,
8609
11252
  summary: result.summary
8610
11253
  });
8611
11254
  return;
8612
11255
  } catch (error) {
8613
- checks.log?.warning("check run finalize failed", {
11256
+ checks.log?.warning("status finalize failed", {
8614
11257
  name: checkRun.name,
8615
11258
  error: error instanceof Error ? error.message : String(error)
8616
11259
  });
@@ -8677,7 +11320,7 @@ function taskCheckSettings(task) {
8677
11320
  };
8678
11321
  }
8679
11322
  //#endregion
8680
- //#region src/action/review-publishing.ts
11323
+ //#region src/host-run/review-publishing.ts
8681
11324
  async function runTrustedReviewAndPublish(options) {
8682
11325
  const checks = await startRuntimeChecks({
8683
11326
  adapter: options.adapter,
@@ -8728,13 +11371,14 @@ async function runTrustedReviewAndPublish(options) {
8728
11371
  const publish = options.adapter.publication?.publish;
8729
11372
  if (!publish) throw new Error("review publication is not available for this code host");
8730
11373
  const publication = await options.log.group("publish review", async () => {
11374
+ const publicationPlan = publicationPlanForHostCapabilities(review.publicationPlan, options.adapter.capabilities);
8731
11375
  options.log.info("publication plan", {
8732
- inlineItems: review.publicationPlan.inlineItems.length,
8733
- threadActions: review.publicationPlan.threadActions.length
11376
+ inlineItems: publicationPlan.inlineItems.length,
11377
+ threadActions: publicationPlan.threadActions.length
8734
11378
  });
8735
11379
  const result = await publish({
8736
11380
  change: options.event,
8737
- plan: review.publicationPlan
11381
+ plan: publicationPlan
8738
11382
  });
8739
11383
  options.log.notice("publication result", {
8740
11384
  main: result.mainComment.action,
@@ -8762,7 +11406,7 @@ async function runTrustedReviewAndPublish(options) {
8762
11406
  }
8763
11407
  }
8764
11408
  //#endregion
8765
- //#region src/action/git-project.ts
11409
+ //#region src/host-run/git-project.ts
8766
11410
  async function loadRuntimeProjectFromGitCommit(options) {
8767
11411
  const configDir = resolveContainedConfigDir(options);
8768
11412
  const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
@@ -8841,7 +11485,7 @@ function showFileAtCommit(rootDir, commitSha, filePath) {
8841
11485
  return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
8842
11486
  }
8843
11487
  //#endregion
8844
- //#region src/action/trusted-runtime.ts
11488
+ //#region src/host-run/trusted-runtime.ts
8845
11489
  async function loadTrustedRuntimeForEvent(options, event, log) {
8846
11490
  const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
8847
11491
  rootDir: options.rootDir,
@@ -8854,30 +11498,78 @@ async function loadTrustedRuntimeForEvent(options, event, log) {
8854
11498
  }
8855
11499
  async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
8856
11500
  addProviderSecrets(log, config, options.env);
8857
- assertTrustedActionProviderEnv(options, config);
11501
+ assertTrustedHostRunProviderEnv(options, config);
8858
11502
  await logPhase(log, "checkout head", async () => {
8859
- adapter.workspace.ensureHeadCheckout({
11503
+ await adapter.workspace.ensureHeadCheckout({
8860
11504
  rootDir: options.rootDir,
8861
11505
  change: event
8862
11506
  });
8863
11507
  });
8864
11508
  }
8865
11509
  //#endregion
8866
- //#region src/action/command-entry.ts
8867
- async function runIssueCommentActionCommand(options, adapter, log) {
8868
- const prepared = await prepareIssueCommentCommand(options, adapter, log);
11510
+ //#region src/host-run/change-request-entry.ts
11511
+ async function runChangeRequestHostRunCommand(options, adapter, log, event) {
11512
+ logEventContext(log, event);
11513
+ const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
11514
+ if (options.dryRun) {
11515
+ log.notice("dry run stop before review runtime, model, or GitHub publishing calls");
11516
+ return {
11517
+ kind: "dry-run",
11518
+ event,
11519
+ configSource: trustedRuntime.settings.source
11520
+ };
11521
+ }
11522
+ await prepareTrustedHeadCheckout(options, adapter, trustedRuntime.settings.config, event, log);
11523
+ const dispatch = dispatchRuntimeEntry({
11524
+ kind: "change-request",
11525
+ plan: trustedRuntime.plan,
11526
+ event
11527
+ });
11528
+ const selectedTasks = dispatch.kind === "change-request" ? dispatch.tasks : [];
11529
+ log.notice("dispatch", { selectedTasks: selectedTasks.map((task) => task.name) });
11530
+ const completed = await runTrustedReviewAndPublish({
11531
+ options,
11532
+ adapter,
11533
+ trustedRuntime,
11534
+ event,
11535
+ selectedTasks,
11536
+ log
11537
+ });
11538
+ if (completed.kind === "skipped") {
11539
+ log.notice("event ignored", { reason: completed.reason });
11540
+ return {
11541
+ kind: "ignored",
11542
+ reason: completed.reason
11543
+ };
11544
+ }
11545
+ if (completed.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
11546
+ return {
11547
+ kind: "review",
11548
+ event,
11549
+ configSource: trustedRuntime.settings.source,
11550
+ review: completed.review,
11551
+ publication: completed.publication
11552
+ };
11553
+ }
11554
+ //#endregion
11555
+ //#region src/host-run/command-entry.ts
11556
+ async function runIssueCommentHostRunCommand(options, adapter, log, comment) {
11557
+ if (!adapter.capabilities.commandComments) {
11558
+ const ignored = {
11559
+ kind: "ignored",
11560
+ reason: "host adapter does not support commands"
11561
+ };
11562
+ log.notice("event ignored", { reason: ignored.reason });
11563
+ return ignored;
11564
+ }
11565
+ const prepared = await prepareIssueCommentCommand(options, adapter, log, comment);
8869
11566
  if (prepared.kind === "ignored") {
8870
- log.notice("action ignored", { reason: prepared.reason });
11567
+ log.notice("event ignored", { reason: prepared.reason });
8871
11568
  return prepared;
8872
11569
  }
8873
11570
  return await dispatchIssueCommentCommand(options, adapter, prepared, log);
8874
11571
  }
8875
- async function prepareIssueCommentCommand(options, adapter, log) {
8876
- const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
8877
- eventPath: options.eventPath,
8878
- env: options.env ?? process.env,
8879
- workspace: options.rootDir
8880
- }));
11572
+ async function prepareIssueCommentCommand(options, adapter, log, comment) {
8881
11573
  const runnable = runnableIssueCommentCommand(comment, options.dryRun);
8882
11574
  if (runnable.kind === "ignored") return runnable;
8883
11575
  const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
@@ -8894,6 +11586,7 @@ async function prepareIssueCommentCommand(options, adapter, log) {
8894
11586
  rawAction: loaded.rawAction ?? comment.rawAction,
8895
11587
  platform: { id: adapter.id },
8896
11588
  repository: loaded.repository,
11589
+ coordinates: loaded.coordinates,
8897
11590
  change: loaded.change,
8898
11591
  workspace: loaded.workspace ?? comment.workspace
8899
11592
  });
@@ -8938,7 +11631,7 @@ function runnableIssueCommentCommand(comment, dryRun) {
8938
11631
  async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
8939
11632
  const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
8940
11633
  const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
8941
- repository: prepared.comment.repository,
11634
+ change: prepared.event,
8942
11635
  actor: prepared.comment.actor
8943
11636
  }));
8944
11637
  log.notice("command dispatch", {
@@ -9008,7 +11701,7 @@ async function issueCommentCommandResult(options) {
9008
11701
  kind: "ignored",
9009
11702
  reason: options.completed.reason
9010
11703
  };
9011
- if (options.completed.kind === "command-response") return await publishCommandResponseActionResult({
11704
+ if (options.completed.kind === "command-response") return await publishCommandResponseHostRunResult({
9012
11705
  adapter: options.adapter,
9013
11706
  completed: options.completed,
9014
11707
  event: options.event,
@@ -9024,7 +11717,7 @@ async function issueCommentCommandResult(options) {
9024
11717
  publication: options.completed.publication
9025
11718
  };
9026
11719
  }
9027
- async function publishCommandResponseActionResult(options) {
11720
+ async function publishCommandResponseHostRunResult(options) {
9028
11721
  const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
9029
11722
  if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
9030
11723
  const publication = await publishCommandResponse({
@@ -9043,76 +11736,21 @@ async function publishCommandResponseActionResult(options) {
9043
11736
  };
9044
11737
  }
9045
11738
  //#endregion
9046
- //#region src/action/pull-request-entry.ts
9047
- async function runPullRequestActionCommand(options, adapter, log) {
9048
- const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
9049
- eventPath: options.eventPath,
9050
- env: options.env ?? process.env,
9051
- workspace: options.rootDir
9052
- }));
9053
- logEventContext(log, event);
9054
- const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
9055
- if (options.dryRun) {
9056
- log.notice("dry run stop before review runtime, model, or GitHub publishing calls");
9057
- return {
9058
- kind: "dry-run",
9059
- event,
9060
- configSource: trustedRuntime.settings.source
9061
- };
9062
- }
9063
- await prepareTrustedHeadCheckout(options, adapter, trustedRuntime.settings.config, event, log);
9064
- const dispatch = dispatchRuntimeEntry({
9065
- kind: "change-request",
9066
- plan: trustedRuntime.plan,
9067
- event
9068
- });
9069
- const selectedTasks = dispatch.kind === "change-request" ? dispatch.tasks : [];
9070
- log.notice("dispatch", { selectedTasks: selectedTasks.map((task) => task.name) });
9071
- const completed = await runTrustedReviewAndPublish({
9072
- options,
9073
- adapter,
9074
- trustedRuntime,
9075
- event,
9076
- selectedTasks,
9077
- log
9078
- });
9079
- if (completed.kind === "skipped") {
9080
- log.notice("action ignored", { reason: completed.reason });
9081
- return {
9082
- kind: "ignored",
9083
- reason: completed.reason
9084
- };
9085
- }
9086
- if (completed.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
9087
- return {
9088
- kind: "review",
9089
- event,
9090
- configSource: trustedRuntime.settings.source,
9091
- review: completed.review,
9092
- publication: completed.publication
9093
- };
9094
- }
9095
- //#endregion
9096
- //#region src/action/verifier-entry.ts
9097
- async function runReviewCommentReplyActionCommand(options, adapter, log) {
11739
+ //#region src/host-run/verifier-entry.ts
11740
+ async function runReviewCommentReplyHostRunCommand(options, adapter, log, reply) {
9098
11741
  const capabilities = reviewCommentReplyDispatchCapabilities(options, adapter);
9099
11742
  if (capabilities.kind === "ignored") {
9100
- log.notice("action ignored", { reason: capabilities.reason });
11743
+ log.notice("event ignored", { reason: capabilities.reason });
9101
11744
  return capabilities;
9102
11745
  }
9103
- const reply = await logPhase(log, "parse review comment reply", async () => capabilities.resolveReviewCommentReply({
9104
- eventPath: options.eventPath,
9105
- env: options.env ?? process.env,
9106
- workspace: options.rootDir
9107
- }));
9108
11746
  const runnable = runnableReviewCommentReply(reply);
9109
11747
  if (runnable.kind === "ignored") {
9110
- log.notice("action ignored", { reason: runnable.reason });
11748
+ log.notice("event ignored", { reason: runnable.reason });
9111
11749
  return runnable;
9112
11750
  }
9113
11751
  const prepared = await prepareReviewCommentVerifier(options, adapter, reply, log);
9114
11752
  if (prepared.kind === "ignored") {
9115
- log.notice("action ignored", { reason: prepared.reason });
11753
+ log.notice("event ignored", { reason: prepared.reason });
9116
11754
  return prepared;
9117
11755
  }
9118
11756
  const result = await runReviewCommentVerifier(options, adapter, prepared, log);
@@ -9133,9 +11771,9 @@ async function runReviewCommentReplyActionCommand(options, adapter, log) {
9133
11771
  };
9134
11772
  }
9135
11773
  function reviewCommentReplyDispatchCapabilities(options, adapter) {
9136
- if (!adapter.events.resolveReviewCommentReply) return {
11774
+ if (!adapter.capabilities.reviewCommentReplies || !adapter.capabilities.threadResolution) return {
9137
11775
  kind: "ignored",
9138
- reason: "host adapter does not support review comment replies"
11776
+ reason: "host adapter does not support verifier replies"
9139
11777
  };
9140
11778
  if (!adapter.publication?.publishThreadActions) return {
9141
11779
  kind: "ignored",
@@ -9147,7 +11785,6 @@ function reviewCommentReplyDispatchCapabilities(options, adapter) {
9147
11785
  };
9148
11786
  return {
9149
11787
  kind: "ready",
9150
- resolveReviewCommentReply: adapter.events.resolveReviewCommentReply,
9151
11788
  publishThreadActions: adapter.publication.publishThreadActions
9152
11789
  };
9153
11790
  }
@@ -9170,6 +11807,7 @@ async function prepareReviewCommentVerifier(options, adapter, reply, log) {
9170
11807
  rawAction: loaded.rawAction ?? reply.rawAction,
9171
11808
  platform: { id: adapter.id },
9172
11809
  repository: loaded.repository,
11810
+ coordinates: loaded.coordinates,
9173
11811
  change: loaded.change,
9174
11812
  workspace: loaded.workspace ?? reply.workspace
9175
11813
  });
@@ -9275,12 +11913,12 @@ async function verifierActorAllowed(adapter, event, reply, config) {
9275
11913
  if (allowed === "any") return true;
9276
11914
  if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
9277
11915
  return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
9278
- repository: event.repository,
11916
+ change: event,
9279
11917
  actor: reply.actor
9280
11918
  }), "write");
9281
11919
  }
9282
11920
  //#endregion
9283
- //#region src/action/commands.ts
11921
+ //#region src/host-run/commands.ts
9284
11922
  /** Initializes the official minimal `.pipr` project files. */
9285
11923
  async function runInitCommand(options) {
9286
11924
  return await initOfficialMinimalProject({
@@ -9310,21 +11948,19 @@ async function runInspectCommand(options) {
9310
11948
  warnings: runtime.settings.warnings
9311
11949
  };
9312
11950
  }
9313
- /** Loads the runtime config and pull request event without running review publication. */
11951
+ /** Loads the runtime config and change request event without running review publication. */
9314
11952
  async function runDryRunCommand(options) {
9315
11953
  const runtime = await loadRuntimeProject({
9316
11954
  ...options,
9317
11955
  requireProviderEnv: false
9318
11956
  });
9319
- const event = await createActionHostAdapter(options).events.parseEvent({
11957
+ const hostEvent = await createHostRunAdapter(options).events.parseEvent({
9320
11958
  eventPath: options.eventPath,
9321
- env: {
9322
- ...options.env,
9323
- GITHUB_WORKSPACE: options.rootDir,
9324
- GITHUB_EVENT_NAME: "pull_request"
9325
- },
11959
+ env: options.env ?? process.env,
9326
11960
  workspace: options.rootDir
9327
11961
  });
11962
+ if (hostEvent.kind !== "change-request") throw new Error(`dry-run requires a change-request event, received ${hostEvent.kind}`);
11963
+ const event = hostEvent.change;
9328
11964
  return {
9329
11965
  configSource: runtime.settings.source,
9330
11966
  event,
@@ -9333,7 +11969,7 @@ async function runDryRunCommand(options) {
9333
11969
  }
9334
11970
  /** Runs configured change-request tasks against local Git base and head revisions. */
9335
11971
  async function runLocalReviewCommand(options) {
9336
- const log = options.logSink ? createRuntimeActionLog({
11972
+ const log = options.logSink ? createRuntimeLog({
9337
11973
  logSink: options.logSink,
9338
11974
  env: options.env
9339
11975
  }) : void 0;
@@ -9397,36 +12033,43 @@ async function runLocalReviewCommand(options) {
9397
12033
  });
9398
12034
  return result;
9399
12035
  }
9400
- /** Runs the GitHub Action workflow for pull request and issue-comment events. */
9401
- async function runActionCommand(options) {
9402
- return await runActionCommandWithDependencies(options);
12036
+ /** Runs a normalized code host event through the selected adapter. */
12037
+ async function runHostRunCommand(options) {
12038
+ return await runHostRunCommandWithDependencies(options);
9403
12039
  }
9404
- async function runActionCommandWithDependencies(options) {
9405
- const log = createRuntimeActionLog({
12040
+ async function runHostRunCommandWithDependencies(options) {
12041
+ const log = createRuntimeLog({
9406
12042
  logSink: options.logSink,
9407
12043
  env: options.env
9408
12044
  });
9409
- return await log.group("pipr action", async () => {
9410
- const eventName = (options.env ?? process.env).GITHUB_EVENT_NAME ?? "pull_request";
9411
- log.notice("action start", {
9412
- eventName,
12045
+ return await log.group("pipr host run", async () => {
12046
+ log.notice("host run start", {
9413
12047
  dryRun: options.dryRun,
9414
12048
  root: options.rootDir,
9415
12049
  configDir: options.configDir
9416
12050
  });
9417
- const adapter = createActionHostAdapter(options);
12051
+ const adapter = createHostRunAdapter(options);
9418
12052
  await logPhase(log, "workspace", async () => {
9419
12053
  adapter.workspace.ensureWorkspaceSafeDirectory?.({
9420
12054
  rootDir: options.rootDir,
9421
12055
  env: options.env
9422
12056
  });
9423
12057
  });
9424
- if (eventName === "issue_comment") return await runIssueCommentActionCommand(options, adapter, log);
9425
- if (eventName === "pull_request_review_comment") return await runReviewCommentReplyActionCommand(options, adapter, log);
9426
- return await runPullRequestActionCommand(options, adapter, log);
12058
+ const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
12059
+ eventPath: options.eventPath,
12060
+ env: options.env ?? process.env,
12061
+ workspace: options.rootDir
12062
+ }));
12063
+ log.notice("event dispatch", { kind: event.kind });
12064
+ switch (event.kind) {
12065
+ case "ignored": return event;
12066
+ case "command-comment": return await runIssueCommentHostRunCommand(options, adapter, log, event.comment);
12067
+ case "review-comment-reply": return await runReviewCommentReplyHostRunCommand(options, adapter, log, event.reply);
12068
+ case "change-request": return await runChangeRequestHostRunCommand(options, adapter, log, event.change);
12069
+ }
9427
12070
  });
9428
12071
  }
9429
12072
  //#endregion
9430
- 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 };
12073
+ export { supportedOfficialInitRecipes as _, runInspectCommand as a, piRequiredCliFlags as b, createGitHubHostAdapter as c, createBitbucketClient as d, createAzureDevOpsClient as f, listOfficialInitRecipes as g, supportedOfficialInitAdapters as h, runInitCommand as i, PublicationError as l, redactPotentialSecrets as m, runHostRunCommand as n, runLocalReviewCommand as o, azureOrganizationFromUrl as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createGitLabClient as u, piBuiltinToolNames as v, piThinkingLevels as x, piReadOnlyToolNames as y };
9431
12074
 
9432
- //# sourceMappingURL=commands-DM63Wmy0.mjs.map
12075
+ //# sourceMappingURL=commands-ZZkXwqEn.mjs.map