harnery 0.23.0 → 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.
package/README.md CHANGED
@@ -22,7 +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. `harn workflow workspace <run-id>` and the dashboard show the validated lifecycle; shared execution remains the 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.
26
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.
27
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.
28
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":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiDnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmejF"}
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,5 +1,6 @@
1
- import { resolve } from "node:path";
2
- 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";
3
4
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
4
5
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
5
6
  import { loadPolicyFile } from "../core/policy/index.js";
@@ -134,7 +135,7 @@ export function registerWorkflowCommand(program, emit) {
134
135
  else {
135
136
  emit.text(`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
136
137
  `journal: ${err.journalPath}\n` +
137
- `resume after resolution: harn workflow resume ${err.runId}\n`);
138
+ `resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`);
138
139
  }
139
140
  return;
140
141
  }
@@ -288,6 +289,186 @@ export function registerWorkflowCommand(program, emit) {
288
289
  : `${inspection.run_id}\tinvalid\t${inspection.error}`)
289
290
  .join("\n")}\n`);
290
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
+ });
291
472
  const approvals = workflow
292
473
  .command("approvals")
293
474
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -379,7 +560,7 @@ export function registerWorkflowCommand(program, emit) {
379
560
  else {
380
561
  emit.text(`${renderWorkflowApproval(resolved.approval)}` +
381
562
  `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
382
- `resume: harn workflow resume ${resolved.approval.request.run_id}\n`);
563
+ approvalNextActionHint(coordRoot, resolved.approval));
383
564
  }
384
565
  }
385
566
  catch (err) {
@@ -422,3 +603,30 @@ export function registerWorkflowCommand(program, emit) {
422
603
  }
423
604
  });
424
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
+ }
@@ -8,5 +8,5 @@ export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowS
8
8
  export type { AcceptanceCriterion, AcceptanceResult, AcceptanceStatus, AcceptanceSummary, AgentOpts, EngineOpts, EvidenceKind, EvidenceSource, EvidenceStatus, HarnessEvidenceCapability, HarnessEvidenceCoverage, HarnessName, ResultDigest, RunReport, Spawner, SpawnRequest, SpawnResult, StageSchema, WorkflowAgentProof, WorkflowApprovalMode, WorkflowAttemptContext, WorkflowAttemptFailureCause, WorkflowAttemptPriorContext, WorkflowAttemptUnresolvedCriterion, WorkflowContext, WorkflowEvidenceInput, WorkflowEvidenceRecord, WorkflowMeta, WorkflowModule, WorkflowPolicyProof, WorkflowProof, WorkflowProofUnknown, WorkflowRepoEvidence, WorkflowRepoSnapshot, WorkflowRunStatus, WorkflowSpecialistProfile, WorkflowWorkContext, } from "./types.js";
9
9
  export { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION, WORKFLOW_PROOF_SCHEMA_VERSION, WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION, } from "./types.js";
10
10
  export type { ApplyIntegrationInput, AuthorizedIntegrationPlan, AuthorizedProviderIntegrationInput, CancelWorkspaceInput, CleanupWorkspaceInput, FilesystemIdentity, IntegrationApplyAttempt, IntegrationApplyReceipt, IntegrationAuthorization, IntegrationPlan, IntegrationReviewRecord, IntegrationTarget, LocalGitWorktreeProviderOptions, PrepareIntegrationInput, ProviderIntegrationInput, ProviderIntegrationPreview, ProviderIntegrationResult, ProviderUnknown, ProviderUnsupportedReason, RepositoryBinding, ValidatedFilesystemPath, WorkflowWorkspaceInspection, WorkflowWorkspaceSelection, WorkflowWorkspaceStatus, WorkflowWorkspaceVerification, WorkspaceAllocationRequest, WorkspaceAttestation, WorkspaceBinding, WorkspaceBoundExecutionEvidence, WorkspaceCancellationReceipt, WorkspaceCancellationResult, WorkspaceCleanupAttempt, WorkspaceCleanupIntent, WorkspaceCleanupMode, WorkspaceCleanupReceipt, WorkspaceCleanupResult, WorkspaceCompatibilityExecutionEvidence, WorkspaceExecutionEvidence, WorkspaceIntegrationState, WorkspaceIsolation, WorkspaceLifecycleProjection, WorkspaceLifecycleProjectionInput, WorkspaceLifecycleState, WorkspaceObservation, WorkspaceOwner, WorkspaceProbeInput, WorkspaceProbeResult, WorkspaceProofOutcome, WorkspaceProvider, WorkspaceProviderCapabilities, WorkspaceProviderRef, WorkspaceResourceState, WorkspaceUnsupportedExecutionEvidence, } from "./workspaces/index.js";
11
- export { applyIntegration, assertWorkspaceLifecycleTransition, cancelWorkspace, cleanupWorkspace, createLocalGitWorktreeProvider, deriveWorkspaceLifecycle, inspectWorkflowWorkspace, isWorkspaceLifecycleState, listWorkflowWorkspaceInspections, prepareIntegration, probeLocalGitWorktreeProvider, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, validateProviderEventChain, WORKSPACE_BINDING_SCHEMA_VERSION, WORKSPACE_RECEIPT_SCHEMA_VERSION, } from "./workspaces/index.js";
11
+ export { applyIntegration, assertWorkspaceLifecycleTransition, cancelWorkspace, cleanupWorkspace, createLocalGitWorktreeProvider, deriveWorkspaceLifecycle, IntegrationPrepareParkedError, inspectWorkflowWorkspace, isWorkspaceLifecycleState, listWorkflowWorkspaceInspections, prepareIntegration, probeLocalGitWorktreeProvider, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, validateProviderEventChain, WORKSPACE_BINDING_SCHEMA_VERSION, WORKSPACE_RECEIPT_SCHEMA_VERSION, } from "./workspaces/index.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"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"}
@@ -4,4 +4,4 @@ export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js"
4
4
  export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
5
5
  export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, readWorkflowRunManifest, WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION, workflowRunManifestPath, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
6
6
  export { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION, WORKFLOW_PROOF_SCHEMA_VERSION, WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION, } from "./types.js";
7
- export { applyIntegration, assertWorkspaceLifecycleTransition, cancelWorkspace, cleanupWorkspace, createLocalGitWorktreeProvider, deriveWorkspaceLifecycle, inspectWorkflowWorkspace, isWorkspaceLifecycleState, listWorkflowWorkspaceInspections, prepareIntegration, probeLocalGitWorktreeProvider, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, validateProviderEventChain, WORKSPACE_BINDING_SCHEMA_VERSION, WORKSPACE_RECEIPT_SCHEMA_VERSION, } from "./workspaces/index.js";
7
+ export { applyIntegration, assertWorkspaceLifecycleTransition, cancelWorkspace, cleanupWorkspace, createLocalGitWorktreeProvider, deriveWorkspaceLifecycle, IntegrationPrepareParkedError, inspectWorkflowWorkspace, isWorkspaceLifecycleState, listWorkflowWorkspaceInspections, prepareIntegration, probeLocalGitWorktreeProvider, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, validateProviderEventChain, WORKSPACE_BINDING_SCHEMA_VERSION, WORKSPACE_RECEIPT_SCHEMA_VERSION, } from "./workspaces/index.js";
@@ -10,6 +10,9 @@
10
10
  * session_id, …}`).
11
11
  * - `--trust` is required: headless runs refuse untrusted workspaces (exit 1,
12
12
  * "Workspace Trust Required") — see the argv comment below.
13
+ * - `--force` is required: print mode has no channel for an operator to approve
14
+ * shell commands, so Smart Auto can leave a child able to patch files but
15
+ * unable to test or commit them.
13
16
  * - Envelope drift guard: when stdout doesn't parse as JSON but the process
14
17
  * exited 0, the raw stdout is returned as the reply text.
15
18
  * - No per-run cost surface → undefined. No max-turns equivalent → `maxTurns`
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASrE,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAWA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAS1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CA6BxE;AAED,eAAO,MAAM,aAAa,EAAE,OAe3B,CAAC"}
1
+ {"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASrE,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAWA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAU1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CA6BxE;AAED,eAAO,MAAM,aAAa,EAAE,OAe3B,CAAC"}
@@ -10,6 +10,9 @@
10
10
  * session_id, …}`).
11
11
  * - `--trust` is required: headless runs refuse untrusted workspaces (exit 1,
12
12
  * "Workspace Trust Required") — see the argv comment below.
13
+ * - `--force` is required: print mode has no channel for an operator to approve
14
+ * shell commands, so Smart Auto can leave a child able to patch files but
15
+ * unable to test or commit them.
13
16
  * - Envelope drift guard: when stdout doesn't parse as JSON but the process
14
17
  * exited 0, the raw stdout is returned as the reply text.
15
18
  * - No per-run cost surface → undefined. No max-turns equivalent → `maxTurns`
@@ -36,10 +39,11 @@ export function parseCursorOutput(stdout) {
36
39
  export function buildCursorInvocation(req) {
37
40
  validateHarnessEffort("cursor", req.effort);
38
41
  // --trust: headless cursor-agent refuses untrusted workspaces (exit 1,
39
- // "Workspace Trust Required"). Workflow children run in the engine's cwd
40
- // deliberately and may edit files — the same posture as the codex adapter's
41
- // `--sandbox workspace-write` — so trusting that directory is implied.
42
- const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust"];
42
+ // "Workspace Trust Required"). --force: a print-mode child has no interactive
43
+ // approval channel, so host-authorized workflow dispatch must let it run
44
+ // commands. Neither flag maps host policy into child tools; that capability
45
+ // remains explicitly unsupported.
46
+ const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust", "--force"];
43
47
  if (req.model)
44
48
  argv.push("--model", req.model);
45
49
  return { argv };
@@ -5,7 +5,7 @@ export { cleanupWorkspace } from "./cleanup.js";
5
5
  export type { WorkflowWorkspaceInspection, WorkflowWorkspaceSelection, WorkflowWorkspaceStatus, WorkflowWorkspaceVerification, } from "./inspect.js";
6
6
  export { inspectWorkflowWorkspace, listWorkflowWorkspaceInspections, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, } from "./inspect.js";
7
7
  export type { ApplyIntegrationInput, PrepareIntegrationInput } from "./integration.js";
8
- export { applyIntegration, prepareIntegration } from "./integration.js";
8
+ export { applyIntegration, IntegrationPrepareParkedError, prepareIntegration, } from "./integration.js";
9
9
  export type { WorkspaceLifecycleProjection, WorkspaceLifecycleProjectionInput, } from "./lifecycle.js";
10
10
  export { assertWorkspaceLifecycleTransition, deriveWorkspaceLifecycle, isWorkspaceLifecycleState, validateProviderEventChain, } from "./lifecycle.js";
11
11
  export type { LocalGitWorktreeProviderOptions } from "./local-git.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EACV,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,6BAA6B,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACxE,YAAY,EACV,4BAA4B,EAC5B,iCAAiC,GAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kCAAkC,EAClC,wBAAwB,EACxB,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,8BAA8B,EAC9B,KAAK,IAAI,6BAA6B,GACvC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,yBAAyB,EACzB,kCAAkC,EAClC,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,iBAAiB,EACjB,wBAAwB,EACxB,0BAA0B,EAC1B,yBAAyB,EACzB,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,oBAAoB,EACpB,gBAAgB,EAChB,+BAA+B,EAC/B,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,uCAAuC,EACvC,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,4BAA4B,EAC5B,qBAAqB,EACrB,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,sBAAsB,EACtB,qCAAqC,GACtC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gCAAgC,EAChC,gCAAgC,GACjC,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EACV,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,6BAA6B,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EACL,gBAAgB,EAChB,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,4BAA4B,EAC5B,iCAAiC,GAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kCAAkC,EAClC,wBAAwB,EACxB,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,8BAA8B,EAC9B,KAAK,IAAI,6BAA6B,GACvC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,yBAAyB,EACzB,kCAAkC,EAClC,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,iBAAiB,EACjB,wBAAwB,EACxB,0BAA0B,EAC1B,yBAAyB,EACzB,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,oBAAoB,EACpB,gBAAgB,EAChB,+BAA+B,EAC/B,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,uCAAuC,EACvC,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,4BAA4B,EAC5B,qBAAqB,EACrB,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,sBAAsB,EACtB,qCAAqC,GACtC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gCAAgC,EAChC,gCAAgC,GACjC,MAAM,YAAY,CAAC"}
@@ -1,7 +1,7 @@
1
1
  export { cancelWorkspace } from "./cancellation.js";
2
2
  export { cleanupWorkspace } from "./cleanup.js";
3
3
  export { inspectWorkflowWorkspace, listWorkflowWorkspaceInspections, readWorkflowWorkspaceStatus, renderWorkflowWorkspaceStatus, } from "./inspect.js";
4
- export { applyIntegration, prepareIntegration } from "./integration.js";
4
+ export { applyIntegration, IntegrationPrepareParkedError, prepareIntegration, } from "./integration.js";
5
5
  export { assertWorkspaceLifecycleTransition, deriveWorkspaceLifecycle, isWorkspaceLifecycleState, validateProviderEventChain, } from "./lifecycle.js";
6
6
  export { createLocalGitWorktreeProvider, probe as probeLocalGitWorktreeProvider, } from "./local-git.js";
7
7
  export { WORKSPACE_BINDING_SCHEMA_VERSION, WORKSPACE_RECEIPT_SCHEMA_VERSION, } from "./types.js";
@@ -19,6 +19,14 @@ export interface ApplyIntegrationInput {
19
19
  provider: WorkspaceProvider;
20
20
  plan?: IntegrationPlan;
21
21
  }
22
+ /** Host-policy ASK still pending during integration prepare. Carries the exact
23
+ * durable IDs an operator needs to approve and resume the same plan. */
24
+ export declare class IntegrationPrepareParkedError extends Error {
25
+ readonly runId: string;
26
+ readonly planId: string;
27
+ readonly approvalId: string;
28
+ constructor(runId: string, planId: string, approvalId: string);
29
+ }
22
30
  export declare function prepareIntegration(input: PrepareIntegrationInput): Promise<IntegrationPlan>;
23
31
  export declare function applyIntegration(input: ApplyIntegrationInput): Promise<IntegrationApplyReceipt>;
24
32
  //# sourceMappingURL=integration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/integration.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,gBAAgB,EAGrB,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAgB/B,OAAO,KAAK,EAEV,uBAAuB,EAEvB,eAAe,EAOf,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EAAE,UAAU,GAAG,gBAAgB,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2GjG;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CAuNlC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/integration.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,gBAAgB,EAGrB,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAgB/B,OAAO,KAAK,EAEV,uBAAuB,EAEvB,eAAe,EAOf,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EAAE,UAAU,GAAG,gBAAgB,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB;AAED;wEACwE;AACxE,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAO9D;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2GjG;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CAuNlC"}
@@ -10,6 +10,20 @@ import { appendIntegrationAttempt, appendWorkflowJournalEvent, fileSha256, readI
10
10
  import { isWorkspaceAllocationRequest, isWorkspaceAttestation, isWorkspaceBoundExecutionEvidence, } from "./validate.js";
11
11
  const INTEGRATION_DECISION_ID = "p99999";
12
12
  const TARGET_LEASE_STALE_MS = 5 * 60 * 1_000;
13
+ /** Host-policy ASK still pending during integration prepare. Carries the exact
14
+ * durable IDs an operator needs to approve and resume the same plan. */
15
+ export class IntegrationPrepareParkedError extends Error {
16
+ runId;
17
+ planId;
18
+ approvalId;
19
+ constructor(runId, planId, approvalId) {
20
+ super(`integration preparation parked: run ${runId}, plan ${planId}, approval ${approvalId}`);
21
+ this.name = "IntegrationPrepareParkedError";
22
+ this.runId = runId;
23
+ this.planId = planId;
24
+ this.approvalId = approvalId;
25
+ }
26
+ }
13
27
  export async function prepareIntegration(input) {
14
28
  const evidence = await readAndValidateEvidence(input.coordRoot, input.runId, input.provider);
15
29
  const targetRoot = resolve(input.targetRoot ?? requiredIntegrationRoot(evidence.binding));
@@ -476,7 +490,7 @@ function authorizePlan(input, plan) {
476
490
  });
477
491
  approvalId = created.approval.request.id;
478
492
  if (created.approval.status === "pending") {
479
- throw new Error(`integration approval ${approvalId} is pending`);
493
+ throw new IntegrationPrepareParkedError(input.runId, plan.plan_id, approvalId);
480
494
  }
481
495
  if (created.approval.status !== "approved" || !created.approval.decision) {
482
496
  throw new Error(`integration approval ${approvalId} was denied`);
@@ -1 +1 @@
1
- {"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CA6FvF"}
1
+ {"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CAmGvF"}
@@ -66,8 +66,15 @@ export function acquireNoClobberLease(input) {
66
66
  recoveryClaimed = true;
67
67
  input.onRecoveryStep?.("claim_created");
68
68
  const pinPath = join(recoveryPath, "stale-pin");
69
- linkSync(currentPath, pinPath);
70
- assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
69
+ try {
70
+ linkSync(currentPath, pinPath);
71
+ assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
72
+ }
73
+ catch {
74
+ abortUnchangedRecoveryClaim(leaseDir, recoveryPath, pinPath, owner);
75
+ recoveryClaimed = false;
76
+ throw new Error(`lease ${input.scope} recovery raced with another contender`);
77
+ }
71
78
  fsyncDirectory(recoveryPath);
72
79
  input.onRecoveryStep?.("stale_pinned");
73
80
  assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
@@ -199,6 +206,16 @@ function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
199
206
  rmdirSync(quarantine);
200
207
  fsyncDirectory(leaseDir);
201
208
  }
209
+ function abortUnchangedRecoveryClaim(leaseDir, recoveryPath, pinPath, claimant) {
210
+ const claim = readRecoveryClaim(recoveryPath);
211
+ if (claim.claimant.owner_id !== claimant.owner_id) {
212
+ throw new Error(`lease ${claimant.scope} recovery ownership changed`);
213
+ }
214
+ safeUnlink(pinPath);
215
+ unlinkSync(join(recoveryPath, "claim.json"));
216
+ rmdirSync(recoveryPath);
217
+ fsyncDirectory(leaseDir);
218
+ }
202
219
  function readRecoveryClaim(recoveryPath) {
203
220
  const path = join(recoveryPath, "claim.json");
204
221
  const size = lstatSync(path).size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.23.0",
3
+ "version": "0.24.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",
@@ -1,13 +1,15 @@
1
- import { resolve } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
2
3
  import type { Command } from "commander";
3
4
  import type { EmitContext } from "../commander.ts";
4
- import { workflowSubscriptionOnly } from "../core/config.ts";
5
+ import { resolveBinName, workflowSubscriptionOnly } from "../core/config.ts";
5
6
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
6
7
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
7
8
  import type { PolicyIsolation } from "../core/policy/index.ts";
8
9
  import { loadPolicyFile } from "../core/policy/index.ts";
9
- import type { WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
10
+ import type { WorkflowApproval, WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
10
11
  import { WorkflowParkedError } from "../core/workflow/engine.ts";
12
+ import type { WorkspaceProvider } from "../core/workflow/index.ts";
11
13
 
12
14
  /**
13
15
  * `workflow run <script>`: execute a workflow script — bounded, schema-gated,
@@ -49,6 +51,21 @@ interface WorkflowApprovalDecisionOpts {
49
51
  json?: boolean;
50
52
  }
51
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
+
52
69
  export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
53
70
  const registry = createBuiltinHarnessRegistry();
54
71
  const harnesses = registry.ids();
@@ -211,7 +228,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
211
228
  emit.text(
212
229
  `run ${err.runId} parked\napproval: ${err.approvalId}\n` +
213
230
  `journal: ${err.journalPath}\n` +
214
- `resume after resolution: harn workflow resume ${err.runId}\n`,
231
+ `resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`,
215
232
  );
216
233
  }
217
234
  return;
@@ -387,6 +404,200 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
387
404
  );
388
405
  });
389
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
+
390
601
  const approvals = workflow
391
602
  .command("approvals")
392
603
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -488,7 +699,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
488
699
  emit.text(
489
700
  `${renderWorkflowApproval(resolved.approval)}` +
490
701
  `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
491
- `resume: harn workflow resume ${resolved.approval.request.run_id}\n`,
702
+ approvalNextActionHint(coordRoot, resolved.approval),
492
703
  );
493
704
  }
494
705
  } catch (err) {
@@ -533,3 +744,44 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
533
744
  }
534
745
  });
535
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
+ }
@@ -149,6 +149,7 @@ export {
149
149
  cleanupWorkspace,
150
150
  createLocalGitWorktreeProvider,
151
151
  deriveWorkspaceLifecycle,
152
+ IntegrationPrepareParkedError,
152
153
  inspectWorkflowWorkspace,
153
154
  isWorkspaceLifecycleState,
154
155
  listWorkflowWorkspaceInspections,
@@ -10,6 +10,9 @@
10
10
  * session_id, …}`).
11
11
  * - `--trust` is required: headless runs refuse untrusted workspaces (exit 1,
12
12
  * "Workspace Trust Required") — see the argv comment below.
13
+ * - `--force` is required: print mode has no channel for an operator to approve
14
+ * shell commands, so Smart Auto can leave a child able to patch files but
15
+ * unable to test or commit them.
13
16
  * - Envelope drift guard: when stdout doesn't parse as JSON but the process
14
17
  * exited 0, the raw stdout is returned as the reply text.
15
18
  * - No per-run cost surface → undefined. No max-turns equivalent → `maxTurns`
@@ -51,10 +54,11 @@ export function parseCursorOutput(stdout: string): {
51
54
  export function buildCursorInvocation(req: SpawnRequest): HarnessInvocation {
52
55
  validateHarnessEffort("cursor", req.effort);
53
56
  // --trust: headless cursor-agent refuses untrusted workspaces (exit 1,
54
- // "Workspace Trust Required"). Workflow children run in the engine's cwd
55
- // deliberately and may edit files — the same posture as the codex adapter's
56
- // `--sandbox workspace-write` — so trusting that directory is implied.
57
- const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust"];
57
+ // "Workspace Trust Required"). --force: a print-mode child has no interactive
58
+ // approval channel, so host-authorized workflow dispatch must let it run
59
+ // commands. Neither flag maps host policy into child tools; that capability
60
+ // remains explicitly unsupported.
61
+ const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust", "--force"];
58
62
  if (req.model) argv.push("--model", req.model);
59
63
  return { argv };
60
64
  }
@@ -15,7 +15,11 @@ export {
15
15
  renderWorkflowWorkspaceStatus,
16
16
  } from "./inspect.ts";
17
17
  export type { ApplyIntegrationInput, PrepareIntegrationInput } from "./integration.ts";
18
- export { applyIntegration, prepareIntegration } from "./integration.ts";
18
+ export {
19
+ applyIntegration,
20
+ IntegrationPrepareParkedError,
21
+ prepareIntegration,
22
+ } from "./integration.ts";
19
23
  export type {
20
24
  WorkspaceLifecycleProjection,
21
25
  WorkspaceLifecycleProjectionInput,
@@ -64,6 +64,22 @@ export interface ApplyIntegrationInput {
64
64
  plan?: IntegrationPlan;
65
65
  }
66
66
 
67
+ /** Host-policy ASK still pending during integration prepare. Carries the exact
68
+ * durable IDs an operator needs to approve and resume the same plan. */
69
+ export class IntegrationPrepareParkedError extends Error {
70
+ readonly runId: string;
71
+ readonly planId: string;
72
+ readonly approvalId: string;
73
+
74
+ constructor(runId: string, planId: string, approvalId: string) {
75
+ super(`integration preparation parked: run ${runId}, plan ${planId}, approval ${approvalId}`);
76
+ this.name = "IntegrationPrepareParkedError";
77
+ this.runId = runId;
78
+ this.planId = planId;
79
+ this.approvalId = approvalId;
80
+ }
81
+ }
82
+
67
83
  export async function prepareIntegration(input: PrepareIntegrationInput): Promise<IntegrationPlan> {
68
84
  const evidence = await readAndValidateEvidence(input.coordRoot, input.runId, input.provider);
69
85
  const targetRoot = resolve(input.targetRoot ?? requiredIntegrationRoot(evidence.binding));
@@ -662,7 +678,7 @@ function authorizePlan(input: PrepareIntegrationInput, plan: IntegrationPlan): v
662
678
  });
663
679
  approvalId = created.approval.request.id;
664
680
  if (created.approval.status === "pending") {
665
- throw new Error(`integration approval ${approvalId} is pending`);
681
+ throw new IntegrationPrepareParkedError(input.runId, plan.plan_id, approvalId);
666
682
  }
667
683
  if (created.approval.status !== "approved" || !created.approval.decision) {
668
684
  throw new Error(`integration approval ${approvalId} was denied`);
@@ -134,8 +134,14 @@ export function acquireNoClobberLease(input: AcquireNoClobberLeaseInput): NoClob
134
134
  input.onRecoveryStep?.("claim_created");
135
135
 
136
136
  const pinPath = join(recoveryPath, "stale-pin");
137
- linkSync(currentPath, pinPath);
138
- assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
137
+ try {
138
+ linkSync(currentPath, pinPath);
139
+ assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
140
+ } catch {
141
+ abortUnchangedRecoveryClaim(leaseDir, recoveryPath, pinPath, owner);
142
+ recoveryClaimed = false;
143
+ throw new Error(`lease ${input.scope} recovery raced with another contender`);
144
+ }
139
145
  fsyncDirectory(recoveryPath);
140
146
  input.onRecoveryStep?.("stale_pinned");
141
147
 
@@ -292,6 +298,22 @@ function reclaimAbandonedRecovery(
292
298
  fsyncDirectory(leaseDir);
293
299
  }
294
300
 
301
+ function abortUnchangedRecoveryClaim(
302
+ leaseDir: string,
303
+ recoveryPath: string,
304
+ pinPath: string,
305
+ claimant: NoClobberLeaseOwner,
306
+ ): void {
307
+ const claim = readRecoveryClaim(recoveryPath);
308
+ if (claim.claimant.owner_id !== claimant.owner_id) {
309
+ throw new Error(`lease ${claimant.scope} recovery ownership changed`);
310
+ }
311
+ safeUnlink(pinPath);
312
+ unlinkSync(join(recoveryPath, "claim.json"));
313
+ rmdirSync(recoveryPath);
314
+ fsyncDirectory(leaseDir);
315
+ }
316
+
295
317
  function readRecoveryClaim(recoveryPath: string): RecoveryClaim {
296
318
  const path = join(recoveryPath, "claim.json");
297
319
  const size = lstatSync(path).size;