harnery 0.23.0 → 0.25.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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/harness.d.ts +2 -1
  3. package/dist/commands/harness.d.ts.map +1 -1
  4. package/dist/commands/harness.js +105 -3
  5. package/dist/commands/supervisor.d.ts.map +1 -1
  6. package/dist/commands/supervisor.js +3 -13
  7. package/dist/commands/work.d.ts.map +1 -1
  8. package/dist/commands/work.js +2 -7
  9. package/dist/commands/workflow.d.ts.map +1 -1
  10. package/dist/commands/workflow.js +215 -17
  11. package/dist/core/harnesses/attest.d.ts +48 -0
  12. package/dist/core/harnesses/attest.d.ts.map +1 -0
  13. package/dist/core/harnesses/attest.js +110 -0
  14. package/dist/core/harnesses/attestation.d.ts +75 -0
  15. package/dist/core/harnesses/attestation.d.ts.map +1 -0
  16. package/dist/core/harnesses/attestation.js +168 -0
  17. package/dist/core/harnesses/bench.d.ts +23 -1
  18. package/dist/core/harnesses/bench.d.ts.map +1 -1
  19. package/dist/core/harnesses/bench.js +135 -32
  20. package/dist/core/harnesses/index.d.ts +6 -2
  21. package/dist/core/harnesses/index.d.ts.map +1 -1
  22. package/dist/core/harnesses/index.js +3 -1
  23. package/dist/core/workflow/engine.js +3 -0
  24. package/dist/core/workflow/index.d.ts +1 -1
  25. package/dist/core/workflow/index.d.ts.map +1 -1
  26. package/dist/core/workflow/index.js +1 -1
  27. package/dist/core/workflow/proof.d.ts +4 -1
  28. package/dist/core/workflow/proof.d.ts.map +1 -1
  29. package/dist/core/workflow/proof.js +3 -2
  30. package/dist/core/workflow/spawn-cursor.d.ts +3 -0
  31. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  32. package/dist/core/workflow/spawn-cursor.js +8 -4
  33. package/dist/core/workflow/types.d.ts +14 -0
  34. package/dist/core/workflow/types.d.ts.map +1 -1
  35. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  36. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  37. package/dist/core/workflow/workspaces/index.js +1 -1
  38. package/dist/core/workflow/workspaces/integration.d.ts +8 -0
  39. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -1
  40. package/dist/core/workflow/workspaces/integration.js +15 -1
  41. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  42. package/dist/core/workflow/workspaces/leases.js +19 -2
  43. package/package.json +1 -1
  44. package/src/commands/harness.ts +123 -2
  45. package/src/commands/supervisor.ts +11 -15
  46. package/src/commands/work.ts +8 -8
  47. package/src/commands/workflow.ts +268 -20
  48. package/src/core/harnesses/attest.ts +162 -0
  49. package/src/core/harnesses/attestation.ts +231 -0
  50. package/src/core/harnesses/bench.ts +187 -32
  51. package/src/core/harnesses/index.ts +26 -1
  52. package/src/core/workflow/engine.ts +3 -0
  53. package/src/core/workflow/index.ts +1 -0
  54. package/src/core/workflow/proof.ts +7 -1
  55. package/src/core/workflow/spawn-cursor.ts +8 -4
  56. package/src/core/workflow/types.ts +15 -0
  57. package/src/core/workflow/workspaces/index.ts +5 -1
  58. package/src/core/workflow/workspaces/integration.ts +17 -1
  59. package/src/core/workflow/workspaces/leases.ts +24 -2
@@ -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
  }
@@ -126,6 +126,14 @@ export interface WorkflowRepoEvidence {
126
126
  };
127
127
  }
128
128
 
129
+ /** Bounded pointer to the live attestation backing a harness's claims
130
+ * (ADR 0038). Structural facts only: no prompt text, no host paths. */
131
+ export interface HarnessAttestationCitation {
132
+ binary_version: string;
133
+ observed_at: string;
134
+ record_digest: string;
135
+ }
136
+
129
137
  export interface HarnessEvidenceCoverage {
130
138
  harness: HarnessName;
131
139
  tool_evidence: {
@@ -137,6 +145,10 @@ export interface HarnessEvidenceCoverage {
137
145
  session_ids: number;
138
146
  costs: number;
139
147
  };
148
+ /** What backs this harness's capability claims (ADR 0038). Absent when the
149
+ * host recorded no live attestation, which is the common case and is not by
150
+ * itself a proof unknown. */
151
+ attestation?: HarnessAttestationCitation;
140
152
  }
141
153
 
142
154
  export interface WorkflowProofUnknown {
@@ -429,6 +441,9 @@ export interface EngineOpts {
429
441
  /** Capability claims used to state whether adapter-native tool evidence was
430
442
  * available. Missing claims remain unknown. */
431
443
  harnessEvidence?: Readonly<Record<HarnessName, HarnessEvidenceCapability | undefined>>;
444
+ /** Live attestations backing each harness's claims (ADR 0038). Read once by
445
+ * the host and injected, so the engine performs no capability lookups. */
446
+ harnessAttestations?: Readonly<Record<HarnessName, HarnessAttestationCitation | undefined>>;
432
447
  /** Immutable host policy. Workflow scripts and model prompts cannot replace it. */
433
448
  policy?: PolicySpec | NormalizedPolicy;
434
449
  /** Host callback for ASK. Missing, invalid, throwing, or timed-out resolution denies. */
@@ -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;