@mgiles/perk 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +5 -0
  2. package/extension/authoring/gist/draft.ts +198 -0
  3. package/extension/authoring/gist/prose.ts +46 -0
  4. package/extension/authoring/gist/review.ts +133 -0
  5. package/extension/authoring/gist/save.ts +118 -0
  6. package/extension/authoring/objective/draft.ts +345 -0
  7. package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
  8. package/extension/authoring/objective/planning.ts +124 -0
  9. package/extension/authoring/objective/prose.ts +103 -0
  10. package/extension/authoring/objective/review.ts +128 -0
  11. package/extension/authoring/objective/save.ts +224 -0
  12. package/extension/authoring/plan/draft.ts +84 -0
  13. package/extension/authoring/plan/prose.ts +41 -0
  14. package/extension/authoring/plan/review.ts +269 -0
  15. package/extension/authoring/plan/save.ts +256 -0
  16. package/extension/authoring/plan/source.ts +82 -0
  17. package/extension/authoring/refinement/context.ts +468 -0
  18. package/extension/authoring/refinement/draft.ts +261 -0
  19. package/extension/authoring/refinement/prose.ts +79 -0
  20. package/extension/authoring/refinement/review.ts +111 -0
  21. package/extension/authoring/refinement/save.ts +119 -0
  22. package/extension/authoring/review/approvalGate.ts +34 -0
  23. package/extension/authoring/review/draftContext.ts +68 -0
  24. package/extension/codeReview/automated.ts +352 -0
  25. package/extension/codeReview/submission.ts +229 -0
  26. package/extension/delivery/address.ts +295 -0
  27. package/extension/delivery/ci.ts +355 -0
  28. package/extension/delivery/commitCompact.ts +93 -0
  29. package/extension/delivery/conflictResolution.ts +247 -0
  30. package/extension/delivery/ready.ts +193 -0
  31. package/extension/delivery/stackConflict.ts +361 -0
  32. package/extension/delivery/stackObjective.ts +16 -0
  33. package/extension/delivery/stackReconcile.ts +165 -0
  34. package/extension/delivery/submit.ts +171 -0
  35. package/extension/index.ts +365 -380
  36. package/extension/learning/analystWave.ts +324 -0
  37. package/extension/learning/audit.ts +667 -0
  38. package/extension/learning/capture.ts +92 -0
  39. package/extension/learning/containment.ts +104 -0
  40. package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
  41. package/extension/learning/dreamAnalysis.ts +435 -0
  42. package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
  43. package/extension/{waves → learning}/dreamReport.ts +35 -31
  44. package/extension/learning/harvest.ts +491 -0
  45. package/extension/learning/prose.ts +66 -0
  46. package/extension/learning/routing.ts +79 -0
  47. package/extension/pi/v1/bashScanTimeout.ts +64 -0
  48. package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
  52. package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
  53. package/extension/pi/v1/codeReview/submit.ts +354 -0
  54. package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
  55. package/extension/pi/v1/contextEvidence.ts +80 -0
  56. package/extension/pi/v1/contextInjection.ts +207 -0
  57. package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
  58. package/extension/pi/v1/delivery/ci.ts +570 -0
  59. package/extension/pi/v1/delivery/commitCompact.ts +201 -0
  60. package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
  61. package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
  62. package/extension/pi/v1/delivery/ready.ts +322 -0
  63. package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
  64. package/extension/pi/v1/delivery/stackDrive.ts +120 -0
  65. package/extension/pi/v1/delivery/stackLand.ts +223 -0
  66. package/extension/pi/v1/delivery/stackRecover.ts +265 -0
  67. package/extension/pi/v1/delivery/stackStatus.ts +237 -0
  68. package/extension/pi/v1/delivery/stackSync.ts +658 -0
  69. package/extension/pi/v1/delivery/submit.ts +389 -0
  70. package/extension/pi/v1/delivery/submitConflict.ts +186 -0
  71. package/extension/pi/v1/draftReview.ts +431 -0
  72. package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
  73. package/extension/pi/v1/gist.ts +794 -0
  74. package/extension/pi/v1/learning/audit.ts +186 -0
  75. package/extension/pi/v1/learning/dream.ts +207 -0
  76. package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
  77. package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
  78. package/extension/pi/v1/learning/learn.ts +585 -0
  79. package/extension/pi/v1/lifecycleGates.ts +127 -0
  80. package/extension/{factories → pi/v1}/objective.ts +53 -33
  81. package/extension/pi/v1/objectiveAuthoring.ts +672 -0
  82. package/extension/pi/v1/objectiveDreamGate.ts +160 -0
  83. package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
  84. package/extension/pi/v1/objectiveRefinement.ts +1320 -0
  85. package/extension/pi/v1/objectiveReview.ts +451 -0
  86. package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
  87. package/extension/pi/v1/plan.ts +812 -0
  88. package/extension/pi/v1/planReview.ts +820 -0
  89. package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
  90. package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
  91. package/extension/pi/v1/providers/plannotator.ts +487 -0
  92. package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
  93. package/extension/pi/v1/providers/selection.ts +43 -0
  94. package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
  95. package/extension/pi/v1/review.ts +538 -0
  96. package/extension/pi/v1/reviewOutcome.ts +9 -0
  97. package/extension/pi/v1/scoutWave.ts +318 -0
  98. package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
  99. package/extension/session/branchWorkflowSession.ts +60 -0
  100. package/extension/session/lifecycle.ts +644 -0
  101. package/extension/session/lifecycleGates.ts +64 -0
  102. package/extension/session/saveDestination.ts +87 -0
  103. package/extension/session/workflowSession.ts +971 -0
  104. package/extension/substrate/agentScratch.ts +27 -54
  105. package/extension/substrate/bashScanTimeout.ts +181 -0
  106. package/extension/substrate/bindingDelivery.ts +38 -30
  107. package/extension/substrate/bindings.ts +4 -5
  108. package/extension/substrate/cache.ts +64 -12
  109. package/extension/substrate/childRestrictions.ts +39 -0
  110. package/extension/substrate/coldDoor.ts +17 -1
  111. package/extension/substrate/config.ts +157 -21
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/prompts.ts +22 -0
  115. package/extension/substrate/registry.ts +2 -0
  116. package/extension/substrate/resolverLease.ts +5 -4
  117. package/extension/substrate/sessionData.ts +85 -152
  118. package/extension/substrate/toolGating.ts +263 -84
  119. package/extension/substrate/unifiedDiff.ts +1 -1
  120. package/extension/substrate/workflowState.ts +178 -163
  121. package/extension/substrate/worktreeResolverLock.ts +261 -0
  122. package/extension/surfaces/surfaces.ts +79 -27
  123. package/extension/waves/adversarialReviewWave.ts +87 -46
  124. package/extension/waves/blockedReports.ts +59 -0
  125. package/extension/waves/draftReviewWave.ts +42 -42
  126. package/extension/waves/laneIdentity.ts +77 -0
  127. package/extension/waves/objectiveExplorerWave.ts +24 -24
  128. package/extension/waves/prReviewWave.ts +89 -77
  129. package/extension/waves/reportWave.ts +438 -578
  130. package/extension/waves/reviewClassifierWave.ts +22 -22
  131. package/extension/waves/rpcAdapter.ts +100 -15
  132. package/extension/waves/scoutWave.ts +192 -0
  133. package/extension/waves/transport.ts +480 -0
  134. package/extension/worker/sdkAdapter.ts +494 -0
  135. package/extension/worker/stageExecution.ts +679 -0
  136. package/extension/workerMain.ts +18 -19
  137. package/package.json +6 -4
  138. package/prompts/_fixtures/live.yaml +43 -18
  139. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  140. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  142. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  143. package/prompts/contexts/objective-refinement.md +17 -0
  144. package/prompts/contexts/read-only.md +1 -1
  145. package/prompts/stages/conflict-resolution-continuation.md +9 -6
  146. package/prompts/stages/conflict-resolution.md +4 -4
  147. package/prompts/stages/objective-plan/guidance.md +2 -2
  148. package/prompts/stages/objective-plan/seed.md +9 -1
  149. package/prompts/stages/objective-reconcile-ready.md +1 -1
  150. package/prompts/stages/objective-reconcile.md +1 -1
  151. package/prompts/stages/objective-refine/seed.md +18 -0
  152. package/prompts/stages/objective-review-browser.md +4 -4
  153. package/prompts/stages/objective-sync.md +1 -1
  154. package/prompts/stages/plan-review-browser.md +4 -4
  155. package/prompts/stages/pr-review-browser/active.md +3 -4
  156. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  157. package/prompts/stages/pr-review-terminal/active.md +3 -3
  158. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  159. package/prompts/stages/pr-review.md +3 -3
  160. package/prompts/stages/stack-review-browser/stack.md +5 -6
  161. package/shared/README.md +8 -0
  162. package/shared/bindings.yaml +3 -3
  163. package/shared/contracts.md +2601 -506
  164. package/shared/fixtures/issues-table.json +130 -0
  165. package/shared/registry.yaml +13 -0
  166. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  167. package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
  168. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  169. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  170. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  171. package/extension/doors/auditWaveTools.ts +0 -352
  172. package/extension/doors/ciExecutor.ts +0 -756
  173. package/extension/doors/commitCompact.ts +0 -251
  174. package/extension/doors/dreamWaveTools.ts +0 -489
  175. package/extension/doors/learn.ts +0 -668
  176. package/extension/doors/lifecycleGates.ts +0 -207
  177. package/extension/doors/objectiveStack.ts +0 -1543
  178. package/extension/doors/prReviewDynamic.ts +0 -276
  179. package/extension/doors/ready.ts +0 -279
  180. package/extension/doors/submit.ts +0 -373
  181. package/extension/doors/submitPrReview.ts +0 -505
  182. package/extension/factories/gistAuthor.ts +0 -94
  183. package/extension/factories/gistDraft.ts +0 -265
  184. package/extension/factories/gistSave.ts +0 -251
  185. package/extension/factories/implementHere.ts +0 -116
  186. package/extension/factories/objectiveAuthor.ts +0 -98
  187. package/extension/factories/objectiveDraft.ts +0 -466
  188. package/extension/factories/objectiveSave.ts +0 -366
  189. package/extension/factories/planDraft.ts +0 -140
  190. package/extension/factories/planMode.ts +0 -205
  191. package/extension/factories/planReview.ts +0 -1237
  192. package/extension/factories/planSave.ts +0 -604
  193. package/extension/factories/planTitle.ts +0 -141
  194. package/extension/substrate/structuredOutput.ts +0 -202
  195. package/extension/waves/auditWave.ts +0 -312
  196. package/extension/waves/harvestWave.ts +0 -399
  197. package/extension/waves/learnWave.ts +0 -155
  198. package/extension/waves/memoryAdapter.ts +0 -139
  199. package/extension/waves/prReviewDynamicWave.ts +0 -777
  200. package/extension/worker/readOnlySession.ts +0 -294
  201. package/extension/worker/worker.ts +0 -899
  202. package/prompts/stages/pr-review-dynamic.md +0 -7
@@ -0,0 +1,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
+ }