@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,468 @@
1
+ // The objective-refinement grounding CONTEXT: the fixed transfer artifact Python prepares
2
+ // (`perk objective refine` / `perk objective refine-context`) and the interior imports, reads
3
+ // and binds drafts to (contracts.md §8.67).
4
+ //
5
+ // Byte ownership: Python is the SOLE serializer of this artifact. The interior validates the
6
+ // raw string's digest and decodes it STRICTLY for use, but every write is the UNCHANGED raw
7
+ // string — never `JSON.stringify(parsed)`, never a trim or newline normalization. Readers hand
8
+ // back the validated fields together with the raw bytes and their digest so no consumer ever
9
+ // serializes the context a second time; the draft's `context_digest` is exactly that digest.
10
+ //
11
+ // Provenance is a capture-time observation, not a code-freshness gate: the interior renders it
12
+ // as such and never refreshes it.
13
+
14
+ import type {
15
+ ReadArtifactResult,
16
+ SessionArtifactReceipt,
17
+ WorkflowSession,
18
+ } from "../../session/workflowSession.ts";
19
+ import { digestSessionData, REFINEMENT_CONTEXT_ARTIFACT } from "../../session/workflowSession.ts";
20
+
21
+ /** The registry stage id of the refinement session (the isolated, read-only stage). */
22
+ export const REFINE_STAGE = "objective-refine";
23
+
24
+ /** The fixed context artifact name — the SAME name in run scratch (cold) and session data. */
25
+ export { REFINEMENT_CONTEXT_ARTIFACT };
26
+
27
+ /** The handoff namespace the cold door carries (never a top-level objective_id/node_id). */
28
+ export const REFINEMENT_HANDOFF_KEY = "objective_refinement";
29
+
30
+ export interface RefinementCodeBasis {
31
+ head_sha: string;
32
+ dirty: boolean;
33
+ captured_at: string;
34
+ }
35
+
36
+ export interface RefinementProvenance {
37
+ authoring_run_id: string;
38
+ authored_at: string;
39
+ code_basis: RefinementCodeBasis;
40
+ }
41
+
42
+ export interface RefinementIdentity {
43
+ backend: string;
44
+ objective_id: string;
45
+ objective_run_id: string;
46
+ node_id: string;
47
+ carrier_id: string;
48
+ }
49
+
50
+ export interface RefinementSource {
51
+ description: string;
52
+ slug: string | null;
53
+ comment: string | null;
54
+ depends_on: string[] | null;
55
+ effective_depends_on: string[];
56
+ issue_description: string;
57
+ }
58
+
59
+ export interface RefinementTarget {
60
+ identity: RefinementIdentity;
61
+ source: RefinementSource;
62
+ source_digest: string;
63
+ carrier_identifier: string;
64
+ carrier_url: string;
65
+ status: string;
66
+ plan_ref: string | null;
67
+ has_plan_metadata: boolean;
68
+ }
69
+
70
+ export interface RefinementPrior {
71
+ markdown: string;
72
+ source_digest: string;
73
+ provenance: RefinementProvenance;
74
+ saved_at: string;
75
+ }
76
+
77
+ /** The validated context fields (the TS mirror of Python's `RefinementContext`). */
78
+ export interface RefinementContext {
79
+ schema_version: 1;
80
+ run_id: string;
81
+ target: RefinementTarget;
82
+ expected: { comment_id: string | null; body_digest: string | null };
83
+ provenance: RefinementProvenance;
84
+ objective: { id: string; title: string; url: string };
85
+ prior: RefinementPrior | null;
86
+ engagement: string;
87
+ warnings: string[];
88
+ }
89
+
90
+ /** The validated fields TOGETHER with the exact bytes they came from. */
91
+ export interface RefinementContextRead {
92
+ context: RefinementContext;
93
+ raw: string;
94
+ digest: string;
95
+ }
96
+
97
+ export type DecodeRefinementContextResult =
98
+ | { ok: true; context: RefinementContext }
99
+ | { ok: false; problem: string };
100
+
101
+ // ------------------------------------------------------------------------- strict decoding
102
+
103
+ class Refuse extends Error {}
104
+
105
+ function refuse(path: string, why: string): never {
106
+ throw new Refuse(`${path} ${why}`);
107
+ }
108
+
109
+ function obj(value: unknown, path: string, keys: readonly string[]): Record<string, unknown> {
110
+ if (typeof value !== "object" || value === null || Array.isArray(value))
111
+ refuse(path, "is not an object");
112
+ const record = value as Record<string, unknown>;
113
+ const present = Object.keys(record);
114
+ for (const key of keys) if (!(key in record)) refuse(`${path}.${key}`, "is missing");
115
+ for (const key of present) if (!keys.includes(key)) refuse(`${path}.${key}`, "is unknown");
116
+ return record;
117
+ }
118
+
119
+ function str(value: unknown, path: string): string {
120
+ if (typeof value !== "string") refuse(path, "is not a string");
121
+ return value;
122
+ }
123
+
124
+ function nonblank(value: unknown, path: string): string {
125
+ const s = str(value, path);
126
+ if (!s.trim()) refuse(path, "is blank");
127
+ return s;
128
+ }
129
+
130
+ function nullableStr(value: unknown, path: string): string | null {
131
+ return value === null ? null : str(value, path);
132
+ }
133
+
134
+ function bool(value: unknown, path: string): boolean {
135
+ if (typeof value !== "boolean") refuse(path, "is not a boolean");
136
+ return value;
137
+ }
138
+
139
+ function strList(value: unknown, path: string): string[] {
140
+ if (!Array.isArray(value)) refuse(path, "is not a list");
141
+ return value.map((item, i) => str(item, `${path}[${i}]`));
142
+ }
143
+
144
+ const HEX40 = /^[0-9a-f]{40}$/;
145
+ const HEX64 = /^[0-9a-f]{64}$/;
146
+ const SAFE_ARTIFACT_DIGEST = /^sha256:[0-9a-f]{64}$/;
147
+
148
+ function bareDigest(value: unknown, path: string): string {
149
+ const s = str(value, path);
150
+ if (!HEX64.test(s)) refuse(path, "is not a 64-char lowercase hex digest");
151
+ return s;
152
+ }
153
+
154
+ function provenanceOf(value: unknown, path: string): RefinementProvenance {
155
+ const p = obj(value, path, ["authoring_run_id", "authored_at", "code_basis"]);
156
+ const cb = obj(p.code_basis, `${path}.code_basis`, ["head_sha", "dirty", "captured_at"]);
157
+ const head = str(cb.head_sha, `${path}.code_basis.head_sha`);
158
+ if (!HEX40.test(head)) refuse(`${path}.code_basis.head_sha`, "is not a 40-char lowercase sha");
159
+ return {
160
+ authoring_run_id: nonblank(p.authoring_run_id, `${path}.authoring_run_id`),
161
+ authored_at: nonblank(p.authored_at, `${path}.authored_at`),
162
+ code_basis: {
163
+ head_sha: head,
164
+ dirty: bool(cb.dirty, `${path}.code_basis.dirty`),
165
+ captured_at: nonblank(cb.captured_at, `${path}.code_basis.captured_at`),
166
+ },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Strictly decode transferred context bytes for USE (validation only — the artifact bytes are
172
+ * never rebuilt from this value). Every defect — malformed JSON, a non-object, unknown or
173
+ * missing keys at any level, mistyped scalars, an unknown `schema_version`, blank identity
174
+ * fields, a non-canonical digest, an inconsistent expectation — is a classified refusal
175
+ * carrying the exact problem. Never throws.
176
+ */
177
+ export function decodeRefinementContext(raw: string): DecodeRefinementContextResult {
178
+ let parsed: unknown;
179
+ try {
180
+ parsed = JSON.parse(raw);
181
+ } catch {
182
+ return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} is not valid JSON` };
183
+ }
184
+ try {
185
+ const c = obj(parsed, "context", [
186
+ "schema_version",
187
+ "run_id",
188
+ "target",
189
+ "expected",
190
+ "provenance",
191
+ "objective",
192
+ "prior",
193
+ "engagement",
194
+ "warnings",
195
+ ]);
196
+ if (c.schema_version !== 1) refuse("context.schema_version", "is not 1");
197
+ const t = obj(c.target, "context.target", [
198
+ "identity",
199
+ "source",
200
+ "source_digest",
201
+ "carrier_identifier",
202
+ "carrier_url",
203
+ "status",
204
+ "plan_ref",
205
+ "has_plan_metadata",
206
+ ]);
207
+ const id = obj(t.identity, "context.target.identity", [
208
+ "backend",
209
+ "objective_id",
210
+ "objective_run_id",
211
+ "node_id",
212
+ "carrier_id",
213
+ ]);
214
+ const s = obj(t.source, "context.target.source", [
215
+ "description",
216
+ "slug",
217
+ "comment",
218
+ "depends_on",
219
+ "effective_depends_on",
220
+ "issue_description",
221
+ ]);
222
+ const expected = obj(c.expected, "context.expected", ["comment_id", "body_digest"]);
223
+ const commentId = nullableStr(expected.comment_id, "context.expected.comment_id");
224
+ const bodyDigest = nullableStr(expected.body_digest, "context.expected.body_digest");
225
+ if ((commentId === null) !== (bodyDigest === null))
226
+ refuse("context.expected", "must carry both comment_id and body_digest, or neither");
227
+ if (commentId !== null && !commentId.trim()) refuse("context.expected.comment_id", "is blank");
228
+ if (bodyDigest !== null && !HEX64.test(bodyDigest))
229
+ refuse("context.expected.body_digest", "is not a 64-char lowercase hex digest");
230
+ const o = obj(c.objective, "context.objective", ["id", "title", "url"]);
231
+ let prior: RefinementPrior | null = null;
232
+ if (c.prior !== null) {
233
+ const p = obj(c.prior, "context.prior", [
234
+ "markdown",
235
+ "source_digest",
236
+ "provenance",
237
+ "saved_at",
238
+ ]);
239
+ prior = {
240
+ markdown: nonblank(p.markdown, "context.prior.markdown"),
241
+ source_digest: bareDigest(p.source_digest, "context.prior.source_digest"),
242
+ provenance: provenanceOf(p.provenance, "context.prior.provenance"),
243
+ saved_at: str(p.saved_at, "context.prior.saved_at"),
244
+ };
245
+ }
246
+ const context: RefinementContext = {
247
+ schema_version: 1,
248
+ run_id: nonblank(c.run_id, "context.run_id"),
249
+ target: {
250
+ identity: {
251
+ backend: nonblank(id.backend, "context.target.identity.backend"),
252
+ objective_id: nonblank(id.objective_id, "context.target.identity.objective_id"),
253
+ objective_run_id: nonblank(
254
+ id.objective_run_id,
255
+ "context.target.identity.objective_run_id",
256
+ ),
257
+ node_id: nonblank(id.node_id, "context.target.identity.node_id"),
258
+ carrier_id: nonblank(id.carrier_id, "context.target.identity.carrier_id"),
259
+ },
260
+ source: {
261
+ description: str(s.description, "context.target.source.description"),
262
+ slug: nullableStr(s.slug, "context.target.source.slug"),
263
+ comment: nullableStr(s.comment, "context.target.source.comment"),
264
+ depends_on:
265
+ s.depends_on === null
266
+ ? null
267
+ : strList(s.depends_on, "context.target.source.depends_on"),
268
+ effective_depends_on: strList(
269
+ s.effective_depends_on,
270
+ "context.target.source.effective_depends_on",
271
+ ),
272
+ issue_description: str(s.issue_description, "context.target.source.issue_description"),
273
+ },
274
+ source_digest: bareDigest(t.source_digest, "context.target.source_digest"),
275
+ carrier_identifier: nonblank(t.carrier_identifier, "context.target.carrier_identifier"),
276
+ carrier_url: str(t.carrier_url, "context.target.carrier_url"),
277
+ status: nonblank(t.status, "context.target.status"),
278
+ plan_ref: nullableStr(t.plan_ref, "context.target.plan_ref"),
279
+ has_plan_metadata: bool(t.has_plan_metadata, "context.target.has_plan_metadata"),
280
+ },
281
+ expected: { comment_id: commentId, body_digest: bodyDigest },
282
+ provenance: provenanceOf(c.provenance, "context.provenance"),
283
+ objective: {
284
+ id: nonblank(o.id, "context.objective.id"),
285
+ title: str(o.title, "context.objective.title"),
286
+ url: str(o.url, "context.objective.url"),
287
+ },
288
+ prior,
289
+ engagement: str(c.engagement, "context.engagement"),
290
+ warnings: strList(c.warnings, "context.warnings"),
291
+ };
292
+ return { ok: true, context };
293
+ } catch (error) {
294
+ if (error instanceof Refuse)
295
+ return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} ${error.message}` };
296
+ return { ok: false, problem: `${REFINEMENT_CONTEXT_ARTIFACT} could not be decoded` };
297
+ }
298
+ }
299
+
300
+ /** Whether a string is the `sha256:<64 lowercase hex>` session-data digest shape. */
301
+ export function isArtifactDigest(value: string): boolean {
302
+ return SAFE_ARTIFACT_DIGEST.test(value);
303
+ }
304
+
305
+ // --------------------------------------------------------------------------- the transfer
306
+
307
+ export type ValidateContextTransferResult =
308
+ | { ok: true; read: RefinementContextRead }
309
+ | { ok: false; problem: string };
310
+
311
+ /**
312
+ * Validate a raw transferred context string against its declared digest and the session's run:
313
+ * the digest is computed over the EXACT string (final LF included), the shape is decoded
314
+ * strictly, and `run_id` must be the current run. Parsing is validation only — the returned
315
+ * `raw` is the caller's unchanged string. Never throws.
316
+ */
317
+ export function validateContextTransfer(
318
+ raw: string,
319
+ opts: { runId: string; expectedDigest?: string },
320
+ ): ValidateContextTransferResult {
321
+ const digest = digestSessionData(raw);
322
+ if (opts.expectedDigest !== undefined) {
323
+ if (!isArtifactDigest(opts.expectedDigest))
324
+ return { ok: false, problem: "the declared context digest is not a sha256: digest" };
325
+ if (opts.expectedDigest !== digest)
326
+ return { ok: false, problem: "the transferred context bytes do not match their digest" };
327
+ }
328
+ const decoded = decodeRefinementContext(raw);
329
+ if (!decoded.ok) return decoded;
330
+ if (decoded.context.run_id !== opts.runId)
331
+ return {
332
+ ok: false,
333
+ problem: `the transferred context belongs to run ${JSON.stringify(decoded.context.run_id)}, not this run`,
334
+ };
335
+ return { ok: true, read: { context: decoded.context, raw, digest } };
336
+ }
337
+
338
+ export type ImportRefinementContextResult =
339
+ | { status: "imported"; receipt: SessionArtifactReceipt; read: RefinementContextRead }
340
+ | { status: "unchanged"; receipt: SessionArtifactReceipt; read: RefinementContextRead }
341
+ | { status: "rejected"; problem: string }
342
+ | { status: "unverified"; problem: string };
343
+
344
+ /**
345
+ * Persist a validated transfer as the session's context artifact — the UNCHANGED raw string
346
+ * through the strict session write. `unchanged` is the byte-identical short-circuit (a
347
+ * re-prepared context whose bytes happen to be identical is no new context). Never throws.
348
+ */
349
+ export function importRefinementContext(
350
+ session: WorkflowSession,
351
+ read: RefinementContextRead,
352
+ ): ImportRefinementContextResult {
353
+ const written = session.writeArtifact(REFINEMENT_CONTEXT_ARTIFACT, read.raw, {
354
+ provenance: "strict",
355
+ });
356
+ switch (written.status) {
357
+ case "applied":
358
+ return { status: "imported", receipt: written.receipt, read };
359
+ case "unchanged":
360
+ return { status: "unchanged", receipt: written.receipt, read };
361
+ case "rejected":
362
+ return { status: "rejected", problem: written.problem };
363
+ case "unverified":
364
+ return { status: "unverified", problem: written.problem };
365
+ }
366
+ }
367
+
368
+ export type ColdImportResult =
369
+ | { status: "imported" | "unchanged"; read: RefinementContextRead }
370
+ | { status: "not-applicable" }
371
+ | { status: "refused"; problem: string };
372
+
373
+ /** The transfer read port the edge binds to the run's fixed scratch path (`null` = missing). */
374
+ export interface ColdImportPorts {
375
+ readTransfer(): string | null;
376
+ }
377
+
378
+ /**
379
+ * The cold claim's ONE-TIME context import (contracts.md §8.67): on the actual `objective-refine`
380
+ * cold claim, read the fixed run-scratch transfer the door materialized (through the edge-bound
381
+ * port), validate its declared digest (from the namespaced handoff block), run, stage and strict
382
+ * shape, and write the exact raw string as the session artifact. `not-applicable` for every
383
+ * non-refinement claim; every defect is a fail-closed `refused` (the session stays read-only and
384
+ * unusable for drafting — never an orphan repair, missing-pointer reimport or target refresh).
385
+ * Never throws.
386
+ */
387
+ export function importColdRefinementContext(
388
+ session: WorkflowSession,
389
+ claim: { runId: string; stage: string | undefined; handoff: Record<string, unknown> },
390
+ ports: ColdImportPorts,
391
+ ): ColdImportResult {
392
+ if (claim.stage !== REFINE_STAGE) return { status: "not-applicable" };
393
+ const refuse = (problem: string): ColdImportResult => ({ status: "refused", problem });
394
+ const identity = session.currentRunIdentity();
395
+ if (!identity.ok) return refuse(`session identity ${identity.reason}`);
396
+ if (identity.runId !== claim.runId) return refuse("the claimed run is not the session's run");
397
+ const block = claim.handoff[REFINEMENT_HANDOFF_KEY];
398
+ if (typeof block !== "object" || block === null || Array.isArray(block))
399
+ return refuse(`the handoff carries no ${REFINEMENT_HANDOFF_KEY} block`);
400
+ const declared = (block as Record<string, unknown>).context_digest;
401
+ if (typeof declared !== "string" || !isArtifactDigest(declared))
402
+ return refuse("the handoff's context_digest is not a sha256: digest");
403
+ let raw: string | null;
404
+ try {
405
+ raw = ports.readTransfer();
406
+ } catch (error) {
407
+ return refuse(`could not read the context transfer (${String(error)})`);
408
+ }
409
+ if (raw === null) return refuse("the context transfer is missing from the run's scratch");
410
+ const validated = validateContextTransfer(raw, { runId: claim.runId, expectedDigest: declared });
411
+ if (!validated.ok) return refuse(validated.problem);
412
+ const imported = importRefinementContext(session, validated.read);
413
+ switch (imported.status) {
414
+ case "imported":
415
+ case "unchanged":
416
+ return { status: imported.status, read: imported.read };
417
+ case "rejected":
418
+ case "unverified":
419
+ return refuse(`could not persist the context artifact (${imported.problem})`);
420
+ }
421
+ }
422
+
423
+ /** The classified context resume: `absent` is the genuine no-context arm; `refused` is a
424
+ * fail-closed STOP (a corrupt/orphan/foreign artifact never falls back to anything). */
425
+ export type ResumeRefinementContextResult =
426
+ | { kind: "valid"; read: RefinementContextRead }
427
+ | { kind: "absent" }
428
+ | { kind: "refused"; problem: string };
429
+
430
+ /**
431
+ * Strictly resume the session's context artifact: the strict seam read (an orphan pointer or a
432
+ * foreign/wrong-run artifact is `invalid` there), the strict decode, and the run check against
433
+ * the session identity. Returns the raw bytes + digest beside the fields. Never throws.
434
+ */
435
+ export function resumeRefinementContext(session: WorkflowSession): ResumeRefinementContextResult {
436
+ const identity = session.currentRunIdentity();
437
+ if (!identity.ok) return { kind: "refused", problem: `session identity ${identity.reason}` };
438
+ const read: ReadArtifactResult = session.readArtifact(REFINEMENT_CONTEXT_ARTIFACT, {
439
+ provenance: "strict",
440
+ });
441
+ if (read.status === "absent") return { kind: "absent" };
442
+ if (read.status === "invalid") return { kind: "refused", problem: read.problem };
443
+ const validated = validateContextTransfer(read.content, { runId: identity.runId });
444
+ if (!validated.ok) return { kind: "refused", problem: validated.problem };
445
+ return { kind: "valid", read: validated.read };
446
+ }
447
+
448
+ /** The one-line target summary every refinement surface relays (never the full context). */
449
+ export function describeRefinementTarget(context: RefinementContext): string {
450
+ const t = context.target;
451
+ return (
452
+ `objective ${context.objective.id} node ${t.identity.node_id} (${t.status}; ` +
453
+ `${t.carrier_identifier}${context.prior !== null ? "; re-refining a prior refinement" : ""})`
454
+ );
455
+ }
456
+
457
+ /**
458
+ * The capture-time observation line — the ONLY way the interior labels code provenance
459
+ * (contracts.md §8.67): a dated observation, never "verified", "frozen" or "current" code.
460
+ */
461
+ export function checkoutObservationLine(provenance: RefinementProvenance): string {
462
+ const cb = provenance.code_basis;
463
+ return (
464
+ `Checkout observation captured at ${cb.captured_at}: HEAD ${cb.head_sha}, dirty ${cb.dirty}; ` +
465
+ "uncommitted files were not snapshotted and later checkout changes are not detected. " +
466
+ "This is not a freshness guarantee."
467
+ );
468
+ }