@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
@@ -0,0 +1,451 @@
1
+ // The objective review arm of the `plan_review` door — a thin ADAPTER over the feature's
2
+ // `reviewObjectiveDraft` (authoring/objective/review.ts). This module owns what is genuinely
3
+ // Pi/provider-shaped: the headless skip, the wave arm's raw-baseline read ordering, the launch
4
+ // chooser, the two reviewer constructions (plannotator bridge / first-party view-only editor —
5
+ // provider vocabulary is translated INTO `ObjectiveReviewOutcome` here, the `# Direct Edits`
6
+ // heading check included), the composed `objectiveApprovalSaveV1` binding, and the rendered
7
+ // Result envelopes (every text/details shape byte-stable, `details.subject: "objective"`
8
+ // included).
9
+ //
10
+ // THE OBJECTIVE ARM: an objective-authoring session (read-only, stage `objective-author` or
11
+ // `objective-save` — the two stages whose working draft IS the objective draft; neither carries
12
+ // `plan_draft`) routes through `executeObjectiveReview` instead of the plan path — the
13
+ // reviewed bytes are the RENDERED objective draft (the feature's resume+render — never raw
14
+ // JSON, never the `plan` param, never the transcript; no draft soft-skips with
15
+ // `reason: "no_objective_draft"`). First-party reviews run VIEW-ONLY (edits are never written
16
+ // back; deny+feedback is the change channel). An APPROVED outcome wires into the approval→save
17
+ // seam: re-read the STRUCTURED artifact → `saveObjective` → D1a gate exit → a TERMINATING
18
+ // result; an unconfirmed save is non-terminating, leaves the gate read-only, and latches
19
+ // automatic saves off for the activation (`draftReview.ts`) — `/objective-save` is the
20
+ // deliberate retry. Every arm opens the current-review slot at entry and runs the decision
21
+ // ladder on a completed verdict before anything is saved.
22
+
23
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
+ import { renderObjectiveDraft, resumeObjectiveDraft } from "../../authoring/objective/draft.ts";
25
+ import {
26
+ completeObjectiveReview,
27
+ type ObjectiveReviewOutcome,
28
+ type ReviewObjectiveDraftResult,
29
+ } from "../../authoring/objective/review.ts";
30
+ import { objectiveApprovalSave } from "../../authoring/objective/save.ts";
31
+ import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
32
+ import type { ToolGating } from "../../substrate/toolGating.ts";
33
+ import {
34
+ checkDraftReviewDecision,
35
+ type DecisionCheck,
36
+ type DraftReviewSlot,
37
+ destinationChangedResult,
38
+ type OpenDraftReview,
39
+ openRefusedResult,
40
+ recordSaveOutcome,
41
+ saveUnconfirmedResult,
42
+ staleApprovalResult,
43
+ supersededReviewResult,
44
+ withDraftChangedNote,
45
+ } from "./draftReview.ts";
46
+ import {
47
+ type ObjectiveApprovalSaveV1Outcome,
48
+ objectiveSaveDepsFor,
49
+ renderObjectiveApprovalSave,
50
+ } from "./objectiveAuthoring.ts";
51
+ import { hasDirectEditsHeading } from "./providers/plannotator.ts";
52
+ import { isPlannotatorPlanSelected } from "./providers/selection.ts";
53
+ import {
54
+ approvedSubjectSaveResult,
55
+ chooseReviewLaunch,
56
+ type DraftReviewBridge,
57
+ type ReviewOutcome,
58
+ type ReviewSubject,
59
+ runFirstPartyReview,
60
+ skipResult,
61
+ subjectReviewOutcomeResult,
62
+ type ToolResult,
63
+ untrustedReviewFeedback,
64
+ verdictsFor,
65
+ type WaveLaunch,
66
+ waveLaunchedResult,
67
+ } from "./review.ts";
68
+
69
+ /** The objective-arm descriptor for the shared renderer cores. */
70
+ export const OBJECTIVE_SUBJECT: ReviewSubject = {
71
+ noun: "objective",
72
+ present: "the complete objective + structured roadmap to the user",
73
+ presentUnavailable: "the complete objective + structured roadmap to the user",
74
+ implementHereWhere: "on the objective path",
75
+ draftTool: "objective_draft",
76
+ failsafeCmd: "/objective-save",
77
+ detailsExtra: { subject: "objective" },
78
+ noSourceError: "no objective draft resolved",
79
+ };
80
+
81
+ export const OBJECTIVE_REVIEW_EDITOR_TITLE =
82
+ "Objective review (view only — edits are not saved) — Enter: continue to verdict · Esc: skip · " +
83
+ "Ctrl+G: $EDITOR";
84
+
85
+ /**
86
+ * Map a non-approved objective review outcome into the model-facing tool result (exported for
87
+ * the offline tests) — the objective-flavored sibling of `reviewOutcomeResult`, delegating to
88
+ * `subjectReviewOutcomeResult` with `OBJECTIVE_SUBJECT`. Every arm carries
89
+ * `details.subject: "objective"`; the texts redirect to `objective_draft` / `/objective-save`.
90
+ * The execute path routes approved outcomes to `approvedObjectiveSaveResult` first, so
91
+ * `completed` renders DENIED here.
92
+ */
93
+ export function objectiveReviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
94
+ return subjectReviewOutcomeResult(OBJECTIVE_SUBJECT, outcome);
95
+ }
96
+
97
+ /**
98
+ * Map an APPROVED objective review outcome + the approval-save outcome into the model-facing
99
+ * tool result (exported for the offline tests) — the objective sibling of `approvedSaveResult`,
100
+ * delegating to `approvedSubjectSaveResult` with `OBJECTIVE_SUBJECT` and no opts (the objective
101
+ * path reviews only the rendered draft, view-only — no `paramMismatch`/`edited`).
102
+ */
103
+ export function approvedObjectiveSaveResult(
104
+ outcome: Extract<ReviewOutcome, { status: "completed" }>,
105
+ save: ObjectiveApprovalSaveV1Outcome,
106
+ ): ToolResult {
107
+ return approvedSubjectSaveResult(
108
+ OBJECTIVE_SUBJECT,
109
+ outcome,
110
+ save.status === "no-draft"
111
+ ? { status: "no-source" }
112
+ : save.status === "refused-draft"
113
+ ? { status: "refused-draft", problem: save.problem }
114
+ : save,
115
+ );
116
+ }
117
+
118
+ /** The no-draft soft skip (byte-stable redirect to objective_draft). */
119
+ function noObjectiveDraftResult(): ToolResult {
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text:
125
+ "no objective draft to review — write the working objective with objective_draft " +
126
+ "(prose + the structured roadmap), then call plan_review again.",
127
+ },
128
+ ],
129
+ details: {
130
+ ok: false,
131
+ error: "no objective draft to review — write it with objective_draft first",
132
+ error_type: "no_objective_draft",
133
+ status: "skipped",
134
+ reason: "no_objective_draft",
135
+ },
136
+ };
137
+ }
138
+
139
+ /**
140
+ * The Direct-Edits revise round (plannotator only, contracts §8.23's objective arm): the save
141
+ * seam re-reads the STRUCTURED artifact, so rendered-markdown edits — roadmap-table rows
142
+ * included — cannot be folded back without model judgment. Nothing was saved, the gate stays
143
+ * read-only; the model folds the diff into `objective_draft`, then re-reviews to confirm.
144
+ */
145
+ function directEditsReviseResult(feedback: string, reviewId: string | undefined): ToolResult {
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text:
151
+ "objective APPROVED with direct browser edits — these cannot be auto-applied to " +
152
+ "the structured draft, so nothing was saved. Fold the Direct Edits diff below into " +
153
+ "the working draft with objective_draft (prose hunks → the prose; roadmap-table " +
154
+ "hunks → the matching node fields), then call plan_review again to confirm.\n\n" +
155
+ `Reviewer feedback:\n${untrustedReviewFeedback(feedback)}`,
156
+ },
157
+ ],
158
+ details: {
159
+ ok: true,
160
+ status: "revise",
161
+ reason: "direct_edits",
162
+ approved: true,
163
+ feedback,
164
+ reviewId,
165
+ subject: "objective",
166
+ },
167
+ };
168
+ }
169
+
170
+ /** Rebuild the door's completed outcome from the feature arm (the shared mappers consume it). */
171
+ function completedOutcome(
172
+ approved: boolean,
173
+ carried: { feedback?: string; reviewId?: string },
174
+ ): Extract<ReviewOutcome, { status: "completed" }> {
175
+ return {
176
+ status: "completed",
177
+ approved,
178
+ ...(carried.feedback !== undefined ? { feedback: carried.feedback } : {}),
179
+ reviewId: carried.reviewId ?? "",
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Translate a review-door outcome (`ReviewOutcome`) into the feature's
185
+ * `ObjectiveReviewOutcome`. Plannotator's vocabulary is translated here: an approval whose
186
+ * feedback OPENS with the `# Direct Edits` heading becomes the `approvedDirectEdits` variant —
187
+ * the heading check suffices (extraction success is irrelevant: the diff goes to the model
188
+ * verbatim either way; the variant requires the feedback, so the edits can never be dropped).
189
+ * The `implement-here` arm is unreachable on the objective path (neither reviewer offers it)
190
+ * and maps defensively to `dismissed`.
191
+ */
192
+ function objectiveOutcomeOf(outcome: ReviewOutcome): ObjectiveReviewOutcome {
193
+ switch (outcome.status) {
194
+ case "completed": {
195
+ const carried = {
196
+ ...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
197
+ reviewId: outcome.reviewId,
198
+ };
199
+ if (outcome.approved) {
200
+ if (outcome.feedback !== undefined && hasDirectEditsHeading(outcome.feedback)) {
201
+ return {
202
+ status: "approvedDirectEdits",
203
+ rawFeedback: outcome.feedback,
204
+ reviewId: outcome.reviewId,
205
+ };
206
+ }
207
+ return { status: "approved", ...carried };
208
+ }
209
+ return { status: "denied", ...carried };
210
+ }
211
+ case "unavailable":
212
+ return { status: "unavailable", warning: outcome.warning };
213
+ case "aborted":
214
+ return { status: "aborted" };
215
+ case "dismissed":
216
+ return { status: "dismissed" };
217
+ case "implement-here":
218
+ return { status: "dismissed" };
219
+ }
220
+ }
221
+
222
+ /**
223
+ * The first-party reviewer: the in-TUI editor review, VIEW-ONLY (3 verdicts). Returns the door
224
+ * vocabulary (`ReviewOutcome`) so both arms share one ladder + completion path.
225
+ */
226
+ async function firstPartyObjectiveReview(
227
+ ctx: ExtensionContext,
228
+ rendered: string,
229
+ signal: AbortSignal | undefined,
230
+ ): Promise<ReviewOutcome> {
231
+ const fp = await runFirstPartyReview({
232
+ ui: ctx.ui,
233
+ plan: rendered,
234
+ writeDraft: () => true, // unreachable under viewOnly — the branch is skipped
235
+ signal,
236
+ editorTitle: OBJECTIVE_REVIEW_EDITOR_TITLE,
237
+ verdicts: verdictsFor(OBJECTIVE_SUBJECT),
238
+ viewOnly: true,
239
+ });
240
+ return fp.outcome;
241
+ }
242
+
243
+ /** Whether a completed bridge outcome's effect is a save (an approval without Direct Edits). */
244
+ export function objectiveEffectOf(
245
+ outcome: Extract<ReviewOutcome, { status: "completed" }>,
246
+ ): "save" | "revision" {
247
+ return outcome.approved &&
248
+ !(outcome.feedback !== undefined && hasDirectEditsHeading(outcome.feedback))
249
+ ? "save"
250
+ : "revision";
251
+ }
252
+
253
+ /**
254
+ * Render a non-`proceed` ladder verdict as the objective arm's tool result (nothing saved; the
255
+ * gate untouched). Shared by the Plannotator arm, the first-party arm and the browser door.
256
+ */
257
+ export function objectiveGuardResult(
258
+ check: Exclude<DecisionCheck, { kind: "proceed" }>,
259
+ review: OpenDraftReview,
260
+ feedback: string | undefined,
261
+ ): ToolResult {
262
+ switch (check.kind) {
263
+ case "superseded":
264
+ return supersededReviewResult("objective");
265
+ case "save-unconfirmed":
266
+ return saveUnconfirmedResult("objective", review.runId, check.detail, feedback);
267
+ case "stale-approval":
268
+ return staleApprovalResult("objective", check.reviewedDigest, feedback);
269
+ case "destination-changed":
270
+ return destinationChangedResult("objective", check.changed, feedback);
271
+ }
272
+ }
273
+
274
+ /** Report a completed objective review's save result into the unconfirmed-save latch. */
275
+ function recordObjectiveSaveOutcome(
276
+ slot: DraftReviewSlot,
277
+ result: ReviewObjectiveDraftResult<ObjectiveApprovalSaveV1Outcome>,
278
+ ): void {
279
+ if (result.status !== "approvedSave") return;
280
+ const save = result.save;
281
+ switch (save.status) {
282
+ case "saved":
283
+ recordSaveOutcome(slot, "objective", { confirmed: true });
284
+ return;
285
+ case "save-failed":
286
+ recordSaveOutcome(slot, "objective", {
287
+ confirmed: false,
288
+ detail: save.result.details.ok ? undefined : save.result.details.error,
289
+ });
290
+ return;
291
+ case "no-draft":
292
+ case "refused-draft":
293
+ // Nothing reached the backend — no attempt to confirm.
294
+ return;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * The objective review arm, mirroring the plan arm's shape: the headless skip, the wave arm's
300
+ * launch chooser, the slot open (both arms — the resume's raw artifact bytes are the reviewed
301
+ * bytes, the rendered markdown what the reviewer saw; ONE validated read serves both), the
302
+ * review, the decision ladder on a completed verdict, then the subject completion with the
303
+ * save reported into the latch.
304
+ */
305
+ export async function executeObjectiveReview(
306
+ pi: ExtensionAPI,
307
+ ctx: ExtensionContext,
308
+ gating: ToolGating,
309
+ bridge: DraftReviewBridge,
310
+ slot: DraftReviewSlot,
311
+ signal?: AbortSignal,
312
+ wave?: WaveLaunch,
313
+ ): Promise<ToolResult> {
314
+ // 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
315
+ if (!ctx.hasUI) return skipResult();
316
+ const sig = signal ?? ctx.signal;
317
+ // 2. ONE validated read serves both the rendering and the reviewed-bytes baseline (the
318
+ // resume carries the exact bytes it decoded), so the baseline can never be newer than
319
+ // what the human saw — a second read could straddle a concurrent objective_draft write
320
+ // and let an approval save bytes nobody reviewed. Raw artifact bytes as the baseline on
321
+ // purpose: the save-authoritative surface catches render-invisible changes.
322
+ const session = openBranchWorkflowSession(pi, ctx);
323
+ // 3. Backend dispatch (mirrors the plan path): plannotator-selected → the bridge; ANY other
324
+ // selection → the first-party editor, view-only.
325
+ const plannotator = isPlannotatorPlanSelected(ctx.cwd);
326
+ if (plannotator && wave?.present()) {
327
+ // The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
328
+ // the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
329
+ // door stale-guards the raw artifact bytes, so an absent draft keeps the plain path
330
+ // (silently: there is no forced mode to warn about). A `refused` resume (raw bytes
331
+ // present but refused) also skips the wave arm — the plain review below renders the
332
+ // refused-draft skip exactly once.
333
+ const resumed = resumeObjectiveDraft(session);
334
+ if (resumed.kind === "valid") {
335
+ const choice = await chooseReviewLaunch(ctx.ui, "Objective", sig);
336
+ if (choice.launch === "aborted") return objectiveReviewOutcomeResult({ status: "aborted" });
337
+ if (choice.launch === "wave") {
338
+ const guidance = await wave.objective(ctx, {
339
+ // The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw
340
+ // JSON; the stale-guard baseline is the SAME read's raw bytes.
341
+ rendered: renderObjectiveDraft(resumed.draft),
342
+ artifactRaw: resumed.raw,
343
+ ...(choice.custom !== undefined ? { custom: choice.custom } : {}),
344
+ });
345
+ // Abort outranks the opener result too: a turn interrupted during the awaited open must
346
+ // never report a successful launch (the door's own bridge abort handling settles the
347
+ // background tasks and clears the primed surfaces).
348
+ if (sig?.aborted) return objectiveReviewOutcomeResult({ status: "aborted" });
349
+ if (guidance !== null) return waveLaunchedResult(OBJECTIVE_SUBJECT, guidance);
350
+ // null = the synchronous port-pick failure (already loudly reported inside the core) —
351
+ // fall open to the plain blocking review in the same call: the review never wedges.
352
+ }
353
+ }
354
+ }
355
+
356
+ if (sig?.aborted) return objectiveReviewOutcomeResult({ status: "aborted" });
357
+ // 4. Resolve the reviewed bytes: a fresh validated resume (the chooser above was a human
358
+ // wait) — its draft for the render, its raw bytes for the guard (never the rendered bytes
359
+ // — the save re-reads the STRUCTURED artifact).
360
+ const resumed = resumeObjectiveDraft(session);
361
+ if (resumed.kind === "absent") return noObjectiveDraftResult();
362
+ if (resumed.kind === "refused")
363
+ return renderObjectiveReviewResult({ status: "refusedDraft", problem: resumed.problem });
364
+ const rendered = renderObjectiveDraft(resumed.draft);
365
+ const opened = slot.open(ctx, {
366
+ subject: "objective",
367
+ source: plannotator ? "artifact" : "editor",
368
+ raw: resumed.raw,
369
+ markdown: rendered,
370
+ });
371
+ if (!opened.ok) return openRefusedResult(opened);
372
+ const review = opened.review;
373
+
374
+ // 5. The review itself, then the ladder, then the completion.
375
+ const outcome = plannotator
376
+ ? await bridge.review(rendered, sig)
377
+ : await firstPartyObjectiveReview(ctx, rendered, sig);
378
+ if (sig?.aborted) return objectiveReviewOutcomeResult({ status: "aborted" });
379
+ if (outcome.status !== "completed") return objectiveReviewOutcomeResult(outcome);
380
+ const check = checkDraftReviewDecision(slot, ctx, review, objectiveEffectOf(outcome));
381
+ if (check.kind !== "proceed") return objectiveGuardResult(check, review, outcome.feedback);
382
+ return withDraftChangedNote(
383
+ await completeObjectiveReviewV1(pi, ctx, gating, slot, outcome),
384
+ check.draftChanged,
385
+ );
386
+ }
387
+
388
+ export function renderObjectiveReviewResult(
389
+ result: ReviewObjectiveDraftResult<ObjectiveApprovalSaveV1Outcome>,
390
+ ): ToolResult {
391
+ switch (result.status) {
392
+ case "noDraft":
393
+ return noObjectiveDraftResult();
394
+ case "refusedDraft":
395
+ // Fail-closed soft skip (the `noObjectiveDraftResult` shape): an invalid artifact is
396
+ // never reviewed — rewrite, then re-review. Gate untouched.
397
+ return {
398
+ content: [
399
+ {
400
+ type: "text",
401
+ text:
402
+ `the working objective draft is invalid: ${result.problem} — rewrite it with ` +
403
+ "objective_draft, then call plan_review again.",
404
+ },
405
+ ],
406
+ details: {
407
+ ok: false,
408
+ error: result.problem,
409
+ error_type: "bad_state",
410
+ status: "skipped",
411
+ reason: "objective_draft_refused",
412
+ },
413
+ };
414
+ case "approvedDirectEdits":
415
+ return directEditsReviseResult(result.rawFeedback, result.reviewId);
416
+ case "approvedSave":
417
+ return approvedObjectiveSaveResult(completedOutcome(true, result), result.save);
418
+ case "denied":
419
+ return objectiveReviewOutcomeResult(completedOutcome(false, result));
420
+ case "dismissed":
421
+ return objectiveReviewOutcomeResult({ status: "dismissed" });
422
+ case "aborted":
423
+ return objectiveReviewOutcomeResult({ status: "aborted" });
424
+ case "unavailable":
425
+ return objectiveReviewOutcomeResult({ status: "unavailable", warning: result.warning });
426
+ }
427
+ }
428
+
429
+ /**
430
+ * The objective completion shared by both tool arms and the browser door: a completed outcome
431
+ * → the feature completion (an approval re-reads the STRUCTURED artifact and saves through the
432
+ * production deps; Direct Edits is the no-save revise round) → the latch record → the rendered
433
+ * tool result. The caller has already run the decision ladder.
434
+ */
435
+ export async function completeObjectiveReviewV1(
436
+ pi: ExtensionAPI,
437
+ ctx: ExtensionContext,
438
+ gating: ToolGating,
439
+ slot: DraftReviewSlot,
440
+ outcome: Extract<ReviewOutcome, { status: "completed" }>,
441
+ ): Promise<ToolResult> {
442
+ const result = await completeObjectiveReview(objectiveOutcomeOf(outcome), async () =>
443
+ renderObjectiveApprovalSave(
444
+ pi,
445
+ ctx,
446
+ await objectiveApprovalSave(objectiveSaveDepsFor(pi, ctx, gating)),
447
+ ),
448
+ );
449
+ recordObjectiveSaveOutcome(slot, result);
450
+ return renderObjectiveReviewResult(result);
451
+ }