@mgiles/perk 3.2.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.
Files changed (202) hide show
  1. package/README.md +5 -0
  2. package/extension/authoring/gist/draft.ts +198 -0
  3. package/extension/authoring/gist/prose.ts +46 -0
  4. package/extension/authoring/gist/review.ts +133 -0
  5. package/extension/authoring/gist/save.ts +118 -0
  6. package/extension/authoring/objective/draft.ts +345 -0
  7. package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
  8. package/extension/authoring/objective/planning.ts +124 -0
  9. package/extension/authoring/objective/prose.ts +103 -0
  10. package/extension/authoring/objective/review.ts +128 -0
  11. package/extension/authoring/objective/save.ts +224 -0
  12. package/extension/authoring/plan/draft.ts +84 -0
  13. package/extension/authoring/plan/prose.ts +41 -0
  14. package/extension/authoring/plan/review.ts +269 -0
  15. package/extension/authoring/plan/save.ts +256 -0
  16. package/extension/authoring/plan/source.ts +82 -0
  17. package/extension/authoring/refinement/context.ts +468 -0
  18. package/extension/authoring/refinement/draft.ts +261 -0
  19. package/extension/authoring/refinement/prose.ts +79 -0
  20. package/extension/authoring/refinement/review.ts +111 -0
  21. package/extension/authoring/refinement/save.ts +119 -0
  22. package/extension/authoring/review/approvalGate.ts +34 -0
  23. package/extension/authoring/review/draftContext.ts +68 -0
  24. package/extension/codeReview/automated.ts +352 -0
  25. package/extension/codeReview/submission.ts +229 -0
  26. package/extension/delivery/address.ts +295 -0
  27. package/extension/delivery/ci.ts +355 -0
  28. package/extension/delivery/commitCompact.ts +93 -0
  29. package/extension/delivery/conflictResolution.ts +247 -0
  30. package/extension/delivery/ready.ts +193 -0
  31. package/extension/delivery/stackConflict.ts +361 -0
  32. package/extension/delivery/stackObjective.ts +16 -0
  33. package/extension/delivery/stackReconcile.ts +165 -0
  34. package/extension/delivery/submit.ts +171 -0
  35. package/extension/index.ts +365 -380
  36. package/extension/learning/analystWave.ts +324 -0
  37. package/extension/learning/audit.ts +667 -0
  38. package/extension/learning/capture.ts +92 -0
  39. package/extension/learning/containment.ts +104 -0
  40. package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
  41. package/extension/learning/dreamAnalysis.ts +435 -0
  42. package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
  43. package/extension/{waves → learning}/dreamReport.ts +35 -31
  44. package/extension/learning/harvest.ts +491 -0
  45. package/extension/learning/prose.ts +66 -0
  46. package/extension/learning/routing.ts +79 -0
  47. package/extension/pi/v1/bashScanTimeout.ts +64 -0
  48. package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
  52. package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
  53. package/extension/pi/v1/codeReview/submit.ts +354 -0
  54. package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
  55. package/extension/pi/v1/contextEvidence.ts +80 -0
  56. package/extension/pi/v1/contextInjection.ts +207 -0
  57. package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
  58. package/extension/pi/v1/delivery/ci.ts +570 -0
  59. package/extension/pi/v1/delivery/commitCompact.ts +201 -0
  60. package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
  61. package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
  62. package/extension/pi/v1/delivery/ready.ts +322 -0
  63. package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
  64. package/extension/pi/v1/delivery/stackDrive.ts +120 -0
  65. package/extension/pi/v1/delivery/stackLand.ts +223 -0
  66. package/extension/pi/v1/delivery/stackRecover.ts +265 -0
  67. package/extension/pi/v1/delivery/stackStatus.ts +237 -0
  68. package/extension/pi/v1/delivery/stackSync.ts +658 -0
  69. package/extension/pi/v1/delivery/submit.ts +389 -0
  70. package/extension/pi/v1/delivery/submitConflict.ts +186 -0
  71. package/extension/pi/v1/draftReview.ts +431 -0
  72. package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
  73. package/extension/pi/v1/gist.ts +794 -0
  74. package/extension/pi/v1/learning/audit.ts +186 -0
  75. package/extension/pi/v1/learning/dream.ts +207 -0
  76. package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
  77. package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
  78. package/extension/pi/v1/learning/learn.ts +585 -0
  79. package/extension/pi/v1/lifecycleGates.ts +127 -0
  80. package/extension/{factories → pi/v1}/objective.ts +53 -33
  81. package/extension/pi/v1/objectiveAuthoring.ts +672 -0
  82. package/extension/pi/v1/objectiveDreamGate.ts +160 -0
  83. package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
  84. package/extension/pi/v1/objectiveRefinement.ts +1320 -0
  85. package/extension/pi/v1/objectiveReview.ts +451 -0
  86. package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
  87. package/extension/pi/v1/plan.ts +812 -0
  88. package/extension/pi/v1/planReview.ts +820 -0
  89. package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
  90. package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
  91. package/extension/pi/v1/providers/plannotator.ts +487 -0
  92. package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
  93. package/extension/pi/v1/providers/selection.ts +43 -0
  94. package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
  95. package/extension/pi/v1/review.ts +538 -0
  96. package/extension/pi/v1/reviewOutcome.ts +9 -0
  97. package/extension/pi/v1/scoutWave.ts +318 -0
  98. package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
  99. package/extension/session/branchWorkflowSession.ts +60 -0
  100. package/extension/session/lifecycle.ts +644 -0
  101. package/extension/session/lifecycleGates.ts +64 -0
  102. package/extension/session/saveDestination.ts +87 -0
  103. package/extension/session/workflowSession.ts +971 -0
  104. package/extension/substrate/agentScratch.ts +27 -54
  105. package/extension/substrate/bashScanTimeout.ts +181 -0
  106. package/extension/substrate/bindingDelivery.ts +38 -30
  107. package/extension/substrate/bindings.ts +4 -5
  108. package/extension/substrate/cache.ts +64 -12
  109. package/extension/substrate/childRestrictions.ts +39 -0
  110. package/extension/substrate/coldDoor.ts +17 -1
  111. package/extension/substrate/config.ts +157 -21
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/prompts.ts +22 -0
  115. package/extension/substrate/registry.ts +2 -0
  116. package/extension/substrate/resolverLease.ts +5 -4
  117. package/extension/substrate/sessionData.ts +85 -152
  118. package/extension/substrate/toolGating.ts +263 -84
  119. package/extension/substrate/unifiedDiff.ts +1 -1
  120. package/extension/substrate/workflowState.ts +178 -163
  121. package/extension/substrate/worktreeResolverLock.ts +261 -0
  122. package/extension/surfaces/surfaces.ts +79 -27
  123. package/extension/waves/adversarialReviewWave.ts +87 -46
  124. package/extension/waves/blockedReports.ts +59 -0
  125. package/extension/waves/draftReviewWave.ts +42 -42
  126. package/extension/waves/laneIdentity.ts +77 -0
  127. package/extension/waves/objectiveExplorerWave.ts +24 -24
  128. package/extension/waves/prReviewWave.ts +89 -77
  129. package/extension/waves/reportWave.ts +438 -578
  130. package/extension/waves/reviewClassifierWave.ts +22 -22
  131. package/extension/waves/rpcAdapter.ts +100 -15
  132. package/extension/waves/scoutWave.ts +192 -0
  133. package/extension/waves/transport.ts +480 -0
  134. package/extension/worker/sdkAdapter.ts +494 -0
  135. package/extension/worker/stageExecution.ts +679 -0
  136. package/extension/workerMain.ts +18 -19
  137. package/package.json +6 -4
  138. package/prompts/_fixtures/live.yaml +43 -18
  139. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  140. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  142. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  143. package/prompts/contexts/objective-refinement.md +17 -0
  144. package/prompts/contexts/read-only.md +1 -1
  145. package/prompts/stages/conflict-resolution-continuation.md +9 -6
  146. package/prompts/stages/conflict-resolution.md +4 -4
  147. package/prompts/stages/objective-plan/guidance.md +2 -2
  148. package/prompts/stages/objective-plan/seed.md +9 -1
  149. package/prompts/stages/objective-reconcile-ready.md +1 -1
  150. package/prompts/stages/objective-reconcile.md +1 -1
  151. package/prompts/stages/objective-refine/seed.md +18 -0
  152. package/prompts/stages/objective-review-browser.md +4 -4
  153. package/prompts/stages/objective-sync.md +1 -1
  154. package/prompts/stages/plan-review-browser.md +4 -4
  155. package/prompts/stages/pr-review-browser/active.md +3 -4
  156. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  157. package/prompts/stages/pr-review-terminal/active.md +3 -3
  158. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  159. package/prompts/stages/pr-review.md +3 -3
  160. package/prompts/stages/stack-review-browser/stack.md +5 -6
  161. package/shared/README.md +8 -0
  162. package/shared/bindings.yaml +3 -3
  163. package/shared/contracts.md +2601 -506
  164. package/shared/fixtures/issues-table.json +130 -0
  165. package/shared/registry.yaml +13 -0
  166. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  167. package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
  168. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  169. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  170. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  171. package/extension/doors/auditWaveTools.ts +0 -352
  172. package/extension/doors/ciExecutor.ts +0 -756
  173. package/extension/doors/commitCompact.ts +0 -251
  174. package/extension/doors/dreamWaveTools.ts +0 -489
  175. package/extension/doors/learn.ts +0 -668
  176. package/extension/doors/lifecycleGates.ts +0 -207
  177. package/extension/doors/objectiveStack.ts +0 -1543
  178. package/extension/doors/prReviewDynamic.ts +0 -276
  179. package/extension/doors/ready.ts +0 -279
  180. package/extension/doors/submit.ts +0 -373
  181. package/extension/doors/submitPrReview.ts +0 -505
  182. package/extension/factories/gistAuthor.ts +0 -94
  183. package/extension/factories/gistDraft.ts +0 -265
  184. package/extension/factories/gistSave.ts +0 -251
  185. package/extension/factories/implementHere.ts +0 -116
  186. package/extension/factories/objectiveAuthor.ts +0 -98
  187. package/extension/factories/objectiveDraft.ts +0 -466
  188. package/extension/factories/objectiveSave.ts +0 -366
  189. package/extension/factories/planDraft.ts +0 -140
  190. package/extension/factories/planMode.ts +0 -205
  191. package/extension/factories/planReview.ts +0 -1237
  192. package/extension/factories/planSave.ts +0 -604
  193. package/extension/factories/planTitle.ts +0 -141
  194. package/extension/substrate/structuredOutput.ts +0 -202
  195. package/extension/waves/auditWave.ts +0 -312
  196. package/extension/waves/harvestWave.ts +0 -399
  197. package/extension/waves/learnWave.ts +0 -155
  198. package/extension/waves/memoryAdapter.ts +0 -139
  199. package/extension/waves/prReviewDynamicWave.ts +0 -777
  200. package/extension/worker/readOnlySession.ts +0 -294
  201. package/extension/worker/worker.ts +0 -899
  202. package/prompts/stages/pr-review-dynamic.md +0 -7
@@ -1,505 +0,0 @@
1
- // The warm `submit_pr_review` tool — the agent-driven curated-posting surface shared by the
2
- // PR-review doors (`/pr-review-terminal`, `/pr-review-browser`, `/stack-review-browser`).
3
- //
4
- // `submit_pr_review` implements the per-door posting contract (contracts §8.4): nothing perk-
5
- // driven reaches GitHub before the human triage; ALL perk-side posting flows through this tool
6
- // on every door (`gh` mutations and direct `perk pr review-submit` calls are forbidden); the
7
- // verdict lands last, atomically with the comments. On the terminal door this tool is the sole
8
- // posting path. On the single-PR browser door the human platform-posts from the plannotator
9
- // UI — that native posting IS the GitHub path; perk composes nothing by default and posts only
10
- // what the human explicitly hands it (typically a request-changes verdict, which the UI cannot
11
- // post). On the stack door the local-diff session has NO attached PR, so ALL posting is
12
- // perk-side after triage: one real call per member PR, bottom→top, each under its own dry-run
13
- // anchor validation. It delegates to the Python cold door (`perk pr review-submit` — mutations
14
- // canonical in Python) via `runColdDoor` (the batch rides the run-scratch stdin channel), then
15
- // appends `last_review` AND the accumulating `review_posts` ledger row to `perk:workflow-state`
16
- // (the stack flow's resume authority — posted PRs are skipped, never replayed). The human gate
17
- // splits: explicit conversational go-ahead ALWAYS (pinned in the guidelines/skill); formal
18
- // events (`approve`/`request-changes`) additionally get the structural gate — headless refuses,
19
- // interactive raises a blocking `ctx.ui.confirm`.
20
- // `dry_run` is the anchor-repair loop: no gates, no record, nothing posted.
21
-
22
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
- import {
24
- booleanField,
25
- type ColdDoorCtx,
26
- type ColdJson,
27
- type ExecHost,
28
- numberField,
29
- runColdDoor,
30
- stringField,
31
- } from "../substrate/coldDoor.ts";
32
- import { failFor, ok, type Result } from "../substrate/result.ts";
33
- import {
34
- arrayParam,
35
- booleanParam,
36
- numberParam,
37
- paramsOf,
38
- stringParam,
39
- type ToolParams,
40
- } from "../substrate/toolParams.ts";
41
- import {
42
- appendWorkflowState,
43
- branchOf,
44
- type EntrySink,
45
- rebuildWorkflowState,
46
- } from "../substrate/workflowState.ts";
47
- import type { Severity } from "../surfaces/report.ts";
48
-
49
- // ------------------------------------------------------------------------ params
50
-
51
- export type ReviewEvent = "approve" | "request-changes" | "comment";
52
-
53
- /** One curated inline comment (the exact `review-submit --batch` `comments[]` row). */
54
- export interface SubmitComment {
55
- path: string;
56
- line: number;
57
- side?: "LEFT" | "RIGHT";
58
- body: string;
59
- }
60
-
61
- export interface SubmitParams {
62
- pr: number;
63
- event: ReviewEvent;
64
- body: string;
65
- comments?: SubmitComment[];
66
- dry_run?: boolean;
67
- allow_repost?: boolean;
68
- }
69
-
70
- /** Decode the optional `comments` array; null = present-but-malformed (whole-batch refusal). */
71
- function decodeSubmitComments(p: ToolParams): SubmitComment[] | undefined | null {
72
- const raw = arrayParam(p, "comments");
73
- if (raw === undefined) return undefined;
74
- if (raw === null) return null;
75
- const comments: SubmitComment[] = [];
76
- for (const item of raw) {
77
- const row = paramsOf(item);
78
- if (row === null) return null;
79
- const path = stringParam(row, "path");
80
- const line = numberParam(row, "line");
81
- const side = stringParam(row, "side");
82
- const body = stringParam(row, "body");
83
- if (typeof path !== "string" || path.length === 0) return null;
84
- if (typeof line !== "number" || !Number.isInteger(line)) return null;
85
- if (side !== undefined && side !== "LEFT" && side !== "RIGHT") return null;
86
- if (typeof body !== "string" || body.length === 0) return null;
87
- const comment: SubmitComment = { path, line, body };
88
- if (side !== undefined) comment.side = side;
89
- comments.push(comment);
90
- }
91
- return comments;
92
- }
93
-
94
- /**
95
- * Strict-decode unknown tool-call params into `SubmitParams` (the tool-boundary seam). Mirrors
96
- * `decodePostParams`: submitting a guessed/partial review is a durable GitHub mutation, so ANY
97
- * malformed field ⇒ null (whole-batch refusal). `pr` must be an int; `event` exactly one of the
98
- * three flag spellings; `body` a string (EMPTY ALLOWED — the cold door owns the event-conditioned
99
- * body rule and reports `bad_batch`); each `comments` row strict on
100
- * path/line(int)/side(LEFT|RIGHT)/body; `dry_run` and `allow_repost` booleans.
101
- */
102
- export function decodeSubmitParams(params: unknown): SubmitParams | null {
103
- const p = paramsOf(params);
104
- if (p === null) return null;
105
- const pr = numberParam(p, "pr");
106
- if (typeof pr !== "number" || !Number.isInteger(pr)) return null;
107
- const event = stringParam(p, "event");
108
- if (event !== "approve" && event !== "request-changes" && event !== "comment") return null;
109
- const body = stringParam(p, "body");
110
- if (typeof body !== "string") return null;
111
- const comments = decodeSubmitComments(p);
112
- if (comments === null) return null;
113
- const dryRun = booleanParam(p, "dry_run");
114
- if (dryRun === null) return null;
115
- const allowRepost = booleanParam(p, "allow_repost");
116
- if (allowRepost === null) return null;
117
- const result: SubmitParams = { pr, event, body };
118
- if (comments !== undefined) result.comments = comments;
119
- if (dryRun !== undefined) result.dry_run = dryRun;
120
- if (allowRepost !== undefined) result.allow_repost = allowRepost;
121
- return result;
122
- }
123
-
124
- // ------------------------------------------------------------------------ the tool core
125
-
126
- /** The cold door's ok-arm fields (the `review-submit --json` surface; render-only → lenient). */
127
- export interface SubmitOk {
128
- dry_run?: boolean;
129
- pr?: number;
130
- event?: string;
131
- mode?: string;
132
- comment_count?: number;
133
- }
134
-
135
- export type SubmitResult = Result<SubmitOk>;
136
-
137
- /** Narrow the cold door's `review-submit --json` payload to the fields the tool reports. */
138
- function decodeSubmitResult(payload: ColdJson): SubmitOk {
139
- return {
140
- dry_run: booleanField(payload, "dry_run"),
141
- pr: numberField(payload, "pr"),
142
- event: stringField(payload, "event"),
143
- mode: stringField(payload, "mode"),
144
- comment_count: numberField(payload, "comment_count"),
145
- };
146
- }
147
-
148
- /** One `bad_anchors` `invalid[]` row (the cold door's per-comment repair detail). */
149
- interface InvalidAnchor {
150
- index: number;
151
- path: string;
152
- line: number;
153
- side: string;
154
- reason: string;
155
- }
156
-
157
- /**
158
- * Strict re-narrow of the `bad_anchors` fail payload's `invalid[]` rows. Null on ANY drift —
159
- * uncertainty renders as a plain fail, never a half table.
160
- */
161
- function decodeInvalidAnchors(payload: ColdJson): InvalidAnchor[] | null {
162
- const raw = payload.invalid;
163
- if (!Array.isArray(raw)) return null;
164
- const rows: InvalidAnchor[] = [];
165
- for (const item of raw) {
166
- const row = paramsOf(item);
167
- if (row === null) return null;
168
- const index = row.index;
169
- const path = row.path;
170
- const line = row.line;
171
- const side = row.side;
172
- const reason = row.reason;
173
- if (typeof index !== "number" || !Number.isInteger(index)) return null;
174
- if (typeof path !== "string") return null;
175
- if (typeof line !== "number" || !Number.isInteger(line)) return null;
176
- if (typeof side !== "string") return null;
177
- if (typeof reason !== "string") return null;
178
- rows.push({ index, path, line, side, reason });
179
- }
180
- return rows;
181
- }
182
-
183
- // ------------------------------------------------------------------------ the posting ledger
184
-
185
- /** One `review_posts` ledger row: a REAL submission that reached GitHub. */
186
- export interface ReviewPostRow {
187
- pr: number;
188
- event: string;
189
- at: string;
190
- }
191
-
192
- /**
193
- * Tolerant re-narrow of the rebuilt `review_posts` list (best-effort tier: a malformed row is
194
- * dropped, never a refusal — the ledger only ever grows from this tool's own writes).
195
- */
196
- export function reviewPostsOf(raw: unknown): ReviewPostRow[] {
197
- if (!Array.isArray(raw)) return [];
198
- const rows: ReviewPostRow[] = [];
199
- for (const item of raw) {
200
- const row = paramsOf(item);
201
- if (row === null) continue;
202
- if (typeof row.pr !== "number" || !Number.isInteger(row.pr)) continue;
203
- if (typeof row.event !== "string" || typeof row.at !== "string") continue;
204
- rows.push({ pr: row.pr, event: row.event, at: row.at });
205
- }
206
- return rows;
207
- }
208
-
209
- /** Ledger equality for the read-back verification (order-sensitive — posting order matters). */
210
- function reviewPostsEqual(rebuilt: unknown, expected: unknown): boolean {
211
- const a = reviewPostsOf(rebuilt);
212
- const b = reviewPostsOf(expected);
213
- if (a.length !== b.length) return false;
214
- return a.every(
215
- (row, i) => row.pr === b[i]?.pr && row.event === b[i]?.event && row.at === b[i]?.at,
216
- );
217
- }
218
-
219
- /** Flag spelling → the REST wire spelling shown in the human confirm. */
220
- const WIRE_EVENT: Record<ReviewEvent, string> = {
221
- approve: "APPROVE",
222
- "request-changes": "REQUEST_CHANGES",
223
- comment: "COMMENT",
224
- };
225
-
226
- /** The body's first line, truncated for the confirm-dialog summary. */
227
- function bodyFirstLine(body: string): string {
228
- const line = body.split("\n", 1)[0] ?? "";
229
- return line.length > 120 ? `${line.slice(0, 117)}…` : line;
230
- }
231
-
232
- /**
233
- * The minimal ctx slice `submitPrReview` needs — `ExtensionContext` satisfies it (compile-checked
234
- * in the test); tests fake it. The dialog method is called ONLY behind the `hasUI` guard.
235
- */
236
- export interface SubmitCtx extends ColdDoorCtx {
237
- hasUI: boolean;
238
- ui: {
239
- notify(message: string, type?: Severity): void;
240
- confirm(title: string, message: string): Promise<boolean>;
241
- };
242
- }
243
-
244
- /**
245
- * Submit the human-curated review batch to the foreign PR (the terminal door's sole posting
246
- * surface; the browser door's request-changes / explicit-request path). Delegates to the Python
247
- * cold door; returns a soft result (never throws). Gate ladder (skipped when `dry_run`): formal
248
- * events refuse headless (`headless_formal_event`) and otherwise require a blocking confirm
249
- * (`user_declined` on decline, nothing executed); `comment` posts on the conversational
250
- * go-ahead alone. On a real success, records `last_review`.
251
- */
252
- export async function submitPrReview(
253
- pi: ExecHost & EntrySink,
254
- ctx: SubmitCtx,
255
- params: SubmitParams,
256
- ): Promise<SubmitResult> {
257
- const fail = failFor(ctx, "review", "submit_pr_review");
258
- const dryRun = params.dry_run === true;
259
- const commentCount = params.comments?.length ?? 0;
260
-
261
- // The enforced resume guard (before the confirm AND the cold-door mutation): a PR that
262
- // already has a review_posts ledger row in this session is a confirmed success — a repeat
263
- // real post is refused unless explicitly deliberate. A ledger row can only be MISSING
264
- // spuriously (best-effort tier), never present spuriously — so the guard refuses on
265
- // presence and stays silent on absence (a missing row still means: verify posted-vs-pending
266
- // against GitHub before re-posting).
267
- if (!dryRun && params.allow_repost !== true) {
268
- const prior = reviewPostsOf(rebuildWorkflowState(branchOf(ctx)).review_posts).filter(
269
- (row) => row.pr === params.pr,
270
- );
271
- const last = prior.at(-1);
272
- if (last !== undefined) {
273
- return fail(
274
- `a ${last.event} review was already posted to PR #${params.pr} in this session ` +
275
- `(review_posts row at ${last.at}) — on a stack resume skip this member; pass ` +
276
- "allow_repost: true only for a deliberate second review of the same PR",
277
- "already_posted",
278
- );
279
- }
280
- }
281
-
282
- if (!dryRun && params.event !== "comment") {
283
- if (!ctx.hasUI) {
284
- return fail(
285
- "headless sessions cannot post formal review verdicts — re-run interactively or use " +
286
- "event: comment",
287
- "headless_formal_event",
288
- );
289
- }
290
- const wire = WIRE_EVENT[params.event];
291
- const firstLine = bodyFirstLine(params.body);
292
- const summary =
293
- `event: ${wire} · ${commentCount} inline comment(s)` +
294
- (firstLine.length > 0 ? `\nbody: ${firstLine}` : "");
295
- const yes = await ctx.ui.confirm(`Post ${wire} review to PR #${params.pr}?`, summary);
296
- if (!yes) {
297
- return fail(
298
- `user declined the ${params.event} review — nothing was submitted`,
299
- "user_declined",
300
- );
301
- }
302
- }
303
-
304
- // The exact `perk pr review-submit --batch` shape ({body, comments?} — the event rides the flag).
305
- const batch: Record<string, unknown> = { body: params.body };
306
- if (params.comments !== undefined) batch.comments = params.comments;
307
-
308
- const r = await runColdDoor<SubmitOk>(
309
- pi,
310
- ctx,
311
- [
312
- "pr",
313
- "review-submit",
314
- "--pr",
315
- String(params.pr),
316
- "--event",
317
- params.event,
318
- ...(dryRun ? ["--dry-run"] : []),
319
- "--json",
320
- ],
321
- {
322
- label: "perk pr review-submit",
323
- decode: decodeSubmitResult,
324
- stdin: {
325
- flag: "--batch",
326
- content: `${JSON.stringify(batch, null, 2)}\n`,
327
- filename: `review-submit-${Date.now()}.json`,
328
- },
329
- },
330
- );
331
-
332
- if (!r.ok) {
333
- // The repair-loop arm: render the per-comment invalid[] detail when it decodes cleanly.
334
- if (r.errorType === "bad_anchors" && r.payload !== undefined) {
335
- const rows = decodeInvalidAnchors(r.payload);
336
- if (rows !== null && rows.length > 0) {
337
- const table = rows
338
- .map(
339
- (row) =>
340
- ` comment[${row.index}] ${row.path}:${row.line} (${row.side}) — ${row.reason}`,
341
- )
342
- .join("\n");
343
- return fail(
344
- `${r.message}\n${table}\nrepair these anchors and re-run with dry_run: true`,
345
- r.errorType,
346
- );
347
- }
348
- }
349
- return fail(r.message, r.errorType);
350
- }
351
-
352
- const data = r.data;
353
- if (dryRun) {
354
- const n = data.comment_count ?? commentCount;
355
- return ok(
356
- `validated — ${n} inline comment(s), event ${params.event}; the batch is submittable`,
357
- { ...data },
358
- );
359
- }
360
-
361
- // Record the outcome (tier-3, best-effort-with-logging, headless-safe). Strict read-back via
362
- // rebuild — loud-but-non-fatal, the submission already succeeded.
363
- const record = {
364
- pr: data.pr ?? params.pr,
365
- event: params.event,
366
- comment_count: data.comment_count ?? null,
367
- mode: data.mode ?? null,
368
- at: new Date().toISOString(),
369
- };
370
- appendWorkflowState(pi, ctx, {
371
- data: { last_review: record },
372
- field: "last_review",
373
- expected: record,
374
- scope: "review",
375
- failure: "last_review read-back failed",
376
- });
377
-
378
- // The accumulating per-PR ledger (read-rebuild-append): ordered rows, one per real success —
379
- // the stack flow's partial-outcome/resume authority. Same best-effort tier as last_review.
380
- const posts: ReviewPostRow[] = [
381
- ...reviewPostsOf(rebuildWorkflowState(branchOf(ctx)).review_posts),
382
- { pr: record.pr, event: params.event, at: record.at },
383
- ];
384
- appendWorkflowState(pi, ctx, {
385
- data: { review_posts: posts },
386
- field: "review_posts",
387
- expected: posts,
388
- scope: "review",
389
- failure: "review_posts read-back failed",
390
- equals: reviewPostsEqual,
391
- });
392
-
393
- let text =
394
- `submitted ${params.event} review to PR #${record.pr} ` +
395
- `(${data.comment_count ?? commentCount} inline comment(s))`;
396
- if (data.mode === "review_folded") {
397
- text +=
398
- " — note: inline anchors rejected by GitHub; comments folded into the review body, " +
399
- "event preserved";
400
- } else if (data.mode === "comment_fallback") {
401
- text += " — note: degraded to a discussion comment";
402
- }
403
- return ok(`${text}.`, { ...data });
404
- }
405
-
406
- const TOOL_GUIDELINES = [
407
- "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.",
408
- "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.",
409
- "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.",
410
- "Formal events (approve / request-changes) additionally raise a blocking in-TUI confirm; headless sessions refuse them (use event: comment or re-run interactively).",
411
- "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.",
412
- ];
413
-
414
- // ------------------------------------------------------------------------ registration
415
-
416
- /** Register the `submit_pr_review` tool (the two review doors register no tools of their own). */
417
- export function registerSubmitPrReview(pi: ExtensionAPI): void {
418
- pi.registerTool({
419
- name: "submit_pr_review",
420
- label: "Submit PR review",
421
- description:
422
- "Submit the human-curated review-door outcome to the target PR as ONE atomic review " +
423
- "(comments + body + event) via the perk cold door — the posting surface of the " +
424
- "/pr-review-terminal, /pr-review-browser, and /stack-review-browser doors (a stack " +
425
- "review makes one real call per member PR). dry_run validates the anchors without " +
426
- "posting (the repair loop); a real submission records last_review and appends the " +
427
- "review_posts ledger row in workflow-state.",
428
- promptSnippet: "Submit the curated review batch to the PR",
429
- promptGuidelines: TOOL_GUIDELINES,
430
- executionMode: "sequential",
431
- parameters: {
432
- type: "object",
433
- additionalProperties: false,
434
- required: ["pr", "event", "body"],
435
- properties: {
436
- pr: { type: "number", description: "The foreign PR number being reviewed." },
437
- event: {
438
- type: "string",
439
- enum: ["approve", "request-changes", "comment"],
440
- description:
441
- "The review event, settled with the human during triage. Formal events " +
442
- "(approve/request-changes) additionally raise a blocking confirm dialog.",
443
- },
444
- body: {
445
- type: "string",
446
- description:
447
- "The overall review body (markdown). comment/request-changes require a non-empty " +
448
- "body; unanchorable findings fold in here.",
449
- },
450
- comments: {
451
- type: "array",
452
- description:
453
- "The curated inline comments — human-authored or human-approved only, each anchored " +
454
- "to a line in the PR diff. Single-PR mode: never re-anchor a child's finding. Stack " +
455
- "mode: the parent re-anchors combined-diff findings into per-PR coordinates under " +
456
- "the dry-run loop.",
457
- items: {
458
- type: "object",
459
- additionalProperties: false,
460
- required: ["path", "line", "body"],
461
- properties: {
462
- path: { type: "string", description: "The changed file path." },
463
- line: { type: "number", description: "A line present in the PR diff." },
464
- side: {
465
- type: "string",
466
- enum: ["LEFT", "RIGHT"],
467
- description: "The diff side the line anchors to (default RIGHT).",
468
- },
469
- body: { type: "string", description: "The comment (markdown)." },
470
- },
471
- },
472
- },
473
- dry_run: {
474
- type: "boolean",
475
- description:
476
- "Validate the batch + anchors without posting (the anchor-repair loop). No gates, " +
477
- "no last_review record.",
478
- },
479
- allow_repost: {
480
- type: "boolean",
481
- description:
482
- "Deliberately post ANOTHER review to a PR that already has a review_posts ledger " +
483
- "row in this session — the enforced resume guard refuses with already_posted " +
484
- "otherwise. Never pass it to work around a stack-resume refusal.",
485
- },
486
- },
487
- },
488
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
489
- const decoded = decodeSubmitParams(params);
490
- if (decoded === null) {
491
- return failFor(
492
- ctx,
493
- "review",
494
- "submit_pr_review",
495
- )(
496
- "submit_pr_review needs { pr: int, event: 'approve'|'request-changes'|'comment', " +
497
- "body: string, comments?: [{path, line: int, side?: 'LEFT'|'RIGHT', body}], " +
498
- "dry_run?: bool }",
499
- "bad_input",
500
- );
501
- }
502
- return submitPrReview(pi, ctx, decoded);
503
- },
504
- });
505
- }
@@ -1,94 +0,0 @@
1
- // Gist-authoring context injection (the gist mirror of objectiveAuthor.ts). A `perk gist
2
- // author` cold launch opens a READ-ONLY session whose handoff `stage` is `gist-author`; this
3
- // module injects the gist-authoring contract under its own `perk:gist-author-context` customType
4
- // (once-only: branch-scan dedup'd on the marker), keyed off (read-only gate AND stage ===
5
- // gist-author), optionally extended by the same `[workflow] plan_authoring` addendum the
6
- // plan-authoring injection consumes (verbatim reuse, read per-event via loadPerkConfig).
7
- // planMode.ts defers when the stage is gist-author, so exactly one authoring context is injected.
8
- //
9
- // The `gist_save` warm door (the tool + `/gist-save` command) lives in gistSave.ts, the mirror
10
- // of objectiveSave.ts.
11
-
12
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
- import { loadPerkConfig } from "../substrate/config.ts";
14
- import { render } from "../substrate/prompts.ts";
15
- import type { ToolGating } from "../substrate/toolGating.ts";
16
- import {
17
- type BranchEntry,
18
- branchCarries,
19
- branchOf,
20
- rebuildWorkflowState,
21
- } from "../substrate/workflowState.ts";
22
-
23
- /** The registry stage id of the gist-authoring session (shared with planMode's defer check). */
24
- export const GIST_AUTHOR_STAGE = "gist-author";
25
-
26
- /** The gist-authoring context customType (distinct from planMode's `perk:plan-context`). */
27
- export const GIST_AUTHOR_CONTEXT_TYPE = "perk:gist-author-context";
28
- const GIST_AUTHOR_MARKER = "[GIST AUTHORING]";
29
-
30
- /**
31
- * The gist-authoring session context: live state + pointers only (contracts.md §8.57 — the flow
32
- * is stated by the launch statement, the detail by the `perk-gist-author` skill). It names the
33
- * working-draft artifact (`gist_draft`), the review tool (`plan_review`), and the bound skill;
34
- * it never restates the flow. Prompting, NOT enforcement (the tool gate is the enforcement).
35
- */
36
- export const GIST_AUTHORING_CONTEXT = render("contexts/gist-authoring.md", {
37
- marker: GIST_AUTHOR_MARKER,
38
- });
39
-
40
- /** Build the full gist-authoring injection, appending the project config addendum when present. */
41
- export function gistAuthoringContextContent(cwd: string): string {
42
- const addendum = loadPerkConfig(cwd).planAuthoring;
43
- return addendum ? `${GIST_AUTHORING_CONTEXT}\n\n${addendum.trim()}` : GIST_AUTHORING_CONTEXT;
44
- }
45
-
46
- /** Whether the current branch is a gist-author session (read-only gate AND stage match). */
47
- function isGistAuthoring(gating: ToolGating, branch: readonly BranchEntry[]): boolean {
48
- return gating.isActive() && rebuildWorkflowState(branch).stage === GIST_AUTHOR_STAGE;
49
- }
50
-
51
- /**
52
- * Register the gist-authoring context injection (display:false), the gist mirror of
53
- * objectiveAuthor's injection. Inert outside a gist-author session; never throws.
54
- */
55
- export function registerGistAuthor(pi: ExtensionAPI, gating: ToolGating): void {
56
- pi.on("before_agent_start", async (_event, ctx) => {
57
- const branch = branchOf(ctx);
58
- if (!isGistAuthoring(gating, branch)) return;
59
- // Once-only: injected customs persist to the branch, so a live copy suppresses re-injection;
60
- // compaction dropping it makes the scan come up clean and the next turn re-injects.
61
- if (branchCarries(branch, GIST_AUTHOR_MARKER)) return;
62
- return {
63
- message: {
64
- customType: GIST_AUTHOR_CONTEXT_TYPE,
65
- content: gistAuthoringContextContent(ctx.cwd),
66
- display: false,
67
- },
68
- };
69
- });
70
-
71
- // Strip the stale gist-authoring marker from context once the session is no longer authoring
72
- // (gate off, or the stage moved on) so it never lingers — the same hygiene planMode applies.
73
- pi.on("context", async (event, ctx) => {
74
- const branch = branchOf(ctx);
75
- if (isGistAuthoring(gating, branch)) return;
76
- return {
77
- messages: event.messages.filter((m) => {
78
- const msg = m as { customType?: string; role?: string; content?: unknown };
79
- if (msg.customType === GIST_AUTHOR_CONTEXT_TYPE) return false;
80
- if (msg.role !== "user") return true;
81
- const content = msg.content;
82
- if (typeof content === "string") return !content.includes(GIST_AUTHOR_MARKER);
83
- if (Array.isArray(content)) {
84
- return !content.some(
85
- (c) =>
86
- (c as { type?: string; text?: string }).type === "text" &&
87
- ((c as { text?: string }).text ?? "").includes(GIST_AUTHOR_MARKER),
88
- );
89
- }
90
- return true;
91
- }),
92
- };
93
- });
94
- }