@orkestrel/workflow 0.0.15 → 0.0.17

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,67 @@
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 raised by the workflow runtime.
10
+ *
11
+ * @remarks
12
+ * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
13
+ * offending node id / status / parameter. Raised for an illegal lifecycle transition
14
+ * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
15
+ * boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), a host
16
+ * schedule refused before arming because the caller's `signal` is not a native
17
+ * `AbortSignal` (`SCHEDULE`, delivered as a rejected promise), or a broken internal
18
+ * invariant (`INVARIANT`).
19
+ */
20
+ var WorkflowError = class extends Error {
21
+ code;
22
+ context;
23
+ constructor(code, message, context) {
24
+ super(message);
25
+ this.name = "WorkflowError";
26
+ this.code = code;
27
+ if (context !== void 0) this.context = context;
28
+ }
29
+ };
30
+ /**
31
+ * Narrows an unknown caught value to a {@link WorkflowError}.
32
+ *
33
+ * @param value - The value to test (typically a `catch` binding)
34
+ * @returns True if `value` is a {@link WorkflowError}; false otherwise
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * try {
39
+ * task.complete('done')
40
+ * } catch (error) {
41
+ * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
42
+ * }
43
+ * ```
44
+ */
45
+ function isWorkflowError(value) {
46
+ try {
47
+ return value instanceof WorkflowError;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+ //#endregion
7
53
  //#region src/core/constants.ts
8
- /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
54
+ /** Names the default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
9
55
  var DEFAULT_BAIL = false;
10
56
  /**
11
- * Every {@link TaskStatus} value, frozen — the lifecycle vocabulary of a task.
57
+ * Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from.
12
58
  *
13
59
  * @remarks
14
60
  * Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
15
- * `stopped`). The source of truth for the union; compose guards / shapes from it.
61
+ * `stopped`). The runtime source of truth for the union:
62
+ * {@link import('./validators.js').isLifecycleStatus} reads this array.
16
63
  */
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([
64
+ var LIFECYCLE_STATUSES = Object.freeze([
36
65
  "pending",
37
66
  "running",
38
67
  "completed",
@@ -41,21 +70,21 @@ var WORKFLOW_STATUSES = Object.freeze([
41
70
  "stopped"
42
71
  ]);
43
72
  /**
44
- * The {@link TaskStatus} values that are TERMINAL — a task in one of these will
73
+ * Lists the {@link LifecycleStatus} values that are TERMINAL — a node in one of these will
45
74
  * not transition further, frozen.
46
75
  *
47
76
  * @remarks
48
77
  * The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
49
78
  * `pending` and `running` are the only non-terminal members.
50
79
  */
51
- var TERMINAL_TASK_STATUSES = Object.freeze([
80
+ var TERMINAL_STATUSES = Object.freeze([
52
81
  "completed",
53
82
  "failed",
54
83
  "skipped",
55
84
  "stopped"
56
85
  ]);
57
86
  /**
58
- * The legal {@link TaskStatus} transition graph of the live W-b task state machine —
87
+ * Declares the legal {@link LifecycleStatus} transition graph of the live W-b task state machine —
59
88
  * each current status mapped to the statuses it may move to directly, frozen.
60
89
  *
61
90
  * @remarks
@@ -85,7 +114,7 @@ var TASK_TRANSITIONS = Object.freeze({
85
114
  stopped: []
86
115
  });
87
116
  /**
88
- * The default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
117
+ * Names the default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
89
118
  * runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
90
119
  * throttle — a cap that is effectively unbounded for any realistic phase.
91
120
  *
@@ -105,74 +134,138 @@ var TASK_TRANSITIONS = Object.freeze({
105
134
  */
106
135
  var DEFAULT_PHASE_CONCURRENCY = 1024;
107
136
  /**
108
- * The largest delay representable by the host timer APIs without overflow or clamping.
137
+ * Names the largest delay representable by the host timer APIs without overflow or clamping.
109
138
  */
110
139
  var MAX_TIMER_MS = 2147483647;
140
+ /**
141
+ * Lists the {@link WorkflowEventMap} / {@link PhaseEventMap} events that make a durable observer
142
+ * re-persist the live tree, frozen.
143
+ *
144
+ * @remarks
145
+ * The two maps carry the same event names, so one list serves both tiers. It is the source of
146
+ * truth behind {@link import('./WorkflowPersistence.js').WorkflowPersistence}'s attach and detach
147
+ * passes: subscribing and unsubscribing loop over these names, so an added event reaches both
148
+ * passes from one edit. `add` and `remove` are deliberately absent — they carry the new or dropped
149
+ * child, so the persistence layer binds its own attaching handler to them instead.
150
+ */
151
+ var PERSISTED_NODE_EVENTS = Object.freeze([
152
+ "start",
153
+ "complete",
154
+ "fail",
155
+ "skip",
156
+ "stop",
157
+ "move",
158
+ "update"
159
+ ]);
160
+ /**
161
+ * Lists the {@link TaskEventMap} events that make a durable observer re-persist the live tree, frozen.
162
+ *
163
+ * @remarks
164
+ * The leaf counterpart of {@link PERSISTED_NODE_EVENTS}, and the source of truth behind the task
165
+ * attach and detach passes of
166
+ * {@link import('./WorkflowPersistence.js').WorkflowPersistence}. `report` and `pulse` join the
167
+ * lifecycle events because an accepted activity frame changes the persisted snapshot; a leaf has
168
+ * no children, so there is no structural event to bind separately.
169
+ */
170
+ var PERSISTED_TASK_EVENTS = Object.freeze([
171
+ "start",
172
+ "complete",
173
+ "fail",
174
+ "skip",
175
+ "stop",
176
+ "report",
177
+ "pulse"
178
+ ]);
111
179
  //#endregion
112
- //#region src/core/errors.ts
180
+ //#region src/core/validators.ts
113
181
  /**
114
- * An error raised by the workflow runtime.
182
+ * Checks whether an unknown value belongs to the workflow lifecycle vocabulary.
115
183
  *
116
184
  * @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).
185
+ * Reads {@link import('./constants.js').LIFECYCLE_STATUSES}, the runtime array every tier draws
186
+ * from, so the vocabulary has one definition rather than a hard-coded copy per guard.
187
+ *
188
+ * @param value - The value to test
189
+ * @returns True if `value` is a {@link LifecycleStatus}; false otherwise
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * isLifecycleStatus('running') // true
194
+ * isLifecycleStatus('paused') // false
195
+ * ```
123
196
  */
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
- };
197
+ function isLifecycleStatus(value) {
198
+ return LIFECYCLE_STATUSES.some((status) => status === value);
199
+ }
134
200
  /**
135
- * Narrow an unknown caught value to a {@link WorkflowError}.
201
+ * Tests a normalized persisted task failure.
136
202
  *
137
- * @param value - The value to test (typically a `catch` binding)
138
- * @returns `true` when `value` is a {@link WorkflowError}
203
+ * @remarks
204
+ * The exact-record guard behind a persisted {@link TaskFailure}: exactly `origin` and `message`,
205
+ * an `origin` drawn from the {@link import('./types.js').TaskFailureOrigin} vocabulary, and a
206
+ * non-empty `message`. Total — a hostile prototype or accessor answers `false` rather than
207
+ * throwing.
208
+ *
209
+ * @param value - The value to test
210
+ * @returns True if `value` is a persisted {@link TaskFailure}; false otherwise
139
211
  *
140
212
  * @example
141
213
  * ```ts
142
- * try {
143
- * task.complete('done')
144
- * } catch (error) {
145
- * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
146
- * }
214
+ * isTaskFailure({ origin: 'handler', message: 'boom' }) // true
215
+ * isTaskFailure({ origin: 'handler' }) // false
147
216
  * ```
148
217
  */
149
- function isWorkflowError(value) {
218
+ function isTaskFailure(value) {
150
219
  try {
151
- return value instanceof WorkflowError;
220
+ 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
221
  } catch {
153
222
  return false;
154
223
  }
155
224
  }
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) {
225
+ /**
226
+ * Checks whether an unknown value is a live workflow entity rather than a definition.
227
+ *
228
+ * @remarks
229
+ * The discriminator behind the overloaded
230
+ * {@link import('./types.js').WorkflowRunnerInterface.execute}: a
231
+ * {@link import('./types.js').WorkflowInterface} is the only one of the two carrying `destroyed`
232
+ * (RUNTIME-ONLY, never a field on the pure-JSON
233
+ * {@link import('./types.js').WorkflowDefinition}) AND a callable `snapshot`. Requiring both is
234
+ * sturdier than `destroyed` alone — a definition could coincidentally carry a `destroyed` field as
235
+ * arbitrary data, and pairing it with a function-typed `snapshot` narrows to the actual entity
236
+ * shape without an `as`. It reads a live class instance, so it tests object identity rather than a
237
+ * plain-record brand, and it is total: any other value answers `false`.
238
+ *
239
+ * @param value - The value to test
240
+ * @returns True if `value` is a live {@link WorkflowInterface}; false otherwise
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * isWorkflowInterface(createWorkflow(definition)) // true
245
+ * isWorkflowInterface(definition) // false
246
+ * ```
247
+ */
248
+ function isWorkflowInterface(value) {
164
249
  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);
250
+ return isObject(value) && "destroyed" in value && "snapshot" in value && isFunction(value.snapshot);
166
251
  } catch {
167
252
  return false;
168
253
  }
169
254
  }
170
255
  /**
171
- * Validate a safe owned JSON graph as a coherent workflow snapshot.
256
+ * Validates a safe owned JSON graph as a coherent workflow snapshot.
172
257
  *
173
258
  * @remarks
174
259
  * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
175
260
  * graph first so this semantic pass never observes accessors or prototypes.
261
+ *
262
+ * @param value - The already-owned JSON graph to validate
263
+ * @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * isOwnedWorkflowSnapshot(workflow.snapshot()) // true
268
+ * ```
176
269
  */
177
270
  function isOwnedWorkflowSnapshot(value) {
178
271
  try {
@@ -194,7 +287,7 @@ function isOwnedWorkflowSnapshot(value) {
194
287
  const statuses = [];
195
288
  if (phase.tasks.length > 0) vacuous = false;
196
289
  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;
290
+ 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
291
  const budget = (task.retries ?? 0) + 1;
199
292
  if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
200
293
  if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
@@ -222,13 +315,82 @@ function isOwnedWorkflowSnapshot(value) {
222
315
  return false;
223
316
  }
224
317
  }
225
- /** Total hostile-boundary workflow snapshot guard. */
318
+ /**
319
+ * Guards the hostile boundary totally for a workflow snapshot.
320
+ *
321
+ * @remarks
322
+ * Owns the value first through the exact-JSON clone of `@orkestrel/contract`, then runs the
323
+ * semantic pass {@link isOwnedWorkflowSnapshot} over the owned copy — so no accessor, prototype,
324
+ * or cycle in the caller's graph is ever observed by the semantic pass. Total: an unclonable
325
+ * value answers `false` rather than throwing.
326
+ *
327
+ * @param value - The untrusted value to test
328
+ * @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * isWorkflowSnapshot(JSON.parse(payload)) // true only for a coherent snapshot
333
+ * ```
334
+ */
226
335
  function isWorkflowSnapshot(value) {
227
336
  const cloned = attempt(() => cloneJSONValue(value));
228
337
  return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
229
338
  }
230
339
  /**
231
- * Test whether an unknown value is a valid whole-frame activity report.
340
+ * Checks whether an unknown value is a valid list of task activity claims.
341
+ *
342
+ * @remarks
343
+ * The one guard behind both claim lists of a {@link TaskActivityInput} — its `operations` and its
344
+ * `constraints` — because {@link import('./types.js').TaskOperation} and
345
+ * {@link import('./types.js').TaskConstraint} are the same {@link TaskClaim} shape. Every member must be a plain record carrying exactly `id`, `name`, and
346
+ * `started`, with non-empty string `id` and `name`, a finite non-negative `started`, and an `id`
347
+ * unique within the list. Total: a hostile prototype, an accessor, or a cycle returns `false`
348
+ * rather than throwing.
349
+ *
350
+ * @param value - The value to test
351
+ * @returns True if `value` is a list of valid, uniquely identified claims; false otherwise
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * isTaskClaimList([{ id: 'fetch', name: 'Fetch', started: 1 }]) // true
356
+ * isTaskClaimList([{ id: 'fetch', name: 'Fetch' }]) // false
357
+ * ```
358
+ */
359
+ function isTaskClaimList(value) {
360
+ try {
361
+ if (!isArray(value)) return false;
362
+ const ids = /* @__PURE__ */ new Set();
363
+ for (const claim of value) {
364
+ if (!isRecord(claim)) return false;
365
+ const prototype = Object.getPrototypeOf(claim);
366
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) return false;
367
+ const id = claim.id;
368
+ const name = claim.name;
369
+ const started = claim.started;
370
+ if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
371
+ ids.add(id);
372
+ }
373
+ return true;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
378
+ /**
379
+ * Tests whether an unknown value is a valid whole-frame activity report.
380
+ *
381
+ * @remarks
382
+ * The guard behind {@link import('./types.js').TaskInterface.report}: exactly the optional `note`,
383
+ * `progress`, `operations`, and `constraints` keys, with the two claim lists checked by
384
+ * {@link isTaskClaimList} and `progress` a finite non-negative value under an optional `total` at
385
+ * least as large. Total — a hostile prototype or accessor answers `false` rather than throwing.
386
+ *
387
+ * @param value - The value to test
388
+ * @returns True if `value` is a valid {@link TaskActivityInput}; false otherwise
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * isTaskActivityInput({ note: 'compiling', progress: { progress: 2, total: 5 } }) // true
393
+ * ```
232
394
  */
233
395
  function isTaskActivityInput(value) {
234
396
  try {
@@ -249,41 +411,29 @@ function isTaskActivityInput(value) {
249
411
  const message = progress.message;
250
412
  if (!isFiniteNumber(reported) || reported < 0 || total !== void 0 && (!isFiniteNumber(total) || total < reported) || message !== void 0 && !isNonEmptyString(message)) return false;
251
413
  }
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
- }
414
+ if (operations !== void 0 && !isTaskClaimList(operations)) return false;
415
+ if (constraints !== void 0 && !isTaskClaimList(constraints)) return false;
280
416
  return true;
281
417
  } catch {
282
418
  return false;
283
419
  }
284
420
  }
285
421
  /**
286
- * Test whether an unknown value is valid persisted task activity.
422
+ * Tests whether an unknown value is valid persisted task activity.
423
+ *
424
+ * @remarks
425
+ * The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the REQUIRED
426
+ * `operations`, `constraints`, and a finite non-negative `updated` stamp, because a stored frame
427
+ * has already been accepted and normalized. Total — a hostile prototype or accessor answers
428
+ * `false` rather than throwing.
429
+ *
430
+ * @param value - The value to test
431
+ * @returns True if `value` is a persisted {@link TaskActivity}; false otherwise
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * isTaskActivity({ operations: [], constraints: [], updated: 1 }) // true
436
+ * ```
287
437
  */
288
438
  function isTaskActivity(value) {
289
439
  try {
@@ -309,7 +459,7 @@ function isTaskActivity(value) {
309
459
  //#endregion
310
460
  //#region src/core/helpers.ts
311
461
  /**
312
- * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
462
+ * Captures every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
313
463
  *
314
464
  * @remarks
315
465
  * Direct property reads preserve inherited and non-enumerable option values while preventing
@@ -344,25 +494,151 @@ function captureWorkflowOptions(options) {
344
494
  });
345
495
  }
346
496
  /**
347
- * Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
497
+ * Tests whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
348
498
  * transition further.
349
499
  *
350
500
  * @remarks
351
- * The ONE terminal check across all three tiers (AGENTS §4.4 "one concept = one word"):
501
+ * The ONE terminal check across every tier (AGENTS.md § Design laws, "one concept, one term"):
352
502
  * a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
353
503
  * 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`.
504
+ * both consult it to tell a settled node from an in-flight one. It reads the terminal set from
505
+ * {@link import('./constants.js').TERMINAL_STATUSES} (`completed` / `failed` / `skipped` /
506
+ * `stopped`), so that constant is the one definition; the only non-terminal states are `pending`
507
+ * and `running`.
357
508
  *
358
509
  * @param status - The lifecycle status to test (a task / phase / workflow status)
359
- * @returns `true` when the status is terminal
510
+ * @returns True if the status is terminal; false otherwise
360
511
  */
361
512
  function isTerminalStatus(status) {
362
- return status === "completed" || status === "failed" || status === "skipped" || status === "stopped";
513
+ return TERMINAL_STATUSES.includes(status);
514
+ }
515
+ /**
516
+ * Tests whether a driving run must stop giving a workflow more work.
517
+ *
518
+ * @remarks
519
+ * The halt gate a {@link import('./WorkflowRunner.js').WorkflowRunner} consults before starting a
520
+ * phase, before dispatching a task, and after every cooperative gate. A workflow is halted after
521
+ * its derived status is terminal but NOT `completed` — a `bail: true` failure, a caller's own
522
+ * graceful `stop()`, or a forced `skip`. `completed` is excluded deliberately: a workflow that
523
+ * completed vacuously is settled, not halted, and the distinction is what keeps the run from
524
+ * sweeping a finished tree. When a `phase` is supplied, its own forced `skipped` / `stopped` halts
525
+ * that phase's work too; a `failed` phase does not, because the workflow's own `bail` policy
526
+ * decides whether a failed phase ends the run.
527
+ *
528
+ * @param workflow - The live workflow the run is driving
529
+ * @param phase - The phase whose own forced terminal status also halts its tasks
530
+ * @returns True if the run must stop giving this workflow (or phase) more work; false otherwise
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * isHalted(workflow) // false while pending or running
535
+ * workflow.stop()
536
+ * isHalted(workflow) // true
537
+ * ```
538
+ */
539
+ function isHalted(workflow, phase) {
540
+ const status = workflow.status;
541
+ return isTerminalStatus(status) && status !== "completed" || phase?.status === "skipped" || phase?.status === "stopped";
542
+ }
543
+ /**
544
+ * Tests whether forcing a workflow `stopped` would still record something.
545
+ *
546
+ * @remarks
547
+ * `stop()` is a no-op after a workflow's status becomes terminal, so a run that must record a
548
+ * cancellation forces it only while this holds. It is NOT the negation of
549
+ * {@link isTerminalStatus}: `completed` and `skipped` both pass, because a run-level cancel that
550
+ * lands on a vacuously-completed or fully-skipped tree still records `stopped` as the outcome the
551
+ * caller asked for. Only an already-`failed` or already-`stopped` workflow has a terminal state
552
+ * worth keeping.
553
+ *
554
+ * @param workflow - The live workflow a run-level cancel would force
555
+ * @returns True if forcing `stopped` would change the recorded outcome; false otherwise
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * isStoppable(workflow) // true while pending, running, completed, or skipped
560
+ * workflow.stop()
561
+ * isStoppable(workflow) // false
562
+ * ```
563
+ */
564
+ function isStoppable(workflow) {
565
+ const status = workflow.status;
566
+ return status !== "failed" && status !== "stopped";
567
+ }
568
+ /**
569
+ * Tests whether a naturally-finished run may force its workflow `completed`.
570
+ *
571
+ * @remarks
572
+ * A run that walked every phase and still derives `pending` executed nothing — zero phases, or
573
+ * every phase empty — so it is vacuously done and the run settles it `completed`. Gated on
574
+ * EXACTLY `pending` so a real `completed`, a `bail: true` `failed`, a `stopped`, or a derived
575
+ * `skipped` is never overridden. The tree-is-empty half of the rule is
576
+ * {@link WorkflowInterface.complete}'s own guard, which refuses a pending tree that still holds
577
+ * tasks.
578
+ *
579
+ * @param workflow - The live workflow the run has finished walking
580
+ * @returns True if the run may force the vacuous completion; false otherwise
581
+ *
582
+ * @example
583
+ * ```ts
584
+ * isCompletable(createWorkflow({ id: 'w', name: 'W', phases: [] })) // true
585
+ * ```
586
+ */
587
+ function isCompletable(workflow) {
588
+ return workflow.status === "pending";
589
+ }
590
+ /**
591
+ * Tests whether a task attempt is being genuinely cancelled rather than merely timed out.
592
+ *
593
+ * @remarks
594
+ * The discriminator that keeps a per-attempt deadline off the skip path. Three causes fire a
595
+ * running task's folded signal, and only two of them mean "skip this task": the task's own
596
+ * `signal` (its `stop` / `skip`), and the unit or run signal (a sibling fail-fast under
597
+ * `bail: true`, or a run-level abort / timeout / budget / `destroy`). A bare per-attempt timeout
598
+ * fires NEITHER — it aborts only the deadline portion of the attempt signal — so it stays a
599
+ * retryable failure of that attempt instead of skipping the leaf and losing the recorded fault.
600
+ * Read fresh at each call so a cancel that lands mid-dispatch is seen.
601
+ *
602
+ * @param task - The live task the attempt is driving
603
+ * @param controller - The substrate unit handle carrying the unit-level abort
604
+ * @param runSignal - The run's folded cancellation signal
605
+ * @returns True if the attempt is being genuinely cancelled; false otherwise
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * isSkipping(task, controller, runSignal) // false until a cancel fires
610
+ * ```
611
+ */
612
+ function isSkipping(task, controller, runSignal) {
613
+ return task.signal.aborted || controller.aborted || runSignal.aborted;
363
614
  }
364
615
  /**
365
- * Derive a phase's status from its tasks' statuses (tasks are concurrent, so this
616
+ * Tests whether one attempt still owns the task it launched.
617
+ *
618
+ * @remarks
619
+ * A retried task is re-dispatched while an earlier attempt's handler may still be resolving, so
620
+ * every settlement path re-checks ownership before touching the leaf. Ownership needs BOTH
621
+ * halves: the run-local `owners` ledger must still name this attempt, and the live task's own
622
+ * `attempts` tally must still match it. A superseded attempt reads `false` and returns without
623
+ * recording anything, so a late resolution can never overwrite the newer attempt's outcome.
624
+ *
625
+ * @param owners - The run-local ledger of the attempt owning each task id
626
+ * @param task - The live task the attempt launched
627
+ * @param attempt - The one-based attempt number to test
628
+ * @returns True if `attempt` still owns `task`; false otherwise
629
+ *
630
+ * @example
631
+ * ```ts
632
+ * const owners = new Map([[task.id, 1]])
633
+ * ownsAttempt(owners, task, 1) // true while the task's own `attempts` is 1
634
+ * ownsAttempt(owners, task, 2) // false
635
+ * ```
636
+ */
637
+ function ownsAttempt(owners, task, attempt) {
638
+ return owners.get(task.id) === attempt && task.attempts === attempt;
639
+ }
640
+ /**
641
+ * Derives a phase's status from its tasks' statuses (tasks are concurrent, so this
366
642
  * is an order-insensitive reduction).
367
643
  *
368
644
  * @remarks
@@ -380,7 +656,7 @@ function isTerminalStatus(status) {
380
656
  * task makes the phase `failed`.
381
657
  *
382
658
  * @param tasks - The phase's task statuses, in any order
383
- * @returns The derived {@link PhaseStatus}
659
+ * @returns The derived phase {@link LifecycleStatus}
384
660
  */
385
661
  function derivePhaseStatus(tasks) {
386
662
  if (tasks.length === 0) return "pending";
@@ -392,13 +668,13 @@ function derivePhaseStatus(tasks) {
392
668
  return "skipped";
393
669
  }
394
670
  /**
395
- * Derive a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
671
+ * Derives a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
396
672
  * paired with the EFFECTIVE `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
397
673
  * failure outcome is PER-PHASE-bail-aware (phases are sequential, but the derivation is an
398
674
  * order-insensitive reduction over the settled set).
399
675
  *
400
676
  * @remarks
401
- * `bail` is now a per-phase override (AGENTS §4.4), so it is carried on each
677
+ * `bail` is a per-phase override, so it is carried on each
402
678
  * {@link PhaseDerivation} rather than passed as one scalar. It is the ONLY axis that changes
403
679
  * the failure outcome, decided per phase:
404
680
  * - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
@@ -417,7 +693,7 @@ function derivePhaseStatus(tasks) {
417
693
  * else (all `skipped`) ⇒ `skipped`.
418
694
  *
419
695
  * @param phases - The workflow's per-phase {@link PhaseDerivation}s (status + effective bail), in any order
420
- * @returns The derived {@link WorkflowStatus}
696
+ * @returns The derived workflow {@link LifecycleStatus}
421
697
  */
422
698
  function deriveWorkflowStatus(phases) {
423
699
  if (phases.length === 0) return "pending";
@@ -429,16 +705,16 @@ function deriveWorkflowStatus(phases) {
429
705
  return "skipped";
430
706
  }
431
707
  /**
432
- * Derive the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
708
+ * Derives the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
433
709
  * the index of the first entry in the contiguous trailing run of `pending` entries.
434
710
  *
435
711
  * @remarks
436
- * The native, hook-free replacement for a runner-installed cursor (AGENTS §12): a
712
+ * The native, hook-free replacement for a runner-installed cursor: a
437
713
  * {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
438
714
  * reads this over its live phases' statuses to decide which positions are safe to edit.
439
715
  * Because entries run SEQUENTIALLY (phases sequential, AGENTS determinism), every
440
716
  * 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
717
+ * entry forms the trailing suffix — so the boundary is the count of leading
442
718
  * non-`pending` entries: the index of the first `pending` entry, or the full length when
443
719
  * none is `pending` (nothing is safely editable). A `pending` container's entries are ALL
444
720
  * `pending`, so the boundary is `0` and every position is naturally accepted — callers
@@ -459,8 +735,8 @@ function deriveBoundary(statuses) {
459
735
  return index === -1 ? statuses.length : index;
460
736
  }
461
737
  /**
462
- * Test whether the live W-b task state machine may move directly from one
463
- * {@link TaskStatus} to another — the legal-transition guard.
738
+ * Tests whether the live W-b task state machine may move directly from one
739
+ * {@link LifecycleStatus} to another — the legal-transition guard.
464
740
  *
465
741
  * @remarks
466
742
  * Reads the {@link import('./constants.js').TASK_TRANSITIONS} graph: `true` only when
@@ -470,13 +746,13 @@ function deriveBoundary(statuses) {
470
746
  *
471
747
  * @param from - The task's current status
472
748
  * @param to - The status the transition would move it to
473
- * @returns `true` when the move is legal
749
+ * @returns True if the move is legal; false otherwise
474
750
  */
475
751
  function canTransitionTask(from, to) {
476
752
  return TASK_TRANSITIONS[from].includes(to);
477
753
  }
478
754
  /**
479
- * Resolve a task's runtime silence window against its workflow default.
755
+ * Resolves a task's runtime silence window against its workflow default.
480
756
  *
481
757
  * @param value - The task-level override; any present non-positive or non-finite value disables
482
758
  * @param fallback - The workflow-level default
@@ -487,7 +763,7 @@ function resolveTaskSilence(value, fallback) {
487
763
  return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
488
764
  }
489
765
  /**
490
- * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
766
+ * Boxes a value as a {@link Success} — the graceful outcome half of a {@link Result}.
491
767
  *
492
768
  * @typeParam T - The boxed value's type
493
769
  * @param value - The value to box
@@ -505,7 +781,7 @@ function success(value) {
505
781
  };
506
782
  }
507
783
  /**
508
- * Box an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
784
+ * Boxes an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
509
785
  *
510
786
  * @typeParam E - The boxed error's type
511
787
  * @param error - The error to box
@@ -523,7 +799,7 @@ function failure(error) {
523
799
  };
524
800
  }
525
801
  /**
526
- * Normalize an unknown thrown value to a non-empty persistence-safe message.
802
+ * Normalizes an unknown thrown value to a non-empty persistence-safe message.
527
803
  *
528
804
  * @param error - The caught value
529
805
  * @returns A non-empty message without stack or cause data
@@ -537,7 +813,7 @@ function errorToMessage(error) {
537
813
  }
538
814
  }
539
815
  /**
540
- * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
816
+ * Finds the first {@link TaskResult} in a positional list whose boxed outcome is a
541
817
  * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
542
818
  * `fail`-event lookup.
543
819
  *
@@ -545,8 +821,8 @@ function errorToMessage(error) {
545
821
  * The shared leaf behind {@link import('./phases/Phase.js').Phase} and
546
822
  * {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers ITS tier's
547
823
  * 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
824
+ * them here; the tier-local method keeps the invariant throw (a derived `failed`
825
+ * status means a failing result exists) because throwing on `undefined` is
550
826
  * orchestration, not a leaf concern.
551
827
  *
552
828
  * @param results - The results to scan, in any order
@@ -561,7 +837,7 @@ function findFailure(results) {
561
837
  return results.find((result) => result.result?.success === false);
562
838
  }
563
839
  /**
564
- * Build a {@link WorkflowContext} — the identity every level inherits — from a node's
840
+ * Builds a {@link WorkflowContext} — the identity every level inherits — from a node's
565
841
  * `id` / `name` / optional `description`.
566
842
  *
567
843
  * @remarks
@@ -580,7 +856,7 @@ function buildWorkflowContext(node) {
580
856
  });
581
857
  }
582
858
  /**
583
- * Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
859
+ * Builds a {@link PhaseContext} — a phase's own identity plus a back-reference to its
584
860
  * workflow — from the parent {@link WorkflowContext} and the phase node's identity.
585
861
  *
586
862
  * @param workflow - The parent workflow context (the lineage pointer UP the tree)
@@ -594,7 +870,7 @@ function buildPhaseContext(workflow, node) {
594
870
  });
595
871
  }
596
872
  /**
597
- * Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
873
+ * Builds a {@link TaskContext} — a task's own identity plus a back-reference to its phase
598
874
  * (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
599
875
  * node's identity.
600
876
  *
@@ -609,20 +885,20 @@ function buildTaskContext(phase, node) {
609
885
  });
610
886
  }
611
887
  /**
612
- * Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
888
+ * Converts a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
613
889
  * node `pending`, no results, empty metadata — so the live W-b tree has ONE construction
614
890
  * path (snapshot-driven) for both a fresh build and a restore.
615
891
  *
616
892
  * @remarks
617
893
  * The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
618
894
  * 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` /
895
+ * {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `behavior` /
620
896
  * `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
621
897
  * so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
622
898
  * real work). The `bail` policy carries over — at the
623
899
  * workflow tier AND, per phase, the
624
900
  * 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.
901
+ * snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped at that point.
626
902
  * {@link import('./factories.js').createWorkflow} builds from this.
627
903
  *
628
904
  * The optional `bail` override is the EFFECTIVE workflow policy the tree will run under
@@ -651,7 +927,7 @@ function definitionToSnapshot(definition, bail) {
651
927
  };
652
928
  }
653
929
  /**
654
- * Convert one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
930
+ * Converts one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
655
931
  * {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
656
932
  *
657
933
  * @remarks
@@ -676,14 +952,14 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
676
952
  };
677
953
  }
678
954
  /**
679
- * Convert one {@link import('./types.js').TaskDefinition} into an initial, `pending`
955
+ * Converts one {@link import('./types.js').TaskDefinition} into an initial, `pending`
680
956
  * {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
681
957
  * result yet, empty metadata).
682
958
  *
683
959
  * @remarks
684
- * `run` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
960
+ * `behavior` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
685
961
  * 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}
962
+ * reliability overrides after pairing with a {@link import('./types.js').WorkflowOptions.functions}
687
963
  * registry.
688
964
  *
689
965
  * @param task - The task definition to seed from
@@ -697,13 +973,13 @@ function taskDefinitionToSnapshot(task) {
697
973
  status: "pending",
698
974
  metadata: {},
699
975
  attempts: 0,
700
- ...task.run === void 0 ? {} : { run: task.run },
976
+ ...task.behavior === void 0 ? {} : { behavior: task.behavior },
701
977
  ...task.retries === void 0 ? {} : { retries: task.retries },
702
978
  ...task.timeout === void 0 ? {} : { timeout: task.timeout }
703
979
  };
704
980
  }
705
981
  /**
706
- * Convert interrupted running work into a recoverable pending suffix or an
982
+ * Converts interrupted running work into a recoverable pending suffix or an
707
983
  * exhausted recovery failure without replenishing attempts.
708
984
  *
709
985
  * @param snapshot - A fully validated owned snapshot with no terminal overrides
@@ -780,11 +1056,58 @@ function recoverWorkflowSnapshot(snapshot) {
780
1056
  updated: now
781
1057
  };
782
1058
  }
783
- /** Compare two optional description values. */
1059
+ /**
1060
+ * Compares two optional description values.
1061
+ *
1062
+ * @remarks
1063
+ * The equality rule a lineage check needs: two descriptions match when they are the same value
1064
+ * AND that value is either a string or genuine absence. Anything else — a number, an object, a
1065
+ * `null` — never matches, even against itself, so a lineage stamped with a non-string description
1066
+ * is rejected rather than silently accepted.
1067
+ *
1068
+ * @param left - The first description value
1069
+ * @param right - The second description value
1070
+ * @returns True if both are the same string or both absent; false otherwise
1071
+ *
1072
+ * @example
1073
+ * ```ts
1074
+ * matchesDescription('build', 'build') // true
1075
+ * matchesDescription(undefined, undefined) // true
1076
+ * matchesDescription('build', undefined) // false
1077
+ * ```
1078
+ */
784
1079
  function matchesDescription(left, right) {
785
1080
  return left === right && (left === void 0 || typeof left === "string");
786
1081
  }
787
- /** Test a result's lineage against its containing snapshot nodes. */
1082
+ /**
1083
+ * Tests a result's lineage against its containing snapshot nodes.
1084
+ *
1085
+ * @remarks
1086
+ * The four arguments are the result and the three snapshot nodes it claims to belong to, read
1087
+ * from the OUTSIDE in: a {@link TaskResult} is self-describing, so restoring one is only safe
1088
+ * when every identity it carries agrees with the tree it was found in. It checks the exact key
1089
+ * set at each level, that `status` equals the owning task's, and that the `task` / `phase` /
1090
+ * `workflow` contexts — including the nested `task.phase.workflow` lineage — carry the same `id`,
1091
+ * `name`, and `description` as the nodes containing them. It then requires the boxed outcome to
1092
+ * match the status: a `Success` holding JSON for `completed`, a `Failure` holding a
1093
+ * {@link TaskFailure} for `failed`, and nothing for any other status. Total — a hostile
1094
+ * prototype, accessor, or cycle answers `false` rather than throwing.
1095
+ *
1096
+ * @param value - The candidate {@link TaskResult}
1097
+ * @param workflow - The workflow snapshot node containing it
1098
+ * @param phase - The phase snapshot node containing it
1099
+ * @param task - The task snapshot node the result belongs to
1100
+ * @returns True if `value` is a {@link TaskResult} whose lineage and outcome match
1101
+ * those nodes; false otherwise
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * const snapshot = workflow.snapshot()
1106
+ * const phase = snapshot.phases[0]
1107
+ * const task = phase?.tasks[0]
1108
+ * isTaskResult(task?.result, snapshot, phase, task) // true for a settled task
1109
+ * ```
1110
+ */
788
1111
  function isTaskResult(value, workflow, phase, task) {
789
1112
  try {
790
1113
  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 +1123,30 @@ function isTaskResult(value, workflow, phase, task) {
800
1123
  }
801
1124
  function hasWorkflowHandlers(workflow, functions) {
802
1125
  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;
1126
+ 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
1127
  return true;
805
1128
  }
806
- const runs = /* @__PURE__ */ new Set();
1129
+ const behaviors = /* @__PURE__ */ new Set();
807
1130
  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;
1131
+ if (task.behavior === void 0 || behaviors.has(task.behavior)) continue;
1132
+ behaviors.add(task.behavior);
1133
+ if (!isFunction(functions?.[task.behavior])) return false;
811
1134
  }
812
1135
  return true;
813
1136
  }
814
- /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
815
- function workflowSnapshotContext(value) {
1137
+ /**
1138
+ * Locates the nearest identifiable node for an inconsistent owned snapshot.
1139
+ *
1140
+ * @remarks
1141
+ * The walk stops at the first phase or task whose persisted fields are inconsistent and returns
1142
+ * the identifiers it could read there, so a diagnostic can name the offending node even when part
1143
+ * of its identity is unreadable.
1144
+ *
1145
+ * @param value - The candidate snapshot, which may be any unknown value
1146
+ * @returns The nearest identifying record naming the offending `phase` and `task`, or `undefined`
1147
+ * when no inconsistent node is identifiable
1148
+ */
1149
+ function scanSnapshotContext(value) {
816
1150
  if (!isRecord(value) || !isArray(value.phases)) return void 0;
817
1151
  for (const phase of value.phases) {
818
1152
  if (!isRecord(phase)) continue;
@@ -820,7 +1154,7 @@ function workflowSnapshotContext(value) {
820
1154
  if (!isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return phaseContext;
821
1155
  for (const task of phase.tasks) {
822
1156
  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 {
1157
+ 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
1158
  ...phaseContext ?? {},
825
1159
  ...isNonEmptyString(task.id) ? { task: task.id } : {}
826
1160
  };
@@ -828,7 +1162,7 @@ function workflowSnapshotContext(value) {
828
1162
  }
829
1163
  }
830
1164
  /**
831
- * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
1165
+ * Flattens a nested list of per-phase {@link TaskResult} lists into one positional list
832
1166
  * — the workflow tier of the result tree, built from each phase's `results()`.
833
1167
  *
834
1168
  * @remarks
@@ -843,16 +1177,15 @@ function collectResults(phases) {
843
1177
  return phases.flat();
844
1178
  }
845
1179
  /**
846
- * Insert one `[key, value]` entry at a positional index into a readonly entries array —
1180
+ * Inserts one `[key, value]` entry at a positional index into a readonly entries array —
847
1181
  * the pure splice-in step behind an insertion-ordered registry's `add`.
848
1182
  *
849
1183
  * @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.
1184
+ * Used by the shared {@link import('./Collection.js').Collection} store both managers hold: it
1185
+ * converts its insertion-ordered `Map` to `[...map.entries()]`, calls this to splice the new entry
1186
+ * in at the target index, then rebuilds the `Map` from the result (a stateful step that stays a
1187
+ * `#` private method this helper does no `Map` construction). Does not mutate `entries`;
1188
+ * returns a new array.
856
1189
  *
857
1190
  * @typeParam T - The entry's value type
858
1191
  * @param entries - The current positional entries, in order
@@ -872,16 +1205,17 @@ function insertEntry(entries, index, key, value) {
872
1205
  return next;
873
1206
  }
874
1207
  /**
875
- * Reposition the entry keyed `key` to a new positional index in a readonly entries
1208
+ * Repositions the entry keyed `key` to a new positional index in a readonly entries
876
1209
  * array — the pure remove-then-reinsert step behind an insertion-ordered registry's
877
1210
  * `move`.
878
1211
  *
879
1212
  * @remarks
880
1213
  * The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
881
1214
  * 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.
1215
+ * of `entries` unchanged) — the caller, the shared
1216
+ * {@link import('./Collection.js').Collection} store's `move`, already gates on the target's
1217
+ * existence before calling this, so the no-op branch is defensive, never reached in practice.
1218
+ * Does not mutate `entries`; returns a new array.
885
1219
  *
886
1220
  * @typeParam T - The entry's value type
887
1221
  * @param entries - The current positional entries, in order
@@ -903,17 +1237,7 @@ function moveEntry(entries, key, index) {
903
1237
  return next;
904
1238
  }
905
1239
  /**
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.
1240
+ * Schedules one cancellable host operation behind an owned settlement signal.
917
1241
  *
918
1242
  * @remarks
919
1243
  * A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
@@ -980,7 +1304,36 @@ function scheduleHost(start, signal) {
980
1304
  });
981
1305
  }
982
1306
  /**
983
- * Park until `signal` aborts a promise-parked wait (AGENTS §21), never a timer or
1307
+ * Schedules the shared host timer boundary every scheduler backend resumes from.
1308
+ *
1309
+ * @remarks
1310
+ * The one `setTimeout` / `clearTimeout` boundary in the package: the cross-environment
1311
+ * {@link import('./Scheduler.js').Scheduler}, both Node primitives, and every browser backend's
1312
+ * `delay` and macrotask fallback route here, so the timer is armed and cleared in one place. It
1313
+ * composes {@link scheduleHost}, which owns listener safety, the cancellation race, the exact
1314
+ * caller reason, and once-only settlement. It does NOT validate `ms`: the value passes straight to
1315
+ * the host `setTimeout`, which clamps a negative value or `NaN` to about zero, so an
1316
+ * out-of-domain `ms` resumes on the next host turn rather than throwing. Pass a non-negative
1317
+ * finite `ms`.
1318
+ *
1319
+ * @param ms - The milliseconds to wait before resuming
1320
+ * @param signal - Optional caller cancellation signal
1321
+ * @returns A promise that resolves after `ms`, or rejects with the caller's exact abort reason
1322
+ *
1323
+ * @example
1324
+ * ```ts
1325
+ * const controller = new AbortController()
1326
+ * await delayHost(0, controller.signal) // a real macrotask host turn
1327
+ * ```
1328
+ */
1329
+ function delayHost(ms, signal) {
1330
+ return scheduleHost((complete) => {
1331
+ const handle = setTimeout(complete, ms);
1332
+ return () => clearTimeout(handle);
1333
+ }, signal);
1334
+ }
1335
+ /**
1336
+ * Parks until `signal` aborts — a promise-parked wait, never a timer or
984
1337
  * busy-loop, that NEVER rejects.
985
1338
  *
986
1339
  * @remarks
@@ -990,7 +1343,7 @@ function scheduleHost(start, signal) {
990
1343
  * at every fold point.
991
1344
  *
992
1345
  * @param signal - The signal to park on
993
- * @returns A promise that resolves once `signal` has aborted
1346
+ * @returns A promise that resolves after `signal` has aborted
994
1347
  *
995
1348
  * @example
996
1349
  * ```ts
@@ -1007,9 +1360,109 @@ function parkSignal(signal) {
1007
1360
  });
1008
1361
  }
1009
1362
  //#endregion
1363
+ //#region src/core/Collection.ts
1364
+ /**
1365
+ * Implements the insertion-ordered gated store both lean managers hold — entities keyed by `id`,
1366
+ * positional order preserved across an interior `skip` or `remove`.
1367
+ *
1368
+ * @remarks
1369
+ * - **One engine, two managers.** {@link import('./tasks/TaskManager.js').TaskManager} and
1370
+ * {@link import('./phases/PhaseManager.js').PhaseManager} differ only in the entity noun and the
1371
+ * patch shape they validate, so both hold one of these and add only their domain accessors
1372
+ * (`task` / `tasks`, `phase` / `phases`). The `Map`'s insertion order is the single source of
1373
+ * positional truth; `add` and `move` rebuild it through the pure
1374
+ * {@link import('./helpers.js').insertEntry} / {@link import('./helpers.js').moveEntry} leaves.
1375
+ * - **Gated mutation API.** `append` is the build-time wiring path and THROWS on a
1376
+ * duplicate id; `add` / `remove` / `move` / `update` return a graceful `MUTATION`
1377
+ * {@link WorkflowError} failure instead. Gating reads ONLY the target's own existence, `pending`
1378
+ * status, id, and bounds — a container's own status is the owning entity's gate, applied before
1379
+ * it delegates here.
1380
+ * - **Event-free.** A purely structural container; the entity that owns it emits on success.
1381
+ *
1382
+ * @typeParam TEntry - The stored entity
1383
+ * @typeParam TPatch - The declarative partial update `update` validates and applies
1384
+ *
1385
+ * @example
1386
+ * ```ts
1387
+ * import { compileGuard } from '@orkestrel/contract'
1388
+ * import { Collection, taskUpdateShape } from '@orkestrel/workflow'
1389
+ * import type { TaskInterface, TaskUpdate } from '@orkestrel/workflow'
1390
+ *
1391
+ * const tasks = new Collection<TaskInterface, TaskUpdate>('task', compileGuard(taskUpdateShape))
1392
+ * tasks.append(task) // a live Task
1393
+ * tasks.entry(task.id) // the same task
1394
+ * tasks.entries() // [task]
1395
+ * tasks.count // 1
1396
+ * tasks.add(other, 0) // Result — inserted first
1397
+ * tasks.move(other.id, 1) // Result — repositioned
1398
+ * tasks.update(task.id, { name: 'Renamed task' }) // Result — patched
1399
+ * tasks.remove(other.id) // Result — dropped
1400
+ * ```
1401
+ */
1402
+ var Collection = class {
1403
+ #entries = /* @__PURE__ */ new Map();
1404
+ #noun;
1405
+ #isPatch;
1406
+ constructor(noun, patch) {
1407
+ this.#noun = noun;
1408
+ this.#isPatch = patch;
1409
+ }
1410
+ get count() {
1411
+ return this.#entries.size;
1412
+ }
1413
+ append(entry) {
1414
+ if (this.#entries.has(entry.id)) throw new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id });
1415
+ this.#entries.set(entry.id, entry);
1416
+ }
1417
+ add(entry, index) {
1418
+ if (this.#entries.has(entry.id)) return failure(new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id }));
1419
+ const at = index ?? this.#entries.size;
1420
+ if (at < 0 || at > this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
1421
+ this.#reorder(insertEntry([...this.#entries.entries()], at, entry.id, entry));
1422
+ return success(entry);
1423
+ }
1424
+ remove(id) {
1425
+ const target = this.#pending(id);
1426
+ if (target === void 0) return this.#refuse(id);
1427
+ this.#entries.delete(id);
1428
+ return success(target);
1429
+ }
1430
+ move(id, index) {
1431
+ const target = this.#pending(id);
1432
+ if (target === void 0) return this.#refuse(id);
1433
+ if (index < 0 || index >= this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
1434
+ this.#reorder(moveEntry([...this.#entries.entries()], id, index));
1435
+ return success(target);
1436
+ }
1437
+ update(id, patch) {
1438
+ const target = this.#pending(id);
1439
+ if (target === void 0) return this.#refuse(id);
1440
+ if (!this.#isPatch(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for ${this.#noun} '${id}'`, { id }));
1441
+ target.patch(patch);
1442
+ return success(target);
1443
+ }
1444
+ entry(id) {
1445
+ return this.#entries.get(id);
1446
+ }
1447
+ entries() {
1448
+ return [...this.#entries.values()];
1449
+ }
1450
+ #pending(id) {
1451
+ const target = this.#entries.get(id);
1452
+ return target === void 0 || target.status !== "pending" ? void 0 : target;
1453
+ }
1454
+ #refuse(id) {
1455
+ return failure(new WorkflowError("MUTATION", `${this.#noun} '${id}' is not a pending ${this.#noun}`, { id }));
1456
+ }
1457
+ #reorder(entries) {
1458
+ this.#entries.clear();
1459
+ for (const [key, value] of entries) this.#entries.set(key, value);
1460
+ }
1461
+ };
1462
+ //#endregion
1010
1463
  //#region src/core/Scheduler.ts
1011
1464
  /**
1012
- * The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
1465
+ * Implements the safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
1013
1466
  * built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
1014
1467
  * browser and Node.
1015
1468
  *
@@ -1025,7 +1478,7 @@ function parkSignal(signal) {
1025
1478
  * rendering run — it only defers within the current task. A zero-delay timer is
1026
1479
  * the correct cross-environment "give the host a turn".
1027
1480
  * - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
1028
- * {@link scheduleHost} links an owned settlement composite to the caller before arming
1481
+ * {@link delayHost} links an owned settlement composite to the caller before arming
1029
1482
  * the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
1030
1483
  * cancellation clears the handle, and native first-settlement wins exactly once.
1031
1484
  * - **Priority is accepted but uniform.** `options.priority` is part of the
@@ -1044,36 +1497,30 @@ function parkSignal(signal) {
1044
1497
  */
1045
1498
  var Scheduler = class {
1046
1499
  /**
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,
1500
+ * Yields control back to the host so other tasks (I/O, timers, rendering) can
1501
+ * run, then resumes — a macrotask turn through `setTimeout(0)` (NOT a microtask,
1049
1502
  * which would resume before the host regains control).
1050
1503
  */
1051
1504
  yield(options) {
1052
- return this.#sleep(0, options?.signal);
1505
+ return delayHost(0, options?.signal);
1053
1506
  }
1054
1507
  /**
1055
- * Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
1508
+ * Resumes after at least `ms` milliseconds; abort rejects with `signal.reason`.
1056
1509
  *
1057
1510
  * @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.
1511
+ * Pass a non-negative finite `ms`. The primitive stays minimal and does no
1512
+ * validation: it passes `ms` straight to the host `setTimeout`, which clamps a
1513
+ * negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on the next
1514
+ * host turn rather than throwing.
1062
1515
  */
1063
1516
  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);
1517
+ return delayHost(ms, options?.signal);
1071
1518
  }
1072
1519
  };
1073
1520
  //#endregion
1074
1521
  //#region src/core/cloners.ts
1075
1522
  /**
1076
- * Validate and own a workflow snapshot before live construction.
1523
+ * Validates and owns a workflow snapshot before live construction.
1077
1524
  *
1078
1525
  * @param input - The hostile snapshot boundary
1079
1526
  * @param id - The optional storage key the owned snapshot must match
@@ -1089,7 +1536,7 @@ function cloneWorkflowSnapshot(input, id) {
1089
1536
  if (isContractError(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
1090
1537
  throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
1091
1538
  }
1092
- if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
1539
+ if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", scanSnapshotContext(cloned));
1093
1540
  if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
1094
1541
  requested: id,
1095
1542
  payload: cloned.id
@@ -1097,7 +1544,49 @@ function cloneWorkflowSnapshot(input, id) {
1097
1544
  return cloned;
1098
1545
  }
1099
1546
  /**
1100
- * Validate and clone one complete task activity frame.
1547
+ * Validates and owns one list of task activity claims.
1548
+ *
1549
+ * @remarks
1550
+ * The one cloner behind both claim lists of a task activity frame — its `operations` and its
1551
+ * `constraints` — because {@link import('./types.js').TaskOperation} and
1552
+ * {@link import('./types.js').TaskConstraint} are the same {@link import('./types.js').TaskClaim}
1553
+ * shape. An omitted
1554
+ * list is an empty one. Each member is read exactly once inside the caller's protected boundary
1555
+ * and returned frozen; the semantic pass over the copied values is
1556
+ * {@link import('./validators.js').isTaskClaimList}, so this cloner refuses only what it cannot
1557
+ * read: a non-array list, a non-record member, a hostile prototype, or an unexpected key.
1558
+ *
1559
+ * @param input - The untrusted claim list
1560
+ * @param noun - The singular claim noun the refusal message names, pluralized by adding `s`
1561
+ * @returns The owned frozen claims, in input order
1562
+ * @throws {WorkflowError} With `MUTATION` when the list or one of its members cannot be read
1563
+ *
1564
+ * @example
1565
+ * ```ts
1566
+ * cloneTaskClaims([{ id: 'fetch', name: 'Fetch', started: 1 }], 'operation')
1567
+ * ```
1568
+ */
1569
+ function cloneTaskClaims(input, noun) {
1570
+ const inputs = input === void 0 ? [] : isArray(input) ? [...input] : void 0;
1571
+ if (inputs === void 0) throw new WorkflowError("MUTATION", `task activity ${noun}s must be an array`);
1572
+ const claims = [];
1573
+ for (const claim of inputs) {
1574
+ if (!isRecord(claim)) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
1575
+ const prototype = Object.getPrototypeOf(claim);
1576
+ 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}`);
1577
+ const id = claim.id;
1578
+ const name = claim.name;
1579
+ const started = claim.started;
1580
+ claims.push(Object.freeze({
1581
+ id,
1582
+ name,
1583
+ started
1584
+ }));
1585
+ }
1586
+ return claims;
1587
+ }
1588
+ /**
1589
+ * Validates and clones one complete task activity frame.
1101
1590
  *
1102
1591
  * @remarks
1103
1592
  * This is the hostile boundary behind task reports and snapshot hydration. Supplying
@@ -1121,22 +1610,7 @@ function cloneTaskActivity(input, updated) {
1121
1610
  const operationsInput = input.operations;
1122
1611
  const constraintsInput = input.constraints;
1123
1612
  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
- }
1613
+ const operations = cloneTaskClaims(operationsInput, "operation");
1140
1614
  let progress;
1141
1615
  if (progressInput !== void 0) {
1142
1616
  if (!isRecord(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
@@ -1151,22 +1625,7 @@ function cloneTaskActivity(input, updated) {
1151
1625
  ...message === void 0 ? {} : { message }
1152
1626
  });
1153
1627
  }
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
- }
1628
+ const constraints = cloneTaskClaims(constraintsInput, "constraint");
1170
1629
  const activity = Object.freeze({
1171
1630
  ...note === void 0 ? {} : { note },
1172
1631
  ...progress === void 0 ? {} : { progress },
@@ -1184,8 +1643,8 @@ function cloneTaskActivity(input, updated) {
1184
1643
  //#endregion
1185
1644
  //#region src/core/shapers.ts
1186
1645
  /**
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
1646
+ * Describes the shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
1647
+ * `behavior` behavior reference (a plain registry-key string, min length 1). `description` is
1189
1648
  * optional prose.
1190
1649
  */
1191
1650
  var taskShape = objectShape({
@@ -1198,7 +1657,7 @@ var taskShape = objectShape({
1198
1657
  description: "Human-readable task name."
1199
1658
  }),
1200
1659
  description: optionalShape(stringShape({ description: "Optional task description." })),
1201
- run: optionalShape(stringShape({
1660
+ behavior: optionalShape(stringShape({
1202
1661
  min: 1,
1203
1662
  description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
1204
1663
  })),
@@ -1213,7 +1672,7 @@ var taskShape = objectShape({
1213
1672
  }))
1214
1673
  });
1215
1674
  /**
1216
- * The shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
1675
+ * Describes the shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
1217
1676
  * {@link taskShape} tasks, and an optional positive-integer `concurrency` throttle
1218
1677
  * (max tasks in flight; omitted ⇒ unbounded).
1219
1678
  */
@@ -1235,7 +1694,7 @@ var phaseShape = objectShape({
1235
1694
  bail: optionalShape(literalShape([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
1236
1695
  });
1237
1696
  /**
1238
- * The shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
1697
+ * Describes the shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
1239
1698
  * identity, its ordered {@link phaseShape} phases, and the optional `bail` boolean
1240
1699
  * failure policy (the literal pair `true`/`false`, the runtime mirror of the boolean
1241
1700
  * toggle; omitted ⇒ the graceful default).
@@ -1254,13 +1713,13 @@ var workflowShape = objectShape({
1254
1713
  bail: optionalShape(literalShape([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
1255
1714
  });
1256
1715
  /**
1257
- * The shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
1716
+ * Describes the shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
1258
1717
  * `pending` task's `name` / `description`, both optional.
1259
1718
  *
1260
1719
  * @remarks
1261
1720
  * 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).
1721
+ * `name` still has `minLength: 1`); never `id` / `behavior` / `retries` / `timeout` (those
1722
+ * are not patchable fields).
1264
1723
  */
1265
1724
  var taskUpdateShape = objectShape({
1266
1725
  name: optionalShape(stringShape({
@@ -1270,13 +1729,13 @@ var taskUpdateShape = objectShape({
1270
1729
  description: optionalShape(stringShape({ description: "New task description." }))
1271
1730
  });
1272
1731
  /**
1273
- * The shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
1732
+ * Describes the shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
1274
1733
  * `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
1275
1734
  *
1276
1735
  * @remarks
1277
1736
  * Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
1278
1737
  * `tasks` (structural children change through the phase's own `add` / `remove` /
1279
- * `move`, not a patch, AGENTS §12).
1738
+ * `move`, not a patch).
1280
1739
  */
1281
1740
  var phaseUpdateShape = objectShape({
1282
1741
  name: optionalShape(stringShape({
@@ -1293,7 +1752,7 @@ var phaseUpdateShape = objectShape({
1293
1752
  //#endregion
1294
1753
  //#region src/core/stores/DatabaseWorkflowStore.ts
1295
1754
  /**
1296
- * A {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
1755
+ * Implements a {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
1297
1756
  * workflow's durable run-state IS a row, so persistence reduces to keyed point-access
1298
1757
  * (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
1299
1758
  * plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
@@ -1317,16 +1776,18 @@ var phaseUpdateShape = objectShape({
1317
1776
  *
1318
1777
  * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
1319
1778
  * 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.
1779
+ * - **`get(id)` resolves the stored snapshot for an id**, owning and narrowing the opaque JSON
1780
+ * column back to a {@link WorkflowSnapshot} through
1781
+ * {@link import('../cloners.js').cloneWorkflowSnapshot}, whose semantic pass is
1782
+ * {@link import('../validators.js').isOwnedWorkflowSnapshot} the boundary narrow for
1783
+ * an untrusted storage read — or `undefined` if none is stored. A present snapshot whose own id
1784
+ * differs from the requested key rejects with normalized `RESTORE` evidence.
1324
1785
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1325
1786
  *
1326
1787
  * UNLIKE the server package's `SessionStoreInterface` there is NO
1327
1788
  * idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
1328
1789
  * 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
1790
+ * guide's method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1330
1791
  * snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1331
1792
  *
1332
1793
  * @example
@@ -1345,7 +1806,7 @@ var phaseUpdateShape = objectShape({
1345
1806
  var DatabaseWorkflowStore = class {
1346
1807
  #table;
1347
1808
  /**
1348
- * Wrap a table as a workflow store.
1809
+ * Wraps a table as a workflow store.
1349
1810
  *
1350
1811
  * @param table - The {@link TableInterface} holding the snapshots — its row is the
1351
1812
  * {@link WorkflowSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
@@ -1353,13 +1814,13 @@ var DatabaseWorkflowStore = class {
1353
1814
  constructor(table) {
1354
1815
  this.#table = table;
1355
1816
  }
1356
- /** Resolve and key-check the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
1817
+ /** Resolves and key-checks the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
1357
1818
  async get(id) {
1358
1819
  const row = await this.#table.get(id);
1359
1820
  if (row === void 0) return void 0;
1360
1821
  return cloneWorkflowSnapshot(row.snapshot, id);
1361
1822
  }
1362
- /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1823
+ /** Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1363
1824
  async set(snapshot) {
1364
1825
  const owned = cloneWorkflowSnapshot(snapshot);
1365
1826
  await this.#table.set({
@@ -1367,7 +1828,7 @@ var DatabaseWorkflowStore = class {
1367
1828
  snapshot: owned
1368
1829
  });
1369
1830
  }
1370
- /** Drop a snapshot by id; an absent id is a no-op (no throw). */
1831
+ /** Drops a snapshot by id; an absent id is a no-op (no throw). */
1371
1832
  async delete(id) {
1372
1833
  await this.#table.remove(id);
1373
1834
  }
@@ -1375,12 +1836,12 @@ var DatabaseWorkflowStore = class {
1375
1836
  //#endregion
1376
1837
  //#region src/core/stores/MemoryWorkflowStore.ts
1377
1838
  /**
1378
- * The in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
1839
+ * Implements the in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
1379
1840
  * {@link WorkflowSnapshot}s keyed by workflow id, the DEFAULT store
1380
1841
  * {@link import('../factories.js').createMemoryWorkflowStore} builds.
1381
1842
  *
1382
1843
  * @remarks
1383
- * A plain `Map<string, WorkflowSnapshot>` (AGENTS §21 — the snapshot is already pure,
1844
+ * A plain `Map<string, WorkflowSnapshot>` (the snapshot is already pure,
1384
1845
  * self-contained JSON, so no encoding is needed for the memory tier). UNLIKE the server
1385
1846
  * package's `SessionStoreInterface`'s memory store there is
1386
1847
  * NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
@@ -1396,7 +1857,7 @@ var DatabaseWorkflowStore = class {
1396
1857
  * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
1397
1858
  * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
1398
1859
  *
1399
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1860
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the guide's method
1400
1861
  * bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
1401
1862
  * back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1402
1863
  *
@@ -1431,18 +1892,18 @@ var MemoryWorkflowStore = class {
1431
1892
  //#endregion
1432
1893
  //#region src/core/tasks/Task.ts
1433
1894
  /**
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
1895
+ * Implements the live leaf state machine (W-b) for one task — an observable, guarded
1896
+ * synchronous task whose explicit {@link LifecycleStatus} advances through the declared
1436
1897
  * transitions, recording a {@link TaskResult} on a terminal outcome.
1437
1898
  *
1438
1899
  * @remarks
1439
- * - **Guarded transitions (AGENTS §10).** `start` (→ `running`), then `complete(value)`
1900
+ * - **Guarded transitions.** `start` (→ `running`), then `complete(value)`
1440
1901
  * (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
1441
1902
  * (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
1442
1903
  * `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.
1904
+ * `TRANSITION` {@link WorkflowError} on an illegal move (for example, completing a
1905
+ * non-`running` task) — the legal graph is the single source of truth, so the leaf can never
1906
+ * reach an impossible state.
1446
1907
  * - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
1447
1908
  * statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
1448
1909
  * - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
@@ -1450,16 +1911,16 @@ var MemoryWorkflowStore = class {
1450
1911
  * transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
1451
1912
  * order means an observer sees the CAUSE (this leaf changed) before the EFFECT (the parents
1452
1913
  * 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
1914
+ * - **Observable.** The owned {@link emitter} ({@link TaskEventMap}) fires the
1454
1915
  * matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
1455
1916
  * a listener throw and routes it to its `error` handler (the `error` option), so a buggy
1456
1917
  * observer can never corrupt a transition.
1457
- * - **Declarative config (AGENTS §12).** `run` / `retries` / `timeout` PERSIST in a
1918
+ * - **Declarative config.** `behavior` / `retries` / `timeout` PERSIST in a
1458
1919
  * {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
1459
1920
  * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
1460
- * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
1921
+ * is the RUNTIME-ONLY counterpart — `behavior` resolved ONCE at construction against the
1461
1922
  * 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
1923
+ * NEVER persisted; `undefined` when `behavior` is omitted or unregistered. Only omission is a
1463
1924
  * deliberate no-op; unresolved named work is rejected before dispatch.
1464
1925
  */
1465
1926
  var Task = class {
@@ -1472,7 +1933,8 @@ var Task = class {
1472
1933
  #status;
1473
1934
  #result;
1474
1935
  #name;
1475
- #run;
1936
+ #description;
1937
+ #behavior;
1476
1938
  #retries;
1477
1939
  #timeout;
1478
1940
  #attempts;
@@ -1485,7 +1947,7 @@ var Task = class {
1485
1947
  #paused;
1486
1948
  #gate;
1487
1949
  #timerSignal;
1488
- constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1950
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, behavior, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1489
1951
  this.#context = buildTaskContext(context.phase, context);
1490
1952
  this.#phase = phase;
1491
1953
  this.#workflow = workflow;
@@ -1507,11 +1969,8 @@ var Task = class {
1507
1969
  this.#status = status;
1508
1970
  this.#result = result;
1509
1971
  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;
1972
+ this.#description = context.description;
1973
+ this.#behavior = behavior;
1515
1974
  this.#retries = retries;
1516
1975
  this.#timeout = timeout;
1517
1976
  this.#attempts = attempts;
@@ -1537,6 +1996,9 @@ var Task = class {
1537
1996
  get name() {
1538
1997
  return this.#name;
1539
1998
  }
1999
+ get description() {
2000
+ return this.#description;
2001
+ }
1540
2002
  get context() {
1541
2003
  return this.#context;
1542
2004
  }
@@ -1555,8 +2017,8 @@ var Task = class {
1555
2017
  get attempts() {
1556
2018
  return this.#attempts;
1557
2019
  }
1558
- get run() {
1559
- return this.#run;
2020
+ get behavior() {
2021
+ return this.#behavior;
1560
2022
  }
1561
2023
  get handler() {
1562
2024
  return this.#handler;
@@ -1595,7 +2057,7 @@ var Task = class {
1595
2057
  this.#activity = cloneTaskActivity({}, this.#stamp());
1596
2058
  this.#arm();
1597
2059
  this.#emitter.emit("start", this.id);
1598
- this.#escalate();
2060
+ this.#recompute();
1599
2061
  }
1600
2062
  complete(value) {
1601
2063
  let owned;
@@ -1612,7 +2074,7 @@ var Task = class {
1612
2074
  value: owned
1613
2075
  }));
1614
2076
  this.#emitter.emit("complete", result);
1615
- this.#escalate();
2077
+ this.#recompute();
1616
2078
  }
1617
2079
  fail(error) {
1618
2080
  const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
@@ -1627,21 +2089,21 @@ var Task = class {
1627
2089
  })
1628
2090
  }));
1629
2091
  this.#emitter.emit("fail", result);
1630
- this.#escalate();
2092
+ this.#recompute();
1631
2093
  }
1632
2094
  skip() {
1633
2095
  this.#transition("skipped");
1634
2096
  this.#finish();
1635
2097
  this.#abort.abort();
1636
2098
  this.#emitter.emit("skip");
1637
- this.#escalate();
2099
+ this.#recompute();
1638
2100
  }
1639
2101
  stop() {
1640
2102
  this.#transition("stopped");
1641
2103
  this.#finish();
1642
2104
  this.#abort.abort();
1643
2105
  this.#emitter.emit("stop");
1644
- this.#escalate();
2106
+ this.#recompute();
1645
2107
  }
1646
2108
  report(input) {
1647
2109
  if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
@@ -1669,7 +2131,7 @@ var Task = class {
1669
2131
  pause() {
1670
2132
  if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
1671
2133
  this.#paused = true;
1672
- this.#gate = createDeferred();
2134
+ this.#gate = Promise.withResolvers();
1673
2135
  this.#emitter.emit("pause");
1674
2136
  }
1675
2137
  resume() {
@@ -1682,10 +2144,10 @@ var Task = class {
1682
2144
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1683
2145
  }
1684
2146
  /**
1685
- * Apply a validated declarative patch to SELF (`name` / `description`).
2147
+ * Applies a validated declarative patch to SELF (`name` / `description`).
1686
2148
  *
1687
2149
  * @remarks
1688
- * Defense-in-depth (AGENTS §12): the owning
2150
+ * Defense-in-depth: the owning
1689
2151
  * {@link import('../types.js').TaskManagerInterface.update} gates FIRST (target
1690
2152
  * exists + `pending`), so this is the second, redundant check — it THROWS a
1691
2153
  * `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
@@ -1702,21 +2164,18 @@ var Task = class {
1702
2164
  status: this.#status
1703
2165
  });
1704
2166
  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
- });
2167
+ if (value.description !== void 0) this.#description = value.description;
1709
2168
  }
1710
2169
  snapshot() {
1711
2170
  return {
1712
2171
  id: this.id,
1713
2172
  name: this.name,
1714
- ...this.description === void 0 ? {} : { description: this.description },
2173
+ ...this.#description === void 0 ? {} : { description: this.#description },
1715
2174
  status: this.#status,
1716
2175
  ...this.#result === void 0 ? {} : { result: this.#result },
1717
2176
  metadata: this.#metadata,
1718
2177
  attempts: this.#attempts,
1719
- ...this.#run === void 0 ? {} : { run: this.#run },
2178
+ ...this.#behavior === void 0 ? {} : { behavior: this.#behavior },
1720
2179
  ...this.#retries === void 0 ? {} : { retries: this.#retries },
1721
2180
  ...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
1722
2181
  ...this.#activity === void 0 ? {} : { activity: this.#activity }
@@ -1743,9 +2202,6 @@ var Task = class {
1743
2202
  this.#result = frozen;
1744
2203
  return frozen;
1745
2204
  }
1746
- #escalate() {
1747
- this.#recompute();
1748
- }
1749
2205
  #touch() {
1750
2206
  if (this.#activity === void 0) return;
1751
2207
  this.#activity = Object.freeze({
@@ -1790,25 +2246,30 @@ var Task = class {
1790
2246
  //#endregion
1791
2247
  //#region src/core/tasks/TaskManager.ts
1792
2248
  /**
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`.
2249
+ * Implements the lean child manager of a {@link import('../phases/Phase.js').Phase}'s live
2250
+ * tasks — the task vocabulary over one insertion-ordered {@link Collection}, so positional order
2251
+ * is preserved across an interior `skip` / `remove`.
1796
2252
  *
1797
2253
  * @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.
2254
+ * - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
2255
+ * the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
2256
+ * `task` noun its refusals name and the compiled {@link taskUpdateShape} guard. This class adds
2257
+ * the domain accessors `task` / `tasks` and nothing else, so the task and phase managers cannot
2258
+ * drift apart.
2259
+ * - **Positional store.** `append` adds one live {@link TaskInterface} at the end (the build-time
2260
+ * wiring path), `task(id)` looks one up, `tasks()` lists them in positional order, `count` is
2261
+ * the tally. A `skip` is a STATUS change on a stored task (never a removal), so order survives
2262
+ * it; a snapshot RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
2263
+ * - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
2264
+ * `Result` counterparts to `append`, gating ONLY on the target's OWN existence/status/id/bounds
2265
+ * a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
2266
+ * fails {@link taskUpdateShape} validation all fail gracefully with a `MUTATION`
2267
+ * {@link WorkflowError} instead of throwing.
2268
+ * - **No batch matrix.** A phase's tasks are a fixed positional set, so
2269
+ * `.claude/rules/patterns.md` § Batch operations (the bulk verb
2270
+ * overloads) is deliberately omitted — no `remove` family lives here.
2271
+ * - **Event-free.** A purely structural container — the live {@link TaskInterface}s own their own
2272
+ * emitters; the manager observes nothing.
1812
2273
  *
1813
2274
  * @example
1814
2275
  * ```ts
@@ -1819,58 +2280,37 @@ var Task = class {
1819
2280
  * ```
1820
2281
  */
1821
2282
  var TaskManager = class {
1822
- #tasks = /* @__PURE__ */ new Map();
1823
- #isUpdate = compileGuard(taskUpdateShape);
2283
+ #tasks = new Collection("task", compileGuard(taskUpdateShape));
1824
2284
  get count() {
1825
- return this.#tasks.size;
2285
+ return this.#tasks.count;
1826
2286
  }
1827
2287
  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);
2288
+ this.#tasks.append(task);
1830
2289
  }
1831
2290
  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);
2291
+ return this.#tasks.add(task, index);
1837
2292
  }
1838
2293
  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);
2294
+ return this.#tasks.remove(id);
1843
2295
  }
1844
2296
  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);
2297
+ return this.#tasks.move(id, index);
1850
2298
  }
1851
2299
  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);
2300
+ return this.#tasks.update(id, patch);
1857
2301
  }
1858
2302
  task(id) {
1859
- return this.#tasks.get(id);
2303
+ return this.#tasks.entry(id);
1860
2304
  }
1861
2305
  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);
2306
+ return this.#tasks.entries();
1867
2307
  }
1868
2308
  };
1869
2309
  //#endregion
1870
2310
  //#region src/core/phases/Phase.ts
1871
2311
  /**
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
2312
+ * Implements the live DERIVED state machine (W-b) for one phase — an observable whose
2313
+ * {@link LifecycleStatus} is computed from its tasks (never set directly) and recomputed
1874
2314
  * reactively as a task transitions (the middle tier of the cascade).
1875
2315
  *
1876
2316
  * @remarks
@@ -1878,20 +2318,20 @@ var TaskManager = class {
1878
2318
  * {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
1879
2319
  * each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
1880
2320
  * 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
2321
+ * - **Override.** `skip` / `stop` FORCE the phase's status (for example, skipping a whole
1882
2322
  * phase), overriding the derived value; the override is PERSISTED in the snapshot's own
1883
2323
  * `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`,
2324
+ * - **Children.** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
1885
2325
  * no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
1886
2326
  * `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
1887
2327
  * tree); `workflow` navigates UP to the live parent.
1888
- * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
2328
+ * - **Observable.** The owned {@link emitter} ({@link PhaseEventMap}) fires
1889
2329
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
1890
2330
  * corresponding status or runtime-gate change. Status events fire after the phase recomputes
1891
2331
  * and before it escalates to the workflow, preserving child/phase cause before parent effect.
1892
2332
  * The emitter isolates a listener throw and routes it to its `error` handler (the `error`
1893
2333
  * option); `fail` carries the failing task's {@link TaskResult}.
1894
- * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
2334
+ * - **Structural API.** `add` / `remove` / `move` / `update` gate BEFORE
1895
2335
  * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1896
2336
  * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
1897
2337
  * gating, purely from this phase's own derived `status` (no runner-installed hook): while
@@ -1899,22 +2339,22 @@ var TaskManager = class {
1899
2339
  * append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
1900
2340
  * `update` always fail gracefully (the tasks are already handed to the execution
1901
2341
  * substrate); while terminal, everything is refused.
1902
- * - **Patch (AGENTS §12).** `patch` applies a validated {@link PhaseUpdate} to SELF
2342
+ * - **Patch.** `patch` applies a validated {@link PhaseUpdate} to SELF
1903
2343
  * (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
1904
2344
  * `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
1905
2345
  * the owning {@link WorkflowInterface.update}'s gate.
1906
- * - **Minting (AGENTS §7).** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
2346
+ * - **Minting.** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
1907
2347
  * (converts it to a {@link TaskSnapshot}, builds the task wired to THIS phase) — the same
1908
2348
  * construction path {@link #append} uses at build time, so a live mint and a restored/built
1909
2349
  * 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`
2350
+ * {@link import('../types.js').WorkflowRegistry} registry (threaded from
2351
+ * {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `behavior`
1912
2352
  * name ONCE before any task is built; siblings sharing a name receive the exact same captured
1913
2353
  * runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
1914
2354
  * 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
2355
+ * `behavior` resolves to no handler; only omission is a no-op, while an unresolved present name makes
1916
2356
  * the containing tree non-drivable.
1917
- * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
2357
+ * - **Runtime lifecycle.** `pause` / `resume` / `wait` mirror the workflow's own
1918
2358
  * quartet, scoped to this phase — a driving
1919
2359
  * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
1920
2360
  * pre-dispatch on the workflow's gate FIRST, then this phase's gate, WITHOUT touching
@@ -1926,6 +2366,7 @@ var TaskManager = class {
1926
2366
  var Phase = class {
1927
2367
  #id;
1928
2368
  #name;
2369
+ #description;
1929
2370
  #workflow;
1930
2371
  #escalateUp;
1931
2372
  #tasks = new TaskManager();
@@ -1944,16 +2385,13 @@ var Phase = class {
1944
2385
  const tasks = options?.tasks;
1945
2386
  this.#id = snapshot.id;
1946
2387
  this.#name = snapshot.name;
1947
- if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
1948
- configurable: true,
1949
- value: snapshot.description
1950
- });
2388
+ this.#description = snapshot.description;
1951
2389
  this.#workflow = workflow;
1952
2390
  this.#escalateUp = escalate;
1953
2391
  this.#functions = functions;
1954
2392
  this.#silence = silence;
1955
2393
  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]);
2394
+ for (const task of snapshot.tasks) if (task.behavior !== void 0 && !handlers.has(task.behavior)) handlers.set(task.behavior, functions?.[task.behavior]);
1957
2395
  this.#bail = bail ?? snapshot.bail;
1958
2396
  this.#concurrency = snapshot.concurrency;
1959
2397
  this.#emitter = new Emitter({
@@ -1962,7 +2400,7 @@ var Phase = class {
1962
2400
  });
1963
2401
  for (const task of snapshot.tasks) {
1964
2402
  const taskOptions = tasks?.[task.id];
1965
- const handler = task.run === void 0 ? void 0 : handlers.get(task.run);
2403
+ const handler = task.behavior === void 0 ? void 0 : handlers.get(task.behavior);
1966
2404
  this.#append(task, taskOptions, handler);
1967
2405
  }
1968
2406
  this.#override = snapshot.override;
@@ -1979,11 +2417,14 @@ var Phase = class {
1979
2417
  get name() {
1980
2418
  return this.#name;
1981
2419
  }
2420
+ get description() {
2421
+ return this.#description;
2422
+ }
1982
2423
  get context() {
1983
2424
  return buildPhaseContext(this.#workflow.context, {
1984
2425
  id: this.#id,
1985
2426
  name: this.#name,
1986
- ...this.description === void 0 ? {} : { description: this.description }
2427
+ ...this.#description === void 0 ? {} : { description: this.#description }
1987
2428
  });
1988
2429
  }
1989
2430
  get workflow() {
@@ -2025,7 +2466,7 @@ var Phase = class {
2025
2466
  pause() {
2026
2467
  if (this.#paused || isTerminalStatus(this.status)) return;
2027
2468
  this.#paused = true;
2028
- this.#gate = createDeferred();
2469
+ this.#gate = Promise.withResolvers();
2029
2470
  this.#emitter.emit("pause");
2030
2471
  }
2031
2472
  resume() {
@@ -2087,10 +2528,7 @@ var Phase = class {
2087
2528
  status: this.status
2088
2529
  });
2089
2530
  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
- });
2531
+ if (value.description !== void 0) this.#description = value.description;
2094
2532
  if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
2095
2533
  if (value.bail !== void 0) this.#bail = value.bail;
2096
2534
  }
@@ -2098,7 +2536,7 @@ var Phase = class {
2098
2536
  return {
2099
2537
  id: this.id,
2100
2538
  name: this.name,
2101
- ...this.description === void 0 ? {} : { description: this.description },
2539
+ ...this.#description === void 0 ? {} : { description: this.#description },
2102
2540
  status: this.status,
2103
2541
  ...this.#override === void 0 ? {} : { override: this.#override },
2104
2542
  bail: this.#bail,
@@ -2133,7 +2571,7 @@ var Phase = class {
2133
2571
  }
2134
2572
  #failure() {
2135
2573
  const found = findFailure(this.results());
2136
- if (found === void 0) throw new Error(`phase '${this.id}' derived failed with no failing task result`);
2574
+ if (found === void 0) throw new WorkflowError("INVARIANT", `phase '${this.id}' derived failed with no failing task result`, { phase: this.id });
2137
2575
  return found;
2138
2576
  }
2139
2577
  #release() {
@@ -2151,11 +2589,11 @@ var Phase = class {
2151
2589
  this.#tasks.append(created);
2152
2590
  }
2153
2591
  #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);
2592
+ 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
2593
  }
2156
2594
  #mint(definition) {
2157
2595
  const snapshot = taskDefinitionToSnapshot(definition);
2158
- const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
2596
+ const handler = snapshot.behavior === void 0 ? void 0 : this.#functions?.[snapshot.behavior];
2159
2597
  return this.#create(snapshot, void 0, handler);
2160
2598
  }
2161
2599
  #statuses() {
@@ -2165,24 +2603,28 @@ var Phase = class {
2165
2603
  //#endregion
2166
2604
  //#region src/core/phases/PhaseManager.ts
2167
2605
  /**
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
2606
+ * Implements the lean child manager of a {@link import('../Workflow.js').Workflow}'s live
2607
+ * phases — the phase vocabulary over one insertion-ordered {@link Collection}, the phase analogue
2170
2608
  * of {@link import('../tasks/TaskManager.js').TaskManager}.
2171
2609
  *
2172
2610
  * @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.
2611
+ * - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
2612
+ * the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
2613
+ * `phase` noun its refusals name and the compiled {@link phaseUpdateShape} guard. This class
2614
+ * adds the domain accessors `phase` / `phases` and nothing else.
2615
+ * - **Positional store.** `append` adds one live {@link PhaseInterface} at the end, `phase(id)`
2616
+ * looks one up, `phases()` lists them in positional order, `count` is the tally. A snapshot
2617
+ * RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
2618
+ * - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
2619
+ * `Result` counterparts to `append`, gating ONLY on the target's OWN existence/status/id/bounds
2620
+ * a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
2621
+ * fails {@link phaseUpdateShape} validation all fail gracefully with a `MUTATION`
2622
+ * {@link WorkflowError} instead of throwing.
2623
+ * - **No batch matrix.** A workflow's phases are a fixed positional set, so the batch verbs of
2624
+ * `.claude/rules/patterns.md` § Batch operations are
2625
+ * deliberately omitted.
2626
+ * - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own their own
2627
+ * emitters.
2186
2628
  *
2187
2629
  * @example
2188
2630
  * ```ts
@@ -2193,58 +2635,37 @@ var Phase = class {
2193
2635
  * ```
2194
2636
  */
2195
2637
  var PhaseManager = class {
2196
- #phases = /* @__PURE__ */ new Map();
2197
- #isUpdate = compileGuard(phaseUpdateShape);
2638
+ #phases = new Collection("phase", compileGuard(phaseUpdateShape));
2198
2639
  get count() {
2199
- return this.#phases.size;
2640
+ return this.#phases.count;
2200
2641
  }
2201
2642
  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);
2643
+ this.#phases.append(phase);
2204
2644
  }
2205
2645
  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);
2646
+ return this.#phases.add(phase, index);
2211
2647
  }
2212
2648
  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);
2649
+ return this.#phases.remove(id);
2217
2650
  }
2218
2651
  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);
2652
+ return this.#phases.move(id, index);
2224
2653
  }
2225
2654
  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);
2655
+ return this.#phases.update(id, patch);
2231
2656
  }
2232
2657
  phase(id) {
2233
- return this.#phases.get(id);
2658
+ return this.#phases.entry(id);
2234
2659
  }
2235
2660
  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);
2661
+ return this.#phases.entries();
2241
2662
  }
2242
2663
  };
2243
2664
  //#endregion
2244
2665
  //#region src/core/Workflow.ts
2245
2666
  /**
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
2667
+ * Implements the live DERIVED state machine (W-b) for a whole workflow — the observable ROOT
2668
+ * whose {@link LifecycleStatus} is computed from its phases under the `bail` policy and
2248
2669
  * recomputed reactively as the cascade propagates up from a task transition.
2249
2670
  *
2250
2671
  * @remarks
@@ -2257,7 +2678,7 @@ var PhaseManager = class {
2257
2678
  * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
2258
2679
  * `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
2259
2680
  * change; a CHANGE emits.
2260
- * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
2681
+ * - **Override.** `skip` / `stop` FORCE the status; an executed task-free pending tree
2261
2682
  * may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
2262
2683
  * `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
2263
2684
  * `bail`, so a restore re-derives status identically without a silent policy default.
@@ -2266,12 +2687,12 @@ var PhaseManager = class {
2266
2687
  * navigate UP.
2267
2688
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
2268
2689
  * JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
2269
- * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2690
+ * - **Observable.** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2270
2691
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2271
2692
  * corresponding status or runtime-gate change; the emitter isolates a listener throw and
2272
2693
  * routes it to its `error` handler (the `error` option); `fail` carries the failing task's
2273
2694
  * {@link TaskResult}.
2274
- * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
2695
+ * - **Structural API.** `add` / `remove` / `move` / `update` gate BEFORE
2275
2696
  * delegating to {@link phases} (the manager gates the target's own existence/status/id/
2276
2697
  * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
2277
2698
  * bottom-up gating (no runner-installed hook): refused outright while this workflow's own
@@ -2280,12 +2701,27 @@ var PhaseManager = class {
2280
2701
  * {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
2281
2702
  * workflow's phases are all `pending`, so the boundary is `0` and every position is
2282
2703
  * naturally accepted.
2283
- * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
2704
+ * - **Runtime lifecycle.** `pause` / `resume` / `wait` gate execution at the runner's
2284
2705
  * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
2285
2706
  * persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
2286
2707
  * phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
2287
2708
  * workflow `stop` override when needed, releases its parked waiter, and marks
2288
2709
  * {@link destroyed} — all idempotent.
2710
+ *
2711
+ * @example
2712
+ * ```ts
2713
+ * import { definitionToSnapshot, Workflow } from '@orkestrel/workflow'
2714
+ *
2715
+ * const definition = {
2716
+ * id: 'release',
2717
+ * name: 'Release',
2718
+ * phases: [{ id: 'build', name: 'Build', tasks: [{ id: 'compile', name: 'Compile' }] }],
2719
+ * }
2720
+ * const workflow = new Workflow(definitionToSnapshot(definition))
2721
+ * workflow.status // 'pending'
2722
+ * workflow.phase('build')?.task('compile')?.status // 'pending'
2723
+ * workflow.snapshot().id // 'release'
2724
+ * ```
2289
2725
  */
2290
2726
  var Workflow = class {
2291
2727
  #context;
@@ -2312,7 +2748,6 @@ var Workflow = class {
2312
2748
  const functions = captured.functions;
2313
2749
  const silence = captured.silence;
2314
2750
  this.#context = buildWorkflowContext(snapshot);
2315
- if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
2316
2751
  this.#bail = bail ?? snapshot.bail;
2317
2752
  this.#bailOverride = bail;
2318
2753
  this.#functions = functions;
@@ -2343,6 +2778,9 @@ var Workflow = class {
2343
2778
  get name() {
2344
2779
  return this.#context.name;
2345
2780
  }
2781
+ get description() {
2782
+ return this.#context.description;
2783
+ }
2346
2784
  get context() {
2347
2785
  return this.#context;
2348
2786
  }
@@ -2386,7 +2824,7 @@ var Workflow = class {
2386
2824
  pause() {
2387
2825
  if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
2388
2826
  this.#paused = true;
2389
- this.#gate = createDeferred();
2827
+ this.#gate = Promise.withResolvers();
2390
2828
  this.#emitter.emit("pause");
2391
2829
  }
2392
2830
  resume() {
@@ -2474,7 +2912,7 @@ var Workflow = class {
2474
2912
  return cloneWorkflowSnapshot({
2475
2913
  id: this.id,
2476
2914
  name: this.name,
2477
- ...this.description === void 0 ? {} : { description: this.description },
2915
+ ...this.#context.description === void 0 ? {} : { description: this.#context.description },
2478
2916
  status: this.status,
2479
2917
  ...this.#override === void 0 ? {} : { override: this.#override },
2480
2918
  bail: this.#bail,
@@ -2518,7 +2956,7 @@ var Workflow = class {
2518
2956
  }
2519
2957
  #failure() {
2520
2958
  const found = findFailure(this.results());
2521
- if (found === void 0) throw new Error(`workflow '${this.id}' derived failed with no failing task result`);
2959
+ if (found === void 0) throw new WorkflowError("INVARIANT", `workflow '${this.id}' derived failed with no failing task result`, { workflow: this.id });
2522
2960
  return found;
2523
2961
  }
2524
2962
  #append(phase, options) {
@@ -2543,14 +2981,15 @@ var Workflow = class {
2543
2981
  //#endregion
2544
2982
  //#region src/core/WorkflowManager.ts
2545
2983
  /**
2546
- * The store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
2984
+ * Implements the store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
2547
2985
  * the additive manager tier mirroring the `@orkestrel/agent` line's `ConversationManager` /
2548
2986
  * `WorkspaceManager`. Event-free (a registry, like its twins); the observability lives on each
2549
2987
  * {@link WorkflowInterface}.
2550
2988
  *
2551
2989
  * @remarks
2552
2990
  * - **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
2991
+ * mints a live {@link WorkflowInterface} through the same construction path
2992
+ * {@link import('./factories.js').createWorkflow} takes (flowing the manager's
2554
2993
  * `functions` registry in) and stores it under `definition.id` — an already-present id
2555
2994
  * OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
2556
2995
  * `workflows()` lists them in insertion order.
@@ -2559,8 +2998,8 @@ var Workflow = class {
2559
2998
  * earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
2560
2999
  * workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
2561
3000
  * 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.
3001
+ * - **Removal.** `remove` drops one by id, or a batch (array overload FIRST) — `true` only when
3002
+ * every id was removed. `clear` empties the registry.
2564
3003
  * - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
2565
3004
  * no `active` / `switch` — nothing in the workflow domain renders "the current workflow".
2566
3005
  *
@@ -2598,7 +3037,7 @@ var WorkflowManager = class {
2598
3037
  return [...this.#workflows.values()];
2599
3038
  }
2600
3039
  add(definition) {
2601
- const workflow = createWorkflow(definition, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
3040
+ const workflow = this.#build(definition);
2602
3041
  const mutation = this.#invalidate(workflow.id);
2603
3042
  if (mutation === void 0) this.#additions.delete(workflow.id);
2604
3043
  else this.#additions.set(workflow.id, mutation);
@@ -2635,11 +3074,11 @@ var WorkflowManager = class {
2635
3074
  }
2636
3075
  remove(ids) {
2637
3076
  if (isArray(ids)) {
2638
- let removed = false;
3077
+ let removed = true;
2639
3078
  for (const id of ids) {
2640
3079
  this.#invalidate(id);
2641
3080
  this.#additions.delete(id);
2642
- if (this.#workflows.delete(id)) removed = true;
3081
+ if (!this.#workflows.delete(id)) removed = false;
2643
3082
  }
2644
3083
  return removed;
2645
3084
  }
@@ -2669,7 +3108,7 @@ var WorkflowManager = class {
2669
3108
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2670
3109
  let workflow;
2671
3110
  try {
2672
- workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
3111
+ workflow = this.#restore(owned);
2673
3112
  } catch (error) {
2674
3113
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2675
3114
  throw error;
@@ -2680,6 +3119,15 @@ var WorkflowManager = class {
2680
3119
  this.#releaseHydration(id, lease);
2681
3120
  }
2682
3121
  }
3122
+ #build(definition) {
3123
+ return createWorkflowTree(definition, this.#captured());
3124
+ }
3125
+ #restore(snapshot) {
3126
+ return new Workflow(cloneWorkflowSnapshot(snapshot), this.#captured());
3127
+ }
3128
+ #captured() {
3129
+ return captureWorkflowOptions(this.#functions === void 0 ? {} : { functions: this.#functions });
3130
+ }
2683
3131
  #owns(id, mutation, generation) {
2684
3132
  return this.#generation === generation && this.#mutations.get(id) === mutation;
2685
3133
  }
@@ -2735,9 +3183,50 @@ var WorkflowManager = class {
2735
3183
  }
2736
3184
  };
2737
3185
  //#endregion
3186
+ //#region src/core/RunHolder.ts
3187
+ /**
3188
+ * Holds the active phase {@link RunnerInterface} for one
3189
+ * {@link import('./types.js').WorkflowRunnerInterface.execute} call, for the lifetime of that run.
3190
+ *
3191
+ * @remarks
3192
+ * - **One holder per run.** The engine mints a holder as a run begins and threads that one
3193
+ * instance through every phase of the run, so a nested `execute` reached through application
3194
+ * composition gets its own holder and can never clobber the suspended outer run's.
3195
+ * - **`hold` is the only mutation.** A phase takes the substrate runner with `hold(runner)` as it
3196
+ * starts and releases it with `hold()` as it settles; `runner` reads the held value back and is
3197
+ * `undefined` between phases and after the last one.
3198
+ * - **A cancel closes over the holder.** The run-level abort listener reads `runner` when it
3199
+ * fires, so it reaches whichever phase runner is live at that moment rather than the one that
3200
+ * was live when the listener was armed.
3201
+ * - **Event-free.** A plain cell — no emitter, no lifecycle of its own.
3202
+ */
3203
+ var RunHolder = class {
3204
+ #runner;
3205
+ get runner() {
3206
+ return this.#runner;
3207
+ }
3208
+ /**
3209
+ * Takes the phase runner a starting phase hands this run, or releases the held one.
3210
+ *
3211
+ * @param runner - The phase runner to hold; omitted releases the held runner
3212
+ * @example
3213
+ * ```ts
3214
+ * import type { TaskInterface } from '@orkestrel/workflow'
3215
+ * import { createRunner, RunHolder } from '@orkestrel/workflow'
3216
+ *
3217
+ * const holder = new RunHolder()
3218
+ * holder.hold(createRunner<TaskInterface, void>({ handler: () => undefined }))
3219
+ * holder.hold() // released — `runner` reads `undefined` again
3220
+ * ```
3221
+ */
3222
+ hold(runner) {
3223
+ this.#runner = runner;
3224
+ }
3225
+ };
3226
+ //#endregion
2738
3227
  //#region src/core/Controller.ts
2739
3228
  /**
2740
- * The per-unit handle a runner handler receives — wraps the unit's identity,
3229
+ * Implements the per-unit handle a runner handler receives — wraps the unit's identity,
2741
3230
  * input, cancellation, and the run controls (`wait` / `spawn` / `abort`).
2742
3231
  *
2743
3232
  * @remarks
@@ -2749,10 +3238,10 @@ var WorkflowManager = class {
2749
3238
  * unit's own abort, the runner-level abort (the runner aborts every unit), and
2750
3239
  * the per-attempt timeout — so it fires on any of the three. `aborted` and
2751
3240
  * `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
2752
- * truth); since the attempt signal ANY-includes that abort, `abort()` fires
3241
+ * truth); because the attempt signal ANY-includes that abort, `abort()` fires
2753
3242
  * `signal` too.
2754
3243
  * - **`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
3244
+ * `signal` fires (immediately if already aborted) through a one-shot listener — no
2756
3245
  * `setTimeout`, no polling, no busy-yield — so a parked unit costs no CPU.
2757
3246
  * - **`spawn` is fire-and-track.** It delegates to the runner's launch-a-sibling
2758
3247
  * callback, which routes the sibling through the queue; the runner's `execute`
@@ -2763,23 +3252,32 @@ var WorkflowManager = class {
2763
3252
  * {@link RunnerInterface.emitter} instead (`unit` / `spawn` / `settle` / `fail` carry the id).
2764
3253
  */
2765
3254
  var Controller = class {
2766
- id;
2767
- input;
2768
- signal;
3255
+ #id;
3256
+ #input;
3257
+ #signal;
2769
3258
  #abort;
2770
3259
  #spawn;
2771
3260
  constructor(id, input, abort, signal, spawn) {
2772
- this.id = id;
2773
- this.input = input;
3261
+ this.#id = id;
3262
+ this.#input = input;
2774
3263
  this.#abort = abort;
2775
- this.signal = signal;
3264
+ this.#signal = signal;
2776
3265
  this.#spawn = spawn;
2777
3266
  }
3267
+ get id() {
3268
+ return this.#id;
3269
+ }
3270
+ get input() {
3271
+ return this.#input;
3272
+ }
3273
+ get signal() {
3274
+ return this.#signal;
3275
+ }
2778
3276
  get aborted() {
2779
3277
  return this.#abort.aborted;
2780
3278
  }
2781
3279
  wait() {
2782
- return parkSignal(this.signal);
3280
+ return parkSignal(this.#signal);
2783
3281
  }
2784
3282
  spawn(input) {
2785
3283
  return this.#spawn(input);
@@ -2791,7 +3289,7 @@ var Controller = class {
2791
3289
  //#endregion
2792
3290
  //#region src/core/Runner.ts
2793
3291
  /**
2794
- * A thin generic orchestrator that drives declared units — and any they `spawn` —
3292
+ * Implements a thin generic orchestrator that drives declared units — and any they `spawn` —
2795
3293
  * through a bounded-concurrency {@link createQueue}, collecting ordered results.
2796
3294
  *
2797
3295
  * @remarks
@@ -2805,12 +3303,12 @@ var Controller = class {
2805
3303
  * Results are read back as `#order.map(id => #values.get(id))` — declared first (in
2806
3304
  * input order), then spawns (in spawn order). There is no one-time task snapshot,
2807
3305
  * 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
3306
+ * - **`execute` awaits the full spawn closure through a count gate.** `#launch` increments
2809
3307
  * an outstanding-unit `#count` BEFORE enqueuing and every settle decrements it,
2810
3308
  * resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
2811
3309
  * `#count += 1`) before the parent handler returns, the count never reaches zero
2812
3310
  * mid-run — `execute` parks on `#drained` and so awaits the entire transitive
2813
- * closure, not just the declared units.
3311
+ * closure, not only the declared units.
2814
3312
  * - **`spawn` is fire-and-track.** A spawned unit runs through the queue regardless of
2815
3313
  * whether its promise is awaited; the Runner never awaits a spawned promise from
2816
3314
  * within a handler's slot (it awaits the count gate instead), so a slot-holding
@@ -2824,16 +3322,16 @@ var Controller = class {
2824
3322
  * unit failure (after its retries) records the error and `abort()`s the run, so every
2825
3323
  * sibling's signal fires; later failures are ignored and `execute` rejects with the
2826
3324
  * first error. A user `abort(reason)` likewise rejects a running `execute`.
2827
- * - **`pause` / `resume` / `stop` (§10) ride the backing Queue.** `pause` / `resume`
3325
+ * - **`pause` / `resume` / `stop` ride the backing Queue.** `pause` / `resume`
2828
3326
  * delegate straight to the Queue's own pause/resume (holding/releasing the NEXT
2829
3327
  * dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
2830
3328
  * GRACEFUL permanent end, distinct from `abort`: still-pending (never-dispatched)
2831
3329
  * units are rejected by the Queue's own stop WITHOUT their handler ever running, and
2832
3330
  * `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
2833
3331
  * 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
3332
+ * completion and settles normally. `execute` RESOLVES (never rejects) after every unit
2835
3333
  * has settled, with whatever results actually completed.
2836
- * - **Observable (§13).** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
3334
+ * - **Observable.** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
2837
3335
  * lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
2838
3336
  * fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
2839
3337
  * launch / settle / drain transition; the emitter isolates a listener throw and routes it
@@ -2896,20 +3394,20 @@ var Runner = class {
2896
3394
  return this.#queue.paused;
2897
3395
  }
2898
3396
  /**
2899
- * Inject one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
3397
+ * Injects one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
2900
3398
  * `Controller.spawn`, called from OUTSIDE any unit's handler.
2901
3399
  *
2902
3400
  * @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",
3401
+ * Returns `undefined` synchronously (graceful, non-throwing) unless the
3402
+ * runner is mid-`execute` and not yet stopped — covering "never started",
2905
3403
  * "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-
3404
+ * the SAME backing queue as a declared/`spawn`ed unit through `#launch` — the outstanding-
2907
3405
  * unit count gate increments BEFORE this call returns, so an in-flight `execute`
2908
3406
  * keeps awaiting it (the drain race: `#running` flips to `false` as the very first
2909
3407
  * step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
2910
3408
  * method after the run has fully drained is cleanly rejected with `undefined` —
2911
3409
  * never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
2912
- * with a `parent` of `undefined` (this call has no spawning unit) once accepted.
3410
+ * with a `parent` of `undefined` (this call has no spawning unit) after acceptance.
2913
3411
  *
2914
3412
  * @param input - The unit's work payload
2915
3413
  * @returns The unit's result promise, or `undefined` when no in-flight run can accept it
@@ -2926,11 +3424,11 @@ var Runner = class {
2926
3424
  return this.#launch(input, void 0, true);
2927
3425
  }
2928
3426
  async execute(inputs) {
2929
- if (this.#started) throw new Error("runner has already executed");
2930
- if (this.#stopped) throw new Error("runner is stopped");
3427
+ if (this.#started) throw new WorkflowError("TRANSITION", "runner has already executed", { started: true });
3428
+ if (this.#stopped) throw new WorkflowError("TRANSITION", "runner is stopped", { stopped: true });
2931
3429
  this.#started = true;
2932
3430
  this.#running = true;
2933
- const drained = createDeferred();
3431
+ const drained = Promise.withResolvers();
2934
3432
  this.#drained = drained;
2935
3433
  for (const input of inputs) {
2936
3434
  if (!this.#accepts()) break;
@@ -2951,23 +3449,23 @@ var Runner = class {
2951
3449
  }
2952
3450
  abort(reason) {
2953
3451
  if (this.#abortPromise !== void 0) return this.#abortPromise;
2954
- const barrier = createDeferred();
3452
+ const barrier = Promise.withResolvers();
2955
3453
  this.#abortPromise = barrier.promise;
2956
3454
  barrier.promise.catch(() => {});
2957
- if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
3455
+ if (this.#running && this.#failure === void 0) this.#failure = failure(reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason);
2958
3456
  this.#cancel(reason);
2959
3457
  this.#stopped = true;
2960
3458
  const cleanup = this.#queue.abort(reason);
2961
- this.#settleLifecycle(barrier, cleanup);
3459
+ this.#settleBarrier(barrier, cleanup, false);
2962
3460
  this.#emitter.emit("abort", reason);
2963
3461
  return barrier.promise;
2964
3462
  }
2965
3463
  /**
2966
- * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
3464
+ * Suspends dispatch (resumable): delegates to the backing queue's own
2967
3465
  * `pause`, which holds the NEXT dispatch while any in-flight unit finishes.
2968
3466
  *
2969
3467
  * @remarks
2970
- * A no-op once the runner is `stopped` — a stopped runner has no dispatch left to
3468
+ * A no-op after the runner is `stopped` — a stopped runner has no dispatch left to
2971
3469
  * suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
2972
3470
  * `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
2973
3471
  */
@@ -2976,11 +3474,11 @@ var Runner = class {
2976
3474
  this.#queue.pause();
2977
3475
  }
2978
3476
  /**
2979
- * Continue a paused runner (AGENTS §10); delegates to the backing queue's `resume`.
3477
+ * Continues a paused runner; delegates to the backing queue's `resume`.
2980
3478
  *
2981
3479
  * @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
3480
+ * A no-op after the runner is `stopped` (nothing left to resume) and a no-op when the
3481
+ * runner is not `paused`, so calling it repeatedly or on a never-paused
2984
3482
  * runner is safe.
2985
3483
  */
2986
3484
  resume() {
@@ -2988,7 +3486,7 @@ var Runner = class {
2988
3486
  this.#queue.resume();
2989
3487
  }
2990
3488
  /**
2991
- * Permanently end the runner (AGENTS §10) — a GRACEFUL stop, distinct from `abort`.
3489
+ * Ends the runner permanently — a GRACEFUL stop, distinct from `abort`.
2992
3490
  * Marks the runner `stopping` + `stopped`, then stops the backing queue: every
2993
3491
  * still-PENDING (never-dispatched) unit is rejected by the queue with its own
2994
3492
  * "queue is stopped" error, WITHOUT running its handler; every already-in-flight unit
@@ -3001,24 +3499,24 @@ var Runner = class {
3001
3499
  if (this.#destroyPromise !== void 0) return this.#destroyPromise;
3002
3500
  if (this.#abortPromise !== void 0) return this.#abortPromise;
3003
3501
  if (this.#stopPromise !== void 0) return this.#stopPromise;
3004
- const barrier = createDeferred();
3502
+ const barrier = Promise.withResolvers();
3005
3503
  this.#stopPromise = barrier.promise;
3006
3504
  barrier.promise.catch(() => {});
3007
3505
  this.#stopping = true;
3008
3506
  this.#stopped = true;
3009
3507
  const cleanup = this.#queue.stop();
3010
- this.#settleLifecycle(barrier, cleanup);
3508
+ this.#settleBarrier(barrier, cleanup, false);
3011
3509
  return barrier.promise;
3012
3510
  }
3013
3511
  destroy() {
3014
3512
  if (this.#destroyPromise !== void 0) return this.#destroyPromise;
3015
- const barrier = createDeferred();
3513
+ const barrier = Promise.withResolvers();
3016
3514
  this.#destroyPromise = barrier.promise;
3017
3515
  barrier.promise.catch(() => {});
3018
3516
  this.#stopped = true;
3019
3517
  this.abort();
3020
3518
  const cleanup = this.#queue.destroy();
3021
- this.#settleDestroy(barrier, cleanup);
3519
+ this.#settleBarrier(barrier, cleanup, true);
3022
3520
  return barrier.promise;
3023
3521
  }
3024
3522
  #launch(input, parent, announce = parent !== void 0) {
@@ -3046,33 +3544,27 @@ var Runner = class {
3046
3544
  } catch (error) {
3047
3545
  promise = Promise.reject(error);
3048
3546
  }
3049
- promise.then((value) => this.#settle(id, {
3050
- ok: true,
3051
- value
3052
- }), (error) => this.#settle(id, {
3053
- ok: false,
3054
- error
3055
- }));
3547
+ promise.then((value) => this.#settle(id, success(value)), (error) => this.#settle(id, failure(error)));
3056
3548
  return promise;
3057
3549
  }
3058
- #dispatch(unit, execution) {
3550
+ #dispatch(unit, context) {
3059
3551
  const abort = this.#aborts.get(unit.id);
3060
- if (abort === void 0) throw new Error("unit abort missing");
3552
+ if (abort === void 0) throw new WorkflowError("INVARIANT", "unit abort missing", { unit: unit.id });
3061
3553
  this.#dispatched.add(unit.id);
3062
- const controller = new Controller(unit.id, unit.input, abort, execution.signal, (input) => this.#spawn(input, unit.id));
3554
+ const controller = new Controller(unit.id, unit.input, abort, context.signal, (input) => this.#spawn(input, unit.id));
3063
3555
  this.#emitter.emit("unit", unit.id);
3064
3556
  return this.#handler(controller);
3065
3557
  }
3066
3558
  #spawn(input, parent) {
3067
- if (!this.#accepts()) throw new Error("spawn is unavailable outside an active run");
3559
+ if (!this.#accepts()) throw new WorkflowError("TRANSITION", "spawn is unavailable outside an active run", { parent });
3068
3560
  return this.#launch(input, parent);
3069
3561
  }
3070
3562
  #settle(id, outcome) {
3071
- if (outcome.ok) {
3072
- this.#values.set(id, { value: outcome.value });
3563
+ if (outcome.success) {
3564
+ this.#values.set(id, outcome);
3073
3565
  this.#emitter.emit("settle", id);
3074
3566
  } else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
3075
- this.#failure = { error: outcome.error };
3567
+ this.#failure = failure(outcome.error);
3076
3568
  this.#emitter.emit("fail", id, outcome.error);
3077
3569
  this.abort(outcome.error);
3078
3570
  }
@@ -3097,31 +3589,20 @@ var Runner = class {
3097
3589
  await cleanup;
3098
3590
  return;
3099
3591
  } catch (error) {
3100
- return { error };
3592
+ return failure(error);
3101
3593
  }
3102
3594
  }
3103
- async #settleLifecycle(barrier, cleanup) {
3104
- let failure;
3595
+ async #settleBarrier(barrier, cleanup, teardown) {
3596
+ let cleanupFailure;
3105
3597
  try {
3106
3598
  await cleanup;
3107
3599
  } catch (error) {
3108
- failure = { error };
3600
+ cleanupFailure = failure(error);
3109
3601
  }
3110
3602
  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);
3603
+ if (teardown) this.#emitter.destroy();
3604
+ if (cleanupFailure === void 0) barrier.resolve();
3605
+ else barrier.reject(cleanupFailure.error);
3125
3606
  }
3126
3607
  async #waitDrain() {
3127
3608
  if (this.#count === 0) return;
@@ -3134,7 +3615,7 @@ var Runner = class {
3134
3615
  //#endregion
3135
3616
  //#region src/core/tasks/TaskController.ts
3136
3617
  /**
3137
- * The attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
3618
+ * Implements the attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
3138
3619
  *
3139
3620
  * @remarks
3140
3621
  * - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
@@ -3157,26 +3638,38 @@ var Runner = class {
3157
3638
  * observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
3158
3639
  */
3159
3640
  var TaskController = class {
3160
- signal;
3161
- input;
3162
- task;
3163
- attempt;
3641
+ #signal;
3642
+ #input;
3643
+ #task;
3644
+ #attempt;
3164
3645
  #entity;
3165
3646
  #report;
3166
3647
  #pulse;
3167
3648
  #results;
3168
3649
  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;
3650
+ this.#signal = signal;
3651
+ this.#input = input;
3652
+ this.#task = task.context;
3653
+ this.#attempt = attempt;
3173
3654
  this.#entity = task;
3174
3655
  this.#results = results;
3175
3656
  this.#report = report;
3176
3657
  this.#pulse = pulse;
3177
3658
  }
3659
+ get signal() {
3660
+ return this.#signal;
3661
+ }
3662
+ get input() {
3663
+ return this.#input;
3664
+ }
3665
+ get task() {
3666
+ return this.#task;
3667
+ }
3668
+ get attempt() {
3669
+ return this.#attempt;
3670
+ }
3178
3671
  get aborted() {
3179
- return this.signal.aborted;
3672
+ return this.#signal.aborted;
3180
3673
  }
3181
3674
  get paused() {
3182
3675
  if (this.#ancestorTerminal()) return false;
@@ -3189,7 +3682,7 @@ var TaskController = class {
3189
3682
  return this.#pulse();
3190
3683
  }
3191
3684
  async wait() {
3192
- while (this.paused && !this.signal.aborted) await this.#race(this.#gates());
3685
+ while (this.paused && !this.#signal.aborted) await this.#race(this.#gates());
3193
3686
  }
3194
3687
  results() {
3195
3688
  return this.#results();
@@ -3203,11 +3696,11 @@ var TaskController = class {
3203
3696
  return gates;
3204
3697
  }
3205
3698
  async #race(gates) {
3206
- if (this.signal.aborted || gates.length === 0) return;
3699
+ if (this.#signal.aborted || gates.length === 0) return;
3207
3700
  const deferred = Promise.withResolvers();
3208
3701
  const onAbort = this.#resolve.bind(this, deferred);
3209
3702
  const onTerminal = this.#resolve.bind(this, deferred);
3210
- this.signal.addEventListener("abort", onAbort, { once: true });
3703
+ this.#signal.addEventListener("abort", onAbort, { once: true });
3211
3704
  this.#entity.workflow.emitter.on("skip", onTerminal);
3212
3705
  this.#entity.workflow.emitter.on("stop", onTerminal);
3213
3706
  this.#entity.phase.emitter.on("skip", onTerminal);
@@ -3216,7 +3709,7 @@ var TaskController = class {
3216
3709
  if (this.#ancestorTerminal()) deferred.resolve();
3217
3710
  await Promise.race([Promise.all(gates), deferred.promise]);
3218
3711
  } finally {
3219
- this.signal.removeEventListener("abort", onAbort);
3712
+ this.#signal.removeEventListener("abort", onAbort);
3220
3713
  this.#entity.workflow.emitter.off("skip", onTerminal);
3221
3714
  this.#entity.workflow.emitter.off("stop", onTerminal);
3222
3715
  this.#entity.phase.emitter.off("skip", onTerminal);
@@ -3233,24 +3726,33 @@ var TaskController = class {
3233
3726
  //#endregion
3234
3727
  //#region src/core/WorkflowPersistence.ts
3235
3728
  /**
3236
- * Advanced run-local snapshot persistence with one writer and one coalesced latest obligation.
3729
+ * Coordinates advanced run-local snapshot persistence with one writer and one coalesced most recent obligation.
3237
3730
  *
3238
3731
  * @remarks
3239
3732
  * Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
3240
3733
  * coordinate the same required boundaries around their own runner integration.
3734
+ *
3735
+ * @example
3736
+ * ```ts
3737
+ * import { WorkflowPersistence, createMemoryWorkflowStore, createWorkflow } from '@orkestrel/workflow'
3738
+ *
3739
+ * const workflow = createWorkflow({ id: 'durable', name: 'Durable', phases: [] })
3740
+ * const persistence = new WorkflowPersistence(workflow, createMemoryWorkflowStore())
3741
+ * await persistence.checkpoint('initial')
3742
+ * const durable = await persistence.finalize()
3743
+ * persistence.detach() // idempotent after finalize
3744
+ * ```
3241
3745
  */
3242
3746
  var WorkflowPersistence = class {
3243
3747
  #workflow;
3244
3748
  #store;
3245
3749
  #phases = /* @__PURE__ */ new Set();
3246
3750
  #tasks = /* @__PURE__ */ new Set();
3247
- #onWorkflowChange;
3751
+ #onChange;
3248
3752
  #onWorkflowAdd;
3249
3753
  #onWorkflowRemove;
3250
- #onPhaseChange;
3251
3754
  #onPhaseAdd;
3252
3755
  #onPhaseRemove;
3253
- #onTaskChange;
3254
3756
  #writing;
3255
3757
  #error;
3256
3758
  #fault;
@@ -3260,32 +3762,29 @@ var WorkflowPersistence = class {
3260
3762
  constructor(workflow, store) {
3261
3763
  this.#workflow = workflow;
3262
3764
  this.#store = store;
3263
- this.#onWorkflowChange = this.#change.bind(this);
3765
+ this.#onChange = this.#change.bind(this);
3264
3766
  this.#onWorkflowAdd = this.#addPhase.bind(this);
3265
3767
  this.#onWorkflowRemove = this.#removePhase.bind(this);
3266
- this.#onPhaseChange = this.#change.bind(this);
3267
3768
  this.#onPhaseAdd = this.#addTask.bind(this);
3268
3769
  this.#onPhaseRemove = this.#removeTask.bind(this);
3269
- this.#onTaskChange = this.#change.bind(this);
3270
3770
  this.#attachWorkflow();
3271
3771
  }
3272
3772
  get fault() {
3273
3773
  return this.#fault;
3274
3774
  }
3275
3775
  /**
3276
- * Persist every change through this required boundary.
3776
+ * Persists every change through this required boundary.
3277
3777
  *
3278
3778
  * @param checkpoint - The boundary being made durable
3279
3779
  * @param task - The task owning an attempt or settlement
3280
3780
  * @param attempt - The persisted attempt number
3281
- * @returns Whether the latest state reached the store
3781
+ * @returns True if the most recent state reached the store; false otherwise
3282
3782
  */
3283
3783
  async checkpoint(checkpoint, task, attempt) {
3284
3784
  const revision = this.#mark();
3285
3785
  while (this.#stored < revision) await this.#flush();
3286
3786
  if (this.#error === void 0) return true;
3287
3787
  if (this.#fault === void 0) this.#fault = Object.freeze({
3288
- origin: "persistence",
3289
3788
  checkpoint,
3290
3789
  message: this.#error,
3291
3790
  ...task === void 0 ? {} : { task: task.id },
@@ -3294,37 +3793,25 @@ var WorkflowPersistence = class {
3294
3793
  return false;
3295
3794
  }
3296
3795
  /**
3297
- * Stop observing the live tree and persist its final state.
3796
+ * Stops observing the live tree and persists its final state.
3298
3797
  *
3299
- * @returns Whether the final snapshot reached the store
3798
+ * @returns True if the final snapshot reached the store; false otherwise
3300
3799
  */
3301
3800
  async finalize() {
3302
3801
  this.detach();
3303
3802
  return this.checkpoint("final");
3304
3803
  }
3305
- /** Stop observing the live tree. */
3804
+ /** Stops observing the live tree. */
3306
3805
  detach() {
3307
3806
  if (!this.#attached) return;
3308
3807
  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);
3808
+ for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.off(event, this.#onChange);
3316
3809
  this.#workflow.emitter.off("add", this.#onWorkflowAdd);
3317
3810
  this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
3318
3811
  for (const phase of this.#phases) this.#detachPhase(phase);
3319
3812
  }
3320
3813
  #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);
3814
+ for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.on(event, this.#onChange);
3328
3815
  this.#workflow.emitter.on("add", this.#onWorkflowAdd);
3329
3816
  this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
3330
3817
  for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
@@ -3332,26 +3819,14 @@ var WorkflowPersistence = class {
3332
3819
  #attachPhase(phase) {
3333
3820
  if (this.#phases.has(phase)) return;
3334
3821
  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);
3822
+ for (const event of PERSISTED_NODE_EVENTS) phase.emitter.on(event, this.#onChange);
3342
3823
  phase.emitter.on("add", this.#onPhaseAdd);
3343
3824
  phase.emitter.on("remove", this.#onPhaseRemove);
3344
3825
  for (const task of phase.tasks.tasks()) this.#attachTask(task);
3345
3826
  }
3346
3827
  #detachPhase(phase) {
3347
3828
  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);
3829
+ for (const event of PERSISTED_NODE_EVENTS) phase.emitter.off(event, this.#onChange);
3355
3830
  phase.emitter.off("add", this.#onPhaseAdd);
3356
3831
  phase.emitter.off("remove", this.#onPhaseRemove);
3357
3832
  for (const task of phase.tasks.tasks()) this.#detachTask(task);
@@ -3359,23 +3834,11 @@ var WorkflowPersistence = class {
3359
3834
  #attachTask(task) {
3360
3835
  if (this.#tasks.has(task)) return;
3361
3836
  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);
3837
+ for (const event of PERSISTED_TASK_EVENTS) task.emitter.on(event, this.#onChange);
3369
3838
  }
3370
3839
  #detachTask(task) {
3371
3840
  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);
3841
+ for (const event of PERSISTED_TASK_EVENTS) task.emitter.off(event, this.#onChange);
3379
3842
  }
3380
3843
  #addPhase(phase) {
3381
3844
  this.#attachPhase(phase);
@@ -3433,13 +3896,14 @@ var WorkflowPersistence = class {
3433
3896
  //#endregion
3434
3897
  //#region src/core/WorkflowRunner.ts
3435
3898
  /**
3436
- * The thin orchestrator that EXECUTES a live W-b workflow tree by COMPOSING the shipped
3899
+ * Implements the thin orchestrator that EXECUTES a live W-b workflow tree by COMPOSING the shipped
3437
3900
  * substrate — phases sequential, tasks concurrent — dispatching each task through its OWN
3438
3901
  * resolved handler under the `bail` policy.
3439
3902
  *
3440
3903
  * @remarks
3441
3904
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
3442
- * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
3905
+ * {@link createRunner} per phase (the substrate {@link import('./types.js').RunnerInterface}
3906
+ * over the workers
3443
3907
  * `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
3444
3908
  * timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
3445
3909
  * {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
@@ -3453,15 +3917,15 @@ var WorkflowPersistence = class {
3453
3917
  * resolved its own {@link import('./types.js').WorkflowFunction} into
3454
3918
  * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
3455
3919
  * 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
3920
+ * dispatch is "invoke the task's own handler". Provider, protocol, and tool
3457
3921
  * integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
3458
3922
  * composed into {@link WorkflowOptions.functions}. This module imports none of them.
3459
3923
  * - **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`
3924
+ * from a {@link WorkflowDefinition} (single source of truth for the `behavior` / `concurrency`
3461
3925
  * 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
3926
+ * {@link WorkflowInterface} instead — the entity-native control surface
3927
+ * (`pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
3928
+ * converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition` after the tree
3465
3929
  * exists — `#runTask` reads each task's OWN {@link import('./types.js').TaskInterface.handler}
3466
3930
  * / `retries` / `timeout`, and `#runPhase` reads each phase's OWN
3467
3931
  * {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
@@ -3475,7 +3939,7 @@ var WorkflowPersistence = class {
3475
3939
  * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
3476
3940
  * phase always reaches a coherent terminal state.
3477
3941
  * - **Dispatch by handler.** `#runTask` invokes the live task's own
3478
- * {@link import('./types.js').TaskInterface.handler} directly. An omitted `run` deliberately
3942
+ * {@link import('./types.js').TaskInterface.handler} directly. An omitted `behavior` deliberately
3479
3943
  * auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
3480
3944
  * execution claim and never false-completes.
3481
3945
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
@@ -3506,9 +3970,9 @@ var WorkflowPersistence = class {
3506
3970
  * and the workflow is force-`stop`ped (settles `stopped`). Each task's
3507
3971
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
3508
3972
  * `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.
3973
+ * - **Re-entrant-safe.** No shared per-run mutable field: each `#execute` mints its own
3974
+ * {@link import('./RunHolder.js').RunHolder}, so a nested application-level `execute` cannot
3975
+ * clobber the outer run's state.
3512
3976
  */
3513
3977
  var WorkflowRunner = class WorkflowRunner {
3514
3978
  static #executions = /* @__PURE__ */ new WeakSet();
@@ -3517,7 +3981,7 @@ var WorkflowRunner = class WorkflowRunner {
3517
3981
  this.#scheduler = scheduler;
3518
3982
  }
3519
3983
  execute(target, options) {
3520
- if (this.#isWorkflow(target)) {
3984
+ if (isWorkflowInterface(target)) {
3521
3985
  const signal = options?.signal;
3522
3986
  const timeout = options?.timeout;
3523
3987
  const budget = options?.budget;
@@ -3530,7 +3994,7 @@ var WorkflowRunner = class WorkflowRunner {
3530
3994
  const timeout = options?.timeout;
3531
3995
  const budget = options?.budget;
3532
3996
  const store = options?.store;
3533
- const workflow = new Workflow(definitionToSnapshot(target, captured.bail ?? target.bail ?? false), captured);
3997
+ const workflow = createWorkflowTree(target, captured);
3534
3998
  this.#acquire(workflow);
3535
3999
  return this.#execute(workflow, signal, timeout, budget, store);
3536
4000
  }
@@ -3544,7 +4008,7 @@ var WorkflowRunner = class WorkflowRunner {
3544
4008
  WorkflowRunner.#executions.add(workflow);
3545
4009
  }
3546
4010
  async #execute(workflow, signal, ms, budget, store) {
3547
- const holder = { runner: void 0 };
4011
+ const holder = new RunHolder();
3548
4012
  let timeout;
3549
4013
  let persistence;
3550
4014
  let runSignal;
@@ -3559,7 +4023,7 @@ var WorkflowRunner = class WorkflowRunner {
3559
4023
  if (runSignal.aborted) onCancel();
3560
4024
  else runSignal.addEventListener("abort", onCancel, { once: true });
3561
4025
  if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
3562
- if (this.#stoppable(workflow)) workflow.stop();
4026
+ if (isStoppable(workflow)) workflow.stop();
3563
4027
  this.#skipFrom(workflow.phases.phases(), 0);
3564
4028
  }
3565
4029
  let index = 0;
@@ -3571,12 +4035,12 @@ var WorkflowRunner = class WorkflowRunner {
3571
4035
  index += 1;
3572
4036
  continue;
3573
4037
  }
3574
- if (this.#cancelled(runSignal) || this.#halted(workflow)) {
4038
+ if (runSignal.aborted || isHalted(workflow)) {
3575
4039
  this.#haltFrom(phases, index, workflow, runSignal);
3576
4040
  break;
3577
4041
  }
3578
4042
  if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
3579
- if (this.#cancelled(runSignal) || this.#halted(workflow)) {
4043
+ if (runSignal.aborted || isHalted(workflow)) {
3580
4044
  this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
3581
4045
  break;
3582
4046
  }
@@ -3591,10 +4055,10 @@ var WorkflowRunner = class WorkflowRunner {
3591
4055
  }
3592
4056
  index += 1;
3593
4057
  const remaining = workflow.phases.phases();
3594
- if (index < remaining.length && !this.#cancelled(runSignal)) await this.#pace(runSignal);
4058
+ if (index < remaining.length && !runSignal.aborted) await this.#pace(runSignal);
3595
4059
  }
3596
- if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
3597
- else if (this.#completable(workflow)) workflow.complete();
4060
+ if (runSignal.aborted) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
4061
+ else if (isCompletable(workflow)) workflow.complete();
3598
4062
  const durable = await persistence?.finalize();
3599
4063
  return {
3600
4064
  workflow,
@@ -3604,7 +4068,7 @@ var WorkflowRunner = class WorkflowRunner {
3604
4068
  ...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
3605
4069
  };
3606
4070
  } catch (error) {
3607
- if (this.#stoppable(workflow)) workflow.stop();
4071
+ if (isStoppable(workflow)) workflow.stop();
3608
4072
  this.#skipFrom(workflow.phases.phases(), 0);
3609
4073
  await persistence?.finalize();
3610
4074
  throw error;
@@ -3639,22 +4103,22 @@ var WorkflowRunner = class WorkflowRunner {
3639
4103
  entries: this.#entry.bind(this),
3640
4104
  handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
3641
4105
  });
3642
- holder.runner = created;
4106
+ holder.hold(created);
3643
4107
  try {
3644
4108
  await created.execute(tasks);
3645
4109
  return false;
3646
4110
  } catch {
3647
- return !this.#cancelled(runSignal);
4111
+ return !runSignal.aborted;
3648
4112
  } finally {
3649
4113
  try {
3650
4114
  await created.destroy();
3651
4115
  } finally {
3652
- holder.runner = void 0;
4116
+ holder.hold();
3653
4117
  }
3654
4118
  }
3655
4119
  } finally {
3656
4120
  phase.emitter.off("add", onAdd);
3657
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4121
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3658
4122
  for (const task of phase.tasks.tasks()) this.#skip(task);
3659
4123
  }
3660
4124
  }
@@ -3678,7 +4142,7 @@ var WorkflowRunner = class WorkflowRunner {
3678
4142
  attempts.set(task.id, attempt);
3679
4143
  const last = attempt > Math.max(0, task.retries ?? 0);
3680
4144
  if (task.status !== "pending" && task.status !== "running") return;
3681
- if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
4145
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3682
4146
  this.#settleCancelled(task, workflow, runSignal);
3683
4147
  return;
3684
4148
  }
@@ -3691,15 +4155,15 @@ var WorkflowRunner = class WorkflowRunner {
3691
4155
  owners.set(task.id, attempt);
3692
4156
  deadline?.start();
3693
4157
  const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
3694
- if (!this.#owns(owners, task, attempt)) return;
4158
+ if (!ownsAttempt(owners, task, attempt)) return;
3695
4159
  if (!durable) {
3696
- if (this.#stoppable(workflow)) workflow.stop();
4160
+ if (isStoppable(workflow)) workflow.stop();
3697
4161
  return;
3698
4162
  }
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}'`, {
4163
+ if (task.behavior !== void 0 && task.handler === void 0) {
4164
+ const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved behavior '${task.behavior}'`, {
3701
4165
  task: task.id,
3702
- run: task.run
4166
+ behavior: task.behavior
3703
4167
  });
3704
4168
  task.fail({
3705
4169
  origin: "handler",
@@ -3711,21 +4175,21 @@ var WorkflowRunner = class WorkflowRunner {
3711
4175
  if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3712
4176
  if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3713
4177
  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)) {
4178
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3715
4179
  this.#settleCancelled(task, workflow, runSignal);
3716
4180
  return;
3717
4181
  }
3718
4182
  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`, {
4183
+ 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
4184
  task: task.id,
3721
4185
  attempt
3722
- })), () => this.#owns(owners, task, attempt) && !signal.aborted && task.pulse());
4186
+ })), () => ownsAttempt(owners, task, attempt) && !signal.aborted && task.pulse());
3723
4187
  let outcome;
3724
4188
  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));
4189
+ outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, () => isSkipping(task, controller, runSignal));
3726
4190
  } catch (error) {
3727
- if (!this.#owns(owners, task, attempt)) return;
3728
- if (task.status !== "running" || this.#skipping(task, controller, runSignal)) {
4191
+ if (!ownsAttempt(owners, task, attempt)) return;
4192
+ if (task.status !== "running" || isSkipping(task, controller, runSignal)) {
3729
4193
  this.#settleCancelled(task, workflow, runSignal);
3730
4194
  return;
3731
4195
  }
@@ -3736,13 +4200,13 @@ var WorkflowRunner = class WorkflowRunner {
3736
4200
  this.#failed(owners, task, attempt, error, last, bail);
3737
4201
  return;
3738
4202
  }
3739
- if (!this.#owns(owners, task, attempt)) return;
4203
+ if (!ownsAttempt(owners, task, attempt)) return;
3740
4204
  if (!outcome[0]) {
3741
4205
  this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
3742
4206
  return;
3743
4207
  }
3744
4208
  if (task.status !== "running") return;
3745
- if (this.#skipping(task, controller, runSignal)) {
4209
+ if (isSkipping(task, controller, runSignal)) {
3746
4210
  this.#settleCancelled(task, workflow, runSignal);
3747
4211
  return;
3748
4212
  }
@@ -3750,32 +4214,32 @@ var WorkflowRunner = class WorkflowRunner {
3750
4214
  this.#timedOut(owners, task, attempt, last, bail);
3751
4215
  return;
3752
4216
  }
3753
- if (!this.#owns(owners, task, attempt)) return;
4217
+ if (!ownsAttempt(owners, task, attempt)) return;
3754
4218
  try {
3755
4219
  task.complete(outcome[1]);
3756
4220
  } catch (error) {
3757
- if (!this.#owns(owners, task, attempt)) return;
4221
+ if (!ownsAttempt(owners, task, attempt)) return;
3758
4222
  if (task.status !== "running") throw error;
3759
4223
  this.#failed(owners, task, attempt, error, last, bail);
3760
4224
  }
3761
4225
  } finally {
3762
4226
  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();
4227
+ if (persistence !== void 0 && ownsAttempt(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && isStoppable(workflow)) workflow.stop();
3764
4228
  this.#revoke(owners, task.id, attempt);
3765
4229
  }
3766
4230
  }
3767
4231
  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);
4232
+ const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, () => isSkipping(task, controller, runSignal), workflow, task.phase);
3769
4233
  return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
3770
4234
  }
3771
4235
  #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;
4236
+ if (attempts.get(task.id) !== attempt || !ownsAttempt(owners, task, attempt)) return true;
3773
4237
  if (signal.aborted) {
3774
- if (genuine ?? this.#skipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
4238
+ if (genuine ?? isSkipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
3775
4239
  else this.#timedOut(owners, task, attempt, last, bail);
3776
4240
  return true;
3777
4241
  }
3778
- if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
4242
+ if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
3779
4243
  this.#settleCancelled(task, workflow, runSignal);
3780
4244
  return true;
3781
4245
  }
@@ -3804,7 +4268,7 @@ var WorkflowRunner = class WorkflowRunner {
3804
4268
  ]);
3805
4269
  }
3806
4270
  #timedOut(owners, task, attempt, last, bail) {
3807
- if (!this.#owns(owners, task, attempt)) return;
4271
+ if (!ownsAttempt(owners, task, attempt)) return;
3808
4272
  const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
3809
4273
  if (last) task.fail({
3810
4274
  origin: "timeout",
@@ -3813,7 +4277,7 @@ var WorkflowRunner = class WorkflowRunner {
3813
4277
  if (!last || bail) throw error;
3814
4278
  }
3815
4279
  #failed(owners, task, attempt, error, last, bail) {
3816
- if (!this.#owns(owners, task, attempt)) return;
4280
+ if (!ownsAttempt(owners, task, attempt)) return;
3817
4281
  if (!last) throw error;
3818
4282
  task.fail({
3819
4283
  origin: "handler",
@@ -3821,9 +4285,6 @@ var WorkflowRunner = class WorkflowRunner {
3821
4285
  });
3822
4286
  if (bail) throw error;
3823
4287
  }
3824
- #owns(owners, task, attempt) {
3825
- return owners.get(task.id) === attempt && task.attempts === attempt;
3826
- }
3827
4288
  #revoke(owners, id, attempt) {
3828
4289
  if (owners.get(id) === attempt) owners.delete(id);
3829
4290
  }
@@ -3838,7 +4299,7 @@ var WorkflowRunner = class WorkflowRunner {
3838
4299
  phase?.emitter.on("skip", onTerminal);
3839
4300
  phase?.emitter.on("stop", onTerminal);
3840
4301
  try {
3841
- if (workflow !== void 0 && this.#halted(workflow, phase)) deferred.resolve(void 0);
4302
+ if (workflow !== void 0 && isHalted(workflow, phase)) deferred.resolve(void 0);
3842
4303
  const outcome = await Promise.race([wait, deferred.promise]);
3843
4304
  return typeof outcome === "boolean" ? outcome : void 0;
3844
4305
  } finally {
@@ -3869,7 +4330,7 @@ var WorkflowRunner = class WorkflowRunner {
3869
4330
  return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
3870
4331
  }
3871
4332
  #haltFrom(phases, index, workflow, runSignal) {
3872
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4333
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3873
4334
  this.#skipFrom(phases, index);
3874
4335
  }
3875
4336
  #skipFrom(phases, index) {
@@ -3880,37 +4341,17 @@ var WorkflowRunner = class WorkflowRunner {
3880
4341
  }
3881
4342
  }
3882
4343
  #settleCancelled(task, workflow, runSignal) {
3883
- if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
4344
+ if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
3884
4345
  this.#skip(task);
3885
4346
  }
3886
4347
  #skip(task) {
3887
4348
  if (task.status === "pending" || task.status === "running") task.skip();
3888
4349
  }
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
4350
  };
3910
4351
  //#endregion
3911
4352
  //#region src/core/factories.ts
3912
4353
  /**
3913
- * Compile the workflow definition contract — the JSON Schema, guard, parser, and
4354
+ * Compiles the workflow definition contract — the JSON Schema, guard, parser, and
3914
4355
  * seeded generator for a {@link WorkflowDefinition}, all derived from one shape and
3915
4356
  * kept in lockstep.
3916
4357
  *
@@ -3940,7 +4381,7 @@ function createWorkflowContract() {
3940
4381
  return createContract(workflowShape);
3941
4382
  }
3942
4383
  /**
3943
- * Build the live W-b entity tree from a {@link WorkflowDefinition} — the whole
4384
+ * Builds the live W-b entity tree from a {@link WorkflowDefinition} — the whole
3944
4385
  * {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
3945
4386
  * {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
3946
4387
  * context, its emitter, and the cascade.
@@ -3952,11 +4393,11 @@ function createWorkflowContract() {
3952
4393
  * definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
3953
4394
  * feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
3954
4395
  * 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
4396
+ * `options.phases[id].tasks[id]` (the nested-by-id bag). The W-b tree is the
3956
4397
  * state machine ONLY — it does not execute tasks (W-c drives the transitions).
3957
4398
  *
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
4399
+ * `options.functions` is the {@link import('./types.js').WorkflowRegistry} registry each live
4400
+ * task's `behavior` name resolves against ONCE at construction into its runtime
3960
4401
  * {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
3961
4402
  * an unresolved present name remains inspectable but is rejected if execution is attempted.
3962
4403
  *
@@ -3974,11 +4415,47 @@ function createWorkflowContract() {
3974
4415
  * ```
3975
4416
  */
3976
4417
  function createWorkflow(definition, options) {
3977
- const captured = captureWorkflowOptions(options);
3978
- return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
4418
+ return createWorkflowTree(definition, captureWorkflowOptions(options));
4419
+ }
4420
+ /**
4421
+ * Builds the live entity tree one definition and one owned options bag describe — the shared
4422
+ * construction path behind every definition-driven mint.
4423
+ *
4424
+ * @remarks
4425
+ * Seeds an initial all-`pending` {@link WorkflowSnapshot} from the definition and constructs the
4426
+ * live {@link WorkflowInterface} over it. `bail` is the caller's own override, forwarded to
4427
+ * {@link definitionToSnapshot} so it reaches BOTH tiers: the workflow snapshot AND the inheritance
4428
+ * default of every phase that declares no `bail` of its own, while a phase declaring one still
4429
+ * wins. Omitted, the definition's own `bail` governs, defaulting to the graceful
4430
+ * {@link import('./constants.js').DEFAULT_BAIL}.
4431
+ *
4432
+ * `captured` is forwarded to the entity UNCHANGED — its own `bail` is deliberately not replaced
4433
+ * with the resolved policy, because the snapshot already carries the resolved value at both tiers
4434
+ * and an injected one would make `Workflow` read it as an EXPLICIT uniform override and clobber
4435
+ * the per-phase overrides. Each task's `behavior` / `retries` / `timeout` travel onto the snapshot
4436
+ * too, so `captured.functions` resolves every handler identically whether the tree is built fresh
4437
+ * or restored. Pass a bag {@link captureWorkflowOptions} already owns: this constructs over it
4438
+ * without re-capturing.
4439
+ *
4440
+ * @param definition - The workflow definition to bring to life
4441
+ * @param captured - The already-owned {@link WorkflowOptions} bag the entity is constructed with,
4442
+ * whose `bail` is the caller's failure-policy override, or `undefined` to take the definition's
4443
+ * @returns The live {@link WorkflowInterface} root
4444
+ *
4445
+ * @example
4446
+ * ```ts
4447
+ * import { captureWorkflowOptions, createWorkflowTree } from '@orkestrel/workflow'
4448
+ *
4449
+ * const captured = captureWorkflowOptions({ bail: true })
4450
+ * const workflow = createWorkflowTree(definition, captured)
4451
+ * workflow.bail // true — the override reached the workflow and every inheriting phase
4452
+ * ```
4453
+ */
4454
+ function createWorkflowTree(definition, captured) {
4455
+ return new Workflow(definitionToSnapshot(definition, captured.bail), captured);
3979
4456
  }
3980
4457
  /**
3981
- * Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
4458
+ * Builds an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3982
4459
  * inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
3983
4460
  * + recorded results + positional order + the persisted `#override`.
3984
4461
  *
@@ -3993,7 +4470,7 @@ function createWorkflow(definition, options) {
3993
4470
  * still wins when supplied (to deliberately re-run under a different policy). A structurally
3994
4471
  * invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
3995
4472
  * non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
3996
- * Runtime handlers are optional: without a matching `functions` entry, a persisted `run`
4473
+ * Runtime handlers are optional: without a matching `functions` entry, a persisted `behavior`
3997
4474
  * remains visible with an undefined `handler` so the exact state is inspectable. The runner
3998
4475
  * rejects that unresolved tree if execution is attempted.
3999
4476
  *
@@ -4014,10 +4491,10 @@ function createRestoredWorkflow(snapshot, options) {
4014
4491
  return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
4015
4492
  }
4016
4493
  /**
4017
- * Build an interrupted workflow back to life at its remaining retry budget.
4494
+ * Builds an interrupted workflow back to life at its remaining retry budget.
4018
4495
  *
4019
4496
  * @remarks
4020
- * Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
4497
+ * Each phase captures every unique initial `behavior` binding once before constructing tasks. Recovery
4021
4498
  * validates those live tasks' captured callable handlers without rereading the registry, while the
4022
4499
  * retained registry identity remains available to resolve future live additions at their mint time.
4023
4500
  *
@@ -4038,18 +4515,19 @@ function createRecoveredWorkflow(snapshot, options) {
4038
4515
  const owned = cloneWorkflowSnapshot(snapshot);
4039
4516
  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
4517
  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 });
4518
+ if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved behavior`, { workflow: owned.id });
4042
4519
  return workflow;
4043
4520
  }
4044
4521
  /**
4045
- * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4522
+ * Creates the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4046
4523
  * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
4047
4524
  * backend behind the W-d persistence seam.
4048
4525
  *
4049
4526
  * @remarks
4050
4527
  * The snapshot analogue of the server package's `createMemorySessionStore`
4051
4528
  * (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
4529
+ * options bag (the smallest interface the capability requires): a persisted run-state lives until
4530
+ * an explicit `delete`. This is
4053
4531
  * the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
4054
4532
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
4055
4533
  * table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
@@ -4073,7 +4551,7 @@ function createMemoryWorkflowStore() {
4073
4551
  return new MemoryWorkflowStore();
4074
4552
  }
4075
4553
  /**
4076
- * Create a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
4554
+ * Creates a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
4077
4555
  * driver-pluggable backing for the W-d persistence seam, the opt-in twin of
4078
4556
  * {@link createMemoryWorkflowStore}.
4079
4557
  *
@@ -4084,8 +4562,9 @@ function createMemoryWorkflowStore() {
4084
4562
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
4085
4563
  * AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
4086
4564
  * `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
4565
+ * the opaque column sidesteps it (the column reads back as `unknown`, owned and narrowed on `get` by
4566
+ * {@link cloneWorkflowSnapshot}, whose semantic pass is
4567
+ * {@link import('./validators.js').isOwnedWorkflowSnapshot}). The `driver` DEFAULTS to
4089
4568
  * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
4090
4569
  * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4091
4570
  * the durability is the driver's job, the store engine is shared. It swaps in behind
@@ -4117,9 +4596,9 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
4117
4596
  }).table("snapshots"));
4118
4597
  }
4119
4598
  /**
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.
4599
+ * Creates the thin orchestrator — a {@link WorkflowRunnerInterface} that EXECUTES a live W-b
4600
+ * workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent, each
4601
+ * task dispatched through its OWN resolved handler under the workflow's `bail` policy.
4123
4602
  *
4124
4603
  * @remarks
4125
4604
  * The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
@@ -4132,14 +4611,14 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
4132
4611
  * rest) vs settle-all (`false` — failures are recorded, the run finishes); the run-level abort
4133
4612
  * / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
4134
4613
  * `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
4614
+ * `execute(definition, options?)` BUILDS the live tree from the definition itself (through
4136
4615
  * {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
4137
4616
  * the live entity (`start` → `complete` / `fail`), and resolves a
4138
4617
  * {@link import('./types.js').WorkflowResult}.
4139
4618
  *
4140
4619
  * External integrations remain application-owned: a caller wires an ordinary
4141
4620
  * {@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
4621
+ * registry. Only a task that omits `behavior` auto-completes; unresolved named work is rejected
4143
4622
  * before dispatch.
4144
4623
  *
4145
4624
  * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
@@ -4152,7 +4631,7 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
4152
4631
  *
4153
4632
  * const runner = createWorkflowRunner()
4154
4633
  * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
4155
- * { id: 't', name: 'T', run: 'compile' },
4634
+ * { id: 't', name: 'T', behavior: 'compile' },
4156
4635
  * ] }] }
4157
4636
  * const result = await runner.execute(definition, {
4158
4637
  * functions: { compile: async (controller) => `built ${controller.task.id}` },
@@ -4165,13 +4644,13 @@ function createWorkflowRunner(options) {
4165
4644
  return new WorkflowRunner(options?.scheduler ?? createScheduler());
4166
4645
  }
4167
4646
  /**
4168
- * Create a {@link WorkflowManagerInterface} — the store-backed registry of
4647
+ * Creates a {@link WorkflowManagerInterface} — the store-backed registry of
4169
4648
  * {@link WorkflowInterface}s, the additive manager tier mirroring the `@orkestrel/agent`
4170
4649
  * line's `createConversationManager` / `createWorkspaceManager`.
4171
4650
  *
4172
4651
  * @remarks
4173
- * `options.functions` flows into every workflow the manager mints (`add`, via
4174
- * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
4652
+ * `options.functions` flows into every workflow the manager mints (`add`, through
4653
+ * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, through
4175
4654
  * {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4176
4655
  * mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
4177
4656
  * manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
@@ -4199,12 +4678,12 @@ function createWorkflowManager(options) {
4199
4678
  return new WorkflowManager(options);
4200
4679
  }
4201
4680
  /**
4202
- * Create the safe cross-environment cooperative-yield default — a
4681
+ * Creates the safe cross-environment cooperative-yield default — a
4203
4682
  * {@link SchedulerInterface} built on `setTimeout` / `clearTimeout` alone, so it
4204
4683
  * runs unchanged in both the browser and Node.
4205
4684
  *
4206
4685
  * @remarks
4207
- * `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
4686
+ * `yield()` gives the host a turn through a zero-delay macrotask (so pending I/O,
4208
4687
  * timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
4209
4688
  * after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
4210
4689
  * with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
@@ -4245,7 +4724,7 @@ function createScheduler() {
4245
4724
  return new Scheduler();
4246
4725
  }
4247
4726
  /**
4248
- * Create a thin generic orchestrator that drives declared units — and any they
4727
+ * Creates a thin generic orchestrator that drives declared units — and any they
4249
4728
  * `spawn` — through a bounded-concurrency queue, collecting their results in order.
4250
4729
  *
4251
4730
  * @remarks
@@ -4257,13 +4736,13 @@ function createScheduler() {
4257
4736
  * `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
4258
4737
  * or the attempt's timeout, a promise-parked `wait()`, and `spawn(input)` to fan out
4259
4738
  * 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
4739
+ * aborts every other unit and rejects `execute` with that error. **Observable:** a
4261
4740
  * typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
4262
4741
  *
4263
- * Because `spawn` is fire-and-track (the runner awaits the whole spawn closure via an
4742
+ * Because `spawn` is fire-and-track (the runner awaits the whole spawn closure through an
4264
4743
  * 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.
4744
+ * for them to run — and on a bounded runner do NOT `await` a spawn inline (a slot-holding
4745
+ * handler awaiting its own spawn can deadlock); fan out and return instead.
4267
4746
  *
4268
4747
  * @typeParam TInput - The work input each unit carries
4269
4748
  * @typeParam TResult - The value a unit's handler resolves
@@ -4292,6 +4771,6 @@ function createRunner(options) {
4292
4771
  return new Runner(options);
4293
4772
  }
4294
4773
  //#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 };
4774
+ 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
4775
 
4297
4776
  //# sourceMappingURL=index.js.map