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
@@ -1,12 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
1
3
  import type { Command } from "commander";
2
4
  import type { EmitContext } from "../commander.ts";
3
- import { workflowSubscriptionOnly } from "../core/config.ts";
5
+ import { resolveBinName, workflowSubscriptionOnly } from "../core/config.ts";
4
6
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
5
7
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
6
8
  import type { PolicyIsolation } from "../core/policy/index.ts";
7
9
  import { loadPolicyFile } from "../core/policy/index.ts";
8
- import type { WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
10
+ import type { WorkflowApproval, WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
9
11
  import { WorkflowParkedError } from "../core/workflow/engine.ts";
12
+ import type { WorkspaceProvider } from "../core/workflow/index.ts";
10
13
 
11
14
  /**
12
15
  * `workflow run <script>`: execute a workflow script — bounded, schema-gated,
@@ -28,6 +31,7 @@ interface WorkflowRunOpts {
28
31
  allowApiBilling?: boolean;
29
32
  policy?: string;
30
33
  isolation?: PolicyIsolation;
34
+ workspaceRoot?: string;
31
35
  approvalTo?: string;
32
36
  json?: boolean;
33
37
  }
@@ -47,6 +51,21 @@ interface WorkflowApprovalDecisionOpts {
47
51
  json?: boolean;
48
52
  }
49
53
 
54
+ interface WorkflowIntegrationPrepareOpts {
55
+ policy?: string;
56
+ reviewer?: string;
57
+ reason?: string;
58
+ targetRoot?: string;
59
+ acceptUnknown?: string[];
60
+ approvalTo?: string;
61
+ json?: boolean;
62
+ }
63
+
64
+ interface WorkflowConfirmedMutationOpts {
65
+ yes?: boolean;
66
+ json?: boolean;
67
+ }
68
+
50
69
  export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
51
70
  const registry = createBuiltinHarnessRegistry();
52
71
  const harnesses = registry.ids();
@@ -89,6 +108,10 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
89
108
  "--isolation <mode>",
90
109
  "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)",
91
110
  )
111
+ .option(
112
+ "--workspace-root <dir>",
113
+ "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)",
114
+ )
92
115
  .option("--approval-to <address>", "Address durable ASK requests (default: operator)")
93
116
  .option("--json", "Emit the full RunReport as JSON")
94
117
  .action(async (script: string, opts: WorkflowRunOpts) => {
@@ -117,6 +140,13 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
117
140
  });
118
141
  process.exit(1);
119
142
  }
143
+ if (opts.workspaceRoot && opts.isolation !== "worktree") {
144
+ emit.error({
145
+ code: "bad_workspace_root",
146
+ message: "--workspace-root requires --isolation worktree",
147
+ });
148
+ process.exit(1);
149
+ }
120
150
  // Flag beats config; the two flags contradict each other by design.
121
151
  const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
122
152
  if (subscriptionOnly && opts.allowApiBilling) {
@@ -129,7 +159,15 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
129
159
  process.exit(1);
130
160
  }
131
161
  try {
132
- const { runWorkflow } = await import("../core/workflow/engine.ts");
162
+ const { createLocalGitWorktreeProvider, runWorkflow } = await import(
163
+ "../core/workflow/index.ts"
164
+ );
165
+ const workspace = opts.workspaceRoot
166
+ ? {
167
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
168
+ writableRoots: [resolve(opts.workspaceRoot)],
169
+ }
170
+ : undefined;
133
171
  const report = await runWorkflow(script, {
134
172
  coordRoot,
135
173
  spawners: registry.spawners(),
@@ -152,6 +190,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
152
190
  approvalMode: "park",
153
191
  approvalAddressee: opts.approvalTo,
154
192
  isolation: opts.isolation,
193
+ workspace,
155
194
  // CLI harness subprocesses inherit the host network. A policy that
156
195
  // forbids network must therefore deny dispatch unless a future host
157
196
  // adapter creates and declares a network-disabled boundary.
@@ -189,7 +228,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
189
228
  emit.text(
190
229
  `run ${err.runId} parked\napproval: ${err.approvalId}\n` +
191
230
  `journal: ${err.journalPath}\n` +
192
- `resume after resolution: harn workflow resume ${err.runId}\n`,
231
+ `resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`,
193
232
  );
194
233
  }
195
234
  return;
@@ -220,14 +259,25 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
220
259
  process.exit(1);
221
260
  }
222
261
  try {
223
- const { assertWorkflowRunResumable, runWorkflow } = await import(
224
- "../core/workflow/index.ts"
225
- );
262
+ const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } =
263
+ await import("../core/workflow/index.ts");
226
264
  const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
265
+ const binding = manifest.execution.workspace_binding;
266
+ if (binding && binding.provider.id !== "local-git-worktree") {
267
+ throw new Error(
268
+ `workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`,
269
+ );
270
+ }
227
271
  const report = await runWorkflow(manifest.script.path, {
228
272
  coordRoot,
229
273
  spawners: registry.spawners(),
230
274
  resumeRunId: runId,
275
+ workspace: binding
276
+ ? {
277
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
278
+ writableRoots: [binding.writable_root.configured],
279
+ }
280
+ : undefined,
231
281
  harnessEvidence: Object.fromEntries(
232
282
  registry
233
283
  .list()
@@ -281,6 +331,273 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
281
331
  }
282
332
  });
283
333
 
334
+ workflow
335
+ .command("workspace <run-id>")
336
+ .description("Show validated allocation, verification, integration, and cleanup state.")
337
+ .option("--json", "Emit the validated workspace status as JSON")
338
+ .action(async (runId: string, opts: { json?: boolean }) => {
339
+ const coordRoot = findCoordRoot();
340
+ if (!coordRoot) {
341
+ emit.error({
342
+ code: "no_coord_root",
343
+ message: "no .harnery/ coordination root found; run `init` first",
344
+ });
345
+ process.exit(1);
346
+ }
347
+ const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import(
348
+ "../core/workflow/index.ts"
349
+ );
350
+ const inspection = inspectWorkflowWorkspace(coordRoot, runId);
351
+ if (!inspection.ok) {
352
+ emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
353
+ process.exit(1);
354
+ }
355
+ if (opts.json) {
356
+ emit.config({ format: "json" });
357
+ emit.data(inspection.value);
358
+ return;
359
+ }
360
+ emit.text(renderWorkflowWorkspaceStatus(inspection.value));
361
+ });
362
+
363
+ workflow
364
+ .command("workspaces")
365
+ .description("List validated isolated and shared-compatibility workspace decisions.")
366
+ .option("--json", "Emit workspace inspections as JSON")
367
+ .action(async (opts: { json?: boolean }) => {
368
+ const coordRoot = findCoordRoot();
369
+ if (!coordRoot) {
370
+ emit.error({
371
+ code: "no_coord_root",
372
+ message: "no .harnery/ coordination root found; run `init` first",
373
+ });
374
+ process.exit(1);
375
+ }
376
+ const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.ts");
377
+ const inspections = listWorkflowWorkspaceInspections(coordRoot).filter(
378
+ (inspection) => !inspection.ok || inspection.value.selection !== "shared",
379
+ );
380
+ if (opts.json) {
381
+ emit.config({ format: "json" });
382
+ emit.data(inspections);
383
+ return;
384
+ }
385
+ if (inspections.length === 0) {
386
+ emit.text("no isolated or shared-compatibility workspace runs\n");
387
+ return;
388
+ }
389
+ emit.text(
390
+ `${inspections
391
+ .map((inspection) =>
392
+ inspection.ok
393
+ ? [
394
+ inspection.value.run_id,
395
+ inspection.value.selection,
396
+ inspection.value.lifecycle.state,
397
+ `verify=${inspection.value.verification.status}`,
398
+ `integration=${inspection.value.integration.state}`,
399
+ `cleanup=${inspection.value.cleanup.state}`,
400
+ ].join("\t")
401
+ : `${inspection.run_id}\tinvalid\t${inspection.error}`,
402
+ )
403
+ .join("\n")}\n`,
404
+ );
405
+ });
406
+
407
+ const integration = workflow
408
+ .command("integration")
409
+ .description("Prepare and apply proof-gated isolated-workspace integration.");
410
+
411
+ integration
412
+ .command("prepare <run-id>")
413
+ .description("Preview integration and write exact review and policy authority.")
414
+ .option("--policy <file>", "Current host policy JSON/JSONC for the Git mutation")
415
+ .option("--reviewer <name>", "Explicit reviewer for a standalone workflow")
416
+ .option("--reason <text>", "Reason recorded with the standalone review")
417
+ .option("--target-root <dir>", "Explicit target checkout (default: frozen source checkout)")
418
+ .option(
419
+ "--accept-unknown <code>",
420
+ "Accept one verification unknown by code (repeatable)",
421
+ collectOption,
422
+ [],
423
+ )
424
+ .option("--approval-to <address>", "Address a durable policy ASK request")
425
+ .option("--json", "Emit the durable integration plan as JSON")
426
+ .action(async (runId: string, opts: WorkflowIntegrationPrepareOpts) => {
427
+ const coordRoot = findCoordRoot();
428
+ if (!coordRoot) {
429
+ emit.error({
430
+ code: "no_coord_root",
431
+ message: "no .harnery/ coordination root found; run `init` first",
432
+ });
433
+ process.exit(1);
434
+ }
435
+ if (!opts.policy) {
436
+ emit.error({
437
+ code: "integration_policy_required",
438
+ message: "integration prepare requires --policy <file>",
439
+ });
440
+ process.exit(1);
441
+ }
442
+ if (opts.reason && !opts.reviewer) {
443
+ emit.error({
444
+ code: "integration_reviewer_required",
445
+ message: "--reason requires --reviewer <name>",
446
+ });
447
+ process.exit(1);
448
+ }
449
+ try {
450
+ const provider = await builtInProviderForRun(coordRoot, runId);
451
+ const { IntegrationPrepareParkedError, prepareIntegration } = await import(
452
+ "../core/workflow/index.ts"
453
+ );
454
+ try {
455
+ const plan = await prepareIntegration({
456
+ coordRoot,
457
+ runId,
458
+ provider,
459
+ policy: loadPolicyFile(opts.policy),
460
+ targetRoot: opts.targetRoot,
461
+ review: opts.reviewer ? { actor: opts.reviewer, reason: opts.reason } : undefined,
462
+ acceptedUnknowns: opts.acceptUnknown,
463
+ approvalAddressee: opts.approvalTo,
464
+ });
465
+ if (opts.json) {
466
+ emit.config({ format: "json" });
467
+ emit.data(plan);
468
+ return;
469
+ }
470
+ emit.text(
471
+ `integration plan ${plan.plan_id} authorized\n` +
472
+ `source: ${plan.provider_preview.source_commit}\n` +
473
+ `target: ${plan.provider_preview.target_ref} at ${plan.provider_preview.target_commit}\n` +
474
+ `changes: ${plan.provider_preview.changed_paths.length}\n` +
475
+ `apply: ${resolveBinName()} workflow integration apply ${runId} --yes\n`,
476
+ );
477
+ } catch (error) {
478
+ if (error instanceof IntegrationPrepareParkedError) {
479
+ if (opts.json) {
480
+ emit.config({ format: "json" });
481
+ emit.data({
482
+ status: "parked",
483
+ runId: error.runId,
484
+ planId: error.planId,
485
+ approvalId: error.approvalId,
486
+ });
487
+ } else {
488
+ emit.text(
489
+ `integration preparation parked\n` +
490
+ `run: ${error.runId}\n` +
491
+ `plan: ${error.planId}\n` +
492
+ `approval: ${error.approvalId}\n` +
493
+ `approve: ${resolveBinName()} workflow approvals approve ${error.approvalId}\n`,
494
+ );
495
+ }
496
+ return;
497
+ }
498
+ throw error;
499
+ }
500
+ } catch (error) {
501
+ emit.error({
502
+ code: "workflow_integration_prepare_failed",
503
+ message: error instanceof Error ? error.message : String(error),
504
+ });
505
+ process.exit(1);
506
+ }
507
+ });
508
+
509
+ integration
510
+ .command("apply <run-id>")
511
+ .description("Apply an authorized durable integration plan.")
512
+ .option("--yes", "Confirm the target-branch mutation")
513
+ .option("--json", "Emit the durable integration receipt as JSON")
514
+ .action(async (runId: string, opts: WorkflowConfirmedMutationOpts) => {
515
+ const coordRoot = findCoordRoot();
516
+ if (!coordRoot) {
517
+ emit.error({
518
+ code: "no_coord_root",
519
+ message: "no .harnery/ coordination root found; run `init` first",
520
+ });
521
+ process.exit(1);
522
+ }
523
+ if (!opts.yes) {
524
+ emit.error({
525
+ code: "integration_confirmation_required",
526
+ message: "integration apply changes the target branch; pass --yes to confirm",
527
+ });
528
+ process.exit(1);
529
+ }
530
+ try {
531
+ const provider = await builtInProviderForRun(coordRoot, runId);
532
+ const { applyIntegration } = await import("../core/workflow/index.ts");
533
+ const receipt = await applyIntegration({ coordRoot, runId, provider });
534
+ if (opts.json) {
535
+ emit.config({ format: "json" });
536
+ emit.data(receipt);
537
+ return;
538
+ }
539
+ emit.text(
540
+ `integration ${receipt.status}: ${receipt.target_ref} at ${receipt.target_commit}\n` +
541
+ `receipt: ${receipt.receipt_id}\n` +
542
+ `cleanup: ${resolveBinName()} workflow cleanup ${runId} --yes\n`,
543
+ );
544
+ } catch (error) {
545
+ emit.error({
546
+ code: "workflow_integration_apply_failed",
547
+ message: error instanceof Error ? error.message : String(error),
548
+ });
549
+ process.exit(1);
550
+ }
551
+ });
552
+
553
+ workflow
554
+ .command("cleanup <run-id>")
555
+ .description("Conservatively release a provider-owned isolated workspace.")
556
+ .option("--yes", "Confirm the worktree and provider-branch removal attempt")
557
+ .option("--json", "Emit the cleanup attempt or receipt as JSON")
558
+ .action(async (runId: string, opts: WorkflowConfirmedMutationOpts) => {
559
+ const coordRoot = findCoordRoot();
560
+ if (!coordRoot) {
561
+ emit.error({
562
+ code: "no_coord_root",
563
+ message: "no .harnery/ coordination root found; run `init` first",
564
+ });
565
+ process.exit(1);
566
+ }
567
+ if (!opts.yes) {
568
+ emit.error({
569
+ code: "cleanup_confirmation_required",
570
+ message: "workspace cleanup may remove a worktree and branch; pass --yes to confirm",
571
+ });
572
+ process.exit(1);
573
+ }
574
+ try {
575
+ const provider = await builtInProviderForRun(coordRoot, runId);
576
+ const { cleanupWorkspace } = await import("../core/workflow/index.ts");
577
+ const result = await cleanupWorkspace({ coordRoot, runId, provider });
578
+ if (opts.json) {
579
+ emit.config({ format: "json" });
580
+ emit.data(result);
581
+ return;
582
+ }
583
+ const receipt =
584
+ "receipt_id" in result && typeof result.receipt_id === "string"
585
+ ? `\nreceipt: ${result.receipt_id}`
586
+ : "";
587
+ const reason =
588
+ "reason" in result && typeof result.reason === "string"
589
+ ? `\nreason: ${result.reason}`
590
+ : "";
591
+ emit.text(`workspace cleanup ${result.status}: ${result.binding_id}${receipt}${reason}\n`);
592
+ } catch (error) {
593
+ emit.error({
594
+ code: "workflow_cleanup_failed",
595
+ message: error instanceof Error ? error.message : String(error),
596
+ });
597
+ process.exit(1);
598
+ }
599
+ });
600
+
284
601
  const approvals = workflow
285
602
  .command("approvals")
286
603
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -382,7 +699,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
382
699
  emit.text(
383
700
  `${renderWorkflowApproval(resolved.approval)}` +
384
701
  `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
385
- `resume: harn workflow resume ${resolved.approval.request.run_id}\n`,
702
+ approvalNextActionHint(coordRoot, resolved.approval),
386
703
  );
387
704
  }
388
705
  } catch (err) {
@@ -427,3 +744,44 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
427
744
  }
428
745
  });
429
746
  }
747
+
748
+ async function builtInProviderForRun(coordRoot: string, runId: string): Promise<WorkspaceProvider> {
749
+ const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
750
+ "../core/workflow/index.ts"
751
+ );
752
+ const manifest = readWorkflowRunManifest(coordRoot, runId);
753
+ const binding = manifest.execution.workspace_binding;
754
+ if (!binding) {
755
+ throw new Error(`workflow run ${runId} has no isolated workspace binding`);
756
+ }
757
+ if (binding.provider.id !== "local-git-worktree") {
758
+ throw new Error(
759
+ `workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`,
760
+ );
761
+ }
762
+ return createLocalGitWorktreeProvider({ coordRoot });
763
+ }
764
+
765
+ function collectOption(value: string, previous: string[]): string[] {
766
+ return [...previous, value];
767
+ }
768
+
769
+ /** Next operator command after resolving an approval. Mid-run parks resume the
770
+ * workflow; integration prepare parks after the run is already terminal, so
771
+ * the durable plan is authorized by re-running prepare (resume would fail). */
772
+ function approvalNextActionHint(coordRoot: string, approval: WorkflowApproval): string {
773
+ const bin = resolveBinName();
774
+ const runId = approval.request.run_id;
775
+ const integrationPlanPath = join(
776
+ coordRoot,
777
+ ".harnery",
778
+ "workflows",
779
+ runId,
780
+ "integration",
781
+ "plan.json",
782
+ );
783
+ if (existsSync(integrationPlanPath)) {
784
+ return `prepare: ${bin} workflow integration prepare ${runId}\n`;
785
+ }
786
+ return `resume: ${bin} workflow resume ${runId}\n`;
787
+ }