@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
@@ -0,0 +1,265 @@
1
+ // The stacked-delivery recovery bindings (contracts.md §8.51): the `objective_stack_recover`
2
+ // typed tool and the `/objective-recover` driving command over the Python cold worker
3
+ // `perk objective stack recover` (mutations canonical in Python — recovery classification,
4
+ // roll-forward, and the sweep are cold-plane facts; the warm layer decodes, renders, and
5
+ // delegates). The §8.56 reconcile decision (`decideStackReconcile`) rides every successful
6
+ // envelope — recover's journal-complete re-emission may carry evidence (the death-after-close
7
+ // repair) and must drive exactly like a landing close.
8
+
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { STACK_NO_OBJECTIVE_MESSAGE } from "../../../delivery/stackObjective.ts";
11
+ import { decideStackReconcile } from "../../../delivery/stackReconcile.ts";
12
+ import {
13
+ booleanField,
14
+ type ColdJson,
15
+ numberField,
16
+ objectField,
17
+ objectListField,
18
+ runColdDoor,
19
+ stringField,
20
+ stringListField,
21
+ } from "../../../substrate/coldDoor.ts";
22
+ import { render } from "../../../substrate/prompts.ts";
23
+ import { failFor, ok } from "../../../substrate/result.ts";
24
+ import type { ToolGating } from "../../../substrate/toolGating.ts";
25
+ import { booleanParam, idParam, paramsOf, stringParam } from "../../../substrate/toolParams.ts";
26
+ import { resolveStackObjective } from "../../../substrate/workflowState.ts";
27
+ import { driveStackReconcile, evidenceLines, registerStackDrivingCommand } from "./stackDrive.ts";
28
+ import type { StackResult } from "./stackSync.ts";
29
+
30
+ /** Render the `stack recover --json` envelope (classification rows + sweep) — fully lenient. */
31
+ export function renderRecoverOutcome(payload: ColdJson): string {
32
+ const lines: string[] = [];
33
+ const dryRun = booleanField(payload, "dry_run") === true;
34
+ if (dryRun) lines.push("dry run: nothing was concluded, journaled, or swept");
35
+ const operations = objectListField(payload, "operations");
36
+ if (operations.length === 0) lines.push("no unresolved operations");
37
+ for (const row of operations) {
38
+ lines.push(
39
+ `${stringField(row, "operation_id") ?? "?"} (${stringField(row, "kind") ?? "?"}, ` +
40
+ `prepared ${stringField(row, "prepared_created") ?? "?"}): ` +
41
+ `${stringField(row, "classification") ?? "?"} → ${stringField(row, "action") ?? "?"}`,
42
+ );
43
+ const detail = stringField(row, "detail");
44
+ if (detail !== undefined) lines.push(` ${detail}`);
45
+ // The external-prefix structured preview (dry-run included — what --accept-prefix records).
46
+ for (const merged of objectListField(row, "merged_layers")) {
47
+ const sha = stringField(merged, "merge_commit_sha") ?? "?";
48
+ lines.push(
49
+ ` merged: ${stringField(merged, "node_id") ?? "?"} ` +
50
+ `pr #${numberField(merged, "pr_number") ?? "?"} as ${sha.slice(0, 12)}`,
51
+ );
52
+ }
53
+ for (const rem of objectListField(row, "remainder")) {
54
+ const head = stringField(rem, "head_sha") ?? "?";
55
+ lines.push(
56
+ ` remainder: pr #${numberField(rem, "pr_number") ?? "?"} ` +
57
+ `${stringField(rem, "state") ?? "?"} at ${head.slice(0, 12)}`,
58
+ );
59
+ }
60
+ }
61
+ if (booleanField(payload, "selection_required") === true) {
62
+ lines.push('several operations are unresolved — re-run with operation: "<ULID>" to act on one');
63
+ }
64
+ for (const row of objectListField(payload, "landed_layers")) {
65
+ const finalized = booleanField(row, "finalized");
66
+ const verdict =
67
+ finalized === true
68
+ ? "finalized"
69
+ : finalized === false
70
+ ? "FINALIZE FAILED (see notes)"
71
+ : "would finalize";
72
+ const sha = stringField(row, "merge_commit_sha") ?? "?";
73
+ lines.push(
74
+ `landed ${stringField(row, "node_id") ?? "?"} plan #${stringField(row, "plan_id") ?? "?"} ` +
75
+ `(pr #${numberField(row, "pr_number") ?? "?"}, merged as ${sha.slice(0, 12)}): ${verdict}`,
76
+ );
77
+ }
78
+ if (booleanField(payload, "objective_closed") === true) {
79
+ const id = stringField(objectField(payload, "objective") ?? {}, "id") ?? "?";
80
+ lines.push(`objective #${id} complete — closed`);
81
+ }
82
+ lines.push(...evidenceLines(payload));
83
+ lines.push(...stringListField(payload, "notes").map((note) => `note: ${note}`));
84
+ const sweepSkipped = stringField(payload, "sweep_skipped");
85
+ if (sweepSkipped !== undefined) {
86
+ lines.push(`sweep skipped: ${sweepSkipped}`);
87
+ } else {
88
+ const worktrees = stringListField(payload, "swept_worktrees");
89
+ const refs = stringListField(payload, "swept_refs");
90
+ if (worktrees.length > 0 || refs.length > 0) {
91
+ const verb = dryRun ? "would sweep" : "swept";
92
+ lines.push(
93
+ `${verb} ${worktrees.length} orphaned worktree(s) and ${refs.length} orphaned ref(s)`,
94
+ );
95
+ }
96
+ }
97
+ for (const failure of objectListField(payload, "sweep_failures")) {
98
+ lines.push(
99
+ `sweep failure: ${stringField(failure, "target") ?? "?"} ` +
100
+ `(${stringField(failure, "error") ?? "?"})`,
101
+ );
102
+ }
103
+ return lines.join("\n");
104
+ }
105
+
106
+ /** The seed guidance the warm `/objective-recover` injects (classify → human approval → act). */
107
+ export function objectiveRecoverGuidance(objective: string): string {
108
+ return render("stages/objective-recover.md", { objective });
109
+ }
110
+
111
+ interface RecoverToolParams {
112
+ objective: string | undefined;
113
+ operation: string | undefined;
114
+ dryRun: boolean;
115
+ abandon: boolean;
116
+ acceptPrefix: boolean;
117
+ confirm: boolean;
118
+ }
119
+
120
+ function decodeRecoverParams(params: unknown): RecoverToolParams | null {
121
+ const p = paramsOf(params);
122
+ if (p === null) return null;
123
+ const objective = idParam(p, "objective");
124
+ const operation = stringParam(p, "operation");
125
+ const dryRun = booleanParam(p, "dry_run");
126
+ const abandon = booleanParam(p, "abandon");
127
+ const acceptPrefix = booleanParam(p, "accept_prefix");
128
+ const confirm = booleanParam(p, "confirm");
129
+ if (objective === null || operation === null || dryRun === null || abandon === null) return null;
130
+ if (acceptPrefix === null || confirm === null) return null;
131
+ if (dryRun && (abandon || acceptPrefix)) return null; // the CLI matrix: preview first, then act
132
+ if (abandon && acceptPrefix) return null; // mutually exclusive conclusions
133
+ return {
134
+ objective: objective ?? undefined,
135
+ operation: operation ?? undefined,
136
+ dryRun: dryRun ?? false,
137
+ abandon: abandon ?? false,
138
+ acceptPrefix: acceptPrefix ?? false,
139
+ confirm: confirm ?? false,
140
+ };
141
+ }
142
+
143
+ /** The recover argv: report/dry-run modes pass neither conclusion flag nor `--yes`. */
144
+ export function buildStackRecoverArgs(objective: string, p: RecoverToolParams): string[] {
145
+ const args = ["objective", "stack", "recover", objective];
146
+ if (p.operation !== undefined) args.push("--operation", p.operation);
147
+ if (p.dryRun) args.push("--dry-run");
148
+ if (p.abandon) args.push("--abandon", "--yes");
149
+ if (p.acceptPrefix) args.push("--accept-prefix", "--yes");
150
+ args.push("--json");
151
+ return args;
152
+ }
153
+
154
+ async function stackRecover(
155
+ pi: ExtensionAPI,
156
+ ctx: ExtensionContext,
157
+ p: RecoverToolParams,
158
+ ): Promise<StackResult> {
159
+ const fail = failFor(ctx, "objective-recover", "objective_stack_recover");
160
+ if (p.abandon && !p.confirm) {
161
+ return fail(
162
+ "abandoning an unresolved operation journals its permanent conclusion — preview with " +
163
+ "dry_run: true, then pass confirm: true on explicit human approval.",
164
+ "confirmation_required",
165
+ );
166
+ }
167
+ if (p.acceptPrefix && !p.confirm) {
168
+ return fail(
169
+ "accepting an externally merged prefix journals a permanent degraded-atomicity breach — " +
170
+ "preview with dry_run: true, then pass confirm: true on explicit human approval.",
171
+ "confirmation_required",
172
+ );
173
+ }
174
+ const objective = resolveStackObjective(p.objective, ctx);
175
+ if (objective === null) return fail(STACK_NO_OBJECTIVE_MESSAGE, "no_objective");
176
+ const r = await runColdDoor<ColdJson>(pi, ctx, buildStackRecoverArgs(objective, p), {
177
+ label: "perk objective stack recover",
178
+ decode: (payload) => payload,
179
+ });
180
+ if (!r.ok) return fail(r.message, r.errorType);
181
+ const decision = decideStackReconcile(r.data);
182
+ if (decision.drive) driveStackReconcile(pi, ctx, decision.evidence);
183
+ return ok(renderRecoverOutcome(r.data), { objective });
184
+ }
185
+
186
+ const RECOVER_TOOL_GUIDELINES = [
187
+ "Call objective_stack_recover inside the /objective-recover flow: dry_run: true classifies and reports; the real call concludes deterministically (all-after rolls forward — LAND included) and sweeps orphaned residue.",
188
+ "abandon: true requires confirm: true (explicit human approval) and an all-before classification — never abandon to make a report go away; mixed classifications need human investigation.",
189
+ "accept_prefix: true requires confirm: true and an external_prefix LAND classification — it records the externally merged prefix as a degraded-atomicity breach; then cascade the remainder with objective_stack_sync { base: true } and land it with objective_stack_land.",
190
+ ];
191
+
192
+ /** Install the stacked-delivery recovery bindings: the `objective_stack_recover` typed tool +
193
+ * the `/objective-recover` driving command. */
194
+ export function installStackRecoverBindings(pi: ExtensionAPI, gating: ToolGating): void {
195
+ pi.registerTool({
196
+ name: "objective_stack_recover",
197
+ label: "Objective stack recover",
198
+ description:
199
+ "Conclude an objective's unresolved stack operations (classify against fresh authority; " +
200
+ "roll forward what verified complete — LAND included; abandon with proof under " +
201
+ "abandon+confirm; accept an externally merged LAND prefix as a recorded breach under " +
202
+ "accept_prefix+confirm) and sweep orphaned sync residue. dry_run reports without acting. " +
203
+ "Delegates to the perk cold door.",
204
+ promptSnippet: "Conclude unresolved stack operations + sweep orphaned residue",
205
+ promptGuidelines: RECOVER_TOOL_GUIDELINES,
206
+ executionMode: "sequential",
207
+ parameters: {
208
+ type: "object",
209
+ additionalProperties: false,
210
+ properties: {
211
+ objective: {
212
+ type: ["string", "number"],
213
+ description: "The objective issue id (inferred from the session when omitted).",
214
+ },
215
+ operation: {
216
+ type: "string",
217
+ description: "The target operation ULID (required when several are unresolved).",
218
+ },
219
+ dry_run: {
220
+ type: "boolean",
221
+ description: "Classify and report only — no roll-forward, no abandon, no sweep.",
222
+ },
223
+ abandon: {
224
+ type: "boolean",
225
+ description: "Abandon the target operation (requires an all-before proof + confirm).",
226
+ },
227
+ accept_prefix: {
228
+ type: "boolean",
229
+ description:
230
+ "Accept an externally merged LAND prefix as a recorded degraded-atomicity breach " +
231
+ "(requires an external_prefix classification + confirm).",
232
+ },
233
+ confirm: {
234
+ type: "boolean",
235
+ description: "Explicit human approval (required with abandon or accept_prefix).",
236
+ },
237
+ },
238
+ },
239
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
240
+ const decoded = decodeRecoverParams(params);
241
+ if (decoded === null) {
242
+ return failFor(
243
+ ctx,
244
+ "objective-recover",
245
+ "objective_stack_recover",
246
+ )(
247
+ "objective_stack_recover takes { objective?, operation?, dry_run?, abandon?, " +
248
+ "accept_prefix?, confirm? } — dry_run composes with neither conclusion flag, and " +
249
+ "abandon and accept_prefix are mutually exclusive",
250
+ "bad_input",
251
+ );
252
+ }
253
+ return stackRecover(pi, ctx, decoded);
254
+ },
255
+ });
256
+
257
+ registerStackDrivingCommand(pi, gating, {
258
+ name: "objective-recover",
259
+ description:
260
+ "Drive stack recovery: classify unresolved operations, present the report, conclude via " +
261
+ "the typed recover tool on explicit approval. Pass an objective number (else the active " +
262
+ "objective).",
263
+ guidance: objectiveRecoverGuidance,
264
+ });
265
+ }
@@ -0,0 +1,237 @@
1
+ // The stacked-delivery status read (contracts.md §8.51): the `objective_stack_status` tool +
2
+ // the `/objective-stack` command over the Python cold worker `perk objective stack status`
3
+ // (read-only end to end — the command works in every session, including gate-on; the tool stays
4
+ // deliberately gate-off, documented in toolGating.ts). Pure decoding + rendering + delegation —
5
+ // no feature operation backs this module (zero-policy passthrough); the mutating stack family
6
+ // lives in the sibling adapters (stackSync.ts / stackRecover.ts / stackLand.ts over the
7
+ // `delivery/` feature ops, with the shared drive helpers in stackDrive.ts).
8
+ //
9
+ // Objective inference: explicit param/argument → workflow `active_objective` → plan-ref
10
+ // `objective_id` (`resolveStackObjective`); the warm layer always passes the resolved objective
11
+ // explicitly to the cold door. Cold-envelope decodes are lenient/render-only — the fully-lenient
12
+ // identity decode keeps the decode-rejection arm unreachable (`bad_output` remains reachable
13
+ // only via unparseable/non-object stdout, owned by the cold-door seam).
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import {
17
+ parseStackObjectiveArg,
18
+ STACK_NO_OBJECTIVE_MESSAGE,
19
+ } from "../../../delivery/stackObjective.ts";
20
+ import {
21
+ booleanField,
22
+ type ColdJson,
23
+ numberField,
24
+ objectField,
25
+ objectListField,
26
+ runColdDoor,
27
+ stringField,
28
+ stringListField,
29
+ } from "../../../substrate/coldDoor.ts";
30
+ import { registerPerkCommand } from "../../../substrate/command.ts";
31
+ import { failFor, ok, type Result } from "../../../substrate/result.ts";
32
+ import { idParam, paramsOf } from "../../../substrate/toolParams.ts";
33
+ import { resolveStackObjective } from "../../../substrate/workflowState.ts";
34
+ import { report } from "../../../surfaces/report.ts";
35
+
36
+ /** The lenient finding-row render shared with the landing readiness preview
37
+ * (stackLand.ts imports it — the ONE copy; the door-era duplicate died with the door). */
38
+ export function findingLines(train: ColdJson, key: string): string[] {
39
+ const rows = objectListField(train, key);
40
+ if (rows.length === 0) return [];
41
+ return [
42
+ `${key}:`,
43
+ ...rows.map((f) => ` - [${stringField(f, "code") ?? "?"}] ${stringField(f, "message") ?? ""}`),
44
+ ];
45
+ }
46
+
47
+ /** Render the `stack status --json` envelope (train + operations + continuation + residue) —
48
+ * fully lenient: a missing/mistyped field degrades that line, never the render. */
49
+ export function renderStackStatus(payload: ColdJson): string {
50
+ const lines: string[] = [];
51
+ const id = stringField(objectField(payload, "objective") ?? {}, "id") ?? "?";
52
+ const noTrain = stringField(payload, "no_train");
53
+ if (noTrain !== undefined) lines.push(`Objective #${id}: ${noTrain}`);
54
+ const train = objectField(payload, "train");
55
+ if (train !== undefined) {
56
+ const layers = objectListField(train, "layers");
57
+ const landedLen = numberField(train, "landed_prefix_len") ?? 0;
58
+ const landedNote = landedLen > 0 ? `, landed ${landedLen}` : "";
59
+ lines.push(
60
+ `Objective #${id}: stacked delivery train (base ${stringField(train, "base") ?? "?"}, ` +
61
+ `published prefix ${numberField(train, "published_prefix_len") ?? "?"}/${layers.length}` +
62
+ `${landedNote})`,
63
+ );
64
+ layers.forEach((layer, index) => {
65
+ const parts = [stringField(layer, "node_id") ?? "?"];
66
+ parts.push(stringField(layer, "branch") ?? "no branch");
67
+ const pr = numberField(layer, "pr_number");
68
+ if (pr !== undefined) parts.push(`pr #${pr}`);
69
+ parts.push(`[${stringField(layer, "publication") ?? "?"}]`);
70
+ const handoff = stringField(layer, "handoff");
71
+ if (handoff !== undefined && handoff !== "not_applicable") parts.push(`handoff ${handoff}`);
72
+ lines.push(` ${index + 1}. ${parts.join(" ")}`);
73
+ });
74
+ const readiness = objectField(train, "next_build_ready");
75
+ if (readiness !== undefined) {
76
+ if (booleanField(readiness, "ready") === true) {
77
+ lines.push(` next build-ready: ${stringField(readiness, "node_id") ?? "?"}`);
78
+ } else {
79
+ lines.push(` build blocked: ${stringField(readiness, "reason") ?? "?"}`);
80
+ }
81
+ }
82
+ // The additive planning_gate block (contracts §8.46): render the handoff rows from their
83
+ // pinned fields only — leniently (missing/mistyped fields degrade, never reject); the
84
+ // technical rows already ride the build-blocked line/findings.
85
+ const gate = objectField(train, "planning_gate");
86
+ if (gate !== undefined && booleanField(gate, "ready") !== true) {
87
+ const gatedNode = stringField(gate, "node_id") ?? "?";
88
+ for (const row of objectListField(gate, "blockers")) {
89
+ if (stringField(row, "kind") !== "handoff") continue;
90
+ const state = stringField(row, "handoff_state") ?? "?";
91
+ let detail =
92
+ `${stringField(row, "dependency_node_id") ?? "?"} ` +
93
+ `(plan #${stringField(row, "plan") ?? "?"}, PR #${numberField(row, "pr") ?? "?"}) — ` +
94
+ state;
95
+ const stamped = stringField(row, "stamped_head");
96
+ const current = stringField(row, "current_head");
97
+ if (state === "stale" && stamped !== undefined && current !== undefined) {
98
+ detail += `; stamped ${stamped.slice(0, 12)} ≠ head ${current.slice(0, 12)}`;
99
+ }
100
+ const remediation = stringField(row, "remediation") ?? "?";
101
+ lines.push(
102
+ ` planning gated: ${gatedNode} waits on ${detail}; record the handoff: ${remediation}`,
103
+ );
104
+ }
105
+ }
106
+ lines.push(...findingLines(train, "blockers"));
107
+ lines.push(...findingLines(train, "information"));
108
+ }
109
+ for (const op of objectListField(payload, "operations")) {
110
+ lines.push(
111
+ `unresolved operation: ${stringField(op, "operation_id") ?? "?"} ` +
112
+ `(${stringField(op, "kind") ?? "?"}, prepared ${stringField(op, "prepared_created") ?? "?"})`,
113
+ );
114
+ }
115
+ const continuation = objectField(payload, "continuation");
116
+ if (continuation !== undefined) {
117
+ if (booleanField(continuation, "parseable") === true) {
118
+ lines.push(
119
+ `pending continuation: operation ${stringField(continuation, "operation_id") ?? "?"} ` +
120
+ `stopped on node ${stringField(continuation, "conflict_node_id") ?? "?"} ` +
121
+ `(worktree ${stringField(continuation, "worktree_path") ?? "?"})`,
122
+ );
123
+ } else {
124
+ lines.push(
125
+ `pending continuation: UNPARSEABLE manifest at ${
126
+ stringField(continuation, "manifest_path") ?? "?"
127
+ }`,
128
+ );
129
+ }
130
+ if (booleanField(continuation, "parseable") === true) {
131
+ lines.push(
132
+ " resume via objective_stack_sync { continue: true }, discard via { abort: true }, or " +
133
+ "dispatch automated resolution via { resolve: true } (on explicit human request)",
134
+ );
135
+ } else {
136
+ lines.push(
137
+ " resume via objective_stack_sync { continue: true }, or discard via { abort: true }",
138
+ );
139
+ }
140
+ }
141
+ const orphans = objectField(payload, "orphaned_residue");
142
+ if (orphans !== undefined) {
143
+ const worktrees = stringListField(orphans, "worktrees");
144
+ const refs = stringListField(orphans, "refs");
145
+ if (booleanField(orphans, "observed") === false) {
146
+ lines.push(`orphaned residue: not observed — ${stringField(orphans, "reason") ?? "?"}`);
147
+ } else if (worktrees.length > 0 || refs.length > 0) {
148
+ lines.push(
149
+ `orphaned residue: ${worktrees.length} worktree(s), ${refs.length} ref(s) — ` +
150
+ "sweep via objective_stack_recover",
151
+ );
152
+ }
153
+ }
154
+ return lines.length > 0 ? lines.join("\n") : `Objective #${id}: empty status report`;
155
+ }
156
+
157
+ /** The one status cold-door read both surfaces share (label keeps every fallback/failure text
158
+ * byte-compatible; the identity decode keeps the envelope render-only). */
159
+ function readStackStatus(pi: ExtensionAPI, ctx: ExtensionContext, objective: string) {
160
+ return runColdDoor<ColdJson>(pi, ctx, ["objective", "stack", "status", objective, "--json"], {
161
+ label: "perk objective stack status",
162
+ decode: (payload) => payload,
163
+ });
164
+ }
165
+
166
+ /** The tool implementation: resolve, delegate, render, never throw. */
167
+ async function stackStatus(
168
+ pi: ExtensionAPI,
169
+ ctx: ExtensionContext,
170
+ objectiveParam: string | undefined,
171
+ ): Promise<Result<{ objective: string }>> {
172
+ const fail = failFor(ctx, "objective-stack", "objective_stack_status");
173
+ const objective = resolveStackObjective(objectiveParam, ctx);
174
+ if (objective === null) return fail(STACK_NO_OBJECTIVE_MESSAGE, "no_objective");
175
+ const r = await readStackStatus(pi, ctx, objective);
176
+ if (!r.ok) return fail(r.message, r.errorType);
177
+ return ok(renderStackStatus(r.data), { objective });
178
+ }
179
+
180
+ /** Install the stacked-delivery status read: the `objective_stack_status` tool (strict
181
+ * tri-state param decode, non-terminating) + the `/objective-stack` command. */
182
+ export function installStackStatusBindings(pi: ExtensionAPI): void {
183
+ pi.registerTool({
184
+ name: "objective_stack_status",
185
+ label: "Objective stack status",
186
+ description:
187
+ "Report an objective's stacked delivery train: layers, publication states, build " +
188
+ "readiness, unresolved operations, pending continuation, and orphaned sync residue. " +
189
+ "Read-only (delegates to the perk cold door).",
190
+ promptSnippet: "Report the objective's stacked delivery train (read-only)",
191
+ promptGuidelines: [
192
+ "objective_stack_status is read-only — call it freely to inspect the delivery train, unresolved operations, pending continuations, and orphaned residue (objective inferred when omitted).",
193
+ ],
194
+ executionMode: "sequential",
195
+ parameters: {
196
+ type: "object",
197
+ additionalProperties: false,
198
+ properties: {
199
+ objective: {
200
+ type: ["string", "number"],
201
+ description: "The objective issue id (inferred from the session when omitted).",
202
+ },
203
+ },
204
+ },
205
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
206
+ const p = paramsOf(params);
207
+ const objective = p === null ? null : idParam(p, "objective");
208
+ if (p === null || objective === null) {
209
+ return failFor(
210
+ ctx,
211
+ "objective-stack",
212
+ "objective_stack_status",
213
+ )("objective_stack_status takes { objective?: <id> }", "bad_input");
214
+ }
215
+ return stackStatus(pi, ctx, objective);
216
+ },
217
+ });
218
+
219
+ registerPerkCommand(pi, "objective-stack", {
220
+ description:
221
+ "Show an objective's stacked delivery train (status, operations, continuation, residue). " +
222
+ "Pass an objective number (else the active objective, else the plan-ref's).",
223
+ handler: async (args, ctx) => {
224
+ const objective = resolveStackObjective(parseStackObjectiveArg(args ?? "") ?? undefined, ctx);
225
+ if (objective === null) {
226
+ report(ctx, "objective-stack", "warning", STACK_NO_OBJECTIVE_MESSAGE);
227
+ return;
228
+ }
229
+ const r = await readStackStatus(pi, ctx, objective);
230
+ if (!r.ok) {
231
+ report(ctx, "objective-stack", "error", r.message, { alsoLog: true });
232
+ return;
233
+ }
234
+ report(ctx, "objective-stack", "info", renderStackStatus(r.data));
235
+ },
236
+ });
237
+ }