@orkestrel/scaffold 0.0.91 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/agents/templates/brief.md +2 -0
  2. package/dist/bin/main.js +6 -6
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/AGENTS.md +2 -2
  5. package/dist/host/agents/orchestration.md +1 -1
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +4 -2
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/responsive-layout.md +1 -1
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +3 -3
  9. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +3 -3
  10. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +1 -1
  11. package/dist/host/agents/skills/orkestrel-publish/references/window.md +3 -4
  12. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +1 -1
  13. package/dist/host/agents/templates/brief.md +2 -0
  14. package/dist/host/agents/transports/cursor.md +1 -1
  15. package/dist/host/claude/agents/analyst.md +1 -1
  16. package/dist/host/claude/agents/grok.md +1 -1
  17. package/dist/host/claude/agents/orkestrel.md +8 -8
  18. package/dist/host/claude/agents/planner.md +1 -1
  19. package/dist/host/claude/agents/researcher.md +1 -1
  20. package/dist/host/claude/agents/scout.md +1 -1
  21. package/dist/host/claude/rules/names.md +1 -1
  22. package/dist/host/claude/rules/patterns.md +1 -1
  23. package/dist/host/claude/rules/quality.md +11 -0
  24. package/dist/host/claude/rules/styles.md +5 -0
  25. package/dist/host/claude/rules/tests.md +7 -8
  26. package/dist/host/claude/rules/writing.md +2 -1
  27. package/dist/host/codex/agents/researcher.toml +2 -2
  28. package/dist/host/codex/agents/scout.toml +1 -1
  29. package/dist/host/dotfiles/oxlintrc.json +168 -0
  30. package/dist/host/guides/browser.md +483 -392
  31. package/dist/host/guides/mcp.md +15 -15
  32. package/dist/host/guides/pool.md +190 -69
  33. package/dist/host/guides/probe.md +262 -216
  34. package/dist/host/guides/router.md +1 -1
  35. package/dist/host/guides/worker.md +43 -7
  36. package/dist/host/manifest.json +34 -34
  37. package/dist/host/tests/config.test.ts +46 -3
  38. package/dist/src/core/index.cjs +6 -6
  39. package/dist/src/core/index.cjs.map +1 -1
  40. package/dist/src/core/index.js +6 -6
  41. package/dist/src/core/index.js.map +1 -1
  42. package/package.json +6 -6
@@ -5374,11 +5374,11 @@ the exact statement:
5374
5374
  1. **doc ↔ source bijection.** Every `function` / `class` / `const` /
5375
5375
  `interface` / `type` row in the `## Surface` tables (the core dispatch
5376
5376
  tables and the `### HTTP transport` + `### WebSocket transport` + `### stdio
5377
- transport` tables) is a real export of the mcp layer (`src/core` or
5377
+ transport` tables) is a real export of the mcp layer (`src/core` or
5378
5378
  `src/server`), and every export of either appears as a Surface row —
5379
5379
  exhaustive, both directions.
5380
5380
  2. **JSON-RPC 2.0 envelope.** A `dispatch` response is always `{ jsonrpc:
5381
- '2.0', … }` with exactly one of `result` / `error`. The success arm always
5381
+ '2.0', … }` with exactly one of `result` / `error`. The success arm always
5382
5382
  carries an `id`, echoing the request's, because a result answers a request and
5383
5383
  a request always has one. The error arm is the only arm whose `id` may be
5384
5384
  missing, and it is then omitted — the member is absent from the envelope, never
@@ -5410,7 +5410,7 @@ transport` tables) is a real export of the mcp layer (`src/core` or
5410
5410
  metadata, run through the same dispatcher, and are projected back unstamped — so
5411
5411
  they inherit the modern engine's execution port, cancellation, bounds, and
5412
5412
  validation rather than running beside them. `initialize` → `{ protocolVersion,
5413
- capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negotiated over the
5413
+ capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negotiated over the
5414
5414
  legacy subset only: the client's `params.protocolVersion` is echoed when it is a
5415
5415
  supported legacy revision, and every other request — the modern `'2026-07-28'`,
5416
5416
  an unsupported revision, a non-string, or an absent one — falls back to the
@@ -5435,7 +5435,7 @@ capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negot
5435
5435
  into a `success: false` result, the server adds no try/catch: that branch's
5436
5436
  `error` maps to `{ content: [{ type: 'text', text: <error> }], isError: true }`;
5437
5437
  a valued `success: true` branch maps to `{ content: [{ type: 'text', text:
5438
- JSON.stringify(value) }], structuredContent: value }`, carrying the value unchanged
5438
+ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchanged
5439
5439
  alongside the backwards-compatible text. A value-less success retains the required
5440
5440
  empty text block and omits `structuredContent`.
5441
5441
  6. **One modern seam, subscriptions included, and `-32601` for anything off it.**
@@ -5469,7 +5469,7 @@ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchan
5469
5469
  carries the same stamp. Ending the consumer's producer closes gracefully when the
5470
5470
  honoured filter omits `toolsListChanged`, with
5471
5471
  `{ resultType: 'complete', _meta: { 'io.modelcontextprotocol/subscriptionId': id,
5472
- … } }`. A stream honouring the tools family stays open until failure or signal abort.
5472
+ … } }`. A stream honouring the tools family stays open until failure or signal abort.
5473
5473
  The request id is only stream identity: a later request does not supersede
5474
5474
  an earlier one. The legacy method remains absent and answers `-32601`.
5475
5475
  7. **`handle` maps the boundary failures.** A `JSON.parse` throw (malformed
@@ -5520,7 +5520,7 @@ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchan
5520
5520
  continuation, or principal — carrying the caught value the wire never sees; the
5521
5521
  emitter isolates a listener throw, routing it
5522
5522
  to its own `error` handler (the `error` option, surfaced as `(error,
5523
- event)`, not a domain event) — so a buggy observer can never corrupt a
5523
+ event)`, not a domain event) — so a buggy observer can never corrupt a
5524
5524
  dispatch, and a throwing `error` handler neither escapes nor recurses.
5525
5525
  11. **doc ↔ source method bijection.** The `## Methods` tables list exactly
5526
5526
  the public methods of each behavioral interface — `MCPServerInterface`,
@@ -5565,7 +5565,7 @@ event)`, not a domain event) — so a buggy observer can never corrupt a
5565
5565
  own JSON-parse `try`/`catch`) and draws a sharp line: a TRANSPORT-level
5566
5566
  failure — malformed JSON (`-32700`) or a parsed value that is not a
5567
5567
  JSON-RPC request (`-32600`, narrowed with `parseJSONRPCMessage` + `'method'
5568
- in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
5568
+ in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
5569
5569
  `id` member. A legacy dispatch result — success or in-band JSON-RPC error — is
5570
5570
  HTTP **200**. A modern result is **400** for `-32020` / `-32021` / `-32022`
5571
5571
  / `-32602`, **404** for `-32601`, and **200** otherwise; every notification
@@ -5620,7 +5620,7 @@ in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
5620
5620
  consumer-provided list.
5621
5621
  13. **The client is the modern-only egress mirror (`src/core`).**
5622
5622
  `createMCPClient({ transport, identity?, capabilities?, version?, timeout?,
5623
- on? })` drives a remote server over an injected `MCPMessageTransportInterface`
5623
+ on? })` drives a remote server over an injected `MCPMessageTransportInterface`
5624
5624
  (transport-abstract, like the server). `connect()` issues a modern
5625
5625
  `server/discover` carrying `_meta` with the offered revision,
5626
5626
  client capabilities, and client identity. It intersects the peer's
@@ -5708,9 +5708,9 @@ on? })` drives a remote server over an injected `MCPMessageTransportInterface`
5708
5708
  `error` option, not a domain event). Consumers subscribe through `emitter.on`.
5709
5709
  15. **The HTTP client transport drives a remote server over `fetch`
5710
5710
  (`src/server`).** `createHTTPClientTransport({ url, headers?, fetch?,
5711
- timeout? })` returns a `MCPMessageTransportInterface` whose `send` POSTs one
5711
+ timeout? })` returns a `MCPMessageTransportInterface` whose `send` POSTs one
5712
5712
  JSON-serialized message to `url` with `content-type:
5713
- application/json` and an `Accept` of both `application/json` and
5713
+ application/json` and an `Accept` of both `application/json` and
5714
5714
  `text/event-stream` (plus any `headers`), then decodes the reply and
5715
5715
  emits each carried `JSONRPCMessage` on the `message` event: an
5716
5716
  `application/json` body is narrowed with `parseJSONRPCMessage`; a
@@ -5762,7 +5762,7 @@ application/json` and an `Accept` of both `application/json` and
5762
5762
  (`upgradeRequestPath`) is not `options.path` (default `DEFAULT_MCP_PATH`),
5763
5763
  the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not
5764
5764
  `13`. Otherwise it claims (returns `true`): `createNodeWebSocket({
5765
- socket, key, head, protocol })` (server mode → writes the `101` handshake,
5765
+ socket, key, head, protocol })` (server mode → writes the `101` handshake,
5766
5766
  selecting the configured subprotocol only when the client's offer contains it,
5767
5767
  and sends unmasked frames), wraps it in a `WebSocketServerTransport`, and
5768
5768
  pumps — each inbound `JSONRPCMessage` that `isJSONRPCRequest` runs
@@ -5831,7 +5831,7 @@ socket, key, head, protocol })` (server mode → writes the `101` handshake,
5831
5831
  (`src/server`).** `createMCPSession({ path?, ttl?, session?, clock?, origin?, keepalive? })`
5832
5832
  returns a `MiddlewareHandler<TState>` (`TState extends MCPSessionState`)
5833
5833
  that owns its own closure `Map<string, { session: MCPSession; touched:
5834
- number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and no shared
5834
+ number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and no shared
5835
5835
  session primitive; the store, mint, and validation are all native to
5836
5836
  this package. Compose it with `router.use(createMCPSession())` in front
5837
5837
  of a session-agnostic `createMCPRoutes(mcp)`; it owns its `path` (default
@@ -5875,7 +5875,7 @@ number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and
5875
5875
  the log under a monotone base36 event id (returned), evicting the oldest
5876
5876
  past `capacity` + any entry older than the per-event TTL, and fans the
5877
5877
  message out to every `attach`ed open stream as `stream.write({ id, data:
5878
- JSON.stringify(message) })`. `session.replay(afterId)` returns every
5878
+ JSON.stringify(message) })`. `session.replay(afterId)` returns every
5879
5879
  retained log entry strictly after `afterId` in append order — an unknown
5880
5880
  / evicted cursor replays nothing. The `createMCPSession` middleware
5881
5881
  serves the resumable `GET {path}`: it validates the `mcp-session-id`
@@ -5999,7 +5999,7 @@ JSON.stringify(message) })`. `session.replay(afterId)` returns every
5999
5999
  contract over native host APIs (`src/browser`).**
6000
6000
  `createWebSocketClientTransport({ url, protocols? })` returns a
6001
6001
  `MCPMessageTransportInterface` whose `start()` opens `new WebSocket(url,
6002
- protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
6002
+ protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
6003
6003
  is the host's concern; a connection failure — the native `'error'` event
6004
6004
  while not yet `OPEN` — rejects `start()`); `send` writes each message as
6005
6005
  one text frame once `OPEN`, queuing (in order) any message sent before —
@@ -6051,7 +6051,7 @@ protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
6051
6051
  (the implicit, portless channel — bound once, for the whole lifetime of
6052
6052
  the returned handle, so a dedicated worker's very first portless message
6053
6053
  needs no first-use setup), and registers one `scope.addEventListener(
6054
- 'message', …)` listener built by `createScopeMessageListener`. That one
6054
+ 'message', …)` listener built by `createScopeMessageListener`. That one
6055
6055
  listener handles every shape uniformly, per event, with no upfront
6056
6056
  detection flag: `event.ports.length > 0` spawns a fresh
6057
6057
  `createMessagePortTransport` + `bindServer` for that port (tracked for
@@ -1,12 +1,13 @@
1
1
  # Pool
2
2
 
3
- > A typed resource pool with optional bounded capacity, unique ownership, FIFO settlement,
4
- > validated reuse, caller-owned cancellation, explicit cleanup failures, and a stable
5
- > event-driven teardown barrier.
3
+ > A typed resource pool with optional bounded capacity, a warm floor, bounded loss recovery,
4
+ > exclusive leases by default, FIFO settlement, validated idle reuse, caller-owned cancellation, and explicit cleanup.
6
5
 
7
- The pool has no warm floor, eviction timer, acquire timeout, or polling loop. Every wait parks
8
- on a promise or a signal listener and wakes when a settlement reaches it, and the lifecycle
9
- hooks are the caller's, so the engine itself performs no I/O.
6
+ The pool supports lazy creation or a warm floor, with no eviction timer, acquire timeout,
7
+ or polling loop. Waits park on promises or signal listeners. An acquire rejects with
8
+ `cleanup` when a floor retains a record, owns `min` records, has nothing eligible, and has no
9
+ refill or disposal pending, even with live leases. The lifecycle hooks are the caller's,
10
+ so the engine itself performs no I/O.
10
11
 
11
12
  ## Surface
12
13
 
@@ -38,16 +39,16 @@ try {
38
39
 
39
40
  ### Factories
40
41
 
41
- | API | Kind | Summary |
42
- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
43
- | `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity, unique ownership, and FIFO settlement. |
42
+ | API | Kind | Summary |
43
+ | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity, exclusive leases by default, and FIFO settlement. |
44
45
 
45
46
  ### Classes
46
47
 
47
- | API | Kind | Summary |
48
- | ----------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `Pool` | class | Represents a capacity-aware resource pool whose opaque ownership records preserve FIFO settlement, cancellation, exact lease release, and deterministic teardown under concurrent hooks. |
50
- | `PoolError` | class | Represents a stable, machine-readable pool failure that retains the original thrown value as its cause without unsafe coercion, alongside structured context. |
48
+ | API | Kind | Summary |
49
+ | ----------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `Pool` | class | Represents a resource pool with optional bounded capacity and a warm floor, whose opaque ownership records preserve FIFO settlement, cancellation, exact lease release, and deterministic teardown under concurrent hooks. |
51
+ | `PoolError` | class | Represents a stable, machine-readable pool failure that retains the original thrown value as its cause without unsafe coercion, alongside structured context. |
51
52
 
52
53
  ### Guards
53
54
 
@@ -56,27 +57,29 @@ In a guard table a `Shape` cell holds the type the guard narrows to.
56
57
  | API | Kind | Shape | Summary |
57
58
  | -------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
58
59
  | `isPoolError` | function | `PoolError` | Tests whether an unknown value is a `PoolError`, returning `false` for hostile proxies. |
59
- | `isPoolMax` | function | `number` | Tests whether a value is a positive safe integer, the only valid explicit pool maximum. |
60
+ | `isPoolLimit` | function | `number` | Tests whether a value is a positive safe integer for a pool record or lease limit. |
60
61
  | `isPoolSignal` | function | `AbortSignal` | Tests whether a value is a native `AbortSignal` for the acquire boundary, returning `false` for hostile proxies. |
61
62
 
62
63
  ### Types
63
64
 
64
65
  A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
65
66
 
66
- | API | Kind | Shape | Summary |
67
- | ------------------ | --------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
- | `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'` | Names the machine-readable failure codes produced by `PoolError`. |
69
- | `PoolContext` | interface | `{ value?, failures? }` | Represents the structured context attached to a `PoolError`: the rejected input, or the distinct destroy-hook failures an aggregate cleanup collected. |
70
- | `PoolErrorOptions` | interface | `{ code, cause?, context? }` | Represents the construction options for `PoolError`: the stable code, an optional cause, and optional structured context. |
71
- | `PoolEventMap` | type | `{ create, acquire, release, destroy }` | Represents the observable resource lifecycle events emitted by a `PoolInterface`. |
72
- | `PoolToken` | interface | `{ value } plus release` | Represents a unique lease over one pool-owned resource record, exposing that record as a readonly `value` and returning it through an idempotent `release`. |
73
- | `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, max? }` | Represents the resource lifecycle options for `Pool` and `createPool`: creation, destruction, validation, capacity, and observation. |
74
- | `PoolInterface` | interface | `{ emitter, size, idle, active } plus acquire, clear, destroy` | Represents a FIFO resource pool with optional bounded capacity and deterministic teardown, exposing its record counts and a typed lifecycle emitter. |
75
-
76
- `size` counts every owned record, including records being validated or destroyed. `idle`
77
- counts only immediately available records. `active` counts only leased records. An in-flight
78
- create reservation claims capacity but is not yet an owned record and therefore is not part
79
- of `size`.
67
+ | API | Kind | Shape | Summary |
68
+ | ------------------ | --------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'` | Names the machine-readable failure codes produced by `PoolError`. |
70
+ | `PoolContext` | interface | `{ value?, failures? }` | Represents the structured context attached to a `PoolError`: the rejected input, or the distinct destroy-hook failures an aggregate cleanup collected. |
71
+ | `PoolErrorOptions` | interface | `{ code, cause?, context? }` | Represents the construction options for `PoolError`: the stable code, an optional cause, and optional structured context. |
72
+ | `PoolEventMap` | type | `{ create, acquire, release, destroy }` | Represents the observable resource lifecycle events emitted by a `PoolInterface`. |
73
+ | `PoolToken` | interface | `{ value } plus release, destroy` | Represents a unique lease over one pool-owned resource record, exposing that record as a readonly `value` and ending it through an idempotent `release` or `destroy`. |
74
+ | `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, watch?, capacity?, max?, min?, restarts? }` | Represents the resource lifecycle options for `Pool` and `createPool`: creation, destruction, validation, capacity, a bounded warm floor, and loss observation. |
75
+ | `PoolInterface` | interface | `{ emitter, size, idle, active } plus start, acquire, clear, destroy` | Represents a FIFO resource pool with optional bounded capacity, a warm floor, loss recovery, and deterministic teardown, exposing its record counts and a typed lifecycle emitter. |
76
+
77
+ `size` counts every owned record, including records being validated, destroyed, or retained
78
+ after failed cleanup under `min`. `idle` counts available records with no live lease or pending
79
+ handout. `active` counts records with live leases, counting a shared record once. A partially
80
+ occupied record is active and can accept another lease; it is not idle. A pending handout counts
81
+ against `capacity` but becomes active only at its FIFO grant. An in-flight create reservation
82
+ claims record capacity but is not yet an owned record and therefore is not part of `size`.
80
83
 
81
84
  ## Methods
82
85
 
@@ -85,24 +88,121 @@ The public call-signature members of `PoolInterface` and `PoolToken`; `Pool` imp
85
88
 
86
89
  #### `PoolInterface`
87
90
 
88
- | Method | Returns | Summary |
89
- | --------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
90
- | `acquire` | `Promise<PoolToken<T>>` | Queues the caller in FIFO order, validates an idle record or creates one, and settles queued acquires in that same order. |
91
- | `clear` | `Promise<void>` | Destroys the records that are idle at this call's synchronous snapshot. |
92
- | `destroy` | `Promise<void>` | Tears down the pool permanently and returns its stable completion barrier. |
91
+ | Method | Returns | Summary |
92
+ | --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `start` | `Promise<void>` | Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately. |
94
+ | `acquire` | `Promise<PoolToken<T>>` | Queues the caller in FIFO order, shares an occupied record or validates an idle record, and waits for a floor refill or creates on demand without a floor. |
95
+ | `clear` | `Promise<void>` | Destroys the records that are idle at this call's synchronous snapshot and restores a started floor. |
96
+ | `destroy` | `Promise<void>` | Tears down the pool permanently and returns its stable completion barrier. |
93
97
 
94
98
  #### `PoolToken`
95
99
 
96
- The lease returned by `acquire`, with the operation that returns its record.
100
+ The lease returned by `acquire`, with operations that return or dispose its exact record.
97
101
 
98
- | Method | Returns | Summary |
99
- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
100
- | `release` | `void` | Gives this exact record back to the pool once; a repeat call, and a call after teardown took ownership, are no-ops. |
102
+ | Method | Returns | Summary |
103
+ | --------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
104
+ | `release` | `void` | Ends this exact lease once; a repeat call, and a call after loss or teardown took ownership, are no-ops. Other leases on the record remain live. |
105
+ | `destroy` | `Promise<void>` | Destroys this exact record for every co-holder and invalidates their leases; a repeat call, and a call after release, are no-ops. |
101
106
 
102
107
  ## Contract
103
108
 
109
+ ### Warm floor and loss
110
+
111
+ Construction creates nothing. Set `min` to a positive safe integer and `restarts` to a
112
+ non-negative safe integer, then call `start()`. `max` defaults to `min`; explicit values
113
+ must equal each other. A missing restart bound, a bound without `min`, and a non-callable
114
+ `watch` are invalid. The restart bound has no default.
115
+
116
+ `start()` resolves when the pool owns `min` live records. Concurrent calls share the pending
117
+ promise. Without `min`, it resolves immediately. Under `min`, queued acquires never cause
118
+ creation, including before `start()` and during failed refills. The pool refills missing
119
+ capacity after startup and cleanup, one create attempt at a time.
120
+ Bound your `create` and `destroy` hooks. A create hook that never settles blocks every later
121
+ refill, including the attempt owed to a lost leased record. A destroy hook that never settles
122
+ keeps its record counted against `max` and blocks replacement of that record.
123
+
124
+ `watch(value, signal)` runs once for each created record. A fulfillment, rejection, or
125
+ synchronous throw declares loss while the record is live. The pool removes the record from
126
+ service, disposes it, and refills if the bound permits. Rejections and synchronous throws
127
+ reach `error(error, 'watch')` only while the record is live; a throwing error handler is
128
+ isolated. Disposal aborts the watch signal before the destroy hook runs, including disposal
129
+ through validation, clear, pool teardown, and token destruction.
130
+ The watch must release its listeners when that signal aborts. Any settlement after the signal
131
+ aborts is ignored, including rejection. A late or repeated loss cannot
132
+ dispose a record twice. A lost record cannot be handed out after validation or from a ready
133
+ result waiting behind an earlier caller. The pool disposes a lost leased record while its
134
+ holder still holds the token. The holder learns of that loss from its own `watch` or `destroy`
135
+ hook. After disposal, the token's `release()` and `destroy()` calls do nothing. During disposal,
136
+ the first `destroy()` call waits for that cleanup attempt and rejects with `cleanup` if it fails.
137
+
138
+ A failed refill adds a strike. Losing a record with no live lease also adds a strike,
139
+ including a previously used record and failed idle validation. Granting a lease resets the
140
+ strikes only when the granted record was created after the last strike; successful creation
141
+ alone does not. Granting an older healthy
142
+ record leaves the strikes unchanged. Any idle record that dies, refills, and dies
143
+ again without a grant between those losses can spend the floor.
144
+ When strikes exceed `restarts`, the floor is spent. With `restarts: 1`, the first failed
145
+ create permits another attempt, and the second refuses the next attempt. `start()` rejects
146
+ with `create` and the last cause; another `start()` resets the strikes. While the floor is
147
+ spent, an acquire without an eligible record rejects with `create` after pending refills and
148
+ disposal settle. If the floor retains a record and owns `min` records, that acquire rejects
149
+ with `cleanup` instead, even with live leases. A spent floor and a rejected `start()` still
150
+ serve their live records. A loss without a thrown cause leaves that cause undefined.
151
+
152
+ A record lost while leased earns one refill attempt even after the bound is spent.
153
+ The credit becomes spendable only when successful disposal removes the record; failed disposal
154
+ grants no credit. That attempt remains owed during a concurrent refill and never resets the strikes.
155
+ Its failed create adds a strike; its success alone does not reset the strikes. Granting that
156
+ replacement resets the strikes only if no strike occurred after its creation. A record that was leased
157
+ and later lost after its last release adds a strike. Under sharing, loss or one lease's
158
+ `destroy()` ends the record for every co-holder, invalidates their leases, and owes one refill
159
+ regardless of the lease count. `token.destroy()` waits for an existing cleanup attempt and
160
+ rejects with `cleanup` if disposal fails.
161
+ A token ends once: after `release()` or `destroy()`, the other call does nothing. Repeating
162
+ either call does nothing. A repeat `token.destroy()` after a `cleanup` rejection resolves
163
+ while the record stays retained. Refills hold no lease before a caller's grant, so repeated idle
164
+ losses spend the budget. A consumer that keeps a lease without a holder, or repeatedly acquires
165
+ refilled records that fail during use, can keep earning refills and qualifying grant resets.
166
+ The pool cannot distinguish such a lease from useful work; callers must acquire only for a holder.
167
+
168
+ Under `min`, a failed destroy hook leaves its record counted against `max` and excluded from
169
+ idle and active counts. The pool never replaces that retained record or retries its destroy
170
+ hook. The pool can run short; `start()` rejects with `cleanup` if retained records prevent
171
+ filling. An acquire rejects with `cleanup` when the floor retains a record, owns `min` records,
172
+ has nothing eligible, and has no refill or disposal pending, even with live leases. The cause is
173
+ the retained record's own cleanup failure. The terminal `destroy()` barrier
174
+ reports the original cleanup failures. Without `min`, cleanup retains its lazy behavior and
175
+ frees capacity after either hook outcome.
176
+
177
+ This example warms an inert resource, explicitly loses its lease, and tears down the pool:
178
+
179
+ ```ts
180
+ import { createPool } from '@orkestrel/pool'
181
+
182
+ const pool = createPool({
183
+ create: () => new EventTarget(),
184
+ min: 1,
185
+ restarts: 1,
186
+ watch: (resource, signal) =>
187
+ new Promise<void>((resolve) => {
188
+ resource.addEventListener('loss', () => resolve(), { once: true, signal })
189
+ signal.addEventListener('abort', () => resolve(), { once: true })
190
+ }),
191
+ })
192
+ await pool.start()
193
+ const token = await pool.acquire()
194
+ await token.destroy()
195
+ await pool.destroy()
196
+ ```
197
+
104
198
  ### Capacity and FIFO
105
199
 
200
+ `capacity` is a positive safe integer limiting simultaneous leases on each record; it defaults
201
+ to 1. `min` and `max` still bound records. Acquisition chooses the eligible record with the
202
+ fewest live leases and reserved handouts. Idle ties follow release order; occupied ties follow
203
+ record creation order. A validating record is unavailable until validation settles.
204
+ Reservations count before any awaited hook.
205
+
106
206
  Every `acquire` receives its queue position before a create or validation hook starts. The
107
207
  reentrancy-safe pump may assign several hook operations concurrently, but a head commit
108
208
  barrier settles successes and failures in request order. A later fast create or validation
@@ -110,19 +210,32 @@ cannot overtake an earlier slow one. Capacity obeys:
110
210
 
111
211
  ```text
112
212
  owned records + create reservations <= max
213
+ live leases + reserved handouts per record <= capacity
113
214
  ```
114
215
 
115
- `max` must be a positive safe integer. Omit it for an unbounded pool; `Infinity`, fractions,
116
- zero, negative values, and unsafe integers are invalid. Construction snapshots `max` once and
117
- validates it before retention, then snapshots `on` and `error` once each.
216
+ `max` must be a positive safe integer. Omit `min` and `max` for an unbounded record count.
217
+ `Infinity`, fractions, zero, negative values, and unsafe integers are invalid.
218
+ Construction snapshots `max` and `capacity` once and validates them before retention, then
219
+ snapshots `on` and `error` once each.
118
220
 
119
- Record phases are disjoint:
221
+ Record ownership follows these transitions; an occupied record can carry several leases and
222
+ reserved handouts, and returns to available after its last lease and reservation end:
120
223
 
121
224
  ```text
122
225
  create reservation -> ready -> leased -> available -> validating -> ready
123
226
  \-> destroying -> removed
227
+ \-> retained (failed cleanup with min)
228
+ floor refill --------------------------> available
124
229
  ```
125
230
 
231
+ Validation runs only when reserving an idle record; handouts on an occupied record skip it.
232
+ No caller receives a lease while idle validation is pending. This keeps a failed validation
233
+ from silently disposing a record used by co-holders. After the last lease ends, the next idle
234
+ reuse validates again. A freshly created lazy record keeps its direct handout behavior.
235
+ With `capacity` greater than 1, an occupied record is never revalidated. Only `watch` detects
236
+ its loss: without `watch`, a dead record can keep receiving leases until it becomes idle;
237
+ with `watch`, that exposure lasts until the watch settles.
238
+
126
239
  Invalid validation, whether `false` or a thrown value, claims and cleans the record before a
127
240
  replacement capacity slot becomes available. Cleanup failure rejects that acquire with
128
241
  `PoolError` code `cleanup`; successful cleanup lets the same FIFO waiter seek a replacement.
@@ -145,19 +258,24 @@ eventual `destroy()` barrier.
145
258
 
146
259
  ### Release and cleanup
147
260
 
148
- A token captures its exact opaque record, so `release()` is correct even when multiple
149
- records contain the same value. Release is idempotent and removes the lease synchronously.
150
- With a waiter, the record is validated before handoff. Without an assignable waiter, it
151
- becomes idle and emits `release`. Release after teardown ownership transferred is a no-op.
261
+ A token captures its exact lease and opaque record, so `release()` is correct even when multiple
262
+ records contain the same value or several tokens share a record. Release is idempotent and
263
+ removes only that lease synchronously. A remaining co-holder keeps the record active, and a
264
+ waiter can share the freed place without validation. After the last release, idle reuse validates.
265
+ A record that remains idle emits `release`. Release after loss or teardown took ownership does nothing.
266
+ The pool disposes a lost lease while its holder still holds the token; the holder learns of
267
+ the loss through its own `watch` or `destroy` hook. After disposal, both token methods do nothing.
152
268
 
153
269
  `clear()` synchronously snapshots idle records and installs one cleanup promise per record
154
270
  before invoking the hook. Concurrent clears therefore own disjoint snapshots, and a lease
155
- released after a snapshot is taken is not part of it. Every claimed record stays in `size` until its
156
- hook attempt completes. Distinct failures are aggregated in a code-`cleanup` `PoolError`
157
- whose `context.failures` retains the original thrown values.
158
- Each claimed record's cleanup settlement independently wakes queued acquires after the
159
- destroy ledger transition, whether its cleanup hook succeeded or failed; a failed `clear()`
160
- still rejects its own aggregate cleanup barrier.
271
+ released after a snapshot is taken is not part of it. Every claimed record stays in `size` during
272
+ its hook attempt; failed cleanup under `min` retains it afterward. Distinct failures are
273
+ aggregated in a code-`cleanup` `PoolError` whose `context.failures` retains the original thrown
274
+ values.
275
+ Each claimed record's cleanup settlement wakes queued acquires after the destroy ledger
276
+ transition. A failed `clear()` rejects its own aggregate cleanup barrier; under `min`,
277
+ the pool keeps a record whose cleanup fails counted against `max`, and successful cleanup
278
+ restores a started floor.
161
279
 
162
280
  ### Destruction
163
281
 
@@ -165,7 +283,8 @@ still rejects its own aggregate cleanup barrier.
165
283
  rejects waiters, emits events, or invokes cleanup. Reentrant and repeated calls return that
166
284
  same object. Teardown invalidates idle, leased, and ready records, waits for create,
167
285
  validation, existing clear cleanup, and new cleanup activity, and disposes every late
168
- resource. Unresolved hooks keep the barrier pending without polling.
286
+ resource. A pending `start()` rejects with `destroyed`. Unresolved create, validation, and
287
+ destroy hooks keep the barrier pending without polling; aborted watches do not delay it.
169
288
 
170
289
  Cleanup already owned by an overlapping `clear()` is shared; its failure is reported to both
171
290
  the clear call and the destroy aggregate. Create failures that produced no resource do not
@@ -176,12 +295,12 @@ hook attempt, then the stable barrier resolves or rejects.
176
295
 
177
296
  `PoolError.code` is stable and lowercase:
178
297
 
179
- | Code | Owner |
180
- | ----------- | ------------------------------------------------------------------------ |
181
- | `invalid` | Invalid explicit `max`, invalid acquire signal, or an internal boundary. |
182
- | `destroyed` | Acquire or clear attempted after terminal teardown began. |
183
- | `create` | The create hook failed for its bound acquire. |
184
- | `cleanup` | Invalid-handoff, clear, or destroy cleanup failed. |
298
+ | Code | Owner |
299
+ | ----------- | ----------------------------------------------------------------------------------- |
300
+ | `invalid` | Invalid capacity, restart bound, watch hook, acquire signal, or internal boundary. |
301
+ | `destroyed` | Start, acquire, or clear attempted during terminal teardown. |
302
+ | `create` | A lazy create failed, or a floor refill bound was spent. |
303
+ | `cleanup` | Record disposal failed, including retained records blocking startup or acquisition. |
185
304
 
186
305
  The original thrown value is retained as `cause`; aggregate cleanup values are also in
187
306
  `context.failures`. Message construction and `isPoolError` avoid unsafe string coercion and
@@ -194,12 +313,12 @@ invokes listeners synchronously at each emission point. The `destroy` emission i
194
313
  one microtask after cleanup settlement so bound outcomes precede observer reentry. Events
195
314
  follow their ledger transitions:
196
315
 
197
- | Event | Emission point |
198
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
199
- | `create` | After a fresh resource is inserted into the ownership ledger. |
200
- | `acquire` | After the waiter promise receives its token and the record is leased. |
201
- | `release` | Only when the released or orphaned-ready record remains idle. |
202
- | `destroy` | After the hook attempt and resource-ledger removal, one microtask after private cleanup settlement, while destroying ownership remains installed through the event. |
316
+ | Event | Emission point |
317
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
318
+ | `create` | After a fresh resource is inserted into the ownership ledger. |
319
+ | `acquire` | After the waiter promise receives its token and the record is leased. |
320
+ | `release` | Only when a released, orphaned, or refilled record remains idle. |
321
+ | `destroy` | After the hook attempt and resource-ledger removal or retention, one microtask after private cleanup settlement, while destroying ownership remains installed through the event. |
203
322
 
204
323
  Because listeners may synchronously reenter or destroy the pool, the engine checks terminal
205
324
  and waiter state after every awaited hook and every emit.
@@ -224,10 +343,10 @@ const pool = createPool({
224
343
  Check a candidate value or error against the public boundary guards before acting on it:
225
344
 
226
345
  ```ts
227
- import { PoolError, isPoolError, isPoolMax, isPoolSignal } from '@orkestrel/pool'
346
+ import { PoolError, isPoolError, isPoolLimit, isPoolSignal } from '@orkestrel/pool'
228
347
 
229
- isPoolMax(4) // true
230
- isPoolMax(Infinity) // false: omit max for unbounded capacity
348
+ isPoolLimit(4) // true
349
+ isPoolLimit(Infinity) // false: omit max for unbounded capacity
231
350
  isPoolSignal(new AbortController().signal) // true
232
351
 
233
352
  const failure = new PoolError({ code: 'destroyed' })
@@ -260,7 +379,8 @@ await pool.destroy()
260
379
  hooks, create continuation, abort boundaries, abort-listener detachment, exclusive
261
380
  invalid-cleanup waiter ownership, bounded and unbounded replacement, destroy-observer
262
381
  reentry ordering, concurrent clear, stable reentrant destruction, late resources,
263
- aggregate failures, emitter ordering, and high contention.
382
+ aggregate failures, emitter ordering, high contention, warm floors, exact refill bounds,
383
+ retained cleanup failures, watch cancellation, lost-record handoff races, and owed lease refills.
264
384
  - [`tests/src/core/validators.test.ts`](../tests/src/core/validators.test.ts) — the public
265
385
  boundary guards alone: accepted and rejected maxima, and native versus hostile signals.
266
386
  - [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) — factory
@@ -271,7 +391,8 @@ await pool.destroy()
271
391
  its declaration's description paragraph, the titled `Create a pool` fence against the
272
392
  `@example` block of that title (pinned so the titled pair cannot be retired silently), and the
273
393
  README pitch against this guide's tagline. It also runs the boundary-guard fence and asserts
274
- the values its comments claim.
394
+ the values its comments claim. The floor fence is checked line for line in execution order
395
+ and runs with assertions for warming, token cleanup, and watch-listener removal.
275
396
 
276
397
  ## See also
277
398