harnery 0.22.0 → 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 (103) 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/validate.d.ts +10 -1
  27. package/dist/core/workflow/validate.d.ts.map +1 -1
  28. package/dist/core/workflow/validate.js +75 -1
  29. package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
  30. package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
  31. package/dist/core/workflow/workspaces/attestation-error.js +25 -0
  32. package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
  33. package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
  34. package/dist/core/workflow/workspaces/cancellation.js +95 -0
  35. package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
  36. package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
  37. package/dist/core/workflow/workspaces/cleanup.js +223 -0
  38. package/dist/core/workflow/workspaces/execution.d.ts +25 -0
  39. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
  40. package/dist/core/workflow/workspaces/execution.js +316 -0
  41. package/dist/core/workflow/workspaces/git.d.ts +25 -0
  42. package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
  43. package/dist/core/workflow/workspaces/git.js +102 -0
  44. package/dist/core/workflow/workspaces/index.d.ts +15 -0
  45. package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
  46. package/dist/core/workflow/workspaces/index.js +7 -0
  47. package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
  48. package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
  49. package/dist/core/workflow/workspaces/inspect.js +255 -0
  50. package/dist/core/workflow/workspaces/integration.d.ts +24 -0
  51. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/integration.js +617 -0
  53. package/dist/core/workflow/workspaces/leases.d.ts +37 -0
  54. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
  55. package/dist/core/workflow/workspaces/leases.js +380 -0
  56. package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
  57. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
  58. package/dist/core/workflow/workspaces/lifecycle.js +622 -0
  59. package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
  60. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
  61. package/dist/core/workflow/workspaces/local-git.js +1474 -0
  62. package/dist/core/workflow/workspaces/paths.d.ts +23 -0
  63. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
  64. package/dist/core/workflow/workspaces/paths.js +191 -0
  65. package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
  66. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
  67. package/dist/core/workflow/workspaces/recovery.js +50 -0
  68. package/dist/core/workflow/workspaces/state.d.ts +60 -0
  69. package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
  70. package/dist/core/workflow/workspaces/state.js +296 -0
  71. package/dist/core/workflow/workspaces/types.d.ts +445 -0
  72. package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
  73. package/dist/core/workflow/workspaces/types.js +2 -0
  74. package/dist/core/workflow/workspaces/validate.d.ts +14 -0
  75. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
  76. package/dist/core/workflow/workspaces/validate.js +476 -0
  77. package/package.json +5 -3
  78. package/src/commands/workflow.ts +110 -4
  79. package/src/core/work/state.ts +156 -28
  80. package/src/core/workflow/child-env.ts +18 -7
  81. package/src/core/workflow/durable-record.ts +94 -0
  82. package/src/core/workflow/engine.ts +230 -30
  83. package/src/core/workflow/index.ts +73 -0
  84. package/src/core/workflow/proof.ts +70 -22
  85. package/src/core/workflow/run-state.ts +46 -11
  86. package/src/core/workflow/types.ts +14 -0
  87. package/src/core/workflow/validate.ts +75 -1
  88. package/src/core/workflow/workspaces/attestation-error.ts +30 -0
  89. package/src/core/workflow/workspaces/cancellation.ts +137 -0
  90. package/src/core/workflow/workspaces/cleanup.ts +308 -0
  91. package/src/core/workflow/workspaces/execution.ts +412 -0
  92. package/src/core/workflow/workspaces/git.ts +125 -0
  93. package/src/core/workflow/workspaces/index.ts +85 -0
  94. package/src/core/workflow/workspaces/inspect.ts +421 -0
  95. package/src/core/workflow/workspaces/integration.ts +829 -0
  96. package/src/core/workflow/workspaces/leases.ts +495 -0
  97. package/src/core/workflow/workspaces/lifecycle.ts +792 -0
  98. package/src/core/workflow/workspaces/local-git.ts +1892 -0
  99. package/src/core/workflow/workspaces/paths.ts +247 -0
  100. package/src/core/workflow/workspaces/recovery.ts +56 -0
  101. package/src/core/workflow/workspaces/state.ts +481 -0
  102. package/src/core/workflow/workspaces/types.ts +517 -0
  103. 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
+ }