@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.
Files changed (211) 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 +369 -359
  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 -310
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
  52. package/extension/pi/v1/codeReview/stack.ts +606 -0
  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 +163 -265
  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 +128 -60
  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/{doors → pi/v1}/lifecycleGates.ts +14 -59
  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/pi/v1/objectivePlanning.ts +776 -0
  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 +152 -34
  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 -19
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/paths.ts +2 -7
  115. package/extension/substrate/prompts.ts +22 -0
  116. package/extension/substrate/registry.ts +2 -0
  117. package/extension/substrate/resolverLease.ts +364 -0
  118. package/extension/substrate/sessionData.ts +85 -152
  119. package/extension/substrate/toolGating.ts +279 -84
  120. package/extension/substrate/unifiedDiff.ts +1 -1
  121. package/extension/substrate/workflowState.ts +191 -166
  122. package/extension/substrate/worktreeResolverLock.ts +261 -0
  123. package/extension/surfaces/surfaces.ts +79 -27
  124. package/extension/waves/adversarialReviewWave.ts +103 -48
  125. package/extension/waves/blockedReports.ts +59 -0
  126. package/extension/waves/draftReviewWave.ts +42 -42
  127. package/extension/waves/laneIdentity.ts +77 -0
  128. package/extension/waves/objectiveExplorerWave.ts +24 -24
  129. package/extension/waves/prReviewWave.ts +89 -77
  130. package/extension/waves/reportWave.ts +438 -578
  131. package/extension/waves/reviewClassifierWave.ts +22 -22
  132. package/extension/waves/rpcAdapter.ts +100 -15
  133. package/extension/waves/scoutWave.ts +192 -0
  134. package/extension/waves/transport.ts +480 -0
  135. package/extension/worker/sdkAdapter.ts +494 -0
  136. package/extension/worker/stageExecution.ts +679 -0
  137. package/extension/workerMain.ts +18 -19
  138. package/package.json +6 -4
  139. package/prompts/_fixtures/live.yaml +98 -10
  140. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  142. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  143. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  144. package/prompts/contexts/adapters/tombell-plan.md +4 -0
  145. package/prompts/contexts/objective-refinement.md +17 -0
  146. package/prompts/contexts/plan-authoring.md +6 -5
  147. package/prompts/contexts/read-only.md +1 -1
  148. package/prompts/stages/conflict-resolution-continuation.md +9 -0
  149. package/prompts/stages/conflict-resolution.md +4 -4
  150. package/prompts/stages/objective-author/adopt.md +1 -1
  151. package/prompts/stages/objective-author/file.md +1 -1
  152. package/prompts/stages/objective-author/seed.md +1 -1
  153. package/prompts/stages/objective-plan/guidance.md +2 -2
  154. package/prompts/stages/objective-plan/seed.md +9 -1
  155. package/prompts/stages/objective-reconcile-ready.md +7 -0
  156. package/prompts/stages/objective-reconcile.md +1 -1
  157. package/prompts/stages/objective-refine/seed.md +18 -0
  158. package/prompts/stages/objective-review-browser.md +4 -4
  159. package/prompts/stages/objective-sync.md +1 -1
  160. package/prompts/stages/plan-review-browser.md +4 -4
  161. package/prompts/stages/pr-review-browser/active.md +3 -4
  162. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  163. package/prompts/stages/pr-review-terminal/active.md +3 -3
  164. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  165. package/prompts/stages/pr-review.md +3 -3
  166. package/prompts/stages/stack-review/cold.md +1 -0
  167. package/prompts/stages/stack-review-browser/stack.md +22 -0
  168. package/shared/README.md +8 -3
  169. package/shared/bindings.yaml +6 -3
  170. package/shared/contracts.md +4499 -2147
  171. package/shared/fixtures/issues-table.json +130 -0
  172. package/shared/registry.yaml +29 -1
  173. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  174. package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
  175. package/shared/schemas/outputs/pr-ready.schema.json +110 -2
  176. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  177. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  178. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  179. package/extension/doors/auditWaveTools.ts +0 -352
  180. package/extension/doors/ciExecutor.ts +0 -756
  181. package/extension/doors/commitCompact.ts +0 -251
  182. package/extension/doors/dreamWaveTools.ts +0 -475
  183. package/extension/doors/learn.ts +0 -655
  184. package/extension/doors/objectiveStack.ts +0 -1143
  185. package/extension/doors/prReviewDynamic.ts +0 -276
  186. package/extension/doors/ready.ts +0 -87
  187. package/extension/doors/submit.ts +0 -347
  188. package/extension/doors/submitPrReview.ts +0 -408
  189. package/extension/factories/gistAuthor.ts +0 -94
  190. package/extension/factories/gistDraft.ts +0 -265
  191. package/extension/factories/gistSave.ts +0 -251
  192. package/extension/factories/implementHere.ts +0 -116
  193. package/extension/factories/objectiveAuthor.ts +0 -98
  194. package/extension/factories/objectiveDraft.ts +0 -466
  195. package/extension/factories/objectivePlan.ts +0 -975
  196. package/extension/factories/objectiveSave.ts +0 -363
  197. package/extension/factories/planDraft.ts +0 -140
  198. package/extension/factories/planMode.ts +0 -205
  199. package/extension/factories/planReview.ts +0 -1237
  200. package/extension/factories/planSave.ts +0 -604
  201. package/extension/factories/planTitle.ts +0 -141
  202. package/extension/substrate/structuredOutput.ts +0 -202
  203. package/extension/waves/auditWave.ts +0 -312
  204. package/extension/waves/harvestWave.ts +0 -399
  205. package/extension/waves/learnWave.ts +0 -155
  206. package/extension/waves/memoryAdapter.ts +0 -139
  207. package/extension/waves/prReviewDynamicWave.ts +0 -777
  208. package/extension/worker/readOnlySession.ts +0 -294
  209. package/extension/worker/worker.ts +0 -899
  210. package/prompts/stages/pr-review-dynamic.md +0 -7
  211. package/shared/contracts-history.md +0 -605
@@ -1,408 +0,0 @@
1
- // The warm `submit_pr_review` tool — the agent-driven curated-posting surface shared by the two
2
- // PR-review doors (`/pr-review-terminal`, `/pr-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 browser door the human platform-posts from the plannotator UI — that
9
- // native posting IS the GitHub path; perk composes nothing by default and posts only what the
10
- // human explicitly hands it (typically a request-changes verdict, which the UI cannot post). It
11
- // delegates to the Python cold door (`perk pr review-submit` — mutations canonical in Python)
12
- // via `runColdDoor` (the batch rides the run-scratch stdin channel), then appends `last_review`
13
- // to `perk:workflow-state`. The human gate splits: explicit conversational go-ahead ALWAYS
14
- // (pinned in the guidelines/skill); formal events (`approve`/`request-changes`) additionally get
15
- // the structural gate — headless refuses, interactive raises a blocking `ctx.ui.confirm`.
16
- // `dry_run` is the anchor-repair loop: no gates, no record, nothing posted.
17
-
18
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
- import {
20
- booleanField,
21
- type ColdDoorCtx,
22
- type ColdJson,
23
- type ExecHost,
24
- numberField,
25
- runColdDoor,
26
- stringField,
27
- } from "../substrate/coldDoor.ts";
28
- import { failFor, ok, type Result } from "../substrate/result.ts";
29
- import {
30
- arrayParam,
31
- booleanParam,
32
- numberParam,
33
- paramsOf,
34
- stringParam,
35
- type ToolParams,
36
- } from "../substrate/toolParams.ts";
37
- import { appendWorkflowState, type EntrySink } from "../substrate/workflowState.ts";
38
- import type { Severity } from "../surfaces/report.ts";
39
-
40
- // ------------------------------------------------------------------------ params
41
-
42
- export type ReviewEvent = "approve" | "request-changes" | "comment";
43
-
44
- /** One curated inline comment (the exact `review-submit --batch` `comments[]` row). */
45
- export interface SubmitComment {
46
- path: string;
47
- line: number;
48
- side?: "LEFT" | "RIGHT";
49
- body: string;
50
- }
51
-
52
- export interface SubmitParams {
53
- pr: number;
54
- event: ReviewEvent;
55
- body: string;
56
- comments?: SubmitComment[];
57
- dry_run?: boolean;
58
- }
59
-
60
- /** Decode the optional `comments` array; null = present-but-malformed (whole-batch refusal). */
61
- function decodeSubmitComments(p: ToolParams): SubmitComment[] | undefined | null {
62
- const raw = arrayParam(p, "comments");
63
- if (raw === undefined) return undefined;
64
- if (raw === null) return null;
65
- const comments: SubmitComment[] = [];
66
- for (const item of raw) {
67
- const row = paramsOf(item);
68
- if (row === null) return null;
69
- const path = stringParam(row, "path");
70
- const line = numberParam(row, "line");
71
- const side = stringParam(row, "side");
72
- const body = stringParam(row, "body");
73
- if (typeof path !== "string" || path.length === 0) return null;
74
- if (typeof line !== "number" || !Number.isInteger(line)) return null;
75
- if (side !== undefined && side !== "LEFT" && side !== "RIGHT") return null;
76
- if (typeof body !== "string" || body.length === 0) return null;
77
- const comment: SubmitComment = { path, line, body };
78
- if (side !== undefined) comment.side = side;
79
- comments.push(comment);
80
- }
81
- return comments;
82
- }
83
-
84
- /**
85
- * Strict-decode unknown tool-call params into `SubmitParams` (the tool-boundary seam). Mirrors
86
- * `decodePostParams`: submitting a guessed/partial review is a durable GitHub mutation, so ANY
87
- * malformed field ⇒ null (whole-batch refusal). `pr` must be an int; `event` exactly one of the
88
- * three flag spellings; `body` a string (EMPTY ALLOWED — the cold door owns the event-conditioned
89
- * body rule and reports `bad_batch`); each `comments` row strict on
90
- * path/line(int)/side(LEFT|RIGHT)/body; `dry_run` a boolean.
91
- */
92
- export function decodeSubmitParams(params: unknown): SubmitParams | null {
93
- const p = paramsOf(params);
94
- if (p === null) return null;
95
- const pr = numberParam(p, "pr");
96
- if (typeof pr !== "number" || !Number.isInteger(pr)) return null;
97
- const event = stringParam(p, "event");
98
- if (event !== "approve" && event !== "request-changes" && event !== "comment") return null;
99
- const body = stringParam(p, "body");
100
- if (typeof body !== "string") return null;
101
- const comments = decodeSubmitComments(p);
102
- if (comments === null) return null;
103
- const dryRun = booleanParam(p, "dry_run");
104
- if (dryRun === null) return null;
105
- const result: SubmitParams = { pr, event, body };
106
- if (comments !== undefined) result.comments = comments;
107
- if (dryRun !== undefined) result.dry_run = dryRun;
108
- return result;
109
- }
110
-
111
- // ------------------------------------------------------------------------ the tool core
112
-
113
- /** The cold door's ok-arm fields (the `review-submit --json` surface; render-only → lenient). */
114
- export interface SubmitOk {
115
- dry_run?: boolean;
116
- pr?: number;
117
- event?: string;
118
- mode?: string;
119
- comment_count?: number;
120
- }
121
-
122
- export type SubmitResult = Result<SubmitOk>;
123
-
124
- /** Narrow the cold door's `review-submit --json` payload to the fields the tool reports. */
125
- function decodeSubmitResult(payload: ColdJson): SubmitOk {
126
- return {
127
- dry_run: booleanField(payload, "dry_run"),
128
- pr: numberField(payload, "pr"),
129
- event: stringField(payload, "event"),
130
- mode: stringField(payload, "mode"),
131
- comment_count: numberField(payload, "comment_count"),
132
- };
133
- }
134
-
135
- /** One `bad_anchors` `invalid[]` row (the cold door's per-comment repair detail). */
136
- interface InvalidAnchor {
137
- index: number;
138
- path: string;
139
- line: number;
140
- side: string;
141
- reason: string;
142
- }
143
-
144
- /**
145
- * Strict re-narrow of the `bad_anchors` fail payload's `invalid[]` rows. Null on ANY drift —
146
- * uncertainty renders as a plain fail, never a half table.
147
- */
148
- function decodeInvalidAnchors(payload: ColdJson): InvalidAnchor[] | null {
149
- const raw = payload.invalid;
150
- if (!Array.isArray(raw)) return null;
151
- const rows: InvalidAnchor[] = [];
152
- for (const item of raw) {
153
- const row = paramsOf(item);
154
- if (row === null) return null;
155
- const index = row.index;
156
- const path = row.path;
157
- const line = row.line;
158
- const side = row.side;
159
- const reason = row.reason;
160
- if (typeof index !== "number" || !Number.isInteger(index)) return null;
161
- if (typeof path !== "string") return null;
162
- if (typeof line !== "number" || !Number.isInteger(line)) return null;
163
- if (typeof side !== "string") return null;
164
- if (typeof reason !== "string") return null;
165
- rows.push({ index, path, line, side, reason });
166
- }
167
- return rows;
168
- }
169
-
170
- /** Flag spelling → the REST wire spelling shown in the human confirm. */
171
- const WIRE_EVENT: Record<ReviewEvent, string> = {
172
- approve: "APPROVE",
173
- "request-changes": "REQUEST_CHANGES",
174
- comment: "COMMENT",
175
- };
176
-
177
- /** The body's first line, truncated for the confirm-dialog summary. */
178
- function bodyFirstLine(body: string): string {
179
- const line = body.split("\n", 1)[0] ?? "";
180
- return line.length > 120 ? `${line.slice(0, 117)}…` : line;
181
- }
182
-
183
- /**
184
- * The minimal ctx slice `submitPrReview` needs — `ExtensionContext` satisfies it (compile-checked
185
- * in the test); tests fake it. The dialog method is called ONLY behind the `hasUI` guard.
186
- */
187
- export interface SubmitCtx extends ColdDoorCtx {
188
- hasUI: boolean;
189
- ui: {
190
- notify(message: string, type?: Severity): void;
191
- confirm(title: string, message: string): Promise<boolean>;
192
- };
193
- }
194
-
195
- /**
196
- * Submit the human-curated review batch to the foreign PR (the terminal door's sole posting
197
- * surface; the browser door's request-changes / explicit-request path). Delegates to the Python
198
- * cold door; returns a soft result (never throws). Gate ladder (skipped when `dry_run`): formal
199
- * events refuse headless (`headless_formal_event`) and otherwise require a blocking confirm
200
- * (`user_declined` on decline, nothing executed); `comment` posts on the conversational
201
- * go-ahead alone. On a real success, records `last_review`.
202
- */
203
- export async function submitPrReview(
204
- pi: ExecHost & EntrySink,
205
- ctx: SubmitCtx,
206
- params: SubmitParams,
207
- ): Promise<SubmitResult> {
208
- const fail = failFor(ctx, "review", "submit_pr_review");
209
- const dryRun = params.dry_run === true;
210
- const commentCount = params.comments?.length ?? 0;
211
-
212
- if (!dryRun && params.event !== "comment") {
213
- if (!ctx.hasUI) {
214
- return fail(
215
- "headless sessions cannot post formal review verdicts — re-run interactively or use " +
216
- "event: comment",
217
- "headless_formal_event",
218
- );
219
- }
220
- const wire = WIRE_EVENT[params.event];
221
- const firstLine = bodyFirstLine(params.body);
222
- const summary =
223
- `event: ${wire} · ${commentCount} inline comment(s)` +
224
- (firstLine.length > 0 ? `\nbody: ${firstLine}` : "");
225
- const yes = await ctx.ui.confirm(`Post ${wire} review to PR #${params.pr}?`, summary);
226
- if (!yes) {
227
- return fail(
228
- `user declined the ${params.event} review — nothing was submitted`,
229
- "user_declined",
230
- );
231
- }
232
- }
233
-
234
- // The exact `perk pr review-submit --batch` shape ({body, comments?} — the event rides the flag).
235
- const batch: Record<string, unknown> = { body: params.body };
236
- if (params.comments !== undefined) batch.comments = params.comments;
237
-
238
- const r = await runColdDoor<SubmitOk>(
239
- pi,
240
- ctx,
241
- [
242
- "pr",
243
- "review-submit",
244
- "--pr",
245
- String(params.pr),
246
- "--event",
247
- params.event,
248
- ...(dryRun ? ["--dry-run"] : []),
249
- "--json",
250
- ],
251
- {
252
- label: "perk pr review-submit",
253
- decode: decodeSubmitResult,
254
- stdin: {
255
- flag: "--batch",
256
- content: `${JSON.stringify(batch, null, 2)}\n`,
257
- filename: `review-submit-${Date.now()}.json`,
258
- },
259
- },
260
- );
261
-
262
- if (!r.ok) {
263
- // The repair-loop arm: render the per-comment invalid[] detail when it decodes cleanly.
264
- if (r.errorType === "bad_anchors" && r.payload !== undefined) {
265
- const rows = decodeInvalidAnchors(r.payload);
266
- if (rows !== null && rows.length > 0) {
267
- const table = rows
268
- .map(
269
- (row) =>
270
- ` comment[${row.index}] ${row.path}:${row.line} (${row.side}) — ${row.reason}`,
271
- )
272
- .join("\n");
273
- return fail(
274
- `${r.message}\n${table}\nrepair these anchors and re-run with dry_run: true`,
275
- r.errorType,
276
- );
277
- }
278
- }
279
- return fail(r.message, r.errorType);
280
- }
281
-
282
- const data = r.data;
283
- if (dryRun) {
284
- const n = data.comment_count ?? commentCount;
285
- return ok(
286
- `validated — ${n} inline comment(s), event ${params.event}; the batch is submittable`,
287
- { ...data },
288
- );
289
- }
290
-
291
- // Record the outcome (tier-3, best-effort-with-logging, headless-safe). Strict read-back via
292
- // rebuild — loud-but-non-fatal, the submission already succeeded.
293
- const record = {
294
- pr: data.pr ?? params.pr,
295
- event: params.event,
296
- comment_count: data.comment_count ?? null,
297
- mode: data.mode ?? null,
298
- at: new Date().toISOString(),
299
- };
300
- appendWorkflowState(pi, ctx, {
301
- data: { last_review: record },
302
- field: "last_review",
303
- expected: record,
304
- scope: "review",
305
- failure: "last_review read-back failed",
306
- });
307
-
308
- let text =
309
- `submitted ${params.event} review to PR #${record.pr} ` +
310
- `(${data.comment_count ?? commentCount} inline comment(s))`;
311
- if (data.mode === "review_folded") {
312
- text +=
313
- " — note: inline anchors rejected by GitHub; comments folded into the review body, " +
314
- "event preserved";
315
- } else if (data.mode === "comment_fallback") {
316
- text += " — note: degraded to a discussion comment";
317
- }
318
- return ok(`${text}.`, { ...data });
319
- }
320
-
321
- const TOOL_GUIDELINES = [
322
- "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.",
323
- "Validate first with dry_run: true and repair any reported anchors until validation passes; a dry-run never posts, never gates, and records nothing.",
324
- "Make ONE real call: comments + body + event land atomically in a single review — the verdict never lands before the comments.",
325
- "Formal events (approve / request-changes) additionally raise a blocking in-TUI confirm; headless sessions refuse them (use event: comment or re-run interactively).",
326
- "All perk-side GitHub posting flows through this tool on both review doors — 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).",
327
- ];
328
-
329
- // ------------------------------------------------------------------------ registration
330
-
331
- /** Register the `submit_pr_review` tool (the two review doors register no tools of their own). */
332
- export function registerSubmitPrReview(pi: ExtensionAPI): void {
333
- pi.registerTool({
334
- name: "submit_pr_review",
335
- label: "Submit PR review",
336
- description:
337
- "Submit the human-curated review-door outcome to the foreign PR as ONE atomic review " +
338
- "(comments + body + event) via the perk cold door. dry_run validates the anchors without " +
339
- "posting (the repair loop); a real submission records last_review in workflow-state.",
340
- promptSnippet: "Submit the curated review batch to the PR",
341
- promptGuidelines: TOOL_GUIDELINES,
342
- executionMode: "sequential",
343
- parameters: {
344
- type: "object",
345
- additionalProperties: false,
346
- required: ["pr", "event", "body"],
347
- properties: {
348
- pr: { type: "number", description: "The foreign PR number being reviewed." },
349
- event: {
350
- type: "string",
351
- enum: ["approve", "request-changes", "comment"],
352
- description:
353
- "The review event, settled with the human during triage. Formal events " +
354
- "(approve/request-changes) additionally raise a blocking confirm dialog.",
355
- },
356
- body: {
357
- type: "string",
358
- description:
359
- "The overall review body (markdown). comment/request-changes require a non-empty " +
360
- "body; unanchorable findings fold in here.",
361
- },
362
- comments: {
363
- type: "array",
364
- description:
365
- "The curated inline comments — human-authored or human-approved only, each anchored " +
366
- "to a line in the PR diff (never re-anchor a child's finding).",
367
- items: {
368
- type: "object",
369
- additionalProperties: false,
370
- required: ["path", "line", "body"],
371
- properties: {
372
- path: { type: "string", description: "The changed file path." },
373
- line: { type: "number", description: "A line present in the PR diff." },
374
- side: {
375
- type: "string",
376
- enum: ["LEFT", "RIGHT"],
377
- description: "The diff side the line anchors to (default RIGHT).",
378
- },
379
- body: { type: "string", description: "The comment (markdown)." },
380
- },
381
- },
382
- },
383
- dry_run: {
384
- type: "boolean",
385
- description:
386
- "Validate the batch + anchors without posting (the anchor-repair loop). No gates, " +
387
- "no last_review record.",
388
- },
389
- },
390
- },
391
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
392
- const decoded = decodeSubmitParams(params);
393
- if (decoded === null) {
394
- return failFor(
395
- ctx,
396
- "review",
397
- "submit_pr_review",
398
- )(
399
- "submit_pr_review needs { pr: int, event: 'approve'|'request-changes'|'comment', " +
400
- "body: string, comments?: [{path, line: int, side?: 'LEFT'|'RIGHT', body}], " +
401
- "dry_run?: bool }",
402
- "bad_input",
403
- );
404
- }
405
- return submitPrReview(pi, ctx, decoded);
406
- },
407
- });
408
- }
@@ -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
- }