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