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.
- package/README.md +1 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +91 -2
- package/dist/core/work/state.d.ts +7 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +112 -23
- package/dist/core/workflow/child-env.d.ts +10 -6
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +15 -7
- package/dist/core/workflow/durable-record.d.ts +7 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -0
- package/dist/core/workflow/durable-record.js +83 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +208 -29
- package/dist/core/workflow/index.d.ts +2 -0
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -0
- package/dist/core/workflow/proof.d.ts +4 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +50 -15
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +25 -13
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +10 -1
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +75 -1
- package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
- package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/attestation-error.js +25 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cancellation.js +95 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cleanup.js +223 -0
- package/dist/core/workflow/workspaces/execution.d.ts +25 -0
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/execution.js +316 -0
- package/dist/core/workflow/workspaces/git.d.ts +25 -0
- package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/git.js +102 -0
- package/dist/core/workflow/workspaces/index.d.ts +15 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/index.js +7 -0
- package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/inspect.js +255 -0
- package/dist/core/workflow/workspaces/integration.d.ts +24 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +617 -0
- package/dist/core/workflow/workspaces/leases.d.ts +37 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/leases.js +380 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/lifecycle.js +622 -0
- package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/local-git.js +1474 -0
- package/dist/core/workflow/workspaces/paths.d.ts +23 -0
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/paths.js +191 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/recovery.js +50 -0
- package/dist/core/workflow/workspaces/state.d.ts +60 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/state.js +296 -0
- package/dist/core/workflow/workspaces/types.d.ts +445 -0
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/types.js +2 -0
- package/dist/core/workflow/workspaces/validate.d.ts +14 -0
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/validate.js +476 -0
- package/package.json +5 -3
- package/src/commands/workflow.ts +110 -4
- package/src/core/work/state.ts +156 -28
- package/src/core/workflow/child-env.ts +18 -7
- package/src/core/workflow/durable-record.ts +94 -0
- package/src/core/workflow/engine.ts +230 -30
- package/src/core/workflow/index.ts +73 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/types.ts +14 -0
- package/src/core/workflow/validate.ts +75 -1
- package/src/core/workflow/workspaces/attestation-error.ts +30 -0
- package/src/core/workflow/workspaces/cancellation.ts +137 -0
- package/src/core/workflow/workspaces/cleanup.ts +308 -0
- package/src/core/workflow/workspaces/execution.ts +412 -0
- package/src/core/workflow/workspaces/git.ts +125 -0
- package/src/core/workflow/workspaces/index.ts +85 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +829 -0
- package/src/core/workflow/workspaces/leases.ts +495 -0
- package/src/core/workflow/workspaces/lifecycle.ts +792 -0
- package/src/core/workflow/workspaces/local-git.ts +1892 -0
- package/src/core/workflow/workspaces/paths.ts +247 -0
- package/src/core/workflow/workspaces/recovery.ts +56 -0
- package/src/core/workflow/workspaces/state.ts +481 -0
- package/src/core/workflow/workspaces/types.ts +517 -0
- 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
|
+
}
|