harnery 0.22.1 → 0.24.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 +302 -5
  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/spawn-cursor.d.ts +3 -0
  25. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  26. package/dist/core/workflow/spawn-cursor.js +8 -4
  27. package/dist/core/workflow/types.d.ts +10 -0
  28. package/dist/core/workflow/types.d.ts.map +1 -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 +32 -0
  51. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/integration.js +631 -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 +397 -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 +366 -8
  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 +74 -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/spawn-cursor.ts +8 -4
  87. package/src/core/workflow/types.ts +14 -0
  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 +89 -0
  94. package/src/core/workflow/workspaces/inspect.ts +421 -0
  95. package/src/core/workflow/workspaces/integration.ts +845 -0
  96. package/src/core/workflow/workspaces/leases.ts +517 -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
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
23
  - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
24
24
  - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
25
+ - **Optional isolated workspaces:** add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. Inspect with `harn workflow workspace`, then use the separate `workflow integration prepare`, `integration apply`, and `cleanup` phases; shared execution remains the default.
25
26
  - **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Reusable workflow code receives the exact work ID, title, objective, acceptance criteria, and attempt identity as frozen typed context. A retry also receives the prior run's bounded error and unresolved proof criteria, while retry authority and attempt ceilings remain unchanged. The manifest, proof, and append-only ledger bind the same context, and the one-shot reconciler reports the next explicit action without silently spawning, retrying, or accepting work.
26
27
  - **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
27
28
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgDnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA2XjF"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiEnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAqqBjF"}
@@ -1,4 +1,6 @@
1
- import { workflowSubscriptionOnly } from "../core/config.js";
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { resolveBinName, workflowSubscriptionOnly } from "../core/config.js";
2
4
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
3
5
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
4
6
  import { loadPolicyFile } from "../core/policy/index.js";
@@ -24,6 +26,7 @@ export function registerWorkflowCommand(program, emit) {
24
26
  "without this the engine refuses that silent-override state")
25
27
  .option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
26
28
  .option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
29
+ .option("--workspace-root <dir>", "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)")
27
30
  .option("--approval-to <address>", "Address durable ASK requests (default: operator)")
28
31
  .option("--json", "Emit the full RunReport as JSON")
29
32
  .action(async (script, opts) => {
@@ -50,6 +53,13 @@ export function registerWorkflowCommand(program, emit) {
50
53
  });
51
54
  process.exit(1);
52
55
  }
56
+ if (opts.workspaceRoot && opts.isolation !== "worktree") {
57
+ emit.error({
58
+ code: "bad_workspace_root",
59
+ message: "--workspace-root requires --isolation worktree",
60
+ });
61
+ process.exit(1);
62
+ }
53
63
  // Flag beats config; the two flags contradict each other by design.
54
64
  const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
55
65
  if (subscriptionOnly && opts.allowApiBilling) {
@@ -61,7 +71,13 @@ export function registerWorkflowCommand(program, emit) {
61
71
  process.exit(1);
62
72
  }
63
73
  try {
64
- const { runWorkflow } = await import("../core/workflow/engine.js");
74
+ const { createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
75
+ const workspace = opts.workspaceRoot
76
+ ? {
77
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
78
+ writableRoots: [resolve(opts.workspaceRoot)],
79
+ }
80
+ : undefined;
65
81
  const report = await runWorkflow(script, {
66
82
  coordRoot,
67
83
  spawners: registry.spawners(),
@@ -82,6 +98,7 @@ export function registerWorkflowCommand(program, emit) {
82
98
  approvalMode: "park",
83
99
  approvalAddressee: opts.approvalTo,
84
100
  isolation: opts.isolation,
101
+ workspace,
85
102
  // CLI harness subprocesses inherit the host network. A policy that
86
103
  // forbids network must therefore deny dispatch unless a future host
87
104
  // adapter creates and declares a network-disabled boundary.
@@ -118,7 +135,7 @@ export function registerWorkflowCommand(program, emit) {
118
135
  else {
119
136
  emit.text(`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
120
137
  `journal: ${err.journalPath}\n` +
121
- `resume after resolution: harn workflow resume ${err.runId}\n`);
138
+ `resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`);
122
139
  }
123
140
  return;
124
141
  }
@@ -146,12 +163,22 @@ export function registerWorkflowCommand(program, emit) {
146
163
  process.exit(1);
147
164
  }
148
165
  try {
149
- const { assertWorkflowRunResumable, runWorkflow } = await import("../core/workflow/index.js");
166
+ const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
150
167
  const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
168
+ const binding = manifest.execution.workspace_binding;
169
+ if (binding && binding.provider.id !== "local-git-worktree") {
170
+ throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
171
+ }
151
172
  const report = await runWorkflow(manifest.script.path, {
152
173
  coordRoot,
153
174
  spawners: registry.spawners(),
154
175
  resumeRunId: runId,
176
+ workspace: binding
177
+ ? {
178
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
179
+ writableRoots: [binding.writable_root.configured],
180
+ }
181
+ : undefined,
155
182
  harnessEvidence: Object.fromEntries(registry
156
183
  .list()
157
184
  .map((adapter) => [
@@ -199,6 +226,249 @@ export function registerWorkflowCommand(program, emit) {
199
226
  process.exit(1);
200
227
  }
201
228
  });
229
+ workflow
230
+ .command("workspace <run-id>")
231
+ .description("Show validated allocation, verification, integration, and cleanup state.")
232
+ .option("--json", "Emit the validated workspace status as JSON")
233
+ .action(async (runId, opts) => {
234
+ const coordRoot = findCoordRoot();
235
+ if (!coordRoot) {
236
+ emit.error({
237
+ code: "no_coord_root",
238
+ message: "no .harnery/ coordination root found; run `init` first",
239
+ });
240
+ process.exit(1);
241
+ }
242
+ const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import("../core/workflow/index.js");
243
+ const inspection = inspectWorkflowWorkspace(coordRoot, runId);
244
+ if (!inspection.ok) {
245
+ emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
246
+ process.exit(1);
247
+ }
248
+ if (opts.json) {
249
+ emit.config({ format: "json" });
250
+ emit.data(inspection.value);
251
+ return;
252
+ }
253
+ emit.text(renderWorkflowWorkspaceStatus(inspection.value));
254
+ });
255
+ workflow
256
+ .command("workspaces")
257
+ .description("List validated isolated and shared-compatibility workspace decisions.")
258
+ .option("--json", "Emit workspace inspections as JSON")
259
+ .action(async (opts) => {
260
+ const coordRoot = findCoordRoot();
261
+ if (!coordRoot) {
262
+ emit.error({
263
+ code: "no_coord_root",
264
+ message: "no .harnery/ coordination root found; run `init` first",
265
+ });
266
+ process.exit(1);
267
+ }
268
+ const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.js");
269
+ const inspections = listWorkflowWorkspaceInspections(coordRoot).filter((inspection) => !inspection.ok || inspection.value.selection !== "shared");
270
+ if (opts.json) {
271
+ emit.config({ format: "json" });
272
+ emit.data(inspections);
273
+ return;
274
+ }
275
+ if (inspections.length === 0) {
276
+ emit.text("no isolated or shared-compatibility workspace runs\n");
277
+ return;
278
+ }
279
+ emit.text(`${inspections
280
+ .map((inspection) => inspection.ok
281
+ ? [
282
+ inspection.value.run_id,
283
+ inspection.value.selection,
284
+ inspection.value.lifecycle.state,
285
+ `verify=${inspection.value.verification.status}`,
286
+ `integration=${inspection.value.integration.state}`,
287
+ `cleanup=${inspection.value.cleanup.state}`,
288
+ ].join("\t")
289
+ : `${inspection.run_id}\tinvalid\t${inspection.error}`)
290
+ .join("\n")}\n`);
291
+ });
292
+ const integration = workflow
293
+ .command("integration")
294
+ .description("Prepare and apply proof-gated isolated-workspace integration.");
295
+ integration
296
+ .command("prepare <run-id>")
297
+ .description("Preview integration and write exact review and policy authority.")
298
+ .option("--policy <file>", "Current host policy JSON/JSONC for the Git mutation")
299
+ .option("--reviewer <name>", "Explicit reviewer for a standalone workflow")
300
+ .option("--reason <text>", "Reason recorded with the standalone review")
301
+ .option("--target-root <dir>", "Explicit target checkout (default: frozen source checkout)")
302
+ .option("--accept-unknown <code>", "Accept one verification unknown by code (repeatable)", collectOption, [])
303
+ .option("--approval-to <address>", "Address a durable policy ASK request")
304
+ .option("--json", "Emit the durable integration plan as JSON")
305
+ .action(async (runId, opts) => {
306
+ const coordRoot = findCoordRoot();
307
+ if (!coordRoot) {
308
+ emit.error({
309
+ code: "no_coord_root",
310
+ message: "no .harnery/ coordination root found; run `init` first",
311
+ });
312
+ process.exit(1);
313
+ }
314
+ if (!opts.policy) {
315
+ emit.error({
316
+ code: "integration_policy_required",
317
+ message: "integration prepare requires --policy <file>",
318
+ });
319
+ process.exit(1);
320
+ }
321
+ if (opts.reason && !opts.reviewer) {
322
+ emit.error({
323
+ code: "integration_reviewer_required",
324
+ message: "--reason requires --reviewer <name>",
325
+ });
326
+ process.exit(1);
327
+ }
328
+ try {
329
+ const provider = await builtInProviderForRun(coordRoot, runId);
330
+ const { IntegrationPrepareParkedError, prepareIntegration } = await import("../core/workflow/index.js");
331
+ try {
332
+ const plan = await prepareIntegration({
333
+ coordRoot,
334
+ runId,
335
+ provider,
336
+ policy: loadPolicyFile(opts.policy),
337
+ targetRoot: opts.targetRoot,
338
+ review: opts.reviewer ? { actor: opts.reviewer, reason: opts.reason } : undefined,
339
+ acceptedUnknowns: opts.acceptUnknown,
340
+ approvalAddressee: opts.approvalTo,
341
+ });
342
+ if (opts.json) {
343
+ emit.config({ format: "json" });
344
+ emit.data(plan);
345
+ return;
346
+ }
347
+ emit.text(`integration plan ${plan.plan_id} authorized\n` +
348
+ `source: ${plan.provider_preview.source_commit}\n` +
349
+ `target: ${plan.provider_preview.target_ref} at ${plan.provider_preview.target_commit}\n` +
350
+ `changes: ${plan.provider_preview.changed_paths.length}\n` +
351
+ `apply: ${resolveBinName()} workflow integration apply ${runId} --yes\n`);
352
+ }
353
+ catch (error) {
354
+ if (error instanceof IntegrationPrepareParkedError) {
355
+ if (opts.json) {
356
+ emit.config({ format: "json" });
357
+ emit.data({
358
+ status: "parked",
359
+ runId: error.runId,
360
+ planId: error.planId,
361
+ approvalId: error.approvalId,
362
+ });
363
+ }
364
+ else {
365
+ emit.text(`integration preparation parked\n` +
366
+ `run: ${error.runId}\n` +
367
+ `plan: ${error.planId}\n` +
368
+ `approval: ${error.approvalId}\n` +
369
+ `approve: ${resolveBinName()} workflow approvals approve ${error.approvalId}\n`);
370
+ }
371
+ return;
372
+ }
373
+ throw error;
374
+ }
375
+ }
376
+ catch (error) {
377
+ emit.error({
378
+ code: "workflow_integration_prepare_failed",
379
+ message: error instanceof Error ? error.message : String(error),
380
+ });
381
+ process.exit(1);
382
+ }
383
+ });
384
+ integration
385
+ .command("apply <run-id>")
386
+ .description("Apply an authorized durable integration plan.")
387
+ .option("--yes", "Confirm the target-branch mutation")
388
+ .option("--json", "Emit the durable integration receipt as JSON")
389
+ .action(async (runId, opts) => {
390
+ const coordRoot = findCoordRoot();
391
+ if (!coordRoot) {
392
+ emit.error({
393
+ code: "no_coord_root",
394
+ message: "no .harnery/ coordination root found; run `init` first",
395
+ });
396
+ process.exit(1);
397
+ }
398
+ if (!opts.yes) {
399
+ emit.error({
400
+ code: "integration_confirmation_required",
401
+ message: "integration apply changes the target branch; pass --yes to confirm",
402
+ });
403
+ process.exit(1);
404
+ }
405
+ try {
406
+ const provider = await builtInProviderForRun(coordRoot, runId);
407
+ const { applyIntegration } = await import("../core/workflow/index.js");
408
+ const receipt = await applyIntegration({ coordRoot, runId, provider });
409
+ if (opts.json) {
410
+ emit.config({ format: "json" });
411
+ emit.data(receipt);
412
+ return;
413
+ }
414
+ emit.text(`integration ${receipt.status}: ${receipt.target_ref} at ${receipt.target_commit}\n` +
415
+ `receipt: ${receipt.receipt_id}\n` +
416
+ `cleanup: ${resolveBinName()} workflow cleanup ${runId} --yes\n`);
417
+ }
418
+ catch (error) {
419
+ emit.error({
420
+ code: "workflow_integration_apply_failed",
421
+ message: error instanceof Error ? error.message : String(error),
422
+ });
423
+ process.exit(1);
424
+ }
425
+ });
426
+ workflow
427
+ .command("cleanup <run-id>")
428
+ .description("Conservatively release a provider-owned isolated workspace.")
429
+ .option("--yes", "Confirm the worktree and provider-branch removal attempt")
430
+ .option("--json", "Emit the cleanup attempt or receipt as JSON")
431
+ .action(async (runId, opts) => {
432
+ const coordRoot = findCoordRoot();
433
+ if (!coordRoot) {
434
+ emit.error({
435
+ code: "no_coord_root",
436
+ message: "no .harnery/ coordination root found; run `init` first",
437
+ });
438
+ process.exit(1);
439
+ }
440
+ if (!opts.yes) {
441
+ emit.error({
442
+ code: "cleanup_confirmation_required",
443
+ message: "workspace cleanup may remove a worktree and branch; pass --yes to confirm",
444
+ });
445
+ process.exit(1);
446
+ }
447
+ try {
448
+ const provider = await builtInProviderForRun(coordRoot, runId);
449
+ const { cleanupWorkspace } = await import("../core/workflow/index.js");
450
+ const result = await cleanupWorkspace({ coordRoot, runId, provider });
451
+ if (opts.json) {
452
+ emit.config({ format: "json" });
453
+ emit.data(result);
454
+ return;
455
+ }
456
+ const receipt = "receipt_id" in result && typeof result.receipt_id === "string"
457
+ ? `\nreceipt: ${result.receipt_id}`
458
+ : "";
459
+ const reason = "reason" in result && typeof result.reason === "string"
460
+ ? `\nreason: ${result.reason}`
461
+ : "";
462
+ emit.text(`workspace cleanup ${result.status}: ${result.binding_id}${receipt}${reason}\n`);
463
+ }
464
+ catch (error) {
465
+ emit.error({
466
+ code: "workflow_cleanup_failed",
467
+ message: error instanceof Error ? error.message : String(error),
468
+ });
469
+ process.exit(1);
470
+ }
471
+ });
202
472
  const approvals = workflow
203
473
  .command("approvals")
204
474
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -290,7 +560,7 @@ export function registerWorkflowCommand(program, emit) {
290
560
  else {
291
561
  emit.text(`${renderWorkflowApproval(resolved.approval)}` +
292
562
  `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
293
- `resume: harn workflow resume ${resolved.approval.request.run_id}\n`);
563
+ approvalNextActionHint(coordRoot, resolved.approval));
294
564
  }
295
565
  }
296
566
  catch (err) {
@@ -333,3 +603,30 @@ export function registerWorkflowCommand(program, emit) {
333
603
  }
334
604
  });
335
605
  }
606
+ async function builtInProviderForRun(coordRoot, runId) {
607
+ const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import("../core/workflow/index.js");
608
+ const manifest = readWorkflowRunManifest(coordRoot, runId);
609
+ const binding = manifest.execution.workspace_binding;
610
+ if (!binding) {
611
+ throw new Error(`workflow run ${runId} has no isolated workspace binding`);
612
+ }
613
+ if (binding.provider.id !== "local-git-worktree") {
614
+ throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
615
+ }
616
+ return createLocalGitWorktreeProvider({ coordRoot });
617
+ }
618
+ function collectOption(value, previous) {
619
+ return [...previous, value];
620
+ }
621
+ /** Next operator command after resolving an approval. Mid-run parks resume the
622
+ * workflow; integration prepare parks after the run is already terminal, so
623
+ * the durable plan is authorized by re-running prepare (resume would fail). */
624
+ function approvalNextActionHint(coordRoot, approval) {
625
+ const bin = resolveBinName();
626
+ const runId = approval.request.run_id;
627
+ const integrationPlanPath = join(coordRoot, ".harnery", "workflows", runId, "integration", "plan.json");
628
+ if (existsSync(integrationPlanPath)) {
629
+ return `prepare: ${bin} workflow integration prepare ${runId}\n`;
630
+ }
631
+ return `resume: ${bin} workflow resume ${runId}\n`;
632
+ }
@@ -33,6 +33,10 @@ export interface WorkEvent {
33
33
  run_id?: string;
34
34
  attempt?: number;
35
35
  trigger?: WorkAttemptTrigger;
36
+ workspace_binding_id?: string;
37
+ proof_sha256?: string;
38
+ terminal_attestation_sha256?: string;
39
+ accepted_unknowns?: string[];
36
40
  state?: WorkState;
37
41
  next_action?: WorkNextAction;
38
42
  }
@@ -85,6 +89,7 @@ export declare function reconcileAllWorkItems(coordRoot: string, actor?: string)
85
89
  export declare function acceptWorkItem(coordRoot: string, workId: string, input?: {
86
90
  actor?: string;
87
91
  reason?: string;
92
+ acceptedUnknowns?: readonly string[];
88
93
  }): WorkRecord;
89
94
  export declare function cancelWorkItem(coordRoot: string, workId: string, input?: {
90
95
  actor?: string;
@@ -107,6 +112,8 @@ export declare function readWorkItemIgnoringLease(coordRoot: string, workId: str
107
112
  export declare function assertWorkflowUnchanged(intent: WorkIntent): void;
108
113
  /** @internal Runner seam; not a separate persistence contract. */
109
114
  export declare function acquireWorkLease(coordRoot: string, workId: string): () => void;
115
+ /** @internal Work journal mutation seam. */
116
+ export declare function acquireWorkEventLease(coordRoot: string, workId: string): () => void;
110
117
  /** @internal Runner seam; not a separate persistence contract. */
111
118
  export declare function boundedActor(value?: string, fallback?: string): string;
112
119
  //# sourceMappingURL=state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { readWorkflowApproval } from "../workflow/approvals.js";
6
+ import { stableDigest } from "../workflow/durable-record.js";
6
7
  import { readWorkflowProof } from "../workflow/proof.js";
7
8
  import { readWorkflowRunManifest, workflowScriptDigest } from "../workflow/run-state.js";
9
+ import { isWorkspaceBoundExecutionEvidence } from "../workflow/workspaces/validate.js";
8
10
  export const WORK_INTENT_SCHEMA_VERSION = 1;
9
11
  export const WORK_EVENT_SCHEMA_VERSION = 1;
10
12
  const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
@@ -135,7 +137,7 @@ export function acceptWorkItem(coordRoot, workId, input = {}) {
135
137
  return appendGovernanceEvent(coordRoot, workId, "work.accepted", ["in_review"], input);
136
138
  }
137
139
  export function cancelWorkItem(coordRoot, workId, input = {}) {
138
- return appendGovernanceEvent(coordRoot, workId, "work.cancelled", ["waiting", "ready", "blocked", "in_review"], input);
140
+ return appendGovernanceEvent(coordRoot, workId, "work.cancelled", ["waiting", "ready", "running", "awaiting_approval", "blocked", "in_review"], input);
139
141
  }
140
142
  export function reopenWorkItem(coordRoot, workId, input = {}) {
141
143
  return appendGovernanceEvent(coordRoot, workId, "work.reopened", ["blocked", "in_review", "succeeded", "cancelled"], input);
@@ -340,49 +342,107 @@ function workContextForIntent(intent) {
340
342
  };
341
343
  }
342
344
  function appendGovernanceEvent(coordRoot, workId, event, allowed, input) {
343
- const release = acquireWorkLease(coordRoot, workId);
345
+ const releaseWork = event === "work.cancelled" ? undefined : acquireWorkLease(coordRoot, workId);
346
+ const releaseEvent = acquireWorkEventLease(coordRoot, workId);
344
347
  try {
345
- const current = readWorkItemIgnoringLease(coordRoot, workId);
348
+ const current = releaseWork
349
+ ? readWorkItemIgnoringLease(coordRoot, workId)
350
+ : readWorkItem(coordRoot, workId);
346
351
  if (!allowed.includes(current.projection.state)) {
347
352
  throw new Error(`work item ${workId} cannot ${event.replace("work.", "")} from state ${current.projection.state}`);
348
353
  }
349
- appendWorkEvent(coordRoot, workId, {
354
+ const acceptance = event === "work.accepted"
355
+ ? acceptanceAuthority(coordRoot, current, input.acceptedUnknowns ?? [])
356
+ : undefined;
357
+ appendWorkEventUnderLease(coordRoot, workId, {
350
358
  event,
351
359
  actor: boundedActor(input.actor),
352
360
  reason: boundedOptional(input.reason, "work reason", MAX_REASON),
361
+ ...acceptance,
353
362
  });
354
363
  }
355
364
  finally {
356
- release();
365
+ releaseEvent();
366
+ releaseWork?.();
357
367
  }
358
368
  return reconcileWorkItem(coordRoot, workId, input.actor);
359
369
  }
370
+ function acceptanceAuthority(coordRoot, current, acceptedUnknowns) {
371
+ const latest = current.projection.attempts.at(-1);
372
+ if (!latest?.proof_path)
373
+ throw new Error("work acceptance requires terminal workflow proof");
374
+ const manifest = readWorkflowRunManifest(coordRoot, latest.run_id);
375
+ const proof = readWorkflowProof(coordRoot, latest.run_id);
376
+ if (proof.run.work_item_id !== current.intent.id ||
377
+ proof.run.attempt_context?.number !== latest.number) {
378
+ throw new Error("work acceptance proof does not match the durable attempt");
379
+ }
380
+ const accepted = [...new Set(acceptedUnknowns)].sort();
381
+ for (const code of accepted) {
382
+ boundedString(code, "accepted unknown code", 200);
383
+ }
384
+ const execution = proof.execution;
385
+ return {
386
+ run_id: latest.run_id,
387
+ attempt: latest.number,
388
+ proof_sha256: createHash("sha256").update(readFileSync(latest.proof_path)).digest("hex"),
389
+ workspace_binding_id: manifest.execution.workspace_binding?.binding_id,
390
+ terminal_attestation_sha256: isWorkspaceBoundExecutionEvidence(execution)
391
+ ? stableDigest(execution.terminal_attestation)
392
+ : undefined,
393
+ accepted_unknowns: execution ? accepted : undefined,
394
+ };
395
+ }
360
396
  /** @internal Runner seam; not a separate persistence contract. */
361
397
  export function appendReconcileIfChanged(coordRoot, record, actor) {
362
- const last = [...record.events].reverse().find((event) => event.event === "work.reconciled");
363
- if (last?.state === record.projection.state &&
364
- last.reason === record.projection.reason &&
365
- last.next_action === record.projection.next_action) {
366
- return;
398
+ let release;
399
+ try {
400
+ release = acquireWorkEventLease(coordRoot, record.intent.id);
401
+ }
402
+ catch (error) {
403
+ if (workEventLeaseIsLive(coordRoot, record.intent.id))
404
+ return;
405
+ throw error;
406
+ }
407
+ try {
408
+ const current = readWorkItemIgnoringLease(coordRoot, record.intent.id);
409
+ const last = [...current.events].reverse().find((event) => event.event === "work.reconciled");
410
+ if (last?.state === current.projection.state &&
411
+ last.reason === current.projection.reason &&
412
+ last.next_action === current.projection.next_action) {
413
+ return;
414
+ }
415
+ appendWorkEventUnderLease(coordRoot, current.intent.id, {
416
+ event: "work.reconciled",
417
+ actor,
418
+ reason: current.projection.reason,
419
+ state: current.projection.state,
420
+ next_action: current.projection.next_action,
421
+ });
422
+ }
423
+ finally {
424
+ release();
367
425
  }
368
- appendWorkEvent(coordRoot, record.intent.id, {
369
- event: "work.reconciled",
370
- actor,
371
- reason: record.projection.reason,
372
- state: record.projection.state,
373
- next_action: record.projection.next_action,
374
- });
375
426
  }
376
427
  /** @internal Runner seam; not a separate persistence contract. */
377
428
  export function appendWorkEvent(coordRoot, workId, input) {
429
+ const release = acquireWorkEventLease(coordRoot, workId);
430
+ try {
431
+ return appendWorkEventUnderLease(coordRoot, workId, input);
432
+ }
433
+ finally {
434
+ release();
435
+ }
436
+ }
437
+ function appendWorkEventUnderLease(coordRoot, workId, input) {
378
438
  const events = readWorkEvents(coordRoot, workId);
379
439
  if (events.length === 0 && input.event !== "work.created") {
380
- appendWorkEvent(coordRoot, workId, {
440
+ appendWorkEventUnderLease(coordRoot, workId, {
381
441
  event: "work.created",
382
442
  actor: "recovery",
383
443
  reason: "recovered creation receipt from immutable intent",
384
444
  });
385
- return appendWorkEvent(coordRoot, workId, input);
445
+ return appendWorkEventUnderLease(coordRoot, workId, input);
386
446
  }
387
447
  if (events.length >= MAX_EVENTS)
388
448
  throw new Error(`work item ${workId} exceeds ${MAX_EVENTS} events`);
@@ -479,7 +539,9 @@ function validateWorkEvent(event, workId, seq) {
479
539
  }
480
540
  boundedActor(event.actor);
481
541
  boundedOptional(event.reason, "work reason", MAX_REASON);
482
- if (event.event === "attempt.started" || event.event === "attempt.resumed") {
542
+ if (event.event === "attempt.started" ||
543
+ event.event === "attempt.resumed" ||
544
+ (event.event === "work.accepted" && event.run_id !== undefined)) {
483
545
  if (!event.run_id ||
484
546
  !RUN_ID.test(event.run_id) ||
485
547
  !Number.isSafeInteger(event.attempt) ||
@@ -487,6 +549,22 @@ function validateWorkEvent(event, workId, seq) {
487
549
  throw new Error(`work item ${workId} event ${seq} has invalid attempt data`);
488
550
  }
489
551
  }
552
+ if (event.event === "work.accepted" && event.run_id !== undefined) {
553
+ if (event.workspace_binding_id !== undefined) {
554
+ boundedString(event.workspace_binding_id, "workspace binding id", 200);
555
+ }
556
+ if (!/^[a-f0-9]{64}$/.test(event.proof_sha256 ?? "")) {
557
+ throw new Error(`work item ${workId} event ${seq} has invalid proof digest`);
558
+ }
559
+ if (event.workspace_binding_id !== undefined) {
560
+ if (!/^[a-f0-9]{64}$/.test(event.terminal_attestation_sha256 ?? "") ||
561
+ !Array.isArray(event.accepted_unknowns) ||
562
+ event.accepted_unknowns.some((code) => typeof code !== "string" || code.length === 0 || code.length > 200) ||
563
+ new Set(event.accepted_unknowns).size !== event.accepted_unknowns.length) {
564
+ throw new Error(`work item ${workId} event ${seq} has invalid execution review evidence`);
565
+ }
566
+ }
567
+ }
490
568
  if (event.trigger !== undefined &&
491
569
  (event.event !== "attempt.started" ||
492
570
  (event.trigger !== "initial" && event.trigger !== "retry"))) {
@@ -573,8 +651,15 @@ function workDir(coordRoot, workId) {
573
651
  }
574
652
  /** @internal Runner seam; not a separate persistence contract. */
575
653
  export function acquireWorkLease(coordRoot, workId) {
654
+ return acquireWorkFileLease(coordRoot, workId, "lease.json", "is already active");
655
+ }
656
+ /** @internal Work journal mutation seam. */
657
+ export function acquireWorkEventLease(coordRoot, workId) {
658
+ return acquireWorkFileLease(coordRoot, workId, "events.lease.json", "event journal is already active");
659
+ }
660
+ function acquireWorkFileLease(coordRoot, workId, filename, activeMessage) {
576
661
  readWorkIntent(coordRoot, workId);
577
- const path = join(workDir(coordRoot, workId), "lease.json");
662
+ const path = join(workDir(coordRoot, workId), filename);
578
663
  const owner = {
579
664
  pid: process.pid,
580
665
  host: hostname(),
@@ -602,7 +687,7 @@ export function acquireWorkLease(coordRoot, workId) {
602
687
  if (!acquire()) {
603
688
  const existing = readLease(path);
604
689
  if (existing && leaseIsLive(existing)) {
605
- throw new Error(`work item ${workId} is already active under pid ${existing.pid} on ${existing.host}`);
690
+ throw new Error(`work item ${workId} ${activeMessage} under pid ${existing.pid} on ${existing.host}`);
606
691
  }
607
692
  unlinkSync(path);
608
693
  if (!acquire())
@@ -623,6 +708,10 @@ function workLeaseIsLive(coordRoot, workId) {
623
708
  const lease = readLease(join(workDir(coordRoot, workId), "lease.json"));
624
709
  return lease ? leaseIsLive(lease) : false;
625
710
  }
711
+ function workEventLeaseIsLive(coordRoot, workId) {
712
+ const lease = readLease(join(workDir(coordRoot, workId), "events.lease.json"));
713
+ return lease ? leaseIsLive(lease) : false;
714
+ }
626
715
  function workflowResumeLeaseIsLive(coordRoot, runId) {
627
716
  const lease = readLease(join(coordRoot, ".harnery", "workflows", runId, "resume.lock"));
628
717
  return lease ? leaseIsLive(lease) : false;