@orkestrel/workflow 0.0.16 → 0.0.18

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.
@@ -5,35 +5,69 @@ let _orkestrel_database = require("@orkestrel/database");
5
5
  let _orkestrel_emitter = require("@orkestrel/emitter");
6
6
  let _orkestrel_timeout = require("@orkestrel/timeout");
7
7
  let _orkestrel_queue = require("@orkestrel/queue");
8
+ //#region src/core/errors.ts
9
+ /**
10
+ * Represents an error the workflow runtime raises for an operation it refuses — a
11
+ * {@link WorkflowErrorCode} (`TRANSITION`, `RESTORE`, `MUTATION`, `SCHEDULE`, or `INVARIANT`)
12
+ * beside an optional `context` naming the node or the parameter at fault.
13
+ *
14
+ * @remarks
15
+ * Raised for an illegal lifecycle transition
16
+ * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
17
+ * boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), a host
18
+ * schedule refused before arming because the caller's `signal` is not a native
19
+ * `AbortSignal` (`SCHEDULE`, delivered as a rejected promise), or a broken internal
20
+ * invariant (`INVARIANT`).
21
+ */
22
+ var WorkflowError = class extends Error {
23
+ code;
24
+ context;
25
+ constructor(code, message, context) {
26
+ super(message);
27
+ this.name = "WorkflowError";
28
+ this.code = code;
29
+ if (context !== void 0) this.context = context;
30
+ }
31
+ };
32
+ /**
33
+ * Narrows an unknown caught value to a {@link WorkflowError}.
34
+ *
35
+ * @param value - The value to test (typically a `catch` binding)
36
+ * @returns True if `value` is a {@link WorkflowError}; false otherwise
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * try {
41
+ * task.complete('done')
42
+ * } catch (error) {
43
+ * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
44
+ * }
45
+ * ```
46
+ */
47
+ function isWorkflowError(value) {
48
+ try {
49
+ return value instanceof WorkflowError;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ //#endregion
8
55
  //#region src/core/constants.ts
9
- /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
56
+ /**
57
+ * Names the default {@link import('./types.js').WorkflowDefinition.bail}, `false` — the graceful
58
+ * policy that records a leaf failure and finishes every phase.
59
+ */
10
60
  var DEFAULT_BAIL = false;
11
61
  /**
12
- * Every {@link TaskStatus} value, frozen — the lifecycle vocabulary of a task.
62
+ * Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from,
63
+ * in the order `pending`, `running`, `completed`, `failed`, `skipped`, `stopped`.
13
64
  *
14
65
  * @remarks
15
66
  * Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
16
- * `stopped`). The source of truth for the union; compose guards / shapes from it.
67
+ * `stopped`). The runtime source of truth for the union:
68
+ * {@link import('./validators.js').isLifecycleStatus} reads this array.
17
69
  */
18
- var TASK_STATUSES = Object.freeze([
19
- "pending",
20
- "running",
21
- "completed",
22
- "failed",
23
- "skipped",
24
- "stopped"
25
- ]);
26
- /** Every {@link PhaseStatus} value, frozen — the lifecycle vocabulary of a phase. */
27
- var PHASE_STATUSES = Object.freeze([
28
- "pending",
29
- "running",
30
- "completed",
31
- "failed",
32
- "skipped",
33
- "stopped"
34
- ]);
35
- /** Every {@link WorkflowStatus} value, frozen — the lifecycle vocabulary of a workflow. */
36
- var WORKFLOW_STATUSES = Object.freeze([
70
+ var LIFECYCLE_STATUSES = Object.freeze([
37
71
  "pending",
38
72
  "running",
39
73
  "completed",
@@ -42,21 +76,21 @@ var WORKFLOW_STATUSES = Object.freeze([
42
76
  "stopped"
43
77
  ]);
44
78
  /**
45
- * The {@link TaskStatus} values that are TERMINAL a task in one of these will
46
- * not transition further, frozen.
79
+ * Lists the terminal {@link LifecycleStatus} values, frozen`completed`, `failed`, `skipped`,
80
+ * and `stopped`, each a state a node never transitions out of.
47
81
  *
48
82
  * @remarks
49
83
  * The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
50
84
  * `pending` and `running` are the only non-terminal members.
51
85
  */
52
- var TERMINAL_TASK_STATUSES = Object.freeze([
86
+ var TERMINAL_STATUSES = Object.freeze([
53
87
  "completed",
54
88
  "failed",
55
89
  "skipped",
56
90
  "stopped"
57
91
  ]);
58
92
  /**
59
- * The legal {@link TaskStatus} transition graph of the live W-b task state machine —
93
+ * Declares the legal {@link LifecycleStatus} transition graph of the live W-b task state machine —
60
94
  * each current status mapped to the statuses it may move to directly, frozen.
61
95
  *
62
96
  * @remarks
@@ -86,94 +120,161 @@ var TASK_TRANSITIONS = Object.freeze({
86
120
  stopped: []
87
121
  });
88
122
  /**
89
- * The default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
123
+ * Names the default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
90
124
  * runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
91
- * throttle — a cap that is effectively unbounded for any realistic phase.
125
+ * throttle — `1024`, a cap that is effectively unbounded for any realistic phase.
92
126
  *
93
127
  * @remarks
94
- * The determinism principle fixes that a phase's tasks run CONCURRENTLY; `concurrency` is
128
+ * The determinism principle fixes that a phase's tasks run concurrently; `concurrency` is
95
129
  * only an optional resource throttle (max-in-flight). With none declared, the runner runs
96
130
  * all of a phase's tasks at once — modelled as this finite cap so the value flows straight
97
131
  * into the substrate {@link import('./types.js').RunnerInterface}'s `concurrency` (which
98
132
  * expects a positive integer) without a special unbounded branch. No realistic phase
99
133
  * declares enough tasks to reach it, so it behaves as "run them all".
100
134
  *
101
- * WHY `1024` and not a huge sentinel like `1_000_000`: the backing `@orkestrel/queue` Runner
102
- * EAGERLY spawns one parked worker loop per concurrency unit AT CONSTRUCTION, so this default
135
+ * why `1024` and not a huge sentinel like `1_000_000`: the backing `@orkestrel/queue` Runner
136
+ * eagerly spawns one parked worker loop per concurrency unit at construction, so this default
103
137
  * must be a value whose eager allocation cost is negligible for every default-concurrency
104
138
  * phase — a million-unit default meant ~1e6 promise/closure allocations per such phase. A
105
- * phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
139
+ * phase may still declare a larger explicit `concurrency` and pays that allocation knowingly.
106
140
  */
107
141
  var DEFAULT_PHASE_CONCURRENCY = 1024;
108
142
  /**
109
- * The largest delay representable by the host timer APIs without overflow or clamping.
143
+ * Names the largest delay representable by the host timer APIs without overflow or clamping,
144
+ * `2_147_483_647` milliseconds.
110
145
  */
111
146
  var MAX_TIMER_MS = 2147483647;
147
+ /**
148
+ * Lists the {@link WorkflowEventMap} / {@link PhaseEventMap} events that make a durable observer
149
+ * re-persist the live tree, frozen — `start`, `complete`, `fail`, `skip`, `stop`, `move`, and
150
+ * `update`.
151
+ *
152
+ * @remarks
153
+ * The two maps carry the same event names, so one list serves both tiers. It is the source of
154
+ * truth behind {@link import('./WorkflowPersistence.js').WorkflowPersistence}'s attach and detach
155
+ * passes: subscribing and unsubscribing loop over these names, so an added event reaches both
156
+ * passes from one edit. `add` and `remove` are deliberately absent — they carry the new or dropped
157
+ * child, so the persistence layer binds its own attaching handler to them instead.
158
+ */
159
+ var PERSISTED_NODE_EVENTS = Object.freeze([
160
+ "start",
161
+ "complete",
162
+ "fail",
163
+ "skip",
164
+ "stop",
165
+ "move",
166
+ "update"
167
+ ]);
168
+ /**
169
+ * Lists the {@link TaskEventMap} events that make a durable observer re-persist the live tree,
170
+ * frozen — `start`, `complete`, `fail`, `skip`, `stop`, `report`, and `pulse`.
171
+ *
172
+ * @remarks
173
+ * The leaf counterpart of {@link PERSISTED_NODE_EVENTS}, and the source of truth behind the task
174
+ * attach and detach passes of
175
+ * {@link import('./WorkflowPersistence.js').WorkflowPersistence}. `report` and `pulse` join the
176
+ * lifecycle events because an accepted activity frame changes the persisted snapshot; a leaf has
177
+ * no children, so there is no structural event to bind separately.
178
+ */
179
+ var PERSISTED_TASK_EVENTS = Object.freeze([
180
+ "start",
181
+ "complete",
182
+ "fail",
183
+ "skip",
184
+ "stop",
185
+ "report",
186
+ "pulse"
187
+ ]);
112
188
  //#endregion
113
- //#region src/core/errors.ts
189
+ //#region src/core/validators.ts
114
190
  /**
115
- * An error raised by the workflow runtime.
191
+ * Checks whether an unknown value belongs to the workflow lifecycle vocabulary.
116
192
  *
117
193
  * @remarks
118
- * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
119
- * offending node id / status / parameter. Raised for an illegal lifecycle transition
120
- * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
121
- * boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), or a host
122
- * schedule refused before arming because the caller's `signal` is not a native
123
- * `AbortSignal` (`SCHEDULE`, delivered as a rejected promise).
194
+ * Reads {@link import('./constants.js').LIFECYCLE_STATUSES}, the runtime array every tier draws
195
+ * from, so the vocabulary has one definition rather than a hard-coded copy per guard.
196
+ *
197
+ * @param value - The value to test
198
+ * @returns True if `value` is a {@link LifecycleStatus}; false otherwise
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * isLifecycleStatus('running') // true
203
+ * isLifecycleStatus('paused') // false
204
+ * ```
124
205
  */
125
- var WorkflowError = class extends Error {
126
- code;
127
- context;
128
- constructor(code, message, context) {
129
- super(message);
130
- this.name = "WorkflowError";
131
- this.code = code;
132
- if (context !== void 0) this.context = context;
133
- }
134
- };
206
+ function isLifecycleStatus(value) {
207
+ return LIFECYCLE_STATUSES.some((status) => status === value);
208
+ }
135
209
  /**
136
- * Narrow an unknown caught value to a {@link WorkflowError}.
210
+ * Tests a normalized persisted task failure.
137
211
  *
138
- * @param value - The value to test (typically a `catch` binding)
139
- * @returns `true` when `value` is a {@link WorkflowError}
212
+ * @remarks
213
+ * The exact-record guard behind a persisted {@link TaskFailure}: exactly `origin` and `message`,
214
+ * an `origin` drawn from the {@link import('./types.js').TaskFailureOrigin} vocabulary, and a
215
+ * non-empty `message`. Total — a hostile prototype or accessor answers `false` rather than
216
+ * throwing.
217
+ *
218
+ * @param value - The value to test
219
+ * @returns True if `value` is a persisted {@link TaskFailure}; false otherwise
140
220
  *
141
221
  * @example
142
222
  * ```ts
143
- * try {
144
- * task.complete('done')
145
- * } catch (error) {
146
- * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
147
- * }
223
+ * isTaskFailure({ origin: 'handler', message: 'boom' }) // true
224
+ * isTaskFailure({ origin: 'handler' }) // false
148
225
  * ```
149
226
  */
150
- function isWorkflowError(value) {
227
+ function isTaskFailure(value) {
151
228
  try {
152
- return value instanceof WorkflowError;
229
+ return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
153
230
  } catch {
154
231
  return false;
155
232
  }
156
233
  }
157
- //#endregion
158
- //#region src/core/validators.ts
159
- /** Test the workflow lifecycle vocabulary. */
160
- function isLifecycleStatus(value) {
161
- return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
162
- }
163
- /** Test a normalized persisted task failure. */
164
- function isTaskFailure(value) {
234
+ /**
235
+ * Checks whether an unknown value is a live workflow entity rather than a definition.
236
+ *
237
+ * @remarks
238
+ * The discriminator behind the overloaded
239
+ * {@link import('./types.js').WorkflowRunnerInterface.execute}: a
240
+ * {@link import('./types.js').WorkflowInterface} is the only one of the two carrying `destroyed`
241
+ * (runtime-only, never a field on the pure-JSON
242
+ * {@link import('./types.js').WorkflowDefinition}) and a callable `snapshot`. Requiring both is
243
+ * sturdier than `destroyed` alone — a definition could coincidentally carry a `destroyed` field as
244
+ * arbitrary data, and pairing it with a function-typed `snapshot` narrows to the actual entity
245
+ * shape without an `as`. It reads a live class instance, so it tests object identity rather than a
246
+ * plain-record brand, and it is total: any other value answers `false`.
247
+ *
248
+ * @param value - The value to test
249
+ * @returns True if `value` is a live {@link WorkflowInterface}; false otherwise
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * isWorkflowInterface(createWorkflow(definition)) // true
254
+ * isWorkflowInterface(definition) // false
255
+ * ```
256
+ */
257
+ function isWorkflowInterface(value) {
165
258
  try {
166
- return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
259
+ return (0, _orkestrel_contract.isObject)(value) && "destroyed" in value && "snapshot" in value && (0, _orkestrel_contract.isFunction)(value.snapshot);
167
260
  } catch {
168
261
  return false;
169
262
  }
170
263
  }
171
264
  /**
172
- * Validate a safe owned JSON graph as a coherent workflow snapshot.
265
+ * Validates a safe owned JSON graph as a coherent workflow snapshot.
173
266
  *
174
267
  * @remarks
175
268
  * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
176
269
  * graph first so this semantic pass never observes accessors or prototypes.
270
+ *
271
+ * @param value - The already-owned JSON graph to validate
272
+ * @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * isOwnedWorkflowSnapshot(workflow.snapshot()) // true
277
+ * ```
177
278
  */
178
279
  function isOwnedWorkflowSnapshot(value) {
179
280
  try {
@@ -195,7 +296,7 @@ function isOwnedWorkflowSnapshot(value) {
195
296
  const statuses = [];
196
297
  if (phase.tasks.length > 0) vacuous = false;
197
298
  for (const task of phase.tasks) {
198
- if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
299
+ if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "behavior" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.behavior !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.behavior) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
199
300
  const budget = (task.retries ?? 0) + 1;
200
301
  if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
201
302
  if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
@@ -223,13 +324,82 @@ function isOwnedWorkflowSnapshot(value) {
223
324
  return false;
224
325
  }
225
326
  }
226
- /** Total hostile-boundary workflow snapshot guard. */
327
+ /**
328
+ * Guards the hostile boundary totally for a workflow snapshot.
329
+ *
330
+ * @remarks
331
+ * Owns the value first through the exact-JSON clone of `@orkestrel/contract`, then runs the
332
+ * semantic pass {@link isOwnedWorkflowSnapshot} over the owned copy — so no accessor, prototype,
333
+ * or cycle in the caller's graph is ever observed by the semantic pass. Total: an unclonable
334
+ * value answers `false` rather than throwing.
335
+ *
336
+ * @param value - The untrusted value to test
337
+ * @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * isWorkflowSnapshot(JSON.parse(payload)) // true only for a coherent snapshot
342
+ * ```
343
+ */
227
344
  function isWorkflowSnapshot(value) {
228
345
  const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
229
346
  return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
230
347
  }
231
348
  /**
232
- * Test whether an unknown value is a valid whole-frame activity report.
349
+ * Checks whether an unknown value is a valid list of task activity claims.
350
+ *
351
+ * @remarks
352
+ * The one guard behind both claim lists of a {@link TaskActivityInput} — its `operations` and its
353
+ * `constraints` — because {@link import('./types.js').TaskOperation} and
354
+ * {@link import('./types.js').TaskConstraint} are the same {@link TaskClaim} shape. Every member must be a plain record carrying exactly `id`, `name`, and
355
+ * `started`, with non-empty string `id` and `name`, a finite non-negative `started`, and an `id`
356
+ * unique within the list. Total: a hostile prototype, an accessor, or a cycle returns `false`
357
+ * rather than throwing.
358
+ *
359
+ * @param value - The value to test
360
+ * @returns True if `value` is a list of valid, uniquely identified claims; false otherwise
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * isTaskClaimList([{ id: 'fetch', name: 'Fetch', started: 1 }]) // true
365
+ * isTaskClaimList([{ id: 'fetch', name: 'Fetch' }]) // false
366
+ * ```
367
+ */
368
+ function isTaskClaimList(value) {
369
+ try {
370
+ if (!(0, _orkestrel_contract.isArray)(value)) return false;
371
+ const ids = /* @__PURE__ */ new Set();
372
+ for (const claim of value) {
373
+ if (!(0, _orkestrel_contract.isRecord)(claim)) return false;
374
+ const prototype = Object.getPrototypeOf(claim);
375
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) return false;
376
+ const id = claim.id;
377
+ const name = claim.name;
378
+ const started = claim.started;
379
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
380
+ ids.add(id);
381
+ }
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+ /**
388
+ * Tests whether an unknown value is a valid whole-frame activity report.
389
+ *
390
+ * @remarks
391
+ * The guard behind {@link import('./types.js').TaskInterface.report}: exactly the optional `note`,
392
+ * `progress`, `operations`, and `constraints` keys, with the two claim lists checked by
393
+ * {@link isTaskClaimList} and `progress` a finite non-negative value under an optional `total` at
394
+ * least as large. Total — a hostile prototype or accessor answers `false` rather than throwing.
395
+ *
396
+ * @param value - The value to test
397
+ * @returns True if `value` is a valid {@link TaskActivityInput}; false otherwise
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * isTaskActivityInput({ note: 'compiling', progress: { progress: 2, total: 5 } }) // true
402
+ * ```
233
403
  */
234
404
  function isTaskActivityInput(value) {
235
405
  try {
@@ -250,41 +420,29 @@ function isTaskActivityInput(value) {
250
420
  const message = progress.message;
251
421
  if (!(0, _orkestrel_contract.isFiniteNumber)(reported) || reported < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < reported) || message !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(message)) return false;
252
422
  }
253
- if (operations !== void 0) {
254
- if (!(0, _orkestrel_contract.isArray)(operations)) return false;
255
- const ids = /* @__PURE__ */ new Set();
256
- for (const operation of operations) {
257
- if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
258
- const operationPrototype = Object.getPrototypeOf(operation);
259
- if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
260
- const id = operation.id;
261
- const name = operation.name;
262
- const started = operation.started;
263
- if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
264
- ids.add(id);
265
- }
266
- }
267
- if (constraints !== void 0) {
268
- if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
269
- const ids = /* @__PURE__ */ new Set();
270
- for (const constraint of constraints) {
271
- if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
272
- const constraintPrototype = Object.getPrototypeOf(constraint);
273
- if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
274
- const id = constraint.id;
275
- const name = constraint.name;
276
- const started = constraint.started;
277
- if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
278
- ids.add(id);
279
- }
280
- }
423
+ if (operations !== void 0 && !isTaskClaimList(operations)) return false;
424
+ if (constraints !== void 0 && !isTaskClaimList(constraints)) return false;
281
425
  return true;
282
426
  } catch {
283
427
  return false;
284
428
  }
285
429
  }
286
430
  /**
287
- * Test whether an unknown value is valid persisted task activity.
431
+ * Tests whether an unknown value is valid persisted task activity.
432
+ *
433
+ * @remarks
434
+ * The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the required
435
+ * `operations`, `constraints`, and a finite non-negative `updated` stamp, because a stored frame
436
+ * has already been accepted and normalized. Total — a hostile prototype or accessor answers
437
+ * `false` rather than throwing.
438
+ *
439
+ * @param value - The value to test
440
+ * @returns True if `value` is a persisted {@link TaskActivity}; false otherwise
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * isTaskActivity({ operations: [], constraints: [], updated: 1 }) // true
445
+ * ```
288
446
  */
289
447
  function isTaskActivity(value) {
290
448
  try {
@@ -310,7 +468,7 @@ function isTaskActivity(value) {
310
468
  //#endregion
311
469
  //#region src/core/helpers.ts
312
470
  /**
313
- * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
471
+ * Captures every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
314
472
  *
315
473
  * @remarks
316
474
  * Direct property reads preserve inherited and non-enumerable option values while preventing
@@ -345,32 +503,159 @@ function captureWorkflowOptions(options) {
345
503
  });
346
504
  }
347
505
  /**
348
- * Test whether a {@link LifecycleStatus} is TERMINALa node in this state will not
349
- * transition further.
506
+ * Tests whether a {@link LifecycleStatus} is terminal`completed`, `failed`, `skipped`, or
507
+ * `stopped`, the states a node never transitions out of.
350
508
  *
351
509
  * @remarks
352
- * The ONE terminal check across all three tiers (AGENTS §4.4 "one concept = one word"):
510
+ * The one terminal check across every tier (AGENTS.md § Design laws, "one concept, one term"):
353
511
  * a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
354
512
  * single predicate covers them — {@link derivePhaseStatus} and {@link deriveWorkflowStatus}
355
- * both consult it to tell a settled node from an in-flight one. Terminal: `completed` /
356
- * `failed` / `skipped` / `stopped`; the only non-terminal states are `pending` and
357
- * `running`.
513
+ * both consult it to tell a settled node from an in-flight one. It reads the terminal set from
514
+ * {@link import('./constants.js').TERMINAL_STATUSES} (`completed` / `failed` / `skipped` /
515
+ * `stopped`), so that constant is the one definition; the only non-terminal states are `pending`
516
+ * and `running`.
358
517
  *
359
518
  * @param status - The lifecycle status to test (a task / phase / workflow status)
360
- * @returns `true` when the status is terminal
519
+ * @returns True if the status is terminal; false otherwise
361
520
  */
362
521
  function isTerminalStatus(status) {
363
- return status === "completed" || status === "failed" || status === "skipped" || status === "stopped";
522
+ return TERMINAL_STATUSES.includes(status);
364
523
  }
365
524
  /**
366
- * Derive a phase's status from its tasks' statuses (tasks are concurrent, so this
367
- * is an order-insensitive reduction).
525
+ * Tests whether a driving run must stop giving a workflow or one forced phase of it —
526
+ * more work.
527
+ *
528
+ * @remarks
529
+ * The halt gate a {@link import('./WorkflowRunner.js').WorkflowRunner} consults before starting a
530
+ * phase, before dispatching a task, and after every cooperative gate. A workflow is halted after
531
+ * its derived status is terminal but not `completed` — a `bail: true` failure, a caller's own
532
+ * graceful `stop()`, or a forced `skip`. `completed` is excluded deliberately: a workflow that
533
+ * completed vacuously is settled, not halted, and the distinction is what keeps the run from
534
+ * sweeping a finished tree. When a `phase` is supplied, its own forced `skipped` / `stopped` halts
535
+ * that phase's work too; a `failed` phase does not, because the workflow's own `bail` policy
536
+ * decides whether a failed phase ends the run.
537
+ *
538
+ * @param workflow - The live workflow the run is driving
539
+ * @param phase - The phase whose own forced terminal status also halts its tasks
540
+ * @returns True if the run must stop giving this workflow (or phase) more work; false otherwise
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * isHalted(workflow) // false while pending or running
545
+ * workflow.stop()
546
+ * isHalted(workflow) // true
547
+ * ```
548
+ */
549
+ function isHalted(workflow, phase) {
550
+ const status = workflow.status;
551
+ return isTerminalStatus(status) && status !== "completed" || phase?.status === "skipped" || phase?.status === "stopped";
552
+ }
553
+ /**
554
+ * Tests whether forcing a workflow `stopped` would still record the cancellation.
555
+ *
556
+ * @remarks
557
+ * `stop()` is a no-op after a workflow's status becomes terminal, so a run that must record a
558
+ * cancellation forces it only while this holds. It is not the negation of
559
+ * {@link isTerminalStatus}: `completed` and `skipped` both pass, because a run-level cancel that
560
+ * lands on a vacuously-completed or fully-skipped tree still records `stopped` as the outcome the
561
+ * caller asked for. Only an already-`failed` or already-`stopped` workflow has a terminal state
562
+ * worth keeping.
563
+ *
564
+ * @param workflow - The live workflow a run-level cancel would force
565
+ * @returns True if forcing `stopped` would change the recorded outcome; false otherwise
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * isStoppable(workflow) // true while pending, running, completed, or skipped
570
+ * workflow.stop()
571
+ * isStoppable(workflow) // false
572
+ * ```
573
+ */
574
+ function isStoppable(workflow) {
575
+ const status = workflow.status;
576
+ return status !== "failed" && status !== "stopped";
577
+ }
578
+ /**
579
+ * Tests whether a naturally-finished run may force its workflow `completed`.
580
+ *
581
+ * @remarks
582
+ * A run that walked every phase and still derives `pending` executed nothing — zero phases, or
583
+ * every phase empty — so it is vacuously done and the run settles it `completed`. Gated on
584
+ * exactly `pending` so a real `completed`, a `bail: true` `failed`, a `stopped`, or a derived
585
+ * `skipped` is never overridden. The tree-is-empty half of the rule is
586
+ * {@link WorkflowInterface.complete}'s own guard, which refuses a pending tree that still holds
587
+ * tasks.
588
+ *
589
+ * @param workflow - The live workflow the run has finished walking
590
+ * @returns True if the run may force the vacuous completion; false otherwise
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * isCompletable(createWorkflow({ id: 'w', name: 'W', phases: [] })) // true
595
+ * ```
596
+ */
597
+ function isCompletable(workflow) {
598
+ return workflow.status === "pending";
599
+ }
600
+ /**
601
+ * Tests whether a task attempt is being genuinely cancelled rather than merely timed out.
602
+ *
603
+ * @remarks
604
+ * The discriminator that keeps a per-attempt deadline off the skip path. Three causes fire a
605
+ * running task's folded signal, and only two of them mean "skip this task": the task's own
606
+ * `signal` (its `stop` / `skip`), and the unit or run signal (a sibling fail-fast under
607
+ * `bail: true`, or a run-level abort / timeout / budget / `destroy`). A bare per-attempt timeout
608
+ * fires neither — it aborts only the deadline portion of the attempt signal — so it stays a
609
+ * retryable failure of that attempt instead of skipping the leaf and losing the recorded fault.
610
+ * Read fresh at each call so a cancel that lands mid-dispatch is seen.
611
+ *
612
+ * @param task - The live task the attempt is driving
613
+ * @param controller - The substrate unit handle carrying the unit-level abort
614
+ * @param runSignal - The run's folded cancellation signal
615
+ * @returns True if the attempt is being genuinely cancelled; false otherwise
616
+ *
617
+ * @example
618
+ * ```ts
619
+ * isSkipping(task, controller, runSignal) // false until a cancel fires
620
+ * ```
621
+ */
622
+ function isSkipping(task, controller, runSignal) {
623
+ return task.signal.aborted || controller.aborted || runSignal.aborted;
624
+ }
625
+ /**
626
+ * Tests whether one attempt still owns the task it launched.
627
+ *
628
+ * @remarks
629
+ * A retried task is re-dispatched while an earlier attempt's handler may still be resolving, so
630
+ * every settlement path re-checks ownership before touching the leaf. Ownership needs both
631
+ * halves: the run-local `owners` ledger must still name this attempt, and the live task's own
632
+ * `attempts` tally must still match it. A superseded attempt reads `false` and returns without
633
+ * recording anything, so a late resolution can never overwrite the newer attempt's outcome.
634
+ *
635
+ * @param owners - The run-local ledger of the attempt owning each task id
636
+ * @param task - The live task the attempt launched
637
+ * @param attempt - The one-based attempt number to test
638
+ * @returns True if `attempt` still owns `task`; false otherwise
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * const owners = new Map([[task.id, 1]])
643
+ * ownsAttempt(owners, task, 1) // true while the task's own `attempts` is 1
644
+ * ownsAttempt(owners, task, 2) // false
645
+ * ```
646
+ */
647
+ function ownsAttempt(owners, task, attempt) {
648
+ return owners.get(task.id) === attempt && task.attempts === attempt;
649
+ }
650
+ /**
651
+ * Derives a phase's status from its tasks' statuses, the most severe terminal status winning
652
+ * (tasks are concurrent, so this is an order-insensitive reduction).
368
653
  *
369
654
  * @remarks
370
655
  * The truth table (most-severe terminal wins; `bail`-agnostic — a phase surfaces a
371
656
  * task failure as `failed` so the workflow's `bail` policy can decide):
372
657
  * - no tasks ⇒ `pending`.
373
- * - any task `running`, OR a mix of started-and-unsettled tasks (some non-`pending`
658
+ * - any task `running`, or a mix of started-and-unsettled tasks (some non-`pending`
374
659
  * but not all terminal) ⇒ `running`.
375
660
  * - every task `pending` ⇒ `pending`.
376
661
  * - all terminal: any `failed` ⇒ `failed`; else any `stopped` ⇒ `stopped`; else any
@@ -381,7 +666,7 @@ function isTerminalStatus(status) {
381
666
  * task makes the phase `failed`.
382
667
  *
383
668
  * @param tasks - The phase's task statuses, in any order
384
- * @returns The derived {@link PhaseStatus}
669
+ * @returns The derived phase {@link LifecycleStatus}
385
670
  */
386
671
  function derivePhaseStatus(tasks) {
387
672
  if (tasks.length === 0) return "pending";
@@ -393,24 +678,25 @@ function derivePhaseStatus(tasks) {
393
678
  return "skipped";
394
679
  }
395
680
  /**
396
- * Derive a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
397
- * paired with the EFFECTIVE `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
398
- * failure outcome is PER-PHASE-bail-aware (phases are sequential, but the derivation is an
399
- * order-insensitive reduction over the settled set).
681
+ * Derives a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
682
+ * paired with the effective `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
683
+ * failure outcome is aware of each phase's own policy, and `failed` is reachable only where
684
+ * that policy is `true` (phases are sequential, but the derivation is an order-insensitive
685
+ * reduction over the settled set).
400
686
  *
401
687
  * @remarks
402
- * `bail` is now a per-phase override (AGENTS §4.4), so it is carried on each
403
- * {@link PhaseDerivation} rather than passed as one scalar. It is the ONLY axis that changes
688
+ * `bail` is a per-phase override, so it is carried on each
689
+ * {@link PhaseDerivation} rather than passed as one scalar. It is the only axis that changes
404
690
  * the failure outcome, decided per phase:
405
691
  * - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
406
692
  * `failed` (the database-transaction halt) — even when the workflow default is graceful.
407
- * - **A `failed` phase whose effective `bail` is `false` (graceful)** is DATA, not a workflow
408
- * failure — it folds into completion like a settled phase. A graceful failed phase NEVER
693
+ * - **A `failed` phase whose effective `bail` is `false` (graceful)** is data, not a workflow
694
+ * failure — it folds into completion like a settled phase. A graceful failed phase never
409
695
  * makes the workflow `failed` — even when the workflow default is strict.
410
696
  *
411
697
  * The rest of the table is shared:
412
698
  * - no phases ⇒ `pending`.
413
- * - any phase `running`, OR a mix of started-and-unsettled phases (some non-`pending`
699
+ * - any phase `running`, or a mix of started-and-unsettled phases (some non-`pending`
414
700
  * but not all terminal) ⇒ `running`.
415
701
  * - every phase `pending` ⇒ `pending`.
416
702
  * - all terminal (a `failed` phase counts as terminal here): any `stopped` ⇒ `stopped`; else
@@ -418,7 +704,7 @@ function derivePhaseStatus(tasks) {
418
704
  * else (all `skipped`) ⇒ `skipped`.
419
705
  *
420
706
  * @param phases - The workflow's per-phase {@link PhaseDerivation}s (status + effective bail), in any order
421
- * @returns The derived {@link WorkflowStatus}
707
+ * @returns The derived workflow {@link LifecycleStatus}
422
708
  */
423
709
  function deriveWorkflowStatus(phases) {
424
710
  if (phases.length === 0) return "pending";
@@ -430,18 +716,19 @@ function deriveWorkflowStatus(phases) {
430
716
  return "skipped";
431
717
  }
432
718
  /**
433
- * Derive the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
434
- * the index of the first entry in the contiguous trailing run of `pending` entries.
719
+ * Derives the pending-suffix boundary of a positional list of {@link LifecycleStatus}es —
720
+ * the index of the first entry in the contiguous trailing run of `pending` entries, or the
721
+ * list's length where it has none.
435
722
  *
436
723
  * @remarks
437
- * The native, hook-free replacement for a runner-installed cursor (AGENTS §12): a
724
+ * The native, hook-free replacement for a runner-installed cursor: a
438
725
  * {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
439
726
  * reads this over its live phases' statuses to decide which positions are safe to edit.
440
- * Because entries run SEQUENTIALLY (phases sequential, AGENTS determinism), every
441
- * already-started entry forms a contiguous LEADING prefix and every still-`pending`
442
- * entry forms the trailing suffix — so the boundary is simply the count of leading
727
+ * Because entries run sequentially (phases sequential, AGENTS determinism), every
728
+ * already-started entry forms a contiguous leading prefix and every still-`pending`
729
+ * entry forms the trailing suffix — so the boundary is the count of leading
443
730
  * non-`pending` entries: the index of the first `pending` entry, or the full length when
444
- * none is `pending` (nothing is safely editable). A `pending` container's entries are ALL
731
+ * none is `pending` (nothing is safely editable). A `pending` container's entries are all
445
732
  * `pending`, so the boundary is `0` and every position is naturally accepted — callers
446
733
  * need no special case for that.
447
734
  *
@@ -460,8 +747,8 @@ function deriveBoundary(statuses) {
460
747
  return index === -1 ? statuses.length : index;
461
748
  }
462
749
  /**
463
- * Test whether the live W-b task state machine may move directly from one
464
- * {@link TaskStatus} to another — the legal-transition guard.
750
+ * Tests whether the live W-b task state machine may move directly from one
751
+ * {@link LifecycleStatus} to another — the legal-transition guard.
465
752
  *
466
753
  * @remarks
467
754
  * Reads the {@link import('./constants.js').TASK_TRANSITIONS} graph: `true` only when
@@ -471,13 +758,14 @@ function deriveBoundary(statuses) {
471
758
  *
472
759
  * @param from - The task's current status
473
760
  * @param to - The status the transition would move it to
474
- * @returns `true` when the move is legal
761
+ * @returns True if the move is legal; false otherwise
475
762
  */
476
763
  function canTransitionTask(from, to) {
477
764
  return TASK_TRANSITIONS[from].includes(to);
478
765
  }
479
766
  /**
480
- * Resolve a task's runtime silence window against its workflow default.
767
+ * Resolves a task's runtime silence window against its workflow default, to a host-safe
768
+ * `1..MAX_TIMER_MS` window or to `undefined` where the task disables it.
481
769
  *
482
770
  * @param value - The task-level override; any present non-positive or non-finite value disables
483
771
  * @param fallback - The workflow-level default
@@ -488,7 +776,7 @@ function resolveTaskSilence(value, fallback) {
488
776
  return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
489
777
  }
490
778
  /**
491
- * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
779
+ * Boxes a value as a {@link Success} — the graceful outcome half of a {@link Result}.
492
780
  *
493
781
  * @typeParam T - The boxed value's type
494
782
  * @param value - The value to box
@@ -506,7 +794,7 @@ function success(value) {
506
794
  };
507
795
  }
508
796
  /**
509
- * Box an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
797
+ * Boxes an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
510
798
  *
511
799
  * @typeParam E - The boxed error's type
512
800
  * @param error - The error to box
@@ -524,7 +812,7 @@ function failure(error) {
524
812
  };
525
813
  }
526
814
  /**
527
- * Normalize an unknown thrown value to a non-empty persistence-safe message.
815
+ * Normalizes an unknown thrown value to a non-empty persistence-safe message.
528
816
  *
529
817
  * @param error - The caught value
530
818
  * @returns A non-empty message without stack or cause data
@@ -538,16 +826,16 @@ function errorToMessage(error) {
538
826
  }
539
827
  }
540
828
  /**
541
- * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
829
+ * Finds the first {@link TaskResult} in a positional list whose boxed outcome is a
542
830
  * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
543
831
  * `fail`-event lookup.
544
832
  *
545
833
  * @remarks
546
834
  * The shared leaf behind {@link import('./phases/Phase.js').Phase} and
547
- * {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers ITS tier's
835
+ * {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers its tier's
548
836
  * results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
549
- * them here; the tier-local method keeps the §12 invariant throw (a derived `failed`
550
- * status guarantees a failing result exists) since throwing on `undefined` is
837
+ * them here; the tier-local method keeps the invariant throw (a derived `failed`
838
+ * status means a failing result exists) because throwing on `undefined` is
551
839
  * orchestration, not a leaf concern.
552
840
  *
553
841
  * @param results - The results to scan, in any order
@@ -562,7 +850,7 @@ function findFailure(results) {
562
850
  return results.find((result) => result.result?.success === false);
563
851
  }
564
852
  /**
565
- * Build a {@link WorkflowContext} — the identity every level inherits — from a node's
853
+ * Builds a {@link WorkflowContext} — the identity every level inherits — from a node's
566
854
  * `id` / `name` / optional `description`.
567
855
  *
568
856
  * @remarks
@@ -581,10 +869,10 @@ function buildWorkflowContext(node) {
581
869
  });
582
870
  }
583
871
  /**
584
- * Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
872
+ * Builds a {@link PhaseContext} — a phase's own identity plus a back-reference to its
585
873
  * workflow — from the parent {@link WorkflowContext} and the phase node's identity.
586
874
  *
587
- * @param workflow - The parent workflow context (the lineage pointer UP the tree)
875
+ * @param workflow - The parent workflow context (the lineage pointer up the tree)
588
876
  * @param node - The phase's identity (`id` / `name` / optional `description`)
589
877
  * @returns The {@link PhaseContext}
590
878
  */
@@ -595,11 +883,11 @@ function buildPhaseContext(workflow, node) {
595
883
  });
596
884
  }
597
885
  /**
598
- * Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
886
+ * Builds a {@link TaskContext} — a task's own identity plus a back-reference to its phase
599
887
  * (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
600
888
  * node's identity.
601
889
  *
602
- * @param phase - The parent phase context (carrying the full lineage UP the tree)
890
+ * @param phase - The parent phase context (carrying the full lineage up the tree)
603
891
  * @param node - The task's identity (`id` / `name` / optional `description`)
604
892
  * @returns The {@link TaskContext}
605
893
  */
@@ -610,31 +898,31 @@ function buildTaskContext(phase, node) {
610
898
  });
611
899
  }
612
900
  /**
613
- * Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
614
- * node `pending`, no results, empty metadata — so the live W-b tree has ONE construction
615
- * path (snapshot-driven) for both a fresh build and a restore.
901
+ * Converts a {@link WorkflowDefinition} into an initial {@link WorkflowSnapshot} — every
902
+ * node `pending`, no results, empty metadata — so the live W-b tree has one construction
903
+ * path, snapshot-driven, for a fresh build and for a restore alike.
616
904
  *
617
905
  * @remarks
618
906
  * The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
619
907
  * carry over verbatim, as does each phase's `concurrency` (persisted on the
620
- * {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `run` /
908
+ * {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `behavior` /
621
909
  * `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
622
910
  * so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
623
911
  * real work). The `bail` policy carries over — at the
624
- * workflow tier AND, per phase, the
625
- * EFFECTIVE policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
626
- * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped now.
912
+ * workflow tier and, per phase, the
913
+ * effective policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
914
+ * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped at that point.
627
915
  * {@link import('./factories.js').createWorkflow} builds from this.
628
916
  *
629
- * The optional `bail` override is the EFFECTIVE workflow policy the tree will run under
917
+ * The optional `bail` override is the effective workflow policy the tree will run under
630
918
  * (`createWorkflow` / the runner resolve `options.bail ?? definition.bail ?? DEFAULT_BAIL` and
631
- * pass it here), so an `options.bail` override reaches BOTH the workflow tier AND the
919
+ * pass it here), so an `options.bail` override reaches both the workflow tier and the
632
920
  * inheritance default of every phase that declares no `bail` of its own — otherwise the
633
921
  * per-phase seeds would silently ignore the override. Omitted ⇒ the definition's own `bail`
634
922
  * (defaulting to the graceful {@link import('./constants.js').DEFAULT_BAIL}).
635
923
  *
636
924
  * @param definition - The workflow definition to seed from
637
- * @param bail - The EFFECTIVE workflow bail to seed both tiers with (defaults to the definition's)
925
+ * @param bail - The effective workflow bail to seed both tiers with (defaults to the definition's)
638
926
  * @returns An initial, all-`pending` {@link WorkflowSnapshot}
639
927
  */
640
928
  function definitionToSnapshot(definition, bail) {
@@ -652,11 +940,11 @@ function definitionToSnapshot(definition, bail) {
652
940
  };
653
941
  }
654
942
  /**
655
- * Convert one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
943
+ * Converts one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
656
944
  * {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
657
945
  *
658
946
  * @remarks
659
- * The snapshot persists the EFFECTIVE failure policy this phase runs under: the phase's own
947
+ * The snapshot persists the effective failure policy this phase runs under: the phase's own
660
948
  * `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
661
949
  * the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
662
950
  * `concurrency` (the resource throttle) carries over verbatim, omitted when undefined.
@@ -677,14 +965,14 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
677
965
  };
678
966
  }
679
967
  /**
680
- * Convert one {@link import('./types.js').TaskDefinition} into an initial, `pending`
968
+ * Converts one {@link import('./types.js').TaskDefinition} into an initial, `pending`
681
969
  * {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
682
970
  * result yet, empty metadata).
683
971
  *
684
972
  * @remarks
685
- * `run` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
973
+ * `behavior` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
686
974
  * phase's `bail` / `concurrency`) — a restore reinstates the same behavior reference and
687
- * reliability overrides once paired with a {@link import('./types.js').WorkflowOptions.functions}
975
+ * reliability overrides after pairing with a {@link import('./types.js').WorkflowOptions.functions}
688
976
  * registry.
689
977
  *
690
978
  * @param task - The task definition to seed from
@@ -698,13 +986,13 @@ function taskDefinitionToSnapshot(task) {
698
986
  status: "pending",
699
987
  metadata: {},
700
988
  attempts: 0,
701
- ...task.run === void 0 ? {} : { run: task.run },
989
+ ...task.behavior === void 0 ? {} : { behavior: task.behavior },
702
990
  ...task.retries === void 0 ? {} : { retries: task.retries },
703
991
  ...task.timeout === void 0 ? {} : { timeout: task.timeout }
704
992
  };
705
993
  }
706
994
  /**
707
- * Convert interrupted running work into a recoverable pending suffix or an
995
+ * Converts interrupted running work into a recoverable pending suffix or an
708
996
  * exhausted recovery failure without replenishing attempts.
709
997
  *
710
998
  * @param snapshot - A fully validated owned snapshot with no terminal overrides
@@ -781,11 +1069,58 @@ function recoverWorkflowSnapshot(snapshot) {
781
1069
  updated: now
782
1070
  };
783
1071
  }
784
- /** Compare two optional description values. */
1072
+ /**
1073
+ * Compares two optional description values.
1074
+ *
1075
+ * @remarks
1076
+ * The equality rule a lineage check needs: two descriptions match when they are the same value
1077
+ * and that value is either a string or genuine absence. Anything else — a number, an object, a
1078
+ * `null` — never matches, even against itself, so a lineage stamped with a non-string description
1079
+ * is rejected rather than silently accepted.
1080
+ *
1081
+ * @param left - The first description value
1082
+ * @param right - The second description value
1083
+ * @returns True if both are the same string or both absent; false otherwise
1084
+ *
1085
+ * @example
1086
+ * ```ts
1087
+ * matchesDescription('build', 'build') // true
1088
+ * matchesDescription(undefined, undefined) // true
1089
+ * matchesDescription('build', undefined) // false
1090
+ * ```
1091
+ */
785
1092
  function matchesDescription(left, right) {
786
1093
  return left === right && (left === void 0 || typeof left === "string");
787
1094
  }
788
- /** Test a result's lineage against its containing snapshot nodes. */
1095
+ /**
1096
+ * Tests a result's lineage against its containing snapshot nodes.
1097
+ *
1098
+ * @remarks
1099
+ * The four arguments are the result and the three snapshot nodes it claims to belong to, read
1100
+ * from the outside in: a {@link TaskResult} is self-describing, so restoring one is only safe
1101
+ * when every identity it carries agrees with the tree it was found in. It checks the exact key
1102
+ * set at each level, that `status` equals the owning task's, and that the `task` / `phase` /
1103
+ * `workflow` contexts — including the nested `task.phase.workflow` lineage — carry the same `id`,
1104
+ * `name`, and `description` as the nodes containing them. It then requires the boxed outcome to
1105
+ * match the status: a `Success` holding JSON for `completed`, a `Failure` holding a
1106
+ * {@link TaskFailure} for `failed`, and nothing for any other status. Total — a hostile
1107
+ * prototype, accessor, or cycle answers `false` rather than throwing.
1108
+ *
1109
+ * @param value - The candidate {@link TaskResult}
1110
+ * @param workflow - The workflow snapshot node containing it
1111
+ * @param phase - The phase snapshot node containing it
1112
+ * @param task - The task snapshot node the result belongs to
1113
+ * @returns True if `value` is a {@link TaskResult} whose lineage and outcome match
1114
+ * those nodes; false otherwise
1115
+ *
1116
+ * @example
1117
+ * ```ts
1118
+ * const snapshot = workflow.snapshot()
1119
+ * const phase = snapshot.phases[0]
1120
+ * const task = phase?.tasks[0]
1121
+ * isTaskResult(task?.result, snapshot, phase, task) // true for a settled task
1122
+ * ```
1123
+ */
789
1124
  function isTaskResult(value, workflow, phase, task) {
790
1125
  try {
791
1126
  if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
@@ -801,19 +1136,30 @@ function isTaskResult(value, workflow, phase, task) {
801
1136
  }
802
1137
  function hasWorkflowHandlers(workflow, functions) {
803
1138
  if ("destroyed" in workflow) {
804
- for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
1139
+ for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.behavior !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
805
1140
  return true;
806
1141
  }
807
- const runs = /* @__PURE__ */ new Set();
1142
+ const behaviors = /* @__PURE__ */ new Set();
808
1143
  for (const phase of workflow.phases) for (const task of phase.tasks) {
809
- if (task.run === void 0 || runs.has(task.run)) continue;
810
- runs.add(task.run);
811
- if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
1144
+ if (task.behavior === void 0 || behaviors.has(task.behavior)) continue;
1145
+ behaviors.add(task.behavior);
1146
+ if (!(0, _orkestrel_contract.isFunction)(functions?.[task.behavior])) return false;
812
1147
  }
813
1148
  return true;
814
1149
  }
815
- /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
816
- function workflowSnapshotContext(value) {
1150
+ /**
1151
+ * Locates the nearest identifiable node for an inconsistent owned snapshot.
1152
+ *
1153
+ * @remarks
1154
+ * The walk stops at the first phase or task whose persisted fields are inconsistent and returns
1155
+ * the identifiers it could read there, so a diagnostic can name the offending node even when part
1156
+ * of its identity is unreadable.
1157
+ *
1158
+ * @param value - The candidate snapshot, which may be any unknown value
1159
+ * @returns The nearest identifying record naming the offending `phase` and `task`, or `undefined`
1160
+ * when no inconsistent node is identifiable
1161
+ */
1162
+ function scanSnapshotContext(value) {
817
1163
  if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
818
1164
  for (const phase of value.phases) {
819
1165
  if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
@@ -821,7 +1167,7 @@ function workflowSnapshotContext(value) {
821
1167
  if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
822
1168
  for (const task of phase.tasks) {
823
1169
  if (!(0, _orkestrel_contract.isRecord)(task)) continue;
824
- if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
1170
+ if (task.behavior !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.behavior) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
825
1171
  ...phaseContext ?? {},
826
1172
  ...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
827
1173
  };
@@ -829,7 +1175,7 @@ function workflowSnapshotContext(value) {
829
1175
  }
830
1176
  }
831
1177
  /**
832
- * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
1178
+ * Flattens a nested list of per-phase {@link TaskResult} lists into one positional list
833
1179
  * — the workflow tier of the result tree, built from each phase's `results()`.
834
1180
  *
835
1181
  * @remarks
@@ -844,16 +1190,15 @@ function collectResults(phases) {
844
1190
  return phases.flat();
845
1191
  }
846
1192
  /**
847
- * Insert one `[key, value]` entry at a positional index into a readonly entries array —
1193
+ * Inserts one `[key, value]` entry at a positional index into a readonly entries array —
848
1194
  * the pure splice-in step behind an insertion-ordered registry's `add`.
849
1195
  *
850
1196
  * @remarks
851
- * Shared by {@link import('./tasks/TaskManager.js').TaskManager} and
852
- * {@link import('./phases/PhaseManager.js').PhaseManager}: both convert their
853
- * insertion-ordered `Map` to `[...map.entries()]`, call this to splice the new entry
854
- * in at the target index, then rebuild the `Map` from the result (a stateful step that
855
- * stays a `#` private method — this helper does no `Map` construction). Does not
856
- * mutate `entries`; returns a new array.
1197
+ * Used by the shared {@link import('./Collection.js').Collection} store both managers hold: it
1198
+ * converts its insertion-ordered `Map` to `[...map.entries()]`, calls this to splice the new entry
1199
+ * in at the target index, then rebuilds the `Map` from the result (a stateful step that stays a
1200
+ * `#` private method this helper does no `Map` construction). Does not mutate `entries`;
1201
+ * returns a new array.
857
1202
  *
858
1203
  * @typeParam T - The entry's value type
859
1204
  * @param entries - The current positional entries, in order
@@ -873,16 +1218,17 @@ function insertEntry(entries, index, key, value) {
873
1218
  return next;
874
1219
  }
875
1220
  /**
876
- * Reposition the entry keyed `key` to a new positional index in a readonly entries
1221
+ * Repositions the entry keyed `key` to a new positional index in a readonly entries
877
1222
  * array — the pure remove-then-reinsert step behind an insertion-ordered registry's
878
1223
  * `move`.
879
1224
  *
880
1225
  * @remarks
881
1226
  * The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
882
1227
  * out, then splices it back in at `index`. An absent `key` is a no-op (returns a copy
883
- * of `entries` unchanged) — the caller (`TaskManager.move` / `PhaseManager.move`)
884
- * already gates on the target's existence before calling this, so the no-op branch is
885
- * defensive, never reached in practice. Does not mutate `entries`; returns a new array.
1228
+ * of `entries` unchanged) — the caller, the shared
1229
+ * {@link import('./Collection.js').Collection} store's `move`, already gates on the target's
1230
+ * existence before calling this, so the no-op branch is defensive, never reached in practice.
1231
+ * Does not mutate `entries`; returns a new array.
886
1232
  *
887
1233
  * @typeParam T - The entry's value type
888
1234
  * @param entries - The current positional entries, in order
@@ -904,17 +1250,7 @@ function moveEntry(entries, key, index) {
904
1250
  return next;
905
1251
  }
906
1252
  /**
907
- * Create a {@link DeferredInterface} a promise whose settlement is driven
908
- * externally, so a caller can resolve/reject it from outside the executor.
909
- *
910
- * @typeParam T - The value the deferred promise resolves
911
- * @returns A deferred `promise` plus its `resolve` / `reject`
912
- */
913
- function createDeferred() {
914
- return Promise.withResolvers();
915
- }
916
- /**
917
- * Schedule one cancellable host operation behind an owned settlement signal.
1253
+ * Schedules one cancellable host operation behind an owned settlement signal.
918
1254
  *
919
1255
  * @remarks
920
1256
  * A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
@@ -981,17 +1317,46 @@ function scheduleHost(start, signal) {
981
1317
  });
982
1318
  }
983
1319
  /**
984
- * Park until `signal` aborts a promise-parked wait (AGENTS §21), never a timer or
985
- * busy-loop, that NEVER rejects.
1320
+ * Schedules the shared host timer boundary every scheduler backend resumes from.
986
1321
  *
987
1322
  * @remarks
988
- * Resolves IMMEDIATELY when `signal` is already aborted; otherwise attaches a one-shot
1323
+ * The one `setTimeout` / `clearTimeout` boundary in the package: the cross-environment
1324
+ * {@link import('./Scheduler.js').Scheduler}, both Node primitives, and every browser backend's
1325
+ * `delay` and macrotask fallback route here, so the timer is armed and cleared in one place. It
1326
+ * composes {@link scheduleHost}, which owns listener safety, the cancellation race, the exact
1327
+ * caller reason, and once-only settlement. It does not validate `ms`: the value passes straight to
1328
+ * the host `setTimeout`, which clamps a negative value or `NaN` to about zero, so an
1329
+ * out-of-domain `ms` resumes on the next host turn rather than throwing. Pass a non-negative
1330
+ * finite `ms`.
1331
+ *
1332
+ * @param ms - The milliseconds to wait before resuming
1333
+ * @param signal - Optional caller cancellation signal
1334
+ * @returns A promise that resolves after `ms`, or rejects with the caller's exact abort reason
1335
+ *
1336
+ * @example
1337
+ * ```ts
1338
+ * const controller = new AbortController()
1339
+ * await delayHost(0, controller.signal) // a real macrotask host turn
1340
+ * ```
1341
+ */
1342
+ function delayHost(ms, signal) {
1343
+ return scheduleHost((complete) => {
1344
+ const handle = setTimeout(complete, ms);
1345
+ return () => clearTimeout(handle);
1346
+ }, signal);
1347
+ }
1348
+ /**
1349
+ * Parks until `signal` aborts — a promise-parked wait, never a timer or
1350
+ * busy-loop, that resolves on the abort event and never rejects.
1351
+ *
1352
+ * @remarks
1353
+ * Resolves immediately when `signal` is already aborted; otherwise attaches a one-shot
989
1354
  * `abort` listener and resolves when it fires, removing the listener either way. The
990
1355
  * shared leaf behind the duplicate abort-wiring an execution engine otherwise hand-rolls
991
1356
  * at every fold point.
992
1357
  *
993
1358
  * @param signal - The signal to park on
994
- * @returns A promise that resolves once `signal` has aborted
1359
+ * @returns A promise that resolves after `signal` has aborted
995
1360
  *
996
1361
  * @example
997
1362
  * ```ts
@@ -1008,25 +1373,125 @@ function parkSignal(signal) {
1008
1373
  });
1009
1374
  }
1010
1375
  //#endregion
1376
+ //#region src/core/Collection.ts
1377
+ /**
1378
+ * Implements the insertion-ordered gated store both lean managers hold — entities keyed by `id`,
1379
+ * positional order preserved across an interior `skip` or `remove`.
1380
+ *
1381
+ * @remarks
1382
+ * - **One engine, two managers.** {@link import('./tasks/TaskManager.js').TaskManager} and
1383
+ * {@link import('./phases/PhaseManager.js').PhaseManager} differ only in the entity noun and the
1384
+ * patch shape they validate, so both hold one of these and add only their domain accessors
1385
+ * (`task` / `tasks`, `phase` / `phases`). The `Map`'s insertion order is the single source of
1386
+ * positional truth; `add` and `move` rebuild it through the pure
1387
+ * {@link import('./helpers.js').insertEntry} / {@link import('./helpers.js').moveEntry} leaves.
1388
+ * - **Gated mutation API.** `append` is the build-time wiring path and throws on a
1389
+ * duplicate id; `add` / `remove` / `move` / `update` return a graceful `MUTATION`
1390
+ * {@link WorkflowError} failure instead. Gating reads only the target's own existence, `pending`
1391
+ * status, id, and bounds — a container's own status is the owning entity's gate, applied before
1392
+ * it delegates here.
1393
+ * - **Event-free.** A purely structural container; the entity that owns it emits on success.
1394
+ *
1395
+ * @typeParam TEntry - The stored entity
1396
+ * @typeParam TPatch - The declarative partial update `update` validates and applies
1397
+ *
1398
+ * @example
1399
+ * ```ts
1400
+ * import { compileGuard } from '@orkestrel/contract'
1401
+ * import { Collection, taskUpdateShape } from '@orkestrel/workflow'
1402
+ * import type { TaskInterface, TaskUpdate } from '@orkestrel/workflow'
1403
+ *
1404
+ * const tasks = new Collection<TaskInterface, TaskUpdate>('task', compileGuard(taskUpdateShape))
1405
+ * tasks.append(task) // a live Task
1406
+ * tasks.entry(task.id) // the same task
1407
+ * tasks.entries() // [task]
1408
+ * tasks.count // 1
1409
+ * tasks.add(other, 0) // Result — inserted first
1410
+ * tasks.move(other.id, 1) // Result — repositioned
1411
+ * tasks.update(task.id, { name: 'Renamed task' }) // Result — patched
1412
+ * tasks.remove(other.id) // Result — dropped
1413
+ * ```
1414
+ */
1415
+ var Collection = class {
1416
+ #entries = /* @__PURE__ */ new Map();
1417
+ #noun;
1418
+ #isPatch;
1419
+ constructor(noun, patch) {
1420
+ this.#noun = noun;
1421
+ this.#isPatch = patch;
1422
+ }
1423
+ get count() {
1424
+ return this.#entries.size;
1425
+ }
1426
+ append(entry) {
1427
+ if (this.#entries.has(entry.id)) throw new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id });
1428
+ this.#entries.set(entry.id, entry);
1429
+ }
1430
+ add(entry, index) {
1431
+ if (this.#entries.has(entry.id)) return failure(new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id }));
1432
+ const at = index ?? this.#entries.size;
1433
+ if (at < 0 || at > this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1434
+ this.#reorder(insertEntry([...this.#entries.entries()], at, entry.id, entry));
1435
+ return success(entry);
1436
+ }
1437
+ remove(id) {
1438
+ const target = this.#pending(id);
1439
+ if (target === void 0) return this.#refuse(id);
1440
+ this.#entries.delete(id);
1441
+ return success(target);
1442
+ }
1443
+ move(id, index) {
1444
+ const target = this.#pending(id);
1445
+ if (target === void 0) return this.#refuse(id);
1446
+ if (index < 0 || index >= this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1447
+ this.#reorder(moveEntry([...this.#entries.entries()], id, index));
1448
+ return success(target);
1449
+ }
1450
+ update(id, patch) {
1451
+ const target = this.#pending(id);
1452
+ if (target === void 0) return this.#refuse(id);
1453
+ if (!this.#isPatch(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for ${this.#noun} '${id}'`, { id }));
1454
+ target.patch(patch);
1455
+ return success(target);
1456
+ }
1457
+ entry(id) {
1458
+ return this.#entries.get(id);
1459
+ }
1460
+ entries() {
1461
+ return [...this.#entries.values()];
1462
+ }
1463
+ #pending(id) {
1464
+ const target = this.#entries.get(id);
1465
+ return target === void 0 || target.status !== "pending" ? void 0 : target;
1466
+ }
1467
+ #refuse(id) {
1468
+ return failure(new WorkflowError("MUTATION", `${this.#noun} '${id}' is not a pending ${this.#noun}`, { id }));
1469
+ }
1470
+ #reorder(entries) {
1471
+ this.#entries.clear();
1472
+ for (const [key, value] of entries) this.#entries.set(key, value);
1473
+ }
1474
+ };
1475
+ //#endregion
1011
1476
  //#region src/core/Scheduler.ts
1012
1477
  /**
1013
- * The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
1478
+ * Implements the safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
1014
1479
  * built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
1015
1480
  * browser and Node.
1016
1481
  *
1017
1482
  * @remarks
1018
- * - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
1483
+ * - **Cross-environment.** Uses only `setTimeout` / `clearTimeout` — universally
1019
1484
  * available. It deliberately avoids env-specific fast paths (`setImmediate`,
1020
1485
  * `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
1021
1486
  * `MessageChannel`); those belong to the environment backends, built with the
1022
1487
  * agent loop that consumes them.
1023
1488
  * - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
1024
- * `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
1489
+ * `setTimeout(0)`, not `queueMicrotask`. A microtask drains before the host
1025
1490
  * regains control, so it would not actually let pending I/O, timers, or
1026
1491
  * rendering run — it only defers within the current task. A zero-delay timer is
1027
1492
  * the correct cross-environment "give the host a turn".
1028
1493
  * - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
1029
- * {@link scheduleHost} links an owned settlement composite to the caller before arming
1494
+ * {@link delayHost} links an owned settlement composite to the caller before arming
1030
1495
  * the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
1031
1496
  * cancellation clears the handle, and native first-settlement wins exactly once.
1032
1497
  * - **Priority is accepted but uniform.** `options.priority` is part of the
@@ -1045,36 +1510,30 @@ function parkSignal(signal) {
1045
1510
  */
1046
1511
  var Scheduler = class {
1047
1512
  /**
1048
- * Yield control back to the host so other tasks (I/O, timers, rendering) can
1049
- * run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
1513
+ * Yields control back to the host so other tasks (I/O, timers, rendering) can
1514
+ * run, then resumes — a macrotask turn through `setTimeout(0)` (not a microtask,
1050
1515
  * which would resume before the host regains control).
1051
1516
  */
1052
1517
  yield(options) {
1053
- return this.#sleep(0, options?.signal);
1518
+ return delayHost(0, options?.signal);
1054
1519
  }
1055
1520
  /**
1056
- * Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
1521
+ * Resumes after at least `ms` milliseconds; abort rejects with `signal.reason`.
1057
1522
  *
1058
1523
  * @remarks
1059
- * `ms` should be a non-negative finite number. The primitive stays minimal and
1060
- * does no validation: it passes `ms` straight to the host `setTimeout`, which
1061
- * clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
1062
- * the next host turn rather than throwing.
1524
+ * Pass a non-negative finite `ms`. The primitive stays minimal and does no
1525
+ * validation: it passes `ms` straight to the host `setTimeout`, which clamps a
1526
+ * negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on the next
1527
+ * host turn rather than throwing.
1063
1528
  */
1064
1529
  delay(ms, options) {
1065
- return this.#sleep(ms, options?.signal);
1066
- }
1067
- #sleep(ms, signal) {
1068
- return scheduleHost((complete) => {
1069
- const handle = setTimeout(complete, ms);
1070
- return () => clearTimeout(handle);
1071
- }, signal);
1530
+ return delayHost(ms, options?.signal);
1072
1531
  }
1073
1532
  };
1074
1533
  //#endregion
1075
1534
  //#region src/core/cloners.ts
1076
1535
  /**
1077
- * Validate and own a workflow snapshot before live construction.
1536
+ * Validates and owns a workflow snapshot before live construction.
1078
1537
  *
1079
1538
  * @param input - The hostile snapshot boundary
1080
1539
  * @param id - The optional storage key the owned snapshot must match
@@ -1090,7 +1549,7 @@ function cloneWorkflowSnapshot(input, id) {
1090
1549
  if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
1091
1550
  throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
1092
1551
  }
1093
- if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
1552
+ if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", scanSnapshotContext(cloned));
1094
1553
  if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
1095
1554
  requested: id,
1096
1555
  payload: cloned.id
@@ -1098,7 +1557,49 @@ function cloneWorkflowSnapshot(input, id) {
1098
1557
  return cloned;
1099
1558
  }
1100
1559
  /**
1101
- * Validate and clone one complete task activity frame.
1560
+ * Validates and owns one list of task activity claims.
1561
+ *
1562
+ * @remarks
1563
+ * The one cloner behind both claim lists of a task activity frame — its `operations` and its
1564
+ * `constraints` — because {@link import('./types.js').TaskOperation} and
1565
+ * {@link import('./types.js').TaskConstraint} are the same {@link import('./types.js').TaskClaim}
1566
+ * shape. An omitted
1567
+ * list is an empty one. Each member is read exactly once inside the caller's protected boundary
1568
+ * and returned frozen; the semantic pass over the copied values is
1569
+ * {@link import('./validators.js').isTaskClaimList}, so this cloner refuses only what it cannot
1570
+ * read: a non-array list, a non-record member, a hostile prototype, or an unexpected key.
1571
+ *
1572
+ * @param input - The untrusted claim list
1573
+ * @param noun - The singular claim noun the refusal message names, pluralized by adding `s`
1574
+ * @returns The owned frozen claims, in input order
1575
+ * @throws {WorkflowError} With `MUTATION` when the list or one of its members cannot be read
1576
+ *
1577
+ * @example
1578
+ * ```ts
1579
+ * cloneTaskClaims([{ id: 'fetch', name: 'Fetch', started: 1 }], 'operation')
1580
+ * ```
1581
+ */
1582
+ function cloneTaskClaims(input, noun) {
1583
+ const inputs = input === void 0 ? [] : (0, _orkestrel_contract.isArray)(input) ? [...input] : void 0;
1584
+ if (inputs === void 0) throw new WorkflowError("MUTATION", `task activity ${noun}s must be an array`);
1585
+ const claims = [];
1586
+ for (const claim of inputs) {
1587
+ if (!(0, _orkestrel_contract.isRecord)(claim)) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
1588
+ const prototype = Object.getPrototypeOf(claim);
1589
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
1590
+ const id = claim.id;
1591
+ const name = claim.name;
1592
+ const started = claim.started;
1593
+ claims.push(Object.freeze({
1594
+ id,
1595
+ name,
1596
+ started
1597
+ }));
1598
+ }
1599
+ return claims;
1600
+ }
1601
+ /**
1602
+ * Validates and clones one complete task activity frame.
1102
1603
  *
1103
1604
  * @remarks
1104
1605
  * This is the hostile boundary behind task reports and snapshot hydration. Supplying
@@ -1122,22 +1623,7 @@ function cloneTaskActivity(input, updated) {
1122
1623
  const operationsInput = input.operations;
1123
1624
  const constraintsInput = input.constraints;
1124
1625
  const accepted = updated === void 0 ? input.updated : updated;
1125
- const operationInputs = operationsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(operationsInput) ? [...operationsInput] : void 0;
1126
- if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
1127
- const operations = [];
1128
- for (const operation of operationInputs) {
1129
- if (!(0, _orkestrel_contract.isRecord)(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
1130
- const operationPrototype = Object.getPrototypeOf(operation);
1131
- if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
1132
- const id = operation.id;
1133
- const name = operation.name;
1134
- const started = operation.started;
1135
- operations.push(Object.freeze({
1136
- id,
1137
- name,
1138
- started
1139
- }));
1140
- }
1626
+ const operations = cloneTaskClaims(operationsInput, "operation");
1141
1627
  let progress;
1142
1628
  if (progressInput !== void 0) {
1143
1629
  if (!(0, _orkestrel_contract.isRecord)(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
@@ -1152,22 +1638,7 @@ function cloneTaskActivity(input, updated) {
1152
1638
  ...message === void 0 ? {} : { message }
1153
1639
  });
1154
1640
  }
1155
- const constraintInputs = constraintsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(constraintsInput) ? [...constraintsInput] : void 0;
1156
- if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
1157
- const constraints = [];
1158
- for (const constraint of constraintInputs) {
1159
- if (!(0, _orkestrel_contract.isRecord)(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
1160
- const constraintPrototype = Object.getPrototypeOf(constraint);
1161
- if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
1162
- const id = constraint.id;
1163
- const name = constraint.name;
1164
- const started = constraint.started;
1165
- constraints.push(Object.freeze({
1166
- id,
1167
- name,
1168
- started
1169
- }));
1170
- }
1641
+ const constraints = cloneTaskClaims(constraintsInput, "constraint");
1171
1642
  const activity = Object.freeze({
1172
1643
  ...note === void 0 ? {} : { note },
1173
1644
  ...progress === void 0 ? {} : { progress },
@@ -1185,8 +1656,8 @@ function cloneTaskActivity(input, updated) {
1185
1656
  //#endregion
1186
1657
  //#region src/core/shapers.ts
1187
1658
  /**
1188
- * The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
1189
- * `run` behavior reference (a plain registry-key string, min length 1). `description` is
1659
+ * Describes the shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
1660
+ * `behavior` behavior reference (a plain registry-key string, min length 1). `description` is
1190
1661
  * optional prose.
1191
1662
  */
1192
1663
  var taskShape = (0, _orkestrel_contract.objectShape)({
@@ -1199,7 +1670,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
1199
1670
  description: "Human-readable task name."
1200
1671
  }),
1201
1672
  description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional task description." })),
1202
- run: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
1673
+ behavior: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
1203
1674
  min: 1,
1204
1675
  description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
1205
1676
  })),
@@ -1214,7 +1685,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
1214
1685
  }))
1215
1686
  });
1216
1687
  /**
1217
- * The shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
1688
+ * Describes the shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
1218
1689
  * {@link taskShape} tasks, and an optional positive-integer `concurrency` throttle
1219
1690
  * (max tasks in flight; omitted ⇒ unbounded).
1220
1691
  */
@@ -1236,7 +1707,7 @@ var phaseShape = (0, _orkestrel_contract.objectShape)({
1236
1707
  bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
1237
1708
  });
1238
1709
  /**
1239
- * The shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
1710
+ * Describes the shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
1240
1711
  * identity, its ordered {@link phaseShape} phases, and the optional `bail` boolean
1241
1712
  * failure policy (the literal pair `true`/`false`, the runtime mirror of the boolean
1242
1713
  * toggle; omitted ⇒ the graceful default).
@@ -1255,13 +1726,13 @@ var workflowShape = (0, _orkestrel_contract.objectShape)({
1255
1726
  bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
1256
1727
  });
1257
1728
  /**
1258
- * The shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
1729
+ * Describes the shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
1259
1730
  * `pending` task's `name` / `description`, both optional.
1260
1731
  *
1261
1732
  * @remarks
1262
1733
  * Mirrors {@link taskShape}'s `name` / `description` constraints exactly (a provided
1263
- * `name` still has `minLength: 1`); never `id` / `run` / `retries` / `timeout` (those
1264
- * are not patchable fields, AGENTS §12).
1734
+ * `name` still has `minLength: 1`); never `id` / `behavior` / `retries` / `timeout` (those
1735
+ * are not patchable fields).
1265
1736
  */
1266
1737
  var taskUpdateShape = (0, _orkestrel_contract.objectShape)({
1267
1738
  name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
@@ -1271,13 +1742,13 @@ var taskUpdateShape = (0, _orkestrel_contract.objectShape)({
1271
1742
  description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "New task description." }))
1272
1743
  });
1273
1744
  /**
1274
- * The shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
1745
+ * Describes the shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
1275
1746
  * `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
1276
1747
  *
1277
1748
  * @remarks
1278
1749
  * Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
1279
1750
  * `tasks` (structural children change through the phase's own `add` / `remove` /
1280
- * `move`, not a patch, AGENTS §12).
1751
+ * `move`, not a patch).
1281
1752
  */
1282
1753
  var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1283
1754
  name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
@@ -1294,40 +1765,42 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1294
1765
  //#endregion
1295
1766
  //#region src/core/stores/DatabaseWorkflowStore.ts
1296
1767
  /**
1297
- * A {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
1298
- * workflow's durable run-state IS a row, so persistence reduces to keyed point-access
1768
+ * Implements a {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
1769
+ * workflow's durable run state is a row, so persistence reduces to keyed point-access
1299
1770
  * (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
1300
1771
  * plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
1301
1772
  *
1302
1773
  * @remarks
1303
1774
  * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend
1304
1775
  * (memory, JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a
1305
- * JSON / SQLite / IndexedDB backend swaps in WITHOUT touching the runner or the entity tree
1776
+ * JSON / SQLite / IndexedDB backend swaps in without touching the runner or the entity tree
1306
1777
  * — the same seam as `@orkestrel/queue`'s `DatabaseQueueStore`.
1307
1778
  * The driver defaults to memory ({@link import('../factories.js').createDatabaseWorkflowStore}
1308
- * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
1779
+ * passes `createMemoryDriver()`), so it also works in memory out of the box; you opt into the
1309
1780
  * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
1310
1781
  *
1311
- * The {@link WorkflowSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
1782
+ * The {@link WorkflowSnapshot} is stored as one opaque JSON column — the table is a row of
1312
1783
  * `{ id; snapshot }` ({@link WorkflowSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
1313
1784
  * column the factory builds) — exactly as `DatabaseQueueStore` stores its `input`. The snapshot is
1314
- * already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
1785
+ * already a complete, self-contained, pure-JSON payload, so storing it whole is lossless and
1315
1786
  * sidesteps a TS2589 instantiation-depth blow-up: a structured multi-column table would force the
1316
1787
  * contract to `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results),
1317
1788
  * tripping the compiler — one JSON column keeps the row type flat (`snapshot` reads back as `unknown`).
1318
1789
  *
1319
- * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
1790
+ * - **`set(snapshot)` upserts under the snapshot's own `id`** (no separate id param) — it writes
1320
1791
  * the row `{ id: snapshot.id, snapshot }`.
1321
- * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
1322
- * a {@link WorkflowSnapshot} ({@link import('../helpers.js').isWorkflowSnapshot} — the AGENTS §14
1323
- * boundary narrow for an untrusted storage read), or `undefined` if none is stored. A present
1324
- * snapshot whose own id differs from the requested key rejects with normalized `RESTORE` evidence.
1792
+ * - **`get(id)` resolves the stored snapshot for an id**, owning and narrowing the opaque JSON
1793
+ * column back to a {@link WorkflowSnapshot} through
1794
+ * {@link import('../cloners.js').cloneWorkflowSnapshot}, whose semantic pass is
1795
+ * {@link import('../validators.js').isOwnedWorkflowSnapshot} the boundary narrow for
1796
+ * an untrusted storage read — or `undefined` if none is stored. A present snapshot whose own id
1797
+ * differs from the requested key rejects with normalized `RESTORE` evidence.
1325
1798
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1326
1799
  *
1327
- * UNLIKE the server package's `SessionStoreInterface` there is NO
1800
+ * unlike the server package's `SessionStoreInterface` there is no
1328
1801
  * idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
1329
- * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
1330
- * §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1802
+ * explicit `delete`. The public surface is exactly `get` / `set` / `delete` — no extra members (the
1803
+ * guide's method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1331
1804
  * snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1332
1805
  *
1333
1806
  * @example
@@ -1346,7 +1819,7 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1346
1819
  var DatabaseWorkflowStore = class {
1347
1820
  #table;
1348
1821
  /**
1349
- * Wrap a table as a workflow store.
1822
+ * Wraps a table as a workflow store.
1350
1823
  *
1351
1824
  * @param table - The {@link TableInterface} holding the snapshots — its row is the
1352
1825
  * {@link WorkflowSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
@@ -1354,13 +1827,13 @@ var DatabaseWorkflowStore = class {
1354
1827
  constructor(table) {
1355
1828
  this.#table = table;
1356
1829
  }
1357
- /** Resolve and key-check the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
1830
+ /** Resolves and key-checks the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
1358
1831
  async get(id) {
1359
1832
  const row = await this.#table.get(id);
1360
1833
  if (row === void 0) return void 0;
1361
1834
  return cloneWorkflowSnapshot(row.snapshot, id);
1362
1835
  }
1363
- /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1836
+ /** Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is `{ id, snapshot }`. */
1364
1837
  async set(snapshot) {
1365
1838
  const owned = cloneWorkflowSnapshot(snapshot);
1366
1839
  await this.#table.set({
@@ -1368,7 +1841,7 @@ var DatabaseWorkflowStore = class {
1368
1841
  snapshot: owned
1369
1842
  });
1370
1843
  }
1371
- /** Drop a snapshot by id; an absent id is a no-op (no throw). */
1844
+ /** Drops a snapshot by id; an absent id is a no-op (no throw). */
1372
1845
  async delete(id) {
1373
1846
  await this.#table.remove(id);
1374
1847
  }
@@ -1376,28 +1849,25 @@ var DatabaseWorkflowStore = class {
1376
1849
  //#endregion
1377
1850
  //#region src/core/stores/MemoryWorkflowStore.ts
1378
1851
  /**
1379
- * The in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
1380
- * {@link WorkflowSnapshot}s keyed by workflow id, the DEFAULT store
1381
- * {@link import('../factories.js').createMemoryWorkflowStore} builds.
1852
+ * Implements the in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
1853
+ * {@link WorkflowSnapshot}s keyed by workflow id, the default store
1854
+ * {@link import('../factories.js').createMemoryWorkflowStore} builds. It expires nothing: a
1855
+ * persisted snapshot lives until an explicit `delete`.
1382
1856
  *
1383
1857
  * @remarks
1384
- * A plain `Map<string, WorkflowSnapshot>` (AGENTS §21 — the snapshot is already pure,
1385
- * self-contained JSON, so no encoding is needed for the memory tier). UNLIKE the server
1386
- * package's `SessionStoreInterface`'s memory store there is
1387
- * NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
1388
- * that lives until an explicit `delete`, never silently aging out (a run that vanished
1389
- * mid-flight would be a silent data loss, not a freed session). A durable backend (JSON /
1390
- * SQLite / IndexedDB) swaps in through the SAME interface without touching the runner or the
1858
+ * A plain `Map<string, WorkflowSnapshot>` (the snapshot is already pure,
1859
+ * self-contained JSON, so no encoding is needed for the memory tier). A durable backend (JSON /
1860
+ * SQLite / IndexedDB) swaps in through the same interface without touching the runner or the
1391
1861
  * entity tree — its driver-pluggable twin is
1392
1862
  * {@link import('./DatabaseWorkflowStore.js').DatabaseWorkflowStore} (the snapshot as one opaque
1393
1863
  * JSON column), exactly as `@orkestrel/queue`'s `MemoryQueueStore`
1394
1864
  * twins `DatabaseQueueStore`.
1395
1865
  *
1396
1866
  * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
1397
- * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
1867
+ * - **`set` inserts / replaces under the snapshot's own `id`** (no separate id param).
1398
1868
  * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
1399
1869
  *
1400
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1870
+ * The public surface is exactly `get` / `set` / `delete` — no extra members (the guide's method
1401
1871
  * bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
1402
1872
  * back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1403
1873
  *
@@ -1432,35 +1902,35 @@ var MemoryWorkflowStore = class {
1432
1902
  //#endregion
1433
1903
  //#region src/core/tasks/Task.ts
1434
1904
  /**
1435
- * The live leaf state machine (W-b) for one task — an observable (AGENTS §13), guarded
1436
- * synchronous task whose explicit {@link TaskStatus} advances through the AGENTS §10
1905
+ * Implements the live leaf state machine (W-b) for one task — an observable, guarded
1906
+ * synchronous task whose explicit {@link LifecycleStatus} advances through the declared
1437
1907
  * transitions, recording a {@link TaskResult} on a terminal outcome.
1438
1908
  *
1439
1909
  * @remarks
1440
- * - **Guarded transitions (AGENTS §10).** `start` (→ `running`), then `complete(value)`
1910
+ * - **Guarded transitions.** `start` (→ `running`), then `complete(value)`
1441
1911
  * (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
1442
1912
  * (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
1443
- * `stop` (→ `stopped`). Each consults {@link canTransitionTask} FIRST and throws a
1444
- * `TRANSITION` {@link WorkflowError} on an illegal move (e.g. completing a non-`running`
1445
- * task) — the legal graph is the single source of truth, so the leaf can never reach an
1446
- * impossible state.
1913
+ * `stop` (→ `stopped`). Each consults {@link canTransitionTask} first and throws a
1914
+ * `TRANSITION` {@link WorkflowError} on an illegal move (for example, completing a
1915
+ * non-`running` task) — the legal graph is the single source of truth, so the leaf can never
1916
+ * reach an impossible state.
1447
1917
  * - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
1448
1918
  * statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
1449
1919
  * - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
1450
- * OWN event, THEN calls the parent phase's `#recompute` (injected at construction) so the
1451
- * transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
1452
- * order means an observer sees the CAUSE (this leaf changed) before the EFFECT (the parents
1920
+ * own event, then calls the parent phase's `#recompute` (injected at construction) so the
1921
+ * transition propagates up (Task → Phase → Workflow re-derive). The own-event-before-cascade
1922
+ * order means an observer sees the cause (this leaf changed) before the effect (the parents
1453
1923
  * re-derive) — the project precedent (`Runner.#settle` emits its own `fail` before propagating).
1454
- * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link TaskEventMap}) fires the
1455
- * matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
1924
+ * - **Observable.** The owned {@link emitter} ({@link TaskEventMap}) fires the
1925
+ * matching event strictly after the state change, before the cascade; the emitter isolates
1456
1926
  * a listener throw and routes it to its `error` handler (the `error` option), so a buggy
1457
1927
  * observer can never corrupt a transition.
1458
- * - **Declarative config (AGENTS §12).** `run` / `retries` / `timeout` PERSIST in a
1928
+ * - **Declarative config.** `behavior` / `retries` / `timeout` persist in a
1459
1929
  * {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
1460
1930
  * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
1461
- * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
1931
+ * is the runtime-only counterpart — `behavior` resolved once at construction against the
1462
1932
  * workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
1463
- * NEVER persisted; `undefined` when `run` is omitted or unregistered. Only omission is a
1933
+ * never persisted; `undefined` when `behavior` is omitted or unregistered. Only omission is a
1464
1934
  * deliberate no-op; unresolved named work is rejected before dispatch.
1465
1935
  */
1466
1936
  var Task = class {
@@ -1473,7 +1943,8 @@ var Task = class {
1473
1943
  #status;
1474
1944
  #result;
1475
1945
  #name;
1476
- #run;
1946
+ #description;
1947
+ #behavior;
1477
1948
  #retries;
1478
1949
  #timeout;
1479
1950
  #attempts;
@@ -1486,7 +1957,7 @@ var Task = class {
1486
1957
  #paused;
1487
1958
  #gate;
1488
1959
  #timerSignal;
1489
- constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1960
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, behavior, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1490
1961
  this.#context = buildTaskContext(context.phase, context);
1491
1962
  this.#phase = phase;
1492
1963
  this.#workflow = workflow;
@@ -1508,11 +1979,8 @@ var Task = class {
1508
1979
  this.#status = status;
1509
1980
  this.#result = result;
1510
1981
  this.#name = context.name;
1511
- if (context.description !== void 0) Object.defineProperty(this, "description", {
1512
- configurable: true,
1513
- value: context.description
1514
- });
1515
- this.#run = run;
1982
+ this.#description = context.description;
1983
+ this.#behavior = behavior;
1516
1984
  this.#retries = retries;
1517
1985
  this.#timeout = timeout;
1518
1986
  this.#attempts = attempts;
@@ -1538,6 +2006,9 @@ var Task = class {
1538
2006
  get name() {
1539
2007
  return this.#name;
1540
2008
  }
2009
+ get description() {
2010
+ return this.#description;
2011
+ }
1541
2012
  get context() {
1542
2013
  return this.#context;
1543
2014
  }
@@ -1556,8 +2027,8 @@ var Task = class {
1556
2027
  get attempts() {
1557
2028
  return this.#attempts;
1558
2029
  }
1559
- get run() {
1560
- return this.#run;
2030
+ get behavior() {
2031
+ return this.#behavior;
1561
2032
  }
1562
2033
  get handler() {
1563
2034
  return this.#handler;
@@ -1596,7 +2067,7 @@ var Task = class {
1596
2067
  this.#activity = cloneTaskActivity({}, this.#stamp());
1597
2068
  this.#arm();
1598
2069
  this.#emitter.emit("start", this.id);
1599
- this.#escalate();
2070
+ this.#recompute();
1600
2071
  }
1601
2072
  complete(value) {
1602
2073
  let owned;
@@ -1613,7 +2084,7 @@ var Task = class {
1613
2084
  value: owned
1614
2085
  }));
1615
2086
  this.#emitter.emit("complete", result);
1616
- this.#escalate();
2087
+ this.#recompute();
1617
2088
  }
1618
2089
  fail(error) {
1619
2090
  const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
@@ -1628,21 +2099,21 @@ var Task = class {
1628
2099
  })
1629
2100
  }));
1630
2101
  this.#emitter.emit("fail", result);
1631
- this.#escalate();
2102
+ this.#recompute();
1632
2103
  }
1633
2104
  skip() {
1634
2105
  this.#transition("skipped");
1635
2106
  this.#finish();
1636
2107
  this.#abort.abort();
1637
2108
  this.#emitter.emit("skip");
1638
- this.#escalate();
2109
+ this.#recompute();
1639
2110
  }
1640
2111
  stop() {
1641
2112
  this.#transition("stopped");
1642
2113
  this.#finish();
1643
2114
  this.#abort.abort();
1644
2115
  this.#emitter.emit("stop");
1645
- this.#escalate();
2116
+ this.#recompute();
1646
2117
  }
1647
2118
  report(input) {
1648
2119
  if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
@@ -1670,7 +2141,7 @@ var Task = class {
1670
2141
  pause() {
1671
2142
  if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
1672
2143
  this.#paused = true;
1673
- this.#gate = createDeferred();
2144
+ this.#gate = Promise.withResolvers();
1674
2145
  this.#emitter.emit("pause");
1675
2146
  }
1676
2147
  resume() {
@@ -1683,12 +2154,12 @@ var Task = class {
1683
2154
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1684
2155
  }
1685
2156
  /**
1686
- * Apply a validated declarative patch to SELF (`name` / `description`).
2157
+ * Applies a validated declarative patch to self (`name` / `description`).
1687
2158
  *
1688
2159
  * @remarks
1689
- * Defense-in-depth (AGENTS §12): the owning
1690
- * {@link import('../types.js').TaskManagerInterface.update} gates FIRST (target
1691
- * exists + `pending`), so this is the second, redundant check — it THROWS a
2160
+ * Defense-in-depth: the owning
2161
+ * {@link import('../types.js').TaskManagerInterface.update} gates first (target
2162
+ * exists + `pending`), so this is the second, redundant check — it throws a
1692
2163
  * `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
1693
2164
  *
1694
2165
  * @param value - The {@link TaskUpdate} fields to apply
@@ -1703,21 +2174,18 @@ var Task = class {
1703
2174
  status: this.#status
1704
2175
  });
1705
2176
  if (value.name !== void 0) this.#name = value.name;
1706
- if (value.description !== void 0) Object.defineProperty(this, "description", {
1707
- configurable: true,
1708
- value: value.description
1709
- });
2177
+ if (value.description !== void 0) this.#description = value.description;
1710
2178
  }
1711
2179
  snapshot() {
1712
2180
  return {
1713
2181
  id: this.id,
1714
2182
  name: this.name,
1715
- ...this.description === void 0 ? {} : { description: this.description },
2183
+ ...this.#description === void 0 ? {} : { description: this.#description },
1716
2184
  status: this.#status,
1717
2185
  ...this.#result === void 0 ? {} : { result: this.#result },
1718
2186
  metadata: this.#metadata,
1719
2187
  attempts: this.#attempts,
1720
- ...this.#run === void 0 ? {} : { run: this.#run },
2188
+ ...this.#behavior === void 0 ? {} : { behavior: this.#behavior },
1721
2189
  ...this.#retries === void 0 ? {} : { retries: this.#retries },
1722
2190
  ...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
1723
2191
  ...this.#activity === void 0 ? {} : { activity: this.#activity }
@@ -1744,9 +2212,6 @@ var Task = class {
1744
2212
  this.#result = frozen;
1745
2213
  return frozen;
1746
2214
  }
1747
- #escalate() {
1748
- this.#recompute();
1749
- }
1750
2215
  #touch() {
1751
2216
  if (this.#activity === void 0) return;
1752
2217
  this.#activity = Object.freeze({
@@ -1791,25 +2256,30 @@ var Task = class {
1791
2256
  //#endregion
1792
2257
  //#region src/core/tasks/TaskManager.ts
1793
2258
  /**
1794
- * The lean child manager (AGENTS §9) of a {@link import('../phases/Phase.js').Phase}'s live
1795
- * tasks — an insertion-ordered registry keyed by task `id`, so positional order is
1796
- * preserved across an interior `skip` / `remove`.
2259
+ * Implements the lean child manager of a {@link import('../phases/Phase.js').Phase}'s live
2260
+ * tasks — the task vocabulary over one insertion-ordered {@link Collection}, so positional order
2261
+ * is preserved across an interior `skip` / `remove`.
1797
2262
  *
1798
2263
  * @remarks
1799
- * - **Positional store.** Tasks live in an insertion-ordered `Map` keyed by `id`;
1800
- * `append` adds one at the end (the build-time wiring path), `task(id)` looks one up,
1801
- * `tasks()` lists them in positional order, `count` is the size. A `skip` is a STATUS
1802
- * change on a stored task (never a removal), so order survives it; a snapshot RESTORE
1803
- * re-`append`s in the snapshot's order, reproducing it exactly.
1804
- * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
1805
- * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
1806
- * existence/status/id/bounds a duplicate id, an absent/non-`pending` target, an
1807
- * out-of-bounds `index`, or a patch that fails {@link taskUpdateShape} validation all
1808
- * fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
1809
- * - **No batch matrix.** A phase's tasks are a fixed positional set, so AGENTS §9.2 (the
1810
- * bulk verb overloads) is deliberately omitted there is no `remove` family here.
1811
- * - **Event-free.** A purely structural container the live {@link TaskInterface}s own
1812
- * their own emitters; the manager observes nothing.
2264
+ * - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
2265
+ * the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
2266
+ * `task` noun its refusals name and the compiled {@link taskUpdateShape} guard. This class adds
2267
+ * the domain accessors `task` / `tasks` and nothing else, so the task and phase managers cannot
2268
+ * drift apart.
2269
+ * - **Positional store.** `append` adds one live {@link TaskInterface} at the end (the build-time
2270
+ * wiring path), `task(id)` looks one up, `tasks()` lists them in positional order, `count` is
2271
+ * the tally. A `skip` is a status change on a stored task (never a removal), so order survives
2272
+ * it; a snapshot RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
2273
+ * - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
2274
+ * `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
2275
+ * a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
2276
+ * fails {@link taskUpdateShape} validation all fail gracefully with a `MUTATION`
2277
+ * {@link WorkflowError} instead of throwing.
2278
+ * - **No batch matrix.** A phase's tasks are a fixed positional set, so
2279
+ * `.claude/rules/patterns.md` § Batch operations (the bulk verb
2280
+ * overloads) is deliberately omitted — no `remove` family lives here.
2281
+ * - **Event-free.** A purely structural container — the live {@link TaskInterface}s own their own
2282
+ * emitters; the manager observes nothing.
1813
2283
  *
1814
2284
  * @example
1815
2285
  * ```ts
@@ -1820,105 +2290,84 @@ var Task = class {
1820
2290
  * ```
1821
2291
  */
1822
2292
  var TaskManager = class {
1823
- #tasks = /* @__PURE__ */ new Map();
1824
- #isUpdate = (0, _orkestrel_contract.compileGuard)(taskUpdateShape);
2293
+ #tasks = new Collection("task", (0, _orkestrel_contract.compileGuard)(taskUpdateShape));
1825
2294
  get count() {
1826
- return this.#tasks.size;
2295
+ return this.#tasks.count;
1827
2296
  }
1828
2297
  append(task) {
1829
- if (this.#tasks.has(task.id)) throw new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id });
1830
- this.#tasks.set(task.id, task);
2298
+ this.#tasks.append(task);
1831
2299
  }
1832
2300
  add(task, index) {
1833
- if (this.#tasks.has(task.id)) return failure(new WorkflowError("MUTATION", `duplicate task id '${task.id}'`, { id: task.id }));
1834
- const at = index ?? this.#tasks.size;
1835
- if (at < 0 || at > this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1836
- this.#reorder(insertEntry([...this.#tasks.entries()], at, task.id, task));
1837
- return success(task);
2301
+ return this.#tasks.add(task, index);
1838
2302
  }
1839
2303
  remove(id) {
1840
- const target = this.#tasks.get(id);
1841
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1842
- this.#tasks.delete(id);
1843
- return success(target);
2304
+ return this.#tasks.remove(id);
1844
2305
  }
1845
2306
  move(id, index) {
1846
- const target = this.#tasks.get(id);
1847
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1848
- if (index < 0 || index >= this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1849
- this.#reorder(moveEntry([...this.#tasks.entries()], id, index));
1850
- return success(target);
2307
+ return this.#tasks.move(id, index);
1851
2308
  }
1852
2309
  update(id, patch) {
1853
- const target = this.#tasks.get(id);
1854
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
1855
- if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for task '${id}'`, { id }));
1856
- target.patch(patch);
1857
- return success(target);
2310
+ return this.#tasks.update(id, patch);
1858
2311
  }
1859
2312
  task(id) {
1860
- return this.#tasks.get(id);
2313
+ return this.#tasks.entry(id);
1861
2314
  }
1862
2315
  tasks() {
1863
- return [...this.#tasks.values()];
1864
- }
1865
- #reorder(entries) {
1866
- this.#tasks.clear();
1867
- for (const [key, value] of entries) this.#tasks.set(key, value);
2316
+ return this.#tasks.entries();
1868
2317
  }
1869
2318
  };
1870
2319
  //#endregion
1871
2320
  //#region src/core/phases/Phase.ts
1872
2321
  /**
1873
- * The live DERIVED state machine (W-b) for one phase — an observable (AGENTS §13) whose
1874
- * {@link PhaseStatus} is computed from its tasks (never set directly) and recomputed
2322
+ * Implements the live derived state machine (W-b) for one phase — an observable whose
2323
+ * {@link LifecycleStatus} is computed from its tasks (never set directly) and recomputed
1875
2324
  * reactively as a task transitions (the middle tier of the cascade).
1876
2325
  *
1877
2326
  * @remarks
1878
2327
  * - **Derived status.** `status` is `#override` when one is in force, else
1879
2328
  * {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
1880
- * each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
1881
- * event AND escalates to the workflow (`#escalate`, the upward step of the cascade).
1882
- * - **Override (AGENTS §10).** `skip` / `stop` FORCE the phase's status (e.g. skipping a whole
1883
- * phase), overriding the derived value; the override is PERSISTED in the snapshot's own
1884
- * `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
1885
- * - **Children (AGENTS §9).** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
2329
+ * each child {@link Task}) re-derives on every child transition; a change emits the matching
2330
+ * event and escalates to the workflow (`#escalate`, the upward step of the cascade).
2331
+ * - **Override.** `skip` / `stop` force the phase's status (for example, skipping a whole
2332
+ * phase), overriding the derived value; the override is persisted in the snapshot's own
2333
+ * `override` field and restored directly (no divergence guess), so a forced phase round-trips.
2334
+ * - **Children.** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
1886
2335
  * no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
1887
2336
  * `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
1888
- * tree); `workflow` navigates UP to the live parent.
1889
- * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
2337
+ * tree); `workflow` navigates up to the live parent.
2338
+ * - **Observable.** The owned {@link emitter} ({@link PhaseEventMap}) fires
1890
2339
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
1891
2340
  * corresponding status or runtime-gate change. Status events fire after the phase recomputes
1892
2341
  * and before it escalates to the workflow, preserving child/phase cause before parent effect.
1893
2342
  * The emitter isolates a listener throw and routes it to its `error` handler (the `error`
1894
2343
  * option); `fail` carries the failing task's {@link TaskResult}.
1895
- * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
2344
+ * - **Structural API.** `add` / `remove` / `move` / `update` gate before
1896
2345
  * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1897
- * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
2346
+ * bounds), then emit the matching {@link PhaseEventMap} event on success only. Native
1898
2347
  * gating, purely from this phase's own derived `status` (no runner-installed hook): while
1899
- * `pending`, any valid `index` is accepted; while `running`, `add` accepts ONLY a pure
2348
+ * `pending`, any valid `index` is accepted; while `running`, `add` accepts only a pure
1900
2349
  * append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
1901
2350
  * `update` always fail gracefully (the tasks are already handed to the execution
1902
2351
  * substrate); while terminal, everything is refused.
1903
- * - **Patch (AGENTS §12).** `patch` applies a validated {@link PhaseUpdate} to SELF
2352
+ * - **Patch.** `patch` applies a validated {@link PhaseUpdate} to self
1904
2353
  * (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
1905
2354
  * `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
1906
2355
  * the owning {@link WorkflowInterface.update}'s gate.
1907
- * - **Minting (AGENTS §7).** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
1908
- * (converts it to a {@link TaskSnapshot}, builds the task wired to THIS phase) — the same
2356
+ * - **Minting.** {@link add} mints a live {@link Task} from a {@link TaskDefinition}
2357
+ * (converts it to a {@link TaskSnapshot}, builds the task wired to this phase) — the same
1909
2358
  * construction path {@link #append} uses at build time, so a live mint and a restored/built
1910
- * task are wired IDENTICALLY. At construction, the workflow-level
1911
- * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1912
- * {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `run`
1913
- * name ONCE before any task is built; siblings sharing a name receive the exact same captured
2359
+ * task are wired identically. At construction, the workflow-level
2360
+ * {@link import('../types.js').WorkflowRegistry} registry (threaded from
2361
+ * {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `behavior`
2362
+ * name once before any task is built; siblings sharing a name receive the exact same captured
1914
2363
  * runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
1915
2364
  * that name once from the retained registry at its own mint moment. An omitted or unregistered
1916
- * `run` resolves to no handler; only omission is a no-op, while an unresolved present name makes
2365
+ * `behavior` resolves to no handler; only omission is a no-op, while an unresolved present name makes
1917
2366
  * the containing tree non-drivable.
1918
- * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
2367
+ * - **Runtime lifecycle.** `pause` / `resume` / `wait` mirror the workflow's own
1919
2368
  * quartet, scoped to this phase — a driving
1920
2369
  * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
1921
- * pre-dispatch on the workflow's gate FIRST, then this phase's gate, WITHOUT touching
2370
+ * pre-dispatch on the workflow's gate first, then this phase's gate, without touching
1922
2371
  * {@link status} — `paused` is runtime-only, never persisted. `skip` / `stop` (this phase's
1923
2372
  * own terminal forcing) always release a parked {@link wait} waiter, mirroring
1924
2373
  * {@link import('../Workflow.js').Workflow.destroy}'s cascade — a permanently-ended phase
@@ -1927,6 +2376,7 @@ var TaskManager = class {
1927
2376
  var Phase = class {
1928
2377
  #id;
1929
2378
  #name;
2379
+ #description;
1930
2380
  #workflow;
1931
2381
  #escalateUp;
1932
2382
  #tasks = new TaskManager();
@@ -1945,16 +2395,13 @@ var Phase = class {
1945
2395
  const tasks = options?.tasks;
1946
2396
  this.#id = snapshot.id;
1947
2397
  this.#name = snapshot.name;
1948
- if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
1949
- configurable: true,
1950
- value: snapshot.description
1951
- });
2398
+ this.#description = snapshot.description;
1952
2399
  this.#workflow = workflow;
1953
2400
  this.#escalateUp = escalate;
1954
2401
  this.#functions = functions;
1955
2402
  this.#silence = silence;
1956
2403
  const handlers = /* @__PURE__ */ new Map();
1957
- for (const task of snapshot.tasks) if (task.run !== void 0 && !handlers.has(task.run)) handlers.set(task.run, functions?.[task.run]);
2404
+ for (const task of snapshot.tasks) if (task.behavior !== void 0 && !handlers.has(task.behavior)) handlers.set(task.behavior, functions?.[task.behavior]);
1958
2405
  this.#bail = bail ?? snapshot.bail;
1959
2406
  this.#concurrency = snapshot.concurrency;
1960
2407
  this.#emitter = new _orkestrel_emitter.Emitter({
@@ -1963,7 +2410,7 @@ var Phase = class {
1963
2410
  });
1964
2411
  for (const task of snapshot.tasks) {
1965
2412
  const taskOptions = tasks?.[task.id];
1966
- const handler = task.run === void 0 ? void 0 : handlers.get(task.run);
2413
+ const handler = task.behavior === void 0 ? void 0 : handlers.get(task.behavior);
1967
2414
  this.#append(task, taskOptions, handler);
1968
2415
  }
1969
2416
  this.#override = snapshot.override;
@@ -1980,11 +2427,14 @@ var Phase = class {
1980
2427
  get name() {
1981
2428
  return this.#name;
1982
2429
  }
2430
+ get description() {
2431
+ return this.#description;
2432
+ }
1983
2433
  get context() {
1984
2434
  return buildPhaseContext(this.#workflow.context, {
1985
2435
  id: this.#id,
1986
2436
  name: this.#name,
1987
- ...this.description === void 0 ? {} : { description: this.description }
2437
+ ...this.#description === void 0 ? {} : { description: this.#description }
1988
2438
  });
1989
2439
  }
1990
2440
  get workflow() {
@@ -2026,7 +2476,7 @@ var Phase = class {
2026
2476
  pause() {
2027
2477
  if (this.#paused || isTerminalStatus(this.status)) return;
2028
2478
  this.#paused = true;
2029
- this.#gate = createDeferred();
2479
+ this.#gate = Promise.withResolvers();
2030
2480
  this.#emitter.emit("pause");
2031
2481
  }
2032
2482
  resume() {
@@ -2088,10 +2538,7 @@ var Phase = class {
2088
2538
  status: this.status
2089
2539
  });
2090
2540
  if (value.name !== void 0) this.#name = value.name;
2091
- if (value.description !== void 0) Object.defineProperty(this, "description", {
2092
- configurable: true,
2093
- value: value.description
2094
- });
2541
+ if (value.description !== void 0) this.#description = value.description;
2095
2542
  if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
2096
2543
  if (value.bail !== void 0) this.#bail = value.bail;
2097
2544
  }
@@ -2099,7 +2546,7 @@ var Phase = class {
2099
2546
  return {
2100
2547
  id: this.id,
2101
2548
  name: this.name,
2102
- ...this.description === void 0 ? {} : { description: this.description },
2549
+ ...this.#description === void 0 ? {} : { description: this.#description },
2103
2550
  status: this.status,
2104
2551
  ...this.#override === void 0 ? {} : { override: this.#override },
2105
2552
  bail: this.#bail,
@@ -2134,7 +2581,7 @@ var Phase = class {
2134
2581
  }
2135
2582
  #failure() {
2136
2583
  const found = findFailure(this.results());
2137
- if (found === void 0) throw new Error(`phase '${this.id}' derived failed with no failing task result`);
2584
+ if (found === void 0) throw new WorkflowError("INVARIANT", `phase '${this.id}' derived failed with no failing task result`, { phase: this.id });
2138
2585
  return found;
2139
2586
  }
2140
2587
  #release() {
@@ -2152,11 +2599,11 @@ var Phase = class {
2152
2599
  this.#tasks.append(created);
2153
2600
  }
2154
2601
  #create(snapshot, options, handler) {
2155
- return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
2602
+ return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.behavior, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
2156
2603
  }
2157
2604
  #mint(definition) {
2158
2605
  const snapshot = taskDefinitionToSnapshot(definition);
2159
- const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
2606
+ const handler = snapshot.behavior === void 0 ? void 0 : this.#functions?.[snapshot.behavior];
2160
2607
  return this.#create(snapshot, void 0, handler);
2161
2608
  }
2162
2609
  #statuses() {
@@ -2166,24 +2613,28 @@ var Phase = class {
2166
2613
  //#endregion
2167
2614
  //#region src/core/phases/PhaseManager.ts
2168
2615
  /**
2169
- * The lean child manager (AGENTS §9) of a {@link import('../Workflow.js').Workflow}'s
2170
- * live phases — an insertion-ordered registry keyed by phase `id`, the phase analogue
2616
+ * Implements the lean child manager of a {@link import('../Workflow.js').Workflow}'s live
2617
+ * phases — the phase vocabulary over one insertion-ordered {@link Collection}, the phase analogue
2171
2618
  * of {@link import('../tasks/TaskManager.js').TaskManager}.
2172
2619
  *
2173
2620
  * @remarks
2174
- * - **Positional store.** Phases live in an insertion-ordered `Map` keyed by `id`;
2175
- * `append` adds one at the end, `phase(id)` looks one up, `phases()` lists them in
2176
- * positional order, `count` is the size. A snapshot RESTORE re-`append`s in the
2177
- * snapshot's order, reproducing it exactly.
2178
- * - **Gated mutation API (AGENTS §12).** `add` / `remove` / `move` / `update` are the
2179
- * graceful `Result` counterparts to `append`, gating ONLY on the target's OWN
2180
- * existence/status/id/bounds a duplicate id, an absent/non-`pending` target, an
2181
- * out-of-bounds `index`, or a patch that fails {@link phaseUpdateShape} validation
2182
- * all fail gracefully with a `MUTATION` {@link WorkflowError} instead of throwing.
2183
- * - **No batch matrix.** A workflow's phases are a fixed positional set, so AGENTS §9.2
2184
- * is deliberately omitted.
2185
- * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own
2186
- * their own emitters.
2621
+ * - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
2622
+ * the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
2623
+ * `phase` noun its refusals name and the compiled {@link phaseUpdateShape} guard. This class
2624
+ * adds the domain accessors `phase` / `phases` and nothing else.
2625
+ * - **Positional store.** `append` adds one live {@link PhaseInterface} at the end, `phase(id)`
2626
+ * looks one up, `phases()` lists them in positional order, `count` is the tally. A snapshot
2627
+ * RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
2628
+ * - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
2629
+ * `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
2630
+ * a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
2631
+ * fails {@link phaseUpdateShape} validation all fail gracefully with a `MUTATION`
2632
+ * {@link WorkflowError} instead of throwing.
2633
+ * - **No batch matrix.** A workflow's phases are a fixed positional set, so the batch verbs of
2634
+ * `.claude/rules/patterns.md` § Batch operations are
2635
+ * deliberately omitted.
2636
+ * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own their own
2637
+ * emitters.
2187
2638
  *
2188
2639
  * @example
2189
2640
  * ```ts
@@ -2194,58 +2645,37 @@ var Phase = class {
2194
2645
  * ```
2195
2646
  */
2196
2647
  var PhaseManager = class {
2197
- #phases = /* @__PURE__ */ new Map();
2198
- #isUpdate = (0, _orkestrel_contract.compileGuard)(phaseUpdateShape);
2648
+ #phases = new Collection("phase", (0, _orkestrel_contract.compileGuard)(phaseUpdateShape));
2199
2649
  get count() {
2200
- return this.#phases.size;
2650
+ return this.#phases.count;
2201
2651
  }
2202
2652
  append(phase) {
2203
- if (this.#phases.has(phase.id)) throw new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id });
2204
- this.#phases.set(phase.id, phase);
2653
+ this.#phases.append(phase);
2205
2654
  }
2206
2655
  add(phase, index) {
2207
- if (this.#phases.has(phase.id)) return failure(new WorkflowError("MUTATION", `duplicate phase id '${phase.id}'`, { id: phase.id }));
2208
- const at = index ?? this.#phases.size;
2209
- if (at < 0 || at > this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
2210
- this.#reorder(insertEntry([...this.#phases.entries()], at, phase.id, phase));
2211
- return success(phase);
2656
+ return this.#phases.add(phase, index);
2212
2657
  }
2213
2658
  remove(id) {
2214
- const target = this.#phases.get(id);
2215
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
2216
- this.#phases.delete(id);
2217
- return success(target);
2659
+ return this.#phases.remove(id);
2218
2660
  }
2219
2661
  move(id, index) {
2220
- const target = this.#phases.get(id);
2221
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
2222
- if (index < 0 || index >= this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
2223
- this.#reorder(moveEntry([...this.#phases.entries()], id, index));
2224
- return success(target);
2662
+ return this.#phases.move(id, index);
2225
2663
  }
2226
2664
  update(id, patch) {
2227
- const target = this.#phases.get(id);
2228
- if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
2229
- if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for phase '${id}'`, { id }));
2230
- target.patch(patch);
2231
- return success(target);
2665
+ return this.#phases.update(id, patch);
2232
2666
  }
2233
2667
  phase(id) {
2234
- return this.#phases.get(id);
2668
+ return this.#phases.entry(id);
2235
2669
  }
2236
2670
  phases() {
2237
- return [...this.#phases.values()];
2238
- }
2239
- #reorder(entries) {
2240
- this.#phases.clear();
2241
- for (const [key, value] of entries) this.#phases.set(key, value);
2671
+ return this.#phases.entries();
2242
2672
  }
2243
2673
  };
2244
2674
  //#endregion
2245
2675
  //#region src/core/Workflow.ts
2246
2676
  /**
2247
- * The live DERIVED state machine (W-b) for a whole workflow — the observable (AGENTS §13)
2248
- * ROOT whose {@link WorkflowStatus} is computed from its phases under the `bail` policy and
2677
+ * Implements the live derived state machine (W-b) for a whole workflow — the observable root
2678
+ * whose {@link LifecycleStatus} is computed from its phases under the `bail` policy and
2249
2679
  * recomputed reactively as the cascade propagates up from a task transition.
2250
2680
  *
2251
2681
  * @remarks
@@ -2255,38 +2685,53 @@ var PhaseManager = class {
2255
2685
  * passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
2256
2686
  * - **Derived status.** `status` is `#override` when forced, else
2257
2687
  * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
2258
- * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
2688
+ * reachable only under `bail: true` (a single failed task halts the workflow); under
2259
2689
  * `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
2260
- * change; a CHANGE emits.
2261
- * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
2262
- * may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
2263
- * `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
2690
+ * change; a change emits.
2691
+ * - **Override.** `skip` / `stop` force the status; an executed task-free pending tree
2692
+ * may also be force-completed vacuously. The override is persisted in the snapshot's own
2693
+ * `override` field and restored directly (no divergence guess). The snapshot also persists
2264
2694
  * `bail`, so a restore re-derives status identically without a silent policy default.
2265
2695
  * - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
2266
- * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
2267
- * navigate UP.
2696
+ * workflow tier; `phase(id)` + each `phase.task(id)` navigate down, a task's `phase` / `workflow`
2697
+ * navigate up.
2268
2698
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
2269
2699
  * JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
2270
- * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2700
+ * - **Observable.** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2271
2701
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2272
2702
  * corresponding status or runtime-gate change; the emitter isolates a listener throw and
2273
2703
  * routes it to its `error` handler (the `error` option); `fail` carries the failing task's
2274
2704
  * {@link TaskResult}.
2275
- * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
2705
+ * - **Structural API.** `add` / `remove` / `move` / `update` gate before
2276
2706
  * delegating to {@link phases} (the manager gates the target's own existence/status/id/
2277
- * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
2707
+ * bounds), then emit the matching {@link WorkflowEventMap} event on success only. Native,
2278
2708
  * bottom-up gating (no runner-installed hook): refused outright while this workflow's own
2279
- * `status` is terminal; otherwise a target position must fall within the PENDING SUFFIX
2709
+ * `status` is terminal; otherwise a target position must fall within the pending suffix
2280
2710
  * the contiguous trailing run of `pending` phases — whose boundary is
2281
2711
  * {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
2282
2712
  * workflow's phases are all `pending`, so the boundary is `0` and every position is
2283
2713
  * naturally accepted.
2284
- * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
2285
- * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
2714
+ * - **Runtime lifecycle.** `pause` / `resume` / `wait` gate execution at the runner's
2715
+ * phase/task boundaries without touching {@link status} — `paused` is runtime-only, never
2286
2716
  * persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
2287
2717
  * phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
2288
2718
  * workflow `stop` override when needed, releases its parked waiter, and marks
2289
2719
  * {@link destroyed} — all idempotent.
2720
+ *
2721
+ * @example
2722
+ * ```ts
2723
+ * import { definitionToSnapshot, Workflow } from '@orkestrel/workflow'
2724
+ *
2725
+ * const definition = {
2726
+ * id: 'release',
2727
+ * name: 'Release',
2728
+ * phases: [{ id: 'build', name: 'Build', tasks: [{ id: 'compile', name: 'Compile' }] }],
2729
+ * }
2730
+ * const workflow = new Workflow(definitionToSnapshot(definition))
2731
+ * workflow.status // 'pending'
2732
+ * workflow.phase('build')?.task('compile')?.status // 'pending'
2733
+ * workflow.snapshot().id // 'release'
2734
+ * ```
2290
2735
  */
2291
2736
  var Workflow = class {
2292
2737
  #context;
@@ -2313,7 +2758,6 @@ var Workflow = class {
2313
2758
  const functions = captured.functions;
2314
2759
  const silence = captured.silence;
2315
2760
  this.#context = buildWorkflowContext(snapshot);
2316
- if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
2317
2761
  this.#bail = bail ?? snapshot.bail;
2318
2762
  this.#bailOverride = bail;
2319
2763
  this.#functions = functions;
@@ -2344,6 +2788,9 @@ var Workflow = class {
2344
2788
  get name() {
2345
2789
  return this.#context.name;
2346
2790
  }
2791
+ get description() {
2792
+ return this.#context.description;
2793
+ }
2347
2794
  get context() {
2348
2795
  return this.#context;
2349
2796
  }
@@ -2387,7 +2834,7 @@ var Workflow = class {
2387
2834
  pause() {
2388
2835
  if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
2389
2836
  this.#paused = true;
2390
- this.#gate = createDeferred();
2837
+ this.#gate = Promise.withResolvers();
2391
2838
  this.#emitter.emit("pause");
2392
2839
  }
2393
2840
  resume() {
@@ -2475,7 +2922,7 @@ var Workflow = class {
2475
2922
  return cloneWorkflowSnapshot({
2476
2923
  id: this.id,
2477
2924
  name: this.name,
2478
- ...this.description === void 0 ? {} : { description: this.description },
2925
+ ...this.#context.description === void 0 ? {} : { description: this.#context.description },
2479
2926
  status: this.status,
2480
2927
  ...this.#override === void 0 ? {} : { override: this.#override },
2481
2928
  bail: this.#bail,
@@ -2519,7 +2966,7 @@ var Workflow = class {
2519
2966
  }
2520
2967
  #failure() {
2521
2968
  const found = findFailure(this.results());
2522
- if (found === void 0) throw new Error(`workflow '${this.id}' derived failed with no failing task result`);
2969
+ if (found === void 0) throw new WorkflowError("INVARIANT", `workflow '${this.id}' derived failed with no failing task result`, { workflow: this.id });
2523
2970
  return found;
2524
2971
  }
2525
2972
  #append(phase, options) {
@@ -2544,24 +2991,25 @@ var Workflow = class {
2544
2991
  //#endregion
2545
2992
  //#region src/core/WorkflowManager.ts
2546
2993
  /**
2547
- * The store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
2994
+ * Implements the store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
2548
2995
  * the additive manager tier mirroring the `@orkestrel/agent` line's `ConversationManager` /
2549
2996
  * `WorkspaceManager`. Event-free (a registry, like its twins); the observability lives on each
2550
2997
  * {@link WorkflowInterface}.
2551
2998
  *
2552
2999
  * @remarks
2553
3000
  * - **Registry.** Workflows live in an insertion-ordered `Map` keyed by `id`. `add(definition)`
2554
- * mints a live {@link WorkflowInterface} through {@link createWorkflow} (flowing the manager's
3001
+ * mints a live {@link WorkflowInterface} through the same construction path
3002
+ * {@link import('./factories.js').createWorkflow} takes (flowing the manager's
2555
3003
  * `functions` registry in) and stores it under `definition.id` — an already-present id
2556
- * OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
3004
+ * overwrites (last write wins). `count` is the map size, `workflow(id)` looks one up,
2557
3005
  * `workflows()` lists them in insertion order.
2558
3006
  * - **Durable open / save.** `open(id)` returns an already-registered workflow directly; same-id
2559
3007
  * misses share one hydration. A concurrent `add` wins, while `remove` / `clear` invalidate
2560
3008
  * earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
2561
3009
  * workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
2562
3010
  * Both remain lenient without a store or registered id.
2563
- * - **Removal.** `remove` drops one by id, or a batch (§9.2, array overload FIRST) — `true` when
2564
- * any was removed. `clear` empties the registry.
3011
+ * - **Removal.** `remove` drops one by id, or a batch (array overload first) — `true` only when
3012
+ * every id was removed. `clear` empties the registry.
2565
3013
  * - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
2566
3014
  * no `active` / `switch` — nothing in the workflow domain renders "the current workflow".
2567
3015
  *
@@ -2570,7 +3018,7 @@ var Workflow = class {
2570
3018
  * const manager = new WorkflowManager({
2571
3019
  * functions: { compile: async (controller) => `built ${controller.task.id}` },
2572
3020
  * })
2573
- * const workflow = manager.add(definition) // minted, registered, RUNNABLE
3021
+ * const workflow = manager.add(definition) // minted, registered, runnable
2574
3022
  * manager.workflow(workflow.id) // the same workflow
2575
3023
  * manager.count // 1
2576
3024
  * ```
@@ -2599,7 +3047,7 @@ var WorkflowManager = class {
2599
3047
  return [...this.#workflows.values()];
2600
3048
  }
2601
3049
  add(definition) {
2602
- const workflow = createWorkflow(definition, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
3050
+ const workflow = this.#build(definition);
2603
3051
  const mutation = this.#invalidate(workflow.id);
2604
3052
  if (mutation === void 0) this.#additions.delete(workflow.id);
2605
3053
  else this.#additions.set(workflow.id, mutation);
@@ -2636,11 +3084,11 @@ var WorkflowManager = class {
2636
3084
  }
2637
3085
  remove(ids) {
2638
3086
  if ((0, _orkestrel_contract.isArray)(ids)) {
2639
- let removed = false;
3087
+ let removed = true;
2640
3088
  for (const id of ids) {
2641
3089
  this.#invalidate(id);
2642
3090
  this.#additions.delete(id);
2643
- if (this.#workflows.delete(id)) removed = true;
3091
+ if (!this.#workflows.delete(id)) removed = false;
2644
3092
  }
2645
3093
  return removed;
2646
3094
  }
@@ -2670,7 +3118,7 @@ var WorkflowManager = class {
2670
3118
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2671
3119
  let workflow;
2672
3120
  try {
2673
- workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
3121
+ workflow = this.#restore(owned);
2674
3122
  } catch (error) {
2675
3123
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2676
3124
  throw error;
@@ -2681,6 +3129,15 @@ var WorkflowManager = class {
2681
3129
  this.#releaseHydration(id, lease);
2682
3130
  }
2683
3131
  }
3132
+ #build(definition) {
3133
+ return createWorkflowTree(definition, this.#captured());
3134
+ }
3135
+ #restore(snapshot) {
3136
+ return new Workflow(cloneWorkflowSnapshot(snapshot), this.#captured());
3137
+ }
3138
+ #captured() {
3139
+ return captureWorkflowOptions(this.#functions === void 0 ? {} : { functions: this.#functions });
3140
+ }
2684
3141
  #owns(id, mutation, generation) {
2685
3142
  return this.#generation === generation && this.#mutations.get(id) === mutation;
2686
3143
  }
@@ -2736,9 +3193,50 @@ var WorkflowManager = class {
2736
3193
  }
2737
3194
  };
2738
3195
  //#endregion
3196
+ //#region src/core/RunHolder.ts
3197
+ /**
3198
+ * Holds the active phase {@link RunnerInterface} for one
3199
+ * {@link import('./types.js').WorkflowRunnerInterface.execute} call, for the lifetime of that run.
3200
+ *
3201
+ * @remarks
3202
+ * - **One holder per run.** The engine mints a holder as a run begins and threads that one
3203
+ * instance through every phase of the run, so a nested `execute` reached through application
3204
+ * composition gets its own holder and can never clobber the suspended outer run's.
3205
+ * - **`hold` is the only mutation.** A phase takes the substrate runner with `hold(runner)` as it
3206
+ * starts and releases it with `hold()` as it settles; `runner` reads the held value back and is
3207
+ * `undefined` between phases and after the last one.
3208
+ * - **A cancel closes over the holder.** The run-level abort listener reads `runner` when it
3209
+ * fires, so it reaches whichever phase runner is live at that moment rather than the one that
3210
+ * was live when the listener was armed.
3211
+ * - **Event-free.** A plain cell — no emitter, no lifecycle of its own.
3212
+ */
3213
+ var RunHolder = class {
3214
+ #runner;
3215
+ get runner() {
3216
+ return this.#runner;
3217
+ }
3218
+ /**
3219
+ * Takes the phase runner a starting phase hands this run, or releases the held one.
3220
+ *
3221
+ * @param runner - The phase runner to hold; omitted releases the held runner
3222
+ * @example
3223
+ * ```ts
3224
+ * import type { TaskInterface } from '@orkestrel/workflow'
3225
+ * import { createRunner, RunHolder } from '@orkestrel/workflow'
3226
+ *
3227
+ * const holder = new RunHolder()
3228
+ * holder.hold(createRunner<TaskInterface, void>({ handler: () => undefined }))
3229
+ * holder.hold() // released — `runner` reads `undefined` again
3230
+ * ```
3231
+ */
3232
+ hold(runner) {
3233
+ this.#runner = runner;
3234
+ }
3235
+ };
3236
+ //#endregion
2739
3237
  //#region src/core/Controller.ts
2740
3238
  /**
2741
- * The per-unit handle a runner handler receives — wraps the unit's identity,
3239
+ * Implements the per-unit handle a runner handler receives — wraps the unit's identity,
2742
3240
  * input, cancellation, and the run controls (`wait` / `spawn` / `abort`).
2743
3241
  *
2744
3242
  * @remarks
@@ -2746,14 +3244,14 @@ var WorkflowManager = class {
2746
3244
  * unit it dispatches, handing it the unit's `id`, `input`, the unit's `Abort`
2747
3245
  * handle, the queue attempt's `signal`, and a `spawn` callback that launches a
2748
3246
  * sibling through the same queue.
2749
- * - **Signal.** `signal` is the queue attempt's signal, which ANY-combines the
3247
+ * - **Signal.** `signal` is the queue attempt's signal, which any-combines the
2750
3248
  * unit's own abort, the runner-level abort (the runner aborts every unit), and
2751
3249
  * the per-attempt timeout — so it fires on any of the three. `aborted` and
2752
3250
  * `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
2753
- * truth); since the attempt signal ANY-includes that abort, `abort()` fires
3251
+ * truth); because the attempt signal any-includes that abort, `abort()` fires
2754
3252
  * `signal` too.
2755
3253
  * - **`wait` promise-parks (never a timer).** It resolves the instant the unit's
2756
- * `signal` fires (immediately if already aborted) via a one-shot listener — no
3254
+ * `signal` fires (immediately if already aborted) through a one-shot listener — no
2757
3255
  * `setTimeout`, no polling, no busy-yield — so a parked unit costs no CPU.
2758
3256
  * - **`spawn` is fire-and-track.** It delegates to the runner's launch-a-sibling
2759
3257
  * callback, which routes the sibling through the queue; the runner's `execute`
@@ -2764,23 +3262,32 @@ var WorkflowManager = class {
2764
3262
  * {@link RunnerInterface.emitter} instead (`unit` / `spawn` / `settle` / `fail` carry the id).
2765
3263
  */
2766
3264
  var Controller = class {
2767
- id;
2768
- input;
2769
- signal;
3265
+ #id;
3266
+ #input;
3267
+ #signal;
2770
3268
  #abort;
2771
3269
  #spawn;
2772
3270
  constructor(id, input, abort, signal, spawn) {
2773
- this.id = id;
2774
- this.input = input;
3271
+ this.#id = id;
3272
+ this.#input = input;
2775
3273
  this.#abort = abort;
2776
- this.signal = signal;
3274
+ this.#signal = signal;
2777
3275
  this.#spawn = spawn;
2778
3276
  }
3277
+ get id() {
3278
+ return this.#id;
3279
+ }
3280
+ get input() {
3281
+ return this.#input;
3282
+ }
3283
+ get signal() {
3284
+ return this.#signal;
3285
+ }
2779
3286
  get aborted() {
2780
3287
  return this.#abort.aborted;
2781
3288
  }
2782
3289
  wait() {
2783
- return parkSignal(this.signal);
3290
+ return parkSignal(this.#signal);
2784
3291
  }
2785
3292
  spawn(input) {
2786
3293
  return this.#spawn(input);
@@ -2792,7 +3299,7 @@ var Controller = class {
2792
3299
  //#endregion
2793
3300
  //#region src/core/Runner.ts
2794
3301
  /**
2795
- * A thin generic orchestrator that drives declared units — and any they `spawn` —
3302
+ * Implements a thin generic orchestrator that drives declared units — and any they `spawn` —
2796
3303
  * through a bounded-concurrency {@link createQueue}, collecting ordered results.
2797
3304
  *
2798
3305
  * @remarks
@@ -2801,17 +3308,17 @@ var Controller = class {
2801
3308
  * bounded concurrency, retries, and the per-attempt timeout are all the Queue's —
2802
3309
  * the Runner adds only orchestration (launching, ordering, draining, fail-fast).
2803
3310
  * - **Spawns actually run, results stay ordered (the B2 fix).** Declared inputs and
2804
- * `spawn`ed siblings flow through the SAME `#launch`, which appends the unit's `id`
3311
+ * `spawn`ed siblings flow through the same `#launch`, which appends the unit's `id`
2805
3312
  * to an ordered `#order` list and records its settled value into `#values` by `id`.
2806
3313
  * Results are read back as `#order.map(id => #values.get(id))` — declared first (in
2807
3314
  * input order), then spawns (in spawn order). There is no one-time task snapshot,
2808
3315
  * so a unit spawned mid-handler is run and ordered like any other.
2809
- * - **`execute` awaits the full spawn closure via a count gate.** `#launch` increments
2810
- * an outstanding-unit `#count` BEFORE enqueuing and every settle decrements it,
3316
+ * - **`execute` awaits the full spawn closure through a count gate.** `#launch` increments
3317
+ * an outstanding-unit `#count` before enqueuing and every settle decrements it,
2811
3318
  * resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
2812
3319
  * `#count += 1`) before the parent handler returns, the count never reaches zero
2813
3320
  * mid-run — `execute` parks on `#drained` and so awaits the entire transitive
2814
- * closure, not just the declared units.
3321
+ * closure, not only the declared units.
2815
3322
  * - **`spawn` is fire-and-track.** A spawned unit runs through the queue regardless of
2816
3323
  * whether its promise is awaited; the Runner never awaits a spawned promise from
2817
3324
  * within a handler's slot (it awaits the count gate instead), so a slot-holding
@@ -2819,26 +3326,26 @@ var Controller = class {
2819
3326
  * spawn by a bounded handler can still deadlock — that caveat is the caller's.)
2820
3327
  * - **Per-unit Controller + signal.** Each unit gets a `Controller` carrying its `id`,
2821
3328
  * `input`, the unit's `Abort` (so `aborted` / `abort` delegate to it), and the queue
2822
- * attempt's `signal` (which ANY-combines the unit abort + runner abort + timeout). A
3329
+ * attempt's `signal` (which any-combines the unit abort + runner abort + timeout). A
2823
3330
  * `spawn` callback is injected so `controller.spawn(input)` delegates to `#launch`.
2824
3331
  * - **One-shot + fail-fast.** `execute` runs once (a second call throws). The first
2825
3332
  * unit failure (after its retries) records the error and `abort()`s the run, so every
2826
3333
  * sibling's signal fires; later failures are ignored and `execute` rejects with the
2827
3334
  * first error. A user `abort(reason)` likewise rejects a running `execute`.
2828
- * - **`pause` / `resume` / `stop` (§10) ride the backing Queue.** `pause` / `resume`
2829
- * delegate straight to the Queue's own pause/resume (holding/releasing the NEXT
3335
+ * - **`pause` / `resume` / `stop` ride the backing Queue.** `pause` / `resume`
3336
+ * delegate straight to the Queue's own pause/resume (holding/releasing the next
2830
3337
  * dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
2831
- * GRACEFUL permanent end, distinct from `abort`: still-pending (never-dispatched)
2832
- * units are rejected by the Queue's own stop WITHOUT their handler ever running, and
3338
+ * graceful permanent end, distinct from `abort`: still-pending (never-dispatched)
3339
+ * units are rejected by the Queue's own stop without their handler ever running, and
2833
3340
  * `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
2834
3341
  * not a failure, never tripping fail-fast — while an in-flight unit still runs to
2835
- * completion and settles normally. `execute` RESOLVES (never rejects) once every unit
3342
+ * completion and settles normally. `execute` resolves (never rejects) after every unit
2836
3343
  * has settled, with whatever results actually completed.
2837
- * - **Observable (§13).** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
3344
+ * - **Observable.** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
2838
3345
  * lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
2839
- * fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
3346
+ * fire-and-forget observers. Every event is emitted directly, strictly after the relevant
2840
3347
  * launch / settle / drain transition; the emitter isolates a listener throw and routes it
2841
- * to its `error` handler (the `error` option), so a buggy observer can NEVER reorder, throw
3348
+ * to its `error` handler (the `error` option), so a buggy observer can never reorder, throw
2842
3349
  * into, or corrupt the one-shot / fail-fast / spawn-tracking engine: the outstanding-unit
2843
3350
  * count gate stays balanced and fail-fast still fires regardless of what a listener does.
2844
3351
  * Observation is purely a side-channel.
@@ -2897,20 +3404,20 @@ var Runner = class {
2897
3404
  return this.#queue.paused;
2898
3405
  }
2899
3406
  /**
2900
- * Inject one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
2901
- * `Controller.spawn`, called from OUTSIDE any unit's handler.
3407
+ * Injects one more unit into an in-flight `execute` run — a live counterpart to a
3408
+ * `Controller.spawn`, called from outside any unit's handler.
2902
3409
  *
2903
3410
  * @remarks
2904
- * Returns `undefined` synchronously (graceful, non-throwing — AGENTS §12) unless the
2905
- * runner is currently mid-`execute` and not yet stopped — covering "never started",
3411
+ * Returns `undefined` synchronously (graceful, non-throwing) unless the
3412
+ * runner is mid-`execute` and not yet stopped — covering "never started",
2906
3413
  * "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
2907
- * the SAME backing queue as a declared/`spawn`ed unit via `#launch` — the outstanding-
2908
- * unit count gate increments BEFORE this call returns, so an in-flight `execute`
3414
+ * the same backing queue as a declared/`spawn`ed unit through `#launch` — the outstanding-
3415
+ * unit count gate increments before this call returns, so an in-flight `execute`
2909
3416
  * keeps awaiting it (the drain race: `#running` flips to `false` as the very first
2910
3417
  * step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
2911
3418
  * method after the run has fully drained is cleanly rejected with `undefined` —
2912
3419
  * never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
2913
- * with a `parent` of `undefined` (this call has no spawning unit) once accepted.
3420
+ * with a `parent` of `undefined` (this call has no spawning unit) after acceptance.
2914
3421
  *
2915
3422
  * @param input - The unit's work payload
2916
3423
  * @returns The unit's result promise, or `undefined` when no in-flight run can accept it
@@ -2927,11 +3434,11 @@ var Runner = class {
2927
3434
  return this.#launch(input, void 0, true);
2928
3435
  }
2929
3436
  async execute(inputs) {
2930
- if (this.#started) throw new Error("runner has already executed");
2931
- if (this.#stopped) throw new Error("runner is stopped");
3437
+ if (this.#started) throw new WorkflowError("TRANSITION", "runner has already executed", { started: true });
3438
+ if (this.#stopped) throw new WorkflowError("TRANSITION", "runner is stopped", { stopped: true });
2932
3439
  this.#started = true;
2933
3440
  this.#running = true;
2934
- const drained = createDeferred();
3441
+ const drained = Promise.withResolvers();
2935
3442
  this.#drained = drained;
2936
3443
  for (const input of inputs) {
2937
3444
  if (!this.#accepts()) break;
@@ -2952,23 +3459,23 @@ var Runner = class {
2952
3459
  }
2953
3460
  abort(reason) {
2954
3461
  if (this.#abortPromise !== void 0) return this.#abortPromise;
2955
- const barrier = createDeferred();
3462
+ const barrier = Promise.withResolvers();
2956
3463
  this.#abortPromise = barrier.promise;
2957
3464
  barrier.promise.catch(() => {});
2958
- if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
3465
+ if (this.#running && this.#failure === void 0) this.#failure = failure(reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason);
2959
3466
  this.#cancel(reason);
2960
3467
  this.#stopped = true;
2961
3468
  const cleanup = this.#queue.abort(reason);
2962
- this.#settleLifecycle(barrier, cleanup);
3469
+ this.#settleBarrier(barrier, cleanup, false);
2963
3470
  this.#emitter.emit("abort", reason);
2964
3471
  return barrier.promise;
2965
3472
  }
2966
3473
  /**
2967
- * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
2968
- * `pause`, which holds the NEXT dispatch while any in-flight unit finishes.
3474
+ * Suspends dispatch (resumable): delegates to the backing queue's own
3475
+ * `pause`, which holds the next dispatch while any in-flight unit finishes.
2969
3476
  *
2970
3477
  * @remarks
2971
- * A no-op once the runner is `stopped` — a stopped runner has no dispatch left to
3478
+ * A no-op after the runner is `stopped` — a stopped runner has no dispatch left to
2972
3479
  * suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
2973
3480
  * `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
2974
3481
  */
@@ -2977,11 +3484,11 @@ var Runner = class {
2977
3484
  this.#queue.pause();
2978
3485
  }
2979
3486
  /**
2980
- * Continue a paused runner (AGENTS §10); delegates to the backing queue's `resume`.
3487
+ * Continues a paused runner; delegates to the backing queue's `resume`.
2981
3488
  *
2982
3489
  * @remarks
2983
- * A no-op once the runner is `stopped` (nothing left to resume) and a no-op when the
2984
- * runner is not currently `paused`, so calling it repeatedly or on a never-paused
3490
+ * A no-op after the runner is `stopped` (nothing left to resume) and a no-op when the
3491
+ * runner is not `paused`, so calling it repeatedly or on a never-paused
2985
3492
  * runner is safe.
2986
3493
  */
2987
3494
  resume() {
@@ -2989,10 +3496,10 @@ var Runner = class {
2989
3496
  this.#queue.resume();
2990
3497
  }
2991
3498
  /**
2992
- * Permanently end the runner (AGENTS §10) — a GRACEFUL stop, distinct from `abort`.
3499
+ * Ends the runner permanently — a graceful stop, distinct from `abort`.
2993
3500
  * Marks the runner `stopping` + `stopped`, then stops the backing queue: every
2994
- * still-PENDING (never-dispatched) unit is rejected by the queue with its own
2995
- * "queue is stopped" error, WITHOUT running its handler; every already-in-flight unit
3501
+ * still-pending (never-dispatched) unit is rejected by the queue with its own
3502
+ * "queue is stopped" error, without running its handler; every already-in-flight unit
2996
3503
  * keeps running to completion and settles normally. `#settle` reads `#stopping` to
2997
3504
  * classify a never-dispatched unit's rejection as a stop artifact (decrement the count
2998
3505
  * gate, no recorded failure, no fail-fast trip) rather than a genuine failure — a
@@ -3002,24 +3509,24 @@ var Runner = class {
3002
3509
  if (this.#destroyPromise !== void 0) return this.#destroyPromise;
3003
3510
  if (this.#abortPromise !== void 0) return this.#abortPromise;
3004
3511
  if (this.#stopPromise !== void 0) return this.#stopPromise;
3005
- const barrier = createDeferred();
3512
+ const barrier = Promise.withResolvers();
3006
3513
  this.#stopPromise = barrier.promise;
3007
3514
  barrier.promise.catch(() => {});
3008
3515
  this.#stopping = true;
3009
3516
  this.#stopped = true;
3010
3517
  const cleanup = this.#queue.stop();
3011
- this.#settleLifecycle(barrier, cleanup);
3518
+ this.#settleBarrier(barrier, cleanup, false);
3012
3519
  return barrier.promise;
3013
3520
  }
3014
3521
  destroy() {
3015
3522
  if (this.#destroyPromise !== void 0) return this.#destroyPromise;
3016
- const barrier = createDeferred();
3523
+ const barrier = Promise.withResolvers();
3017
3524
  this.#destroyPromise = barrier.promise;
3018
3525
  barrier.promise.catch(() => {});
3019
3526
  this.#stopped = true;
3020
3527
  this.abort();
3021
3528
  const cleanup = this.#queue.destroy();
3022
- this.#settleDestroy(barrier, cleanup);
3529
+ this.#settleBarrier(barrier, cleanup, true);
3023
3530
  return barrier.promise;
3024
3531
  }
3025
3532
  #launch(input, parent, announce = parent !== void 0) {
@@ -3047,33 +3554,27 @@ var Runner = class {
3047
3554
  } catch (error) {
3048
3555
  promise = Promise.reject(error);
3049
3556
  }
3050
- promise.then((value) => this.#settle(id, {
3051
- ok: true,
3052
- value
3053
- }), (error) => this.#settle(id, {
3054
- ok: false,
3055
- error
3056
- }));
3557
+ promise.then((value) => this.#settle(id, success(value)), (error) => this.#settle(id, failure(error)));
3057
3558
  return promise;
3058
3559
  }
3059
- #dispatch(unit, execution) {
3560
+ #dispatch(unit, context) {
3060
3561
  const abort = this.#aborts.get(unit.id);
3061
- if (abort === void 0) throw new Error("unit abort missing");
3562
+ if (abort === void 0) throw new WorkflowError("INVARIANT", "unit abort missing", { unit: unit.id });
3062
3563
  this.#dispatched.add(unit.id);
3063
- const controller = new Controller(unit.id, unit.input, abort, execution.signal, (input) => this.#spawn(input, unit.id));
3564
+ const controller = new Controller(unit.id, unit.input, abort, context.signal, (input) => this.#spawn(input, unit.id));
3064
3565
  this.#emitter.emit("unit", unit.id);
3065
3566
  return this.#handler(controller);
3066
3567
  }
3067
3568
  #spawn(input, parent) {
3068
- if (!this.#accepts()) throw new Error("spawn is unavailable outside an active run");
3569
+ if (!this.#accepts()) throw new WorkflowError("TRANSITION", "spawn is unavailable outside an active run", { parent });
3069
3570
  return this.#launch(input, parent);
3070
3571
  }
3071
3572
  #settle(id, outcome) {
3072
- if (outcome.ok) {
3073
- this.#values.set(id, { value: outcome.value });
3573
+ if (outcome.success) {
3574
+ this.#values.set(id, outcome);
3074
3575
  this.#emitter.emit("settle", id);
3075
3576
  } else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
3076
- this.#failure = { error: outcome.error };
3577
+ this.#failure = failure(outcome.error);
3077
3578
  this.#emitter.emit("fail", id, outcome.error);
3078
3579
  this.abort(outcome.error);
3079
3580
  }
@@ -3098,31 +3599,20 @@ var Runner = class {
3098
3599
  await cleanup;
3099
3600
  return;
3100
3601
  } catch (error) {
3101
- return { error };
3602
+ return failure(error);
3102
3603
  }
3103
3604
  }
3104
- async #settleLifecycle(barrier, cleanup) {
3105
- let failure;
3605
+ async #settleBarrier(barrier, cleanup, teardown) {
3606
+ let cleanupFailure;
3106
3607
  try {
3107
3608
  await cleanup;
3108
3609
  } catch (error) {
3109
- failure = { error };
3610
+ cleanupFailure = failure(error);
3110
3611
  }
3111
3612
  await this.#waitDrain();
3112
- if (failure === void 0) barrier.resolve();
3113
- else barrier.reject(failure.error);
3114
- }
3115
- async #settleDestroy(barrier, cleanup) {
3116
- let failure;
3117
- try {
3118
- await cleanup;
3119
- } catch (error) {
3120
- failure = { error };
3121
- }
3122
- await this.#waitDrain();
3123
- this.#emitter.destroy();
3124
- if (failure === void 0) barrier.resolve();
3125
- else barrier.reject(failure.error);
3613
+ if (teardown) this.#emitter.destroy();
3614
+ if (cleanupFailure === void 0) barrier.resolve();
3615
+ else barrier.reject(cleanupFailure.error);
3126
3616
  }
3127
3617
  async #waitDrain() {
3128
3618
  if (this.#count === 0) return;
@@ -3135,49 +3625,61 @@ var Runner = class {
3135
3625
  //#endregion
3136
3626
  //#region src/core/tasks/TaskController.ts
3137
3627
  /**
3138
- * The attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
3628
+ * Implements the attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
3139
3629
  *
3140
3630
  * @remarks
3141
- * - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
3631
+ * - **A leaf handle, not the runner `Controller`.** A workflow task is a leaf of the
3142
3632
  * declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
3143
3633
  * checkpoints the workflow, phase, and task cooperative gates.
3144
- * - **Folded signal.** `signal` is the cancellation folded for THIS attempt: its per-attempt
3634
+ * - **Folded signal.** `signal` is the cancellation folded for this attempt: its per-attempt
3145
3635
  * deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
3146
3636
  * A handler races its work against it; `aborted` reads it.
3147
3637
  * - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
3148
3638
  * after this signal aborts or a retry token supersedes this handle.
3149
3639
  * - **Input + lineage.** `input` is the task's open `metadata` bag (its
3150
3640
  * {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
3151
- * {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate UP the lineage.
3641
+ * {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate up the lineage.
3152
3642
  * - **Read-up results.** `results()` returns every settled task's {@link TaskResult} across
3153
3643
  * the phases that have already finished (a closure over the live
3154
3644
  * {@link import('./types.js').WorkflowInterface}), so a `function` task can read an earlier
3155
- * phase's output. Read-only — a task records its OWN outcome by returning / throwing, not
3645
+ * phase's output. Read-only — a task records its own outcome by returning / throwing, not
3156
3646
  * by mutating the tree.
3157
3647
  * - **Event-free.** Like the runner `Controller`, the per-task handle carries no Emitter;
3158
3648
  * observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
3159
3649
  */
3160
3650
  var TaskController = class {
3161
- signal;
3162
- input;
3163
- task;
3164
- attempt;
3651
+ #signal;
3652
+ #input;
3653
+ #task;
3654
+ #attempt;
3165
3655
  #entity;
3166
3656
  #report;
3167
3657
  #pulse;
3168
3658
  #results;
3169
3659
  constructor(signal, input, task, attempt, results, report, pulse) {
3170
- this.signal = signal;
3171
- this.input = input;
3172
- this.task = task.context;
3173
- this.attempt = attempt;
3660
+ this.#signal = signal;
3661
+ this.#input = input;
3662
+ this.#task = task.context;
3663
+ this.#attempt = attempt;
3174
3664
  this.#entity = task;
3175
3665
  this.#results = results;
3176
3666
  this.#report = report;
3177
3667
  this.#pulse = pulse;
3178
3668
  }
3669
+ get signal() {
3670
+ return this.#signal;
3671
+ }
3672
+ get input() {
3673
+ return this.#input;
3674
+ }
3675
+ get task() {
3676
+ return this.#task;
3677
+ }
3678
+ get attempt() {
3679
+ return this.#attempt;
3680
+ }
3179
3681
  get aborted() {
3180
- return this.signal.aborted;
3682
+ return this.#signal.aborted;
3181
3683
  }
3182
3684
  get paused() {
3183
3685
  if (this.#ancestorTerminal()) return false;
@@ -3190,7 +3692,7 @@ var TaskController = class {
3190
3692
  return this.#pulse();
3191
3693
  }
3192
3694
  async wait() {
3193
- while (this.paused && !this.signal.aborted) await this.#race(this.#gates());
3695
+ while (this.paused && !this.#signal.aborted) await this.#race(this.#gates());
3194
3696
  }
3195
3697
  results() {
3196
3698
  return this.#results();
@@ -3204,11 +3706,11 @@ var TaskController = class {
3204
3706
  return gates;
3205
3707
  }
3206
3708
  async #race(gates) {
3207
- if (this.signal.aborted || gates.length === 0) return;
3709
+ if (this.#signal.aborted || gates.length === 0) return;
3208
3710
  const deferred = Promise.withResolvers();
3209
3711
  const onAbort = this.#resolve.bind(this, deferred);
3210
3712
  const onTerminal = this.#resolve.bind(this, deferred);
3211
- this.signal.addEventListener("abort", onAbort, { once: true });
3713
+ this.#signal.addEventListener("abort", onAbort, { once: true });
3212
3714
  this.#entity.workflow.emitter.on("skip", onTerminal);
3213
3715
  this.#entity.workflow.emitter.on("stop", onTerminal);
3214
3716
  this.#entity.phase.emitter.on("skip", onTerminal);
@@ -3217,7 +3719,7 @@ var TaskController = class {
3217
3719
  if (this.#ancestorTerminal()) deferred.resolve();
3218
3720
  await Promise.race([Promise.all(gates), deferred.promise]);
3219
3721
  } finally {
3220
- this.signal.removeEventListener("abort", onAbort);
3722
+ this.#signal.removeEventListener("abort", onAbort);
3221
3723
  this.#entity.workflow.emitter.off("skip", onTerminal);
3222
3724
  this.#entity.workflow.emitter.off("stop", onTerminal);
3223
3725
  this.#entity.phase.emitter.off("skip", onTerminal);
@@ -3234,24 +3736,34 @@ var TaskController = class {
3234
3736
  //#endregion
3235
3737
  //#region src/core/WorkflowPersistence.ts
3236
3738
  /**
3237
- * Advanced run-local snapshot persistence with one writer and one coalesced latest obligation.
3739
+ * Coordinates advanced run-local snapshot persistence with one writer and one coalesced most
3740
+ * recent obligation, normally composed through `execute({ store })` rather than built directly.
3238
3741
  *
3239
3742
  * @remarks
3240
- * Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
3241
- * coordinate the same required boundaries around their own runner integration.
3743
+ * Exported for hosts that need to coordinate the same required boundaries around their own runner
3744
+ * integration.
3745
+ *
3746
+ * @example
3747
+ * ```ts
3748
+ * import { WorkflowPersistence, createMemoryWorkflowStore, createWorkflow } from '@orkestrel/workflow'
3749
+ *
3750
+ * const workflow = createWorkflow({ id: 'durable', name: 'Durable', phases: [] })
3751
+ * const persistence = new WorkflowPersistence(workflow, createMemoryWorkflowStore())
3752
+ * await persistence.checkpoint('initial')
3753
+ * const durable = await persistence.finalize()
3754
+ * persistence.detach() // idempotent after finalize
3755
+ * ```
3242
3756
  */
3243
3757
  var WorkflowPersistence = class {
3244
3758
  #workflow;
3245
3759
  #store;
3246
3760
  #phases = /* @__PURE__ */ new Set();
3247
3761
  #tasks = /* @__PURE__ */ new Set();
3248
- #onWorkflowChange;
3762
+ #onChange;
3249
3763
  #onWorkflowAdd;
3250
3764
  #onWorkflowRemove;
3251
- #onPhaseChange;
3252
3765
  #onPhaseAdd;
3253
3766
  #onPhaseRemove;
3254
- #onTaskChange;
3255
3767
  #writing;
3256
3768
  #error;
3257
3769
  #fault;
@@ -3261,32 +3773,29 @@ var WorkflowPersistence = class {
3261
3773
  constructor(workflow, store) {
3262
3774
  this.#workflow = workflow;
3263
3775
  this.#store = store;
3264
- this.#onWorkflowChange = this.#change.bind(this);
3776
+ this.#onChange = this.#change.bind(this);
3265
3777
  this.#onWorkflowAdd = this.#addPhase.bind(this);
3266
3778
  this.#onWorkflowRemove = this.#removePhase.bind(this);
3267
- this.#onPhaseChange = this.#change.bind(this);
3268
3779
  this.#onPhaseAdd = this.#addTask.bind(this);
3269
3780
  this.#onPhaseRemove = this.#removeTask.bind(this);
3270
- this.#onTaskChange = this.#change.bind(this);
3271
3781
  this.#attachWorkflow();
3272
3782
  }
3273
3783
  get fault() {
3274
3784
  return this.#fault;
3275
3785
  }
3276
3786
  /**
3277
- * Persist every change through this required boundary.
3787
+ * Persists every change through this required boundary.
3278
3788
  *
3279
3789
  * @param checkpoint - The boundary being made durable
3280
3790
  * @param task - The task owning an attempt or settlement
3281
3791
  * @param attempt - The persisted attempt number
3282
- * @returns Whether the latest state reached the store
3792
+ * @returns True if the most recent state reached the store; false otherwise
3283
3793
  */
3284
3794
  async checkpoint(checkpoint, task, attempt) {
3285
3795
  const revision = this.#mark();
3286
3796
  while (this.#stored < revision) await this.#flush();
3287
3797
  if (this.#error === void 0) return true;
3288
3798
  if (this.#fault === void 0) this.#fault = Object.freeze({
3289
- origin: "persistence",
3290
3799
  checkpoint,
3291
3800
  message: this.#error,
3292
3801
  ...task === void 0 ? {} : { task: task.id },
@@ -3295,37 +3804,25 @@ var WorkflowPersistence = class {
3295
3804
  return false;
3296
3805
  }
3297
3806
  /**
3298
- * Stop observing the live tree and persist its final state.
3807
+ * Stops observing the live tree and persists its final state.
3299
3808
  *
3300
- * @returns Whether the final snapshot reached the store
3809
+ * @returns True if the final snapshot reached the store; false otherwise
3301
3810
  */
3302
3811
  async finalize() {
3303
3812
  this.detach();
3304
3813
  return this.checkpoint("final");
3305
3814
  }
3306
- /** Stop observing the live tree. */
3815
+ /** Stops observing the live tree. */
3307
3816
  detach() {
3308
3817
  if (!this.#attached) return;
3309
3818
  this.#attached = false;
3310
- this.#workflow.emitter.off("start", this.#onWorkflowChange);
3311
- this.#workflow.emitter.off("complete", this.#onWorkflowChange);
3312
- this.#workflow.emitter.off("fail", this.#onWorkflowChange);
3313
- this.#workflow.emitter.off("skip", this.#onWorkflowChange);
3314
- this.#workflow.emitter.off("stop", this.#onWorkflowChange);
3315
- this.#workflow.emitter.off("move", this.#onWorkflowChange);
3316
- this.#workflow.emitter.off("update", this.#onWorkflowChange);
3819
+ for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.off(event, this.#onChange);
3317
3820
  this.#workflow.emitter.off("add", this.#onWorkflowAdd);
3318
3821
  this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
3319
3822
  for (const phase of this.#phases) this.#detachPhase(phase);
3320
3823
  }
3321
3824
  #attachWorkflow() {
3322
- this.#workflow.emitter.on("start", this.#onWorkflowChange);
3323
- this.#workflow.emitter.on("complete", this.#onWorkflowChange);
3324
- this.#workflow.emitter.on("fail", this.#onWorkflowChange);
3325
- this.#workflow.emitter.on("skip", this.#onWorkflowChange);
3326
- this.#workflow.emitter.on("stop", this.#onWorkflowChange);
3327
- this.#workflow.emitter.on("move", this.#onWorkflowChange);
3328
- this.#workflow.emitter.on("update", this.#onWorkflowChange);
3825
+ for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.on(event, this.#onChange);
3329
3826
  this.#workflow.emitter.on("add", this.#onWorkflowAdd);
3330
3827
  this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
3331
3828
  for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
@@ -3333,26 +3830,14 @@ var WorkflowPersistence = class {
3333
3830
  #attachPhase(phase) {
3334
3831
  if (this.#phases.has(phase)) return;
3335
3832
  this.#phases.add(phase);
3336
- phase.emitter.on("start", this.#onPhaseChange);
3337
- phase.emitter.on("complete", this.#onPhaseChange);
3338
- phase.emitter.on("fail", this.#onPhaseChange);
3339
- phase.emitter.on("skip", this.#onPhaseChange);
3340
- phase.emitter.on("stop", this.#onPhaseChange);
3341
- phase.emitter.on("move", this.#onPhaseChange);
3342
- phase.emitter.on("update", this.#onPhaseChange);
3833
+ for (const event of PERSISTED_NODE_EVENTS) phase.emitter.on(event, this.#onChange);
3343
3834
  phase.emitter.on("add", this.#onPhaseAdd);
3344
3835
  phase.emitter.on("remove", this.#onPhaseRemove);
3345
3836
  for (const task of phase.tasks.tasks()) this.#attachTask(task);
3346
3837
  }
3347
3838
  #detachPhase(phase) {
3348
3839
  if (!this.#phases.delete(phase)) return;
3349
- phase.emitter.off("start", this.#onPhaseChange);
3350
- phase.emitter.off("complete", this.#onPhaseChange);
3351
- phase.emitter.off("fail", this.#onPhaseChange);
3352
- phase.emitter.off("skip", this.#onPhaseChange);
3353
- phase.emitter.off("stop", this.#onPhaseChange);
3354
- phase.emitter.off("move", this.#onPhaseChange);
3355
- phase.emitter.off("update", this.#onPhaseChange);
3840
+ for (const event of PERSISTED_NODE_EVENTS) phase.emitter.off(event, this.#onChange);
3356
3841
  phase.emitter.off("add", this.#onPhaseAdd);
3357
3842
  phase.emitter.off("remove", this.#onPhaseRemove);
3358
3843
  for (const task of phase.tasks.tasks()) this.#detachTask(task);
@@ -3360,23 +3845,11 @@ var WorkflowPersistence = class {
3360
3845
  #attachTask(task) {
3361
3846
  if (this.#tasks.has(task)) return;
3362
3847
  this.#tasks.add(task);
3363
- task.emitter.on("start", this.#onTaskChange);
3364
- task.emitter.on("complete", this.#onTaskChange);
3365
- task.emitter.on("fail", this.#onTaskChange);
3366
- task.emitter.on("skip", this.#onTaskChange);
3367
- task.emitter.on("stop", this.#onTaskChange);
3368
- task.emitter.on("report", this.#onTaskChange);
3369
- task.emitter.on("pulse", this.#onTaskChange);
3848
+ for (const event of PERSISTED_TASK_EVENTS) task.emitter.on(event, this.#onChange);
3370
3849
  }
3371
3850
  #detachTask(task) {
3372
3851
  if (!this.#tasks.delete(task)) return;
3373
- task.emitter.off("start", this.#onTaskChange);
3374
- task.emitter.off("complete", this.#onTaskChange);
3375
- task.emitter.off("fail", this.#onTaskChange);
3376
- task.emitter.off("skip", this.#onTaskChange);
3377
- task.emitter.off("stop", this.#onTaskChange);
3378
- task.emitter.off("report", this.#onTaskChange);
3379
- task.emitter.off("pulse", this.#onTaskChange);
3852
+ for (const event of PERSISTED_TASK_EVENTS) task.emitter.off(event, this.#onChange);
3380
3853
  }
3381
3854
  #addPhase(phase) {
3382
3855
  this.#attachPhase(phase);
@@ -3434,82 +3907,83 @@ var WorkflowPersistence = class {
3434
3907
  //#endregion
3435
3908
  //#region src/core/WorkflowRunner.ts
3436
3909
  /**
3437
- * The thin orchestrator that EXECUTES a live W-b workflow tree by COMPOSING the shipped
3438
- * substrate — phases sequential, tasks concurrent — dispatching each task through its OWN
3910
+ * Implements the thin orchestrator that executes a live W-b workflow tree by composing the shipped
3911
+ * substrate — phases sequential, tasks concurrent — dispatching each task through its own
3439
3912
  * resolved handler under the `bail` policy.
3440
3913
  *
3441
3914
  * @remarks
3442
3915
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
3443
- * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
3916
+ * {@link createRunner} per phase (the substrate {@link import('./types.js').RunnerInterface}
3917
+ * over the workers
3444
3918
  * `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
3445
3919
  * timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
3446
3920
  * {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
3447
3921
  * pacing is the shipped
3448
- * {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
3922
+ * {@link SchedulerInterface}. The runner writes zero concurrency / retry / abort logic of
3449
3923
  * its own — it only sequences phases, dispatches a task's own handler, and drives the live
3450
3924
  * entity. The workflow layer owns per-task deadlines because timeout settlement must
3451
3925
  * update the live leaf under the phase's `bail` policy before the substrate unit settles.
3452
3926
  * - **Pure engine — no integration registry.** The runner carries no behavior or provider
3453
3927
  * registry: each live {@link TaskInterface} already
3454
3928
  * resolved its own {@link import('./types.js').WorkflowFunction} into
3455
- * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
3929
+ * {@link import('./types.js').TaskInterface.handler} once at construction (build, restore,
3456
3930
  * or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
3457
- * dispatch is simply "invoke the task's own handler". Provider, protocol, and tool
3931
+ * dispatch is "invoke the task's own handler". Provider, protocol, and tool
3458
3932
  * integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
3459
3933
  * composed into {@link WorkflowOptions.functions}. This module imports none of them.
3460
- * - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
3461
- * from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
3462
- * metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
3463
- * {@link WorkflowInterface} instead — the entity-native control surface (AGENTS §10:
3464
- * `pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
3465
- * converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition` once the tree
3466
- * exists — `#runTask` reads each task's OWN {@link import('./types.js').TaskInterface.handler}
3467
- * / `retries` / `timeout`, and `#runPhase` reads each phase's OWN
3934
+ * - **Two `execute` forms, one engine.** `execute(definition, options)` builds the live tree
3935
+ * from a {@link WorkflowDefinition} (single source of truth for the `behavior` / `concurrency`
3936
+ * metadata); `execute(workflow, options)` drives a caller-owned, already-built
3937
+ * {@link WorkflowInterface} instead — the entity-native control surface
3938
+ * (`pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
3939
+ * converge on the same `#execute` engine: neither reads a `WorkflowDefinition` after the tree
3940
+ * exists — `#runTask` reads each task's own {@link import('./types.js').TaskInterface.handler}
3941
+ * / `retries` / `timeout`, and `#runPhase` reads each phase's own
3468
3942
  * {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
3469
- * runs under EXACTLY the same rules as one built from the original definition.
3470
- * - **Phases sequential, tasks concurrent — LIVE continuity.** `#execute` drives the phases in
3471
- * order, RE-READING `workflow.phases.phases()` every iteration (a cursor over the live
3943
+ * runs under exactly the same rules as one built from the original definition.
3944
+ * - **Phases sequential, tasks concurrent — live continuity.** `#execute` drives the phases in
3945
+ * order, re-reading `workflow.phases.phases()` every iteration (a cursor over the live
3472
3946
  * manager, not a one-time snapshot) so a caller's `workflow.add(phaseDefinition)` mid-run is
3473
- * picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event BEFORE
3474
- * capturing its task list, then `spawn`s any task added mid-phase onto the SAME substrate
3947
+ * picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event before
3948
+ * capturing its task list, then `spawn`s any task added mid-phase onto the same substrate
3475
3949
  * Runner (so it is actually dispatched, under the same `concurrency`); a task added too late
3476
3950
  * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
3477
3951
  * phase always reaches a coherent terminal state.
3478
3952
  * - **Dispatch by handler.** `#runTask` invokes the live task's own
3479
- * {@link import('./types.js').TaskInterface.handler} directly. An omitted `run` deliberately
3953
+ * {@link import('./types.js').TaskInterface.handler} directly. An omitted `behavior` deliberately
3480
3954
  * auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
3481
3955
  * execution claim and never false-completes.
3482
3956
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
3483
- * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
3957
+ * then re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
3484
3958
  * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
3485
3959
  * `#execute` then `skip`s the remaining tasks / phases (the workflow derives `failed`).
3486
- * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
3960
+ * Under `bail: false` (graceful) a failure `fail`s the leaf and resolves (never throws), so
3487
3961
  * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
3488
3962
  * `completed`, the failure recorded in the result tree).
3489
3963
  * - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
3490
3964
  * dispatch, and a running handler can checkpoint their folded state through
3491
3965
  * {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
3492
3966
  * concurrency before this handler gate, a paused task occupies one phase slot until resume;
3493
- * already-running siblings continue and its per-attempt timeout keeps counting. A GRACEFUL
3967
+ * already-running siblings continue and its per-attempt timeout keeps counting. A graceful
3494
3968
  * `workflow.stop()` (no signal involved) is caught at
3495
3969
  * those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
3496
- * HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
3970
+ * hard `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
3497
3971
  * into the run's composed signal — so it cancels the active phase Runner (and every
3498
- * in-flight task) exactly like an external abort / timeout / budget fire. EVERY park on a
3499
- * `wait()` gate is RACED against that same run signal (`#raceWait`, S2) — so a cancel firing
3500
- * WHILE parked unparks the engine promptly instead of hanging until `resume`; the existing
3972
+ * in-flight task) exactly like an external abort / timeout / budget fire. Every park on a
3973
+ * `wait()` gate is raced against that same run signal (`#raceWait`, S2) — so a cancel firing
3974
+ * while parked unparks the engine promptly instead of hanging until `resume`; the existing
3501
3975
  * halt / abort re-checks after the gate then decide the outcome.
3502
3976
  * - **Abort / Timeout / Budget / entity-signal fold.** `#execute` folds the live workflow's
3503
3977
  * own {@link WorkflowInterface.signal}, the run's external `signal`, a
3504
3978
  * {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
3505
3979
  * `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
3506
- * (cancelling every in-flight task) and HALTS the run — the remaining tasks / phases `skip`
3980
+ * (cancelling every in-flight task) and halts the run — the remaining tasks / phases `skip`
3507
3981
  * and the workflow is force-`stop`ped (settles `stopped`). Each task's
3508
3982
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
3509
3983
  * `runSignal`, so a handler observes either cause directly.
3510
- * - **Re-entrant-safe.** No shared per-run mutable field: the active-Runner holder is LOCAL to
3511
- * each `#execute`, so a nested application-level `execute` cannot clobber the outer run's
3512
- * state.
3984
+ * - **Re-entrant-safe.** No shared per-run mutable field: each `#execute` mints its own
3985
+ * {@link import('./RunHolder.js').RunHolder}, so a nested application-level `execute` cannot
3986
+ * clobber the outer run's state.
3513
3987
  */
3514
3988
  var WorkflowRunner = class WorkflowRunner {
3515
3989
  static #executions = /* @__PURE__ */ new WeakSet();
@@ -3518,7 +3992,7 @@ var WorkflowRunner = class WorkflowRunner {
3518
3992
  this.#scheduler = scheduler;
3519
3993
  }
3520
3994
  execute(target, options) {
3521
- if (this.#isWorkflow(target)) {
3995
+ if (isWorkflowInterface(target)) {
3522
3996
  const signal = options?.signal;
3523
3997
  const timeout = options?.timeout;
3524
3998
  const budget = options?.budget;
@@ -3531,7 +4005,7 @@ var WorkflowRunner = class WorkflowRunner {
3531
4005
  const timeout = options?.timeout;
3532
4006
  const budget = options?.budget;
3533
4007
  const store = options?.store;
3534
- const workflow = new Workflow(definitionToSnapshot(target, captured.bail ?? target.bail ?? false), captured);
4008
+ const workflow = createWorkflowTree(target, captured);
3535
4009
  this.#acquire(workflow);
3536
4010
  return this.#execute(workflow, signal, timeout, budget, store);
3537
4011
  }
@@ -3545,7 +4019,7 @@ var WorkflowRunner = class WorkflowRunner {
3545
4019
  WorkflowRunner.#executions.add(workflow);
3546
4020
  }
3547
4021
  async #execute(workflow, signal, ms, budget, store) {
3548
- const holder = { runner: void 0 };
4022
+ const holder = new RunHolder();
3549
4023
  let timeout;
3550
4024
  let persistence;
3551
4025
  let runSignal;
@@ -3560,7 +4034,7 @@ var WorkflowRunner = class WorkflowRunner {
3560
4034
  if (runSignal.aborted) onCancel();
3561
4035
  else runSignal.addEventListener("abort", onCancel, { once: true });
3562
4036
  if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
3563
- if (this.#stoppable(workflow)) workflow.stop();
4037
+ if (isStoppable(workflow)) workflow.stop();
3564
4038
  this.#skipFrom(workflow.phases.phases(), 0);
3565
4039
  }
3566
4040
  let index = 0;
@@ -3572,12 +4046,12 @@ var WorkflowRunner = class WorkflowRunner {
3572
4046
  index += 1;
3573
4047
  continue;
3574
4048
  }
3575
- if (this.#cancelled(runSignal) || this.#halted(workflow)) {
4049
+ if (runSignal.aborted || isHalted(workflow)) {
3576
4050
  this.#haltFrom(phases, index, workflow, runSignal);
3577
4051
  break;
3578
4052
  }
3579
4053
  if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
3580
- if (this.#cancelled(runSignal) || this.#halted(workflow)) {
4054
+ if (runSignal.aborted || isHalted(workflow)) {
3581
4055
  this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
3582
4056
  break;
3583
4057
  }
@@ -3592,10 +4066,10 @@ var WorkflowRunner = class WorkflowRunner {
3592
4066
  }
3593
4067
  index += 1;
3594
4068
  const remaining = workflow.phases.phases();
3595
- if (index < remaining.length && !this.#cancelled(runSignal)) await this.#pace(runSignal);
4069
+ if (index < remaining.length && !runSignal.aborted) await this.#pace(runSignal);
3596
4070
  }
3597
- if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
3598
- else if (this.#completable(workflow)) workflow.complete();
4071
+ if (runSignal.aborted) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
4072
+ else if (isCompletable(workflow)) workflow.complete();
3599
4073
  const durable = await persistence?.finalize();
3600
4074
  return {
3601
4075
  workflow,
@@ -3605,7 +4079,7 @@ var WorkflowRunner = class WorkflowRunner {
3605
4079
  ...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
3606
4080
  };
3607
4081
  } catch (error) {
3608
- if (this.#stoppable(workflow)) workflow.stop();
4082
+ if (isStoppable(workflow)) workflow.stop();
3609
4083
  this.#skipFrom(workflow.phases.phases(), 0);
3610
4084
  await persistence?.finalize();
3611
4085
  throw error;
@@ -3640,22 +4114,22 @@ var WorkflowRunner = class WorkflowRunner {
3640
4114
  entries: this.#entry.bind(this),
3641
4115
  handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
3642
4116
  });
3643
- holder.runner = created;
4117
+ holder.hold(created);
3644
4118
  try {
3645
4119
  await created.execute(tasks);
3646
4120
  return false;
3647
4121
  } catch {
3648
- return !this.#cancelled(runSignal);
4122
+ return !runSignal.aborted;
3649
4123
  } finally {
3650
4124
  try {
3651
4125
  await created.destroy();
3652
4126
  } finally {
3653
- holder.runner = void 0;
4127
+ holder.hold();
3654
4128
  }
3655
4129
  }
3656
4130
  } finally {
3657
4131
  phase.emitter.off("add", onAdd);
3658
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4132
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3659
4133
  for (const task of phase.tasks.tasks()) this.#skip(task);
3660
4134
  }
3661
4135
  }
@@ -3679,7 +4153,7 @@ var WorkflowRunner = class WorkflowRunner {
3679
4153
  attempts.set(task.id, attempt);
3680
4154
  const last = attempt > Math.max(0, task.retries ?? 0);
3681
4155
  if (task.status !== "pending" && task.status !== "running") return;
3682
- if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
4156
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3683
4157
  this.#settleCancelled(task, workflow, runSignal);
3684
4158
  return;
3685
4159
  }
@@ -3692,15 +4166,15 @@ var WorkflowRunner = class WorkflowRunner {
3692
4166
  owners.set(task.id, attempt);
3693
4167
  deadline?.start();
3694
4168
  const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
3695
- if (!this.#owns(owners, task, attempt)) return;
4169
+ if (!ownsAttempt(owners, task, attempt)) return;
3696
4170
  if (!durable) {
3697
- if (this.#stoppable(workflow)) workflow.stop();
4171
+ if (isStoppable(workflow)) workflow.stop();
3698
4172
  return;
3699
4173
  }
3700
- if (task.run !== void 0 && task.handler === void 0) {
3701
- const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved run '${task.run}'`, {
4174
+ if (task.behavior !== void 0 && task.handler === void 0) {
4175
+ const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved behavior '${task.behavior}'`, {
3702
4176
  task: task.id,
3703
- run: task.run
4177
+ behavior: task.behavior
3704
4178
  });
3705
4179
  task.fail({
3706
4180
  origin: "handler",
@@ -3712,21 +4186,21 @@ var WorkflowRunner = class WorkflowRunner {
3712
4186
  if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3713
4187
  if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3714
4188
  if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3715
- if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
4189
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3716
4190
  this.#settleCancelled(task, workflow, runSignal);
3717
4191
  return;
3718
4192
  }
3719
4193
  if (task.status !== "running") return;
3720
- const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) => this.#owns(owners, task, attempt) && !signal.aborted ? task.report(input) : failure(new WorkflowError("TRANSITION", `task '${task.id}' attempt '${attempt}' no longer owns activity`, {
4194
+ const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) => ownsAttempt(owners, task, attempt) && !signal.aborted ? task.report(input) : failure(new WorkflowError("TRANSITION", `task '${task.id}' attempt '${attempt}' no longer owns activity`, {
3721
4195
  task: task.id,
3722
4196
  attempt
3723
- })), () => this.#owns(owners, task, attempt) && !signal.aborted && task.pulse());
4197
+ })), () => ownsAttempt(owners, task, attempt) && !signal.aborted && task.pulse());
3724
4198
  let outcome;
3725
4199
  try {
3726
- outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, this.#skipping.bind(this, task, controller, runSignal));
4200
+ outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, () => isSkipping(task, controller, runSignal));
3727
4201
  } catch (error) {
3728
- if (!this.#owns(owners, task, attempt)) return;
3729
- if (task.status !== "running" || this.#skipping(task, controller, runSignal)) {
4202
+ if (!ownsAttempt(owners, task, attempt)) return;
4203
+ if (task.status !== "running" || isSkipping(task, controller, runSignal)) {
3730
4204
  this.#settleCancelled(task, workflow, runSignal);
3731
4205
  return;
3732
4206
  }
@@ -3737,13 +4211,13 @@ var WorkflowRunner = class WorkflowRunner {
3737
4211
  this.#failed(owners, task, attempt, error, last, bail);
3738
4212
  return;
3739
4213
  }
3740
- if (!this.#owns(owners, task, attempt)) return;
4214
+ if (!ownsAttempt(owners, task, attempt)) return;
3741
4215
  if (!outcome[0]) {
3742
4216
  this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
3743
4217
  return;
3744
4218
  }
3745
4219
  if (task.status !== "running") return;
3746
- if (this.#skipping(task, controller, runSignal)) {
4220
+ if (isSkipping(task, controller, runSignal)) {
3747
4221
  this.#settleCancelled(task, workflow, runSignal);
3748
4222
  return;
3749
4223
  }
@@ -3751,32 +4225,32 @@ var WorkflowRunner = class WorkflowRunner {
3751
4225
  this.#timedOut(owners, task, attempt, last, bail);
3752
4226
  return;
3753
4227
  }
3754
- if (!this.#owns(owners, task, attempt)) return;
4228
+ if (!ownsAttempt(owners, task, attempt)) return;
3755
4229
  try {
3756
4230
  task.complete(outcome[1]);
3757
4231
  } catch (error) {
3758
- if (!this.#owns(owners, task, attempt)) return;
4232
+ if (!ownsAttempt(owners, task, attempt)) return;
3759
4233
  if (task.status !== "running") throw error;
3760
4234
  this.#failed(owners, task, attempt, error, last, bail);
3761
4235
  }
3762
4236
  } finally {
3763
4237
  deadline?.clear();
3764
- if (persistence !== void 0 && this.#owns(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && this.#stoppable(workflow)) workflow.stop();
4238
+ if (persistence !== void 0 && ownsAttempt(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && isStoppable(workflow)) workflow.stop();
3765
4239
  this.#revoke(owners, task.id, attempt);
3766
4240
  }
3767
4241
  }
3768
4242
  async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
3769
- const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, this.#skipping.bind(this, task, controller, runSignal), workflow, task.phase);
4243
+ const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, () => isSkipping(task, controller, runSignal), workflow, task.phase);
3770
4244
  return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
3771
4245
  }
3772
4246
  #settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
3773
- if (attempts.get(task.id) !== attempt || !this.#owns(owners, task, attempt)) return true;
4247
+ if (attempts.get(task.id) !== attempt || !ownsAttempt(owners, task, attempt)) return true;
3774
4248
  if (signal.aborted) {
3775
- if (genuine ?? this.#skipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
4249
+ if (genuine ?? isSkipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
3776
4250
  else this.#timedOut(owners, task, attempt, last, bail);
3777
4251
  return true;
3778
4252
  }
3779
- if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
4253
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3780
4254
  this.#settleCancelled(task, workflow, runSignal);
3781
4255
  return true;
3782
4256
  }
@@ -3805,7 +4279,7 @@ var WorkflowRunner = class WorkflowRunner {
3805
4279
  ]);
3806
4280
  }
3807
4281
  #timedOut(owners, task, attempt, last, bail) {
3808
- if (!this.#owns(owners, task, attempt)) return;
4282
+ if (!ownsAttempt(owners, task, attempt)) return;
3809
4283
  const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
3810
4284
  if (last) task.fail({
3811
4285
  origin: "timeout",
@@ -3814,7 +4288,7 @@ var WorkflowRunner = class WorkflowRunner {
3814
4288
  if (!last || bail) throw error;
3815
4289
  }
3816
4290
  #failed(owners, task, attempt, error, last, bail) {
3817
- if (!this.#owns(owners, task, attempt)) return;
4291
+ if (!ownsAttempt(owners, task, attempt)) return;
3818
4292
  if (!last) throw error;
3819
4293
  task.fail({
3820
4294
  origin: "handler",
@@ -3822,9 +4296,6 @@ var WorkflowRunner = class WorkflowRunner {
3822
4296
  });
3823
4297
  if (bail) throw error;
3824
4298
  }
3825
- #owns(owners, task, attempt) {
3826
- return owners.get(task.id) === attempt && task.attempts === attempt;
3827
- }
3828
4299
  #revoke(owners, id, attempt) {
3829
4300
  if (owners.get(id) === attempt) owners.delete(id);
3830
4301
  }
@@ -3839,7 +4310,7 @@ var WorkflowRunner = class WorkflowRunner {
3839
4310
  phase?.emitter.on("skip", onTerminal);
3840
4311
  phase?.emitter.on("stop", onTerminal);
3841
4312
  try {
3842
- if (workflow !== void 0 && this.#halted(workflow, phase)) deferred.resolve(void 0);
4313
+ if (workflow !== void 0 && isHalted(workflow, phase)) deferred.resolve(void 0);
3843
4314
  const outcome = await Promise.race([wait, deferred.promise]);
3844
4315
  return typeof outcome === "boolean" ? outcome : void 0;
3845
4316
  } finally {
@@ -3870,7 +4341,7 @@ var WorkflowRunner = class WorkflowRunner {
3870
4341
  return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
3871
4342
  }
3872
4343
  #haltFrom(phases, index, workflow, runSignal) {
3873
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4344
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3874
4345
  this.#skipFrom(phases, index);
3875
4346
  }
3876
4347
  #skipFrom(phases, index) {
@@ -3881,37 +4352,17 @@ var WorkflowRunner = class WorkflowRunner {
3881
4352
  }
3882
4353
  }
3883
4354
  #settleCancelled(task, workflow, runSignal) {
3884
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4355
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3885
4356
  this.#skip(task);
3886
4357
  }
3887
4358
  #skip(task) {
3888
4359
  if (task.status === "pending" || task.status === "running") task.skip();
3889
4360
  }
3890
- #skipping(task, controller, runSignal) {
3891
- return task.signal.aborted || controller.aborted || runSignal.aborted;
3892
- }
3893
- #cancelled(runSignal) {
3894
- return runSignal.aborted;
3895
- }
3896
- #halted(workflow, phase) {
3897
- const status = workflow.status;
3898
- return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
3899
- }
3900
- #stoppable(workflow) {
3901
- const status = workflow.status;
3902
- return status !== "failed" && status !== "stopped";
3903
- }
3904
- #completable(workflow) {
3905
- return workflow.status === "pending";
3906
- }
3907
- #isWorkflow(target) {
3908
- return "destroyed" in target && "snapshot" in target && typeof target.snapshot === "function";
3909
- }
3910
4361
  };
3911
4362
  //#endregion
3912
4363
  //#region src/core/factories.ts
3913
4364
  /**
3914
- * Compile the workflow definition contract — the JSON Schema, guard, parser, and
4365
+ * Compiles the workflow definition contract — the JSON Schema, guard, parser, and
3915
4366
  * seeded generator for a {@link WorkflowDefinition}, all derived from one shape and
3916
4367
  * kept in lockstep.
3917
4368
  *
@@ -3941,23 +4392,23 @@ function createWorkflowContract() {
3941
4392
  return (0, _orkestrel_contract.createContract)(workflowShape);
3942
4393
  }
3943
4394
  /**
3944
- * Build the live W-b entity tree from a {@link WorkflowDefinition} — the whole
4395
+ * Builds the live W-b entity tree from a {@link WorkflowDefinition} — the whole
3945
4396
  * {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
3946
4397
  * {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
3947
- * context, its emitter, and the cascade.
4398
+ * context, its emitter, and the cascade, and every node born `pending`.
3948
4399
  *
3949
4400
  * @remarks
3950
- * The definition is the DECLARATIVE blueprint; this seeds an initial all-`pending`
4401
+ * The definition is the declarative blueprint; this seeds an initial all-`pending`
3951
4402
  * {@link WorkflowSnapshot} from it ({@link definitionToSnapshot}) and constructs the live
3952
4403
  * tree over that one path. The `bail` failure policy resolves to `options.bail`, else the
3953
4404
  * definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
3954
4405
  * feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
3955
4406
  * listeners + metadata travel through `options.phases[id].on` /
3956
- * `options.phases[id].tasks[id]` (the AGENTS §8 nested-by-id bag). The W-b tree is the
3957
- * state machine ONLY — it does not execute tasks (W-c drives the transitions).
4407
+ * `options.phases[id].tasks[id]` (the nested-by-id bag). The W-b tree is the
4408
+ * state machine only — it does not execute tasks (W-c drives the transitions).
3958
4409
  *
3959
- * `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
3960
- * task's `run` name resolves against ONCE at construction into its runtime
4410
+ * `options.functions` is the {@link import('./types.js').WorkflowRegistry} registry each live
4411
+ * task's `behavior` name resolves against once at construction into its runtime
3961
4412
  * {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
3962
4413
  * an unresolved present name remains inspectable but is rejected if execution is attempted.
3963
4414
  *
@@ -3975,26 +4426,62 @@ function createWorkflowContract() {
3975
4426
  * ```
3976
4427
  */
3977
4428
  function createWorkflow(definition, options) {
3978
- const captured = captureWorkflowOptions(options);
3979
- return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
4429
+ return createWorkflowTree(definition, captureWorkflowOptions(options));
4430
+ }
4431
+ /**
4432
+ * Builds the live entity tree one definition and one owned options bag describe — the shared
4433
+ * construction path behind every definition-driven mint.
4434
+ *
4435
+ * @remarks
4436
+ * Seeds an initial all-`pending` {@link WorkflowSnapshot} from the definition and constructs the
4437
+ * live {@link WorkflowInterface} over it. `bail` is the caller's own override, forwarded to
4438
+ * {@link definitionToSnapshot} so it reaches both tiers: the workflow snapshot and the inheritance
4439
+ * default of every phase that declares no `bail` of its own, while a phase declaring one still
4440
+ * wins. Omitted, the definition's own `bail` governs, defaulting to the graceful
4441
+ * {@link import('./constants.js').DEFAULT_BAIL}.
4442
+ *
4443
+ * `captured` is forwarded to the entity unchanged — its own `bail` is deliberately not replaced
4444
+ * with the resolved policy, because the snapshot already carries the resolved value at both tiers
4445
+ * and an injected one would make `Workflow` read it as an explicit uniform override and clobber
4446
+ * the per-phase overrides. Each task's `behavior` / `retries` / `timeout` travel onto the snapshot
4447
+ * too, so `captured.functions` resolves every handler identically whether the tree is built fresh
4448
+ * or restored. Pass a bag {@link captureWorkflowOptions} already owns: this constructs over it
4449
+ * without re-capturing.
4450
+ *
4451
+ * @param definition - The workflow definition to bring to life
4452
+ * @param captured - The already-owned {@link WorkflowOptions} bag the entity is constructed with,
4453
+ * whose `bail` is the caller's failure-policy override, or `undefined` to take the definition's
4454
+ * @returns The live {@link WorkflowInterface} root
4455
+ *
4456
+ * @example
4457
+ * ```ts
4458
+ * import { captureWorkflowOptions, createWorkflowTree } from '@orkestrel/workflow'
4459
+ *
4460
+ * const captured = captureWorkflowOptions({ bail: true })
4461
+ * const workflow = createWorkflowTree(definition, captured)
4462
+ * workflow.bail // true — the override reached the workflow and every inheriting phase
4463
+ * ```
4464
+ */
4465
+ function createWorkflowTree(definition, captured) {
4466
+ return new Workflow(definitionToSnapshot(definition, captured.bail), captured);
3980
4467
  }
3981
4468
  /**
3982
- * Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
4469
+ * Builds an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3983
4470
  * inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
3984
4471
  * + recorded results + positional order + the persisted `#override`.
3985
4472
  *
3986
4473
  * @remarks
3987
4474
  * Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
3988
- * same status at every node (each `#override` restored DIRECTLY from the snapshot's own
4475
+ * same status at every node (each `#override` restored directly from the snapshot's own
3989
4476
  * `override` field, not guessed from a status divergence), the same recorded
3990
4477
  * {@link import('./types.js').TaskResult}s, and the same positional order (an interior
3991
- * `skip` / `remove` survives). The snapshot is SELF-CONTAINED — it persists the `bail`
3992
- * policy it ran under, so the restore re-derives status IDENTICALLY without a silent
4478
+ * `skip` / `remove` survives). The snapshot is self-contained — it persists the `bail`
4479
+ * policy it ran under, so the restore re-derives status identically without a silent
3993
4480
  * default; the snapshot's `bail` is the source of truth, while an explicit `options.bail`
3994
4481
  * still wins when supplied (to deliberately re-run under a different policy). A structurally
3995
4482
  * invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
3996
4483
  * non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
3997
- * Runtime handlers are optional: without a matching `functions` entry, a persisted `run`
4484
+ * Runtime handlers are optional: without a matching `functions` entry, a persisted `behavior`
3998
4485
  * remains visible with an undefined `handler` so the exact state is inspectable. The runner
3999
4486
  * rejects that unresolved tree if execution is attempted.
4000
4487
  *
@@ -4015,10 +4502,11 @@ function createRestoredWorkflow(snapshot, options) {
4015
4502
  return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
4016
4503
  }
4017
4504
  /**
4018
- * Build an interrupted workflow back to life at its remaining retry budget.
4505
+ * Builds an interrupted workflow back to life at its remaining retry budget, normalizing a
4506
+ * leaf whose attempts are exhausted into a recovery failure.
4019
4507
  *
4020
4508
  * @remarks
4021
- * Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
4509
+ * Each phase captures every unique initial `behavior` binding once before constructing tasks. Recovery
4022
4510
  * validates those live tasks' captured callable handlers without rereading the registry, while the
4023
4511
  * retained registry identity remains available to resolve future live additions at their mint time.
4024
4512
  *
@@ -4039,22 +4527,22 @@ function createRecoveredWorkflow(snapshot, options) {
4039
4527
  const owned = cloneWorkflowSnapshot(snapshot);
4040
4528
  if (owned.override !== void 0 || owned.phases.some((phase) => phase.override !== void 0)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has a terminal override`, { workflow: owned.id });
4041
4529
  const workflow = new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), captured);
4042
- if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved run`, { workflow: owned.id });
4530
+ if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved behavior`, { workflow: owned.id });
4043
4531
  return workflow;
4044
4532
  }
4045
4533
  /**
4046
- * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4047
- * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
4048
- * backend behind the W-d persistence seam.
4534
+ * Creates the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4535
+ * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the default
4536
+ * backend behind the W-d persistence seam. It takes no options and expires nothing: a
4537
+ * persisted run state lives until an explicit `delete`.
4049
4538
  *
4050
4539
  * @remarks
4051
4540
  * The snapshot analogue of the server package's `createMemorySessionStore`
4052
- * (and the `createMemoryQueueStore` family), but LEANER there is no idle-TTL, so no
4053
- * options bag (AGENTS §21 minimal): a persisted run-state lives until an explicit `delete`. This is
4054
- * the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
4541
+ * (and the `createMemoryQueueStore` family) is the zero-plumbing default (a plain `Map`); its
4542
+ * driver-pluggable twin is
4055
4543
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
4056
- * table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
4057
- * driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
4544
+ * table) — for a durable store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
4545
+ * driver, and it swaps in without touching the runner or the entity tree. Restore stays a caller
4058
4546
  * concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
4059
4547
  *
4060
4548
  * @returns A memory-backed {@link WorkflowStoreInterface}
@@ -4074,23 +4562,24 @@ function createMemoryWorkflowStore() {
4074
4562
  return new MemoryWorkflowStore();
4075
4563
  }
4076
4564
  /**
4077
- * Create a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
4565
+ * Creates a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
4078
4566
  * driver-pluggable backing for the W-d persistence seam, the opt-in twin of
4079
- * {@link createMemoryWorkflowStore}.
4567
+ * {@link createMemoryWorkflowStore}. It holds the snapshot as one opaque JSON column, and its
4568
+ * `driver` defaults to memory, so it works before any durable driver is passed.
4080
4569
  *
4081
4570
  * @remarks
4082
- * Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver, the snapshot
4083
- * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
4571
+ * Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver. The column map
4572
+ * is `{ id; snapshot }` where `snapshot` is a
4084
4573
  * `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
4085
- * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
4086
- * AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
4574
+ * snapshot is already a complete, self-contained, pure-JSON payload, so storing it whole is lossless
4575
+ * and keeps the row type flat — a structured multi-column snapshot table would force the contract to
4087
4576
  * `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
4088
- * the opaque column sidesteps it (the column reads back as `unknown`, narrowed on `get` by
4089
- * {@link import('./helpers.js').isWorkflowSnapshot}). The `driver` DEFAULTS to
4090
- * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
4091
- * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4577
+ * the opaque column sidesteps it (the column reads back as `unknown`, owned and narrowed on `get` by
4578
+ * {@link cloneWorkflowSnapshot}, whose semantic pass is
4579
+ * {@link import('./validators.js').isOwnedWorkflowSnapshot}). Pass a server `createJSONDriver` /
4580
+ * `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4092
4581
  * the durability is the driver's job, the store engine is shared. It swaps in behind
4093
- * {@link WorkflowStoreInterface} WITHOUT touching the runner or the entity tree.
4582
+ * {@link WorkflowStoreInterface} without touching the runner or the entity tree.
4094
4583
  *
4095
4584
  * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
4096
4585
  * @returns A {@link WorkflowStoreInterface} over the driver
@@ -4118,65 +4607,91 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
4118
4607
  }).table("snapshots"));
4119
4608
  }
4120
4609
  /**
4121
- * Create a workflow runner — a {@link WorkflowRunnerInterface} that EXECUTES a live W-b
4122
- * workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent,
4123
- * each task dispatched through its OWN resolved handler under the workflow's `bail` policy.
4610
+ * Creates the thin orchestrator — a {@link WorkflowRunnerInterface} that executes a live W-b
4611
+ * workflow tree by composing the shipped substrate: phases sequential, tasks concurrent, each
4612
+ * task dispatched through its own resolved handler under the workflow's `bail` policy. The
4613
+ * engine is pure — it carries no behavior or provider registry, and its only option is the
4614
+ * scheduler it paces phase boundaries with.
4124
4615
  *
4125
4616
  * @remarks
4126
- * The runner is a PURE engine it re-implements no concurrency / retry / abort logic, AND it
4127
- * carries no behavior or provider registry of its own: each live task already
4128
- * resolved its own {@link import('./types.js').WorkflowFunction} into
4129
- * {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
4617
+ * Each live task already resolved its own {@link import('./types.js').WorkflowFunction} into
4618
+ * {@link import('./types.js').TaskInterface.handler} once at construction, from the
4130
4619
  * {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
4131
4620
  * Per-phase bounded concurrency is one {@link createRunner} per phase; `bail` maps onto that
4132
4621
  * Runner's fail-fast (`true` — the first failure aborts the in-flight siblings + skips the
4133
4622
  * rest) vs settle-all (`false` — failures are recorded, the run finishes); the run-level abort
4134
4623
  * / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
4135
4624
  * `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
4136
- * `execute(definition, options?)` BUILDS the live tree from the definition itself (via
4625
+ * `execute(definition, options?)` builds the live tree from the definition itself (through
4137
4626
  * {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
4138
4627
  * the live entity (`start` → `complete` / `fail`), and resolves a
4139
4628
  * {@link import('./types.js').WorkflowResult}.
4140
4629
  *
4141
4630
  * External integrations remain application-owned: a caller wires an ordinary
4142
4631
  * {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
4143
- * registry. Only a task that omits `run` auto-completes; unresolved named work is rejected
4632
+ * registry. Only a task that omits `behavior` auto-completes; unresolved named work is rejected
4144
4633
  * before dispatch.
4145
4634
  *
4146
4635
  * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
4147
4636
  * See {@link WorkflowRunnerOptions}.
4148
4637
  * @returns A working {@link WorkflowRunnerInterface}
4149
4638
  *
4150
- * @example
4639
+ * @example Author a definition and run it
4151
4640
  * ```ts
4152
4641
  * import { createWorkflowRunner } from '@orkestrel/workflow'
4642
+ * import type { WorkflowDefinition } from '@orkestrel/workflow'
4643
+ *
4644
+ * const definition: WorkflowDefinition = {
4645
+ * id: 'release',
4646
+ * name: 'Release',
4647
+ * phases: [
4648
+ * {
4649
+ * id: 'build',
4650
+ * name: 'Build',
4651
+ * tasks: [
4652
+ * { id: 'compile', name: 'Compile', behavior: 'compile' },
4653
+ * { id: 'lint', name: 'Lint', behavior: 'lint' },
4654
+ * ],
4655
+ * },
4656
+ * {
4657
+ * id: 'ship',
4658
+ * name: 'Ship',
4659
+ * tasks: [{ id: 'publish', name: 'Publish', behavior: 'publish' }],
4660
+ * },
4661
+ * ],
4662
+ * }
4663
+ *
4664
+ * const runner = createWorkflowRunner() // a pure engine — no registries
4153
4665
  *
4154
- * const runner = createWorkflowRunner()
4155
- * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
4156
- * { id: 't', name: 'T', run: 'compile' },
4157
- * ] }] }
4158
4666
  * const result = await runner.execute(definition, {
4159
- * functions: { compile: async (controller) => `built ${controller.task.id}` },
4667
+ * functions: {
4668
+ * compile: async (controller) => `built ${controller.task.id}`,
4669
+ * lint: async () => 'clean',
4670
+ * publish: async () => 'published',
4671
+ * },
4160
4672
  * })
4161
4673
  * result.status // 'completed'
4162
- * result.workflow.phase('p')?.task('t')?.status // 'completed'
4674
+ * result.workflow.phase('build')?.task('compile')?.status // 'completed'
4675
+ * result.results // every settled task's TaskResult, in positional order
4163
4676
  * ```
4164
4677
  */
4165
4678
  function createWorkflowRunner(options) {
4166
4679
  return new WorkflowRunner(options?.scheduler ?? createScheduler());
4167
4680
  }
4168
4681
  /**
4169
- * Create a {@link WorkflowManagerInterface} — the store-backed registry of
4682
+ * Creates a {@link WorkflowManagerInterface} — the store-backed registry of
4170
4683
  * {@link WorkflowInterface}s, the additive manager tier mirroring the `@orkestrel/agent`
4171
- * line's `createConversationManager` / `createWorkspaceManager`.
4684
+ * line's `createConversationManager` / `createWorkspaceManager`. The returned registry makes
4685
+ * hydrated named work runnable when `options.functions` is supplied, and leaves it
4686
+ * inspectable when it is not.
4172
4687
  *
4173
4688
  * @remarks
4174
- * `options.functions` flows into every workflow the manager mints (`add`, via
4175
- * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
4176
- * {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4177
- * mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
4689
+ * `options.functions` flows into every workflow the manager mints (`add`, through
4690
+ * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, through
4691
+ * {@link createRestoredWorkflow}), so a hydrated workflow is runnable rather than a dead snapshot
4692
+ * mirror. `options.store` is the exact analogue of the twins' `store` seam — omitted ⇒ the
4178
4693
  * manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
4179
- * is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
4694
+ * is purely additive: direct {@link WorkflowStoreInterface} use and
4180
4695
  * {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
4181
4696
  * seam, not a replacement.
4182
4697
  *
@@ -4191,7 +4706,7 @@ function createWorkflowRunner(options) {
4191
4706
  * store: createMemoryWorkflowStore(),
4192
4707
  * functions: { compile: async (controller) => `built ${controller.task.id}` },
4193
4708
  * })
4194
- * const workflow = manager.add(definition) // minted, registered, RUNNABLE
4709
+ * const workflow = manager.add(definition) // minted, registered, runnable
4195
4710
  * await manager.save(workflow.id) // persisted to the store
4196
4711
  * const reopened = await manager.open(workflow.id) // already registered — no store hit
4197
4712
  * ```
@@ -4200,12 +4715,12 @@ function createWorkflowManager(options) {
4200
4715
  return new WorkflowManager(options);
4201
4716
  }
4202
4717
  /**
4203
- * Create the safe cross-environment cooperative-yield default — a
4718
+ * Creates the safe cross-environment cooperative-yield default — a
4204
4719
  * {@link SchedulerInterface} built on `setTimeout` / `clearTimeout` alone, so it
4205
4720
  * runs unchanged in both the browser and Node.
4206
4721
  *
4207
4722
  * @remarks
4208
- * `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
4723
+ * `yield()` gives the host a turn through a zero-delay macrotask (so pending I/O,
4209
4724
  * timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
4210
4725
  * after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
4211
4726
  * with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
@@ -4246,25 +4761,26 @@ function createScheduler() {
4246
4761
  return new Scheduler();
4247
4762
  }
4248
4763
  /**
4249
- * Create a thin generic orchestrator that drives declared units — and any they
4250
- * `spawn` — through a bounded-concurrency queue, collecting their results in order.
4764
+ * Creates a thin generic orchestrator that drives declared units — and any they
4765
+ * `spawn` — through a bounded-concurrency queue, collecting their results in order and
4766
+ * failing the run fast on the first genuine unit failure.
4251
4767
  *
4252
4768
  * @remarks
4253
4769
  * The Runner composes the workers `Queue` for backpressure, FIFO ordering, bounded
4254
4770
  * concurrency, retries, and the per-attempt timeout — it adds only orchestration, not
4255
- * a second concurrency engine. `execute(inputs)` runs the unit set ONCE (a second call
4771
+ * a second concurrency engine. `execute(inputs)` runs the unit set once (a second call
4256
4772
  * throws) and resolves the units' results in order: the declared inputs first, then
4257
4773
  * any `spawn`ed siblings in spawn order. Each unit's handler gets a `Controller` — its
4258
4774
  * `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
4259
4775
  * or the attempt's timeout, a promise-parked `wait()`, and `spawn(input)` to fan out
4260
4776
  * sibling units. The run is **fail-fast**: the first unit failure (after retries)
4261
- * aborts every other unit and rejects `execute` with that error. **Observable (§13):** a
4777
+ * aborts every other unit and rejects `execute` with that error. **Observable:** a
4262
4778
  * typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
4263
4779
  *
4264
- * Because `spawn` is fire-and-track (the runner awaits the whole spawn closure via an
4265
- * outstanding-unit count, not a one-time snapshot), a handler need NOT await its spawns
4266
- * for them to run — and on a bounded runner it should NOT `await` a spawn inline (a
4267
- * slot-holding handler awaiting its own spawn can deadlock); fan out and return instead.
4780
+ * Because `spawn` is fire-and-track (the runner awaits the whole spawn closure through an
4781
+ * outstanding-unit count, not a one-time snapshot), a handler need not await its spawns
4782
+ * for them to run — and on a bounded runner do not `await` a spawn inline (a slot-holding
4783
+ * handler awaiting its own spawn can deadlock); fan out and return instead.
4268
4784
  *
4269
4785
  * @typeParam TInput - The work input each unit carries
4270
4786
  * @typeParam TResult - The value a unit's handler resolves
@@ -4293,24 +4809,22 @@ function createRunner(options) {
4293
4809
  return new Runner(options);
4294
4810
  }
4295
4811
  //#endregion
4296
- exports.Controller = Controller;
4812
+ exports.Collection = Collection;
4297
4813
  exports.DEFAULT_BAIL = DEFAULT_BAIL;
4298
4814
  exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
4299
4815
  exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
4816
+ exports.LIFECYCLE_STATUSES = LIFECYCLE_STATUSES;
4300
4817
  exports.MAX_TIMER_MS = MAX_TIMER_MS;
4301
4818
  exports.MemoryWorkflowStore = MemoryWorkflowStore;
4302
- exports.PHASE_STATUSES = PHASE_STATUSES;
4303
- exports.Phase = Phase;
4819
+ exports.PERSISTED_NODE_EVENTS = PERSISTED_NODE_EVENTS;
4820
+ exports.PERSISTED_TASK_EVENTS = PERSISTED_TASK_EVENTS;
4304
4821
  exports.PhaseManager = PhaseManager;
4822
+ exports.RunHolder = RunHolder;
4305
4823
  exports.Runner = Runner;
4306
4824
  exports.Scheduler = Scheduler;
4307
- exports.TASK_STATUSES = TASK_STATUSES;
4308
4825
  exports.TASK_TRANSITIONS = TASK_TRANSITIONS;
4309
- exports.TERMINAL_TASK_STATUSES = TERMINAL_TASK_STATUSES;
4310
- exports.Task = Task;
4311
- exports.TaskController = TaskController;
4826
+ exports.TERMINAL_STATUSES = TERMINAL_STATUSES;
4312
4827
  exports.TaskManager = TaskManager;
4313
- exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
4314
4828
  exports.Workflow = Workflow;
4315
4829
  exports.WorkflowError = WorkflowError;
4316
4830
  exports.WorkflowManager = WorkflowManager;
@@ -4322,10 +4836,10 @@ exports.buildWorkflowContext = buildWorkflowContext;
4322
4836
  exports.canTransitionTask = canTransitionTask;
4323
4837
  exports.captureWorkflowOptions = captureWorkflowOptions;
4324
4838
  exports.cloneTaskActivity = cloneTaskActivity;
4839
+ exports.cloneTaskClaims = cloneTaskClaims;
4325
4840
  exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
4326
4841
  exports.collectResults = collectResults;
4327
4842
  exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
4328
- exports.createDeferred = createDeferred;
4329
4843
  exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
4330
4844
  exports.createRecoveredWorkflow = createRecoveredWorkflow;
4331
4845
  exports.createRestoredWorkflow = createRestoredWorkflow;
@@ -4335,7 +4849,9 @@ exports.createWorkflow = createWorkflow;
4335
4849
  exports.createWorkflowContract = createWorkflowContract;
4336
4850
  exports.createWorkflowManager = createWorkflowManager;
4337
4851
  exports.createWorkflowRunner = createWorkflowRunner;
4852
+ exports.createWorkflowTree = createWorkflowTree;
4338
4853
  exports.definitionToSnapshot = definitionToSnapshot;
4854
+ exports.delayHost = delayHost;
4339
4855
  exports.deriveBoundary = deriveBoundary;
4340
4856
  exports.derivePhaseStatus = derivePhaseStatus;
4341
4857
  exports.deriveWorkflowStatus = deriveWorkflowStatus;
@@ -4344,29 +4860,36 @@ exports.failure = failure;
4344
4860
  exports.findFailure = findFailure;
4345
4861
  exports.hasWorkflowHandlers = hasWorkflowHandlers;
4346
4862
  exports.insertEntry = insertEntry;
4863
+ exports.isCompletable = isCompletable;
4864
+ exports.isHalted = isHalted;
4347
4865
  exports.isLifecycleStatus = isLifecycleStatus;
4348
4866
  exports.isOwnedWorkflowSnapshot = isOwnedWorkflowSnapshot;
4867
+ exports.isSkipping = isSkipping;
4868
+ exports.isStoppable = isStoppable;
4349
4869
  exports.isTaskActivity = isTaskActivity;
4350
4870
  exports.isTaskActivityInput = isTaskActivityInput;
4871
+ exports.isTaskClaimList = isTaskClaimList;
4351
4872
  exports.isTaskFailure = isTaskFailure;
4352
4873
  exports.isTaskResult = isTaskResult;
4353
4874
  exports.isTerminalStatus = isTerminalStatus;
4354
4875
  exports.isWorkflowError = isWorkflowError;
4876
+ exports.isWorkflowInterface = isWorkflowInterface;
4355
4877
  exports.isWorkflowSnapshot = isWorkflowSnapshot;
4356
4878
  exports.matchesDescription = matchesDescription;
4357
4879
  exports.moveEntry = moveEntry;
4880
+ exports.ownsAttempt = ownsAttempt;
4358
4881
  exports.parkSignal = parkSignal;
4359
4882
  exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
4360
4883
  exports.phaseShape = phaseShape;
4361
4884
  exports.phaseUpdateShape = phaseUpdateShape;
4362
4885
  exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
4363
4886
  exports.resolveTaskSilence = resolveTaskSilence;
4887
+ exports.scanSnapshotContext = scanSnapshotContext;
4364
4888
  exports.scheduleHost = scheduleHost;
4365
4889
  exports.success = success;
4366
4890
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
4367
4891
  exports.taskShape = taskShape;
4368
4892
  exports.taskUpdateShape = taskUpdateShape;
4369
4893
  exports.workflowShape = workflowShape;
4370
- exports.workflowSnapshotContext = workflowSnapshotContext;
4371
4894
 
4372
4895
  //# sourceMappingURL=index.cjs.map