@henryqw/pi-pr 6.0.4 → 6.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/pr-ci.ts +6 -14
- package/extensions/pr-command.ts +4 -1
- package/extensions/pr-comment-sweep.ts +7 -16
- package/extensions/pr-create.ts +1 -3
- package/extensions/pr-execution.ts +2 -4
- package/extensions/pr-github.ts +8 -10
- package/extensions/pr-routing.ts +7 -8
- package/extensions/pr-ui.ts +2 -2
- package/extensions/pr-update-branch.ts +1 -4
- package/extensions/pr.ts +3 -6
- package/package.json +1 -1
- package/skills/pi-pr-comment-sweep/scripts/pr-feedback.mjs +4 -19
- package/skills/pi-pr-fix-ci/SKILL.md +1 -1
package/extensions/pr-ci.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
extensionExecApi,
|
|
13
13
|
inspectWorktree,
|
|
14
14
|
isAncestor,
|
|
15
|
+
parseSingleOutputLine,
|
|
15
16
|
readHead,
|
|
16
17
|
readRemoteOid,
|
|
17
18
|
requiredOid,
|
|
@@ -177,13 +178,6 @@ function parseJson(output: string, label: string): unknown {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
function outputLine(output: string, label: string): string {
|
|
181
|
-
const normalized = output.replace(/\r\n/g, "\n");
|
|
182
|
-
const lines = (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n");
|
|
183
|
-
if (lines.length !== 1 || !lines[0]) throw new Error(`${label} returned invalid output`);
|
|
184
|
-
return requiredText(lines[0], label);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
181
|
function actionsUrl(authority: CurrentPullRequest, suffix: string): string {
|
|
188
182
|
const url = new URL(authority.url.origin);
|
|
189
183
|
url.pathname = `/${authority.base.repository}/actions/${suffix}`;
|
|
@@ -312,9 +306,7 @@ export class PullRequestCiFixer {
|
|
|
312
306
|
private collectedFingerprint?: string;
|
|
313
307
|
|
|
314
308
|
constructor(options: PullRequestCiFixOptions) {
|
|
315
|
-
if (!options.authority || options.authority.
|
|
316
|
-
options.authority.target.provenance !== "configured" || options.authority.conditions.ci !== "failure" ||
|
|
317
|
-
options.authority.local.worktree !== "clean" || options.authority.local.head !== "equal") {
|
|
309
|
+
if (!options.authority || options.authority.target.provenance !== "configured") {
|
|
318
310
|
throw new TypeError("CI repair requires a configured failed pull request with a clean equal local HEAD");
|
|
319
311
|
}
|
|
320
312
|
const head = requiredOid(options.authority.head.oid, "pull request head OID");
|
|
@@ -357,10 +349,10 @@ export class PullRequestCiFixer {
|
|
|
357
349
|
}
|
|
358
350
|
|
|
359
351
|
private async requireOriginalLocal(): Promise<void> {
|
|
360
|
-
const branch =
|
|
352
|
+
const branch = requiredText(parseSingleOutputLine(
|
|
361
353
|
(await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout,
|
|
362
354
|
"current branch",
|
|
363
|
-
);
|
|
355
|
+
), "current branch");
|
|
364
356
|
if (branch !== this.authority.target.branch) throw new Error("CI repair cancelled: current branch changed");
|
|
365
357
|
if (await inspectWorktree(this.exec, this.options()) !== "clean") {
|
|
366
358
|
throw new Error("CI repair requires a clean worktree with no Git operation in progress");
|
|
@@ -695,10 +687,10 @@ export class PullRequestCiFixer {
|
|
|
695
687
|
await this.requireCollectedEvidence();
|
|
696
688
|
const original = this.authority.head.oid;
|
|
697
689
|
await this.requireSavedDestination(original);
|
|
698
|
-
const branch =
|
|
690
|
+
const branch = requiredText(parseSingleOutputLine(
|
|
699
691
|
(await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout,
|
|
700
692
|
"current branch",
|
|
701
|
-
);
|
|
693
|
+
), "current branch");
|
|
702
694
|
if (branch !== this.authority.target.branch) throw new Error("CI repair publish cancelled: current branch changed");
|
|
703
695
|
if (await inspectWorktree(this.exec, this.options()) !== "clean") {
|
|
704
696
|
throw new Error("CI repair publish requires a clean worktree with no Git operation in progress");
|
package/extensions/pr-command.ts
CHANGED
|
@@ -288,9 +288,12 @@ export function createPrCommandHandler(
|
|
|
288
288
|
const discovery = await load(pi, ctx, undefined, undefined, base);
|
|
289
289
|
commandInvocation?.assertCurrent();
|
|
290
290
|
const decision = deriveRouteDecision(discovery, intent);
|
|
291
|
+
if (decision.kind === "feedback-blocked") {
|
|
292
|
+
onRouteResolved?.("blocked");
|
|
293
|
+
throw new Error(feedbackBlockerMessage(decision.blocker));
|
|
294
|
+
}
|
|
291
295
|
const nextStep = decision.nextStep;
|
|
292
296
|
onRouteResolved?.(nextStep);
|
|
293
|
-
if (decision.kind === "feedback-blocked") throw new Error(feedbackBlockerMessage(decision.blocker));
|
|
294
297
|
if (base !== undefined && nextStep !== "create") {
|
|
295
298
|
throw new Error("/pr --base is accepted only for pull request creation");
|
|
296
299
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { realpath, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
import { spawnBounded, type Exec, type ExecOptions } from "@henryqw/pi-process";
|
|
5
6
|
import {
|
|
6
7
|
extensionConfigDir,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
isAncestor,
|
|
33
34
|
isRecord,
|
|
34
35
|
parseNulPaths,
|
|
36
|
+
parseSingleOutputLine,
|
|
35
37
|
parseStatusSnapshot,
|
|
36
38
|
readHead,
|
|
37
39
|
readRemoteOid,
|
|
@@ -342,16 +344,12 @@ function parseProjection(value: unknown): SweepFinalProjection {
|
|
|
342
344
|
};
|
|
343
345
|
}
|
|
344
346
|
|
|
345
|
-
function sameProjection(left: SweepFinalProjection, right: SweepFinalProjection): boolean {
|
|
346
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
347
|
function projectionMatches(snapshot: FeedbackSnapshot, projection: SweepFinalProjection): boolean {
|
|
350
348
|
if (feedbackContentFingerprint(snapshot) !== projection.contentFingerprint) return false;
|
|
351
349
|
const items = feedbackEntries(snapshot).map(({ id, kind }) => ({ id, kind })).sort((left, right) =>
|
|
352
350
|
left.id.localeCompare(right.id) || left.kind.localeCompare(right.kind));
|
|
353
351
|
const threads = snapshot.reviewThreads.map(({ id, isResolved }) => ({ id, isResolved })).sort((left, right) => left.id.localeCompare(right.id));
|
|
354
|
-
return
|
|
352
|
+
return isDeepStrictEqual(items, projection.items) && isDeepStrictEqual(threads, projection.threads);
|
|
355
353
|
}
|
|
356
354
|
|
|
357
355
|
function resolutionAttempt(value: unknown, index: number): ResolutionAttempt {
|
|
@@ -463,7 +461,7 @@ function parseState(value: unknown, expectedRoot: string, expectedId: string): S
|
|
|
463
461
|
throw new Error("push attempt head does not match publication head");
|
|
464
462
|
}
|
|
465
463
|
if (hasFinalProjection !== (projection !== null)) throw new Error("sweep phase and final projection are inconsistent");
|
|
466
|
-
if (projection && (!ledger || !
|
|
464
|
+
if (projection && (!ledger || !isDeepStrictEqual(projection, buildProjection(feedback.generation, snapshot, ledger)))) {
|
|
467
465
|
throw new Error("final projection does not match feedback and ledger coverage");
|
|
468
466
|
}
|
|
469
467
|
return state;
|
|
@@ -505,13 +503,6 @@ function status(state: SweepState): SweepStatus {
|
|
|
505
503
|
};
|
|
506
504
|
}
|
|
507
505
|
|
|
508
|
-
function oneLine(output: string, label: string): string {
|
|
509
|
-
const normalized = output.replace(/\r\n/g, "\n");
|
|
510
|
-
const lines = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
|
|
511
|
-
if (lines.length !== 1 || !lines[0]) throw new Error(`${label} returned invalid output`);
|
|
512
|
-
return lines[0];
|
|
513
|
-
}
|
|
514
|
-
|
|
515
506
|
function onlyResolutionChanged(before: FeedbackSnapshot, after: FeedbackSnapshot, threadId: string): boolean {
|
|
516
507
|
if (feedbackContentFingerprint(before) !== feedbackContentFingerprint(after)) return false;
|
|
517
508
|
const beforeStates = before.reviewThreads.map(({ id, isResolved }) => ({ id, isResolved })).sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -564,7 +555,7 @@ export class PullRequestCommentSweep {
|
|
|
564
555
|
}
|
|
565
556
|
|
|
566
557
|
private async location(): Promise<{ root: string; id: string; path: string }> {
|
|
567
|
-
const top =
|
|
558
|
+
const top = parseSingleOutputLine((await runChecked(this.exec, "git", ["rev-parse", "--show-toplevel"], this.options())).stdout, "Git worktree root");
|
|
568
559
|
const root = await realpath(top);
|
|
569
560
|
const id = createHash("sha256").update(root).digest("hex");
|
|
570
561
|
return { root, id, path: join(extensionConfigDir("pi-pr", this.agentDir), "sweep", id, STATE_FILE) };
|
|
@@ -1016,7 +1007,7 @@ export class PullRequestCommentSweep {
|
|
|
1016
1007
|
throw new Error("Comment sweep is not ready to finalize");
|
|
1017
1008
|
}
|
|
1018
1009
|
const projection = parseProjection(projectionInput);
|
|
1019
|
-
if (!
|
|
1010
|
+
if (!isDeepStrictEqual(projection, state.projection)) throw new Error("Final projection does not match the declared refreshed projection");
|
|
1020
1011
|
const checks = parseChecks(checksInput);
|
|
1021
1012
|
if (state.attempts.resolutions.some(({ state: attempt }) => attempt !== "applied")) {
|
|
1022
1013
|
throw new Error("Comment sweep has unresolved or unknown mutation attempts");
|
|
@@ -1026,7 +1017,7 @@ export class PullRequestCommentSweep {
|
|
|
1026
1017
|
}
|
|
1027
1018
|
await this.freshProjection(state);
|
|
1028
1019
|
const alreadyChecked = state.attempts.finalize.state === "applied" &&
|
|
1029
|
-
|
|
1020
|
+
isDeepStrictEqual(state.attempts.finalize.checks, checks);
|
|
1030
1021
|
if (!alreadyChecked) {
|
|
1031
1022
|
state.attempts.finalize = { state: "attempting", checks };
|
|
1032
1023
|
await this.save(location, state);
|
package/extensions/pr-create.ts
CHANGED
|
@@ -173,9 +173,7 @@ export class PullRequestCreator {
|
|
|
173
173
|
private explicitBase?: string;
|
|
174
174
|
|
|
175
175
|
constructor(options: CreatePullRequestOptions) {
|
|
176
|
-
if (
|
|
177
|
-
throw new TypeError("PR creation requires a validated creation target");
|
|
178
|
-
}
|
|
176
|
+
if (options.target.remoteOid !== null) requiredOid(options.target.remoteOid, "remote OID");
|
|
179
177
|
this.cwd = options.cwd;
|
|
180
178
|
this.target = { ...options.target };
|
|
181
179
|
this.noTarget = options.target.provenance === "inferred" && options.target.remoteOid === null;
|
|
@@ -115,10 +115,8 @@ export async function withWorktreeLock<T>(
|
|
|
115
115
|
cwd: requiredText(cwd, "cwd"),
|
|
116
116
|
signal: options.signal,
|
|
117
117
|
});
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
if (rootLines.length !== 1 || !rootLines[0]) throw new Error("Git worktree root resolution returned invalid output");
|
|
121
|
-
const canonical = requiredText(await realpath(rootLines[0]), "canonical Git worktree root");
|
|
118
|
+
const root = parseSingleOutputLine(rootResult.stdout, "Git worktree root resolution");
|
|
119
|
+
const canonical = requiredText(await realpath(root), "canonical Git worktree root");
|
|
122
120
|
const lockNamespace = resolve(extensionConfigDir("pi-pr", options.agentDir));
|
|
123
121
|
const lockDirectory = join(lockNamespace, "worktree-locks");
|
|
124
122
|
const identity = createHash("sha256").update(canonical).digest("hex");
|
package/extensions/pr-github.ts
CHANGED
|
@@ -197,8 +197,6 @@ type PullRequestCandidate = {
|
|
|
197
197
|
headOid: string;
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
type SearchPullRequest = PullRequestCandidate;
|
|
201
|
-
|
|
202
200
|
export type PullRequestPublication = {
|
|
203
201
|
number: number;
|
|
204
202
|
url: URL;
|
|
@@ -224,7 +222,7 @@ export type BranchUpstreamTarget = {
|
|
|
224
222
|
};
|
|
225
223
|
|
|
226
224
|
type SearchSelection =
|
|
227
|
-
| { kind: "candidate"; candidate:
|
|
225
|
+
| { kind: "candidate"; candidate: PullRequestCandidate; pullRequest: ListedPullRequest | null }
|
|
228
226
|
| { kind: "none" }
|
|
229
227
|
| { kind: "ambiguous"; urls: URL[] }
|
|
230
228
|
| { kind: "oid-mismatch"; urls: URL[] }
|
|
@@ -232,7 +230,7 @@ type SearchSelection =
|
|
|
232
230
|
|
|
233
231
|
type SearchPage = {
|
|
234
232
|
totalCount: number;
|
|
235
|
-
candidates:
|
|
233
|
+
candidates: PullRequestCandidate[];
|
|
236
234
|
cursors: string[];
|
|
237
235
|
hasNextPage: boolean;
|
|
238
236
|
endCursor: string | null;
|
|
@@ -676,7 +674,7 @@ function listedPullRequest(value: unknown): ListedPullRequest | null {
|
|
|
676
674
|
};
|
|
677
675
|
}
|
|
678
676
|
|
|
679
|
-
function searchPullRequest(value: unknown, host: string):
|
|
677
|
+
function searchPullRequest(value: unknown, host: string): PullRequestCandidate {
|
|
680
678
|
if (!isRecord(value) || value.__typename !== "PullRequest") {
|
|
681
679
|
fail("Find pull requests", "invalid GitHub CLI output");
|
|
682
680
|
}
|
|
@@ -742,7 +740,7 @@ function parseSearchPage(output: string, pushTarget: Pick<PushTarget, "repositor
|
|
|
742
740
|
!Array.isArray(search.edges) || search.edges.length > PR_DISCOVERY_PAGE_SIZE ||
|
|
743
741
|
!isRecord(search.pageInfo)
|
|
744
742
|
) fail("Find pull requests", "invalid GitHub CLI output");
|
|
745
|
-
const candidates:
|
|
743
|
+
const candidates: PullRequestCandidate[] = [];
|
|
746
744
|
const cursors: string[] = [];
|
|
747
745
|
for (const edge of search.edges) {
|
|
748
746
|
if (!isRecord(edge)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
@@ -766,7 +764,7 @@ function parseSearchPage(output: string, pushTarget: Pick<PushTarget, "repositor
|
|
|
766
764
|
};
|
|
767
765
|
}
|
|
768
766
|
|
|
769
|
-
function matchingSearchPullRequests(candidates:
|
|
767
|
+
function matchingSearchPullRequests(candidates: PullRequestCandidate[], pushTarget: PushTarget): PullRequestCandidate[] {
|
|
770
768
|
return candidates.filter((candidate) =>
|
|
771
769
|
candidate.url.hostname.toLowerCase() === pushTarget.repository.host &&
|
|
772
770
|
candidate.headRepository !== null &&
|
|
@@ -775,7 +773,7 @@ function matchingSearchPullRequests(candidates: SearchPullRequest[], pushTarget:
|
|
|
775
773
|
);
|
|
776
774
|
}
|
|
777
775
|
|
|
778
|
-
function selectSearchPullRequest(candidates:
|
|
776
|
+
function selectSearchPullRequest(candidates: PullRequestCandidate[], pushTarget: PushTarget): SearchSelection {
|
|
779
777
|
const matching = matchingSearchPullRequests(candidates, pushTarget);
|
|
780
778
|
const open = matching.filter((candidate) => candidate.lifecycle === "open");
|
|
781
779
|
if (open.length > 1) return { kind: "ambiguous", urls: open.map(({ url }) => url) };
|
|
@@ -1681,9 +1679,9 @@ async function enumerateSearchPullRequests(
|
|
|
1681
1679
|
pi: Pick<ExtensionAPI, "exec">,
|
|
1682
1680
|
context: PullRequestLoadContext,
|
|
1683
1681
|
target: Pick<PushTarget, "repository" | "ref">,
|
|
1684
|
-
): Promise<
|
|
1682
|
+
): Promise<PullRequestCandidate[] | null> {
|
|
1685
1683
|
const [owner, name] = target.repository.nameWithOwner.split("/");
|
|
1686
|
-
const candidates:
|
|
1684
|
+
const candidates: PullRequestCandidate[] = [];
|
|
1687
1685
|
const cursors = new Set<string>();
|
|
1688
1686
|
let totalCount: number | null = null;
|
|
1689
1687
|
let endCursor: string | null = null;
|
package/extensions/pr-routing.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type FeedbackRouteBlocker =
|
|
|
71
71
|
| { kind: "head-not-equal"; relation: Exclude<LocalHeadRelation, "equal"> };
|
|
72
72
|
type RouteDecision =
|
|
73
73
|
| { kind: "selected"; nextStep: NextStep }
|
|
74
|
-
| { kind: "feedback-blocked";
|
|
74
|
+
| { kind: "feedback-blocked"; blocker: FeedbackRouteBlocker };
|
|
75
75
|
|
|
76
76
|
function localMutationSafe(local: LocalMergeSafety): boolean {
|
|
77
77
|
return local.worktree === "clean" && local.head === "equal";
|
|
@@ -122,25 +122,24 @@ export function deriveRouteDecision(
|
|
|
122
122
|
): RouteDecision {
|
|
123
123
|
if (intent === "automatic") return { kind: "selected", nextStep: deriveAutomaticNextStep(discovery) };
|
|
124
124
|
if (discovery.kind === "blocked") {
|
|
125
|
-
return { kind: "feedback-blocked",
|
|
125
|
+
return { kind: "feedback-blocked", blocker: { kind: "discovery-blocked" } };
|
|
126
126
|
}
|
|
127
127
|
if (discovery.kind !== "current") {
|
|
128
|
-
return { kind: "feedback-blocked",
|
|
128
|
+
return { kind: "feedback-blocked", blocker: { kind: "pull-request-unavailable" } };
|
|
129
129
|
}
|
|
130
130
|
const pullRequest = discovery.pullRequest;
|
|
131
131
|
if (pullRequest.lifecycle !== "open") {
|
|
132
|
-
return { kind: "feedback-blocked",
|
|
132
|
+
return { kind: "feedback-blocked", blocker: { kind: "pull-request-not-open" } };
|
|
133
133
|
}
|
|
134
134
|
if (pullRequest.target.provenance !== "configured") {
|
|
135
|
-
return { kind: "feedback-blocked",
|
|
135
|
+
return { kind: "feedback-blocked", blocker: { kind: "target-not-configured" } };
|
|
136
136
|
}
|
|
137
137
|
if (pullRequest.local.worktree !== "clean") {
|
|
138
|
-
return { kind: "feedback-blocked",
|
|
138
|
+
return { kind: "feedback-blocked", blocker: { kind: "worktree-dirty" } };
|
|
139
139
|
}
|
|
140
140
|
if (pullRequest.local.head !== "equal") {
|
|
141
141
|
return {
|
|
142
142
|
kind: "feedback-blocked",
|
|
143
|
-
nextStep: "blocked",
|
|
144
143
|
blocker: { kind: "head-not-equal", relation: pullRequest.local.head },
|
|
145
144
|
};
|
|
146
145
|
}
|
|
@@ -148,5 +147,5 @@ export function deriveRouteDecision(
|
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
export function deriveNextStep(discovery: PullRequestDiscovery<PullRequest & { target: PullRequestTarget }>): NextStep {
|
|
151
|
-
return
|
|
150
|
+
return deriveAutomaticNextStep(discovery);
|
|
152
151
|
}
|
package/extensions/pr-ui.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export type PrDisplayInput = PullRequest & {
|
|
13
13
|
number: number;
|
|
14
|
-
url:
|
|
14
|
+
url: URL;
|
|
15
15
|
approved: boolean;
|
|
16
16
|
target: PullRequestTarget;
|
|
17
17
|
};
|
|
@@ -140,7 +140,7 @@ export function projectPrDisplay(
|
|
|
140
140
|
nextStep,
|
|
141
141
|
footer: {
|
|
142
142
|
number: input.number,
|
|
143
|
-
url:
|
|
143
|
+
url: input.url.href,
|
|
144
144
|
...footerStatus(input),
|
|
145
145
|
},
|
|
146
146
|
widget: widgetText(input, nextStep),
|
|
@@ -62,12 +62,9 @@ export class PullRequestBranchUpdater {
|
|
|
62
62
|
private readonly load: Load;
|
|
63
63
|
|
|
64
64
|
constructor(options: UpdateBranchOptions) {
|
|
65
|
-
if (!options.authority || options.authority.
|
|
65
|
+
if (!options.authority || options.authority.target.provenance !== "configured") {
|
|
66
66
|
throw new TypeError("Branch update requires a configured open pull request");
|
|
67
67
|
}
|
|
68
|
-
if (!options.authority.conditions.baseUpdateRequired && !options.authority.conditions.conflict) {
|
|
69
|
-
throw new TypeError("Pull request does not require a branch update");
|
|
70
|
-
}
|
|
71
68
|
this.cwd = options.cwd;
|
|
72
69
|
this.authority = cloneCurrentPullRequest(options.authority);
|
|
73
70
|
this.signal = options.signal;
|
package/extensions/pr.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
samePullRequestObservation,
|
|
27
27
|
type PullRequestObservation,
|
|
28
28
|
} from "./pr-github.ts";
|
|
29
|
-
import { isRecord, runChecked } from "./pr-execution.ts";
|
|
29
|
+
import { isRecord, parseSingleOutputLine, runChecked } from "./pr-execution.ts";
|
|
30
30
|
import {
|
|
31
31
|
discoveryIssueKey,
|
|
32
32
|
discoveryIssueMessage,
|
|
@@ -48,7 +48,7 @@ const GH_PR_CREATE = /(?:^|[;&|]\s*|\n\s*)gh\s+pr\s+create(?=\s|$|[;&|])/;
|
|
|
48
48
|
const GIT_COMMIT = /(?:^|[;&|]\s*|\n\s*)git\s+commit(?=\s|$|[;&|])/;
|
|
49
49
|
const GIT_PUSH = /(?:^|[;&|]\s*|\n\s*)git\s+push(?=\s|$|[;&|])/;
|
|
50
50
|
const WORKFLOW_ROUTES = new Set(["create", "update-branch", "sweep", "fix-ci"]);
|
|
51
|
-
const DELEGATED_TOOLS = new Set(["delegate_task"
|
|
51
|
+
const DELEGATED_TOOLS = new Set(["delegate_task"]);
|
|
52
52
|
const CLOSED = { additionalProperties: false } as const;
|
|
53
53
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
54
54
|
|
|
@@ -171,10 +171,7 @@ type PullRequestExtensionDependencies = {
|
|
|
171
171
|
|
|
172
172
|
async function canonicalWorktree(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
173
173
|
const result = await runChecked(spawnBounded, "git", ["rev-parse", "--show-toplevel"], { cwd, signal });
|
|
174
|
-
|
|
175
|
-
const lines = (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n");
|
|
176
|
-
if (lines.length !== 1 || !lines[0]) throw new Error("Git worktree root resolution returned invalid output");
|
|
177
|
-
return await realpath(lines[0]);
|
|
174
|
+
return await realpath(parseSingleOutputLine(result.stdout, "Git worktree root resolution"));
|
|
178
175
|
}
|
|
179
176
|
|
|
180
177
|
function toolResult(value: unknown) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { chmod, mkdtemp,
|
|
5
|
+
import { chmod, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
8
|
import { readTextFileBounded } from "@henryqw/pi-config-store";
|
|
@@ -118,28 +118,13 @@ async function requireCleanHead(cwd, signal, pullRequest, expectedHead = pullReq
|
|
|
118
118
|
async function writeSnapshotAtomically(path, contents, signal) {
|
|
119
119
|
signal.throwIfAborted();
|
|
120
120
|
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
121
|
-
let file;
|
|
122
|
-
let created = false;
|
|
123
121
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
signal.throwIfAborted();
|
|
127
|
-
if (process.platform !== "win32") await file.chmod(0o600);
|
|
128
|
-
signal.throwIfAborted();
|
|
129
|
-
await file.writeFile(contents, { encoding: "utf8", signal });
|
|
130
|
-
signal.throwIfAborted();
|
|
131
|
-
await file.sync();
|
|
132
|
-
signal.throwIfAborted();
|
|
133
|
-
await file.close();
|
|
134
|
-
file = undefined;
|
|
122
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", flag: "wx", mode: 0o600, flush: true, signal });
|
|
123
|
+
if (process.platform !== "win32") await chmod(temporaryPath, 0o600);
|
|
135
124
|
signal.throwIfAborted();
|
|
136
125
|
await rename(temporaryPath, path);
|
|
137
126
|
} catch (error) {
|
|
138
|
-
|
|
139
|
-
await file?.close();
|
|
140
|
-
} finally {
|
|
141
|
-
if (created) await rm(temporaryPath, { force: true });
|
|
142
|
-
}
|
|
127
|
+
await rm(temporaryPath, { force: true });
|
|
143
128
|
throw error;
|
|
144
129
|
}
|
|
145
130
|
}
|
|
@@ -25,7 +25,7 @@ report the blocker and stop.
|
|
|
25
25
|
## Diagnose and repair
|
|
26
26
|
|
|
27
27
|
1. Inspect the workflow and repository files implicated by the returned evidence.
|
|
28
|
-
2. Run the narrowest existing local reproducer before editing when one is available.
|
|
28
|
+
2. Run the narrowest existing local reproducer before editing when one is available. For package failures, reproduce with an empty dependency cache; do not use offline installs that still resolve registry metadata. Assemble bundled local package resources deterministically.
|
|
29
29
|
3. Find the root cause. Do not guess from a check name alone.
|
|
30
30
|
4. Stop if the evidence is insufficient or the fix needs a product decision.
|
|
31
31
|
5. Edit only the files needed for the diagnosed failure.
|