harnery 0.22.1 → 0.23.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 (99) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/workflow.d.ts.map +1 -1
  3. package/dist/commands/workflow.js +91 -2
  4. package/dist/core/work/state.d.ts +7 -0
  5. package/dist/core/work/state.d.ts.map +1 -1
  6. package/dist/core/work/state.js +112 -23
  7. package/dist/core/workflow/child-env.d.ts +10 -6
  8. package/dist/core/workflow/child-env.d.ts.map +1 -1
  9. package/dist/core/workflow/child-env.js +15 -7
  10. package/dist/core/workflow/durable-record.d.ts +7 -0
  11. package/dist/core/workflow/durable-record.d.ts.map +1 -0
  12. package/dist/core/workflow/durable-record.js +83 -0
  13. package/dist/core/workflow/engine.d.ts.map +1 -1
  14. package/dist/core/workflow/engine.js +208 -29
  15. package/dist/core/workflow/index.d.ts +2 -0
  16. package/dist/core/workflow/index.d.ts.map +1 -1
  17. package/dist/core/workflow/index.js +1 -0
  18. package/dist/core/workflow/proof.d.ts +4 -0
  19. package/dist/core/workflow/proof.d.ts.map +1 -1
  20. package/dist/core/workflow/proof.js +50 -15
  21. package/dist/core/workflow/run-state.d.ts +3 -0
  22. package/dist/core/workflow/run-state.d.ts.map +1 -1
  23. package/dist/core/workflow/run-state.js +25 -13
  24. package/dist/core/workflow/types.d.ts +10 -0
  25. package/dist/core/workflow/types.d.ts.map +1 -1
  26. package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
  27. package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
  28. package/dist/core/workflow/workspaces/attestation-error.js +25 -0
  29. package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
  30. package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
  31. package/dist/core/workflow/workspaces/cancellation.js +95 -0
  32. package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
  33. package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
  34. package/dist/core/workflow/workspaces/cleanup.js +223 -0
  35. package/dist/core/workflow/workspaces/execution.d.ts +25 -0
  36. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
  37. package/dist/core/workflow/workspaces/execution.js +316 -0
  38. package/dist/core/workflow/workspaces/git.d.ts +25 -0
  39. package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
  40. package/dist/core/workflow/workspaces/git.js +102 -0
  41. package/dist/core/workflow/workspaces/index.d.ts +15 -0
  42. package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
  43. package/dist/core/workflow/workspaces/index.js +7 -0
  44. package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
  45. package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
  46. package/dist/core/workflow/workspaces/inspect.js +255 -0
  47. package/dist/core/workflow/workspaces/integration.d.ts +24 -0
  48. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  49. package/dist/core/workflow/workspaces/integration.js +617 -0
  50. package/dist/core/workflow/workspaces/leases.d.ts +37 -0
  51. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/leases.js +380 -0
  53. package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
  54. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
  55. package/dist/core/workflow/workspaces/lifecycle.js +622 -0
  56. package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
  57. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
  58. package/dist/core/workflow/workspaces/local-git.js +1474 -0
  59. package/dist/core/workflow/workspaces/paths.d.ts +23 -0
  60. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
  61. package/dist/core/workflow/workspaces/paths.js +191 -0
  62. package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
  63. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
  64. package/dist/core/workflow/workspaces/recovery.js +50 -0
  65. package/dist/core/workflow/workspaces/state.d.ts +60 -0
  66. package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
  67. package/dist/core/workflow/workspaces/state.js +296 -0
  68. package/dist/core/workflow/workspaces/types.d.ts +445 -0
  69. package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
  70. package/dist/core/workflow/workspaces/types.js +2 -0
  71. package/dist/core/workflow/workspaces/validate.d.ts +14 -0
  72. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
  73. package/dist/core/workflow/workspaces/validate.js +476 -0
  74. package/package.json +5 -3
  75. package/src/commands/workflow.ts +110 -4
  76. package/src/core/work/state.ts +156 -28
  77. package/src/core/workflow/child-env.ts +18 -7
  78. package/src/core/workflow/durable-record.ts +94 -0
  79. package/src/core/workflow/engine.ts +230 -30
  80. package/src/core/workflow/index.ts +73 -0
  81. package/src/core/workflow/proof.ts +70 -22
  82. package/src/core/workflow/run-state.ts +46 -11
  83. package/src/core/workflow/types.ts +14 -0
  84. package/src/core/workflow/workspaces/attestation-error.ts +30 -0
  85. package/src/core/workflow/workspaces/cancellation.ts +137 -0
  86. package/src/core/workflow/workspaces/cleanup.ts +308 -0
  87. package/src/core/workflow/workspaces/execution.ts +412 -0
  88. package/src/core/workflow/workspaces/git.ts +125 -0
  89. package/src/core/workflow/workspaces/index.ts +85 -0
  90. package/src/core/workflow/workspaces/inspect.ts +421 -0
  91. package/src/core/workflow/workspaces/integration.ts +829 -0
  92. package/src/core/workflow/workspaces/leases.ts +495 -0
  93. package/src/core/workflow/workspaces/lifecycle.ts +792 -0
  94. package/src/core/workflow/workspaces/local-git.ts +1892 -0
  95. package/src/core/workflow/workspaces/paths.ts +247 -0
  96. package/src/core/workflow/workspaces/recovery.ts +56 -0
  97. package/src/core/workflow/workspaces/state.ts +481 -0
  98. package/src/core/workflow/workspaces/types.ts +517 -0
  99. package/src/core/workflow/workspaces/validate.ts +595 -0
@@ -0,0 +1,421 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { readWorkItem } from "../../work/state.ts";
4
+ import { readWorkflowProof } from "../proof.ts";
5
+ import { readWorkflowRunManifest } from "../run-state.ts";
6
+ import type { WorkflowProof } from "../types.ts";
7
+ import { deriveWorkspaceLifecycle, type WorkspaceLifecycleProjection } from "./lifecycle.ts";
8
+ import {
9
+ fileSha256,
10
+ readCleanupAttempts,
11
+ readIntegrationAttempts,
12
+ readWorkflowSupplement,
13
+ readWorkspaceEvents,
14
+ workflowRunDir,
15
+ } from "./state.ts";
16
+ import type {
17
+ IntegrationApplyReceipt,
18
+ IntegrationAuthorization,
19
+ IntegrationPlan,
20
+ WorkspaceAttestation,
21
+ WorkspaceCancellationReceipt,
22
+ WorkspaceCleanupIntent,
23
+ WorkspaceCleanupReceipt,
24
+ WorkspaceCompatibilityExecutionEvidence,
25
+ WorkspaceIsolation,
26
+ WorkspaceProviderRef,
27
+ } from "./types.ts";
28
+ import { isWorkspaceBoundExecutionEvidence } from "./validate.ts";
29
+
30
+ const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
31
+
32
+ export type WorkflowWorkspaceSelection = "shared" | "compatibility" | "isolated";
33
+ export type WorkflowWorkspaceVerification =
34
+ | "not_applicable"
35
+ | "pending"
36
+ | "ok"
37
+ | "blocked"
38
+ | "lost";
39
+
40
+ export interface WorkflowWorkspaceStatus {
41
+ schema_version: 1;
42
+ run_id: string;
43
+ run_name: string;
44
+ work_item_id?: string;
45
+ started_at: string;
46
+ selection: WorkflowWorkspaceSelection;
47
+ requested_isolation: "shared" | WorkspaceIsolation;
48
+ effective_isolation: "shared" | WorkspaceIsolation;
49
+ provider?: WorkspaceProviderRef;
50
+ compatibility?: {
51
+ reason: WorkspaceCompatibilityExecutionEvidence["selection_reason"];
52
+ unsupported: WorkspaceCompatibilityExecutionEvidence["unsupported"];
53
+ unknowns: WorkspaceCompatibilityExecutionEvidence["unknowns"];
54
+ };
55
+ allocation?: {
56
+ binding_id: string;
57
+ workspace_id: string;
58
+ workspace_root: string;
59
+ active_root: string;
60
+ created_at: string;
61
+ };
62
+ lifecycle: WorkspaceLifecycleProjection;
63
+ verification: {
64
+ status: WorkflowWorkspaceVerification;
65
+ workflow_status?: WorkflowProof["run"]["status"];
66
+ attested_at?: string;
67
+ drift: string[];
68
+ unsupported: Array<{ code: string; message: string }>;
69
+ unknowns: Array<{ code: string; message: string }>;
70
+ };
71
+ integration: {
72
+ state: WorkspaceLifecycleProjection["integration_state"];
73
+ changed_paths: string[];
74
+ target_ref?: string;
75
+ target_commit?: string;
76
+ receipt_id?: string;
77
+ };
78
+ cleanup: {
79
+ state: "not_requested" | "pending" | "released" | "preserved_dirty" | "blocked";
80
+ attempts: number;
81
+ reason?: string;
82
+ receipt_id?: string;
83
+ };
84
+ repository: {
85
+ dirty_paths: string[];
86
+ conflicts: string[];
87
+ operations_in_progress: string[];
88
+ };
89
+ integrity: {
90
+ status: "verified";
91
+ proof_sha256?: string;
92
+ provider_event_chain_sha256: string | null;
93
+ };
94
+ }
95
+
96
+ export type WorkflowWorkspaceInspection =
97
+ | { ok: true; value: WorkflowWorkspaceStatus }
98
+ | { ok: false; run_id: string; error: string };
99
+
100
+ interface WorkflowJournalProjectionEvent {
101
+ event: string;
102
+ ok?: boolean;
103
+ status?: "blocked" | "lost" | "cancelled" | "unsupported" | "preserved_dirty";
104
+ work_event_seq?: number;
105
+ work_event_sha256?: string;
106
+ }
107
+
108
+ /**
109
+ * Read one workflow's workspace state from validated durable authority.
110
+ *
111
+ * Corrupt, contradictory, or foreign records throw instead of being projected
112
+ * as healthy. Use inspectWorkflowWorkspace when a UI needs a typed error row.
113
+ */
114
+ export function readWorkflowWorkspaceStatus(
115
+ coordRoot: string,
116
+ runId: string,
117
+ ): WorkflowWorkspaceStatus {
118
+ assertRunId(runId);
119
+ const root = resolve(coordRoot);
120
+ const runDir = workflowRunDir(root, runId);
121
+ const manifest = readWorkflowRunManifest(root, runId);
122
+ const journal = readWorkspaceJournal(runDir, runId);
123
+ const proofPath = join(runDir, "proof.json");
124
+ const proof = existsSync(proofPath) ? readWorkflowProof(root, runId) : undefined;
125
+ const proofSha256 = proof ? fileSha256(proofPath) : undefined;
126
+ const binding = manifest.execution.workspace_binding;
127
+ const fallback = manifest.execution.workspace_fallback;
128
+
129
+ if (!binding) {
130
+ const lifecycle = deriveWorkspaceLifecycle({
131
+ manifest,
132
+ workflow_journal: journal,
133
+ proof,
134
+ proof_sha256: proofSha256,
135
+ allocation_unsupported: fallback !== undefined,
136
+ });
137
+ return {
138
+ schema_version: 1,
139
+ run_id: runId,
140
+ run_name: manifest.name,
141
+ work_item_id: manifest.work_item_id,
142
+ started_at: manifest.started_at,
143
+ selection: fallback ? "compatibility" : "shared",
144
+ requested_isolation: manifest.execution.isolation,
145
+ effective_isolation: "shared",
146
+ provider: fallback?.provider,
147
+ compatibility: fallback
148
+ ? {
149
+ reason: fallback.selection_reason,
150
+ unsupported: [...fallback.unsupported],
151
+ unknowns: [...fallback.unknowns],
152
+ }
153
+ : undefined,
154
+ lifecycle,
155
+ verification: {
156
+ status: "not_applicable",
157
+ workflow_status: proof?.run.status,
158
+ drift: [...(fallback?.drift ?? [])],
159
+ unsupported: [...(fallback?.unsupported ?? [])],
160
+ unknowns: [...(fallback?.unknowns ?? [])],
161
+ },
162
+ integration: { state: "none", changed_paths: [] },
163
+ cleanup: { state: "not_requested", attempts: 0 },
164
+ repository: { dirty_paths: [], conflicts: [], operations_in_progress: [] },
165
+ integrity: {
166
+ status: "verified",
167
+ proof_sha256: proofSha256,
168
+ provider_event_chain_sha256: null,
169
+ },
170
+ };
171
+ }
172
+
173
+ const providerEvents = readWorkspaceEvents(root, binding.provider.id, binding.binding_id);
174
+ const integrationPlan = readWorkflowSupplement<IntegrationPlan>(
175
+ root,
176
+ runId,
177
+ "integration/plan.json",
178
+ );
179
+ const integrationAuthorization = readWorkflowSupplement<IntegrationAuthorization>(
180
+ root,
181
+ runId,
182
+ "integration/authorization.json",
183
+ );
184
+ const integrationAttempts = readIntegrationAttempts(root, runId);
185
+ const integrationReceipt = readWorkflowSupplement<IntegrationApplyReceipt>(
186
+ root,
187
+ runId,
188
+ "integration/receipt.json",
189
+ );
190
+ const cleanupIntent = readWorkflowSupplement<WorkspaceCleanupIntent>(
191
+ root,
192
+ runId,
193
+ "cleanup/intent.json",
194
+ );
195
+ const cleanupAttempts = readCleanupAttempts(root, runId);
196
+ const cleanupReceipt = readWorkflowSupplement<WorkspaceCleanupReceipt>(
197
+ root,
198
+ runId,
199
+ "cleanup/receipt.json",
200
+ );
201
+ const cancellationReceipt = readWorkflowSupplement<WorkspaceCancellationReceipt>(
202
+ root,
203
+ runId,
204
+ "cancellation/outcome.json",
205
+ );
206
+ const workEvents =
207
+ binding.owner.kind === "work_attempt"
208
+ ? readWorkItem(root, binding.owner.work_item_id).events
209
+ : undefined;
210
+
211
+ const lifecycle = deriveWorkspaceLifecycle({
212
+ binding,
213
+ provider_events: providerEvents,
214
+ manifest,
215
+ workflow_journal: journal,
216
+ proof,
217
+ proof_sha256: proofSha256,
218
+ integration_plan: integrationPlan,
219
+ integration_authorization: integrationAuthorization,
220
+ integration_attempts: integrationAttempts,
221
+ integration_receipt: integrationReceipt,
222
+ cleanup_intent: cleanupIntent,
223
+ cleanup_attempts: cleanupAttempts,
224
+ cleanup_receipt: cleanupReceipt,
225
+ work_events: workEvents,
226
+ cancellation_receipt: cancellationReceipt,
227
+ });
228
+ const latestCleanup = cleanupAttempts.at(-1);
229
+ const attestation = latestAttestation(proof, latestCleanup?.attestation, cleanupReceipt);
230
+
231
+ return {
232
+ schema_version: 1,
233
+ run_id: runId,
234
+ run_name: manifest.name,
235
+ work_item_id: manifest.work_item_id,
236
+ started_at: manifest.started_at,
237
+ selection: "isolated",
238
+ requested_isolation: manifest.execution.isolation,
239
+ effective_isolation: binding.isolation,
240
+ provider: binding.provider,
241
+ allocation: {
242
+ binding_id: binding.binding_id,
243
+ workspace_id: binding.workspace_id,
244
+ workspace_root: binding.workspace_root,
245
+ active_root: binding.active_root,
246
+ created_at: binding.created_at,
247
+ },
248
+ lifecycle,
249
+ verification: {
250
+ status: verificationStatus(proof, attestation),
251
+ workflow_status: proof?.run.status,
252
+ attested_at: attestation?.recorded_at,
253
+ drift: [...(attestation?.provider_drift ?? [])],
254
+ unsupported: [...(attestation?.unsupported ?? [])],
255
+ unknowns: [...(attestation?.unknowns ?? [])],
256
+ },
257
+ integration: {
258
+ state: lifecycle.integration_state,
259
+ changed_paths: [...(integrationPlan?.provider_preview.changed_paths ?? [])],
260
+ target_ref: integrationPlan?.provider_preview.target_ref,
261
+ target_commit:
262
+ integrationReceipt?.target_commit ?? integrationPlan?.provider_preview.target_commit,
263
+ receipt_id: integrationReceipt?.receipt_id,
264
+ },
265
+ cleanup: {
266
+ state: cleanupState(cleanupIntent, latestCleanup?.status, cleanupReceipt),
267
+ attempts: cleanupAttempts.length,
268
+ reason: latestCleanup?.reason,
269
+ receipt_id: cleanupReceipt?.receipt_id,
270
+ },
271
+ repository: {
272
+ dirty_paths: [...(attestation?.repository?.dirty_paths ?? [])],
273
+ conflicts: [...(attestation?.repository?.conflicts ?? [])],
274
+ operations_in_progress: [...(attestation?.repository?.operation_in_progress ?? [])],
275
+ },
276
+ integrity: {
277
+ status: "verified",
278
+ proof_sha256: proofSha256,
279
+ provider_event_chain_sha256: lifecycle.provider_event_chain_sha256,
280
+ },
281
+ };
282
+ }
283
+
284
+ export function inspectWorkflowWorkspace(
285
+ coordRoot: string,
286
+ runId: string,
287
+ ): WorkflowWorkspaceInspection {
288
+ try {
289
+ return { ok: true, value: readWorkflowWorkspaceStatus(coordRoot, runId) };
290
+ } catch (error) {
291
+ return {
292
+ ok: false,
293
+ run_id: runId,
294
+ error: error instanceof Error ? error.message : String(error),
295
+ };
296
+ }
297
+ }
298
+
299
+ export function listWorkflowWorkspaceInspections(coordRoot: string): WorkflowWorkspaceInspection[] {
300
+ const workflowsRoot = join(resolve(coordRoot), ".harnery", "workflows");
301
+ if (!existsSync(workflowsRoot)) return [];
302
+ return readdirSync(workflowsRoot)
303
+ .filter((runId) => RUN_ID.test(runId) && existsSync(join(workflowsRoot, runId, "run.json")))
304
+ .sort((a, b) => b.localeCompare(a))
305
+ .map((runId) => inspectWorkflowWorkspace(coordRoot, runId));
306
+ }
307
+
308
+ export function renderWorkflowWorkspaceStatus(status: WorkflowWorkspaceStatus): string {
309
+ const lines = [
310
+ `run ${status.run_id} (${status.run_name}): ${status.selection}`,
311
+ `isolation: requested ${status.requested_isolation}; effective ${status.effective_isolation}`,
312
+ `lifecycle: ${status.lifecycle.state}; resource ${
313
+ status.lifecycle.resource_state ?? "not applicable"
314
+ }; integration ${status.lifecycle.integration_state}; cancellation ${status.lifecycle.cancellation}`,
315
+ ];
316
+ if (status.provider) {
317
+ lines.push(`provider: ${status.provider.id}@${status.provider.version}`);
318
+ }
319
+ if (status.compatibility) {
320
+ lines.push(`compatibility: ${status.compatibility.reason}`);
321
+ }
322
+ if (status.allocation) {
323
+ lines.push(
324
+ `binding: ${status.allocation.binding_id}; workspace ${status.allocation.workspace_id}`,
325
+ `workspace root: ${status.allocation.workspace_root}`,
326
+ `active root: ${status.allocation.active_root}`,
327
+ );
328
+ }
329
+ lines.push(
330
+ `verification: ${status.verification.status}; workflow ${
331
+ status.verification.workflow_status ?? "not terminal"
332
+ }; ${status.verification.drift.length} drift; ${status.verification.unknowns.length} unknown`,
333
+ `repository: ${status.repository.dirty_paths.length} dirty; ${
334
+ status.repository.conflicts.length
335
+ } conflict; ${status.repository.operations_in_progress.length} operation in progress`,
336
+ `integration: ${status.integration.state}; ${status.integration.changed_paths.length} changed path`,
337
+ `cleanup: ${status.cleanup.state}; ${status.cleanup.attempts} attempt`,
338
+ );
339
+ if (status.cleanup.reason) lines.push(`cleanup reason: ${status.cleanup.reason}`);
340
+ return `${lines.join("\n")}\n`;
341
+ }
342
+
343
+ function readWorkspaceJournal(runDir: string, runId: string): WorkflowJournalProjectionEvent[] {
344
+ const path = join(runDir, "journal.jsonl");
345
+ if (!existsSync(path)) return [];
346
+ return readFileSync(path, "utf8")
347
+ .split("\n")
348
+ .filter(Boolean)
349
+ .map((line, index) => {
350
+ let record: Record<string, unknown>;
351
+ try {
352
+ record = JSON.parse(line) as Record<string, unknown>;
353
+ } catch (error) {
354
+ throw new Error(
355
+ `cannot parse workflow journal event ${index + 1}: ${
356
+ error instanceof Error ? error.message : String(error)
357
+ }`,
358
+ );
359
+ }
360
+ if (
361
+ typeof record.event !== "string" ||
362
+ (record.run_id !== undefined && record.run_id !== runId)
363
+ ) {
364
+ throw new Error(`workflow journal event ${index + 1} is foreign or unsupported`);
365
+ }
366
+ return {
367
+ event: record.event,
368
+ ok: typeof record.ok === "boolean" ? record.ok : undefined,
369
+ status: journalStatus(record.status),
370
+ work_event_seq:
371
+ typeof record.work_event_seq === "number" ? record.work_event_seq : undefined,
372
+ work_event_sha256:
373
+ typeof record.work_event_sha256 === "string" ? record.work_event_sha256 : undefined,
374
+ };
375
+ });
376
+ }
377
+
378
+ function latestAttestation(
379
+ proof: WorkflowProof | undefined,
380
+ cleanupAttempt: WorkspaceAttestation | undefined,
381
+ cleanupReceipt: WorkspaceCleanupReceipt | undefined,
382
+ ): WorkspaceAttestation | undefined {
383
+ if (cleanupReceipt) return cleanupReceipt.attestation;
384
+ if (cleanupAttempt) return cleanupAttempt;
385
+ if (proof && isWorkspaceBoundExecutionEvidence(proof.execution, proof.run.id)) {
386
+ return proof.execution.terminal_attestation;
387
+ }
388
+ return undefined;
389
+ }
390
+
391
+ function verificationStatus(
392
+ proof: WorkflowProof | undefined,
393
+ attestation: WorkspaceAttestation | undefined,
394
+ ): WorkflowWorkspaceVerification {
395
+ if (!proof || !attestation) return "pending";
396
+ return attestation.status;
397
+ }
398
+
399
+ function cleanupState(
400
+ intent: WorkspaceCleanupIntent | undefined,
401
+ attemptStatus: string | undefined,
402
+ receipt: WorkspaceCleanupReceipt | undefined,
403
+ ): WorkflowWorkspaceStatus["cleanup"]["state"] {
404
+ if (receipt) return "released";
405
+ if (attemptStatus === "preserved_dirty") return "preserved_dirty";
406
+ if (attemptStatus && ["blocked", "unsupported", "partial"].includes(attemptStatus)) {
407
+ return "blocked";
408
+ }
409
+ return intent || attemptStatus ? "pending" : "not_requested";
410
+ }
411
+
412
+ function journalStatus(value: unknown): WorkflowJournalProjectionEvent["status"] {
413
+ return typeof value === "string" &&
414
+ ["blocked", "lost", "cancelled", "unsupported", "preserved_dirty"].includes(value)
415
+ ? (value as WorkflowJournalProjectionEvent["status"])
416
+ : undefined;
417
+ }
418
+
419
+ function assertRunId(runId: string): void {
420
+ if (!RUN_ID.test(runId)) throw new Error("workflow run id is invalid");
421
+ }