@mgiles/perk 3.1.0 → 3.3.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.
- package/README.md +5 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +369 -359
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +163 -265
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +128 -60
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/{doors → pi/v1}/lifecycleGates.ts +14 -59
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/pi/v1/objectivePlanning.ts +776 -0
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +152 -34
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +98 -10
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
- package/shared/contracts-history.md +0 -605
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// The v1 Pi installer for the curated-submission flow: `installCuratedSubmissionBindings` owns
|
|
2
|
+
// the warm `submit_pr_review` tool — the agent-driven curated-posting surface shared by the
|
|
3
|
+
// PR-review doors (`/pr-review-terminal`, `/pr-review-browser`, `/stack-review-browser`).
|
|
4
|
+
// Registration metadata is pinned by the suite's registration-parity tests; the policy (gate ladder, resume guard, session
|
|
5
|
+
// records) lives in `codeReview/submission.ts` — this module decodes at the tool boundary,
|
|
6
|
+
// composes the `perk pr review-submit` cold-door `ReviewSubmitter` (mutations canonical in
|
|
7
|
+
// Python; the batch rides the run-scratch stdin channel), selects the `FormalEventGate` arm
|
|
8
|
+
// from `ctx.hasUI`, opens the branch `WorkflowSession` at the execute site, and renders the
|
|
9
|
+
// Result envelope (`kind` → the error_type vocabulary; success texts).
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
type CuratedSubmission,
|
|
14
|
+
type FormalEventGate,
|
|
15
|
+
type InvalidAnchor,
|
|
16
|
+
type ReviewSubmitOutcome,
|
|
17
|
+
type ReviewSubmitter,
|
|
18
|
+
type SubmitBatch,
|
|
19
|
+
type SubmitComment,
|
|
20
|
+
type SubmitCuratedOutcome,
|
|
21
|
+
type SubmitOk,
|
|
22
|
+
submitCuratedReview,
|
|
23
|
+
} from "../../../codeReview/submission.ts";
|
|
24
|
+
import { openBranchWorkflowSession } from "../../../session/branchWorkflowSession.ts";
|
|
25
|
+
import {
|
|
26
|
+
booleanField,
|
|
27
|
+
type ColdDoorCtx,
|
|
28
|
+
type ColdJson,
|
|
29
|
+
type ExecHost,
|
|
30
|
+
numberField,
|
|
31
|
+
runColdDoor,
|
|
32
|
+
stringField,
|
|
33
|
+
} from "../../../substrate/coldDoor.ts";
|
|
34
|
+
import { failFor, ok, type Result } from "../../../substrate/result.ts";
|
|
35
|
+
import {
|
|
36
|
+
arrayParam,
|
|
37
|
+
booleanParam,
|
|
38
|
+
numberParam,
|
|
39
|
+
paramsOf,
|
|
40
|
+
stringParam,
|
|
41
|
+
type ToolParams,
|
|
42
|
+
} from "../../../substrate/toolParams.ts";
|
|
43
|
+
import type { Severity } from "../../../surfaces/report.ts";
|
|
44
|
+
|
|
45
|
+
// ------------------------------------------------------------------------ params
|
|
46
|
+
|
|
47
|
+
/** Decode the optional `comments` array; null = present-but-malformed (whole-batch refusal). */
|
|
48
|
+
function decodeSubmitComments(p: ToolParams): SubmitComment[] | undefined | null {
|
|
49
|
+
const raw = arrayParam(p, "comments");
|
|
50
|
+
if (raw === undefined) return undefined;
|
|
51
|
+
if (raw === null) return null;
|
|
52
|
+
const comments: SubmitComment[] = [];
|
|
53
|
+
for (const item of raw) {
|
|
54
|
+
const row = paramsOf(item);
|
|
55
|
+
if (row === null) return null;
|
|
56
|
+
const path = stringParam(row, "path");
|
|
57
|
+
const line = numberParam(row, "line");
|
|
58
|
+
const side = stringParam(row, "side");
|
|
59
|
+
const body = stringParam(row, "body");
|
|
60
|
+
if (typeof path !== "string" || path.length === 0) return null;
|
|
61
|
+
if (typeof line !== "number" || !Number.isInteger(line)) return null;
|
|
62
|
+
if (side !== undefined && side !== "LEFT" && side !== "RIGHT") return null;
|
|
63
|
+
if (typeof body !== "string" || body.length === 0) return null;
|
|
64
|
+
const comment: SubmitComment = { path, line, body };
|
|
65
|
+
if (side !== undefined) comment.side = side;
|
|
66
|
+
comments.push(comment);
|
|
67
|
+
}
|
|
68
|
+
return comments;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Strict-decode unknown tool-call params straight into the normalized `CuratedSubmission`
|
|
73
|
+
* feature input (the tool-boundary seam — no intermediate wire DTO: the two boolean defaults
|
|
74
|
+
* are applied here, absent ⇒ false). Mirrors `decodePostParams`: submitting a guessed/partial
|
|
75
|
+
* review is a durable GitHub mutation, so ANY malformed field ⇒ null (whole-batch refusal).
|
|
76
|
+
* `pr` must be an int; `event` exactly one of the three flag spellings; `body` a string (EMPTY
|
|
77
|
+
* ALLOWED — the cold door owns the event-conditioned body rule and reports `bad_batch`); each
|
|
78
|
+
* `comments` row strict on path/line(int)/side(LEFT|RIGHT)/body; `dry_run` and `allow_repost`
|
|
79
|
+
* booleans.
|
|
80
|
+
*/
|
|
81
|
+
export function decodeSubmitParams(params: unknown): CuratedSubmission | null {
|
|
82
|
+
const p = paramsOf(params);
|
|
83
|
+
if (p === null) return null;
|
|
84
|
+
const pr = numberParam(p, "pr");
|
|
85
|
+
if (typeof pr !== "number" || !Number.isInteger(pr)) return null;
|
|
86
|
+
const event = stringParam(p, "event");
|
|
87
|
+
if (event !== "approve" && event !== "request-changes" && event !== "comment") return null;
|
|
88
|
+
const body = stringParam(p, "body");
|
|
89
|
+
if (typeof body !== "string") return null;
|
|
90
|
+
const comments = decodeSubmitComments(p);
|
|
91
|
+
if (comments === null) return null;
|
|
92
|
+
const dryRun = booleanParam(p, "dry_run");
|
|
93
|
+
if (dryRun === null) return null;
|
|
94
|
+
const allowRepost = booleanParam(p, "allow_repost");
|
|
95
|
+
if (allowRepost === null) return null;
|
|
96
|
+
const result: CuratedSubmission = {
|
|
97
|
+
pr,
|
|
98
|
+
event,
|
|
99
|
+
body,
|
|
100
|
+
dryRun: dryRun === true,
|
|
101
|
+
allowRepost: allowRepost === true,
|
|
102
|
+
};
|
|
103
|
+
if (comments !== undefined) result.comments = comments;
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ------------------------------------------------------------------------ port productions
|
|
108
|
+
|
|
109
|
+
/** Narrow the cold door's `review-submit --json` payload to the fields the tool reports. */
|
|
110
|
+
function decodeSubmitResult(payload: ColdJson): SubmitOk {
|
|
111
|
+
return {
|
|
112
|
+
dry_run: booleanField(payload, "dry_run"),
|
|
113
|
+
pr: numberField(payload, "pr"),
|
|
114
|
+
event: stringField(payload, "event"),
|
|
115
|
+
mode: stringField(payload, "mode"),
|
|
116
|
+
comment_count: numberField(payload, "comment_count"),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Strict re-narrow of the `bad_anchors` fail payload's `invalid[]` rows. Null on ANY drift —
|
|
122
|
+
* uncertainty renders as a plain fail, never a half table.
|
|
123
|
+
*/
|
|
124
|
+
function decodeInvalidAnchors(payload: ColdJson): InvalidAnchor[] | null {
|
|
125
|
+
const raw = payload.invalid;
|
|
126
|
+
if (!Array.isArray(raw)) return null;
|
|
127
|
+
const rows: InvalidAnchor[] = [];
|
|
128
|
+
for (const item of raw) {
|
|
129
|
+
const row = paramsOf(item);
|
|
130
|
+
if (row === null) return null;
|
|
131
|
+
const index = row.index;
|
|
132
|
+
const path = row.path;
|
|
133
|
+
const line = row.line;
|
|
134
|
+
const side = row.side;
|
|
135
|
+
const reason = row.reason;
|
|
136
|
+
if (typeof index !== "number" || !Number.isInteger(index)) return null;
|
|
137
|
+
if (typeof path !== "string") return null;
|
|
138
|
+
if (typeof line !== "number" || !Number.isInteger(line)) return null;
|
|
139
|
+
if (typeof side !== "string") return null;
|
|
140
|
+
if (typeof reason !== "string") return null;
|
|
141
|
+
rows.push({ index, path, line, side, reason });
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The production `ReviewSubmitter`: the `perk pr review-submit` cold-door composition (the exact
|
|
148
|
+
* `--batch` shape: `{body, comments?}` — the event rides the flag). The adapter performs the
|
|
149
|
+
* strict `invalid[]` decode; decode drift ⇒ the `failed` arm.
|
|
150
|
+
*/
|
|
151
|
+
export function createColdDoorReviewSubmitter(pi: ExecHost, ctx: ColdDoorCtx): ReviewSubmitter {
|
|
152
|
+
return {
|
|
153
|
+
async submit(batch: SubmitBatch): Promise<ReviewSubmitOutcome> {
|
|
154
|
+
const payload: Record<string, unknown> = { body: batch.body };
|
|
155
|
+
if (batch.comments !== undefined) payload.comments = batch.comments;
|
|
156
|
+
const r = await runColdDoor<SubmitOk>(
|
|
157
|
+
pi,
|
|
158
|
+
ctx,
|
|
159
|
+
[
|
|
160
|
+
"pr",
|
|
161
|
+
"review-submit",
|
|
162
|
+
"--pr",
|
|
163
|
+
String(batch.pr),
|
|
164
|
+
"--event",
|
|
165
|
+
batch.event,
|
|
166
|
+
...(batch.dryRun ? ["--dry-run"] : []),
|
|
167
|
+
"--json",
|
|
168
|
+
],
|
|
169
|
+
{
|
|
170
|
+
label: "perk pr review-submit",
|
|
171
|
+
decode: decodeSubmitResult,
|
|
172
|
+
stdin: {
|
|
173
|
+
flag: "--batch",
|
|
174
|
+
content: `${JSON.stringify(payload, null, 2)}\n`,
|
|
175
|
+
filename: `review-submit-${Date.now()}.json`,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
if (r.ok) return { ok: true, data: r.data };
|
|
180
|
+
if (r.errorType === "bad_anchors" && r.payload !== undefined) {
|
|
181
|
+
const rows = decodeInvalidAnchors(r.payload);
|
|
182
|
+
if (rows !== null && rows.length > 0) {
|
|
183
|
+
return { ok: false, kind: "bad_anchors", invalid: rows, message: r.message };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { ok: false, kind: "failed", message: r.message, errorType: r.errorType };
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The minimal ctx slice the gate production needs — `ExtensionContext` satisfies it
|
|
193
|
+
* (compile-checked in the test). The dialog method is reachable ONLY through the interactive arm.
|
|
194
|
+
*/
|
|
195
|
+
export interface SubmitGateCtx {
|
|
196
|
+
hasUI: boolean;
|
|
197
|
+
ui: {
|
|
198
|
+
notify(message: string, type?: Severity): void;
|
|
199
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The production `FormalEventGate`: `ctx.hasUI` selects the arm; interactive wraps the dialog. */
|
|
204
|
+
export function formalEventGateFor(ctx: SubmitGateCtx): FormalEventGate {
|
|
205
|
+
return ctx.hasUI
|
|
206
|
+
? { kind: "interactive", confirm: (question, summary) => ctx.ui.confirm(question, summary) }
|
|
207
|
+
: { kind: "headless" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ------------------------------------------------------------------------ result rendering
|
|
211
|
+
|
|
212
|
+
/** Map the enumerated feature outcome onto the tool Result envelope (texts pinned by tests). */
|
|
213
|
+
function renderSubmitOutcome(
|
|
214
|
+
fail: (message: string, errorType: string) => Result<SubmitOk>,
|
|
215
|
+
input: CuratedSubmission,
|
|
216
|
+
outcome: SubmitCuratedOutcome,
|
|
217
|
+
): Result<SubmitOk> {
|
|
218
|
+
const commentCount = input.comments?.length ?? 0;
|
|
219
|
+
switch (outcome.kind) {
|
|
220
|
+
case "already_posted":
|
|
221
|
+
return fail(outcome.message, "already_posted");
|
|
222
|
+
case "headless_formal_event":
|
|
223
|
+
return fail(outcome.message, "headless_formal_event");
|
|
224
|
+
case "user_declined":
|
|
225
|
+
return fail(outcome.message, "user_declined");
|
|
226
|
+
case "bad_anchors":
|
|
227
|
+
return fail(outcome.message, "bad_anchors");
|
|
228
|
+
case "submit_failed":
|
|
229
|
+
return fail(outcome.message, outcome.errorType);
|
|
230
|
+
case "dry_run_ok": {
|
|
231
|
+
const n = outcome.data.comment_count ?? commentCount;
|
|
232
|
+
return ok(
|
|
233
|
+
`validated — ${n} inline comment(s), event ${input.event}; the batch is submittable`,
|
|
234
|
+
{ ...outcome.data },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
case "posted": {
|
|
238
|
+
let text =
|
|
239
|
+
`submitted ${input.event} review to PR #${outcome.record.pr} ` +
|
|
240
|
+
`(${outcome.data.comment_count ?? commentCount} inline comment(s))`;
|
|
241
|
+
if (outcome.data.mode === "review_folded") {
|
|
242
|
+
text +=
|
|
243
|
+
" — note: inline anchors rejected by GitHub; comments folded into the review body, " +
|
|
244
|
+
"event preserved";
|
|
245
|
+
} else if (outcome.data.mode === "comment_fallback") {
|
|
246
|
+
text += " — note: degraded to a discussion comment";
|
|
247
|
+
}
|
|
248
|
+
return ok(`${text}.`, { ...outcome.data });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const TOOL_GUIDELINES = [
|
|
254
|
+
"Call submit_pr_review only after the human triage has settled the batch AND the human has explicitly approved posting — nothing reaches GitHub before triage.",
|
|
255
|
+
"Validate first with dry_run: true and repair any reported anchors until validation passes; a dry-run never posts, never gates, and records nothing. A stack review dry-runs ALL per-PR batches before ANY real post.",
|
|
256
|
+
"Make ONE real call per target PR: comments + body + event land atomically in a single review — the verdict never lands before the comments. A stack review posts one review per member PR, bottom→top; each real success appends a {pr, event, at} row to the review_posts workflow-state ledger. The tool ENFORCES skip-on-resume: a real post to a PR that already has a ledger row is refused (already_posted) unless allow_repost: true — a deliberate second review only. A MISSING row is not proof of no post (the ledger is best-effort): verify posted-vs-pending against GitHub before re-posting.",
|
|
257
|
+
"Formal events (approve / request-changes) additionally raise a blocking in-TUI confirm; headless sessions refuse them (use event: comment or re-run interactively).",
|
|
258
|
+
"All perk-side GitHub posting flows through this tool on every review door — never post via gh or bash (direct perk pr review-submit calls are forbidden). On /pr-review-terminal this tool is the sole posting path. On /pr-review-browser the plannotator UI's native platform-posting is the human's own GitHub path, and perk posts only what the human explicitly hands it (typically a request-changes verdict). On /stack-review-browser the local-diff session has NO attached PR, so ALL posting is perk-side after triage.",
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
// ------------------------------------------------------------------------ registration
|
|
262
|
+
|
|
263
|
+
/** Install the `submit_pr_review` tool (the review doors register no posting tools of their own). */
|
|
264
|
+
export function installCuratedSubmissionBindings(pi: ExtensionAPI): void {
|
|
265
|
+
pi.registerTool({
|
|
266
|
+
name: "submit_pr_review",
|
|
267
|
+
label: "Submit PR review",
|
|
268
|
+
description:
|
|
269
|
+
"Submit the human-curated review-door outcome to the target PR as ONE atomic review " +
|
|
270
|
+
"(comments + body + event) via the perk cold door — the posting surface of the " +
|
|
271
|
+
"/pr-review-terminal, /pr-review-browser, and /stack-review-browser doors (a stack " +
|
|
272
|
+
"review makes one real call per member PR). dry_run validates the anchors without " +
|
|
273
|
+
"posting (the repair loop); a real submission records last_review and appends the " +
|
|
274
|
+
"review_posts ledger row in workflow-state.",
|
|
275
|
+
promptSnippet: "Submit the curated review batch to the PR",
|
|
276
|
+
promptGuidelines: TOOL_GUIDELINES,
|
|
277
|
+
executionMode: "sequential",
|
|
278
|
+
parameters: {
|
|
279
|
+
type: "object",
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
required: ["pr", "event", "body"],
|
|
282
|
+
properties: {
|
|
283
|
+
pr: { type: "number", description: "The foreign PR number being reviewed." },
|
|
284
|
+
event: {
|
|
285
|
+
type: "string",
|
|
286
|
+
enum: ["approve", "request-changes", "comment"],
|
|
287
|
+
description:
|
|
288
|
+
"The review event, settled with the human during triage. Formal events " +
|
|
289
|
+
"(approve/request-changes) additionally raise a blocking confirm dialog.",
|
|
290
|
+
},
|
|
291
|
+
body: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description:
|
|
294
|
+
"The overall review body (markdown). comment/request-changes require a non-empty " +
|
|
295
|
+
"body; unanchorable findings fold in here.",
|
|
296
|
+
},
|
|
297
|
+
comments: {
|
|
298
|
+
type: "array",
|
|
299
|
+
description:
|
|
300
|
+
"The curated inline comments — human-authored or human-approved only, each anchored " +
|
|
301
|
+
"to a line in the PR diff. Single-PR mode: never re-anchor a child's finding. Stack " +
|
|
302
|
+
"mode: the parent re-anchors combined-diff findings into per-PR coordinates under " +
|
|
303
|
+
"the dry-run loop.",
|
|
304
|
+
items: {
|
|
305
|
+
type: "object",
|
|
306
|
+
additionalProperties: false,
|
|
307
|
+
required: ["path", "line", "body"],
|
|
308
|
+
properties: {
|
|
309
|
+
path: { type: "string", description: "The changed file path." },
|
|
310
|
+
line: { type: "number", description: "A line present in the PR diff." },
|
|
311
|
+
side: {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: ["LEFT", "RIGHT"],
|
|
314
|
+
description: "The diff side the line anchors to (default RIGHT).",
|
|
315
|
+
},
|
|
316
|
+
body: { type: "string", description: "The comment (markdown)." },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
dry_run: {
|
|
321
|
+
type: "boolean",
|
|
322
|
+
description:
|
|
323
|
+
"Validate the batch + anchors without posting (the anchor-repair loop). No gates, " +
|
|
324
|
+
"no last_review record.",
|
|
325
|
+
},
|
|
326
|
+
allow_repost: {
|
|
327
|
+
type: "boolean",
|
|
328
|
+
description:
|
|
329
|
+
"Deliberately post ANOTHER review to a PR that already has a review_posts ledger " +
|
|
330
|
+
"row in this session — the enforced resume guard refuses with already_posted " +
|
|
331
|
+
"otherwise. Never pass it to work around a stack-resume refusal.",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
336
|
+
const fail = failFor(ctx, "review", "submit_pr_review");
|
|
337
|
+
const decoded = decodeSubmitParams(params);
|
|
338
|
+
if (decoded === null) {
|
|
339
|
+
return fail(
|
|
340
|
+
"submit_pr_review needs { pr: int, event: 'approve'|'request-changes'|'comment', " +
|
|
341
|
+
"body: string, comments?: [{path, line: int, side?: 'LEFT'|'RIGHT', body}], " +
|
|
342
|
+
"dry_run?: bool }",
|
|
343
|
+
"bad_input",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const outcome = await submitCuratedReview(decoded, {
|
|
347
|
+
submitter: createColdDoorReviewSubmitter(pi, ctx),
|
|
348
|
+
gate: formalEventGateFor(ctx),
|
|
349
|
+
session: openBranchWorkflowSession(pi, ctx),
|
|
350
|
+
});
|
|
351
|
+
return renderSubmitOutcome(fail, decoded, outcome);
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -5,27 +5,34 @@
|
|
|
5
5
|
// foreign — `/pr-review-terminal <pr|url> [focus]`: the detached `perk pr review checkout`,
|
|
6
6
|
// the R7 handoff, the full adversarial-reviewer flow (the fan-out rides the
|
|
7
7
|
// globally registered `start_review_wave`/`collect_review_wave` tool pair; the
|
|
8
|
-
// injected guidance
|
|
8
|
+
// injected guidance yields between native wakes and owns the hunk sink).
|
|
9
9
|
// active — `/pr-review-terminal [focus]` from a plan worktree whose branch HAS a PR: the same
|
|
10
10
|
// flow re-homed to the human's own worktree (no checkout, no cleanup) on the local
|
|
11
|
-
//
|
|
11
|
+
// diff against the PR's current base (`sinceBaseSha` — best-effort fetch, then
|
|
12
|
+
// merge-base), never the plan-ref or repository default.
|
|
12
13
|
// local — no PR yet (`perk pr url` → `no_pr`): a surface-only since-base review — hunk is
|
|
13
14
|
// launched, NO reviewers are spawned and NOTHING posts to GitHub; the guidance is a
|
|
14
15
|
// minimal notes read-back loop.
|
|
15
16
|
// Every launch carries `--agent-notes` so pushed findings are visible in hunk immediately.
|
|
16
17
|
// Hunk-sink mechanics and arg semantics are unchanged (`parseReviewDoorArgs` untouched).
|
|
17
18
|
//
|
|
18
|
-
// The door registers NO tools — the fan-out pair is
|
|
19
|
-
// (`
|
|
20
|
-
// `
|
|
19
|
+
// The door registers NO tools — the fan-out pair is installed globally
|
|
20
|
+
// (`installReviewWaveBindings`) and posting reuses `submit_pr_review` (installed by
|
|
21
|
+
// `installCuratedSubmissionBindings`), whose gate ladder (contracts §8.4) applies unchanged.
|
|
21
22
|
|
|
22
23
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import { bindingSuffix } from "
|
|
24
|
-
import { runColdDoor } from "
|
|
25
|
-
import { registerPerkCommand } from "
|
|
26
|
-
import { sinceBaseSha } from "
|
|
27
|
-
import { render } from "
|
|
28
|
-
import { report } from "
|
|
24
|
+
import { bindingSuffix } from "../../../substrate/bindingDelivery.ts";
|
|
25
|
+
import { runColdDoor } from "../../../substrate/coldDoor.ts";
|
|
26
|
+
import { registerPerkCommand } from "../../../substrate/command.ts";
|
|
27
|
+
import { sinceBaseSha } from "../../../substrate/git.ts";
|
|
28
|
+
import { render } from "../../../substrate/prompts.ts";
|
|
29
|
+
import { report } from "../../../surfaces/report.ts";
|
|
30
|
+
import {
|
|
31
|
+
decodePrUrl,
|
|
32
|
+
type PrUrl,
|
|
33
|
+
planRefBaseOf,
|
|
34
|
+
resolveReviewTarget,
|
|
35
|
+
} from "../providers/plannotatorHandoff.ts";
|
|
29
36
|
import {
|
|
30
37
|
type CheckoutOk,
|
|
31
38
|
decodeCheckout,
|
|
@@ -33,8 +40,7 @@ import {
|
|
|
33
40
|
handleHunkLaunch,
|
|
34
41
|
hunkPresent,
|
|
35
42
|
parseReviewArgs,
|
|
36
|
-
} from "./
|
|
37
|
-
import { decodePrUrl, planRefBaseOf, resolveReviewTarget } from "./plannotatorHandoff.ts";
|
|
43
|
+
} from "./checkout.ts";
|
|
38
44
|
|
|
39
45
|
/** The door's report scope — also the `command:<id>` binding trigger id. */
|
|
40
46
|
const SCOPE = "pr-review-terminal";
|
|
@@ -103,8 +109,8 @@ export function prReviewTerminalGuidance(opts: PrReviewTerminalGuidanceOpts): st
|
|
|
103
109
|
|
|
104
110
|
// ------------------------------------------------------------------------ registration
|
|
105
111
|
|
|
106
|
-
/**
|
|
107
|
-
export function
|
|
112
|
+
/** Install the warm `/pr-review-terminal` command (no tools — posting rides submit_pr_review). */
|
|
113
|
+
export function installPrReviewTerminalBindings(pi: ExtensionAPI): void {
|
|
108
114
|
registerPerkCommand(pi, SCOPE, {
|
|
109
115
|
description:
|
|
110
116
|
"Review a PR human-in-the-loop in the hunk terminal TUI: no arg reviews the active " +
|
|
@@ -187,14 +193,11 @@ export function registerPrReviewTerminal(pi: ExtensionAPI): void {
|
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
// The active arm: resolve the worktree's own PR via the shared active-PR ladder.
|
|
190
|
-
const r = await runColdDoor<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
);
|
|
196
|
-
const planRefBase = planRefBaseOf(ctx.cwd);
|
|
197
|
-
const target = resolveReviewTarget(r, planRefBase);
|
|
196
|
+
const r = await runColdDoor<PrUrl>(pi, ctx, ["pr", "url", "--json"], {
|
|
197
|
+
label: "perk pr url",
|
|
198
|
+
decode: decodePrUrl,
|
|
199
|
+
});
|
|
200
|
+
const target = resolveReviewTarget(r, planRefBaseOf(ctx.cwd));
|
|
198
201
|
if (target.mode === "fail") {
|
|
199
202
|
report(
|
|
200
203
|
ctx,
|
|
@@ -207,18 +210,20 @@ export function registerPrReviewTerminal(pi: ExtensionAPI): void {
|
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
// The since-base merge-base (best-effort fetch first): the sha hunk diffs the working tree
|
|
210
|
-
// against.
|
|
211
|
-
//
|
|
213
|
+
// against. The PR's current base is authoritative; only the pre-PR local arm uses the
|
|
214
|
+
// plan-ref's pinned base (null ⇒ repo default via origin/HEAD).
|
|
212
215
|
const fullSha = sinceBaseSha(
|
|
213
216
|
ctx.cwd,
|
|
214
|
-
target.mode === "
|
|
217
|
+
target.mode === "pr" ? target.baseRef : target.defaultBranch,
|
|
215
218
|
);
|
|
216
219
|
if (fullSha === null) {
|
|
217
220
|
report(
|
|
218
221
|
ctx,
|
|
219
222
|
SCOPE,
|
|
220
223
|
"error",
|
|
221
|
-
|
|
224
|
+
target.mode === "pr"
|
|
225
|
+
? `could not resolve the since-base merge-base for PR #${target.number} against base branch '${target.baseRef}' — pass a PR number/URL instead`
|
|
226
|
+
: "could not resolve the since-base merge-base — pass a PR number/URL instead",
|
|
222
227
|
);
|
|
223
228
|
return;
|
|
224
229
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Live-context evidence over Pi's OWN projection (contracts §8.31 semantics). Pi decides which
|
|
2
|
+
// session entries are still represented in model context (`sessionManager.buildContextEntries()`
|
|
3
|
+
// — the current leaf's compaction-aware entry list) and how each projects into a runtime message
|
|
4
|
+
// (`sessionEntryToContextMessages`); perk owns only the typed predicates over those native
|
|
5
|
+
// messages. There is no second traversal here: no compaction lookup, no kept-entry cutoff
|
|
6
|
+
// reconstruction, no retained-tail storage inspection — and no cache, registration, or
|
|
7
|
+
// persistent state.
|
|
8
|
+
//
|
|
9
|
+
// Evidence is TYPED, never serialized: a marker counts only when it rides user content (a cold
|
|
10
|
+
// launch's prompt) or the owning customType's custom content (a prior hidden injection). A
|
|
11
|
+
// compaction/branch summary quoting the marker, assistant/tool/bash output mentioning it, an
|
|
12
|
+
// unrelated custom, or plain `custom` state (`data.content`) is NOT a live delivery — Pi's
|
|
13
|
+
// converter keeps those roles distinguishable, so the predicate never has to guess from bytes.
|
|
14
|
+
//
|
|
15
|
+
// Full-branch history (`substrate/workflowState.ts::branchOf` + `branchCarries`) stays a separate
|
|
16
|
+
// authority: eligibility/state rebuilds and the strict once-per-selected-branch read-only marker
|
|
17
|
+
// read the whole branch; THIS leaf answers only "is the delivery live in model context now".
|
|
18
|
+
// Projection/conversion failures propagate — a read failure is never manufactured into an empty
|
|
19
|
+
// (and therefore falsely clean) projection; each consumer decides its own failure policy.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type ExtensionContext,
|
|
23
|
+
sessionEntryToContextMessages,
|
|
24
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
|
+
|
|
26
|
+
/** The minimal structural read the projection needs; `ExtensionContext` satisfies it. */
|
|
27
|
+
export interface ContextProjectionSource {
|
|
28
|
+
sessionManager: Pick<ExtensionContext["sessionManager"], "buildContextEntries">;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Pi's native runtime message, exactly as its converter yields it (no perk message union). */
|
|
32
|
+
export type ContextMessage = ReturnType<typeof sessionEntryToContextMessages>[number];
|
|
33
|
+
|
|
34
|
+
/** The owner of a marker: the injected customType the marker's hidden copy rides under. */
|
|
35
|
+
export interface MarkerOwner {
|
|
36
|
+
customType: string;
|
|
37
|
+
marker: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The messages Pi currently projects into model context for the selected branch, BEFORE any
|
|
42
|
+
* extension `context` filter runs: one `buildContextEntries()` read, flattened through Pi's
|
|
43
|
+
* package-root converter. Returned unchanged (roles, shapes, and bytes are Pi's).
|
|
44
|
+
*/
|
|
45
|
+
export function activeContextMessages(source: ContextProjectionSource): ContextMessage[] {
|
|
46
|
+
return source.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a live delivery of `marker` is in `messages`: user content, or custom content whose
|
|
51
|
+
* `customType` is exactly the owner's. A string must contain the marker; an array needs ONE valid
|
|
52
|
+
* `{ type: "text", text: string }` part containing the whole marker — parts are never joined, so
|
|
53
|
+
* a marker split across parts is not evidence, and non-text/malformed parts are ignored. Nothing
|
|
54
|
+
* else (assistant/tool/bash output, other customs, summaries, metadata/`details`) ever counts.
|
|
55
|
+
*/
|
|
56
|
+
export function contextCarriesMarker(
|
|
57
|
+
messages: readonly ContextMessage[],
|
|
58
|
+
owner: MarkerOwner,
|
|
59
|
+
): boolean {
|
|
60
|
+
return messages.some((message) => {
|
|
61
|
+
if (message.role === "user") return contentCarries(message.content, owner.marker);
|
|
62
|
+
if (message.role === "custom") {
|
|
63
|
+
return (
|
|
64
|
+
message.customType === owner.customType && contentCarries(message.content, owner.marker)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Narrow runtime content check (session files are parsed unvalidated — trust shapes, not types). */
|
|
72
|
+
function contentCarries(content: unknown, marker: string): boolean {
|
|
73
|
+
if (typeof content === "string") return content.includes(marker);
|
|
74
|
+
if (!Array.isArray(content)) return false;
|
|
75
|
+
return content.some((part) => {
|
|
76
|
+
if (typeof part !== "object" || part === null) return false;
|
|
77
|
+
const { type, text } = part as { type?: unknown; text?: unknown };
|
|
78
|
+
return type === "text" && typeof text === "string" && text.includes(marker);
|
|
79
|
+
});
|
|
80
|
+
}
|