@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,373 +0,0 @@
1
- // The warm `/submit` door. The in-session twin of the Python cold door
2
- // (`perk pr submit`): a deterministic, terminating tool + command that DELEGATE the GitHub write —
3
- // they do NOT reimplement it (GitHub mutations are canonical in the Python gateway). Mirrors
4
- // `planSave.ts`: write nothing, delegate to `perk pr submit --json` via `pi.exec`, surface the
5
- // structured result, never throw (failures are loud-but-non-fatal via `details.ok = false`).
6
- //
7
- // Mergeability gate: `perk pr submit` probes the PR's mergeability against the base branch
8
- // (a deterministic local `git merge-tree` probe). When it reports a definitively-unmergeable PR
9
- // (`mergeable === false` + conflicts), the warm door drives a fresh-context, write-capable
10
- // `perk.conflict-resolver` subagent (mirroring `/pr-review`'s dispatch and `/land`'s reconcile
11
- // drive) to rebase + resolve + push, then asks the model to re-`/submit` to confirm. Bounded by
12
- // `CONFLICT_RESOLUTION_ATTEMPT_CAP` via the `conflict_resolution_attempts` workflow-state field.
13
-
14
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
16
- import {
17
- booleanField,
18
- type ColdJson,
19
- numberField,
20
- objectField,
21
- runColdDoor,
22
- 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, type OkDetails, ok, type Result } from "../substrate/result.ts";
28
- import { captureSessionPointer } from "../substrate/sessionPointers.ts";
29
- import { appendWorkflowState, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
30
- import { report } from "../surfaces/report.ts";
31
- import { planningStageRefusal } from "./lifecycleGates.ts";
32
-
33
- /**
34
- * The bounded conflict-resolution re-drive cap: drive the resolver at most this many times.
35
- * The counter behind it (`conflict_resolution_attempts`) is SHARED with `/objective-sync`'s
36
- * retained-continuation conflict drive (objectiveStack.ts) — submit- and sync-episode attempts
37
- * deliberately share one bound, reset on any clean completion of either surface.
38
- */
39
- export const CONFLICT_RESOLUTION_ATTEMPT_CAP = 2;
40
-
41
- /** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
42
- export interface SubmitOk {
43
- pr: { number: number; url: string; is_draft: boolean; existed: boolean };
44
- branch?: string;
45
- issue?: number;
46
- plan_embedded?: boolean;
47
- /** The target branch the PR merges into (the conflict-resolver rebases onto it). */
48
- base?: string;
49
- /** Tri-state mergeability from the Python `git merge-tree` probe: true/false/null/absent. */
50
- mergeable?: boolean | null;
51
- /** The conflicted paths when `mergeable === false`; `[]` otherwise (advisory). */
52
- conflicts?: string[];
53
- /** `"stacked"` when the submit routed through the delivery publish operation (§8.47). */
54
- delivery?: string;
55
- /** The native-stack facts of a stacked submit (absent for the bottom layer). */
56
- stack?: { number: number; size: number; position: number };
57
- /** Lenient summary of an automatic suffix synchronization. */
58
- operation?: {
59
- kind: string;
60
- operation_id: string | null;
61
- no_op: boolean;
62
- affected_count: number;
63
- notes: string[];
64
- };
65
- }
66
-
67
- export type SubmitResult = Result<SubmitOk>;
68
- export type SubmitDetails = SubmitResult["details"];
69
-
70
- /**
71
- * A tri-state read of the advisory `mergeable` field: `true`/`false`/`null` pass through;
72
- * anything else (absent, mistyped) → `undefined`. Kept lenient so a malformed value never sinks
73
- * an otherwise-successful submit decode.
74
- */
75
- function mergeableField(payload: ColdJson): boolean | null | undefined {
76
- const value = payload.mergeable;
77
- if (value === true || value === false || value === null) return value;
78
- return undefined;
79
- }
80
-
81
- /** A lenient string-array read for the advisory `conflicts` field; malformed → `[]`. */
82
- function conflictsField(payload: ColdJson): string[] {
83
- const value = payload.conflicts;
84
- if (Array.isArray(value) && value.every((p) => typeof p === "string")) return value as string[];
85
- return [];
86
- }
87
-
88
- /**
89
- * A lenient read of the advisory stacked `stack` facts: all three numbers or nothing — a
90
- * malformed value must NOT sink a successful submit decode (it just drops the suffix).
91
- */
92
- function stackField(payload: ColdJson): SubmitOk["stack"] {
93
- const value = objectField(payload, "stack");
94
- if (value === undefined) return undefined;
95
- const number = numberField(value, "number");
96
- const size = numberField(value, "size");
97
- const position = numberField(value, "position");
98
- if (number === undefined || size === undefined || position === undefined) return undefined;
99
- return { number, size, position };
100
- }
101
-
102
- /**
103
- * Lenient all-or-nothing decode of the cascade operation block. The full affected rows remain a
104
- * Python/CLI detail; the warm door needs only their count plus the recovery notes.
105
- */
106
- function operationField(payload: ColdJson): SubmitOk["operation"] {
107
- const value = objectField(payload, "operation");
108
- if (value === undefined) return undefined;
109
- const kind = stringField(value, "kind");
110
- const operationId = value.operation_id;
111
- const noOp = booleanField(value, "no_op");
112
- const affected = value.affected;
113
- const notes = value.notes;
114
- if (
115
- kind === undefined ||
116
- (typeof operationId !== "string" && operationId !== null) ||
117
- noOp === undefined ||
118
- !Array.isArray(affected) ||
119
- !Array.isArray(notes) ||
120
- !notes.every((note) => typeof note === "string")
121
- ) {
122
- return undefined;
123
- }
124
- return {
125
- kind,
126
- operation_id: operationId,
127
- no_op: noOp,
128
- affected_count: affected.length,
129
- notes: notes as string[],
130
- };
131
- }
132
-
133
- /**
134
- * Narrow the `perk pr submit --json` success payload; strict on `pr`, lenient on the rest. The
135
- * `base`/`mergeable`/`conflicts` mergeability fields are advisory (mirror land.ts's lenient
136
- * sub-fields): a malformed value must NOT make a successful submit decode to `null`.
137
- */
138
- function decodeSubmit(payload: ColdJson): SubmitOk | null {
139
- const pr = objectField(payload, "pr");
140
- if (pr === undefined) return null;
141
- const number = numberField(pr, "number");
142
- const url = stringField(pr, "url");
143
- const isDraft = booleanField(pr, "is_draft");
144
- const existed = booleanField(pr, "existed");
145
- if (number === undefined || url === undefined || isDraft === undefined || existed === undefined) {
146
- return null;
147
- }
148
- return {
149
- pr: { number, url, is_draft: isDraft, existed },
150
- branch: stringField(payload, "branch"),
151
- issue: numberField(payload, "issue"),
152
- plan_embedded: booleanField(payload, "plan_embedded"),
153
- base: stringField(payload, "base"),
154
- mergeable: mergeableField(payload),
155
- conflicts: conflictsField(payload),
156
- delivery: stringField(payload, "delivery"),
157
- stack: stackField(payload),
158
- operation: operationField(payload),
159
- };
160
- }
161
-
162
- /** A definitively-unmergeable submit; parsed conflict paths are advisory only. */
163
- function isUnmergeable(details: SubmitDetails): details is OkDetails<SubmitOk> {
164
- return details.ok && details.mergeable === false;
165
- }
166
-
167
- /**
168
- * The single submit implementation both surfaces call. Delegates to the Python cold door; returns a
169
- * soft result (never throws) — failures set `details.ok = false`. On a clean submit
170
- * (`mergeable !== false`) it resets the bounded conflict-resolution counter so a later independent
171
- * conflict starts fresh; on conflicts the success message reflects the in-flight resolution.
172
- */
173
- export async function submitPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<SubmitResult> {
174
- const fail = failFor(ctx, "submit");
175
-
176
- // Planning sessions never legitimately submit — the first check, before any cold-door
177
- // delegation (a positioned stacked planning session's cwd binding is the PREDECESSOR).
178
- const planningRefusal = planningStageRefusal(ctx, "submit");
179
- if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
180
-
181
- // Stamp this implement run id into the plan-header `impl_run_ids` linkage (contracts.md
182
- // §8.35) so a later/other session can resolve the implement session pointers cross-run.
183
- // Mirrors planSave's `--run-id` thread; covers the interactive /submit AND the headless worker
184
- // drive (both flow through this warm door). Absent run_id ⇒ omit (bare-stamp untouched).
185
- const runId = rebuildWorkflowState(branchOf(ctx)).run_id ?? "";
186
- const args = ["pr", "submit", "--json"];
187
- if (runId) args.push("--run-id", runId);
188
- const r = await runColdDoor<SubmitOk>(pi, ctx, args, {
189
- label: "perk pr submit",
190
- decode: decodeSubmit,
191
- });
192
- if (!r.ok) return fail(r.message, r.errorType);
193
-
194
- // Capture `implementation/main` at the moment the run id enters `impl_run_ids` (contracts.md
195
- // §8.35): any run id stamped into the linkage gets its pointer captured in the same gesture.
196
- // This covers address/warm sessions that submit — which the stage-gated `session_start` capture
197
- // never sees — so a submitted run resolves `found` instead of `missing`. For the implement
198
- // session's own /submit it is an idempotent same-session refresh; `preserveForeign` guarantees
199
- // it can never clobber a different session's pointer. Best-effort + non-fatal like every
200
- // capture site (a successful submit must stand).
201
- if (runId) {
202
- captureSessionPointer({
203
- cwd: ctx.cwd,
204
- runId,
205
- klass: "implementation",
206
- site: "main",
207
- // Optional-chained: best-effort, and some side-session fakes have no getSessionFile.
208
- sessionFile: ctx.sessionManager.getSessionFile?.(),
209
- preserveForeign: true,
210
- });
211
- }
212
-
213
- const verb = r.data.pr.existed ? "Found existing" : "Opened draft";
214
- const conflicted = r.data.mergeable === false;
215
- // Reset the counter on every clean (or undetermined) submit — idempotent; keeps a later
216
- // independent conflict bounded fresh.
217
- if (r.data.mergeable !== false) resetConflictAttempts(pi, ctx, "submit");
218
- // Automatic-cascade facts supersede the generic stacked suffix. A malformed operation block was
219
- // dropped by the lenient decoder, so it falls back to the pre-existing stack wording.
220
- const deliverySuffix =
221
- r.data.operation?.kind === "sync"
222
- ? r.data.operation.no_op
223
- ? " (suffix already in sync)"
224
- : ` (cascaded ${r.data.operation.affected_count} layer(s))`
225
- : r.data.stack
226
- ? ` (stack #${r.data.stack.number}, layer ${r.data.stack.position}/${r.data.stack.size})`
227
- : r.data.delivery === "stacked"
228
- ? " (stacked layer)"
229
- : "";
230
- for (const note of r.data.operation?.notes ?? []) {
231
- report(ctx, "submit", "warning", note);
232
- }
233
- const message = conflicted
234
- ? `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} — merge conflicts detected; resolving${deliverySuffix}`
235
- : `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} (${
236
- r.data.plan_embedded ? "plan embedded" : "no plan embed"
237
- })${deliverySuffix}`;
238
- return ok(message, r.data, { terminate: true });
239
- }
240
-
241
- /**
242
- * Reset `conflict_resolution_attempts` to 0 (idempotent: a no-op when already 0/absent). The
243
- * counter is shared across the two warm conflict drives, so `scope` names the TRUE resetting
244
- * surface for failure reports — `/submit` passes "submit", the stack door "objective-sync".
245
- */
246
- export function resetConflictAttempts(
247
- pi: ExtensionAPI,
248
- ctx: ExtensionContext,
249
- scope: string,
250
- ): void {
251
- const attempts = rebuildWorkflowState(branchOf(ctx)).conflict_resolution_attempts ?? 0;
252
- if (attempts === 0) return;
253
- appendWorkflowState(pi, ctx, {
254
- data: { conflict_resolution_attempts: 0 },
255
- field: "conflict_resolution_attempts",
256
- expected: 0,
257
- scope,
258
- failure: "conflict_resolution_attempts reset read-back failed (expected 0)",
259
- });
260
- }
261
-
262
- /**
263
- * The follow-up guidance the warm `/submit` injects to dispatch the conflict-resolver (modeled on
264
- * `prReviewGuidance`). Pure + exported for offline tests. `worktree` is the plan worktree the
265
- * child's task text pins with a concrete `cd <worktree>` command — a dispatched child otherwise
266
- * has no cwd guarantee and can run its commands outside the plan worktree. When `model` is set,
267
- * the ONE workflowScript call carries a workflow-level `model` default; otherwise the agent's
268
- * default model is used.
269
- */
270
- export function conflictResolutionGuidance(
271
- base: string,
272
- attempt: number,
273
- cap: number,
274
- worktree: string,
275
- model?: string,
276
- ): string {
277
- return render("stages/conflict-resolution.md", {
278
- base,
279
- attempt: String(attempt),
280
- cap: String(cap),
281
- worktree,
282
- model: model ?? "",
283
- });
284
- }
285
-
286
- /**
287
- * After a submit that opened a PR with detected merge conflicts, drive the session into the
288
- * conflict-resolution pass by injecting `conflictResolutionGuidance` (warm-door driving pattern,
289
- * modeled on `driveReconcileAfterLand`). The terminating `submit` tool stays terminating — a
290
- * `followUp` user message is a separate deliberate new turn. Short-circuits (sends nothing) unless
291
- * the submit succeeded with a definitively-unmergeable PR. Bounded by
292
- * `CONFLICT_RESOLUTION_ATTEMPT_CAP` via the `conflict_resolution_attempts` field: past the cap it
293
- * surfaces the unresolved conflict loudly instead of looping. The counter is shared with
294
- * `/objective-sync`'s retained-continuation conflict drive.
295
- */
296
- export function driveConflictResolution(
297
- pi: ExtensionAPI,
298
- ctx: ExtensionContext,
299
- details: SubmitDetails,
300
- ): void {
301
- if (!isUnmergeable(details)) return;
302
- const base = details.base ?? "";
303
- const attempts = rebuildWorkflowState(branchOf(ctx)).conflict_resolution_attempts ?? 0;
304
- if (attempts >= CONFLICT_RESOLUTION_ATTEMPT_CAP) {
305
- report(
306
- ctx,
307
- "submit",
308
- "error",
309
- `merge conflicts persist after ${attempts} resolution attempt(s) — resolve manually ` +
310
- `(rebase onto \`${base}\` and push), then re-run /submit.`,
311
- { alsoLog: true },
312
- );
313
- return;
314
- }
315
- const next = attempts + 1;
316
- appendWorkflowState(pi, ctx, {
317
- data: { conflict_resolution_attempts: next },
318
- field: "conflict_resolution_attempts",
319
- expected: next,
320
- scope: "submit",
321
- failure: `conflict_resolution_attempts read-back failed (expected ${next})`,
322
- });
323
- const model = subagentModel(ctx.cwd, "conflict-resolver");
324
- const message =
325
- // `/submit` runs only in worktree-bound sessions (planning sessions are refused first), so
326
- // the session cwd IS the plan worktree.
327
- conflictResolutionGuidance(base, next, CONFLICT_RESOLUTION_ATTEMPT_CAP, ctx.cwd, model) +
328
- bindingSuffix(ctx.cwd, "command:submit");
329
- if (ctx.isIdle()) {
330
- // The `/submit` command path (idle): inject an immediate turn.
331
- pi.sendUserMessage(message);
332
- } else {
333
- // The `submit` tool path (streaming): deliver after the terminating submit batch.
334
- pi.sendUserMessage(message, { deliverAs: "followUp" });
335
- }
336
- }
337
-
338
- const TOOL_GUIDELINES = [
339
- "Call submit only after the implementation is committed in this worktree; it pushes the branch and opens the draft PR, then ends the turn.",
340
- "submit operates on the active plan's worktree — it takes no arguments; the branch and plan come from the local plan-ref.",
341
- ];
342
-
343
- /** Register the warm door: the `submit` terminating tool (canonical) + the `/submit` command twin. */
344
- export function registerSubmit(pi: ExtensionAPI): void {
345
- pi.registerTool({
346
- name: "submit",
347
- label: "Submit PR",
348
- description:
349
- "Push the current plan's branch and open a draft pull request linking the plan. " +
350
- "Terminating: ends the turn on submit. Call only after the implementation is committed.",
351
- promptSnippet: "Open the draft PR for the committed implementation (terminates the turn)",
352
- promptGuidelines: TOOL_GUIDELINES,
353
- executionMode: "sequential",
354
- parameters: { type: "object", additionalProperties: false, properties: {} },
355
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
356
- const result = await submitPr(pi, ctx);
357
- driveConflictResolution(pi, ctx, result.details);
358
- return result;
359
- },
360
- });
361
-
362
- registerPerkCommand(pi, "submit", {
363
- description: "Push the branch and open a draft PR for the active plan (implement → submit).",
364
- handler: async (_args, ctx) => {
365
- const result = await submitPr(pi, ctx);
366
- // Failure already reported loudly via failFor (the single error surface) — success only.
367
- if (result.details.ok) {
368
- report(ctx, "submit", "info", result.content[0]?.text ?? "submit done");
369
- }
370
- driveConflictResolution(pi, ctx, result.details);
371
- },
372
- });
373
- }