harnery 0.12.1 → 0.13.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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/workflow.d.ts.map +1 -1
  3. package/dist/commands/workflow.js +193 -0
  4. package/dist/core/agents/coord-client.d.ts +8 -0
  5. package/dist/core/agents/coord-client.d.ts.map +1 -1
  6. package/dist/core/agents/coord-client.js +18 -2
  7. package/dist/core/policy/types.d.ts +1 -1
  8. package/dist/core/policy/types.d.ts.map +1 -1
  9. package/dist/core/scratch/index.d.ts.map +1 -1
  10. package/dist/core/scratch/index.js +3 -3
  11. package/dist/core/workflow/approvals.d.ts +71 -0
  12. package/dist/core/workflow/approvals.d.ts.map +1 -0
  13. package/dist/core/workflow/approvals.js +288 -0
  14. package/dist/core/workflow/engine.d.ts +6 -0
  15. package/dist/core/workflow/engine.d.ts.map +1 -1
  16. package/dist/core/workflow/engine.js +224 -43
  17. package/dist/core/workflow/index.d.ts +6 -2
  18. package/dist/core/workflow/index.d.ts.map +1 -1
  19. package/dist/core/workflow/index.js +3 -1
  20. package/dist/core/workflow/run-state.d.ts +43 -0
  21. package/dist/core/workflow/run-state.d.ts.map +1 -0
  22. package/dist/core/workflow/run-state.js +231 -0
  23. package/dist/core/workflow/types.d.ts +9 -0
  24. package/dist/core/workflow/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/commands/workflow.ts +226 -0
  27. package/src/core/agents/coord-client.ts +19 -2
  28. package/src/core/policy/types.ts +1 -1
  29. package/src/core/scratch/index.ts +3 -2
  30. package/src/core/workflow/approvals.ts +393 -0
  31. package/src/core/workflow/engine.ts +268 -44
  32. package/src/core/workflow/index.ts +31 -1
  33. package/src/core/workflow/run-state.ts +287 -0
  34. package/src/core/workflow/types.ts +9 -0
@@ -30,8 +30,10 @@ import { isAbsolute, join, resolve } from "node:path";
30
30
  import { pathToFileURL } from "node:url";
31
31
  import { snapshotRepo } from "../context/index.js";
32
32
  import { evaluatePolicy, normalizePolicy, PolicyDeniedError, policyDigest, summarizePolicyRequest, } from "../policy/index.js";
33
+ import { createWorkflowApproval } from "./approvals.js";
33
34
  import { probeBilling } from "./billing.js";
34
35
  import { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, writeWorkflowProof, } from "./proof.js";
36
+ import { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
35
37
  import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
36
38
  import { parseStageOutput, validateAgainstSchema } from "./validate.js";
37
39
  const DEFAULT_MAX_AGENTS = 50;
@@ -51,8 +53,48 @@ export class WorkflowRunError extends Error {
51
53
  this.proofPath = proofPath;
52
54
  }
53
55
  }
56
+ export class WorkflowParkedError extends Error {
57
+ runId;
58
+ approvalId;
59
+ journalPath;
60
+ constructor(message, runId, approvalId, journalPath) {
61
+ super(message);
62
+ this.name = "WorkflowParkedError";
63
+ this.runId = runId;
64
+ this.approvalId = approvalId;
65
+ this.journalPath = journalPath;
66
+ }
67
+ }
54
68
  export async function runWorkflow(scriptPath, opts) {
69
+ if (opts.resumeRunId && opts.resumeFrom) {
70
+ throw new Error("resumeRunId and resumeFrom are mutually exclusive");
71
+ }
72
+ const resumeState = opts.resumeRunId
73
+ ? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
74
+ : undefined;
55
75
  const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
76
+ if (resumeState) {
77
+ if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
78
+ throw new Error(`workflow run ${opts.resumeRunId} belongs to a different script`);
79
+ }
80
+ assertWorkflowScriptUnchanged(resumeState.manifest);
81
+ }
82
+ const releaseResumeLease = opts.resumeRunId
83
+ ? acquireWorkflowResumeLease(opts.coordRoot, opts.resumeRunId)
84
+ : undefined;
85
+ try {
86
+ if (resumeState) {
87
+ // Recheck under the exclusive lease. Another process may have completed
88
+ // the run between the optimistic read and lease acquisition.
89
+ assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
90
+ }
91
+ return await executeWorkflow(scriptPath, absScript, opts, resumeState);
92
+ }
93
+ finally {
94
+ releaseResumeLease?.();
95
+ }
96
+ }
97
+ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
56
98
  const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(absScript).href)));
57
99
  if (typeof mod.default !== "function") {
58
100
  throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
@@ -60,20 +102,56 @@ export async function runWorkflow(scriptPath, opts) {
60
102
  const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
61
103
  const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
62
104
  const name = meta.name;
63
- const runId = `wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
105
+ const runId = opts.resumeRunId ??
106
+ `wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
64
107
  const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
65
108
  mkdirSync(runDir, { recursive: true });
66
109
  const journalPath = join(runDir, "journal.jsonl");
67
110
  const proofPath = join(runDir, "proof.json");
68
- const maxAgents = opts.maxAgents ?? DEFAULT_MAX_AGENTS;
69
- const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY;
70
- const cwd = opts.cwd ?? opts.coordRoot;
111
+ const frozen = resumeState?.manifest.execution;
112
+ const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
113
+ const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
114
+ assertPositiveWorkflowBound(maxAgents, "maxAgents");
115
+ assertPositiveWorkflowBound(concurrency, "concurrency");
116
+ const cwd = frozen?.cwd ?? opts.cwd ?? opts.coordRoot;
71
117
  const log = opts.onLog ?? ((line) => process.stderr.write(`${line}\n`));
72
- const defaultHarness = opts.defaultHarness ?? "claude-code";
73
- const isolation = opts.isolation ?? "shared";
74
- const networkAccess = opts.networkAccess ?? "unknown";
75
- const policy = opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined;
76
- const repoBefore = snapshotRepo(cwd);
118
+ const defaultHarness = frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
119
+ const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
120
+ const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
121
+ const policy = frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
122
+ const approvalMode = frozen?.approval_mode ?? opts.approvalMode ?? "deny";
123
+ const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
124
+ const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
125
+ const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
126
+ const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
127
+ const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
128
+ const t0 = resumeState ? Date.parse(startedAt) : Date.now();
129
+ if (!resumeState) {
130
+ writeWorkflowRunManifest({
131
+ coordRoot: opts.coordRoot,
132
+ manifest: {
133
+ schema_version: 1,
134
+ run_id: runId,
135
+ name,
136
+ started_at: startedAt,
137
+ script: { path: absScript, sha256: workflowScriptDigest(absScript) },
138
+ repository_before: repoBefore,
139
+ execution: {
140
+ cwd,
141
+ default_harness: defaultHarness,
142
+ max_agents: maxAgents,
143
+ concurrency,
144
+ subscription_only: subscriptionOnly,
145
+ allow_api_billing: allowApiBilling,
146
+ approval_mode: approvalMode,
147
+ approval_addressee: approvalAddressee,
148
+ isolation,
149
+ network_access: networkAccess,
150
+ policy: policy ? policy : undefined,
151
+ },
152
+ },
153
+ });
154
+ }
77
155
  // Per-child fixed context overhead: children spawn in `cwd` and load its
78
156
  // repo-instructions file into their system prompt, cache-writing it once
79
157
  // per child. A fan-out multiplies this, so surface it BEFORE the burn.
@@ -83,12 +161,14 @@ export async function runWorkflow(scriptPath, opts) {
83
161
  `instructions from ${cwd}; a fan-out multiplies this per agent`);
84
162
  }
85
163
  // Resume: journaled results of a prior run, keyed by agent-call identity.
86
- const resumeCache = opts.resumeFrom
87
- ? loadResumeCache(opts.coordRoot, opts.resumeFrom)
164
+ const resumeSource = opts.resumeRunId ?? opts.resumeFrom;
165
+ const resumeCache = resumeSource
166
+ ? loadResumeCache(opts.coordRoot, resumeSource)
88
167
  : new Map();
89
- let agentsSpawned = 0;
168
+ const history = opts.resumeRunId ? loadRunHistory(journalPath) : undefined;
169
+ let agentsSpawned = history?.agentsSpawned ?? 0;
90
170
  let agentsCached = 0;
91
- let costUsd = 0;
171
+ let costUsd = history?.costUsd ?? 0;
92
172
  let reservedCostUsd = 0;
93
173
  let currentStage = "";
94
174
  let agentSeq = 0;
@@ -97,7 +177,7 @@ export async function runWorkflow(scriptPath, opts) {
97
177
  const billingProbed = new Map();
98
178
  const agentProofs = new Map();
99
179
  const evidenceRecords = [];
100
- const policyDecisions = [];
180
+ const policyDecisions = new Map(history?.policyDecisions.map((decision) => [decision.id, decision]));
101
181
  const acceptanceIds = new Set(meta.acceptance.map((criterion) => criterion.id));
102
182
  const journal = (event, data) => {
103
183
  const line = JSON.stringify({
@@ -149,7 +229,7 @@ export async function runWorkflow(scriptPath, opts) {
149
229
  if (!policy) {
150
230
  throw new Error("external mutation authorization requires a host policy");
151
231
  }
152
- if (policyDecisions.length >= MAX_POLICY_DECISIONS) {
232
+ if (policyDecisions.size >= MAX_POLICY_DECISIONS && !policyDecisions.has(`p${policySeq + 1}`)) {
153
233
  throw new Error(`workflow policy decision cap reached (${MAX_POLICY_DECISIONS}); split the workflow or reduce protected actions`);
154
234
  }
155
235
  const request = summarizePolicyRequest(rawRequest);
@@ -169,10 +249,8 @@ export async function runWorkflow(scriptPath, opts) {
169
249
  let reason = evaluation.reason;
170
250
  if (evaluation.verdict === "ask") {
171
251
  resolvedBy = "fail_closed";
172
- if (!opts.resolvePolicyAsk) {
173
- reason = `${evaluation.reason}; no host approval resolver is configured`;
174
- }
175
- else {
252
+ let immediateResolution = false;
253
+ if (opts.resolvePolicyAsk) {
176
254
  try {
177
255
  const resolution = await withTimeout(Promise.resolve(opts.resolvePolicyAsk(request, evaluation)), opts.policyAskTimeoutMs ?? DEFAULT_POLICY_ASK_TIMEOUT_MS);
178
256
  if (!resolution || (resolution.verdict !== "allow" && resolution.verdict !== "deny")) {
@@ -182,12 +260,54 @@ export async function runWorkflow(scriptPath, opts) {
182
260
  verdict = resolution.verdict;
183
261
  resolvedBy = "host";
184
262
  reason = boundedPolicyReason(resolution.reason ?? evaluation.reason);
263
+ immediateResolution = true;
185
264
  }
186
265
  }
187
266
  catch (error) {
188
267
  reason = `${evaluation.reason}; approval failed closed: ${error.message}`;
189
268
  }
190
269
  }
270
+ else {
271
+ reason = `${evaluation.reason}; no host approval resolver is configured`;
272
+ }
273
+ if (!immediateResolution && approvalMode === "park") {
274
+ const stored = createWorkflowApproval({
275
+ coordRoot: opts.coordRoot,
276
+ runId,
277
+ decisionId: id,
278
+ addressedTo: approvalAddressee,
279
+ policy: { name: policy.name, sha256: policyDigest(policy) },
280
+ request,
281
+ evaluation,
282
+ });
283
+ if (stored.approval.status === "pending") {
284
+ if (stored.created) {
285
+ journal("approval.requested", {
286
+ approval_id: stored.approval.request.id,
287
+ decision_id: id,
288
+ addressed_to: stored.approval.request.addressed_to,
289
+ request_sha256: stored.approval.request.request_sha256,
290
+ });
291
+ }
292
+ journal("run.parked", {
293
+ approval_id: stored.approval.request.id,
294
+ decision_id: id,
295
+ phase: request.phase,
296
+ action: request.action,
297
+ });
298
+ throw new WorkflowParkedError(`workflow parked for approval ${stored.approval.request.id}: ${evaluation.reason}`, runId, stored.approval.request.id, journalPath);
299
+ }
300
+ verdict = stored.approval.decision.verdict;
301
+ resolvedBy = "approval";
302
+ reason = boundedPolicyReason(stored.approval.decision.reason ?? evaluation.reason);
303
+ journal("approval.consumed", {
304
+ approval_id: stored.approval.request.id,
305
+ decision_id: id,
306
+ verdict,
307
+ actor: stored.approval.decision.actor,
308
+ decided_at: stored.approval.decision.decided_at,
309
+ });
310
+ }
191
311
  }
192
312
  const decision = {
193
313
  id,
@@ -201,7 +321,7 @@ export async function runWorkflow(scriptPath, opts) {
201
321
  rule_codes: evaluation.rules.map((rule) => rule.code),
202
322
  request,
203
323
  };
204
- policyDecisions.push(decision);
324
+ policyDecisions.set(decision.id, decision);
205
325
  journal("policy.resolve", { ...decision });
206
326
  if (decision.verdict === "deny") {
207
327
  throw new PolicyDeniedError(`policy ${JSON.stringify(policy.name)} denied ${request.phase} ${JSON.stringify(request.action)}: ${decision.reason}`, decision.id);
@@ -236,6 +356,11 @@ export async function runWorkflow(scriptPath, opts) {
236
356
  const key = agentCallKey(currentStage, harness, agentOpts, prompt);
237
357
  const cached = resumeCache.get(key);
238
358
  if (cached) {
359
+ // Exact-run replay skips dispatch authorization because no dispatch
360
+ // occurs, but it must still reserve the original policy slot so a later
361
+ // durable ASK resolves against the same pN identity.
362
+ if (policy && opts.resumeRunId)
363
+ policySeq++;
239
364
  agentsCached++;
240
365
  agentProof.status = "cached";
241
366
  agentProof.result = digestResult(cached.value, cached.kind);
@@ -247,7 +372,7 @@ export async function runWorkflow(scriptPath, opts) {
247
372
  model: agentOpts.model ?? null,
248
373
  kind: cached.kind,
249
374
  });
250
- log(`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${opts.resumeFrom})`);
375
+ log(`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${resumeSource})`);
251
376
  return cached.value;
252
377
  }
253
378
  if (policy) {
@@ -327,19 +452,19 @@ export async function runWorkflow(scriptPath, opts) {
327
452
  billingProbed.set(harness, probe);
328
453
  journal("billing.probe", {
329
454
  harness,
330
- mode: opts.subscriptionOnly ? "subscription" : probe.mode,
455
+ mode: subscriptionOnly ? "subscription" : probe.mode,
331
456
  api_key_source: probe.apiKeySource,
332
457
  login: probe.login,
333
- subscription_only: Boolean(opts.subscriptionOnly),
458
+ subscription_only: subscriptionOnly,
334
459
  });
335
- if (opts.subscriptionOnly) {
460
+ if (subscriptionOnly) {
336
461
  if (probe.login === "absent") {
337
462
  throw new Error(`subscription-only: no stored login detected for ${harness}; ` +
338
463
  `log the harness CLI in (or drop --subscription-only for a key-only host)`);
339
464
  }
340
465
  log(`[billing] ${harness}: subscription-only (API-key vars scrubbed from child env)`);
341
466
  }
342
- else if (probe.mode === "api-key-override" && !opts.allowApiBilling) {
467
+ else if (probe.mode === "api-key-override" && !allowApiBilling) {
343
468
  throw new Error(`${probe.apiKeySource} is set AND a stored ${harness} login exists — the key silently ` +
344
469
  `overrides your subscription auth, so children would bill per-token API rates. ` +
345
470
  `Either unset ${probe.apiKeySource}, run with --subscription-only to scrub it from ` +
@@ -392,7 +517,7 @@ export async function runWorkflow(scriptPath, opts) {
392
517
  maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
393
518
  cwd,
394
519
  runId,
395
- subscriptionOnly: opts.subscriptionOnly,
520
+ subscriptionOnly,
396
521
  });
397
522
  agentProof.attempts = attempt;
398
523
  agentProof.duration_ms += last.durationMs;
@@ -413,6 +538,7 @@ export async function runWorkflow(scriptPath, opts) {
413
538
  key,
414
539
  attempts: attempt,
415
540
  cost_usd: last.costUsd,
541
+ total_cost_usd: agentCostUsd,
416
542
  duration_ms: last.durationMs,
417
543
  session_id: last.sessionId,
418
544
  result_kind: "text",
@@ -435,6 +561,7 @@ export async function runWorkflow(scriptPath, opts) {
435
561
  key,
436
562
  attempts: attempt,
437
563
  cost_usd: last.costUsd,
564
+ total_cost_usd: agentCostUsd,
438
565
  duration_ms: last.durationMs,
439
566
  session_id: last.sessionId,
440
567
  result_kind: "json",
@@ -511,20 +638,27 @@ export async function runWorkflow(scriptPath, opts) {
511
638
  });
512
639
  };
513
640
  const ctx = { agent, parallel, stage, log, evidence, authorize };
514
- const t0 = Date.now();
515
- const startedAt = new Date(t0).toISOString();
516
- journal("run.start", {
517
- name,
518
- script: absScript,
519
- objective: meta.objective ?? null,
520
- acceptance: meta.acceptance,
521
- max_agents: maxAgents,
522
- concurrency,
523
- policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
524
- isolation,
525
- network_access: networkAccess,
526
- });
527
641
  try {
642
+ if (resumeState) {
643
+ journal("run.resume", {
644
+ approval_id: resumeState.approvalId,
645
+ script: absScript,
646
+ resumed_at: new Date().toISOString(),
647
+ });
648
+ }
649
+ else {
650
+ journal("run.start", {
651
+ name,
652
+ script: absScript,
653
+ objective: meta.objective ?? null,
654
+ acceptance: meta.acceptance,
655
+ max_agents: maxAgents,
656
+ concurrency,
657
+ policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
658
+ isolation,
659
+ network_access: networkAccess,
660
+ });
661
+ }
528
662
  const result = await mod.default(ctx);
529
663
  const endedAt = new Date().toISOString();
530
664
  const durationMs = Date.now() - t0;
@@ -551,7 +685,12 @@ export async function runWorkflow(scriptPath, opts) {
551
685
  evidence: evidenceRecords,
552
686
  harnessEvidence: opts.harnessEvidence,
553
687
  policy: policy
554
- ? { config: policy, decisions: policyDecisions, isolation, networkAccess }
688
+ ? {
689
+ config: policy,
690
+ decisions: Array.from(policyDecisions.values()),
691
+ isolation,
692
+ networkAccess,
693
+ }
555
694
  : undefined,
556
695
  result,
557
696
  });
@@ -574,14 +713,14 @@ export async function runWorkflow(scriptPath, opts) {
574
713
  contextTokensPerChildEstimate,
575
714
  billing: Array.from(billingProbed.values()).map((p) => ({
576
715
  harness: p.harness,
577
- mode: opts.subscriptionOnly ? "subscription" : p.mode,
716
+ mode: subscriptionOnly ? "subscription" : p.mode,
578
717
  })),
579
718
  policy: proof.policy?.summary,
580
719
  };
581
720
  return report;
582
721
  }
583
722
  catch (err) {
584
- if (err instanceof WorkflowRunError)
723
+ if (err instanceof WorkflowParkedError || err instanceof WorkflowRunError)
585
724
  throw err;
586
725
  const endedAt = new Date().toISOString();
587
726
  const durationMs = Date.now() - t0;
@@ -608,7 +747,12 @@ export async function runWorkflow(scriptPath, opts) {
608
747
  evidence: evidenceRecords,
609
748
  harnessEvidence: opts.harnessEvidence,
610
749
  policy: policy
611
- ? { config: policy, decisions: policyDecisions, isolation, networkAccess }
750
+ ? {
751
+ config: policy,
752
+ decisions: Array.from(policyDecisions.values()),
753
+ isolation,
754
+ networkAccess,
755
+ }
612
756
  : undefined,
613
757
  error: err.message,
614
758
  });
@@ -676,6 +820,38 @@ function loadResumeCache(coordRoot, resumeFrom) {
676
820
  }
677
821
  return cache;
678
822
  }
823
+ function loadRunHistory(path) {
824
+ const agentIds = new Set();
825
+ const agentCosts = new Map();
826
+ const decisions = new Map();
827
+ for (const line of readFileSync(path, "utf8").split("\n")) {
828
+ if (!line.trim())
829
+ continue;
830
+ const event = JSON.parse(line);
831
+ if (event.event === "agent.start" && typeof event.id === "string") {
832
+ agentIds.add(event.id);
833
+ }
834
+ if (event.event === "agent.end" && typeof event.id === "string") {
835
+ const cost = event.total_cost_usd ?? event.cost_usd;
836
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
837
+ agentCosts.set(event.id, cost);
838
+ }
839
+ }
840
+ if (event.event === "policy.resolve" &&
841
+ typeof event.id === "string" &&
842
+ (event.verdict === "allow" || event.verdict === "deny") &&
843
+ typeof event.resolved_by === "string" &&
844
+ event.request &&
845
+ typeof event.request === "object") {
846
+ decisions.set(event.id, event);
847
+ }
848
+ }
849
+ return {
850
+ agentsSpawned: agentIds.size,
851
+ costUsd: round4(Array.from(agentCosts.values()).reduce((sum, value) => sum + value, 0)),
852
+ policyDecisions: Array.from(decisions.values()),
853
+ };
854
+ }
679
855
  function round4(n) {
680
856
  return Math.round(n * 10_000) / 10_000;
681
857
  }
@@ -684,6 +860,11 @@ function assertAgentCapacity(agentsSpawned, maxAgents) {
684
860
  throw new Error(`workflow agent cap reached (${maxAgents}); raise --max-agents deliberately if the fan-out is intended`);
685
861
  }
686
862
  }
863
+ function assertPositiveWorkflowBound(value, field) {
864
+ if (!Number.isSafeInteger(value) || value <= 0) {
865
+ throw new Error(`${field} must be a positive safe integer`);
866
+ }
867
+ }
687
868
  function boundedPolicyReason(value) {
688
869
  const normalized = Array.from(value, (character) => {
689
870
  const code = character.charCodeAt(0);
@@ -1,5 +1,9 @@
1
- export { runWorkflow, WorkflowRunError } from "./engine.js";
1
+ export type { CreateWorkflowApprovalInput, ResolveWorkflowApprovalInput, WorkflowApproval, WorkflowApprovalDecision, WorkflowApprovalRequest, WorkflowApprovalStatus, } from "./approvals.js";
2
+ export { assertWorkflowRunId, createWorkflowApproval, listWorkflowApprovals, readWorkflowApproval, renderWorkflowApproval, resolveWorkflowApproval, WORKFLOW_APPROVAL_SCHEMA_VERSION, workflowApprovalId, } from "./approvals.js";
3
+ export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js";
2
4
  export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
3
- export type { AcceptanceCriterion, AcceptanceResult, AcceptanceStatus, AcceptanceSummary, AgentOpts, EngineOpts, EvidenceKind, EvidenceSource, EvidenceStatus, HarnessEvidenceCapability, HarnessEvidenceCoverage, HarnessName, ResultDigest, RunReport, Spawner, SpawnRequest, SpawnResult, StageSchema, WorkflowAgentProof, WorkflowContext, WorkflowEvidenceInput, WorkflowEvidenceRecord, WorkflowMeta, WorkflowModule, WorkflowPolicyProof, WorkflowProof, WorkflowProofUnknown, WorkflowRepoEvidence, WorkflowRepoSnapshot, WorkflowRunStatus, } from "./types.js";
5
+ export type { WorkflowRunManifest } from "./run-state.js";
6
+ export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, readWorkflowRunManifest, WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION, workflowRunManifestPath, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
7
+ export type { AcceptanceCriterion, AcceptanceResult, AcceptanceStatus, AcceptanceSummary, AgentOpts, EngineOpts, EvidenceKind, EvidenceSource, EvidenceStatus, HarnessEvidenceCapability, HarnessEvidenceCoverage, HarnessName, ResultDigest, RunReport, Spawner, SpawnRequest, SpawnResult, StageSchema, WorkflowAgentProof, WorkflowApprovalMode, WorkflowContext, WorkflowEvidenceInput, WorkflowEvidenceRecord, WorkflowMeta, WorkflowModule, WorkflowPolicyProof, WorkflowProof, WorkflowProofUnknown, WorkflowRepoEvidence, WorkflowRepoSnapshot, WorkflowRunStatus, } from "./types.js";
4
8
  export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
5
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,EACX,YAAY,EACZ,SAAS,EACT,OAAO,EACP,YAAY,EACZ,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,gCAAgC,EAChC,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,6BAA6B,EAC7B,uBAAuB,EACvB,oCAAoC,EACpC,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,EACX,YAAY,EACZ,SAAS,EACT,OAAO,EACP,YAAY,EACZ,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC"}
@@ -1,3 +1,5 @@
1
- export { runWorkflow, WorkflowRunError } from "./engine.js";
1
+ export { assertWorkflowRunId, createWorkflowApproval, listWorkflowApprovals, readWorkflowApproval, renderWorkflowApproval, resolveWorkflowApproval, WORKFLOW_APPROVAL_SCHEMA_VERSION, workflowApprovalId, } from "./approvals.js";
2
+ export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js";
2
3
  export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
4
+ export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, readWorkflowRunManifest, WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION, workflowRunManifestPath, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
3
5
  export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
@@ -0,0 +1,43 @@
1
+ import type { RepoSnapshot } from "../context/index.js";
2
+ import { type NormalizedPolicy, type PolicyIsolation, type PolicyNetworkAccess } from "../policy/index.js";
3
+ export declare const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION: 1;
4
+ export interface WorkflowRunManifest {
5
+ schema_version: typeof WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION;
6
+ run_id: string;
7
+ name: string;
8
+ started_at: string;
9
+ script: {
10
+ path: string;
11
+ sha256: string;
12
+ };
13
+ repository_before: RepoSnapshot;
14
+ execution: {
15
+ cwd: string;
16
+ default_harness: string;
17
+ max_agents: number;
18
+ concurrency: number;
19
+ subscription_only: boolean;
20
+ allow_api_billing: boolean;
21
+ approval_mode: "deny" | "park";
22
+ approval_addressee: string;
23
+ isolation: PolicyIsolation;
24
+ network_access: PolicyNetworkAccess;
25
+ policy?: NormalizedPolicy;
26
+ };
27
+ }
28
+ export interface WriteWorkflowRunManifestInput {
29
+ coordRoot: string;
30
+ manifest: WorkflowRunManifest;
31
+ }
32
+ export declare function workflowScriptDigest(scriptPath: string): string;
33
+ export declare function writeWorkflowRunManifest(input: WriteWorkflowRunManifestInput): string;
34
+ export declare function readWorkflowRunManifest(coordRoot: string, runId: string): WorkflowRunManifest;
35
+ export declare function assertWorkflowRunResumable(coordRoot: string, runId: string): {
36
+ manifest: WorkflowRunManifest;
37
+ approvalId: string;
38
+ };
39
+ export declare function assertWorkflowScriptUnchanged(manifest: WorkflowRunManifest): void;
40
+ export declare function workflowRunManifestPath(coordRoot: string, runId: string): string;
41
+ /** Acquire an exclusive, crash-recoverable lease for one resume segment. */
42
+ export declare function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void;
43
+ //# sourceMappingURL=run-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAG5B,eAAO,MAAM,oCAAoC,EAAG,CAAU,CAAC;AAK/D,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,oCAAoC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iBAAiB,EAAE,YAAY,CAAC;IAChC,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,iBAAiB,EAAE,OAAO,CAAC;QAC3B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,SAAS,EAAE,eAAe,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG,MAAM,CAmBrF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CA8B7F;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA4BvD;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CASjF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CA0CvF"}