@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,345 @@
1
+ // The objective working-draft feature: the fixed artifact constant and the draft operations
2
+ // over the WorkflowSession seam — the objective-flavored sibling of `authoring/plan/draft.ts`,
3
+ // JSON-envelope flavored (deliberately unshared shapes: a generic envelope would share syntax
4
+ // while erasing each flow's recovery semantics).
5
+ //
6
+ // Carve-out doctrine: the ops take NO path/name parameter — the artifact name is the fixed
7
+ // constant `OBJECTIVE_DRAFT_ARTIFACT` and every byte flows through the session seam (file +
8
+ // verified `session_artifacts` pointer), so the only thing they can ever touch is the one
9
+ // working-objective artifact in the current run's data dir (gitignored scratch). Full rewrite
10
+ // per call, never a save — `objective_save`/`/objective-save` still persist to GitHub.
11
+ //
12
+ // Format doctrine: JSON is the storage/transport format, NEVER the human review surface. The
13
+ // artifact carries `{schema_version, title?, prose, roadmap}` (plus, in a perk learn dream
14
+ // session, the gate-written `dream_report` block — contracts §8.63) — the structured roadmap
15
+ // rides verbatim (node-shape validation stays with the Python plane at save time, the
16
+ // `parse_structured_roadmap` path). The review surface reads the draft via
17
+ // `resumeObjectiveDraft` (digest-validated, classified valid/absent/refused) and renders markdown via
18
+ // `renderObjectiveDraft` (the prose + a roadmap table) — never raw JSON; the
19
+ // approval→`objective_save` orchestration feeds the recovered roadmap back as structured JSON.
20
+ //
21
+ // The §8.63 dream gate arrives INJECTED (`resolveDreamGate` — ctx-bound by the adapter) so the
22
+ // module stays session-pure; `dreamReportGate.ts` owns the matrix.
23
+
24
+ import type { SessionArtifactReceipt, WorkflowSession } from "../../session/workflowSession.ts";
25
+ import {
26
+ type DreamReportGateOutcome,
27
+ decodeDreamReportBlock,
28
+ type ObjectiveDreamReportBlock,
29
+ } from "./dreamReportGate.ts";
30
+
31
+ /**
32
+ * The reviewed objective delivery vocabulary (contracts §8.45) — the ONE authority every
33
+ * layer derives from: the `DeliveryChoice` union, the tool schema's enum, and the decoder's
34
+ * narrowing all read these values, so a new variant cannot drift between them.
35
+ */
36
+ export const DELIVERY_CHOICES = ["incremental", "stacked"] as const;
37
+
38
+ /** The reviewed objective delivery choice (contracts §8.45). */
39
+ export type DeliveryChoice = (typeof DELIVERY_CHOICES)[number];
40
+
41
+ /** Whether `value` is a member of the §8.45 delivery vocabulary (the decoder's narrowing). */
42
+ export function isDeliveryChoice(value: string): value is DeliveryChoice {
43
+ return (DELIVERY_CHOICES as readonly string[]).includes(value);
44
+ }
45
+
46
+ /** The fixed working-objective artifact name (one JSON file: prose + the structured roadmap). */
47
+ export const OBJECTIVE_DRAFT_ARTIFACT = "objective-draft.json";
48
+
49
+ /** The typed revise input — decode owns the shape at the tool boundary (pi/v1). */
50
+ export interface ObjectiveDraftInput {
51
+ prose: string;
52
+ title?: string;
53
+ roadmap?: unknown[];
54
+ /** The objective's target branch; omitted to use the repo default. */
55
+ base?: string;
56
+ /** The reviewed delivery choice; omitted ⇒ incremental (the §8.42 absence rule). */
57
+ delivery?: DeliveryChoice;
58
+ /** The dream-report input (perk learn dream only — §8.63); deep validation is the gate's. */
59
+ dream_report?: unknown;
60
+ }
61
+
62
+ /**
63
+ * The revise outcome (the `RevisePlanDraftResult` mirror, plus the §8.63 gate arm). `rejected`
64
+ * splits by `reason` AND carries the exact `errorType` the adapter reports (`gate_refused` is
65
+ * the dynamic arm — the resolver's own `invalid_input`/`bad_state`); `unverified` means an
66
+ * effect may have landed but the read-back proof failed. `problem` carries the caller-facing
67
+ * message bytes. `roadmapNodes` rides so the adapter's rendered twin ("… N roadmap nodes")
68
+ * needs no re-parse.
69
+ */
70
+ export type ReviseObjectiveDraftResult =
71
+ | { status: "revised"; receipt: SessionArtifactReceipt; bytes: number; roadmapNodes: number }
72
+ | { status: "unchanged"; receipt: SessionArtifactReceipt; bytes: number; roadmapNodes: number }
73
+ | {
74
+ status: "rejected";
75
+ reason: "blank_prose" | "no_identity" | "gate_refused" | "write_refused";
76
+ problem: string;
77
+ errorType: "invalid_input" | "no_run_id" | "bad_state" | "write_failed";
78
+ }
79
+ | { status: "unverified"; problem: string };
80
+
81
+ /**
82
+ * Rewrite the working objective draft (a whole-value replacement) through the session seam:
83
+ * serialize the explicit-literal payload (deterministic key order; `title`/`base`/`delivery`
84
+ * omitted when blank/absent; `schema_version` stays 1 — an additive optional field, fail-open
85
+ * readers), gate `dream_report` at write time (§8.63 — validated here so a report-less dream
86
+ * bundle can never reach review), then write. Diagnostic precedence preserved: blank prose →
87
+ * identity → the dream gate → the write. A byte-identical rewrite short-circuits `unchanged`
88
+ * (the session engine owns the probe). Never throws.
89
+ */
90
+ export function reviseObjectiveDraft(
91
+ input: ObjectiveDraftInput,
92
+ deps: {
93
+ session: WorkflowSession;
94
+ /** The §8.63 gate, adapter-bound (`resolveDreamReportGate` over the production recovery capability). */
95
+ resolveDreamGate: (input: unknown, generatedAt: string) => DreamReportGateOutcome;
96
+ },
97
+ ): ReviseObjectiveDraftResult {
98
+ if (!input.prose.trim()) {
99
+ return {
100
+ status: "rejected",
101
+ reason: "blank_prose",
102
+ problem: "no objective prose to write (pass the full working draft)",
103
+ errorType: "invalid_input",
104
+ };
105
+ }
106
+ if (deps.session.runId === null) {
107
+ return {
108
+ status: "rejected",
109
+ reason: "no_identity",
110
+ problem: "session has no run_id — cannot write the objective-draft artifact",
111
+ errorType: "no_run_id",
112
+ };
113
+ }
114
+
115
+ // The §8.63 gate: validated at draft-write time via buildDreamReport, the ONE stamp stored
116
+ // with the block; `absent` keeps the payload byte-identical (every non-dream path unchanged).
117
+ const gate = deps.resolveDreamGate(input.dream_report, new Date().toISOString());
118
+ if (gate.kind === "refuse") {
119
+ return {
120
+ status: "rejected",
121
+ reason: "gate_refused",
122
+ problem: gate.detail,
123
+ errorType: gate.errorType,
124
+ };
125
+ }
126
+
127
+ const title = input.title?.trim();
128
+ const base = input.base?.trim();
129
+ const delivery = input.delivery;
130
+ const roadmap = input.roadmap ?? [];
131
+ const payload = {
132
+ schema_version: 1,
133
+ ...(title ? { title } : {}),
134
+ ...(base ? { base } : {}),
135
+ ...(delivery ? { delivery } : {}),
136
+ ...(gate.kind === "block" ? { dream_report: gate.block } : {}),
137
+ prose: input.prose,
138
+ roadmap,
139
+ };
140
+ const content = `${JSON.stringify(payload, null, 2)}\n`;
141
+ const bytes = Buffer.byteLength(content, "utf8");
142
+
143
+ const written = deps.session.writeArtifact(OBJECTIVE_DRAFT_ARTIFACT, content);
144
+ switch (written.status) {
145
+ case "applied":
146
+ return { status: "revised", receipt: written.receipt, bytes, roadmapNodes: roadmap.length };
147
+ case "unchanged":
148
+ return {
149
+ status: "unchanged",
150
+ receipt: written.receipt,
151
+ bytes,
152
+ roadmapNodes: roadmap.length,
153
+ };
154
+ case "rejected":
155
+ return {
156
+ status: "rejected",
157
+ reason: "write_refused",
158
+ problem: `could not write the ${OBJECTIVE_DRAFT_ARTIFACT} artifact (see warnings)`,
159
+ errorType: "write_failed",
160
+ };
161
+ case "unverified":
162
+ return {
163
+ status: "unverified",
164
+ problem: `could not write the ${OBJECTIVE_DRAFT_ARTIFACT} artifact (see warnings)`,
165
+ };
166
+ }
167
+ }
168
+
169
+ // ------------------------------------------------------------------- the reader + the renderer
170
+
171
+ /** The validated working-objective draft shape consumers receive from `resumeObjectiveDraft`. */
172
+ export interface ObjectiveDraft {
173
+ title?: string;
174
+ prose: string;
175
+ roadmap: unknown[];
176
+ /** The objective's target branch; kept only when a non-blank string in the artifact. */
177
+ base?: string;
178
+ /** The reviewed delivery choice; kept only when exactly the enum (junk → absent, like `base`). */
179
+ delivery?: DeliveryChoice;
180
+ /** The dream-report block (§8.63); a present-but-malformed block refuses the WHOLE draft. */
181
+ dream_report?: ObjectiveDreamReportBlock;
182
+ }
183
+
184
+ /**
185
+ * The classified resume outcome: a refused draft is a fail-closed STOP at every consumer —
186
+ * it never takes the no-draft fallbacks' side effects (gate exit, driven turn). `raw` is the
187
+ * artifact bytes the draft was decoded from — the reviewed-bytes baseline; consumers that
188
+ * render and fence must use this one read for both (a second read could be newer than what
189
+ * the human saw).
190
+ */
191
+ export type ResumeObjectiveDraftResult =
192
+ | { kind: "valid"; draft: ObjectiveDraft; raw: string }
193
+ | { kind: "absent" }
194
+ | { kind: "refused"; problem: string };
195
+
196
+ /**
197
+ * Resume + validate the working-objective draft artifact from the session, classified: seam
198
+ * `absent` → `absent` (the genuine no-draft arm); seam `invalid` → `refused` carrying the
199
+ * seam's problem (a corrupted artifact is truthfully rendered at the edge — the seam's own
200
+ * stderr tier is untouched); decoder refusals (`decodeObjectiveDraft`) pass through. Never
201
+ * throws.
202
+ */
203
+ export function resumeObjectiveDraft(session: WorkflowSession): ResumeObjectiveDraftResult {
204
+ const read = session.readArtifact(OBJECTIVE_DRAFT_ARTIFACT);
205
+ if (read.status === "absent") return { kind: "absent" };
206
+ if (read.status === "invalid") return { kind: "refused", problem: read.problem };
207
+ return decodeObjectiveDraft(read.content);
208
+ }
209
+
210
+ /**
211
+ * Decode + validate working-objective artifact bytes (the content half of
212
+ * `resumeObjectiveDraft`, for consumers that already hold the seam-validated bytes — e.g. the
213
+ * browser door, whose stale-guard baseline and decode input must be the SAME read): malformed
214
+ * JSON, a non-object payload, an unsupported `schema_version`, or blank prose → `refused`
215
+ * with the decoder's problem. `roadmap` defaults to `[]` when absent/non-array; `title` is
216
+ * kept only when a non-blank string. A present-but-malformed `dream_report` block refuses the
217
+ * WHOLE draft — deliberately stricter than the lenient junk→absent handling of
218
+ * `base`/`delivery`, because silently dropping a malformed report is exactly what §8.63
219
+ * forbids. Never throws.
220
+ */
221
+ export function decodeObjectiveDraft(
222
+ content: string,
223
+ ): Exclude<ResumeObjectiveDraftResult, { kind: "absent" }> {
224
+ const refuse = (why: string): { kind: "refused"; problem: string } => ({
225
+ kind: "refused",
226
+ problem: `${OBJECTIVE_DRAFT_ARTIFACT} ${why} — refusing the draft`,
227
+ });
228
+ let parsed: unknown;
229
+ try {
230
+ parsed = JSON.parse(content);
231
+ } catch {
232
+ return refuse("is not valid JSON");
233
+ }
234
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
235
+ return refuse("is not a JSON object");
236
+ }
237
+ const payload = parsed as Record<string, unknown>;
238
+ if (payload.schema_version !== 1) {
239
+ return refuse(`has an unsupported schema_version (${JSON.stringify(payload.schema_version)})`);
240
+ }
241
+ const prose = payload.prose;
242
+ if (typeof prose !== "string" || !prose.trim()) {
243
+ return refuse("has no prose");
244
+ }
245
+ const roadmap = Array.isArray(payload.roadmap) ? payload.roadmap : [];
246
+ const title =
247
+ typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
248
+ const base = typeof payload.base === "string" && payload.base.trim() ? payload.base : undefined;
249
+ const delivery =
250
+ typeof payload.delivery === "string" && isDeliveryChoice(payload.delivery)
251
+ ? payload.delivery
252
+ : undefined;
253
+ let dreamReport: ObjectiveDreamReportBlock | undefined;
254
+ if ("dream_report" in payload) {
255
+ const block = decodeDreamReportBlock(payload.dream_report);
256
+ if (block === null) {
257
+ return refuse("carries a malformed dream_report block");
258
+ }
259
+ dreamReport = block;
260
+ }
261
+ return {
262
+ kind: "valid",
263
+ draft: {
264
+ ...(title !== undefined ? { title } : {}),
265
+ ...(base !== undefined ? { base } : {}),
266
+ ...(delivery !== undefined ? { delivery } : {}),
267
+ ...(dreamReport !== undefined ? { dream_report: dreamReport } : {}),
268
+ prose,
269
+ roadmap,
270
+ },
271
+ raw: content,
272
+ };
273
+ }
274
+
275
+ /** Sanitize a table cell: `|` escaped, newlines collapsed to a single space. */
276
+ function tableCell(value: string): string {
277
+ return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
278
+ }
279
+
280
+ /** Read a string field off an unknown-shaped roadmap node (`""` when absent/mistyped). */
281
+ function nodeString(node: unknown, key: string): string {
282
+ if (typeof node !== "object" || node === null) return "";
283
+ const value = (node as Record<string, unknown>)[key];
284
+ return typeof value === "string" ? value : "";
285
+ }
286
+
287
+ /** Render a node's `depends_on` as a `", "`-join of its string members; `-` when empty/absent. */
288
+ function nodeDependsOn(node: unknown): string {
289
+ if (typeof node !== "object" || node === null) return "-";
290
+ const value = (node as Record<string, unknown>).depends_on;
291
+ if (!Array.isArray(value)) return "-";
292
+ const deps = value.filter((d): d is string => typeof d === "string");
293
+ return deps.length > 0 ? deps.join(", ") : "-";
294
+ }
295
+
296
+ /** The always-present prominent `**Delivery:**` review line (contracts §8.45). */
297
+ function deliveryLine(draft: ObjectiveDraft): string {
298
+ if (draft.delivery === "stacked") {
299
+ return (
300
+ "**Delivery: STACKED** — all non-skipped roadmap nodes land as ONE atomic pull-request " +
301
+ "train (capability-checked at save)"
302
+ );
303
+ }
304
+ return "**Delivery: incremental** (the default — each plan lands independently)";
305
+ }
306
+
307
+ /**
308
+ * Render the draft as the markdown review surface (JSON is storage/transport only — contracts
309
+ * §8.1): the optional `# title` heading, the prose verbatim, and (when the roadmap is non-empty)
310
+ * a `## Roadmap` section with ONE markdown table. A prominent `**Delivery:**` line renders
311
+ * directly under the title unconditionally (the reviewed choice must be visible either way —
312
+ * contracts §8.45). The `Phase` column appears only when some node carries a non-blank string
313
+ * `phase`. When the draft carries a `dream_report` block, the stored CANONICAL parts append as
314
+ * the final section — the review surface IS the approval bundle: the objective and its report
315
+ * review (and are approved or denied) together (§8.63); the parts carry their own
316
+ * `# Dream report — <run_id>` headers. Pure; never throws.
317
+ */
318
+ export function renderObjectiveDraft(draft: ObjectiveDraft): string {
319
+ let out = "";
320
+ if (draft.title) out += `# ${draft.title}\n\n`;
321
+ out += `${deliveryLine(draft)}\n\n`;
322
+ out += draft.prose;
323
+
324
+ if (draft.roadmap.length > 0) {
325
+ const withPhase = draft.roadmap.some((node) => nodeString(node, "phase").trim().length > 0);
326
+ const header = withPhase
327
+ ? "| Node | Phase | Description | Depends On | Status |\n| --- | --- | --- | --- | --- |"
328
+ : "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
329
+ const rows = draft.roadmap.map((node) => {
330
+ const cells = [
331
+ tableCell(nodeString(node, "id")),
332
+ ...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
333
+ tableCell(nodeString(node, "description")),
334
+ tableCell(nodeDependsOn(node)),
335
+ tableCell(nodeString(node, "status") || "pending"),
336
+ ];
337
+ return `| ${cells.join(" | ")} |`;
338
+ });
339
+ out = `${out.trimEnd()}\n\n## Roadmap\n\n${header}\n${rows.join("\n")}\n`;
340
+ }
341
+
342
+ if (draft.dream_report === undefined) return out;
343
+ // The approval bundle: objective first, then the stored CANONICAL report parts.
344
+ return `${out.trimEnd()}\n\n${draft.dream_report.parts.join("\n\n")}\n`;
345
+ }
@@ -1,9 +1,9 @@
1
1
  // The `dream_report` gate for the objective draft/review/save path (contracts.md §8.63).
2
2
  //
3
- // ONE resolver implements the whole gate matrix — `writeObjectiveDraft` (objectiveDraft.ts)
4
- // and `saveObjective` (objectiveSave.ts) both consume its typed outcome, so no parallel
5
- // branch/message implementation can drift. "Dream session" is detected structurally, exactly
6
- // like `run_dream_wave` (doors/dreamWaveTools.ts): the session's claimed `run_id` plus the
3
+ // ONE resolver implements the whole gate matrix — `reviseObjectiveDraft` (draft.ts) and
4
+ // `saveObjective` (save.ts) both consume its typed outcome, so no parallel branch/message
5
+ // implementation can drift. "Dream session" is detected structurally, exactly
6
+ // like `run_dream_wave` (pi/v1/learning/dream.ts): the session's claimed `run_id` plus the
7
7
  // existence of the run-scoped dream manifest (no claimed run counts as non-dream). The gate is
8
8
  // fail-closed in BOTH directions: a dream session refuses a report-less objective (the
9
9
  // objective and its report review as ONE bundle — an approval is always savable), and a
@@ -13,28 +13,25 @@
13
13
  // Trusted-context recovery follows the session-artifacts digest-pointer doctrine: the bare
14
14
  // run-scratch bundle is never trusted — the `dream_bundle_digest` workflow-state marker
15
15
  // (cleared at wave entry, set to the finalized bytes' digest after a successful finalize) is
16
- // the freshness/integrity authority, and the bundle is strictly re-decoded through
17
- // `decodeFinalizedDreamBundle` on every recovery read (untrusted-at-rest posture). After a
18
- // successful recovery the revalidation bracket (contracts.md §8.65) re-proves HEAD-unchanged +
19
- // tree-clean against the manifest's stamped `commit_sha` at draft-write AND save, since both
20
- // consumers flow through the one resolver; drift refuses `bad_state` (the analysis is stale).
16
+ // the freshness/integrity authority, and the bundle is strictly re-decoded through the
17
+ // finalized-bundle decoder on every recovery read (untrusted-at-rest posture). The recovery
18
+ // MECHANICS are edge-owned: the resolver consumes the runtime-minted `DreamGateRecovery`
19
+ // capability (production: `pi/v1/objectiveDreamGate.ts`), so this module stays storage-free.
20
+ // After a successful recovery the revalidation bracket (contracts.md §8.65) re-proves
21
+ // HEAD-unchanged + tree-clean against the manifest's stamped `commit_sha` — at draft-write AND
22
+ // save, since both consumers flow through the one resolver; drift refuses `bad_state` (the
23
+ // analysis is stale).
21
24
  //
22
- // Imports only the dream wave siblings, the substrate seams, and node builtins — cycle-free
23
- // (nothing in `waves/` imports factories) and loadable under `node --test`.
25
+ // Imports only the dream wave siblings cycle-free (nothing in `waves/` imports `authoring/`)
26
+ // and loadable under `node --test`.
24
27
 
25
- import { existsSync, readFileSync } from "node:fs";
26
- import { dirname, join } from "node:path";
27
- import { runScratchDir } from "../substrate/cache.ts";
28
- import { revalidationBracket } from "../substrate/git.ts";
29
- import { digestSessionData, type SessionDataCtx } from "../substrate/sessionData.ts";
30
- import { branchOf, rebuildWorkflowState, type WorkflowState } from "../substrate/workflowState.ts";
31
- import { DREAM_ANALYSES_FILENAME, decodeFinalizedDreamBundle } from "../waves/dreamReducerWave.ts";
32
- import { buildDreamReport, type DreamReportContext } from "../waves/dreamReport.ts";
33
28
  import {
34
29
  codePointLength,
35
- DREAM_MANIFEST_FILENAME,
36
- decodeDreamManifest,
37
- } from "../waves/dreamWave.ts";
30
+ type DreamLaneAnalysis,
31
+ type DreamManifest,
32
+ } from "../../learning/dream.ts";
33
+ import type { DreamReducerAnalysis } from "../../learning/dreamReducer.ts";
34
+ import { buildDreamReport, type DreamReportContext } from "../../learning/dreamReport.ts";
38
35
 
39
36
  /**
40
37
  * The shared part-invariance + size rule's comment-body cap (contracts §8.64) — the full
@@ -152,91 +149,35 @@ export function decodeDreamReportBlock(value: unknown): ObjectiveDreamReportBloc
152
149
  }
153
150
 
154
151
  /**
155
- * Recover the trusted `DreamReportContext` from the claimed run's scratch state — every arm
156
- * fail-closed with a named detail (the caller maps them to `bad_state`):
157
- *
158
- * 1. the run-scoped manifest: read + parse + `decodeDreamManifest` (the strict §8.60 decoder,
159
- * path bound at decode time). No `verifyDocContainment` this path reads no doc files;
160
- * the lexical decode suffices (resolved containment is the wave tool's pre-spawn concern);
161
- * 2. the freshness check: the `dream_bundle_digest` marker (read from the caller's ONE
162
- * workflow-state snapshot) must be present, non-empty, and equal the digest of the bundle
163
- * bytes just read — a bare file is never trusted (missing marker = no finalized wave;
164
- * empty = invalidated by a newer attempt, including a cleanup-failure residue; mismatch =
165
- * stale/tampered bytes);
166
- * 3. the strict finalized decode (`decodeFinalizedDreamBundle` over the digest of the
167
- * manifest bytes just read — the marker authenticates the bundle bytes and the bundle's
168
- * `manifest_digest` extends that authority to the manifest, so an at-rest manifest edit
169
- * refuses; the analyses-only mid-wave shape refuses here too).
152
+ * The narrow per-operation trusted-context recovery capability the Pi/session edge mints
153
+ * (production: `productionDreamGateRecovery` in `pi/v1/objectiveDreamGate.ts`; tests inject
154
+ * fakes). ANTI-PROOF-OBJECT CONTRACT: the decode/digest/revalidation checks behind
155
+ * `recoverContext` and `bracket` are runtime verification executed on EVERY consuming
156
+ * operation (draft-write and save), never replaced by a structural type, assertion, or
157
+ * previously computed proof object.
170
158
  */
171
- function recoverDreamReportContext(
172
- ctx: SessionDataCtx,
173
- runId: string,
174
- marker: string | undefined,
175
- generatedAt: string,
176
- ): { ok: true; context: DreamReportContext } | { ok: false; detail: string } {
177
- const manifestPath = join(runScratchDir(ctx.cwd, runId), DREAM_MANIFEST_FILENAME);
178
- let manifestBytes: string;
179
- let rawManifest: unknown;
180
- try {
181
- manifestBytes = readFileSync(manifestPath, "utf8");
182
- rawManifest = JSON.parse(manifestBytes);
183
- } catch (error) {
184
- const detail = error instanceof Error ? error.message : String(error);
185
- return { ok: false, detail: `dream manifest unreadable at '${manifestPath}': ${detail}` };
186
- }
187
- const manifest = decodeDreamManifest(rawManifest, manifestPath);
188
- if (!manifest.ok) {
189
- return { ok: false, detail: `dream manifest invalid: ${manifest.detail}` };
190
- }
191
-
192
- const bundlePath = join(dirname(manifestPath), DREAM_ANALYSES_FILENAME);
193
- let bundleBytes: string;
194
- try {
195
- bundleBytes = readFileSync(bundlePath, "utf8");
196
- } catch {
197
- return {
198
- ok: false,
199
- detail: `no dream bundle at '${bundlePath}' — re-run the dream wave`,
200
- };
201
- }
202
- if (marker === undefined || marker === "") {
203
- return {
204
- ok: false,
205
- detail: "no finalized dream wave for this session — re-run the dream wave",
206
- };
207
- }
208
- if (digestSessionData(bundleBytes) !== marker) {
209
- return {
210
- ok: false,
211
- detail:
212
- "the dream bundle does not match the session's finalized digest — re-run the dream wave",
213
- };
214
- }
215
- let rawBundle: unknown;
216
- try {
217
- rawBundle = JSON.parse(bundleBytes);
218
- } catch (error) {
219
- const detail = error instanceof Error ? error.message : String(error);
220
- return { ok: false, detail: `dream bundle is not valid JSON: ${detail}` };
221
- }
222
- const decoded = decodeFinalizedDreamBundle(
223
- rawBundle,
224
- manifest.manifest,
225
- digestSessionData(manifestBytes),
226
- );
227
- if (!decoded.ok) {
228
- return { ok: false, detail: `${decoded.detail} — re-run the dream wave` };
229
- }
230
- return {
231
- ok: true,
232
- context: {
233
- manifest: manifest.manifest,
234
- analyses: decoded.analyses,
235
- reducers: decoded.reducers,
236
- run_id: runId,
237
- generated_at: generatedAt,
238
- },
239
- };
159
+ export interface DreamGateRecovery {
160
+ /** ONE fresh workflow-state snapshot per gate resolution (run identity + freshness marker + dream detection).
161
+ * `detail` is always the RAW CAUSE (a caught message or a fixed cause literal) — the resolver
162
+ * owns the one rendering prefix; the capability never pre-renders. */
163
+ readSession():
164
+ | { kind: "unreadable"; detail: string }
165
+ | { kind: "read"; runId: string | null; dream: boolean; marker: string | undefined };
166
+ /** Fresh manifest+bundle read + the full decode/digest ladder — re-executed on EVERY call, never cached.
167
+ * `detail` here IS the final text (today's recovery details are complete sentences; byte-preserved). */
168
+ recoverContext(
169
+ runId: string,
170
+ marker: string | undefined,
171
+ ):
172
+ | {
173
+ ok: true;
174
+ manifest: DreamManifest;
175
+ analyses: DreamLaneAnalysis[];
176
+ reducers: DreamReducerAnalysis[];
177
+ }
178
+ | { ok: false; detail: string };
179
+ /** The §8.65 revalidation bracket against the recovered manifest's stamped commit_sha. */
180
+ bracket(expectedSha: string): { ok: boolean; detail: string | null };
240
181
  }
241
182
 
242
183
  /** The typed gate outcome both consumers branch on — the whole matrix, one vocabulary. */
@@ -257,40 +198,34 @@ export type DreamReportGateOutcome =
257
198
  * | dream | absent | refuse `invalid_input` (one approval bundle) |
258
199
  * | dream | present | recover context → `buildDreamReport` → refuse or `block` |
259
200
  *
260
- * An UNREADABLE workflow state (a throwing branch read) refuses `bad_state` BEFORE the matrix
261
- * it is never conflated with a confirmed non-dream session (the `activeSessionRunId`
201
+ * An UNREADABLE workflow state (`readSession()`'s `unreadable` arm a throwing branch read,
202
+ * or the capability's fail-closed run-id/marker narrowing) refuses `bad_state` BEFORE the
203
+ * matrix — it is never conflated with a confirmed non-dream session (the `activeSessionRunId`
262
204
  * null-on-throw sentinel would otherwise let a transient read failure surface as `absent`).
205
+ * The capability's `detail` is the RAW CAUSE; this resolver owns the one rendering prefix.
263
206
  *
264
207
  * Failure taxonomy: gate violations + `buildDreamReport` refusals → `invalid_input` (the
265
208
  * bounded ≤25 named details newline-joined); an unreadable workflow state and
266
- * context-recovery failures → `bad_state`.
209
+ * context-recovery failures → `bad_state` (recovery details pass through UNPREFIXED).
267
210
  */
268
211
  export function resolveDreamReportGate(
269
- ctx: SessionDataCtx,
212
+ recovery: DreamGateRecovery,
270
213
  input: unknown,
271
214
  generatedAt: string,
272
- bracket: (
273
- cwd: string,
274
- expectedSha: string,
275
- ) => { ok: boolean; detail: string | null } = revalidationBracket,
276
215
  ): DreamReportGateOutcome {
277
- // ONE workflow-state snapshot for the whole gate (run identity + the freshness marker),
278
- // read with error distinction: unreadable state fails closed, never "non-dream".
279
- let state: WorkflowState;
280
- try {
281
- state = rebuildWorkflowState(branchOf(ctx));
282
- } catch (error) {
283
- const detail = error instanceof Error ? error.message : String(error);
216
+ // ONE workflow-state snapshot for the whole gate (run identity + the freshness marker +
217
+ // dream detection), read with error distinction: unreadable state fails closed, never
218
+ // "non-dream".
219
+ const session = recovery.readSession();
220
+ if (session.kind === "unreadable") {
284
221
  return {
285
222
  kind: "refuse",
286
223
  errorType: "bad_state",
287
- detail: `session workflow state is unreadable — cannot resolve the dream_report gate: ${detail}`,
224
+ detail: `session workflow state is unreadable — cannot resolve the dream_report gate: ${session.detail}`,
288
225
  };
289
226
  }
290
- const runId = typeof state.run_id === "string" && state.run_id.length > 0 ? state.run_id : null;
291
- const dream =
292
- runId !== null && existsSync(join(runScratchDir(ctx.cwd, runId), DREAM_MANIFEST_FILENAME));
293
- if (runId === null || !dream) {
227
+ const runId = session.runId;
228
+ if (runId === null || !session.dream) {
294
229
  if (input === undefined) return { kind: "absent" };
295
230
  return {
296
231
  kind: "refuse",
@@ -309,17 +244,25 @@ export function resolveDreamReportGate(
309
244
  "report review as one bundle",
310
245
  };
311
246
  }
312
- const recovered = recoverDreamReportContext(ctx, runId, state.dream_bundle_digest, generatedAt);
247
+ // The marker rides the SAME snapshot as the run identity (the one-snapshot rule); the
248
+ // capability re-executes the full read+decode+digest ladder on every call, never caching.
249
+ const recovered = recovery.recoverContext(runId, session.marker);
313
250
  if (!recovered.ok) {
314
251
  return { kind: "refuse", errorType: "bad_state", detail: recovered.detail };
315
252
  }
253
+ const context: DreamReportContext = {
254
+ manifest: recovered.manifest,
255
+ analyses: recovered.analyses,
256
+ reducers: recovered.reducers,
257
+ run_id: runId,
258
+ generated_at: generatedAt,
259
+ };
316
260
  // The revalidation-bracket re-check (contracts.md §8.65): the manifest — with its stamped
317
261
  // commit_sha — is now decoded and authenticated, so re-prove HEAD-unchanged + tree-clean
318
262
  // against it. Both `writeObjectiveDraft` and `saveObjective` flow through this resolver, so
319
263
  // the bracket re-fires at draft-write AND save; non-dream paths never reach it (the matrix
320
- // above returned already). The parameter default is the production bracket — tests inject
321
- // stubs.
322
- const drift = bracket(ctx.cwd, recovered.context.manifest.commit_sha);
264
+ // above returned already).
265
+ const drift = recovery.bracket(context.manifest.commit_sha);
323
266
  if (!drift.ok) {
324
267
  return {
325
268
  kind: "refuse",
@@ -329,7 +272,7 @@ export function resolveDreamReportGate(
329
272
  "stale; re-run perk learn dream",
330
273
  };
331
274
  }
332
- const built = buildDreamReport(input, recovered.context);
275
+ const built = buildDreamReport(input, context);
333
276
  if (!built.ok) {
334
277
  return { kind: "refuse", errorType: "invalid_input", detail: built.details.join("\n") };
335
278
  }