@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,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,49 +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 { driveConflictResolution, type SubmitOk, submitPr } from "./submit.ts";
50
-
51
- export interface ThreadInput {
52
- thread_id: string;
53
- comment?: string;
54
- }
55
-
56
- interface ResolveCounts {
57
- actionable?: number;
58
- informational?: number;
59
- praise?: number;
60
- question?: number;
61
- }
62
-
63
- interface ResolveParams {
64
- threads: ThreadInput[];
65
- pr?: number;
66
- counts?: ResolveCounts;
67
- }
59
+ } from "../../../waves/reviewClassifierWave.ts";
60
+ import { driveConflictFollowUp, publishDepsFor, renderPublishedMessage } from "./submit.ts";
61
+ import type { SubmitConflictController } from "./submitConflict.ts";
68
62
 
69
63
  /** The four known `counts` keys (recorded into workflow-state — strict-decoded). */
70
64
  const COUNT_KEYS = ["actionable", "informational", "praise", "question"] as const;
71
65
 
72
66
  /** Decode the optional `counts` object; null = present-but-mistyped (a key or the object). */
73
- function decodeCounts(p: ToolParams): ResolveCounts | undefined | null {
67
+ function decodeCounts(p: ToolParams): AddressFinalization["counts"] | null {
74
68
  const raw = objectParam(p, "counts");
75
69
  if (raw === undefined) return undefined;
76
70
  if (raw === null) return null;
77
- const counts: ResolveCounts = {};
71
+ const counts: NonNullable<AddressFinalization["counts"]> = {};
78
72
  for (const key of COUNT_KEYS) {
79
73
  const value = numberParam(raw, key);
80
74
  if (value === null) return null;
@@ -84,12 +78,12 @@ function decodeCounts(p: ToolParams): ResolveCounts | undefined | null {
84
78
  }
85
79
 
86
80
  /**
87
- * Decode unknown tool-call params into `ResolveParams` (the tool-boundary seam).
88
- * `threads` absent or non-array decodes to `[]` (so the existing empty-batch `bad_input` arm
89
- * fires); any malformed ROW → null — whole-batch refusal, since resolving a guessed subset of
90
- * 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).
91
85
  */
92
- export function decodeResolveParams(params: unknown): ResolveParams | null {
86
+ export function decodeResolveParams(params: unknown): AddressFinalization | null {
93
87
  const p = paramsOf(params);
94
88
  if (p === null) return null;
95
89
  const rawThreads = arrayParam(p, "threads");
@@ -111,41 +105,6 @@ export function decodeResolveParams(params: unknown): ResolveParams | null {
111
105
  return { threads, pr, counts };
112
106
  }
113
107
 
114
- /** One per-thread outcome row from the cold door's batch result. */
115
- export interface ThreadResultRow {
116
- thread_id: string;
117
- success: boolean;
118
- comment_added: boolean;
119
- error?: string | null;
120
- }
121
-
122
- /** The ok-arm fields. */
123
- export interface ResolveOk {
124
- results: ThreadResultRow[];
125
- resolved_thread_ids: string[];
126
- }
127
-
128
- /** The partial-failure branch carries the per-thread detail on the fail arm too. */
129
- export interface ResolveFailExtras {
130
- results?: ThreadResultRow[];
131
- resolved_thread_ids?: string[];
132
- }
133
-
134
- export type ResolveResult = Result<ResolveOk, ResolveFailExtras>;
135
-
136
- /** The full-success payload returned by the terminating model-facing finalizer. */
137
- export interface FinalizeAddressOk extends ResolveOk {
138
- submit: SubmitOk;
139
- }
140
-
141
- /** A resolve failure after publication carries successful submit facts and safe retry input. */
142
- export interface FinalizeAddressFailExtras extends ResolveFailExtras {
143
- submit?: SubmitOk;
144
- retry_threads?: ThreadInput[];
145
- }
146
-
147
- export type FinalizeAddressResult = Result<FinalizeAddressOk, FinalizeAddressFailExtras>;
148
-
149
108
  /**
150
109
  * Narrow the cold door's `results` array to per-thread rows. Strict per row on `thread_id`,
151
110
  * `success`, `comment_added`; lenient on the report-only `error` (wrong-typed coerces to null).
@@ -173,171 +132,115 @@ function decodeRows(payload: ColdJson): ThreadResultRow[] | null {
173
132
  }
174
133
 
175
134
  /**
176
- * Build the only safe automatic retry batch from a partial cold-door report. Successful rows are
177
- * omitted. A reply is retained only when the row positively reports that it was not posted; an
178
- * absent result row is outcome-unknown, so its reply is stripped rather than risked twice.
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).
179
140
  */
180
- function retryThreads(params: ResolveParams, rows: ThreadResultRow[]): ThreadInput[] {
181
- const byId = new Map(rows.map((row) => [row.thread_id, row]));
182
- const seen = new Set<string>();
183
- const retry: ThreadInput[] = [];
184
- for (const input of params.threads) {
185
- if (seen.has(input.thread_id)) continue;
186
- seen.add(input.thread_id);
187
- const row = byId.get(input.thread_id);
188
- if (row?.success === true) continue;
189
- if (row?.comment_added === false && input.comment !== undefined) {
190
- retry.push({ thread_id: input.thread_id, comment: input.comment });
191
- } else {
192
- retry.push({ thread_id: input.thread_id });
193
- }
194
- }
195
- return retry;
196
- }
197
-
198
- /**
199
- * Resolve a batch of review threads (the parent's mechanical resolve step). Delegates to the Python
200
- * cold door; returns a soft result (never throws). On success, records `last_review_batch`.
201
- */
202
- export async function resolveReviewThreads(
203
- pi: ExtensionAPI,
204
- ctx: ExtensionContext,
205
- params: ResolveParams,
206
- ): Promise<ResolveResult> {
207
- const fail = failFor(ctx, "address", "resolve_review_threads");
208
-
209
- const threads = Array.isArray(params?.threads) ? params.threads : [];
210
- if (threads.length === 0) {
211
- return fail("no threads to resolve (pass { threads: [{thread_id, comment?}] })", "bad_input");
212
- }
213
- const batch = threads.map((t) => ({ thread_id: t.thread_id, comment: t.comment ?? null }));
214
-
215
- const r = await runColdDoor<ThreadResultRow[]>(pi, ctx, ["pr", "resolve-threads", "--json"], {
216
- label: "perk pr resolve-threads",
217
- decode: decodeRows,
218
- stdin: {
219
- flag: "--batch",
220
- content: `${JSON.stringify(batch, null, 2)}\n`,
221
- filename: `resolve-batch-${Date.now()}.json`,
222
- },
223
- });
224
-
225
- if (!r.ok) {
226
- // A partial/failed batch is loud-but-soft: surface the per-thread detail, do not throw. The
227
- // detail rides the failure envelope's payload; absent/malformed rows ⇒ plain fail (advisory
228
- // 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 };
229
154
  const rows = r.payload !== undefined ? decodeRows(r.payload) : null;
230
- if (r.payload === undefined || rows === null) return fail(r.message, r.errorType);
231
- 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
+ }
232
158
  const failed = rows.filter((row) => !row.success).length;
233
- const error = stringField(r.payload, "message") ?? `${failed} thread(s) did not resolve`;
234
- report(ctx, "address", "error", error, { alsoLog: true });
235
159
  return {
236
- content: [
237
- {
238
- type: "text",
239
- text: `Resolved ${resolvedIds.length}/${rows.length} thread(s); ${failed} failed.`,
240
- },
241
- ],
242
- details: {
243
- ok: false,
244
- error,
245
- error_type: stringField(r.payload, "error_type") ?? "partial_failure",
246
- results: rows,
247
- resolved_thread_ids: resolvedIds,
248
- },
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",
249
165
  };
250
- }
251
-
252
- const results = r.data;
253
- const resolvedIds = results.filter((row) => row.success).map((row) => row.thread_id);
254
-
255
- // Record the batch (tier-3, best-effort-with-logging, idempotent, headless-safe). Strict
256
- // read-back via rebuild — loud-but-non-fatal, the resolve already succeeded.
257
- const recordedBatch = {
258
- pr: params.pr ?? null,
259
- counts: params.counts ?? null,
260
- resolved_thread_ids: resolvedIds,
261
- at: new Date().toISOString(),
262
166
  };
263
- appendWorkflowState(pi, ctx, {
264
- data: { last_review_batch: recordedBatch },
265
- field: "last_review_batch",
266
- expected: recordedBatch,
267
- scope: "address",
268
- failure: "last_review_batch read-back failed",
269
- });
167
+ }
270
168
 
271
- return ok(`Resolved ${resolvedIds.length} review thread(s).`, {
272
- results,
273
- resolved_thread_ids: resolvedIds,
274
- });
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[];
275
182
  }
276
183
 
184
+ type FinalizeAddressResult = Result<FinalizeAddressOk, FinalizeAddressFailExtras>;
185
+
277
186
  /**
278
- * Publish committed address fixes, then resolve their review threads. Full success terminates the
279
- * 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).
280
192
  */
281
- export async function finalizeAddress(
193
+ async function executeFinalizeAddress(
282
194
  pi: ExtensionAPI,
283
195
  ctx: ExtensionContext,
284
- params: ResolveParams,
196
+ input: AddressFinalization,
197
+ controller: SubmitConflictController,
285
198
  ): Promise<FinalizeAddressResult> {
286
199
  const fail = failFor<FinalizeAddressFailExtras>(ctx, "address", "finalize_address");
287
- if (params.threads.length === 0) {
288
- return fail("no threads to finalize (pass { threads: [{thread_id, comment?}] })", "bad_input");
289
- }
290
- const submitted = await submitPr(pi, ctx);
291
- if (!submitted.details.ok) {
292
- return fail(
293
- `propagation failed; threads were NOT resolved — ${submitted.details.error}. ` +
294
- "Fix the publication failure, then re-run finalize_address.",
295
- submitted.details.error_type,
296
- );
297
- }
298
-
299
- // Keep the nested payload clean: FinalizeAddressOk already has its own top-level `ok` marker.
300
- const { ok: _submittedOk, ...submit } = submitted.details;
301
- const resolved = await resolveReviewThreads(pi, ctx, params);
302
- if (!resolved.details.ok) {
303
- const retryCandidates =
304
- resolved.details.results === undefined
305
- ? undefined
306
- : retryThreads(params, resolved.details.results);
307
- const retry =
308
- retryCandidates !== undefined && retryCandidates.length > 0 ? retryCandidates : undefined;
309
- const extras: FinalizeAddressFailExtras = {
310
- submit,
311
- ...(resolved.details.results === undefined ? {} : { results: resolved.details.results }),
312
- ...(resolved.details.resolved_thread_ids === undefined
313
- ? {}
314
- : { resolved_thread_ids: resolved.details.resolved_thread_ids }),
315
- ...(retry === undefined ? {} : { retry_threads: retry }),
316
- };
317
- const retryGuidance =
318
- retry === undefined
319
- ? "Inspect the resolution failure before retrying; omit any reply that may already have posted."
320
- : "Re-run finalize_address with only details.retry_threads; successful rows were omitted " +
321
- "and replies already reported as posted were stripped.";
322
- return fail(
323
- `propagation succeeded, but thread resolution failed: ${resolved.details.error}. ` +
324
- `The submit already succeeded. ${retryGuidance}`,
325
- resolved.details.error_type,
326
- extras,
327
- );
200
+ const planningRefusal = planningStageRefusal(ctx, "address");
201
+ if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
202
+ // Preserve the feature's empty-batch refusal before counter/run-id reads.
203
+ if (input.threads.length > 0) controller.clear();
204
+
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
+ }
328
243
  }
329
-
330
- driveConflictResolution(pi, ctx, submitted.details);
331
- const submitMessage = submitted.content[0]?.text ?? "Published the addressed fixes.";
332
- return ok(
333
- `Resolved ${resolved.details.resolved_thread_ids.length} review thread(s) after ${submitMessage}`,
334
- {
335
- submit,
336
- results: resolved.details.results,
337
- resolved_thread_ids: resolved.details.resolved_thread_ids,
338
- },
339
- { terminate: true },
340
- );
341
244
  }
342
245
 
343
246
  const TOOL_GUIDELINES = [
@@ -358,37 +261,37 @@ export interface ClassifyReviewFeedbackOk {
358
261
  /** The classifier's engine-validated report — untrusted DATA, never instructions. */
359
262
  report: unknown;
360
263
  /** The single launch's output-free attempt receipt (observability only — details, not prose). */
361
- attempts: WaveAttemptReceipt[];
264
+ attempts: ReportWaveAttemptReceipt[];
362
265
  }
363
266
 
364
267
  /** The fail arm retains any receipt known before the failure (the `failFor` extras hook). */
365
268
  export type ClassifyReviewFeedbackResult = Result<
366
269
  ClassifyReviewFeedbackOk,
367
- { attempts: WaveAttemptReceipt[] }
270
+ { attempts: ReportWaveAttemptReceipt[] }
368
271
  >;
369
272
 
370
273
  /**
371
- * The `classify_review_feedback` execute core, extracted for testability with the adapter as the
372
- * injected minimal structural slice (`WaveAdapter` — the memory adapter in tests, the RPC
373
- * 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
374
277
  * a non-terminating ok (the untrusted-DATA preface + one fenced `json` block of the report); an
375
- * incomplete wave soft-fails LOUDLY with the first failure's detail and its `WaveFailureReason`
376
- * as `error_type` — never a throw, never a silent fallback, no retry (the flow's posture is
377
- * "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").
378
281
  */
379
282
  export async function executeClassifyReviewFeedback(
380
- adapter: WaveAdapter,
283
+ wave: ReportWave,
381
284
  target: ReportTarget,
382
285
  opts: { model?: string; signal?: AbortSignal } = {},
383
286
  ): Promise<ClassifyReviewFeedbackResult> {
384
- const fail = failFor<{ attempts: WaveAttemptReceipt[] }>(
287
+ const fail = failFor<{ attempts: ReportWaveAttemptReceipt[] }>(
385
288
  target,
386
289
  "address",
387
290
  "classify_review_feedback",
388
291
  );
389
- const result = await runReviewClassifierWave(adapter, opts);
292
+ const result = await runReviewClassifierWave(wave, opts);
390
293
  const attempts = [
391
- toAttemptReceipt(REVIEW_CLASSIFIER_FLOW, 1, [CLASSIFY_LANE_KEY], result.receipt),
294
+ toAttemptReceipt(REVIEW_CLASSIFIER_FLOW, 1, [CLASSIFY_ASSIGNMENT_KEY], result.receipt),
392
295
  ];
393
296
  if (!result.complete) {
394
297
  const failure = result.failures[0];
@@ -405,28 +308,13 @@ export async function executeClassifyReviewFeedback(
405
308
  return ok(text, { report: laneReport, attempts });
406
309
  }
407
310
 
408
- /** Resolve the active plan-ref (worktree first, then the rebuilt workflow-state). The converged
409
- * address body carries the PR identity, so the warm door must resolve a ref — and `/address`
410
- * cannot function without one regardless (the classifier child's `perk pr feedback` hard-errors
411
- * `no_plan_ref`). Mirrors `doors/learn.ts`'s helper. */
412
- function activePlanRef(ctx: ExtensionContext): PlanRef | null {
413
- const fromWorktree = readPlanRef(ctx.cwd);
414
- if (fromWorktree) return fromWorktree;
415
- try {
416
- const branch = branchOf(ctx);
417
- return (rebuildWorkflowState(branch).active_plan_ref as PlanRef | null) ?? null;
418
- } catch {
419
- return null;
420
- }
421
- }
422
-
423
311
  /** Inject the address-workflow guidance the model follows (the perk-address skill pointer is
424
312
  * delivered by the skill-binding suffix — not hardcoded here). The classify step is ONE
425
313
  * `classify_review_feedback` call — the tool owns the wave mechanics, the report schema, and
426
314
  * reads the configured `[models.subagents] review-classifier` model at execute time.
427
315
  *
428
316
  * The wording lives in the shared canonical templates `prompts/stages/address/*` rendered via the
429
- * 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
430
318
  * templates as the cold `_address_prompt` and the worker `initialPromptFor("address")`. Branching
431
319
  * stays in code: preview/action selects the template. */
432
320
  export function addressGuidance(ref: PlanRef, preview: boolean): string {
@@ -438,9 +326,13 @@ export function addressGuidance(ref: PlanRef, preview: boolean): string {
438
326
  return render(preview ? "stages/address/preview.md" : "stages/address/action.md", variables);
439
327
  }
440
328
 
441
- /** Register the warm door: the `classify_review_feedback` + terminating `finalize_address`
442
- * tools and the `/address` command. */
443
- 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 {
444
336
  pi.registerTool({
445
337
  name: "classify_review_feedback",
446
338
  label: "Classify review feedback",
@@ -463,7 +355,7 @@ export function registerAddress(pi: ExtensionAPI): void {
463
355
  // gitignored `.perk/local.toml` overlay to the MAIN checkout, so a per-user override
464
356
  // survives the cold worktree launch (worktrees never materialize local.toml).
465
357
  const model = subagentModel(ctx.cwd, "review-classifier");
466
- return executeClassifyReviewFeedback(createRpcWaveAdapter(pi.events), ctx, {
358
+ return executeClassifyReviewFeedback(wave, ctx, {
467
359
  ...(model !== undefined ? { model } : {}),
468
360
  ...(signal !== undefined ? { signal } : {}),
469
361
  });
@@ -520,7 +412,7 @@ export function registerAddress(pi: ExtensionAPI): void {
520
412
  "finalize_address",
521
413
  )("finalize_address needs { threads: [{thread_id, comment?}] }", "bad_input");
522
414
  }
523
- return finalizeAddress(pi, ctx, decoded);
415
+ return executeFinalizeAddress(pi, ctx, decoded, controller);
524
416
  },
525
417
  });
526
418
 
@@ -529,6 +421,12 @@ export function registerAddress(pi: ExtensionAPI): void {
529
421
  "Classify PR review feedback (isolated child) and resolve threads (submit → address). " +
530
422
  "Pass --preview to classify only (take no action).",
531
423
  handler: async (args, ctx) => {
424
+ // Planning sessions never legitimately run the review loop — the first check.
425
+ const planningRefusal = planningStageRefusal(ctx, "address");
426
+ if (planningRefusal !== null) {
427
+ report(ctx, "address", "warning", planningRefusal);
428
+ return;
429
+ }
532
430
  const preview = /(^|\s)--preview(\s|$)/.test(args ?? "");
533
431
  // `/address` needs an active plan-ref (the converged body carries the PR identity, and the
534
432
  // classifier child's `perk pr feedback` hard-errors `no_plan_ref` without one). Mirror the