@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,30 +1,42 @@
1
- // The warm `/address` door (the review loop). Classify-then-act: the flow-scoped
1
+ // The review-feedback bindings (the `/address` review loop). Classify-then-act: the flow-scoped
2
2
  // `classify_review_feedback` tool runs perk's read-only `perk.review-classifier` child through
3
3
  // the report-wave module (ONE lane, engine-validated report schema, the configured
4
4
  // `[models.subagents] review-classifier` model read at execute time), so the verbose GitHub
5
5
  // JSON never enters this session and nothing schema-shaped is model-transcribed; the PARENT
6
6
  // applies fixes (judgment + edits stay here) and finishes through one terminating
7
- // `finalize_address` tool.
7
+ // `finalize_address` tool adapting the Pi-free finalization operation in `delivery/address.ts`.
8
8
  //
9
- // Finalization is deliberately submit-then-resolve: committed fixes first flow through the normal
10
- // submit operation (including a stacked suffix cascade), and only a successful publication may
11
- // reply to and resolve review threads. The mechanical resolve half remains an exported internal
12
- // seam over `perk pr resolve-threads`; GitHub mutations stay canonical in Python.
9
+ // Finalization is deliberately submit-then-resolve: committed fixes first flow through the
10
+ // normal publish operation (including a stacked suffix cascade), and only a successful
11
+ // publication may reply to and resolve review threads. Both external effects stay canonical in
12
+ // Python (`perk pr submit` / `perk pr resolve-threads`); this adapter owns the wire vocabulary
13
+ // (params decode, rows decode, the Result projection) and the report loudness of each arm.
13
14
 
14
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
16
- import { type PlanRef, readPlanRef } from "../substrate/cache.ts";
16
+ import {
17
+ type AddressFinalization,
18
+ type FinalizeAddressDeps,
19
+ finalizeAddress,
20
+ type ResolveThreads,
21
+ type ThreadInput,
22
+ type ThreadResultRow,
23
+ } from "../../../delivery/address.ts";
24
+ import type { PublishedChange } from "../../../delivery/submit.ts";
25
+ import { openBranchWorkflowSession } from "../../../session/branchWorkflowSession.ts";
26
+ import { planningStageRefusal } from "../../../session/lifecycleGates.ts";
27
+ import { bindingSuffix } from "../../../substrate/bindingDelivery.ts";
28
+ import type { PlanRef } from "../../../substrate/cache.ts";
17
29
  import {
18
30
  booleanField,
19
31
  type ColdJson,
20
32
  nullableStringField,
21
33
  runColdDoor,
22
34
  stringField,
23
- } from "../substrate/coldDoor.ts";
24
- import { registerPerkCommand } from "../substrate/command.ts";
25
- import { subagentModel } from "../substrate/config.ts";
26
- import { render } from "../substrate/prompts.ts";
27
- import { failFor, ok, type Result } from "../substrate/result.ts";
35
+ } from "../../../substrate/coldDoor.ts";
36
+ import { registerPerkCommand } from "../../../substrate/command.ts";
37
+ import { subagentModel } from "../../../substrate/config.ts";
38
+ import { render } from "../../../substrate/prompts.ts";
39
+ import { failFor, ok, type Result } from "../../../substrate/result.ts";
28
40
  import {
29
41
  arrayParam,
30
42
  numberParam,
@@ -32,50 +44,31 @@ import {
32
44
  paramsOf,
33
45
  stringParam,
34
46
  type ToolParams,
35
- } from "../substrate/toolParams.ts";
36
- import { appendWorkflowState, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
37
- import { type ReportTarget, report } from "../surfaces/report.ts";
47
+ } from "../../../substrate/toolParams.ts";
48
+ import { activePlanRef } from "../../../substrate/workflowState.ts";
49
+ import { type ReportTarget, report } from "../../../surfaces/report.ts";
38
50
  import {
51
+ type ReportWave,
52
+ type ReportWaveAttemptReceipt,
39
53
  toAttemptReceipt,
40
- type WaveAdapter,
41
- type WaveAttemptReceipt,
42
- } from "../waves/reportWave.ts";
54
+ } from "../../../waves/reportWave.ts";
43
55
  import {
44
- CLASSIFY_LANE_KEY,
56
+ CLASSIFY_ASSIGNMENT_KEY,
45
57
  REVIEW_CLASSIFIER_FLOW,
46
58
  runReviewClassifierWave,
47
- } from "../waves/reviewClassifierWave.ts";
48
- import { createRpcWaveAdapter } from "../waves/rpcAdapter.ts";
49
- import { planningStageRefusal } from "./lifecycleGates.ts";
50
- import { driveConflictResolution, type SubmitOk, submitPr } from "./submit.ts";
51
-
52
- export interface ThreadInput {
53
- thread_id: string;
54
- comment?: string;
55
- }
56
-
57
- interface ResolveCounts {
58
- actionable?: number;
59
- informational?: number;
60
- praise?: number;
61
- question?: number;
62
- }
63
-
64
- interface ResolveParams {
65
- threads: ThreadInput[];
66
- pr?: number;
67
- counts?: ResolveCounts;
68
- }
59
+ } from "../../../waves/reviewClassifierWave.ts";
60
+ import { driveConflictFollowUp, publishDepsFor, renderPublishedMessage } from "./submit.ts";
61
+ import type { SubmitConflictController } from "./submitConflict.ts";
69
62
 
70
63
  /** The four known `counts` keys (recorded into workflow-state — strict-decoded). */
71
64
  const COUNT_KEYS = ["actionable", "informational", "praise", "question"] as const;
72
65
 
73
66
  /** Decode the optional `counts` object; null = present-but-mistyped (a key or the object). */
74
- function decodeCounts(p: ToolParams): ResolveCounts | undefined | null {
67
+ function decodeCounts(p: ToolParams): AddressFinalization["counts"] | null {
75
68
  const raw = objectParam(p, "counts");
76
69
  if (raw === undefined) return undefined;
77
70
  if (raw === null) return null;
78
- const counts: ResolveCounts = {};
71
+ const counts: NonNullable<AddressFinalization["counts"]> = {};
79
72
  for (const key of COUNT_KEYS) {
80
73
  const value = numberParam(raw, key);
81
74
  if (value === null) return null;
@@ -85,12 +78,12 @@ function decodeCounts(p: ToolParams): ResolveCounts | undefined | null {
85
78
  }
86
79
 
87
80
  /**
88
- * Decode unknown tool-call params into `ResolveParams` (the tool-boundary seam).
89
- * `threads` absent or non-array decodes to `[]` (so the existing empty-batch `bad_input` arm
90
- * fires); any malformed ROW → null — whole-batch refusal, since resolving a guessed subset of
91
- * threads is a durable GitHub mutation. `pr`/`counts` mistyped → null (recorded state).
81
+ * Decode unknown tool-call params into `AddressFinalization` (the tool-boundary seam).
82
+ * `threads` absent or non-array decodes to `[]` (so the feature's empty-batch refusal fires);
83
+ * any malformed ROW → null — whole-batch refusal, since resolving a guessed subset of threads
84
+ * is a durable GitHub mutation. `pr`/`counts` mistyped → null (recorded state).
92
85
  */
93
- export function decodeResolveParams(params: unknown): ResolveParams | null {
86
+ export function decodeResolveParams(params: unknown): AddressFinalization | null {
94
87
  const p = paramsOf(params);
95
88
  if (p === null) return null;
96
89
  const rawThreads = arrayParam(p, "threads");
@@ -112,41 +105,6 @@ export function decodeResolveParams(params: unknown): ResolveParams | null {
112
105
  return { threads, pr, counts };
113
106
  }
114
107
 
115
- /** One per-thread outcome row from the cold door's batch result. */
116
- export interface ThreadResultRow {
117
- thread_id: string;
118
- success: boolean;
119
- comment_added: boolean;
120
- error?: string | null;
121
- }
122
-
123
- /** The ok-arm fields. */
124
- export interface ResolveOk {
125
- results: ThreadResultRow[];
126
- resolved_thread_ids: string[];
127
- }
128
-
129
- /** The partial-failure branch carries the per-thread detail on the fail arm too. */
130
- export interface ResolveFailExtras {
131
- results?: ThreadResultRow[];
132
- resolved_thread_ids?: string[];
133
- }
134
-
135
- export type ResolveResult = Result<ResolveOk, ResolveFailExtras>;
136
-
137
- /** The full-success payload returned by the terminating model-facing finalizer. */
138
- export interface FinalizeAddressOk extends ResolveOk {
139
- submit: SubmitOk;
140
- }
141
-
142
- /** A resolve failure after publication carries successful submit facts and safe retry input. */
143
- export interface FinalizeAddressFailExtras extends ResolveFailExtras {
144
- submit?: SubmitOk;
145
- retry_threads?: ThreadInput[];
146
- }
147
-
148
- export type FinalizeAddressResult = Result<FinalizeAddressOk, FinalizeAddressFailExtras>;
149
-
150
108
  /**
151
109
  * Narrow the cold door's `results` array to per-thread rows. Strict per row on `thread_id`,
152
110
  * `success`, `comment_added`; lenient on the report-only `error` (wrong-typed coerces to null).
@@ -174,175 +132,115 @@ function decodeRows(payload: ColdJson): ThreadResultRow[] | null {
174
132
  }
175
133
 
176
134
  /**
177
- * Build the only safe automatic retry batch from a partial cold-door report. Successful rows are
178
- * omitted. A reply is retained only when the row positively reports that it was not posted; an
179
- * absent result row is outcome-unknown, so its reply is stripped rather than risked twice.
180
- */
181
- function retryThreads(params: ResolveParams, rows: ThreadResultRow[]): ThreadInput[] {
182
- const byId = new Map(rows.map((row) => [row.thread_id, row]));
183
- const seen = new Set<string>();
184
- const retry: ThreadInput[] = [];
185
- for (const input of params.threads) {
186
- if (seen.has(input.thread_id)) continue;
187
- seen.add(input.thread_id);
188
- const row = byId.get(input.thread_id);
189
- if (row?.success === true) continue;
190
- if (row?.comment_added === false && input.comment !== undefined) {
191
- retry.push({ thread_id: input.thread_id, comment: input.comment });
192
- } else {
193
- retry.push({ thread_id: input.thread_id });
194
- }
195
- }
196
- return retry;
197
- }
198
-
199
- /**
200
- * Resolve a batch of review threads (the parent's mechanical resolve step). Delegates to the Python
201
- * cold door; returns a soft result (never throws). On success, records `last_review_batch`.
135
+ * The production `ResolveThreads` port: `perk pr resolve-threads --json --batch` through the
136
+ * cold-door seam (the temp-file stdin channel). A failure envelope whose payload re-narrows
137
+ * with the SAME rows decode is a `partial` report (the per-thread detail is trustworthy); an
138
+ * absent/malformed payload drops the partial detail — `failed` (uncertainty ⇒ no half-rendered
139
+ * partial table).
202
140
  */
203
- export async function resolveReviewThreads(
204
- pi: ExtensionAPI,
205
- ctx: ExtensionContext,
206
- params: ResolveParams,
207
- ): Promise<ResolveResult> {
208
- const fail = failFor(ctx, "address", "resolve_review_threads");
209
-
210
- const threads = Array.isArray(params?.threads) ? params.threads : [];
211
- if (threads.length === 0) {
212
- return fail("no threads to resolve (pass { threads: [{thread_id, comment?}] })", "bad_input");
213
- }
214
- const batch = threads.map((t) => ({ thread_id: t.thread_id, comment: t.comment ?? null }));
215
-
216
- const r = await runColdDoor<ThreadResultRow[]>(pi, ctx, ["pr", "resolve-threads", "--json"], {
217
- label: "perk pr resolve-threads",
218
- decode: decodeRows,
219
- stdin: {
220
- flag: "--batch",
221
- content: `${JSON.stringify(batch, null, 2)}\n`,
222
- filename: `resolve-batch-${Date.now()}.json`,
223
- },
224
- });
225
-
226
- if (!r.ok) {
227
- // A partial/failed batch is loud-but-soft: surface the per-thread detail, do not throw. The
228
- // detail rides the failure envelope's payload; absent/malformed rows ⇒ plain fail (advisory
229
- // drop — never a half-rendered partial table).
141
+ function createThreadResolver(pi: ExtensionAPI, ctx: ExtensionContext): ResolveThreads {
142
+ return async (threads) => {
143
+ const batch = threads.map((t) => ({ thread_id: t.thread_id, comment: t.comment ?? null }));
144
+ const r = await runColdDoor<ThreadResultRow[]>(pi, ctx, ["pr", "resolve-threads", "--json"], {
145
+ label: "perk pr resolve-threads",
146
+ decode: decodeRows,
147
+ stdin: {
148
+ flag: "--batch",
149
+ content: `${JSON.stringify(batch, null, 2)}\n`,
150
+ filename: `resolve-batch-${Date.now()}.json`,
151
+ },
152
+ });
153
+ if (r.ok) return { ok: true, rows: r.data };
230
154
  const rows = r.payload !== undefined ? decodeRows(r.payload) : null;
231
- if (r.payload === undefined || rows === null) return fail(r.message, r.errorType);
232
- const resolvedIds = rows.filter((row) => row.success).map((row) => row.thread_id);
155
+ if (r.payload === undefined || rows === null) {
156
+ return { ok: false, kind: "failed", message: r.message, errorType: r.errorType };
157
+ }
233
158
  const failed = rows.filter((row) => !row.success).length;
234
- const error = stringField(r.payload, "message") ?? `${failed} thread(s) did not resolve`;
235
- report(ctx, "address", "error", error, { alsoLog: true });
236
159
  return {
237
- content: [
238
- {
239
- type: "text",
240
- text: `Resolved ${resolvedIds.length}/${rows.length} thread(s); ${failed} failed.`,
241
- },
242
- ],
243
- details: {
244
- ok: false,
245
- error,
246
- error_type: stringField(r.payload, "error_type") ?? "partial_failure",
247
- results: rows,
248
- resolved_thread_ids: resolvedIds,
249
- },
160
+ ok: false,
161
+ kind: "partial",
162
+ rows,
163
+ message: stringField(r.payload, "message") ?? `${failed} thread(s) did not resolve`,
164
+ errorType: stringField(r.payload, "error_type") ?? "partial_failure",
250
165
  };
251
- }
252
-
253
- const results = r.data;
254
- const resolvedIds = results.filter((row) => row.success).map((row) => row.thread_id);
255
-
256
- // Record the batch (tier-3, best-effort-with-logging, idempotent, headless-safe). Strict
257
- // read-back via rebuild — loud-but-non-fatal, the resolve already succeeded.
258
- const recordedBatch = {
259
- pr: params.pr ?? null,
260
- counts: params.counts ?? null,
261
- resolved_thread_ids: resolvedIds,
262
- at: new Date().toISOString(),
263
166
  };
264
- appendWorkflowState(pi, ctx, {
265
- data: { last_review_batch: recordedBatch },
266
- field: "last_review_batch",
267
- expected: recordedBatch,
268
- scope: "address",
269
- failure: "last_review_batch read-back failed",
270
- });
167
+ }
271
168
 
272
- return ok(`Resolved ${resolvedIds.length} review thread(s).`, {
273
- results,
274
- resolved_thread_ids: resolvedIds,
275
- });
169
+ /** The full-success payload returned by the terminating model-facing finalizer. */
170
+ interface FinalizeAddressOk {
171
+ submit: PublishedChange;
172
+ results: ThreadResultRow[];
173
+ resolved_thread_ids: string[];
174
+ }
175
+
176
+ /** A resolve failure after publication carries successful submit facts and safe retry input. */
177
+ interface FinalizeAddressFailExtras {
178
+ submit?: PublishedChange;
179
+ results?: ThreadResultRow[];
180
+ resolved_thread_ids?: string[];
181
+ retry_threads?: ThreadInput[];
276
182
  }
277
183
 
184
+ type FinalizeAddressResult = Result<FinalizeAddressOk, FinalizeAddressFailExtras>;
185
+
278
186
  /**
279
- * Publish committed address fixes, then resolve their review threads. Full success terminates the
280
- * turn; either failure is non-terminating and explains the safe retry boundary.
187
+ * The finalize execute core: planning refusal first (a positioned stacked planning session's
188
+ * cwd binding is the PREDECESSOR), then the feature op over the one production composition —
189
+ * `publishDepsFor` extended with the resolve port and the branch session — then the outcome →
190
+ * Result projection. Publish notes were already reported by the shared publisher at publish
191
+ * time (pre-resolve order preserved on EVERY published arm).
281
192
  */
282
- export async function finalizeAddress(
193
+ async function executeFinalizeAddress(
283
194
  pi: ExtensionAPI,
284
195
  ctx: ExtensionContext,
285
- params: ResolveParams,
196
+ input: AddressFinalization,
197
+ controller: SubmitConflictController,
286
198
  ): Promise<FinalizeAddressResult> {
287
199
  const fail = failFor<FinalizeAddressFailExtras>(ctx, "address", "finalize_address");
288
- // Planning sessions never legitimately publish review fixes — the first check (a positioned
289
- // stacked planning session's cwd binding is the PREDECESSOR).
290
200
  const planningRefusal = planningStageRefusal(ctx, "address");
291
201
  if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
292
- if (params.threads.length === 0) {
293
- return fail("no threads to finalize (pass { threads: [{thread_id, comment?}] })", "bad_input");
294
- }
295
- const submitted = await submitPr(pi, ctx);
296
- if (!submitted.details.ok) {
297
- return fail(
298
- `propagation failed; threads were NOT resolved — ${submitted.details.error}. ` +
299
- "Fix the publication failure, then re-run finalize_address.",
300
- submitted.details.error_type,
301
- );
302
- }
202
+ // Preserve the feature's empty-batch refusal before counter/run-id reads.
203
+ if (input.threads.length > 0) controller.clear();
303
204
 
304
- // Keep the nested payload clean: FinalizeAddressOk already has its own top-level `ok` marker.
305
- const { ok: _submittedOk, ...submit } = submitted.details;
306
- const resolved = await resolveReviewThreads(pi, ctx, params);
307
- if (!resolved.details.ok) {
308
- const retryCandidates =
309
- resolved.details.results === undefined
310
- ? undefined
311
- : retryThreads(params, resolved.details.results);
312
- const retry =
313
- retryCandidates !== undefined && retryCandidates.length > 0 ? retryCandidates : undefined;
314
- const extras: FinalizeAddressFailExtras = {
315
- submit,
316
- ...(resolved.details.results === undefined ? {} : { results: resolved.details.results }),
317
- ...(resolved.details.resolved_thread_ids === undefined
318
- ? {}
319
- : { resolved_thread_ids: resolved.details.resolved_thread_ids }),
320
- ...(retry === undefined ? {} : { retry_threads: retry }),
321
- };
322
- const retryGuidance =
323
- retry === undefined
324
- ? "Inspect the resolution failure before retrying; omit any reply that may already have posted."
325
- : "Re-run finalize_address with only details.retry_threads; successful rows were omitted " +
326
- "and replies already reported as posted were stripped.";
327
- return fail(
328
- `propagation succeeded, but thread resolution failed: ${resolved.details.error}. ` +
329
- `The submit already succeeded. ${retryGuidance}`,
330
- resolved.details.error_type,
331
- extras,
332
- );
205
+ const deps: FinalizeAddressDeps = {
206
+ ...publishDepsFor(pi, ctx),
207
+ resolve: createThreadResolver(pi, ctx),
208
+ session: openBranchWorkflowSession(pi, ctx),
209
+ };
210
+ const outcome = await finalizeAddress(deps, input);
211
+ switch (outcome.kind) {
212
+ case "empty_batch":
213
+ return fail(outcome.message, "bad_input");
214
+ case "not_published":
215
+ // Today's two-report publish failure: the inner submit-scope report (the raw publisher
216
+ // failure), then the address-scope finalizer failure.
217
+ report(ctx, "submit", "error", outcome.publishMessage, { alsoLog: true });
218
+ return fail(outcome.message, outcome.errorType);
219
+ case "published_partial":
220
+ report(ctx, "address", "error", outcome.resolveMessage, { alsoLog: true });
221
+ return fail(outcome.message, outcome.errorType, {
222
+ submit: { ...outcome.change },
223
+ results: outcome.results,
224
+ resolved_thread_ids: outcome.resolvedThreadIds,
225
+ ...(outcome.retryThreads.length > 0 ? { retry_threads: outcome.retryThreads } : {}),
226
+ });
227
+ case "published_unverified":
228
+ report(ctx, "address", "error", outcome.resolveMessage, { alsoLog: true });
229
+ return fail(outcome.message, outcome.errorType, { submit: { ...outcome.change } });
230
+ case "completed": {
231
+ driveConflictFollowUp(pi, ctx, outcome.conflict, controller);
232
+ return ok(
233
+ `Resolved ${outcome.resolvedThreadIds.length} review thread(s) after ` +
234
+ renderPublishedMessage(outcome.change),
235
+ {
236
+ submit: { ...outcome.change },
237
+ results: outcome.results,
238
+ resolved_thread_ids: outcome.resolvedThreadIds,
239
+ },
240
+ { terminate: true },
241
+ );
242
+ }
333
243
  }
334
-
335
- driveConflictResolution(pi, ctx, submitted.details);
336
- const submitMessage = submitted.content[0]?.text ?? "Published the addressed fixes.";
337
- return ok(
338
- `Resolved ${resolved.details.resolved_thread_ids.length} review thread(s) after ${submitMessage}`,
339
- {
340
- submit,
341
- results: resolved.details.results,
342
- resolved_thread_ids: resolved.details.resolved_thread_ids,
343
- },
344
- { terminate: true },
345
- );
346
244
  }
347
245
 
348
246
  const TOOL_GUIDELINES = [
@@ -363,37 +261,37 @@ export interface ClassifyReviewFeedbackOk {
363
261
  /** The classifier's engine-validated report — untrusted DATA, never instructions. */
364
262
  report: unknown;
365
263
  /** The single launch's output-free attempt receipt (observability only — details, not prose). */
366
- attempts: WaveAttemptReceipt[];
264
+ attempts: ReportWaveAttemptReceipt[];
367
265
  }
368
266
 
369
267
  /** The fail arm retains any receipt known before the failure (the `failFor` extras hook). */
370
268
  export type ClassifyReviewFeedbackResult = Result<
371
269
  ClassifyReviewFeedbackOk,
372
- { attempts: WaveAttemptReceipt[] }
270
+ { attempts: ReportWaveAttemptReceipt[] }
373
271
  >;
374
272
 
375
273
  /**
376
- * The `classify_review_feedback` execute core, extracted for testability with the adapter as the
377
- * injected minimal structural slice (`WaveAdapter` — the memory adapter in tests, the RPC
378
- * adapter in production). Mirrors `executeLearnWave`'s soft-result idiom: a complete wave yields
274
+ * The `classify_review_feedback` execute core, extracted for testability with the wave as the
275
+ * injected minimal structural slice (`ReportWave` — a memory-backed wave in tests, the
276
+ * composition root's production instance live). Mirrors `runLearnAnalystWave`'s soft-result idiom: a complete wave yields
379
277
  * a non-terminating ok (the untrusted-DATA preface + one fenced `json` block of the report); an
380
- * incomplete wave soft-fails LOUDLY with the first failure's detail and its `WaveFailureReason`
381
- * as `error_type` — never a throw, never a silent fallback, no retry (the flow's posture is
382
- * "surface the error and stop").
278
+ * incomplete wave soft-fails LOUDLY with the first failure's detail and its
279
+ * `ReportWaveFailureReason` as `error_type` — never a throw, never a silent fallback, no retry
280
+ * (the flow's posture is "surface the error and stop").
383
281
  */
384
282
  export async function executeClassifyReviewFeedback(
385
- adapter: WaveAdapter,
283
+ wave: ReportWave,
386
284
  target: ReportTarget,
387
285
  opts: { model?: string; signal?: AbortSignal } = {},
388
286
  ): Promise<ClassifyReviewFeedbackResult> {
389
- const fail = failFor<{ attempts: WaveAttemptReceipt[] }>(
287
+ const fail = failFor<{ attempts: ReportWaveAttemptReceipt[] }>(
390
288
  target,
391
289
  "address",
392
290
  "classify_review_feedback",
393
291
  );
394
- const result = await runReviewClassifierWave(adapter, opts);
292
+ const result = await runReviewClassifierWave(wave, opts);
395
293
  const attempts = [
396
- toAttemptReceipt(REVIEW_CLASSIFIER_FLOW, 1, [CLASSIFY_LANE_KEY], result.receipt),
294
+ toAttemptReceipt(REVIEW_CLASSIFIER_FLOW, 1, [CLASSIFY_ASSIGNMENT_KEY], result.receipt),
397
295
  ];
398
296
  if (!result.complete) {
399
297
  const failure = result.failures[0];
@@ -410,28 +308,13 @@ export async function executeClassifyReviewFeedback(
410
308
  return ok(text, { report: laneReport, attempts });
411
309
  }
412
310
 
413
- /** Resolve the active plan-ref (worktree first, then the rebuilt workflow-state). The converged
414
- * address body carries the PR identity, so the warm door must resolve a ref — and `/address`
415
- * cannot function without one regardless (the classifier child's `perk pr feedback` hard-errors
416
- * `no_plan_ref`). Mirrors `doors/learn.ts`'s helper. */
417
- function activePlanRef(ctx: ExtensionContext): PlanRef | null {
418
- const fromWorktree = readPlanRef(ctx.cwd);
419
- if (fromWorktree) return fromWorktree;
420
- try {
421
- const branch = branchOf(ctx);
422
- return (rebuildWorkflowState(branch).active_plan_ref as PlanRef | null) ?? null;
423
- } catch {
424
- return null;
425
- }
426
- }
427
-
428
311
  /** Inject the address-workflow guidance the model follows (the perk-address skill pointer is
429
312
  * delivered by the skill-binding suffix — not hardcoded here). The classify step is ONE
430
313
  * `classify_review_feedback` call — the tool owns the wave mechanics, the report schema, and
431
314
  * reads the configured `[models.subagents] review-classifier` model at execute time.
432
315
  *
433
316
  * The wording lives in the shared canonical templates `prompts/stages/address/*` rendered via the
434
- * cross-plane render seam (contracts.md §8.31) — the warm door converges onto the SAME two
317
+ * cross-plane render seam (contracts.md §8.31) — the warm surface converges onto the SAME two
435
318
  * templates as the cold `_address_prompt` and the worker `initialPromptFor("address")`. Branching
436
319
  * stays in code: preview/action selects the template. */
437
320
  export function addressGuidance(ref: PlanRef, preview: boolean): string {
@@ -443,9 +326,13 @@ export function addressGuidance(ref: PlanRef, preview: boolean): string {
443
326
  return render(preview ? "stages/address/preview.md" : "stages/address/action.md", variables);
444
327
  }
445
328
 
446
- /** Register the warm door: the `classify_review_feedback` + terminating `finalize_address`
447
- * tools and the `/address` command. */
448
- export function registerAddress(pi: ExtensionAPI): void {
329
+ /** Install the review-feedback bindings: the `classify_review_feedback` + terminating
330
+ * `finalize_address` tools and the `/address` command. */
331
+ export function installAddressBindings(
332
+ pi: ExtensionAPI,
333
+ wave: ReportWave,
334
+ controller: SubmitConflictController,
335
+ ): void {
449
336
  pi.registerTool({
450
337
  name: "classify_review_feedback",
451
338
  label: "Classify review feedback",
@@ -468,7 +355,7 @@ export function registerAddress(pi: ExtensionAPI): void {
468
355
  // gitignored `.perk/local.toml` overlay to the MAIN checkout, so a per-user override
469
356
  // survives the cold worktree launch (worktrees never materialize local.toml).
470
357
  const model = subagentModel(ctx.cwd, "review-classifier");
471
- return executeClassifyReviewFeedback(createRpcWaveAdapter(pi.events), ctx, {
358
+ return executeClassifyReviewFeedback(wave, ctx, {
472
359
  ...(model !== undefined ? { model } : {}),
473
360
  ...(signal !== undefined ? { signal } : {}),
474
361
  });
@@ -525,7 +412,7 @@ export function registerAddress(pi: ExtensionAPI): void {
525
412
  "finalize_address",
526
413
  )("finalize_address needs { threads: [{thread_id, comment?}] }", "bad_input");
527
414
  }
528
- return finalizeAddress(pi, ctx, decoded);
415
+ return executeFinalizeAddress(pi, ctx, decoded, controller);
529
416
  },
530
417
  });
531
418