@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
@@ -2,7 +2,7 @@
2
2
  // review — plannotator always, no provider dispatch (the surface-named command IS the selection).
3
3
  //
4
4
  // Three modes, keyed off the arg parse + the active-PR resolution ladder (the parse is IMPORTED
5
- // from prReviewTerminal.ts — one function ⇒ identical arg semantics by construction):
5
+ // from terminal.ts — one function ⇒ identical arg semantics by construction):
6
6
  // foreign — `/pr-review-browser <pr|url> [focus]`: the detached `perk pr review checkout`, the
7
7
  // browser opened in the background on the PR URL, the async adversarial-reviewer
8
8
  // fan-out with per-angle annotation waves streamed to the local plannotator server.
@@ -17,7 +17,8 @@
17
17
  // (plannotator reads `PLANNOTATOR_PORT` at bind time — see plannotatorHandoff.ts), so in the PR
18
18
  // modes the handler starts `startPlannotatorBrowser`, injects the mode guidance IMMEDIATELY, and
19
19
  // ends its turn — no blocking readiness poll in the handler. The readiness promise is observed
20
- // in a background task: ready → an info note; timeout / an error-or-unavailable bridge settle
20
+ // in a background task: ready → an info note + a continuation for pending annotation work;
21
+ // timeout / an error-or-unavailable bridge settle →
21
22
  // a loud error plus a degrade notice injected to the model (findings render in-session; posting
22
23
  // unchanged) AND the annotation surface cleared. `push_annotations` owns the
23
24
  // hold-and-accumulate discipline: a held batch before any door failure notice means "not up
@@ -30,23 +31,31 @@
30
31
  //
31
32
  // THE COMPANION TOOLS: the reviewer fan-out is the globally registered `start_review_wave` /
32
33
  // `collect_review_wave` pair, and the annotation delivery is the globally registered
33
- // `push_annotations` tool PRIMED BY THIS DOOR (`primeAnnotationSurface` on a PR-mode open,
34
- // cleared on bridge settle and on the readiness-degrade arm the model never sees the URL).
34
+ // `push_annotations` tool PRIMED BY THIS DOOR (`primeAnnotationSurface` over the threaded
35
+ // per-activation annotation state on a PR-mode open, cleared on bridge settle and on the
36
+ // readiness-degrade arm — the model never sees the URL).
35
37
  // The door still registers NO tools of its own; perk-side posting reuses `submit_pr_review`
36
- // (registered by `registerSubmitPrReview`). The local (pre-PR) mode never primes.
38
+ // (installed by `installCuratedSubmissionBindings`). The local (pre-PR) mode never primes.
37
39
 
38
40
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
39
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
40
- import { runColdDoor } from "../substrate/coldDoor.ts";
41
- import { registerPerkCommand } from "../substrate/command.ts";
42
- import { interceptConsoleError } from "../substrate/consoleCapture.ts";
43
- import { render } from "../substrate/prompts.ts";
44
- import { type ReportTarget, report } from "../surfaces/report.ts";
45
- import { clearAnnotationSurface, primeAnnotationSurface } from "./annotationPush.ts";
46
- import { type CheckoutOk, decodeCheckout } from "./hunkHandoff.ts";
41
+ import { bindingSuffix } from "../../../substrate/bindingDelivery.ts";
42
+ import { runColdDoor } from "../../../substrate/coldDoor.ts";
43
+ import { registerPerkCommand } from "../../../substrate/command.ts";
44
+ import { interceptConsoleError } from "../../../substrate/consoleCapture.ts";
45
+ import { render } from "../../../substrate/prompts.ts";
46
+ import { type ReportTarget, report } from "../../../surfaces/report.ts";
47
+ import type { ActivityHandle } from "../../../surfaces/surfaces.ts";
47
48
  import {
49
+ type AnnotationState,
50
+ clearAnnotationSurface,
51
+ primeAnnotationSurface,
52
+ resumeAnnotationDelivery,
53
+ } from "../providers/annotations.ts";
54
+ import {
55
+ type CodeReviewOutcome,
48
56
  decodePrUrl,
49
57
  LOCAL_REVIEW_DIFF_TYPE,
58
+ type PrUrl,
50
59
  plannotatorPresent,
51
60
  planRefBaseOf,
52
61
  type RespondSink,
@@ -56,8 +65,9 @@ import {
56
65
  routePrReviewOutcome,
57
66
  type StartedBrowser,
58
67
  startPlannotatorBrowser,
59
- } from "./plannotatorHandoff.ts";
60
- import { parseReviewDoorArgs } from "./prReviewTerminal.ts";
68
+ } from "../providers/plannotatorHandoff.ts";
69
+ import { type CheckoutOk, decodeCheckout } from "./checkout.ts";
70
+ import { parseReviewDoorArgs } from "./terminal.ts";
61
71
 
62
72
  /** The door's report scope — also the `command:<id>` binding trigger id. */
63
73
  const SCOPE = "pr-review-browser";
@@ -95,8 +105,9 @@ export function prReviewBrowserGuidance(opts: PrReviewBrowserGuidanceOpts): stri
95
105
  /**
96
106
  * The degrade notice injected when the browser never comes up — the model renders the findings
97
107
  * in-session and runs the same triage conversationally; the posting contract is unchanged.
108
+ * Exported as the `openReviewBrowserCore` default (the stack door supplies its own).
98
109
  */
99
- const DEGRADE_NOTICE =
110
+ export const DEGRADE_NOTICE =
100
111
  "The plannotator browser review is unavailable (the review server never became ready) — " +
101
112
  "degrade in-session: render the reviewers' reconciled findings as a table in your reply and " +
102
113
  "run the same triage loop conversationally. The annotation surface is cleared — " +
@@ -106,8 +117,9 @@ const DEGRADE_NOTICE =
106
117
 
107
118
  /**
108
119
  * Observe the readiness poll in the background (the handler has already injected the guidance
109
- * and ended): ready → an info note naming the URL; timeout / an error-or-unavailable bridge
110
- * settle a loud error report PLUS the degrade notice injected to the model (idle → immediate,
120
+ * and ended): ready → an info note + an immediate/followUp annotation-delivery continuation
121
+ * when the still-current surface has held or in-flight work; timeout / an error-or-unavailable
122
+ * bridge settle → a loud error report PLUS the degrade notice injected to the model (idle → immediate,
111
123
  * streaming → followUp). A bridge that settled `handled` (the human finished before the poll saw
112
124
  * the server) or `aborted` routes via the respond routing alone — no degrade. Structural param
113
125
  * slices keep it offline-testable; exported for the door tests.
@@ -116,10 +128,16 @@ export async function observeBrowserReadiness(
116
128
  pi: RespondSink,
117
129
  ctx: ReportTarget & Pick<ExtensionContext, "isIdle">,
118
130
  started: StartedBrowser,
131
+ annotations: AnnotationState,
132
+ opts?: { scope?: string; degradeNotice?: string },
119
133
  ): Promise<void> {
134
+ const scope = opts?.scope ?? SCOPE;
135
+ const degradeNotice = opts?.degradeNotice ?? DEGRADE_NOTICE;
136
+ const surface = annotations.surface;
120
137
  const state = await started.readiness;
121
138
  if (state === "ready") {
122
- report(ctx, SCOPE, "info", `plannotator is up at ${started.url} — browser opening`);
139
+ report(ctx, scope, "info", `plannotator is up at ${started.url} — browser opening`);
140
+ resumeAnnotationDelivery(annotations, surface, pi, ctx);
123
141
  return;
124
142
  }
125
143
  if (state === "aborted") return; // the turn was interrupted — no-op
@@ -129,85 +147,137 @@ export async function observeBrowserReadiness(
129
147
  }
130
148
  report(
131
149
  ctx,
132
- SCOPE,
150
+ scope,
133
151
  "error",
134
152
  `the plannotator review server did not become ready at ${started.url} — the browser ` +
135
153
  "review is unavailable",
136
154
  { alsoLog: true },
137
155
  );
138
156
  if (ctx.isIdle()) {
139
- pi.sendUserMessage(DEGRADE_NOTICE);
157
+ pi.sendUserMessage(degradeNotice);
140
158
  } else {
141
- pi.sendUserMessage(DEGRADE_NOTICE, { deliverAs: "followUp" });
159
+ pi.sendUserMessage(degradeNotice, { deliverAs: "followUp" });
142
160
  }
143
161
  // Consistent with "render findings in-session": a post-degrade push_annotations refuses
144
162
  // loudly (`no_surface`). Idempotent beside the bridge-settle clear.
145
- clearAnnotationSurface();
163
+ clearAnnotationSurface(annotations);
164
+ }
165
+
166
+ /** The parameterized browser-lifecycle core's options (see `openReviewBrowserCore`). */
167
+ export interface ReviewBrowserCoreOpts {
168
+ /** The invoking door's report scope. */
169
+ scope: string;
170
+ /** The `startPlannotatorBrowser` opts minus `signal`/`activity` (both threaded by the core). */
171
+ browserOpts: { cwd: string; prUrl?: string; diffType?: string; defaultBranch?: string };
172
+ /** The fully composed guidance to inject (binding suffix included by the caller). */
173
+ guidance: string;
174
+ /** The degrade notice for the browser-never-ready arm (default: the PR-mode notice). */
175
+ degradeNotice?: string;
176
+ /** The respond → injection mapper (default: `respondMessage` — the single-PR contract). */
177
+ respondMessageFor?: (outcome: CodeReviewOutcome) => string | null;
178
+ /**
179
+ * Whether the core injects `guidance` as a user message at the end (default true — the warm
180
+ * doors). The `open_stack_review` tool passes false and returns the guidance as its ok text
181
+ * instead (the tool result is the seeded session's delivery channel).
182
+ */
183
+ injectGuidance?: boolean;
146
184
  }
147
185
 
148
186
  /**
149
- * The shared PR-mode arm (foreign + active): start the browser open in the background, inject
150
- * the mode guidance immediately (the URL is deterministic once the port is picked), and return
151
- * the readiness observation and the bridge respond both ride background tasks. While plannotator
152
- * sets up, its in-process `console.error` chatter re-routes through the TUI-safe report() seam
153
- * (the debounce restores once setup goes quiet, with the `finally` as a backstop).
187
+ * The full browser-lifecycle core, extracted from the PR-mode arm and parameterized for the
188
+ * stack door: start the browser open in the background, prime the annotation surface the
189
+ * moment the port is picked (push_annotations now serves this browser session), observe
190
+ * readiness in the background (degrade notice on never-ready), route the bridge respond
191
+ * through the injectable mapper, clear the surface on settle, and inject the guidance
192
+ * immediately (the URL is deterministic once the port is picked). While plannotator sets up,
193
+ * its in-process `console.error` chatter re-routes through the TUI-safe report() seam (the
194
+ * debounce restores once setup goes quiet, with the `finally` as a backstop).
195
+ *
196
+ * Accepted stale-clear edge (unchanged from the pre-extraction arm): a second browser door
197
+ * while this browser is still open re-primes (a new browser session supersedes everything),
198
+ * and THIS bridge's later settle would clear the second session's surface — rare and loud
199
+ * (the fixed-port EADDRINUSE caveat, contracts §8.4), noted, not engineered around.
154
200
  */
155
- async function openBrowserAndGuide(
201
+ export async function openReviewBrowserCore(
156
202
  pi: ExtensionAPI,
157
203
  ctx: ExtensionContext,
158
- opts: PrReviewBrowserGuidanceOpts,
159
- ): Promise<void> {
204
+ annotations: AnnotationState,
205
+ status: ActivityHandle,
206
+ opts: ReviewBrowserCoreOpts,
207
+ ): Promise<boolean> {
160
208
  let started: StartedBrowser;
161
209
  try {
162
210
  started = await startPlannotatorBrowser(pi.events, {
163
- prUrl: opts.prUrl,
164
- cwd: ctx.cwd,
211
+ ...opts.browserOpts,
165
212
  signal: ctx.signal,
213
+ activity: (text) => status.beginActivity(ctx, text),
166
214
  });
167
215
  } catch (error) {
168
216
  const detail = error instanceof Error ? error.message : String(error);
169
217
  report(
170
218
  ctx,
171
- SCOPE,
219
+ opts.scope,
172
220
  "error",
173
221
  `could not pick a free local port for the plannotator review server: ${detail}`,
174
222
  { alsoLog: true },
175
223
  );
176
- return;
224
+ return false;
177
225
  }
178
226
 
179
- // Prime the annotation surface the moment the port is picked (the URL is deterministic — see
180
- // the background-open header note): push_annotations now serves this browser session. Accepted
181
- // stale-clear edge: a second /pr-review-browser while this browser is still open re-primes (a
182
- // new browser session supersedes everything), and THIS bridge's later settle would clear the
183
- // second session's surface — the overlap is already rare and loud (the fixed-port EADDRINUSE
184
- // caveat, contracts §8.4), so it is noted, not engineered around.
185
- primeAnnotationSurface({ mode: "review", url: started.url });
227
+ primeAnnotationSurface(annotations, { mode: "review", url: started.url });
186
228
 
187
- void observeBrowserReadiness(pi, ctx, started);
229
+ void observeBrowserReadiness(pi, ctx, started, annotations, {
230
+ scope: opts.scope,
231
+ ...(opts.degradeNotice !== undefined ? { degradeNotice: opts.degradeNotice } : {}),
232
+ });
188
233
 
189
234
  void (async () => {
190
- const interceptor = interceptConsoleError((line) => report(ctx, SCOPE, "info", line), {
235
+ const interceptor = interceptConsoleError((line) => report(ctx, opts.scope, "info", line), {
191
236
  // plannotator can pause up to ~4s between setup lines — keep the quiet window above that.
192
237
  quietMs: 6000,
193
238
  });
194
239
  try {
195
240
  const out = await started.bridgePromise;
196
- routeBrowserRespond(pi, ctx, out, SCOPE);
241
+ routeBrowserRespond(pi, ctx, out, opts.scope, opts.respondMessageFor);
197
242
  } finally {
198
243
  // The browser session is over — drop the surface so a later push refuses (`no_surface`).
199
- clearAnnotationSurface();
244
+ clearAnnotationSurface(annotations);
200
245
  interceptor.restore();
201
246
  }
202
247
  })();
203
248
 
204
- pi.sendUserMessage(prReviewBrowserGuidance(opts) + bindingSuffix(ctx.cwd, `command:${SCOPE}`));
249
+ if (opts.injectGuidance !== false) pi.sendUserMessage(opts.guidance);
250
+ return true;
251
+ }
252
+
253
+ /**
254
+ * The shared PR-mode arm (foreign + active): the extracted core with this door's values —
255
+ * PR-mode browser opts, the mode guidance + binding suffix, and the default degrade notice /
256
+ * respond mapper (the byte-stability of the pre-extraction behavior is proven by this door's
257
+ * untouched tests).
258
+ */
259
+ async function openBrowserAndGuide(
260
+ pi: ExtensionAPI,
261
+ ctx: ExtensionContext,
262
+ annotations: AnnotationState,
263
+ status: ActivityHandle,
264
+ opts: PrReviewBrowserGuidanceOpts,
265
+ ): Promise<void> {
266
+ await openReviewBrowserCore(pi, ctx, annotations, status, {
267
+ scope: SCOPE,
268
+ browserOpts: { prUrl: opts.prUrl, cwd: ctx.cwd },
269
+ guidance: prReviewBrowserGuidance(opts) + bindingSuffix(ctx.cwd, `command:${SCOPE}`),
270
+ });
205
271
  }
206
272
 
207
273
  // ------------------------------------------------------------------------ registration
208
274
 
209
- /** Register the warm `/pr-review-browser` command (no tools — posting rides submit_pr_review). */
210
- export function registerPrReviewBrowser(pi: ExtensionAPI): void {
275
+ /** Install the warm `/pr-review-browser` command (no tools — posting rides submit_pr_review). */
276
+ export function installPrReviewBrowserBindings(
277
+ pi: ExtensionAPI,
278
+ annotations: AnnotationState,
279
+ status: ActivityHandle,
280
+ ): void {
211
281
  registerPerkCommand(pi, SCOPE, {
212
282
  description:
213
283
  "Review a PR human-in-the-loop in the plannotator browser UI: no arg reviews the active " +
@@ -269,7 +339,7 @@ export function registerPrReviewBrowser(pi: ExtensionAPI): void {
269
339
  ? `PR #${parsed.pr} → adversarial reviewers (focus: ${parsed.directive}) → plannotator browser triage → you post from the browser`
270
340
  : `PR #${parsed.pr} → adversarial reviewers → plannotator browser triage → you post from the browser`,
271
341
  );
272
- await openBrowserAndGuide(pi, ctx, {
342
+ await openBrowserAndGuide(pi, ctx, annotations, status, {
273
343
  mode: "foreign",
274
344
  pr: parsed.pr,
275
345
  prUrl: checkout.data.url,
@@ -280,12 +350,10 @@ export function registerPrReviewBrowser(pi: ExtensionAPI): void {
280
350
  }
281
351
 
282
352
  // The active arm: resolve the worktree's own PR via the shared ladder.
283
- const r = await runColdDoor<{ number: number; url: string }>(
284
- pi,
285
- ctx,
286
- ["pr", "url", "--json"],
287
- { label: "perk pr url", decode: decodePrUrl },
288
- );
353
+ const r = await runColdDoor<PrUrl>(pi, ctx, ["pr", "url", "--json"], {
354
+ label: "perk pr url",
355
+ decode: decodePrUrl,
356
+ });
289
357
  const target = resolveReviewTarget(r, planRefBaseOf(ctx.cwd));
290
358
  if (target.mode === "fail") {
291
359
  report(
@@ -307,7 +375,7 @@ export function registerPrReviewBrowser(pi: ExtensionAPI): void {
307
375
  ? `PR #${target.number} (active worktree) → adversarial reviewers (focus: ${parsed.directive}) → plannotator browser triage → you post from the browser`
308
376
  : `PR #${target.number} (active worktree) → adversarial reviewers → plannotator browser triage → you post from the browser`,
309
377
  );
310
- await openBrowserAndGuide(pi, ctx, {
378
+ await openBrowserAndGuide(pi, ctx, annotations, status, {
311
379
  mode: "active",
312
380
  pr: target.number,
313
381
  prUrl: target.prUrl,
@@ -1,14 +1,18 @@
1
- // The hunk terminal-review substrate serving `/pr-review-terminal`: the strict
1
+ // The neutral shared checkout/PR-token substrate serving the code-review doors: the strict
2
2
  // `perk pr review checkout` decode, the `hunk --version` presence probe, and the R7 launch
3
3
  // handoff (clipboard copy + terminal auto-launch raced against a soft deadline) — plus the
4
4
  // door-common PR-token arg grammar (`parseReviewArgs`) that `/pr-review-browser` also consumes
5
- // via `parseReviewDoorArgs`. Re-homing the door-common pieces to a neutral module is a deferred
6
- // residual (accepted — no code moves yet).
7
-
8
- import { copyToClipboard } from "../substrate/clipboard.ts";
9
- import { type ColdJson, type ExecHost, numberField, stringField } from "../substrate/coldDoor.ts";
10
- import { LAUNCH_SURFACE, launchInTerminal } from "../substrate/terminalLaunch.ts";
11
- import { type ReportTarget, report } from "../surfaces/report.ts";
5
+ // via `parseReviewDoorArgs`. Its only consumers are the `pi/v1/codeReview/` siblings.
6
+
7
+ import { copyToClipboard } from "../../../substrate/clipboard.ts";
8
+ import {
9
+ type ColdJson,
10
+ type ExecHost,
11
+ numberField,
12
+ stringField,
13
+ } from "../../../substrate/coldDoor.ts";
14
+ import { LAUNCH_SURFACE, launchInTerminal } from "../../../substrate/terminalLaunch.ts";
15
+ import { type ReportTarget, report } from "../../../surfaces/report.ts";
12
16
 
13
17
  /** The install hint for the absent hunk binary — the exact `HUNK_INSTALL_HINT` wording
14
18
  * (src/perk/convergence/init/review_cli.py). */