harnery 0.20.0 → 0.22.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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/core/work/index.d.ts +1 -1
  3. package/dist/core/work/index.d.ts.map +1 -1
  4. package/dist/core/work/read.d.ts +1 -1
  5. package/dist/core/work/read.d.ts.map +1 -1
  6. package/dist/core/work/runner.d.ts +1 -1
  7. package/dist/core/work/runner.d.ts.map +1 -1
  8. package/dist/core/work/runner.js +61 -0
  9. package/dist/core/work/state.d.ts +3 -0
  10. package/dist/core/work/state.d.ts.map +1 -1
  11. package/dist/core/work/state.js +40 -6
  12. package/dist/core/workflow/attempt-context.d.ts +5 -0
  13. package/dist/core/workflow/attempt-context.d.ts.map +1 -0
  14. package/dist/core/workflow/attempt-context.js +189 -0
  15. package/dist/core/workflow/engine.d.ts.map +1 -1
  16. package/dist/core/workflow/engine.js +47 -3
  17. package/dist/core/workflow/index.d.ts +3 -2
  18. package/dist/core/workflow/index.d.ts.map +1 -1
  19. package/dist/core/workflow/index.js +2 -1
  20. package/dist/core/workflow/proof.d.ts +3 -1
  21. package/dist/core/workflow/proof.d.ts.map +1 -1
  22. package/dist/core/workflow/proof.js +14 -1
  23. package/dist/core/workflow/run-state.d.ts +3 -1
  24. package/dist/core/workflow/run-state.d.ts.map +1 -1
  25. package/dist/core/workflow/run-state.js +10 -0
  26. package/dist/core/workflow/types.d.ts +43 -0
  27. package/dist/core/workflow/types.d.ts.map +1 -1
  28. package/dist/core/workflow/types.js +2 -0
  29. package/dist/core/workflow/work-context.d.ts +5 -0
  30. package/dist/core/workflow/work-context.d.ts.map +1 -0
  31. package/dist/core/workflow/work-context.js +58 -0
  32. package/package.json +1 -1
  33. package/src/core/work/index.ts +1 -0
  34. package/src/core/work/read.ts +1 -0
  35. package/src/core/work/runner.ts +76 -2
  36. package/src/core/work/state.ts +68 -6
  37. package/src/core/workflow/attempt-context.ts +215 -0
  38. package/src/core/workflow/engine.ts +61 -2
  39. package/src/core/workflow/index.ts +15 -1
  40. package/src/core/workflow/proof.ts +20 -1
  41. package/src/core/workflow/run-state.ts +17 -1
  42. package/src/core/workflow/types.ts +52 -0
  43. package/src/core/workflow/work-context.ts +66 -0
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
- - **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Its one-shot reconciler derives state from append-only local evidence and reports the next explicit action without silently spawning, retrying, or accepting work.
25
+ - **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
26
  - **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
27
  - **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)).
28
28
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
@@ -1,3 +1,3 @@
1
1
  export { type RunWorkItemInput, runWorkItem } from "./runner.js";
2
- export { acceptWorkItem, assertWorkId, type CreateWorkItemInput, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
2
+ export { acceptWorkItem, assertWorkId, type CreateWorkItemInput, cancelWorkItem, createWorkItem, listWorkItems, newWorkId, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkAttemptTrigger, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/work/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/work/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
@@ -1,2 +1,2 @@
1
- export { assertWorkId, listWorkItems, readWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
1
+ export { assertWorkId, listWorkItems, readWorkItem, WORK_EVENT_SCHEMA_VERSION, WORK_INTENT_SCHEMA_VERSION, type WorkAttempt, type WorkAttemptTrigger, type WorkEvent, type WorkEventType, type WorkIntent, type WorkNextAction, type WorkProjection, type WorkRecord, type WorkState, } from "./state.js";
2
2
  //# sourceMappingURL=read.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/work/read.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/work/read.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
@@ -2,7 +2,7 @@ import type { EngineOpts, RunReport } from "../workflow/types.js";
2
2
  export interface RunWorkItemInput {
3
3
  coordRoot: string;
4
4
  workId: string;
5
- engine: Omit<EngineOpts, "coordRoot" | "runId" | "resumeRunId" | "workItemId">;
5
+ engine: Omit<EngineOpts, "coordRoot" | "runId" | "resumeRunId" | "workItemId" | "workContext" | "attemptContext">;
6
6
  retry?: boolean;
7
7
  actor?: string;
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/work/runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAWlE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2D7E"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/work/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EAGV,MAAM,sBAAsB,CAAC;AAgB9B,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,CACV,UAAU,EACV,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,GAAG,gBAAgB,CACxF,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAqF7E"}
@@ -1,5 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { runWorkflow } from "../workflow/engine.js";
3
+ import { readWorkflowProof } from "../workflow/proof.js";
4
+ import { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION, WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION, } from "../workflow/types.js";
3
5
  import { acquireWorkLease, appendReconcileIfChanged, appendWorkEvent, assertWorkflowUnchanged, boundedActor, newWorkflowRunId, readWorkItemIgnoringLease, } from "./state.js";
4
6
  export async function runWorkItem(input) {
5
7
  const coordRoot = resolve(input.coordRoot);
@@ -29,6 +31,9 @@ export async function runWorkItem(input) {
29
31
  if (record.projection.state === "blocked" && !input.retry) {
30
32
  throw new Error(`work item ${input.workId} is blocked; use an explicit retry`);
31
33
  }
34
+ if (record.projection.state === "ready" && input.retry) {
35
+ throw new Error(`work item ${input.workId} is ready; retry requires a blocked prior attempt`);
36
+ }
32
37
  if (!(record.projection.state === "ready" || record.projection.state === "blocked")) {
33
38
  throw new Error(`work item ${input.workId} cannot run from state ${record.projection.state}`);
34
39
  }
@@ -36,6 +41,20 @@ export async function runWorkItem(input) {
36
41
  throw new Error(`work item ${input.workId} exhausted its ${record.intent.max_attempts} attempts`);
37
42
  }
38
43
  const attempt = record.projection.attempts_used + 1;
44
+ const trigger = input.retry ? "retry" : "initial";
45
+ let prior;
46
+ if (trigger === "retry") {
47
+ if (!latest) {
48
+ throw new Error(`work item ${input.workId} has no prior attempt to retry`);
49
+ }
50
+ prior = priorContext(coordRoot, latest);
51
+ }
52
+ const attemptContext = {
53
+ schema_version: WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION,
54
+ number: attempt,
55
+ trigger,
56
+ ...(prior ? { prior } : {}),
57
+ };
39
58
  const runId = newWorkflowRunId();
40
59
  appendWorkEvent(coordRoot, input.workId, {
41
60
  event: "attempt.started",
@@ -43,12 +62,21 @@ export async function runWorkItem(input) {
43
62
  reason: input.retry ? "explicit retry started" : "workflow attempt started",
44
63
  run_id: runId,
45
64
  attempt,
65
+ trigger,
46
66
  });
47
67
  return await runWorkflow(record.intent.workflow.path, {
48
68
  ...input.engine,
49
69
  coordRoot,
50
70
  runId,
51
71
  workItemId: input.workId,
72
+ workContext: {
73
+ schema_version: WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
74
+ id: record.intent.id,
75
+ title: record.intent.title,
76
+ objective: record.intent.objective,
77
+ acceptance: [...record.intent.acceptance],
78
+ },
79
+ attemptContext,
52
80
  });
53
81
  }
54
82
  finally {
@@ -61,3 +89,36 @@ export async function runWorkItem(input) {
61
89
  }
62
90
  }
63
91
  }
92
+ function priorContext(coordRoot, prior) {
93
+ if (!prior.proof_path) {
94
+ return {
95
+ run_id: prior.run_id,
96
+ causes: ["lost"],
97
+ unresolved: [],
98
+ };
99
+ }
100
+ const proof = readWorkflowProof(coordRoot, prior.run_id);
101
+ const causes = [];
102
+ if (proof.run.status === "failed")
103
+ causes.push("workflow_error");
104
+ if (proof.acceptance.summary.unsatisfied > 0)
105
+ causes.push("acceptance_unsatisfied");
106
+ if (proof.acceptance.summary.unknown > 0)
107
+ causes.push("acceptance_unknown");
108
+ if (causes.length === 0) {
109
+ throw new Error(`workflow proof ${prior.run_id} does not describe a retryable failure`);
110
+ }
111
+ return {
112
+ run_id: prior.run_id,
113
+ causes,
114
+ error: proof.run.error,
115
+ acceptance: { ...proof.acceptance.summary },
116
+ unresolved: proof.acceptance.criteria
117
+ .filter((criterion) => criterion.status !== "satisfied")
118
+ .map((criterion) => ({
119
+ id: criterion.id,
120
+ statement: criterion.statement,
121
+ status: criterion.status,
122
+ })),
123
+ };
124
+ }
@@ -2,6 +2,7 @@ export declare const WORK_INTENT_SCHEMA_VERSION: 1;
2
2
  export declare const WORK_EVENT_SCHEMA_VERSION: 1;
3
3
  export type WorkState = "waiting" | "ready" | "running" | "awaiting_approval" | "in_review" | "blocked" | "succeeded" | "cancelled";
4
4
  export type WorkNextAction = "wait_for_dependencies" | "run" | "wait_for_run" | "resolve_approval" | "resume" | "retry" | "review" | "none";
5
+ export type WorkAttemptTrigger = "initial" | "retry";
5
6
  export interface WorkIntent {
6
7
  schema_version: typeof WORK_INTENT_SCHEMA_VERSION;
7
8
  id: string;
@@ -31,6 +32,7 @@ export interface WorkEvent {
31
32
  reason?: string;
32
33
  run_id?: string;
33
34
  attempt?: number;
35
+ trigger?: WorkAttemptTrigger;
34
36
  state?: WorkState;
35
37
  next_action?: WorkNextAction;
36
38
  }
@@ -38,6 +40,7 @@ export interface WorkAttempt {
38
40
  number: number;
39
41
  run_id: string;
40
42
  started_at: string;
43
+ trigger?: WorkAttemptTrigger;
41
44
  status: "running" | "parked" | "succeeded" | "failed" | "lost";
42
45
  approval_id?: string;
43
46
  proof_path?: string;
@@ -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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"}
@@ -156,7 +156,7 @@ function deriveWorkProjection(coordRoot, intent, events, ignoreLease = false) {
156
156
  .find((event) => ["work.accepted", "work.cancelled", "work.reopened"].includes(event.event));
157
157
  const terminal = latestGovernance?.event === "work.accepted" || latestGovernance?.event === "work.cancelled";
158
158
  const attemptEvents = events.filter((event) => event.event === "attempt.started");
159
- const attempts = attemptEvents.map((event) => inspectAttempt(coordRoot, event));
159
+ const attempts = attemptEvents.map((event, index) => inspectAttempt(coordRoot, event, intent, attemptEvents[index - 1]?.run_id));
160
160
  const base = {
161
161
  id: intent.id,
162
162
  title: intent.title,
@@ -179,9 +179,7 @@ function deriveWorkProjection(coordRoot, intent, events, ignoreLease = false) {
179
179
  };
180
180
  }
181
181
  const reopenSeq = latestGovernance?.event === "work.reopened" ? latestGovernance.seq : 0;
182
- const currentAttempts = attemptEvents
183
- .filter((event) => event.seq > reopenSeq)
184
- .map((event) => inspectAttempt(coordRoot, event));
182
+ const currentAttempts = attempts.filter((_, index) => attemptEvents[index].seq > reopenSeq);
185
183
  const unresolved = intent.dependencies.filter((dependency) => {
186
184
  try {
187
185
  return readWorkItem(coordRoot, dependency).projection.state !== "succeeded";
@@ -257,27 +255,49 @@ function deriveWorkProjection(coordRoot, intent, events, ignoreLease = false) {
257
255
  next_action: attemptsRemaining > 0 ? "retry" : "none",
258
256
  };
259
257
  }
260
- function inspectAttempt(coordRoot, event) {
258
+ function inspectAttempt(coordRoot, event, intent, priorRunId) {
261
259
  const runId = event.run_id;
262
260
  const attempt = {
263
261
  number: event.attempt,
264
262
  run_id: runId,
265
263
  started_at: event.ts,
264
+ trigger: event.trigger,
266
265
  status: "lost",
267
266
  };
268
267
  const proofPath = join(coordRoot, ".harnery", "workflows", runId, "proof.json");
269
268
  const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
269
+ let manifest;
270
270
  if (existsSync(manifestPath)) {
271
- const manifest = readWorkflowRunManifest(coordRoot, runId);
271
+ manifest = readWorkflowRunManifest(coordRoot, runId);
272
272
  if (manifest.work_item_id !== event.work_id) {
273
273
  throw new Error(`workflow run ${runId} does not belong to work item ${event.work_id}`);
274
274
  }
275
+ if (manifest.work_context !== undefined &&
276
+ JSON.stringify(manifest.work_context) !== JSON.stringify(workContextForIntent(intent))) {
277
+ throw new Error(`workflow run ${runId} work context does not match work item ${event.work_id}`);
278
+ }
279
+ if (manifest.attempt_context !== undefined &&
280
+ (manifest.attempt_context.number !== event.attempt ||
281
+ manifest.attempt_context.trigger !== event.trigger ||
282
+ (manifest.attempt_context.trigger === "retry" &&
283
+ manifest.attempt_context.prior?.run_id !== priorRunId))) {
284
+ throw new Error(`workflow run ${runId} attempt context does not match work item ${event.work_id}`);
285
+ }
275
286
  }
276
287
  if (existsSync(proofPath)) {
277
288
  if (!existsSync(manifestPath)) {
278
289
  throw new Error(`workflow run ${runId} has proof without a work-linked manifest`);
279
290
  }
280
291
  const proof = readWorkflowProof(coordRoot, runId);
292
+ if (proof.run.work_item_id !== event.work_id) {
293
+ throw new Error(`workflow proof ${runId} does not belong to work item ${event.work_id}`);
294
+ }
295
+ if (JSON.stringify(proof.run.work_context) !== JSON.stringify(manifest?.work_context)) {
296
+ throw new Error(`workflow proof ${runId} work context does not match its run manifest`);
297
+ }
298
+ if (JSON.stringify(proof.run.attempt_context) !== JSON.stringify(manifest?.attempt_context)) {
299
+ throw new Error(`workflow proof ${runId} attempt context does not match its run manifest`);
300
+ }
281
301
  attempt.proof_path = proofPath;
282
302
  attempt.status =
283
303
  proof.run.status === "succeeded" &&
@@ -310,6 +330,15 @@ function inspectAttempt(coordRoot, event) {
310
330
  attempt.status = "running";
311
331
  return attempt;
312
332
  }
333
+ function workContextForIntent(intent) {
334
+ return {
335
+ schema_version: 1,
336
+ id: intent.id,
337
+ title: intent.title,
338
+ objective: intent.objective,
339
+ acceptance: [...intent.acceptance],
340
+ };
341
+ }
313
342
  function appendGovernanceEvent(coordRoot, workId, event, allowed, input) {
314
343
  const release = acquireWorkLease(coordRoot, workId);
315
344
  try {
@@ -458,6 +487,11 @@ function validateWorkEvent(event, workId, seq) {
458
487
  throw new Error(`work item ${workId} event ${seq} has invalid attempt data`);
459
488
  }
460
489
  }
490
+ if (event.trigger !== undefined &&
491
+ (event.event !== "attempt.started" ||
492
+ (event.trigger !== "initial" && event.trigger !== "retry"))) {
493
+ throw new Error(`work item ${workId} event ${seq} has invalid attempt trigger`);
494
+ }
461
495
  if (event.event === "work.reconciled") {
462
496
  const states = [
463
497
  "waiting",
@@ -0,0 +1,5 @@
1
+ import type { WorkflowAttemptContext } from "./types.js";
2
+ export declare function normalizeWorkflowAttemptContext(value: unknown): WorkflowAttemptContext;
3
+ export declare function freezeWorkflowAttemptContext(value: unknown): Readonly<WorkflowAttemptContext>;
4
+ export declare function isCanonicalWorkflowAttemptContext(value: unknown): value is WorkflowAttemptContext;
5
+ //# sourceMappingURL=attempt-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-context.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/attempt-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EAIvB,MAAM,YAAY,CAAC;AAoBpB,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CA8BtF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,sBAAsB,CAAC,CAU7F;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CAOjG"}
@@ -0,0 +1,189 @@
1
+ import { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION } from "./types.js";
2
+ const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
3
+ const ACCEPTANCE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4
+ const MAX_ATTEMPTS = 100;
5
+ const MAX_ERROR = 2_000;
6
+ const MAX_CRITERIA = 50;
7
+ const MAX_STATEMENT = 500;
8
+ const CONTEXT_FIELDS = new Set(["schema_version", "number", "trigger", "prior"]);
9
+ const PRIOR_FIELDS = new Set(["run_id", "causes", "error", "acceptance", "unresolved"]);
10
+ const SUMMARY_FIELDS = new Set(["satisfied", "unsatisfied", "unknown", "total"]);
11
+ const CRITERION_FIELDS = new Set(["id", "statement", "status"]);
12
+ const CAUSE_ORDER = [
13
+ "workflow_error",
14
+ "acceptance_unsatisfied",
15
+ "acceptance_unknown",
16
+ "lost",
17
+ ];
18
+ export function normalizeWorkflowAttemptContext(value) {
19
+ const input = objectValue(value, "workflow attempt context");
20
+ rejectExtraFields(input, CONTEXT_FIELDS, "workflow attempt context");
21
+ if (input.schema_version !== WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION) {
22
+ throw new Error("workflow attempt context has an unsupported schema");
23
+ }
24
+ const number = safeInteger(input.number, "workflow attempt context number", 1, MAX_ATTEMPTS);
25
+ if (input.trigger !== "initial" && input.trigger !== "retry") {
26
+ throw new Error('workflow attempt context trigger must be "initial" or "retry"');
27
+ }
28
+ if (input.trigger === "initial") {
29
+ if (input.prior !== undefined) {
30
+ throw new Error("initial workflow attempt context cannot contain prior evidence");
31
+ }
32
+ return {
33
+ schema_version: WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION,
34
+ number,
35
+ trigger: "initial",
36
+ };
37
+ }
38
+ if (number < 2)
39
+ throw new Error("retry workflow attempt context number must be at least 2");
40
+ if (input.prior === undefined) {
41
+ throw new Error("retry workflow attempt context requires prior evidence");
42
+ }
43
+ return {
44
+ schema_version: WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION,
45
+ number,
46
+ trigger: "retry",
47
+ prior: normalizePrior(input.prior),
48
+ };
49
+ }
50
+ export function freezeWorkflowAttemptContext(value) {
51
+ const normalized = normalizeWorkflowAttemptContext(value);
52
+ if (normalized.prior) {
53
+ for (const criterion of normalized.prior.unresolved)
54
+ Object.freeze(criterion);
55
+ Object.freeze(normalized.prior.causes);
56
+ Object.freeze(normalized.prior.acceptance);
57
+ Object.freeze(normalized.prior.unresolved);
58
+ Object.freeze(normalized.prior);
59
+ }
60
+ return Object.freeze(normalized);
61
+ }
62
+ export function isCanonicalWorkflowAttemptContext(value) {
63
+ try {
64
+ const normalized = normalizeWorkflowAttemptContext(value);
65
+ return JSON.stringify(normalized) === JSON.stringify(value);
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function normalizePrior(value) {
72
+ const input = objectValue(value, "workflow attempt prior context");
73
+ rejectExtraFields(input, PRIOR_FIELDS, "workflow attempt prior context");
74
+ const runId = requiredString(input.run_id, "workflow attempt prior run id", 200);
75
+ if (!RUN_ID.test(runId)) {
76
+ throw new Error(`invalid workflow attempt prior run id ${JSON.stringify(runId)}`);
77
+ }
78
+ if (!Array.isArray(input.causes) || input.causes.length === 0) {
79
+ throw new Error("workflow attempt prior causes must be a non-empty array");
80
+ }
81
+ const seen = new Set();
82
+ for (const cause of input.causes) {
83
+ if (!CAUSE_ORDER.includes(cause)) {
84
+ throw new Error(`unsupported workflow attempt failure cause ${JSON.stringify(cause)}`);
85
+ }
86
+ if (seen.has(cause)) {
87
+ throw new Error(`duplicate workflow attempt failure cause ${JSON.stringify(cause)}`);
88
+ }
89
+ seen.add(cause);
90
+ }
91
+ const causes = CAUSE_ORDER.filter((cause) => seen.has(cause));
92
+ if (causes.includes("lost")) {
93
+ if (causes.length !== 1)
94
+ throw new Error("lost must be the only workflow attempt failure cause");
95
+ if (input.error !== undefined || input.acceptance !== undefined) {
96
+ throw new Error("lost workflow attempt evidence cannot contain proof-derived fields");
97
+ }
98
+ const unresolved = normalizeUnresolved(input.unresolved);
99
+ if (unresolved.length > 0) {
100
+ throw new Error("lost workflow attempt evidence cannot contain unresolved criteria");
101
+ }
102
+ return { run_id: runId, causes, unresolved };
103
+ }
104
+ const acceptance = normalizeAcceptanceSummary(input.acceptance);
105
+ const unresolved = normalizeUnresolved(input.unresolved);
106
+ const unsatisfied = unresolved.filter((criterion) => criterion.status === "unsatisfied").length;
107
+ const unknown = unresolved.filter((criterion) => criterion.status === "unknown").length;
108
+ if (unsatisfied !== acceptance.unsatisfied ||
109
+ unknown !== acceptance.unknown ||
110
+ causes.includes("acceptance_unsatisfied") !== acceptance.unsatisfied > 0 ||
111
+ causes.includes("acceptance_unknown") !== acceptance.unknown > 0) {
112
+ throw new Error("workflow attempt failure evidence does not match acceptance");
113
+ }
114
+ if (!causes.includes("workflow_error") && acceptance.unsatisfied + acceptance.unknown === 0) {
115
+ throw new Error("workflow attempt prior context does not describe a failure");
116
+ }
117
+ const error = input.error === undefined
118
+ ? undefined
119
+ : requiredString(input.error, "workflow attempt prior error", MAX_ERROR);
120
+ return {
121
+ run_id: runId,
122
+ causes,
123
+ ...(error === undefined ? {} : { error }),
124
+ acceptance,
125
+ unresolved,
126
+ };
127
+ }
128
+ function normalizeAcceptanceSummary(value) {
129
+ const input = objectValue(value, "workflow attempt acceptance summary");
130
+ rejectExtraFields(input, SUMMARY_FIELDS, "workflow attempt acceptance summary");
131
+ const satisfied = safeInteger(input.satisfied, "acceptance satisfied", 0, MAX_CRITERIA);
132
+ const unsatisfied = safeInteger(input.unsatisfied, "acceptance unsatisfied", 0, MAX_CRITERIA);
133
+ const unknown = safeInteger(input.unknown, "acceptance unknown", 0, MAX_CRITERIA);
134
+ const total = safeInteger(input.total, "acceptance total", 0, MAX_CRITERIA);
135
+ if (satisfied + unsatisfied + unknown !== total) {
136
+ throw new Error("workflow attempt acceptance summary total does not match its counts");
137
+ }
138
+ return { satisfied, unsatisfied, unknown, total };
139
+ }
140
+ function normalizeUnresolved(value) {
141
+ if (!Array.isArray(value) || value.length > MAX_CRITERIA) {
142
+ throw new Error(`workflow attempt unresolved criteria must contain at most ${MAX_CRITERIA} entries`);
143
+ }
144
+ const ids = new Set();
145
+ return value.map((candidate, index) => {
146
+ const input = objectValue(candidate, `workflow attempt unresolved[${index}]`);
147
+ rejectExtraFields(input, CRITERION_FIELDS, `workflow attempt unresolved[${index}]`);
148
+ const id = requiredString(input.id, `workflow attempt unresolved[${index}].id`, 64);
149
+ if (!ACCEPTANCE_ID.test(id)) {
150
+ throw new Error(`invalid workflow attempt acceptance id ${JSON.stringify(id)}`);
151
+ }
152
+ if (ids.has(id))
153
+ throw new Error(`duplicate workflow attempt acceptance id ${JSON.stringify(id)}`);
154
+ ids.add(id);
155
+ if (input.status !== "unsatisfied" && input.status !== "unknown") {
156
+ throw new Error(`workflow attempt unresolved[${index}].status is invalid`);
157
+ }
158
+ return {
159
+ id,
160
+ statement: requiredString(input.statement, `workflow attempt unresolved[${index}].statement`, MAX_STATEMENT),
161
+ status: input.status,
162
+ };
163
+ });
164
+ }
165
+ function objectValue(value, field) {
166
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
167
+ throw new Error(`${field} must be an object`);
168
+ }
169
+ return value;
170
+ }
171
+ function rejectExtraFields(value, allowed, field) {
172
+ const extra = Object.keys(value).find((key) => !allowed.has(key));
173
+ if (extra)
174
+ throw new Error(`${field} has unknown field ${JSON.stringify(extra)}`);
175
+ }
176
+ function requiredString(value, field, max) {
177
+ if (typeof value !== "string" || value.trim() === "")
178
+ throw new Error(`${field} is required`);
179
+ const normalized = value.trim();
180
+ if (normalized.length > max)
181
+ throw new Error(`${field} exceeds ${max} characters`);
182
+ return normalized;
183
+ }
184
+ function safeInteger(value, field, min, max) {
185
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
186
+ throw new Error(`${field} must be an integer from ${min} to ${max}`);
187
+ }
188
+ return value;
189
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqCH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAQV,MAAM,YAAY,CAAC;AAYpB,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,CAyC1F"}
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"}
@@ -31,12 +31,14 @@ 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
33
  import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.js";
34
+ import { freezeWorkflowAttemptContext } from "./attempt-context.js";
34
35
  import { probeBilling } from "./billing.js";
35
36
  import { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, writeWorkflowProof, } from "./proof.js";
36
37
  import { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
37
38
  import { normalizeWorkflowSpecialists, resolveSpecialistAssignment } from "./specialists.js";
38
39
  import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
39
40
  import { parseStageOutput, validateAgainstSchema } from "./validate.js";
41
+ import { freezeWorkflowWorkContext } from "./work-context.js";
40
42
  const DEFAULT_MAX_AGENTS = 50;
41
43
  const DEFAULT_CONCURRENCY = 4;
42
44
  const DEFAULT_MAX_ATTEMPTS = 2;
@@ -75,6 +77,25 @@ export async function runWorkflow(scriptPath, opts) {
75
77
  if (opts.workItemId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts.workItemId)) {
76
78
  throw new Error(`invalid work item id ${JSON.stringify(opts.workItemId)}`);
77
79
  }
80
+ if (opts.resumeRunId && opts.workContext !== undefined) {
81
+ throw new Error("parked workflow resume uses the frozen manifest work context");
82
+ }
83
+ if (opts.resumeRunId && opts.attemptContext !== undefined) {
84
+ throw new Error("parked workflow resume uses the frozen manifest attempt context");
85
+ }
86
+ const requestedWorkContext = opts.workContext === undefined ? undefined : freezeWorkflowWorkContext(opts.workContext);
87
+ const requestedAttemptContext = opts.attemptContext === undefined
88
+ ? undefined
89
+ : freezeWorkflowAttemptContext(opts.attemptContext);
90
+ if (requestedWorkContext && !opts.workItemId) {
91
+ throw new Error("workflow work context requires a work item id");
92
+ }
93
+ if (requestedWorkContext && requestedWorkContext.id !== opts.workItemId) {
94
+ throw new Error(`workflow work context ${requestedWorkContext.id} does not match work item ${opts.workItemId}`);
95
+ }
96
+ if (requestedAttemptContext && !requestedWorkContext) {
97
+ throw new Error("workflow attempt context requires work context");
98
+ }
78
99
  if (opts.resumeRunId && opts.resumeFrom) {
79
100
  throw new Error("resumeRunId and resumeFrom are mutually exclusive");
80
101
  }
@@ -86,6 +107,12 @@ export async function runWorkflow(scriptPath, opts) {
86
107
  resumeState.manifest.work_item_id !== opts.workItemId) {
87
108
  throw new Error(`workflow run ${opts.resumeRunId} belongs to a different work item`);
88
109
  }
110
+ const workContext = resumeState?.manifest.work_context
111
+ ? freezeWorkflowWorkContext(resumeState.manifest.work_context)
112
+ : requestedWorkContext;
113
+ const attemptContext = resumeState?.manifest.attempt_context
114
+ ? freezeWorkflowAttemptContext(resumeState.manifest.attempt_context)
115
+ : requestedAttemptContext;
89
116
  const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
90
117
  if (resumeState) {
91
118
  if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
@@ -102,13 +129,13 @@ export async function runWorkflow(scriptPath, opts) {
102
129
  // the run between the optimistic read and lease acquisition.
103
130
  assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
104
131
  }
105
- return await executeWorkflow(scriptPath, absScript, opts, resumeState);
132
+ return await executeWorkflow(scriptPath, absScript, opts, resumeState, workContext, attemptContext);
106
133
  }
107
134
  finally {
108
135
  releaseResumeLease?.();
109
136
  }
110
137
  }
111
- async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
138
+ async function executeWorkflow(scriptPath, absScript, opts, resumeState, workContext, attemptContext) {
112
139
  const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(absScript).href)));
113
140
  if (typeof mod.default !== "function") {
114
141
  throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
@@ -149,6 +176,8 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
149
176
  schema_version: 1,
150
177
  run_id: runId,
151
178
  work_item_id: opts.workItemId,
179
+ work_context: workContext,
180
+ attempt_context: attemptContext,
152
181
  name,
153
182
  started_at: startedAt,
154
183
  script: { path: absScript, sha256: workflowScriptDigest(absScript) },
@@ -668,7 +697,16 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
668
697
  current_cost_usd: round4(costUsd),
669
698
  });
670
699
  };
671
- const ctx = { agent, parallel, stage, log, evidence, authorize };
700
+ const ctx = {
701
+ work: workContext,
702
+ attempt: attemptContext,
703
+ agent,
704
+ parallel,
705
+ stage,
706
+ log,
707
+ evidence,
708
+ authorize,
709
+ };
672
710
  try {
673
711
  if (resumeState) {
674
712
  journal("run.resume", {
@@ -681,6 +719,8 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
681
719
  journal("run.start", {
682
720
  name,
683
721
  work_item_id: opts.workItemId ?? null,
722
+ work_context: workContext ?? null,
723
+ attempt_context: attemptContext ?? null,
684
724
  script: absScript,
685
725
  objective: meta.objective ?? null,
686
726
  acceptance: meta.acceptance,
@@ -707,6 +747,8 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
707
747
  proof = buildWorkflowProof({
708
748
  runId,
709
749
  workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
750
+ workContext,
751
+ attemptContext,
710
752
  meta,
711
753
  status: "succeeded",
712
754
  startedAt,
@@ -771,6 +813,8 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
771
813
  const proof = buildWorkflowProof({
772
814
  runId,
773
815
  workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
816
+ workContext,
817
+ attemptContext,
774
818
  meta,
775
819
  status: "failed",
776
820
  startedAt,
@@ -1,9 +1,10 @@
1
1
  export type { CreateWorkflowApprovalInput, ResolveWorkflowApprovalInput, WorkflowApproval, WorkflowApprovalDecision, WorkflowApprovalRequest, WorkflowApprovalStatus, } from "./approvals.js";
2
2
  export { assertWorkflowRunId, createWorkflowApproval, listWorkflowApprovals, readWorkflowApproval, renderWorkflowApproval, resolveWorkflowApproval, WORKFLOW_APPROVAL_SCHEMA_VERSION, workflowApprovalId, } from "./approvals.js";
3
+ export { freezeWorkflowAttemptContext, isCanonicalWorkflowAttemptContext, normalizeWorkflowAttemptContext, } from "./attempt-context.js";
3
4
  export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js";
4
5
  export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
5
6
  export type { WorkflowRunManifest } from "./run-state.js";
6
7
  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, WorkflowSpecialistProfile, } from "./types.js";
8
- export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
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
+ export { WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION, WORKFLOW_PROOF_SCHEMA_VERSION, WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION, } from "./types.js";
9
10
  //# sourceMappingURL=index.d.ts.map