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