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,476 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+ import { policyDigest } from "../../policy/index.js";
3
+ import { stableDigest } from "../durable-record.js";
4
+ const DIGEST = /^[a-f0-9]{64}$/;
5
+ const OBJECT_ID = /^[a-f0-9]{40,64}$/;
6
+ export function isWorkspaceOwner(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ return false;
9
+ const owner = value;
10
+ return owner.kind === "standalone"
11
+ ? owner.work_item_id === null && owner.attempt === null
12
+ : owner.kind === "work_attempt" &&
13
+ typeof owner.work_item_id === "string" &&
14
+ owner.work_item_id.length > 0 &&
15
+ Number.isSafeInteger(owner.attempt) &&
16
+ owner.attempt > 0;
17
+ }
18
+ export function isWorkspaceAllocationRequest(value) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ return false;
21
+ const request = value;
22
+ if (request.schema_version !== 1 ||
23
+ typeof request.run_id !== "string" ||
24
+ !isWorkspaceOwner(request.owner) ||
25
+ !isAbsolute(request.requested_cwd) ||
26
+ !["worktree", "sandbox", "remote"].includes(request.requested_isolation) ||
27
+ !["enabled", "disabled", "unknown"].includes(request.network_access) ||
28
+ !isAbsolute(request.script?.path) ||
29
+ !DIGEST.test(request.script?.sha256) ||
30
+ !(request.policy_sha256 === null || DIGEST.test(request.policy_sha256)) ||
31
+ !validatedPathArray(request.allowed_paths) ||
32
+ !validatedPathArray(request.writable_roots) ||
33
+ request.writable_roots.length !== 1 ||
34
+ !validValidatedPath(request.selected_writable_root) ||
35
+ stableDigest(request.writable_roots[0]) !== stableDigest(request.selected_writable_root) ||
36
+ typeof request.provider_id !== "string" ||
37
+ request.provider_id.length === 0 ||
38
+ !DIGEST.test(request.capability_digest) ||
39
+ !DIGEST.test(request.idempotency_key)) {
40
+ return false;
41
+ }
42
+ const { idempotency_key: _idempotencyKey, ...authority } = request;
43
+ return request.idempotency_key === stableDigest(authority);
44
+ }
45
+ export function isWorkspaceBinding(value, runId) {
46
+ if (!value || typeof value !== "object" || Array.isArray(value))
47
+ return false;
48
+ const binding = value;
49
+ return (binding.schema_version === 1 &&
50
+ typeof binding.binding_id === "string" &&
51
+ binding.binding_id.length > 0 &&
52
+ typeof binding.workspace_id === "string" &&
53
+ binding.workspace_id.length > 0 &&
54
+ typeof binding.run_id === "string" &&
55
+ (runId === undefined || binding.run_id === runId) &&
56
+ isWorkspaceOwner(binding.owner) &&
57
+ binding.provider !== undefined &&
58
+ typeof binding.provider.id === "string" &&
59
+ typeof binding.provider.version === "string" &&
60
+ DIGEST.test(binding.provider.capability_digest) &&
61
+ ["worktree", "sandbox", "remote"].includes(binding.isolation) &&
62
+ ["enabled", "disabled", "unknown"].includes(binding.network_access) &&
63
+ isAbsolute(binding.workspace_root) &&
64
+ resolve(binding.workspace_root) === binding.workspace_root &&
65
+ validIdentity(binding.workspace_root_identity) &&
66
+ isAbsolute(binding.active_root) &&
67
+ resolve(binding.active_root) === binding.active_root &&
68
+ containsResolvedPath(binding.workspace_root, binding.active_root) &&
69
+ validIdentity(binding.active_root_identity) &&
70
+ (binding.integration_root === undefined ||
71
+ (isAbsolute(binding.integration_root) &&
72
+ resolve(binding.integration_root) === binding.integration_root)) &&
73
+ validValidatedPath(binding.writable_root) &&
74
+ Number.isSafeInteger(binding.generation) &&
75
+ binding.generation > 0 &&
76
+ /^[a-f0-9]{64}$/.test(binding.recovery_token) &&
77
+ DIGEST.test(binding.request_sha256) &&
78
+ validTimestamp(binding.created_at) &&
79
+ (binding.repository === undefined ||
80
+ (validValidatedPath(binding.repository.source_root) &&
81
+ validObservedIdentity(binding.repository.common_dir) &&
82
+ OBJECT_ID.test(binding.repository.base_commit) &&
83
+ OBJECT_ID.test(binding.repository.target_commit) &&
84
+ typeof binding.repository.target_ref === "string" &&
85
+ typeof binding.repository.workspace_ref === "string" &&
86
+ typeof binding.repository.workspace_branch === "string" &&
87
+ validObservedIdentity(binding.repository.gitdir) &&
88
+ typeof binding.repository.branch_created_by_provider === "boolean")));
89
+ }
90
+ export function isWorkspaceAttestation(value, binding) {
91
+ if (!value || typeof value !== "object" || Array.isArray(value))
92
+ return false;
93
+ const attestation = value;
94
+ const containment = attestation.containment;
95
+ const filesystem = attestation.filesystem;
96
+ const validContainment = containment !== undefined &&
97
+ containment !== null &&
98
+ typeof containment === "object" &&
99
+ typeof containment.writable_root === "boolean" &&
100
+ typeof containment.workspace_root === "boolean" &&
101
+ typeof containment.active_root === "boolean" &&
102
+ (typeof containment.integration_root === "boolean" || containment.integration_root === null);
103
+ const validFilesystem = filesystem !== undefined &&
104
+ filesystem !== null &&
105
+ typeof filesystem === "object" &&
106
+ typeof filesystem.root_identity_match === "boolean" &&
107
+ (filesystem.workspace_identity === undefined || validIdentity(filesystem.workspace_identity)) &&
108
+ (filesystem.active_identity === undefined || validIdentity(filesystem.active_identity));
109
+ const repository = attestation.repository;
110
+ const validDrift = stringArray(attestation.provider_drift);
111
+ const validUnsupported = providerFactArray(attestation.unsupported);
112
+ const validUnknowns = providerFactArray(attestation.unknowns);
113
+ const validRepository = repository === undefined ||
114
+ (typeof repository === "object" &&
115
+ repository !== null &&
116
+ validObservedSourceRoot(repository.source_root) &&
117
+ (repository.common_dir === undefined || validObservedIdentity(repository.common_dir)) &&
118
+ (repository.active_gitdir === undefined || validObservedIdentity(repository.active_gitdir)) &&
119
+ typeof repository.worktree_registered === "boolean" &&
120
+ (repository.current_ref === undefined || typeof repository.current_ref === "string") &&
121
+ (repository.workspace_ref_oid === undefined ||
122
+ OBJECT_ID.test(repository.workspace_ref_oid)) &&
123
+ (repository.target_ref_oid === undefined || OBJECT_ID.test(repository.target_ref_oid)) &&
124
+ (repository.head_commit === undefined || OBJECT_ID.test(repository.head_commit)) &&
125
+ (typeof repository.base_is_ancestor === "boolean" || repository.base_is_ancestor === null) &&
126
+ (typeof repository.target_is_ancestor === "boolean" ||
127
+ repository.target_is_ancestor === null) &&
128
+ stringArray(repository.dirty_paths) &&
129
+ stringArray(repository.conflicts) &&
130
+ stringArray(repository.operation_in_progress));
131
+ const validReleasedAuthority = attestation.resource_state === "released" &&
132
+ attestation.workspace_exists === false &&
133
+ containment?.writable_root === true &&
134
+ containment.workspace_root === false &&
135
+ containment.active_root === false &&
136
+ filesystem?.root_identity_match === true &&
137
+ filesystem.workspace_identity === undefined &&
138
+ filesystem.active_identity === undefined &&
139
+ (binding.repository === undefined ||
140
+ (repository !== undefined &&
141
+ repositorySourceMatches(repository, binding) &&
142
+ repositoryCommonDirMatches(repository, binding) &&
143
+ repository?.active_gitdir === undefined &&
144
+ repository.worktree_registered === false &&
145
+ repository.current_ref === undefined &&
146
+ repository.workspace_ref_oid === undefined &&
147
+ repository.target_ref_oid !== undefined &&
148
+ repository.head_commit === undefined &&
149
+ repository.base_is_ancestor === null &&
150
+ repository.target_is_ancestor === null &&
151
+ repository.dirty_paths.length === 0 &&
152
+ repository.conflicts.length === 0 &&
153
+ repository.operation_in_progress.length === 0));
154
+ const validActiveAuthority = (attestation.resource_state === "active" || attestation.resource_state === "preserved_dirty") &&
155
+ attestation.workspace_exists === true &&
156
+ containment?.writable_root === true &&
157
+ containment.workspace_root === true &&
158
+ containment.active_root === true &&
159
+ (binding.integration_root === undefined || containment.integration_root === true) &&
160
+ filesystem?.root_identity_match === true &&
161
+ validIdentity(filesystem.workspace_identity) &&
162
+ validIdentity(filesystem.active_identity) &&
163
+ sameIdentity(filesystem.workspace_identity, binding.workspace_root_identity) &&
164
+ sameIdentity(filesystem.active_identity, binding.active_root_identity) &&
165
+ (binding.repository === undefined ||
166
+ (repository !== undefined &&
167
+ repositorySourceMatches(repository, binding) &&
168
+ repositoryCommonDirMatches(repository, binding) &&
169
+ repositoryGitdirMatches(repository, binding) &&
170
+ repository.worktree_registered === true &&
171
+ binding.repository.branch_created_by_provider === true &&
172
+ repository.current_ref === binding.repository.workspace_ref &&
173
+ repository.workspace_ref_oid !== undefined &&
174
+ repository.target_ref_oid !== undefined &&
175
+ repository.head_commit !== undefined &&
176
+ repository.workspace_ref_oid === repository.head_commit &&
177
+ repository.base_is_ancestor === true &&
178
+ repository.target_is_ancestor === true));
179
+ const validOkAuthority = attestation.status !== "ok" ||
180
+ (validDrift &&
181
+ attestation.provider_drift.length === 0 &&
182
+ (validActiveAuthority || validReleasedAuthority));
183
+ const validFailureEvidence = validDrift &&
184
+ validUnsupported &&
185
+ validUnknowns &&
186
+ (attestation.status === "ok" ||
187
+ repository !== undefined ||
188
+ attestation.provider_drift.length > 0 ||
189
+ attestation.unsupported.length > 0 ||
190
+ attestation.unknowns.length > 0);
191
+ return (attestation.schema_version === 1 &&
192
+ attestation.binding_id === binding.binding_id &&
193
+ attestation.workspace_id === binding.workspace_id &&
194
+ attestation.run_id === binding.run_id &&
195
+ sameOwner(attestation.owner, binding.owner) &&
196
+ validProviderRef(attestation.provider) &&
197
+ attestation.provider.id === binding.provider.id &&
198
+ attestation.provider.version === binding.provider.version &&
199
+ attestation.provider.capability_digest === binding.provider.capability_digest &&
200
+ validTimestamp(attestation.recorded_at) &&
201
+ validContainment &&
202
+ validFilesystem &&
203
+ validRepository &&
204
+ validOkAuthority &&
205
+ validFailureEvidence &&
206
+ (binding.repository === undefined
207
+ ? repository === undefined
208
+ : attestation.status !== "ok" || repository !== undefined) &&
209
+ typeof attestation.workspace_exists === "boolean" &&
210
+ ["active", "preserved_dirty", "released", "blocked", "lost"].includes(attestation.resource_state) &&
211
+ (attestation.status === "blocked"
212
+ ? attestation.resource_state === "blocked"
213
+ : attestation.status === "lost"
214
+ ? attestation.resource_state === "lost"
215
+ : attestation.resource_state === "active" ||
216
+ attestation.resource_state === "preserved_dirty" ||
217
+ attestation.resource_state === "released") &&
218
+ ["ok", "blocked", "lost"].includes(attestation.status) &&
219
+ validDrift &&
220
+ validUnsupported &&
221
+ validUnknowns);
222
+ }
223
+ export function isWorkspaceExecutionEvidence(value, runId, workflowStatus) {
224
+ if (!value || typeof value !== "object" || Array.isArray(value))
225
+ return false;
226
+ const execution = value;
227
+ if (isWorkspaceCompatibilityExecutionEvidence(execution, runId))
228
+ return true;
229
+ return isWorkspaceBoundExecutionEvidence(execution, runId, workflowStatus);
230
+ }
231
+ export function isWorkspaceBoundExecutionEvidence(value, runId, workflowStatus) {
232
+ if (!value || typeof value !== "object" || Array.isArray(value))
233
+ return false;
234
+ const execution = value;
235
+ return (execution.schema_version === 1 &&
236
+ isWorkspaceBinding(execution.binding, runId) &&
237
+ isWorkspaceAttestation(execution.terminal_attestation, execution.binding) &&
238
+ validWorkspaceProofOutcome(execution.terminal_lifecycle_state, execution.terminal_attestation, workflowStatus) &&
239
+ stableDigest(execution.drift) === stableDigest(execution.terminal_attestation.provider_drift) &&
240
+ stableDigest(execution.unsupported) ===
241
+ stableDigest(execution.terminal_attestation.unsupported) &&
242
+ stableDigest(execution.unknowns) === stableDigest(execution.terminal_attestation.unknowns) &&
243
+ validBoundReceiptLocations(execution.receipts));
244
+ }
245
+ export function workspaceProofLifecycle(workflowStatus, attestation) {
246
+ if (attestation.status === "lost")
247
+ return "lost";
248
+ if (attestation.status === "blocked")
249
+ return "blocked";
250
+ return workflowStatus === "succeeded" ? "completed_unintegrated" : "failed_retained";
251
+ }
252
+ export function assertWorkspaceManifestAuthority(manifest, request) {
253
+ const binding = manifest.execution.workspace_binding;
254
+ const fallback = manifest.execution.workspace_fallback;
255
+ if (binding) {
256
+ if (fallback) {
257
+ throw new Error("workflow manifest cannot bind a workspace and a fallback");
258
+ }
259
+ const expectedOwner = manifest.work_item_id && manifest.attempt_context
260
+ ? {
261
+ kind: "work_attempt",
262
+ work_item_id: manifest.work_item_id,
263
+ attempt: manifest.attempt_context.number,
264
+ }
265
+ : { kind: "standalone", work_item_id: null, attempt: null };
266
+ const expectedPolicyDigest = manifest.execution.policy
267
+ ? policyDigest(manifest.execution.policy)
268
+ : null;
269
+ if (!isWorkspaceAllocationRequest(request) ||
270
+ request.run_id !== manifest.run_id ||
271
+ binding.run_id !== manifest.run_id ||
272
+ stableDigest(request.owner) !== stableDigest(expectedOwner) ||
273
+ stableDigest(binding.owner) !== stableDigest(expectedOwner) ||
274
+ request.requested_isolation !== manifest.execution.isolation ||
275
+ binding.isolation !== manifest.execution.isolation ||
276
+ request.network_access !== manifest.execution.network_access ||
277
+ binding.network_access !== manifest.execution.network_access ||
278
+ manifest.execution.cwd !== binding.active_root ||
279
+ request.provider_id !== binding.provider.id ||
280
+ request.capability_digest !== binding.provider.capability_digest ||
281
+ stableDigest(request.selected_writable_root) !== stableDigest(binding.writable_root) ||
282
+ request.writable_roots.length !== 1 ||
283
+ stableDigest(request.writable_roots[0]) !== stableDigest(request.selected_writable_root) ||
284
+ stableDigest(request.script) !== stableDigest(manifest.script) ||
285
+ request.policy_sha256 !== expectedPolicyDigest ||
286
+ stableDigest(request.allowed_paths.map((path) => path.configured)) !==
287
+ stableDigest(manifest.execution.policy?.allowed_paths ?? []) ||
288
+ binding.request_sha256 !== stableDigest(request) ||
289
+ (binding.repository !== undefined &&
290
+ (!workspaceCwdMatchesBinding(request, binding) ||
291
+ binding.integration_root !== binding.repository.source_root.realpath))) {
292
+ throw new Error("workflow manifest, workspace request, and provider binding disagree");
293
+ }
294
+ return;
295
+ }
296
+ if (request) {
297
+ throw new Error("workflow manifest has an unbound workspace request");
298
+ }
299
+ if (fallback &&
300
+ (fallback.run_id !== manifest.run_id ||
301
+ fallback.requested_isolation !== manifest.execution.isolation ||
302
+ fallback.effective_isolation !== "shared")) {
303
+ throw new Error("workflow manifest and workspace fallback disagree");
304
+ }
305
+ }
306
+ function workspaceCwdMatchesBinding(request, binding) {
307
+ if (!binding.repository)
308
+ return true;
309
+ const relativeCwd = relative(binding.repository.source_root.configured, request.requested_cwd);
310
+ return ((relativeCwd === "" ||
311
+ (!relativeCwd.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) &&
312
+ relativeCwd !== ".." &&
313
+ !isAbsolute(relativeCwd))) &&
314
+ binding.active_root === resolve(binding.workspace_root, relativeCwd));
315
+ }
316
+ function validWorkspaceProofOutcome(outcome, attestation, workflowStatus) {
317
+ if (workflowStatus !== undefined) {
318
+ return outcome === workspaceProofLifecycle(workflowStatus, attestation);
319
+ }
320
+ if (attestation.status === "lost")
321
+ return outcome === "lost";
322
+ if (attestation.status === "blocked")
323
+ return outcome === "blocked";
324
+ return outcome === "completed_unintegrated" || outcome === "failed_retained";
325
+ }
326
+ export function isWorkspaceCompatibilityExecutionEvidence(value, runId) {
327
+ if (!value || typeof value !== "object" || Array.isArray(value))
328
+ return false;
329
+ const execution = value;
330
+ const providerNotConfigured = execution.selection_reason === "provider_not_configured" &&
331
+ execution.provider === undefined &&
332
+ execution.unsupported.some((fact) => fact.code === "provider_not_configured");
333
+ const providerUnsupported = execution.selection_reason === "provider_unsupported" && validProviderRef(execution.provider);
334
+ return (execution.schema_version === 1 &&
335
+ typeof execution.run_id === "string" &&
336
+ (runId === undefined || execution.run_id === runId) &&
337
+ ["worktree", "sandbox", "remote"].includes(execution.requested_isolation) &&
338
+ execution.effective_isolation === "shared" &&
339
+ (providerNotConfigured || providerUnsupported) &&
340
+ execution.terminal_lifecycle_state === "shared" &&
341
+ Array.isArray(execution.drift) &&
342
+ execution.drift.length === 0 &&
343
+ providerFactArray(execution.unsupported) &&
344
+ execution.unsupported.length > 0 &&
345
+ providerFactArray(execution.unknowns) &&
346
+ execution.receipts !== null &&
347
+ typeof execution.receipts === "object" &&
348
+ !Array.isArray(execution.receipts) &&
349
+ Object.keys(execution.receipts).length === 0);
350
+ }
351
+ /** @deprecated Use isWorkspaceCompatibilityExecutionEvidence. */
352
+ export const isWorkspaceUnsupportedExecutionEvidence = isWorkspaceCompatibilityExecutionEvidence;
353
+ function sameOwner(left, right) {
354
+ return (left.kind === right.kind &&
355
+ left.work_item_id === right.work_item_id &&
356
+ left.attempt === right.attempt);
357
+ }
358
+ function repositorySourceMatches(observation, binding) {
359
+ const expected = binding.repository?.source_root;
360
+ return Boolean(observation &&
361
+ expected &&
362
+ observation.source_root &&
363
+ observation.source_root.configured === expected.configured &&
364
+ observation.source_root.realpath === expected.realpath &&
365
+ sameIdentity(observation.source_root.identity, expected.identity));
366
+ }
367
+ function repositoryCommonDirMatches(observation, binding) {
368
+ const expected = binding.repository?.common_dir;
369
+ return Boolean(observation &&
370
+ expected &&
371
+ observation.common_dir?.realpath === expected.realpath &&
372
+ sameIdentity(observation.common_dir?.identity, expected.identity));
373
+ }
374
+ function repositoryGitdirMatches(observation, binding) {
375
+ const expected = binding.repository?.gitdir;
376
+ return Boolean(observation &&
377
+ expected &&
378
+ observation.active_gitdir?.realpath === expected.realpath &&
379
+ sameIdentity(observation.active_gitdir?.identity, expected.identity));
380
+ }
381
+ function validObservedSourceRoot(value) {
382
+ if (!value || typeof value !== "object" || Array.isArray(value))
383
+ return false;
384
+ const root = value;
385
+ return (typeof root.configured === "string" &&
386
+ isAbsolute(root.configured) &&
387
+ resolve(root.configured) === root.configured &&
388
+ ((root.realpath === undefined && root.identity === undefined) ||
389
+ (typeof root.realpath === "string" &&
390
+ isAbsolute(root.realpath) &&
391
+ resolve(root.realpath) === root.realpath &&
392
+ validIdentity(root.identity))));
393
+ }
394
+ function validObservedIdentity(value) {
395
+ if (!value || typeof value !== "object" || Array.isArray(value))
396
+ return false;
397
+ const root = value;
398
+ return (typeof root.realpath === "string" &&
399
+ isAbsolute(root.realpath) &&
400
+ resolve(root.realpath) === root.realpath &&
401
+ validIdentity(root.identity));
402
+ }
403
+ function sameIdentity(left, right) {
404
+ if (!validIdentity(left) || !validIdentity(right))
405
+ return false;
406
+ return stableDigest(left) === stableDigest(right);
407
+ }
408
+ function validBoundReceiptLocations(value) {
409
+ if (!value || typeof value !== "object" || Array.isArray(value))
410
+ return false;
411
+ const expected = {
412
+ request: "workspace-request.json",
413
+ cancellation_outcome: "cancellation/outcome.json",
414
+ integration_plan: "integration/plan.json",
415
+ integration_authorization: "integration/authorization.json",
416
+ integration_apply: "integration/receipt.json",
417
+ cleanup_intent: "cleanup/intent.json",
418
+ cleanup_receipt: "cleanup/receipt.json",
419
+ };
420
+ return (value.request === expected.request &&
421
+ Object.entries(value).every(([key, location]) => expected[key] === location));
422
+ }
423
+ function validIdentity(value) {
424
+ if (!value || typeof value !== "object")
425
+ return false;
426
+ const identity = value;
427
+ return (typeof identity.platform === "string" &&
428
+ typeof identity.device === "string" &&
429
+ typeof identity.inode === "string");
430
+ }
431
+ function validProviderRef(value) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value))
433
+ return false;
434
+ const provider = value;
435
+ return (typeof provider.id === "string" &&
436
+ provider.id.length > 0 &&
437
+ typeof provider.version === "string" &&
438
+ provider.version.length > 0 &&
439
+ typeof provider.capability_digest === "string" &&
440
+ DIGEST.test(provider.capability_digest));
441
+ }
442
+ function validTimestamp(value) {
443
+ return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
444
+ }
445
+ function stringArray(value) {
446
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
447
+ }
448
+ function validValidatedPath(value) {
449
+ if (!value || typeof value !== "object" || Array.isArray(value))
450
+ return false;
451
+ const path = value;
452
+ return (typeof path.configured === "string" &&
453
+ isAbsolute(path.configured) &&
454
+ resolve(path.configured) === path.configured &&
455
+ typeof path.realpath === "string" &&
456
+ isAbsolute(path.realpath) &&
457
+ resolve(path.realpath) === path.realpath &&
458
+ validIdentity(path.identity));
459
+ }
460
+ function validatedPathArray(value) {
461
+ return Array.isArray(value) && value.every(validValidatedPath);
462
+ }
463
+ function containsResolvedPath(root, candidate) {
464
+ const rel = relative(root, candidate);
465
+ return (rel === "" ||
466
+ (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel)));
467
+ }
468
+ function providerFactArray(value) {
469
+ return (Array.isArray(value) &&
470
+ value.every((item) => item !== null &&
471
+ typeof item === "object" &&
472
+ typeof item.code === "string" &&
473
+ item.code.length > 0 &&
474
+ typeof item.message === "string" &&
475
+ item.message.length > 0));
476
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -148,7 +148,7 @@
148
148
  "agent-coord": "./bin/agent-coord",
149
149
  "agent-hook": "./bin/agent-hook"
150
150
  },
151
- "//files": "src ships as TS so the `bun` export condition resolves to source; negate co-located tests so they don't ride into the tarball (a .npmignore is inert for paths already inside the files allowlist).",
151
+ "//files": "src ships as TS so the `bun` export condition resolves to source; negate co-located tests and their shared fixture helper so they don't ride into the tarball (a .npmignore is inert for paths already inside the files allowlist).",
152
152
  "files": [
153
153
  "bin",
154
154
  "src",
@@ -157,7 +157,9 @@
157
157
  "README.md",
158
158
  "LICENSE",
159
159
  "!src/**/*.test.ts",
160
- "!src/**/*.test.tsx"
160
+ "!src/**/*.test.tsx",
161
+ "!dist/**/test-helpers.*",
162
+ "!src/**/test-helpers.ts"
161
163
  ],
162
164
  "engines": {
163
165
  "node": ">=20.19.0"
@@ -1,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import type { Command } from "commander";
2
3
  import type { EmitContext } from "../commander.ts";
3
4
  import { workflowSubscriptionOnly } from "../core/config.ts";
@@ -28,6 +29,7 @@ interface WorkflowRunOpts {
28
29
  allowApiBilling?: boolean;
29
30
  policy?: string;
30
31
  isolation?: PolicyIsolation;
32
+ workspaceRoot?: string;
31
33
  approvalTo?: string;
32
34
  json?: boolean;
33
35
  }
@@ -89,6 +91,10 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
89
91
  "--isolation <mode>",
90
92
  "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)",
91
93
  )
94
+ .option(
95
+ "--workspace-root <dir>",
96
+ "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)",
97
+ )
92
98
  .option("--approval-to <address>", "Address durable ASK requests (default: operator)")
93
99
  .option("--json", "Emit the full RunReport as JSON")
94
100
  .action(async (script: string, opts: WorkflowRunOpts) => {
@@ -117,6 +123,13 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
117
123
  });
118
124
  process.exit(1);
119
125
  }
126
+ if (opts.workspaceRoot && opts.isolation !== "worktree") {
127
+ emit.error({
128
+ code: "bad_workspace_root",
129
+ message: "--workspace-root requires --isolation worktree",
130
+ });
131
+ process.exit(1);
132
+ }
120
133
  // Flag beats config; the two flags contradict each other by design.
121
134
  const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
122
135
  if (subscriptionOnly && opts.allowApiBilling) {
@@ -129,7 +142,15 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
129
142
  process.exit(1);
130
143
  }
131
144
  try {
132
- const { runWorkflow } = await import("../core/workflow/engine.ts");
145
+ const { createLocalGitWorktreeProvider, runWorkflow } = await import(
146
+ "../core/workflow/index.ts"
147
+ );
148
+ const workspace = opts.workspaceRoot
149
+ ? {
150
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
151
+ writableRoots: [resolve(opts.workspaceRoot)],
152
+ }
153
+ : undefined;
133
154
  const report = await runWorkflow(script, {
134
155
  coordRoot,
135
156
  spawners: registry.spawners(),
@@ -152,6 +173,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
152
173
  approvalMode: "park",
153
174
  approvalAddressee: opts.approvalTo,
154
175
  isolation: opts.isolation,
176
+ workspace,
155
177
  // CLI harness subprocesses inherit the host network. A policy that
156
178
  // forbids network must therefore deny dispatch unless a future host
157
179
  // adapter creates and declares a network-disabled boundary.
@@ -220,14 +242,25 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
220
242
  process.exit(1);
221
243
  }
222
244
  try {
223
- const { assertWorkflowRunResumable, runWorkflow } = await import(
224
- "../core/workflow/index.ts"
225
- );
245
+ const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } =
246
+ await import("../core/workflow/index.ts");
226
247
  const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
248
+ const binding = manifest.execution.workspace_binding;
249
+ if (binding && binding.provider.id !== "local-git-worktree") {
250
+ throw new Error(
251
+ `workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`,
252
+ );
253
+ }
227
254
  const report = await runWorkflow(manifest.script.path, {
228
255
  coordRoot,
229
256
  spawners: registry.spawners(),
230
257
  resumeRunId: runId,
258
+ workspace: binding
259
+ ? {
260
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
261
+ writableRoots: [binding.writable_root.configured],
262
+ }
263
+ : undefined,
231
264
  harnessEvidence: Object.fromEntries(
232
265
  registry
233
266
  .list()
@@ -281,6 +314,79 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
281
314
  }
282
315
  });
283
316
 
317
+ workflow
318
+ .command("workspace <run-id>")
319
+ .description("Show validated allocation, verification, integration, and cleanup state.")
320
+ .option("--json", "Emit the validated workspace status as JSON")
321
+ .action(async (runId: string, opts: { json?: boolean }) => {
322
+ const coordRoot = findCoordRoot();
323
+ if (!coordRoot) {
324
+ emit.error({
325
+ code: "no_coord_root",
326
+ message: "no .harnery/ coordination root found; run `init` first",
327
+ });
328
+ process.exit(1);
329
+ }
330
+ const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import(
331
+ "../core/workflow/index.ts"
332
+ );
333
+ const inspection = inspectWorkflowWorkspace(coordRoot, runId);
334
+ if (!inspection.ok) {
335
+ emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
336
+ process.exit(1);
337
+ }
338
+ if (opts.json) {
339
+ emit.config({ format: "json" });
340
+ emit.data(inspection.value);
341
+ return;
342
+ }
343
+ emit.text(renderWorkflowWorkspaceStatus(inspection.value));
344
+ });
345
+
346
+ workflow
347
+ .command("workspaces")
348
+ .description("List validated isolated and shared-compatibility workspace decisions.")
349
+ .option("--json", "Emit workspace inspections as JSON")
350
+ .action(async (opts: { json?: boolean }) => {
351
+ const coordRoot = findCoordRoot();
352
+ if (!coordRoot) {
353
+ emit.error({
354
+ code: "no_coord_root",
355
+ message: "no .harnery/ coordination root found; run `init` first",
356
+ });
357
+ process.exit(1);
358
+ }
359
+ const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.ts");
360
+ const inspections = listWorkflowWorkspaceInspections(coordRoot).filter(
361
+ (inspection) => !inspection.ok || inspection.value.selection !== "shared",
362
+ );
363
+ if (opts.json) {
364
+ emit.config({ format: "json" });
365
+ emit.data(inspections);
366
+ return;
367
+ }
368
+ if (inspections.length === 0) {
369
+ emit.text("no isolated or shared-compatibility workspace runs\n");
370
+ return;
371
+ }
372
+ emit.text(
373
+ `${inspections
374
+ .map((inspection) =>
375
+ inspection.ok
376
+ ? [
377
+ inspection.value.run_id,
378
+ inspection.value.selection,
379
+ inspection.value.lifecycle.state,
380
+ `verify=${inspection.value.verification.status}`,
381
+ `integration=${inspection.value.integration.state}`,
382
+ `cleanup=${inspection.value.cleanup.state}`,
383
+ ].join("\t")
384
+ : `${inspection.run_id}\tinvalid\t${inspection.error}`,
385
+ )
386
+ .join("\n")}\n`,
387
+ );
388
+ });
389
+
284
390
  const approvals = workflow
285
391
  .command("approvals")
286
392
  .description("Inspect and resolve durable workflow policy approvals.");