@henryqw/pi-pr 4.0.6 → 4.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
|
|
|
70
70
|
| Base update required or merge conflict | Update from the base branch's current target when the tree is clean and local HEAD equals the PR head. |
|
|
71
71
|
| GitHub Actions job failed | Run the CI fix workflow when the same local prerequisite holds. |
|
|
72
72
|
| External check or commit status failed | Show `CI failed` as a no-action blocker. |
|
|
73
|
-
| Changes requested or unresolved review threads |
|
|
73
|
+
| Changes requested or unresolved review threads | Start or resume the package comment sweep when the same local prerequisite holds. |
|
|
74
74
|
| No-action state | Report the state without taking action. |
|
|
75
75
|
| Merge-ready pull request | Ask for final confirmation, recheck fresh state, and squash-merge if confirmed. |
|
|
76
76
|
|
|
@@ -90,6 +90,10 @@ The creation workflow repeats destination, remote OID, PR, and configuration che
|
|
|
90
90
|
|
|
91
91
|
Each helper workflow receives a random run ID and its first action. The run stays bound to one session, canonical worktree, route, and fresh authority. Helper calls from another run, session, worktree, or route fail.
|
|
92
92
|
|
|
93
|
+
For comment sweeps, `/pr` checks the package recovery file without changing it. It selects `start` when recovery is absent. It selects `resume` only when valid recovery matches the fresh route authority. Invalid recovery stays unchanged and blocks dispatch with its path and reason.
|
|
94
|
+
|
|
95
|
+
Direct skill or `pi_pr_*` tool calls cannot create route authority. Run `/pr` to reserve a fresh route.
|
|
96
|
+
|
|
93
97
|
Only one helper run can exist at a time. Most runs expire when the agent settles. A create or branch-update conflict stays available for one user-guided continuation, then expires after that continuation settles. Session replacement and shutdown forget the run without aborting or cleaning a pending merge.
|
|
94
98
|
|
|
95
99
|
After a `/pr` create workflow settles, the extension waits for a refresh that finds a configured current PR. It then prefixes the Herdr workspace label with `#<number> • `.
|
|
@@ -149,7 +153,7 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
|
|
|
149
153
|
|
|
150
154
|
- `/pr` accepts creation syntax only as a leading `--base BRANCH`, followed by optional creation guidance. It does not open a browser.
|
|
151
155
|
- It does not run `/done` or `/sweep`.
|
|
152
|
-
- Presentation refreshes do not auto-triage comments or start a workflow. The package comment sweep
|
|
156
|
+
- Presentation refreshes do not auto-triage comments or start a workflow. The package comment sweep starts or resumes only when an explicit `/pr` selects it.
|
|
153
157
|
- It does not enable auto-merge or add a merge queue.
|
|
154
158
|
- It does not rebase the local branch, overwrite concurrent remote updates, delete branches, or clean up worktrees. Creation uses exact leases plus ancestry checks; an empty lease is only an atomic absence check.
|
|
155
159
|
- Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
|
package/extensions/pr-command.ts
CHANGED
|
@@ -17,15 +17,17 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
|
|
19
19
|
|
|
20
|
-
const WORKFLOWS: Record<WorkflowNextStep, { command: string
|
|
21
|
-
create: { command: "skill:pi-pr-create"
|
|
22
|
-
"update-branch": { command: "skill:pi-pr-update-branch"
|
|
23
|
-
sweep: { command: "skill:pi-pr-comment-sweep"
|
|
24
|
-
"fix-ci": { command: "skill:pi-pr-fix-ci"
|
|
20
|
+
const WORKFLOWS: Record<WorkflowNextStep, { command: string }> = {
|
|
21
|
+
create: { command: "skill:pi-pr-create" },
|
|
22
|
+
"update-branch": { command: "skill:pi-pr-update-branch" },
|
|
23
|
+
sweep: { command: "skill:pi-pr-comment-sweep" },
|
|
24
|
+
"fix-ci": { command: "skill:pi-pr-fix-ci" },
|
|
25
25
|
};
|
|
26
26
|
export type WorkflowReservation =
|
|
27
27
|
| { route: "create"; target: PullRequestTarget; base?: string }
|
|
28
28
|
| { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
|
|
29
|
+
export type WorkflowLaunchAction = "prepare" | "merge" | "start" | "resume" | "collect";
|
|
30
|
+
export type WorkflowReservationResult = { runId: string; action: WorkflowLaunchAction };
|
|
29
31
|
|
|
30
32
|
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
31
33
|
export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
|
|
@@ -42,7 +44,7 @@ export type WorkflowPromptIdentity = Readonly<{
|
|
|
42
44
|
route: WorkflowNextStep;
|
|
43
45
|
skill: string;
|
|
44
46
|
runId: string;
|
|
45
|
-
action:
|
|
47
|
+
action: WorkflowLaunchAction;
|
|
46
48
|
}>;
|
|
47
49
|
|
|
48
50
|
export type PrCommandDependencies = {
|
|
@@ -52,7 +54,7 @@ export type PrCommandDependencies = {
|
|
|
52
54
|
reservation: WorkflowReservation,
|
|
53
55
|
ctx: ExtensionCommandContext,
|
|
54
56
|
invocation?: PrCommandInvocation,
|
|
55
|
-
) => Promise<
|
|
57
|
+
) => Promise<WorkflowReservationResult>;
|
|
56
58
|
markWorkflowPromptQueued?: (identity: WorkflowPromptIdentity, queued: boolean) => void;
|
|
57
59
|
releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
|
|
58
60
|
};
|
|
@@ -96,7 +98,7 @@ function packageWorkflowCommand(pi: PrCommandPi, route: WorkflowNextStep) {
|
|
|
96
98
|
candidate.sourceInfo.origin === "package"
|
|
97
99
|
);
|
|
98
100
|
if (!command) throw new Error(`${workflow.command} failed: bundled workflow is unavailable`);
|
|
99
|
-
return
|
|
101
|
+
return command;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async function dispatchWorkflow(
|
|
@@ -113,10 +115,11 @@ async function dispatchWorkflow(
|
|
|
113
115
|
const workflow = packageWorkflowCommand(pi, route);
|
|
114
116
|
let runId: string | undefined;
|
|
115
117
|
try {
|
|
116
|
-
|
|
118
|
+
const reserved = await reserve(reservation, ctx, invocation);
|
|
119
|
+
runId = reserved.runId;
|
|
117
120
|
invocation?.assertCurrent();
|
|
118
121
|
const queued = !ctx.isIdle();
|
|
119
|
-
const identity = { route, skill: workflow.
|
|
122
|
+
const identity = { route, skill: workflow.name, runId, action: reserved.action };
|
|
120
123
|
markPromptQueued(identity, queued);
|
|
121
124
|
const options = queued
|
|
122
125
|
? { deliverAs: "followUp" as const, expandPromptTemplates: true }
|
|
@@ -231,6 +231,16 @@ function sameLinkage(expected: SweepAuthority, current: SweepAuthority, remoteHe
|
|
|
231
231
|
current.target.remoteOid === remoteHead;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
function recoveryMatchesRouteAuthority(state: SweepState, suppliedAuthority: SweepAuthority): boolean {
|
|
235
|
+
const permittedHeads = new Set<string>();
|
|
236
|
+
if (state.attempts.push.state !== "applied") permittedHeads.add(state.original.lease);
|
|
237
|
+
if (
|
|
238
|
+
(state.attempts.push.state === "attempting" || state.attempts.push.state === "unknown" || state.attempts.push.state === "applied") &&
|
|
239
|
+
state.publicationHead
|
|
240
|
+
) permittedHeads.add(state.publicationHead);
|
|
241
|
+
return [...permittedHeads].some((head) => sameLinkage(state.authority, suppliedAuthority, head));
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
function feedbackMatchesAuthority(snapshot: FeedbackSnapshot, authority: SweepAuthority, head: string): boolean {
|
|
235
245
|
const current = snapshot.pullRequest;
|
|
236
246
|
return current.id === authority.id && current.number === authority.number && current.url === authority.url &&
|
|
@@ -575,6 +585,17 @@ export class PullRequestCommentSweep {
|
|
|
575
585
|
return (await this.location()).path;
|
|
576
586
|
}
|
|
577
587
|
|
|
588
|
+
async recoveryLaunchAction(): Promise<"start" | "resume"> {
|
|
589
|
+
if (!this.suppliedAuthority) throw new Error("Comment sweep recovery inspection requires route authority");
|
|
590
|
+
const location = await this.location();
|
|
591
|
+
const state = await this.loadIfPresent(location);
|
|
592
|
+
if (!state) return "start";
|
|
593
|
+
if (!recoveryMatchesRouteAuthority(state, this.suppliedAuthority)) {
|
|
594
|
+
throw new Error(`Comment sweep recovery is preserved at ${location.path}: recovery does not match freshly discovered route authority`);
|
|
595
|
+
}
|
|
596
|
+
return "resume";
|
|
597
|
+
}
|
|
598
|
+
|
|
578
599
|
private async loadState(location: Awaited<ReturnType<PullRequestCommentSweep["location"]>>): Promise<SweepState> {
|
|
579
600
|
const raw = await readTextFileBounded(location.path, SWEEP_RECOVERY_MAX_BYTES, { signal: this.signal });
|
|
580
601
|
let value: unknown;
|
|
@@ -789,17 +810,10 @@ export class PullRequestCommentSweep {
|
|
|
789
810
|
async resume(): Promise<SweepStatus> {
|
|
790
811
|
return await withWorktreeLock(this.cwd, async () => {
|
|
791
812
|
if (!this.suppliedAuthority) throw new Error("Comment sweep resume requires route authority");
|
|
792
|
-
const suppliedAuthority = this.suppliedAuthority;
|
|
793
813
|
const location = await this.location();
|
|
794
814
|
const state = await this.loadState(location);
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (
|
|
798
|
-
(state.attempts.push.state === "attempting" || state.attempts.push.state === "unknown" || state.attempts.push.state === "applied") &&
|
|
799
|
-
state.publicationHead
|
|
800
|
-
) permittedHeads.add(state.publicationHead);
|
|
801
|
-
if (![...permittedHeads].some((head) => sameLinkage(state.authority, suppliedAuthority, head))) {
|
|
802
|
-
throw new Error("Comment sweep recovery does not match supplied route authority");
|
|
815
|
+
if (!recoveryMatchesRouteAuthority(state, this.suppliedAuthority)) {
|
|
816
|
+
throw new Error(`Comment sweep recovery is preserved at ${location.path}: recovery does not match supplied route authority`);
|
|
803
817
|
}
|
|
804
818
|
await this.reconcile(state);
|
|
805
819
|
state.attempts.resolutions = state.attempts.resolutions.filter(({ state: attempt }) => attempt !== "blocked");
|
package/extensions/pr.ts
CHANGED
|
@@ -140,7 +140,7 @@ const FixCiParameters = Type.Union([
|
|
|
140
140
|
|
|
141
141
|
type UpdateBranchWorkflow = Pick<PullRequestBranchUpdater, "state" | "merge" | "continue" | "publish">;
|
|
142
142
|
type CreateWorkflow = Pick<PullRequestCreator, "state" | "prepare" | "merge" | "continue" | "push" | "publish">;
|
|
143
|
-
type SweepWorkflow = Pick<PullRequestCommentSweep, "start" | "resume" | "show" | "record" | "publish" | "refresh" | "resolve" | "finalize">;
|
|
143
|
+
type SweepWorkflow = Pick<PullRequestCommentSweep, "recoveryLaunchAction" | "start" | "resume" | "show" | "record" | "publish" | "refresh" | "resolve" | "finalize">;
|
|
144
144
|
type FixCiWorkflow = Pick<PullRequestCiFixer, "collect" | "publish">;
|
|
145
145
|
|
|
146
146
|
type WorkflowContextBase = {
|
|
@@ -337,7 +337,7 @@ export default function pullRequestExtension(
|
|
|
337
337
|
loadCurrentPullRequest: load,
|
|
338
338
|
}),
|
|
339
339
|
};
|
|
340
|
-
|
|
340
|
+
return { runId, action: "merge" };
|
|
341
341
|
case "create":
|
|
342
342
|
workflowContext = {
|
|
343
343
|
...common,
|
|
@@ -350,9 +350,9 @@ export default function pullRequestExtension(
|
|
|
350
350
|
loadCurrentPullRequest: load,
|
|
351
351
|
}),
|
|
352
352
|
};
|
|
353
|
-
|
|
354
|
-
case "sweep":
|
|
355
|
-
|
|
353
|
+
return { runId, action: "prepare" };
|
|
354
|
+
case "sweep": {
|
|
355
|
+
const selected: Extract<WorkflowContext, { route: "sweep" }> = {
|
|
356
356
|
...common,
|
|
357
357
|
route: "sweep",
|
|
358
358
|
workflow: createCommentSweep({
|
|
@@ -362,7 +362,17 @@ export default function pullRequestExtension(
|
|
|
362
362
|
loadCurrentPullRequest: load,
|
|
363
363
|
}),
|
|
364
364
|
};
|
|
365
|
-
|
|
365
|
+
workflowContext = selected;
|
|
366
|
+
try {
|
|
367
|
+
const action = await selected.workflow.recoveryLaunchAction();
|
|
368
|
+
invocation.assertCurrent();
|
|
369
|
+
if (workflowContext !== selected) throw new Error("PR workflow session changed during recovery inspection");
|
|
370
|
+
return { runId, action };
|
|
371
|
+
} catch (error) {
|
|
372
|
+
clearWorkflow(selected);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
366
376
|
case "fix-ci":
|
|
367
377
|
workflowContext = {
|
|
368
378
|
...common,
|
|
@@ -374,9 +384,8 @@ export default function pullRequestExtension(
|
|
|
374
384
|
loadCurrentPullRequest: load,
|
|
375
385
|
}),
|
|
376
386
|
};
|
|
377
|
-
|
|
387
|
+
return { runId, action: "collect" };
|
|
378
388
|
}
|
|
379
|
-
return common.runId;
|
|
380
389
|
};
|
|
381
390
|
|
|
382
391
|
const markWorkflowPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]> = (identity, queued) => {
|
|
@@ -403,7 +412,7 @@ export default function pullRequestExtension(
|
|
|
403
412
|
) => {
|
|
404
413
|
signal?.throwIfAborted();
|
|
405
414
|
const selected = workflowContext;
|
|
406
|
-
if (!selected) throw new Error("No PR workflow is active");
|
|
415
|
+
if (!selected) throw new Error("No PR workflow is active; run /pr to discover and reserve the current route");
|
|
407
416
|
if (selected.runId !== runId) throw new Error("PR workflow runId is wrong or stale");
|
|
408
417
|
if (selected.sessionGeneration !== sessionGeneration) throw new Error("PR workflow session is stale");
|
|
409
418
|
if (selected.route !== route) throw new Error(`PR workflow route is ${selected.route}, not ${route}`);
|
package/package.json
CHANGED
|
@@ -9,9 +9,10 @@ Use the package-owned comment-sweep workflow. It exposes these closed actions:
|
|
|
9
9
|
`start`, `resume`, `show`, `record`, `publish`, `refresh`, `resolve`, and
|
|
10
10
|
`finalize`.
|
|
11
11
|
|
|
12
|
-
1.
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
1. Use the `start` or `resume` action supplied by `/pr`. `/pr` checks saved work
|
|
13
|
+
and chooses the action. Do not change it. Direct skill or tool calls cannot
|
|
14
|
+
create route authority; run `/pr` instead. Never replace or delete blocked
|
|
15
|
+
recovery state by hand. See [Sweep recovery](references/recovery.md).
|
|
15
16
|
2. Use `show` for one feedback ID at a time. Inspect every conversation
|
|
16
17
|
comment, review, thread, and thread comment. Follow
|
|
17
18
|
[Thread triage](references/thread-triage.md).
|
|
@@ -10,10 +10,14 @@ The file is private, bounded to 1 MiB, and replaced atomically. It contains the
|
|
|
10
10
|
frozen PR authority, original head and lease, complete feedback, exact ledger,
|
|
11
11
|
owned paths, and mutation attempts.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
Run `/pr` to enter recovery. After fresh route discovery, `/pr` checks this file
|
|
14
|
+
without changing it. It selects `start` when the file is absent. It selects
|
|
15
|
+
`resume` only when valid recovery matches the fresh route authority.
|
|
16
|
+
|
|
17
|
+
Resume checks the canonical worktree, local changes, PR linkage, and remote
|
|
18
|
+
head again under its lock. It reconciles an attempted push or thread resolution
|
|
19
|
+
before issuing a new epoch and run ID. Calls from the old run then fail. Direct
|
|
20
|
+
skill or tool calls cannot create route authority.
|
|
17
21
|
|
|
18
22
|
A completed post-publish `refresh` stores the new complete snapshot before any
|
|
19
23
|
replacement ledger. Recovery keeps that snapshot in `refresh-pending`, with its
|
|
@@ -22,7 +26,9 @@ Use `show` with the resumed guard to inspect each frozen item. Then use `record`
|
|
|
22
26
|
without `ownedPaths` to supply exact complete coverage for that snapshot.
|
|
23
27
|
Resolution and finalization remain blocked until this record succeeds.
|
|
24
28
|
|
|
25
|
-
Malformed
|
|
26
|
-
repair, move, replace, or delete it
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
Malformed, oversized, obsolete, wrong-worktree, or route-mismatched recovery is
|
|
30
|
+
preserved and blocks dispatch. Never repair, move, replace, or delete it
|
|
31
|
+
automatically. Report the state path and exact blocker.
|
|
32
|
+
|
|
33
|
+
An unknown mutation is never replayed. If reconciliation cannot prove its exact
|
|
34
|
+
result, stop and report the state path and blocker.
|