harnery 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +91 -2
  4. package/dist/core/work/state.d.ts +7 -0
  5. package/dist/core/work/state.d.ts.map +1 -1
  6. package/dist/core/work/state.js +112 -23
  7. package/dist/core/workflow/child-env.d.ts +10 -6
  8. package/dist/core/workflow/child-env.d.ts.map +1 -1
  9. package/dist/core/workflow/child-env.js +15 -7
  10. package/dist/core/workflow/durable-record.d.ts +7 -0
  11. package/dist/core/workflow/durable-record.d.ts.map +1 -0
  12. package/dist/core/workflow/durable-record.js +83 -0
  13. package/dist/core/workflow/engine.d.ts.map +1 -1
  14. package/dist/core/workflow/engine.js +208 -29
  15. package/dist/core/workflow/index.d.ts +2 -0
  16. package/dist/core/workflow/index.d.ts.map +1 -1
  17. package/dist/core/workflow/index.js +1 -0
  18. package/dist/core/workflow/proof.d.ts +4 -0
  19. package/dist/core/workflow/proof.d.ts.map +1 -1
  20. package/dist/core/workflow/proof.js +50 -15
  21. package/dist/core/workflow/run-state.d.ts +3 -0
  22. package/dist/core/workflow/run-state.d.ts.map +1 -1
  23. package/dist/core/workflow/run-state.js +25 -13
  24. package/dist/core/workflow/types.d.ts +10 -0
  25. package/dist/core/workflow/types.d.ts.map +1 -1
  26. package/dist/core/workflow/validate.d.ts +10 -1
  27. package/dist/core/workflow/validate.d.ts.map +1 -1
  28. package/dist/core/workflow/validate.js +75 -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 +24 -0
  51. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/integration.js +617 -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 +380 -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 +110 -4
  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 +73 -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/types.ts +14 -0
  87. package/src/core/workflow/validate.ts +75 -1
  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 +85 -0
  94. package/src/core/workflow/workspaces/inspect.ts +421 -0
  95. package/src/core/workflow/workspaces/integration.ts +829 -0
  96. package/src/core/workflow/workspaces/leases.ts +495 -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. `harn workflow workspace <run-id>` and the dashboard show the validated lifecycle; 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":"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,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import { workflowSubscriptionOnly } from "../core/config.js";
2
3
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
3
4
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
@@ -24,6 +25,7 @@ export function registerWorkflowCommand(program, emit) {
24
25
  "without this the engine refuses that silent-override state")
25
26
  .option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
26
27
  .option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
28
+ .option("--workspace-root <dir>", "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)")
27
29
  .option("--approval-to <address>", "Address durable ASK requests (default: operator)")
28
30
  .option("--json", "Emit the full RunReport as JSON")
29
31
  .action(async (script, opts) => {
@@ -50,6 +52,13 @@ export function registerWorkflowCommand(program, emit) {
50
52
  });
51
53
  process.exit(1);
52
54
  }
55
+ if (opts.workspaceRoot && opts.isolation !== "worktree") {
56
+ emit.error({
57
+ code: "bad_workspace_root",
58
+ message: "--workspace-root requires --isolation worktree",
59
+ });
60
+ process.exit(1);
61
+ }
53
62
  // Flag beats config; the two flags contradict each other by design.
54
63
  const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
55
64
  if (subscriptionOnly && opts.allowApiBilling) {
@@ -61,7 +70,13 @@ export function registerWorkflowCommand(program, emit) {
61
70
  process.exit(1);
62
71
  }
63
72
  try {
64
- const { runWorkflow } = await import("../core/workflow/engine.js");
73
+ const { createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
74
+ const workspace = opts.workspaceRoot
75
+ ? {
76
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
77
+ writableRoots: [resolve(opts.workspaceRoot)],
78
+ }
79
+ : undefined;
65
80
  const report = await runWorkflow(script, {
66
81
  coordRoot,
67
82
  spawners: registry.spawners(),
@@ -82,6 +97,7 @@ export function registerWorkflowCommand(program, emit) {
82
97
  approvalMode: "park",
83
98
  approvalAddressee: opts.approvalTo,
84
99
  isolation: opts.isolation,
100
+ workspace,
85
101
  // CLI harness subprocesses inherit the host network. A policy that
86
102
  // forbids network must therefore deny dispatch unless a future host
87
103
  // adapter creates and declares a network-disabled boundary.
@@ -146,12 +162,22 @@ export function registerWorkflowCommand(program, emit) {
146
162
  process.exit(1);
147
163
  }
148
164
  try {
149
- const { assertWorkflowRunResumable, runWorkflow } = await import("../core/workflow/index.js");
165
+ const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
150
166
  const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
167
+ const binding = manifest.execution.workspace_binding;
168
+ if (binding && binding.provider.id !== "local-git-worktree") {
169
+ throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
170
+ }
151
171
  const report = await runWorkflow(manifest.script.path, {
152
172
  coordRoot,
153
173
  spawners: registry.spawners(),
154
174
  resumeRunId: runId,
175
+ workspace: binding
176
+ ? {
177
+ provider: createLocalGitWorktreeProvider({ coordRoot }),
178
+ writableRoots: [binding.writable_root.configured],
179
+ }
180
+ : undefined,
155
181
  harnessEvidence: Object.fromEntries(registry
156
182
  .list()
157
183
  .map((adapter) => [
@@ -199,6 +225,69 @@ export function registerWorkflowCommand(program, emit) {
199
225
  process.exit(1);
200
226
  }
201
227
  });
228
+ workflow
229
+ .command("workspace <run-id>")
230
+ .description("Show validated allocation, verification, integration, and cleanup state.")
231
+ .option("--json", "Emit the validated workspace status as JSON")
232
+ .action(async (runId, opts) => {
233
+ const coordRoot = findCoordRoot();
234
+ if (!coordRoot) {
235
+ emit.error({
236
+ code: "no_coord_root",
237
+ message: "no .harnery/ coordination root found; run `init` first",
238
+ });
239
+ process.exit(1);
240
+ }
241
+ const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import("../core/workflow/index.js");
242
+ const inspection = inspectWorkflowWorkspace(coordRoot, runId);
243
+ if (!inspection.ok) {
244
+ emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
245
+ process.exit(1);
246
+ }
247
+ if (opts.json) {
248
+ emit.config({ format: "json" });
249
+ emit.data(inspection.value);
250
+ return;
251
+ }
252
+ emit.text(renderWorkflowWorkspaceStatus(inspection.value));
253
+ });
254
+ workflow
255
+ .command("workspaces")
256
+ .description("List validated isolated and shared-compatibility workspace decisions.")
257
+ .option("--json", "Emit workspace inspections as JSON")
258
+ .action(async (opts) => {
259
+ const coordRoot = findCoordRoot();
260
+ if (!coordRoot) {
261
+ emit.error({
262
+ code: "no_coord_root",
263
+ message: "no .harnery/ coordination root found; run `init` first",
264
+ });
265
+ process.exit(1);
266
+ }
267
+ const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.js");
268
+ const inspections = listWorkflowWorkspaceInspections(coordRoot).filter((inspection) => !inspection.ok || inspection.value.selection !== "shared");
269
+ if (opts.json) {
270
+ emit.config({ format: "json" });
271
+ emit.data(inspections);
272
+ return;
273
+ }
274
+ if (inspections.length === 0) {
275
+ emit.text("no isolated or shared-compatibility workspace runs\n");
276
+ return;
277
+ }
278
+ emit.text(`${inspections
279
+ .map((inspection) => inspection.ok
280
+ ? [
281
+ inspection.value.run_id,
282
+ inspection.value.selection,
283
+ inspection.value.lifecycle.state,
284
+ `verify=${inspection.value.verification.status}`,
285
+ `integration=${inspection.value.integration.state}`,
286
+ `cleanup=${inspection.value.cleanup.state}`,
287
+ ].join("\t")
288
+ : `${inspection.run_id}\tinvalid\t${inspection.error}`)
289
+ .join("\n")}\n`);
290
+ });
202
291
  const approvals = workflow
203
292
  .command("approvals")
204
293
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -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;
@@ -6,13 +6,17 @@
6
6
  * harness CLI under a live session inherits vars that make it exit 1 with
7
7
  * empty output; an empty-string var still reads as set. Scrub all three
8
8
  * families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
9
- * a codex child launched from inside a Claude Code session must not
10
- * inherit CLAUDE* either.
11
- * 2. **The scrub targets SESSION vars, not credentials.** CURSOR_API_KEY
9
+ * a codex child launched from inside a Claude Code session must not inherit
10
+ * CLAUDE* either. Preserve explicit auth/config locators such as CODEX_HOME:
11
+ * changing the home silently changes which subscription account the child
12
+ * uses.
13
+ * 2. **The scrub targets SESSION vars, not credentials or auth locators.** CURSOR_API_KEY
12
14
  * matches the CURSOR* prefix but is an auth credential, so it is carved
13
- * out and re-added — a key-only cursor host must keep working. (The other
14
- * two key vars, ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with
15
- * the scrub prefixes.)
15
+ * out and re-added — a key-only cursor host must keep working. CODEX_HOME
16
+ * is likewise preserved because codex reads its ChatGPT subscription login
17
+ * from `$CODEX_HOME/auth.json`. (The other two key vars,
18
+ * ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with the scrub
19
+ * prefixes.)
16
20
  * 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
17
21
  * child can only authenticate via its stored subscription login — the
18
22
  * guarantee behind `workflow run --subscription-only`.
@@ -1 +1 @@
1
- {"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/child-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7F"}
1
+ {"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/child-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB7F"}
@@ -6,13 +6,17 @@
6
6
  * harness CLI under a live session inherits vars that make it exit 1 with
7
7
  * empty output; an empty-string var still reads as set. Scrub all three
8
8
  * families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
9
- * a codex child launched from inside a Claude Code session must not
10
- * inherit CLAUDE* either.
11
- * 2. **The scrub targets SESSION vars, not credentials.** CURSOR_API_KEY
9
+ * a codex child launched from inside a Claude Code session must not inherit
10
+ * CLAUDE* either. Preserve explicit auth/config locators such as CODEX_HOME:
11
+ * changing the home silently changes which subscription account the child
12
+ * uses.
13
+ * 2. **The scrub targets SESSION vars, not credentials or auth locators.** CURSOR_API_KEY
12
14
  * matches the CURSOR* prefix but is an auth credential, so it is carved
13
- * out and re-added — a key-only cursor host must keep working. (The other
14
- * two key vars, ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with
15
- * the scrub prefixes.)
15
+ * out and re-added — a key-only cursor host must keep working. CODEX_HOME
16
+ * is likewise preserved because codex reads its ChatGPT subscription login
17
+ * from `$CODEX_HOME/auth.json`. (The other two key vars,
18
+ * ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with the scrub
19
+ * prefixes.)
16
20
  * 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
17
21
  * child can only authenticate via its stored subscription login — the
18
22
  * guarantee behind `workflow run --subscription-only`.
@@ -24,6 +28,7 @@
24
28
  */
25
29
  import { API_KEY_VARS } from "./billing.js";
26
30
  const SCRUB_PREFIXES = ["CLAUDE", "CODEX", "CURSOR"];
31
+ const PRESERVED_HARNESS_VARS = new Set(["CODEX_HOME"]);
27
32
  export function buildChildEnv(runId, opts = {}) {
28
33
  const env = {};
29
34
  const keyVars = new Set(Object.values(API_KEY_VARS));
@@ -32,8 +37,11 @@ export function buildChildEnv(runId, opts = {}) {
32
37
  continue;
33
38
  if (opts.subscriptionOnly && keyVars.has(k))
34
39
  continue;
35
- if (SCRUB_PREFIXES.some((p) => k.startsWith(p)) && !keyVars.has(k))
40
+ if (SCRUB_PREFIXES.some((p) => k.startsWith(p)) &&
41
+ !keyVars.has(k) &&
42
+ !PRESERVED_HARNESS_VARS.has(k)) {
36
43
  continue;
44
+ }
37
45
  env[k] = v;
38
46
  }
39
47
  env.HARNERY_WORKFLOW_CHILD = "1";
@@ -0,0 +1,7 @@
1
+ export declare function stableDigest(value: unknown): string;
2
+ export declare function canonicalJson(value: unknown): string;
3
+ export declare function fileSha256(path: string): string;
4
+ export declare function writeImmutableJson(path: string, value: unknown): boolean;
5
+ export declare function fsyncParentDirectory(path: string): void;
6
+ export declare function readJsonRecord<T>(path: string, label: string): T;
7
+ //# sourceMappingURL=durable-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-record.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/durable-record.ts"],"names":[],"mappings":"AAkBA,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CA6BxE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQvD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAShE"}
@@ -0,0 +1,83 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ const JSON_LIMIT = 512 * 1024;
5
+ export function stableDigest(value) {
6
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
7
+ }
8
+ export function canonicalJson(value) {
9
+ return JSON.stringify(canonicalize(value));
10
+ }
11
+ export function fileSha256(path) {
12
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
13
+ }
14
+ export function writeImmutableJson(path, value) {
15
+ const body = `${JSON.stringify(value, null, 2)}\n`;
16
+ if (Buffer.byteLength(body) > JSON_LIMIT) {
17
+ throw new Error(`immutable record exceeds ${JSON_LIMIT} bytes`);
18
+ }
19
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
20
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
21
+ let fd;
22
+ try {
23
+ fd = openSync(temporary, "wx", 0o600);
24
+ writeFileSync(fd, body, "utf8");
25
+ fsyncSync(fd);
26
+ closeSync(fd);
27
+ fd = undefined;
28
+ try {
29
+ linkSync(temporary, path);
30
+ chmodSync(path, 0o600);
31
+ fsyncParentDirectory(path);
32
+ return true;
33
+ }
34
+ catch (error) {
35
+ if (error.code !== "EEXIST")
36
+ throw error;
37
+ const existing = readFileSync(path, "utf8");
38
+ if (existing !== body)
39
+ throw new Error(`immutable record already exists at ${path}`);
40
+ return false;
41
+ }
42
+ }
43
+ finally {
44
+ if (fd !== undefined)
45
+ closeSync(fd);
46
+ if (existsSync(temporary))
47
+ unlinkSync(temporary);
48
+ }
49
+ }
50
+ export function fsyncParentDirectory(path) {
51
+ const directory = dirname(path);
52
+ const fd = openSync(directory, "r");
53
+ try {
54
+ fsyncSync(fd);
55
+ }
56
+ finally {
57
+ closeSync(fd);
58
+ }
59
+ }
60
+ export function readJsonRecord(path, label) {
61
+ if (!existsSync(path))
62
+ throw new Error(`${label} does not exist at ${path}`);
63
+ const size = statSync(path).size;
64
+ if (size <= 0 || size > JSON_LIMIT)
65
+ throw new Error(`${label} has invalid size ${size}`);
66
+ try {
67
+ return JSON.parse(readFileSync(path, "utf8"));
68
+ }
69
+ catch (error) {
70
+ throw new Error(`cannot parse ${label} at ${path}: ${error.message}`);
71
+ }
72
+ }
73
+ function canonicalize(value) {
74
+ if (Array.isArray(value))
75
+ return value.map(canonicalize);
76
+ if (value && typeof value === "object") {
77
+ return Object.fromEntries(Object.entries(value)
78
+ .filter(([, item]) => item !== undefined)
79
+ .sort(([a], [b]) => a.localeCompare(b))
80
+ .map(([key, item]) => [key, canonicalize(item)]));
81
+ }
82
+ return value;
83
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAsCH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAUV,MAAM,YAAY,CAAC;AAapB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAOpF;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA6E1F"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAuCH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAUV,MAAM,YAAY,CAAC;AAyBpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAOpF;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA6E1F"}