@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,672 @@
1
+ // The v1 Pi installer for the objective authoring flow (module-contracts.md's named-installer
2
+ // shape): `installObjectiveAuthoringBindings` owns the objective-authoring context hook pair,
3
+ // the `objective_draft`/`objective_save` tools, and the `/objective-save` command —
4
+ // registration metadata pinned by the suite's registration-parity tests. The feature logic lives in `authoring/objective/`; this
5
+ // module decodes at the tool boundary, builds the cold-door backend + gate + dream-gate
6
+ // adapters, constructs the warm-door Result envelopes, and places the feature-owned prose in Pi
7
+ // fields. `objectiveApprovalSaveV1` is the composed approval→save twin the review arm
8
+ // (`pi/v1/objectiveReview.ts`) and the browser door consume.
9
+ //
10
+ // The objective-authoring injection rides the shared `installInjectedContext` helper
11
+ // (pi/v1/contextInjection.ts — contracts §8.31 semantics): a live copy in the
12
+ // compaction-active window suppresses re-injection, and compaction dropping it from model
13
+ // context re-injects on the next turn.
14
+ //
15
+ // Format doctrine (rides the tools): JSON is the storage/transport format, NEVER the human
16
+ // review surface — the review arm renders markdown via the feature's resume+render helpers; the
17
+ // approval→save orchestration re-reads the STRUCTURED artifact. Carve-out doctrine: the draft
18
+ // tool takes NO path/name parameter (the artifact name is fixed and the bytes flow through the
19
+ // session seam), so allowlisting `objective_draft` in `READ_ONLY_TOOLS` keeps the read-only
20
+ // invariant intact.
21
+
22
+ import { join } from "node:path";
23
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
+ import {
25
+ DELIVERY_CHOICES,
26
+ isDeliveryChoice,
27
+ OBJECTIVE_DRAFT_ARTIFACT,
28
+ type ObjectiveDraftInput,
29
+ reviseObjectiveDraft,
30
+ } from "../../authoring/objective/draft.ts";
31
+ import {
32
+ type DreamReportGateOutcome,
33
+ resolveDreamReportGate,
34
+ } from "../../authoring/objective/dreamReportGate.ts";
35
+ import {
36
+ OBJECTIVE_AUTHOR_CONTEXT_TYPE,
37
+ OBJECTIVE_AUTHOR_MARKER,
38
+ OBJECTIVE_AUTHOR_STAGE,
39
+ objectiveAuthoringContextContent,
40
+ objectiveSaveGuidance,
41
+ } from "../../authoring/objective/prose.ts";
42
+ import {
43
+ type ObjectiveApprovalSaveDeps,
44
+ type ObjectiveBackend,
45
+ objectiveApprovalSave,
46
+ type SaveObjectiveOutcome,
47
+ saveObjective,
48
+ } from "../../authoring/objective/save.ts";
49
+ import type { ApprovalGate } from "../../authoring/review/approvalGate.ts";
50
+ import { DREAM_REPORT_INPUT_SCHEMA } from "../../learning/dreamReport.ts";
51
+ import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
52
+ import type { WorkflowSession } from "../../session/workflowSession.ts";
53
+ import { bindingSuffix } from "../../substrate/bindingDelivery.ts";
54
+ import { atomicWriteFileSync, ensureRunScratch } from "../../substrate/cache.ts";
55
+ import {
56
+ booleanField,
57
+ type ColdJson,
58
+ objectField,
59
+ runColdDoor,
60
+ stringField,
61
+ } from "../../substrate/coldDoor.ts";
62
+ import { registerPerkCommand } from "../../substrate/command.ts";
63
+ import { loadPerkConfig } from "../../substrate/config.ts";
64
+ import { failFor, ok, type Result } from "../../substrate/result.ts";
65
+ import type { ToolGating } from "../../substrate/toolGating.ts";
66
+ import { arrayParam, objectParam, paramsOf, stringParam } from "../../substrate/toolParams.ts";
67
+ import { type BranchEntry, branchOf, rebuildWorkflowState } from "../../substrate/workflowState.ts";
68
+ import { report, type Severity } from "../../surfaces/report.ts";
69
+ import { installInjectedContext } from "./contextInjection.ts";
70
+ import { type DraftReviewSlot, recordSaveOutcome } from "./draftReview.ts";
71
+ import { OBJECTIVE_BUDGET_TYPE } from "./objective.ts";
72
+ import { productionDreamGateRecovery } from "./objectiveDreamGate.ts";
73
+ import { isRefinementSession, refinementStageRefusal } from "./objectiveRefinement.ts";
74
+
75
+ // ------------------------------------------------------------------- the tool-boundary decode
76
+
77
+ /**
78
+ * The decoded `objective_save` tool params (shared with `objective_draft`) — an ALIAS of the
79
+ * feature's draft input, not a second handwritten contract: the tool boundary decodes exactly
80
+ * the shape the feature operations consume (`dream_report` stays opaque here — deep validation
81
+ * is the gate resolver's; the save path wraps it as its `{input}` carrier arm).
82
+ */
83
+ export type ObjectiveSaveParams = ObjectiveDraftInput;
84
+
85
+ /**
86
+ * The `delivery` enum property, shared between `objective_save` and `objective_draft` so the
87
+ * two tools' delivery contracts cannot drift. The description bakes in the explicit-human-choice
88
+ * discipline: the agent must ASK, with incremental recommended.
89
+ */
90
+ export const DELIVERY_PARAM_SCHEMA = {
91
+ type: "string",
92
+ enum: DELIVERY_CHOICES,
93
+ description:
94
+ "The reviewed delivery choice — ask the human explicitly (incremental is the recommended " +
95
+ "default: each plan lands independently; stacked lands ALL non-skipped roadmap nodes as " +
96
+ "one atomic PR train — capability-checked at save).",
97
+ } as const;
98
+
99
+ /**
100
+ * The `dream_report` property, shared between `objective_save` and `objective_draft` so the
101
+ * two tools' dream contracts cannot drift: the §8.62 `DREAM_REPORT_INPUT_SCHEMA` embedded by
102
+ * identifier (the `DELIVERY_PARAM_SCHEMA`/`ROADMAP_PARAM_SCHEMA` shared-schema pattern) plus
103
+ * the gate description. Structurally reachable only inside a `perk learn dream` session (the
104
+ * resolver refuses it outside one).
105
+ */
106
+ export const DREAM_REPORT_PARAM_SCHEMA = {
107
+ ...DREAM_REPORT_INPUT_SCHEMA,
108
+ description:
109
+ "The perk learn dream session's final report input (the parent's decisions only) — " +
110
+ "required inside a dream session, refused outside one.",
111
+ } as const;
112
+
113
+ /**
114
+ * The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
115
+ * so the two tools' roadmap contracts cannot drift.
116
+ */
117
+ export const ROADMAP_PARAM_SCHEMA = {
118
+ type: "object",
119
+ additionalProperties: false,
120
+ required: ["id", "description"],
121
+ properties: {
122
+ id: { type: "string", description: 'A stable node id, e.g. "1.1".' },
123
+ description: { type: "string", description: "What this node delivers." },
124
+ status: {
125
+ type: "string",
126
+ enum: ["pending", "planning", "in_progress", "done", "blocked", "skipped"],
127
+ description: "Optional initial status (defaults to pending).",
128
+ },
129
+ slug: { type: "string", description: "Optional short slug." },
130
+ pr: { type: "string", description: 'Optional plan/PR backlink, e.g. "#42".' },
131
+ depends_on: {
132
+ type: "array",
133
+ items: { type: "string" },
134
+ description: "Optional explicit dependency node ids.",
135
+ },
136
+ comment: { type: "string", description: "Optional note." },
137
+ adopt_issue: {
138
+ type: "string",
139
+ description:
140
+ "Optional: the id/identifier of a pre-existing source issue this node adopts in place " +
141
+ "(objective author --from, Linear only).",
142
+ },
143
+ },
144
+ } as const;
145
+
146
+ /**
147
+ * Decode unknown `objective_save` tool-call params (the tool-boundary seam). `prose`
148
+ * absent decodes to `""` (so `saveObjective`'s "no objective prose to save" `invalid_input` arm
149
+ * keeps owning that message) but present-but-mistyped → null (strict-fail). `roadmap` stays
150
+ * `unknown[]` — the Python cold door owns node-shape validation.
151
+ */
152
+ export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams | null {
153
+ const p = paramsOf(params);
154
+ if (p === null) return null;
155
+ const prose = stringParam(p, "prose");
156
+ const title = stringParam(p, "title");
157
+ const roadmap = arrayParam(p, "roadmap");
158
+ const base = stringParam(p, "base");
159
+ const delivery = stringParam(p, "delivery");
160
+ // `dream_report` must be a plain object when present (absent → undefined); deep validation
161
+ // stays with the gate resolver (resolveDreamReportGate).
162
+ const dreamReport = objectParam(p, "dream_report");
163
+ if (
164
+ prose === null ||
165
+ title === null ||
166
+ roadmap === null ||
167
+ base === null ||
168
+ delivery === null ||
169
+ dreamReport === null
170
+ ) {
171
+ return null;
172
+ }
173
+ // The delivery enum is strict beyond `string`: an off-enum value is present-but-mistyped.
174
+ if (delivery !== undefined && !isDeliveryChoice(delivery)) return null;
175
+ return { prose: prose ?? "", title, roadmap, base, delivery, dream_report: dreamReport };
176
+ }
177
+
178
+ // -------------------------------------------------------------- the cold-door backend adapter
179
+
180
+ /**
181
+ * The run-scoped dream-report transfer filename (contracts §8.64) — the extension→door handoff
182
+ * carrying the reviewed CANONICAL parts. The Python mirror is
183
+ * `perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` (parity-pinned by test), beside
184
+ * the existing `DREAM_MANIFEST_FILENAME` mirror pair.
185
+ */
186
+ export const DREAM_REPORT_TRANSFER_FILENAME = "dream-report-transfer.json";
187
+
188
+ /** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
189
+ export interface ObjectiveSaveOk {
190
+ /** `id` is the opaque string objective id (GitHub "7", Linear "ENG-7") — §8.21. */
191
+ objective: { id: string; url: string };
192
+ existed: boolean | null;
193
+ }
194
+
195
+ export type ObjectiveSaveResult = Result<ObjectiveSaveOk>;
196
+
197
+ /** The decoded `perk objective create --json` payload slice the warm door consumes. */
198
+ interface ObjectiveCreatePayload {
199
+ objective: { id: string; url: string; existed: boolean | undefined };
200
+ }
201
+
202
+ /** Narrow the `perk objective create --json` success payload; strict on `objective`. */
203
+ function decodeObjectiveCreate(payload: ColdJson): ObjectiveCreatePayload | null {
204
+ const objective = objectField(payload, "objective");
205
+ if (objective === undefined) return null;
206
+ const id = stringField(objective, "id");
207
+ const url = stringField(objective, "url");
208
+ if (id === undefined || url === undefined) return null;
209
+ return { objective: { id, url, existed: booleanField(objective, "existed") } };
210
+ }
211
+
212
+ /**
213
+ * The production `ObjectiveBackend` over the Python cold door (`perk objective create --json`
214
+ * via the shared cold-door client; the prose rides the run-scratch stdin channel; `runId: null`
215
+ * or blank omits `--run-id` — an identity-less save keeps working). On the dream arm
216
+ * (`dreamParts` present) the reviewed CANONICAL parts cross to the Python plane through the
217
+ * run-scoped `dream-report-transfer.json` handoff (§8.64) — staged atomically BEFORE the cold
218
+ * door (a write failure is the soft `scratch_failed` refusal, the runColdDoor stdin-staging
219
+ * precedent: the door is NOT invoked, nothing activates, the read-only gate stays on) — and
220
+ * `perk objective create` re-validates the transfer and converges the companion idempotently.
221
+ * Non-dream saves write nothing (byte-identical).
222
+ */
223
+ function coldDoorObjectiveBackend(pi: ExtensionAPI, ctx: ExtensionContext): ObjectiveBackend {
224
+ return {
225
+ async create(req) {
226
+ const runId = req.runId ?? "";
227
+ if (req.dreamParts !== undefined) {
228
+ try {
229
+ const dir = ensureRunScratch(ctx.cwd, runId);
230
+ const content = `${JSON.stringify(
231
+ { schema_version: "1", run_id: runId, parts: req.dreamParts },
232
+ null,
233
+ 2,
234
+ )}\n`;
235
+ atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
236
+ } catch (err) {
237
+ return {
238
+ status: "failed",
239
+ message: `could not stage the dream-report transfer: ${String(err)}`,
240
+ errorType: "scratch_failed",
241
+ };
242
+ }
243
+ }
244
+ const args = ["objective", "create", "--json"];
245
+ if (req.title) args.push("--title", req.title);
246
+ if (req.base) args.push("--base", req.base);
247
+ // The reviewed delivery choice rides verbatim; the cold door owns validation + preflight.
248
+ if (req.delivery) args.push("--delivery", req.delivery);
249
+ if (runId) args.push("--run-id", runId);
250
+ if (req.roadmap && req.roadmap.length > 0) {
251
+ args.push("--roadmap", JSON.stringify(req.roadmap));
252
+ }
253
+ const r = await runColdDoor<ObjectiveCreatePayload>(pi, ctx, args, {
254
+ label: "perk objective create",
255
+ decode: decodeObjectiveCreate,
256
+ stdin: { flag: "--body", content: req.prose, filename: "objective.md" },
257
+ });
258
+ if (!r.ok) return { status: "failed", message: r.message, errorType: r.errorType };
259
+ return {
260
+ status: "saved",
261
+ id: r.data.objective.id,
262
+ url: r.data.objective.url,
263
+ existed: r.data.objective.existed ?? null,
264
+ };
265
+ },
266
+ };
267
+ }
268
+
269
+ // ------------------------------------------------------------------------- adapter plumbing
270
+
271
+ /** Open the branch-backed session (always opens; `runId: null` is the identity-less arm). */
272
+ function openSession(pi: ExtensionAPI, ctx: ExtensionContext): WorkflowSession {
273
+ return openBranchWorkflowSession(pi, ctx);
274
+ }
275
+
276
+ /** The narrow gate slice the feature releases (D1a: exit only after a verified save). */
277
+ function gateFor(gating: ToolGating, ctx: ExtensionContext): ApprovalGate {
278
+ return { isActive: () => gating.isActive(), exit: () => gating.exit(ctx) };
279
+ }
280
+
281
+ /** The ctx-bound §8.63 gate resolver the feature ops consume — the resolver over the
282
+ * runtime-minted production recovery capability (`pi/v1/objectiveDreamGate.ts`). */
283
+ function dreamGateFor(
284
+ ctx: ExtensionContext,
285
+ ): (input: unknown, generatedAt: string) => DreamReportGateOutcome {
286
+ return (input, generatedAt) =>
287
+ resolveDreamReportGate(productionDreamGateRecovery(ctx), input, generatedAt);
288
+ }
289
+
290
+ /**
291
+ * The production approval→save dependency bag (the `planSaveDepsFor` mirror): session via the
292
+ * branch backing, the cold-door backend, the gate slice, and the ctx-bound dream-gate resolver.
293
+ * The review arm + the browser door consume the composed `objectiveApprovalSaveV1` instead.
294
+ */
295
+ export function objectiveSaveDepsFor(
296
+ pi: ExtensionAPI,
297
+ ctx: ExtensionContext,
298
+ gating: ToolGating,
299
+ ): ObjectiveApprovalSaveDeps {
300
+ return {
301
+ session: openSession(pi, ctx),
302
+ backend: coldDoorObjectiveBackend(pi, ctx),
303
+ gate: gateFor(gating, ctx),
304
+ resolveDreamGate: dreamGateFor(ctx),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Seed a fresh `perk:objective-budget` activation marker off a successful save whose linkage
310
+ * differed (mirrors `/objective <id>`'s activation, so budget tracking starts immediately).
311
+ * Byte-equivalent to the historical `linked !== objectiveId` guard: the seam's `unchanged` is
312
+ * the equal case, and applied/unverified/rejected all imply "differed" (the marker never keyed
313
+ * off the append's read-back result).
314
+ */
315
+ function activateBudgetIfLinked(pi: ExtensionAPI, save: SaveObjectiveOutcome): void {
316
+ if (save.status !== "saved") return;
317
+ if (save.linkage === null || save.linkage.status === "unchanged") return;
318
+ pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
319
+ objective_id: save.id,
320
+ activated_at: new Date().toISOString(),
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Render a save outcome as the warm-door Result envelope (the ONE result-construction site for
326
+ * every objective save surface — tool, command relay, review arm, browser door). A `saved`
327
+ * outcome renders the terminating "Saved/Found existing objective #id → url" twin; a `failed`
328
+ * outcome reports through the `failFor` seam (byte-identical to the failure the save always
329
+ * rendered).
330
+ */
331
+ function objectiveSaveResultOf(
332
+ ctx: ExtensionContext,
333
+ save: SaveObjectiveOutcome,
334
+ ): ObjectiveSaveResult {
335
+ if (save.status === "failed") {
336
+ return failFor(ctx, "objective-save")(save.message, save.errorType);
337
+ }
338
+ const verb = save.existed ? "Found existing" : "Saved";
339
+ return ok(
340
+ `${verb} objective #${save.id} → ${save.url}`,
341
+ {
342
+ objective: { id: save.id, url: save.url },
343
+ existed: save.existed,
344
+ },
345
+ { terminate: true },
346
+ );
347
+ }
348
+
349
+ /** The approval→save orchestration outcome, rendered (the door/arm-facing twin).
350
+ * `refused-draft` passes through unrendered — no `result`, no `gateExited`: nothing was saved,
351
+ * the gate was never touched, and no budget activation runs. */
352
+ export type ObjectiveApprovalSaveV1Outcome =
353
+ | { status: "no-draft" }
354
+ | { status: "refused-draft"; problem: string }
355
+ | { status: "saved" | "save-failed"; result: ObjectiveSaveResult; gateExited: boolean };
356
+
357
+ /**
358
+ * The composed approval→save twin (the shape `plan_review`'s objective arm, the browser door,
359
+ * and the `/objective-save` failsafe consume): run the feature's `objectiveApprovalSave` over
360
+ * the production deps, seed the budget activation marker off a linked save, and render the
361
+ * Result envelope. Flow semantics live in the feature op (artifact re-read, D1a gate exit,
362
+ * §8.63 re-validation); this twin owns only composition + rendering.
363
+ */
364
+ export async function objectiveApprovalSaveV1(
365
+ pi: ExtensionAPI,
366
+ ctx: ExtensionContext,
367
+ gating: ToolGating,
368
+ opts: { title?: string } = {},
369
+ ): Promise<ObjectiveApprovalSaveV1Outcome> {
370
+ return renderObjectiveApprovalSave(
371
+ pi,
372
+ ctx,
373
+ await objectiveApprovalSave(objectiveSaveDepsFor(pi, ctx, gating), opts),
374
+ );
375
+ }
376
+
377
+ /** Preserve the subject's linkage-budget and rendered save policy for explicit owned sessions. */
378
+ export function renderObjectiveApprovalSave(
379
+ pi: ExtensionAPI,
380
+ ctx: ExtensionContext,
381
+ outcome: Awaited<ReturnType<typeof objectiveApprovalSave>>,
382
+ ): ObjectiveApprovalSaveV1Outcome {
383
+ if (outcome.status === "no-draft") return { status: "no-draft" };
384
+ if (outcome.status === "refused-draft") {
385
+ return { status: "refused-draft", problem: outcome.problem };
386
+ }
387
+ activateBudgetIfLinked(pi, outcome.result);
388
+ return {
389
+ status: outcome.status,
390
+ result: objectiveSaveResultOf(ctx, outcome.result),
391
+ gateExited: outcome.gateExited,
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Whether the current branch is an objective-author session (read-only gate AND stage match).
397
+ * Fail-closed: a throwing state rebuild reports false, so no objective-authoring guidance is
398
+ * selected. (The runner fence lives in the shared injection helper.)
399
+ */
400
+ function isObjectiveAuthoring(gating: ToolGating, branch: readonly BranchEntry[]): boolean {
401
+ if (!gating.isActive()) return false;
402
+ try {
403
+ return rebuildWorkflowState(branch).stage === OBJECTIVE_AUTHOR_STAGE;
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+
409
+ // ------------------------------------------------------------------------------ the installer
410
+
411
+ const DRAFT_TOOL_GUIDELINES = [
412
+ "Call objective_draft to persist the current working objective as you author or revise it; pass the FULL prose and the FULL structured roadmap each time (it rewrites the whole draft).",
413
+ "objective_draft never saves to GitHub and never ends the turn — objective_save//objective-save remain the canonical save surface. Never hand-write roadmap YAML — hand the structured roadmap to the tool.",
414
+ "Pass objective_draft's `base` only to target a non-default branch; omit it to use the repo default.",
415
+ ];
416
+
417
+ const SAVE_TOOL_GUIDELINES = [
418
+ "Use objective_save only after the objective + roadmap are decision-complete; it creates the canonical perk:objective issue, activates it, and ends the turn.",
419
+ "Pass objective_save the objective PROSE in `prose` and the STRUCTURED roadmap in `roadmap` (a JSON array of nodes) — never hand-write roadmap YAML.",
420
+ 'Each objective_save roadmap node needs a stable `id` (e.g. "1.1") and a `description`; `status` defaults to pending. Use `depends_on` for explicit ordering.',
421
+ ];
422
+
423
+ /**
424
+ * Install every objective-authoring Pi binding: the objective-authoring context hook pair (the
425
+ * frozen hooks-ordering slot index.ts calls this at — planMode.ts defers when the stage is
426
+ * objective-author, so exactly one authoring context is injected), the `objective_draft` and
427
+ * `objective_save` tools, and the `/objective-save` command — registration metadata pinned by
428
+ * the registration-parity tests. Inert outside objective sessions; never throws.
429
+ */
430
+ export function installObjectiveAuthoringBindings(
431
+ pi: ExtensionAPI,
432
+ gating: ToolGating,
433
+ reviews: DraftReviewSlot,
434
+ runnerChild: () => boolean,
435
+ ): void {
436
+ // The objective-authoring context injection (display:false), keyed off (read-only gate AND
437
+ // stage === objective-author); the inject/strip mechanics (active-window dedup,
438
+ // selection-driven retention) and the runner fence live in the shared helper.
439
+ installInjectedContext(
440
+ pi,
441
+ {
442
+ customType: OBJECTIVE_AUTHOR_CONTEXT_TYPE,
443
+ flavors: {
444
+ [OBJECTIVE_AUTHOR_MARKER]: (ctx) =>
445
+ objectiveAuthoringContextContent(loadPerkConfig(ctx.cwd).planAuthoring),
446
+ },
447
+ select: (_ctx, branch) =>
448
+ isObjectiveAuthoring(gating, branch) ? OBJECTIVE_AUTHOR_MARKER : null,
449
+ },
450
+ runnerChild,
451
+ );
452
+
453
+ pi.registerTool({
454
+ name: "objective_draft",
455
+ label: "Objective draft",
456
+ description:
457
+ "Write (or overwrite) the working objective draft — prose + the structured roadmap — to " +
458
+ "the session data dir and record its provenance pointer. The only sanctioned write surface " +
459
+ "while read-only. NOT a save — objective_save//objective-save still persist the objective " +
460
+ "to GitHub.",
461
+ promptSnippet:
462
+ "Persist the working objective draft (prose + structured roadmap) to the session data dir (full rewrite)",
463
+ promptGuidelines: DRAFT_TOOL_GUIDELINES,
464
+ executionMode: "sequential",
465
+ parameters: {
466
+ type: "object",
467
+ additionalProperties: false,
468
+ required: ["prose"],
469
+ properties: {
470
+ prose: {
471
+ type: "string",
472
+ description: "The objective prose (the why, the design, the boundaries/non-goals).",
473
+ },
474
+ title: {
475
+ type: "string",
476
+ description: "Optional objective title (defaults to the prose's first heading).",
477
+ },
478
+ base: {
479
+ type: "string",
480
+ description:
481
+ "Optional target branch for this objective's plans (omit to use the repo default).",
482
+ },
483
+ delivery: DELIVERY_PARAM_SCHEMA,
484
+ dream_report: DREAM_REPORT_PARAM_SCHEMA,
485
+ roadmap: {
486
+ type: "array",
487
+ description:
488
+ "The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
489
+ items: ROADMAP_PARAM_SCHEMA,
490
+ },
491
+ },
492
+ },
493
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
494
+ // The shared param contract: the same decode as `objective_save`, so the two cannot
495
+ // drift. (The parameter literals are duplicated at both registration sites on purpose —
496
+ // the prose-review workbench needs in-place literals — and pinned identical by the
497
+ // registration baselines.)
498
+ const decoded = decodeObjectiveSaveParams(params);
499
+ if (decoded === null) {
500
+ return failFor(
501
+ ctx,
502
+ "objective-draft",
503
+ "objective_draft",
504
+ )(
505
+ "objective_draft needs { prose: string, roadmap?: array } per the tool schema",
506
+ "bad_input",
507
+ );
508
+ }
509
+ const fail = failFor(ctx, "objective-draft");
510
+ const revised = reviseObjectiveDraft(decoded, {
511
+ session: openSession(pi, ctx),
512
+ resolveDreamGate: dreamGateFor(ctx),
513
+ });
514
+ switch (revised.status) {
515
+ case "revised":
516
+ case "unchanged":
517
+ // A byte-identical rewrite short-circuits interior-side; the rendered result is
518
+ // computed from identical content either way, so the surface stays byte-stable.
519
+ return ok(
520
+ `Objective draft written → ${revised.receipt.path} (${revised.receipt.digest}; ` +
521
+ `${revised.roadmapNodes} roadmap nodes)`,
522
+ {
523
+ name: OBJECTIVE_DRAFT_ARTIFACT,
524
+ path: revised.receipt.path,
525
+ digest: revised.receipt.digest,
526
+ bytes: revised.bytes,
527
+ run_id: revised.receipt.runId,
528
+ roadmap_nodes: revised.roadmapNodes,
529
+ },
530
+ );
531
+ case "rejected":
532
+ return fail(revised.problem, revised.errorType);
533
+ case "unverified":
534
+ return fail(revised.problem, "write_failed");
535
+ }
536
+ },
537
+ });
538
+
539
+ pi.registerTool({
540
+ name: "objective_save",
541
+ label: "Save objective",
542
+ description:
543
+ "Persist a drafted objective + structured roadmap to GitHub as a perk:objective issue, " +
544
+ "activate it, and start budget tracking. Terminating: ends the turn on save. Call only when " +
545
+ "the objective and roadmap are decision-complete.",
546
+ promptSnippet: "Save the decision-complete objective + roadmap to GitHub (terminates the turn)",
547
+ promptGuidelines: SAVE_TOOL_GUIDELINES,
548
+ executionMode: "sequential",
549
+ parameters: {
550
+ type: "object",
551
+ additionalProperties: false,
552
+ required: ["prose"],
553
+ properties: {
554
+ prose: {
555
+ type: "string",
556
+ description: "The objective prose (the why, the design, the boundaries/non-goals).",
557
+ },
558
+ title: {
559
+ type: "string",
560
+ description: "Optional objective title (defaults to the prose's first heading).",
561
+ },
562
+ base: {
563
+ type: "string",
564
+ description:
565
+ "Optional target branch for this objective's plans (omit to use the repo default).",
566
+ },
567
+ delivery: DELIVERY_PARAM_SCHEMA,
568
+ dream_report: DREAM_REPORT_PARAM_SCHEMA,
569
+ roadmap: {
570
+ type: "array",
571
+ description:
572
+ "The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
573
+ items: ROADMAP_PARAM_SCHEMA,
574
+ },
575
+ },
576
+ },
577
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
578
+ const decoded = decodeObjectiveSaveParams(params);
579
+ if (decoded === null) {
580
+ return failFor(
581
+ ctx,
582
+ "objective-save",
583
+ "objective_save",
584
+ )(
585
+ "objective_save needs { prose: string, roadmap?: array } per the tool schema",
586
+ "bad_input",
587
+ );
588
+ }
589
+ // A refinement session never creates an objective — independent of tool visibility.
590
+ if (isRefinementSession(branchOf(ctx)))
591
+ return failFor(ctx, "objective_save")(
592
+ refinementStageRefusal("objective_save"),
593
+ "wrong_stage",
594
+ );
595
+ // The direct tool path wraps ONLY a present decoded value as the union's `direct` arm
596
+ // (the save stamps generated_at); no stored parts, so no byte-compare on this path.
597
+ const { dream_report, ...rest } = decoded;
598
+ const save = await saveObjective(
599
+ {
600
+ ...rest,
601
+ ...(dream_report !== undefined
602
+ ? { dream_report: { source: "direct" as const, input: dream_report } }
603
+ : {}),
604
+ },
605
+ objectiveSaveDepsFor(pi, ctx, gating),
606
+ );
607
+ // The manual save never consults the latch (it IS the deliberate retry) but reports into it.
608
+ recordSaveOutcome(reviews, "objective", {
609
+ confirmed: save.status === "saved",
610
+ ...(save.status === "failed" ? { detail: save.message } : {}),
611
+ });
612
+ activateBudgetIfLinked(pi, save);
613
+ return objectiveSaveResultOf(ctx, save);
614
+ },
615
+ });
616
+
617
+ registerPerkCommand(pi, "objective-save", {
618
+ description:
619
+ "Save the working objective draft to GitHub — the manual failsafe for the approval→save " +
620
+ "flow (artifact-first; drives the structured save only when no draft exists).",
621
+ handler: async (args, ctx) => {
622
+ if (isRefinementSession(branchOf(ctx))) {
623
+ report(ctx, "objective-save", "warning", refinementStageRefusal("/objective-save"));
624
+ return;
625
+ }
626
+ const title = args.trim() || undefined;
627
+ // The artifact-first manual-failsafe invocation of the shared approval→save
628
+ // seam (the D1a gate exit lives in the seam). The drive-the-session fallback covers
629
+ // draft-LESS sessions — objectives have no transcript scrape by design, so a draftless
630
+ // session still needs a working save path.
631
+ const outcome = await objectiveApprovalSaveV1(pi, ctx, gating, { title });
632
+ // The manual save never consults the latch (it IS the deliberate retry) but reports into it.
633
+ if (outcome.status === "saved" || outcome.status === "save-failed")
634
+ recordSaveOutcome(reviews, "objective", {
635
+ confirmed: outcome.status === "saved",
636
+ ...(outcome.result.details.ok ? {} : { detail: outcome.result.details.error }),
637
+ });
638
+ if (outcome.status === "refused-draft") {
639
+ // Fail-closed stop: the command's own precondition is a VALID draft — no gate exit,
640
+ // no driven turn (those fallbacks are for draft-LESS sessions; driving a fresh
641
+ // model-authored save over a corrupted artifact would silently abandon its bytes).
642
+ report(
643
+ ctx,
644
+ "objective-save",
645
+ "error",
646
+ `the working objective draft is invalid: ${outcome.problem} — rewrite it with ` +
647
+ "objective_draft, then re-run /objective-save",
648
+ );
649
+ return;
650
+ }
651
+ if (outcome.status === "no-draft") {
652
+ // Exit the read-only gate so the objective_save tool (excluded from READ_ONLY_TOOLS)
653
+ // becomes reachable on the driven turn, then drive the turn (mirrors /address and
654
+ // /objective-plan).
655
+ if (gating.isActive()) gating.exit(ctx);
656
+ report(ctx, "objective-save", "info", "handing the structured save to the session");
657
+ // The perk-objective-author pointer rides the skill-binding suffix (D5) since a
658
+ // warm /objective-save outside a stage:objective-author session gets none from Mechanism A.
659
+ pi.sendUserMessage(
660
+ objectiveSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:objective-author"),
661
+ );
662
+ return;
663
+ }
664
+ // Saved or save-failed: relay the save message. No node-link sub-step on the objective path,
665
+ // so the severity ladder is simpler than /plan-save's (no warning tier).
666
+ const result = outcome.result;
667
+ const message = result.content[0]?.text ?? "objective-save done";
668
+ const severity: Severity = result.details.ok ? "info" : "error";
669
+ report(ctx, "objective-save", severity, message);
670
+ },
671
+ });
672
+ }