@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
@@ -1,4 +1,4 @@
1
- // The session-data accessor seam (contracts.md §8.1).
1
+ // The session-data accessor seam (contracts.md §8.1) — RAW PRIMITIVES ONLY.
2
2
  //
3
3
  // Every run-scoped session artifact lives under `.perk/workflow/scratch/runs/<run_id>/data/`, and
4
4
  // ALL session-data paths flow through this module (interior) or `perk/state/cache.py` (exterior) — the
@@ -6,59 +6,68 @@
6
6
  // `scratch`/`runs` path segments anywhere else.
7
7
  //
8
8
  // Seam doctrine — degrade gracefully, never invent identity:
9
- // - The current run_id resolves from the rebuilt `perk:workflow-state` and degrades to `null`
10
- // when the session has no identity. CONTRAST with `coldDoor.activeRunId`, which falls back to
11
- // a `cold-door-<ts>` stamp for stdin-staging debuggability: a stamp here would orphan data
12
- // dirs and break run_id-keyed provenance, so this seam never stamps.
13
- // - Reads return `null` on absence (normal, branchable) and on I/O errors (with a loud stderr
14
- // warning); writes return the written path or `null` on failure (with a warning). Never
15
- // throws a broken disk must not wedge a session.
9
+ // - The current run_id resolves from the rebuilt `perk:workflow-state`, narrowed non-empty AND
10
+ // safe as a path component (`isSafeRunId` a hostile rebuilt id degrades to no-identity
11
+ // before any path derivation), and degrades to `null` when the session has no identity.
12
+ // CONTRAST with `coldDoor.activeRunId`, which falls back to a `cold-door-<ts>` stamp for
13
+ // stdin-staging debuggability: a stamp here would orphan data dirs and break run_id-keyed
14
+ // provenance, so this seam never stamps.
15
+ // - Ordinary reads return `null` on absence and I/O errors (with a loud stderr warning);
16
+ // opt-in strict reads classify ENOENT separately and refuse unsafe namespaces. Writes return
17
+ // the written path or `null` on failure (with a warning). Content operations never throw —
18
+ // a broken disk must not wedge a session.
19
+ // - The file primitives (`readSessionData`/`writeSessionData`/`ensureSessionDataDir`) take an
20
+ // EXPLICIT run id — identity is resolved ONCE (by the session engine, or here via
21
+ // `activeSessionRunId`) and passed down, so two independent identity reads can never
22
+ // disagree about which run's storage an operation touches.
16
23
  //
17
- // Provenance doctrine (contracts §8.1/§8.3) the pointer makes it consumable:
18
- // - A session artifact is *consumable* only via its `session_artifacts` pointer
19
- // ({run_id, name, path, digest, at}) in the rebuilt `perk:workflow-state`. A bare file on
20
- // disk is never trusted: `writeSessionArtifact` returns a path only once BOTH the file and
21
- // the pointer landed; `readSessionArtifact` validates the on-disk bytes against the rebuilt
22
- // pointer and fails open to `null` when validation refuses.
23
- // - Validation always derives the path from `run_id` + `name` through the seam; the recorded
24
- // `pointer.path` is informational/debugging only and is never dereferenced (workflow-state
25
- // entries are reconstructable from untrusted session history).
26
- // - The four lifecycle guarantees: REWIND ⇒ the rebuilt branch carries an older pointer while
27
- // disk holds newer bytes ⇒ digest mismatch ⇒ refusal. FORK ⇒ the child run_id no longer
28
- // matches the inherited pointer's ⇒ silent refusal (no inheritance; fresh dir).
29
- // RELOAD/COMPACTION ⇒ same run_id ⇒ pointer + dir persist. CONCURRENT SESSIONS ⇒ run_id
30
- // keying isolates dirs and pointers alike — validation never crosses run_ids.
24
+ // The ARTIFACT DISCIPLINE (provenance pointers, digest validation, the classified write/read
25
+ // tiers contracts §8.1/§8.3) lives in the session engine, `session/workflowSession.ts`: this
26
+ // module supplies only the file mechanics its production binding
27
+ // (`session/branchWorkflowSession.ts`) builds its content port from. A bare file on disk is
28
+ // never trusted consumers go through the `WorkflowSession` seam.
31
29
  //
32
30
  // Imports only node builtins + cache.ts + workflowState.ts + report.ts so the module stays
33
31
  // loadable under `node --test`; accepts a minimal structural ctx (`BranchSource & { cwd }`).
34
32
 
35
33
  import { createHash } from "node:crypto";
36
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
37
- import { join, relative } from "node:path";
34
+ import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
35
+ import { join } from "node:path";
38
36
  import type { ReportTarget } from "../surfaces/report.ts";
39
- import { atomicWriteFileSync, ensureRunScratch, sessionDataDir } from "./cache.ts";
40
37
  import {
41
- appendWorkflowState,
42
- type BranchSource,
43
- branchOf,
44
- type EntrySink,
45
- rebuildWorkflowState,
46
- type SessionArtifactPointer,
47
- } from "./workflowState.ts";
38
+ atomicWriteFileSync,
39
+ canonicalSessionDataDir,
40
+ ensureRunScratch,
41
+ isSafeRunId,
42
+ sessionDataDir,
43
+ } from "./cache.ts";
44
+ import { type BranchSource, branchOf, rebuildWorkflowState } from "./workflowState.ts";
45
+
46
+ export { canonicalSessionDataDir } from "./cache.ts";
48
47
 
49
48
  /** Minimal context slice — `ExtensionContext` satisfies it (the `BranchSource` precedent). */
50
49
  export interface SessionDataCtx extends BranchSource {
51
50
  cwd: string;
52
51
  }
53
52
 
53
+ /**
54
+ * The composed context the session engine's production binding needs (`SessionDataCtx` for
55
+ * paths/branch + `ReportTarget` for the strict-append seam's loud failure reporting). Exported
56
+ * so `session/` consumes the reporting slice THROUGH this seam without importing `surfaces/`
57
+ * directly.
58
+ */
59
+ export type SessionArtifactCtx = SessionDataCtx & ReportTarget;
60
+
54
61
  /**
55
62
  * The current session's run_id from the rebuilt workflow-state; `null` when the session has no
56
- * identity (no stamp fallback — see the header).
63
+ * identity (no stamp fallback — see the header) OR when the rebuilt id is unsafe as a path
64
+ * component (the read-path trust boundary: `isSafeRunId` — an unsafe persisted id must never
65
+ * reach a path derivation or a receipt).
57
66
  */
58
67
  export function activeSessionRunId(ctx: SessionDataCtx): string | null {
59
68
  try {
60
69
  const runId = rebuildWorkflowState(branchOf(ctx)).run_id;
61
- if (typeof runId === "string" && runId.length > 0) return runId;
70
+ if (typeof runId === "string" && isSafeRunId(runId)) return runId;
62
71
  } catch {
63
72
  // a throwing getBranch means no resolvable identity — degrade to null
64
73
  }
@@ -72,13 +81,16 @@ export function activeSessionDataDir(ctx: SessionDataCtx): string | null {
72
81
  return sessionDataDir(ctx.cwd, runId);
73
82
  }
74
83
 
75
- /** Ensure the validated run root, then its data dir; `null` + a warning on failure. */
76
- export function ensureSessionDataDir(ctx: SessionDataCtx): string | null {
77
- const runId = activeSessionRunId(ctx);
78
- if (runId === null) return null;
79
- const dir = sessionDataDir(ctx.cwd, runId);
84
+ /**
85
+ * Ensure the validated run root, then its data dir, for an EXPLICIT run id; `null` + a warning
86
+ * on failure (an unsafe id is refused loudly by the write path's `ensureRunScratch`). The run
87
+ * identity is the CALLER's: the session engine passes its one validated id so storage, pointer,
88
+ * and receipt can never disagree.
89
+ */
90
+ export function ensureSessionDataDir(cwd: string, runId: string): string | null {
91
+ const dir = sessionDataDir(cwd, runId);
80
92
  try {
81
- ensureRunScratch(ctx.cwd, runId);
93
+ ensureRunScratch(cwd, runId);
82
94
  mkdirSync(dir, { recursive: true });
83
95
  } catch (error) {
84
96
  console.error(`perk: warning: could not create session data dir ${dir}: ${error}`);
@@ -88,13 +100,11 @@ export function ensureSessionDataDir(ctx: SessionDataCtx): string | null {
88
100
  }
89
101
 
90
102
  /**
91
- * Read a session-data file; `null` on no run_id or an absent file (normal, branchable), and on
92
- * read errors (with a stderr warning). Never throws.
103
+ * Read a run's session-data file; `null` on an absent file (normal, branchable) and on read
104
+ * errors (with a stderr warning). Never throws.
93
105
  */
94
- export function readSessionData(ctx: SessionDataCtx, name: string): string | null {
95
- const dir = activeSessionDataDir(ctx);
96
- if (dir === null) return null;
97
- const path = join(dir, name);
106
+ export function readSessionData(cwd: string, runId: string, name: string): string | null {
107
+ const path = join(sessionDataDir(cwd, runId), name);
98
108
  if (!existsSync(path)) return null;
99
109
  try {
100
110
  return readFileSync(path, "utf8");
@@ -104,16 +114,39 @@ export function readSessionData(ctx: SessionDataCtx, name: string): string | nul
104
114
  }
105
115
  }
106
116
 
117
+ export type SessionDataRead =
118
+ | { status: "found"; content: string }
119
+ | { status: "absent" }
120
+ | { status: "io-error" };
121
+
122
+ /** Strict content port: only ENOENT is absent; unsafe redirects and non-files are I/O refusals. */
123
+ export function readSessionDataStrict(cwd: string, runId: string, name: string): SessionDataRead {
124
+ try {
125
+ if (!isSafeRunId(name)) return { status: "io-error" };
126
+ const dir = canonicalSessionDataDir(cwd, runId, { create: false });
127
+ if (dir === null) return { status: "absent" };
128
+ const path = join(dir, name);
129
+ if (!lstatSync(path).isFile()) return { status: "io-error" };
130
+ return { status: "found", content: readFileSync(path, "utf8") };
131
+ } catch (error) {
132
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
133
+ return { status: "absent" };
134
+ }
135
+ return { status: "io-error" };
136
+ }
137
+ }
138
+
107
139
  /**
108
- * Write a session-data file (creating the data dir lazily); returns the absolute path, or
109
- * `null` + a stderr warning on any failure. Never throws.
140
+ * Write a run's session-data file (creating the data dir lazily); returns the absolute path,
141
+ * or `null` + a stderr warning on any failure. Never throws.
110
142
  */
111
143
  export function writeSessionData(
112
- ctx: SessionDataCtx,
144
+ cwd: string,
145
+ runId: string,
113
146
  name: string,
114
147
  content: string,
115
148
  ): string | null {
116
- const dir = ensureSessionDataDir(ctx);
149
+ const dir = ensureSessionDataDir(cwd, runId);
117
150
  if (dir === null) return null;
118
151
  const path = join(dir, name);
119
152
  try {
@@ -126,106 +159,6 @@ export function writeSessionData(
126
159
  }
127
160
 
128
161
  /** The session-artifact digest convention: `sha256:` + lowercase hex of the UTF-8 bytes. */
129
- export function digestSessionData(content: string): string {
130
- return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
131
- }
132
-
133
- /** Per-name pointer identity: same run_id + same digest (the custom-equals comparator). */
134
- function artifactMapsEqual(
135
- rebuilt: Record<string, SessionArtifactPointer> | null | undefined,
136
- expected: Record<string, SessionArtifactPointer> | null | undefined,
137
- ): boolean {
138
- const a = rebuilt ?? {};
139
- const b = expected ?? {};
140
- const names = Object.keys(b);
141
- if (Object.keys(a).length !== names.length) return false;
142
- return names.every(
143
- (name) => a[name]?.run_id === b[name]?.run_id && a[name]?.digest === b[name]?.digest,
144
- );
145
- }
146
-
147
- /**
148
- * Write a session artifact AND record its provenance pointer in `perk:workflow-state`.
149
- * Returns the absolute written path only when the artifact is *fully recorded* (file written,
150
- * read back, digested, pointer strict-appended); `null` on any failure — the seam/module has
151
- * already warned, and an orphan file (pointer-append failure) is gitignored scratch for the
152
- * GC to prune. Never throws.
153
- */
154
- export function writeSessionArtifact(
155
- sink: EntrySink,
156
- ctx: SessionDataCtx & ReportTarget,
157
- name: string,
158
- content: string,
159
- ): string | null {
160
- const written = writeSessionData(ctx, name, content);
161
- if (written === null) return null; // already warned; never point at an unwritten file
162
-
163
- // Digest the bytes as read back from disk — catches encoding/disk surprises.
164
- const readBack = readSessionData(ctx, name);
165
- if (readBack === null) {
166
- console.error(`perk: warning: session artifact ${written} unreadable after write`);
167
- return null;
168
- }
169
-
170
- const runId = activeSessionRunId(ctx);
171
- if (runId === null) return null; // unreachable after a successful write; belt-and-braces
172
- const pointer: SessionArtifactPointer = {
173
- run_id: runId,
174
- name,
175
- path: relative(ctx.cwd, written),
176
- digest: digestSessionData(readBack),
177
- at: new Date().toISOString(),
178
- };
179
-
180
- // Per-field LWW: each append must carry the WHOLE merged map so sibling artifacts survive.
181
- const merged: Record<string, SessionArtifactPointer> = {
182
- ...(rebuildWorkflowState(branchOf(ctx)).session_artifacts ?? {}),
183
- [name]: pointer,
184
- };
185
- const ok = appendWorkflowState(sink, ctx, {
186
- data: { session_artifacts: merged },
187
- field: "session_artifacts",
188
- expected: merged,
189
- scope: "session-data",
190
- failure: `session_artifacts pointer read-back failed for ${name}`,
191
- equals: artifactMapsEqual,
192
- });
193
- return ok ? written : null;
194
- }
195
-
196
- /**
197
- * Read a session artifact through its provenance pointer; fail-open `null` when validation
198
- * refuses. Tiering: no identity / no pointer / run_id mismatch (the designed fork-isolation
199
- * path) → silent `null`; pointer matches but the file is absent, unreadable, or its digest
200
- * differs (rewind, tamper) → stderr warning + `null`. The path is always DERIVED from
201
- * `run_id` + `name` via the seam — `pointer.path` is never dereferenced. Never throws.
202
- */
203
- export function readSessionArtifact(
204
- ctx: SessionDataCtx,
205
- name: string,
206
- ): { path: string; content: string } | null {
207
- const runId = activeSessionRunId(ctx);
208
- if (runId === null) return null;
209
- let pointer: SessionArtifactPointer | undefined;
210
- try {
211
- pointer = rebuildWorkflowState(branchOf(ctx)).session_artifacts?.[name];
212
- } catch {
213
- return null;
214
- }
215
- if (pointer === undefined) return null;
216
- if (pointer.run_id !== runId) return null; // fork / concurrent isolation — by design, silent
217
-
218
- const path = join(sessionDataDir(ctx.cwd, runId), name);
219
- const content = readSessionData(ctx, name);
220
- if (content === null) {
221
- console.error(`perk: warning: session artifact ${name} has a pointer but no file at ${path}`);
222
- return null;
223
- }
224
- if (digestSessionData(content) !== pointer.digest) {
225
- console.error(
226
- `perk: warning: session artifact ${path} digest mismatch (rewound or modified) — refusing`,
227
- );
228
- return null;
229
- }
230
- return { path, content };
162
+ export function digestSessionData(content: string | Uint8Array): string {
163
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
231
164
  }