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