@plurnk/plurnk-service 1.24.0 → 1.26.0

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 (226) hide show
  1. package/.env.defaults +17 -15
  2. package/INSTALL.md +96 -20
  3. package/README.md +2 -2
  4. package/SPEC.md +517 -193
  5. package/dist/Paths.d.ts +1 -0
  6. package/dist/Paths.d.ts.map +1 -1
  7. package/dist/Paths.js +1 -0
  8. package/dist/Paths.js.map +1 -1
  9. package/dist/build-info.json +1 -1
  10. package/dist/content/read-projector.d.ts.map +1 -1
  11. package/dist/content/read-projector.js +19 -0
  12. package/dist/content/read-projector.js.map +1 -1
  13. package/dist/core/AdministrativeLoop.d.ts +1 -1
  14. package/dist/core/AdministrativeLoop.d.ts.map +1 -1
  15. package/dist/core/AdministrativeLoop.js +5 -5
  16. package/dist/core/AdministrativeLoop.js.map +1 -1
  17. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  18. package/dist/core/AdmittedTurnExecutor.js +1 -0
  19. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  20. package/dist/core/CapabilityResolver.js +1 -1
  21. package/dist/core/CapabilityResolver.js.map +1 -1
  22. package/dist/core/DataStatementRunner.d.ts.map +1 -1
  23. package/dist/core/DataStatementRunner.js +1 -3
  24. package/dist/core/DataStatementRunner.js.map +1 -1
  25. package/dist/core/Dispatcher.d.ts +1 -1
  26. package/dist/core/Dispatcher.d.ts.map +1 -1
  27. package/dist/core/Dispatcher.js +76 -21
  28. package/dist/core/Dispatcher.js.map +1 -1
  29. package/dist/core/Dispatcher.sql +0 -7
  30. package/dist/core/Engine.d.ts +2 -2
  31. package/dist/core/Engine.d.ts.map +1 -1
  32. package/dist/core/ExecutorRegistry.d.ts +16 -4
  33. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  34. package/dist/core/ExecutorRegistry.js +31 -31
  35. package/dist/core/ExecutorRegistry.js.map +1 -1
  36. package/dist/core/HostPaths.d.ts +7 -1
  37. package/dist/core/HostPaths.d.ts.map +1 -1
  38. package/dist/core/HostPaths.js +25 -12
  39. package/dist/core/HostPaths.js.map +1 -1
  40. package/dist/core/LoopDriver.d.ts.map +1 -1
  41. package/dist/core/LoopDriver.js +11 -2
  42. package/dist/core/LoopDriver.js.map +1 -1
  43. package/dist/core/LoopLifecycle.d.ts +1 -1
  44. package/dist/core/LoopLifecycle.js +1 -1
  45. package/dist/core/LoopLifecycle.sql +2 -2
  46. package/dist/core/LoopPolicies.d.ts.map +1 -1
  47. package/dist/core/LoopPolicies.js +12 -7
  48. package/dist/core/LoopPolicies.js.map +1 -1
  49. package/dist/core/MembershipMaterialization.js +1 -1
  50. package/dist/core/MembershipMaterialization.js.map +1 -1
  51. package/dist/core/OperatorConfig.d.ts.map +1 -1
  52. package/dist/core/OperatorConfig.js +97 -51
  53. package/dist/core/OperatorConfig.js.map +1 -1
  54. package/dist/core/PacketBuilder.d.ts +1 -0
  55. package/dist/core/PacketBuilder.d.ts.map +1 -1
  56. package/dist/core/PacketBuilder.js +27 -25
  57. package/dist/core/PacketBuilder.js.map +1 -1
  58. package/dist/core/ReasoningView.d.ts +1 -1
  59. package/dist/core/ReasoningView.d.ts.map +1 -1
  60. package/dist/core/ReasoningView.js +4 -5
  61. package/dist/core/ReasoningView.js.map +1 -1
  62. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  63. package/dist/core/SchemeRegistry.js +3 -2
  64. package/dist/core/SchemeRegistry.js.map +1 -1
  65. package/dist/core/Turn.d.ts +1 -1
  66. package/dist/core/Turn.d.ts.map +1 -1
  67. package/dist/core/Turn.js +2 -2
  68. package/dist/core/Turn.js.map +1 -1
  69. package/dist/core/Turn.sql +1 -1
  70. package/dist/core/TurnDispositionHandler.d.ts +1 -1
  71. package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
  72. package/dist/core/TurnDispositionHandler.js +6 -15
  73. package/dist/core/TurnDispositionHandler.js.map +1 -1
  74. package/dist/core/TurnMaterialization.js +1 -1
  75. package/dist/core/TurnMaterialization.js.map +1 -1
  76. package/dist/core/TurnOps.d.ts +1 -0
  77. package/dist/core/TurnOps.d.ts.map +1 -1
  78. package/dist/core/TurnOps.js +18 -0
  79. package/dist/core/TurnOps.js.map +1 -1
  80. package/dist/core/TurnRunner.d.ts.map +1 -1
  81. package/dist/core/TurnRunner.js +53 -32
  82. package/dist/core/TurnRunner.js.map +1 -1
  83. package/dist/core/env-defaults.d.ts +12 -1
  84. package/dist/core/env-defaults.d.ts.map +1 -1
  85. package/dist/core/env-defaults.js +50 -7
  86. package/dist/core/env-defaults.js.map +1 -1
  87. package/dist/core/file-creation-policy.d.ts.map +1 -1
  88. package/dist/core/file-creation-policy.js +2 -1
  89. package/dist/core/file-creation-policy.js.map +1 -1
  90. package/dist/core/git-membership.d.ts.map +1 -1
  91. package/dist/core/git-membership.js +0 -3
  92. package/dist/core/git-membership.js.map +1 -1
  93. package/dist/core/namespace.d.ts +1 -0
  94. package/dist/core/namespace.d.ts.map +1 -1
  95. package/dist/core/namespace.js +21 -75
  96. package/dist/core/namespace.js.map +1 -1
  97. package/dist/core/notifications.d.ts +2 -0
  98. package/dist/core/notifications.d.ts.map +1 -1
  99. package/dist/core/packet-wire.d.ts +1 -0
  100. package/dist/core/packet-wire.d.ts.map +1 -1
  101. package/dist/core/packet-wire.js +28 -7
  102. package/dist/core/packet-wire.js.map +1 -1
  103. package/dist/core/results.d.ts +2 -0
  104. package/dist/core/results.d.ts.map +1 -1
  105. package/dist/core/results.js +7 -0
  106. package/dist/core/results.js.map +1 -1
  107. package/dist/digest/Digest.d.ts.map +1 -1
  108. package/dist/digest/Digest.js +1 -2
  109. package/dist/digest/Digest.js.map +1 -1
  110. package/dist/digest/DigestEvidence.d.ts +1 -0
  111. package/dist/digest/DigestEvidence.d.ts.map +1 -1
  112. package/dist/digest/DigestEvidence.js +8 -0
  113. package/dist/digest/DigestEvidence.js.map +1 -1
  114. package/dist/digest/DigestRender.d.ts.map +1 -1
  115. package/dist/digest/DigestRender.js +23 -21
  116. package/dist/digest/DigestRender.js.map +1 -1
  117. package/dist/digest/digest-rows.d.ts +2 -1
  118. package/dist/digest/digest-rows.d.ts.map +1 -1
  119. package/dist/digest/digest.sql +4 -0
  120. package/dist/observe/init.d.ts +1 -0
  121. package/dist/observe/init.d.ts.map +1 -1
  122. package/dist/observe/init.js +5 -1
  123. package/dist/observe/init.js.map +1 -1
  124. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  125. package/dist/schemes/EffectPolicy.js +4 -4
  126. package/dist/schemes/EffectPolicy.js.map +1 -1
  127. package/dist/schemes/Exec.d.ts +1 -0
  128. package/dist/schemes/Exec.d.ts.map +1 -1
  129. package/dist/schemes/Exec.js +30 -14
  130. package/dist/schemes/Exec.js.map +1 -1
  131. package/dist/schemes/ExecScheduler.d.ts.map +1 -1
  132. package/dist/schemes/ExecScheduler.js +4 -3
  133. package/dist/schemes/ExecScheduler.js.map +1 -1
  134. package/dist/schemes/ExecScratch.d.ts.map +1 -1
  135. package/dist/schemes/ExecScratch.js +2 -1
  136. package/dist/schemes/ExecScratch.js.map +1 -1
  137. package/dist/schemes/File.d.ts.map +1 -1
  138. package/dist/schemes/File.js +21 -22
  139. package/dist/schemes/File.js.map +1 -1
  140. package/dist/schemes/_entry-find.d.ts +1 -0
  141. package/dist/schemes/_entry-find.d.ts.map +1 -1
  142. package/dist/schemes/_entry-find.js +4 -3
  143. package/dist/schemes/_entry-find.js.map +1 -1
  144. package/dist/schemes/_path-scope.d.ts +2 -2
  145. package/dist/schemes/_path-scope.d.ts.map +1 -1
  146. package/dist/schemes/_path-scope.js +23 -2
  147. package/dist/schemes/_path-scope.js.map +1 -1
  148. package/dist/server/AgentRoots.d.ts +6 -0
  149. package/dist/server/AgentRoots.d.ts.map +1 -0
  150. package/dist/server/AgentRoots.js +25 -0
  151. package/dist/server/AgentRoots.js.map +1 -0
  152. package/dist/server/ConfigurationDiagnostics.d.ts +12 -0
  153. package/dist/server/ConfigurationDiagnostics.d.ts.map +1 -0
  154. package/dist/server/ConfigurationDiagnostics.js +40 -0
  155. package/dist/server/ConfigurationDiagnostics.js.map +1 -0
  156. package/dist/server/Daemon.d.ts +11 -5
  157. package/dist/server/Daemon.d.ts.map +1 -1
  158. package/dist/server/Daemon.js +125 -48
  159. package/dist/server/Daemon.js.map +1 -1
  160. package/dist/server/DaemonModule.d.ts +11 -4
  161. package/dist/server/DaemonModule.d.ts.map +1 -1
  162. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  163. package/dist/server/EnvFunctionality.js +5 -2
  164. package/dist/server/EnvFunctionality.js.map +1 -1
  165. package/dist/server/Functionality.d.ts +6 -0
  166. package/dist/server/Functionality.d.ts.map +1 -1
  167. package/dist/server/Functionality.js +173 -37
  168. package/dist/server/Functionality.js.map +1 -1
  169. package/dist/server/FunctionalityManager.js +6 -6
  170. package/dist/server/FunctionalityManager.js.map +1 -1
  171. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  172. package/dist/server/MembersFunctionality.js +13 -41
  173. package/dist/server/MembersFunctionality.js.map +1 -1
  174. package/dist/server/PluginSources.d.ts +20 -0
  175. package/dist/server/PluginSources.d.ts.map +1 -0
  176. package/dist/server/PluginSources.js +52 -0
  177. package/dist/server/PluginSources.js.map +1 -0
  178. package/dist/server/PlurnkSkill.js +1 -1
  179. package/dist/server/PlurnkSkill.js.map +1 -1
  180. package/dist/server/Retention.d.ts.map +1 -1
  181. package/dist/server/Retention.js +11 -30
  182. package/dist/server/Retention.js.map +1 -1
  183. package/dist/server/ServiceModules.d.ts +1 -0
  184. package/dist/server/ServiceModules.d.ts.map +1 -1
  185. package/dist/server/ServiceModules.js +9 -3
  186. package/dist/server/ServiceModules.js.map +1 -1
  187. package/dist/server/SkillSource.d.ts +37 -0
  188. package/dist/server/SkillSource.d.ts.map +1 -0
  189. package/dist/server/SkillSource.js +267 -0
  190. package/dist/server/SkillSource.js.map +1 -0
  191. package/dist/server/SkillsFunctionality.d.ts +8 -27
  192. package/dist/server/SkillsFunctionality.d.ts.map +1 -1
  193. package/dist/server/SkillsFunctionality.js +218 -270
  194. package/dist/server/SkillsFunctionality.js.map +1 -1
  195. package/dist/server/WorkerModelResolver.d.ts +1 -2
  196. package/dist/server/WorkerModelResolver.d.ts.map +1 -1
  197. package/dist/server/WorkerModelResolver.js +18 -19
  198. package/dist/server/WorkerModelResolver.js.map +1 -1
  199. package/dist/server/WorkspacePlugins.d.ts +25 -0
  200. package/dist/server/WorkspacePlugins.d.ts.map +1 -0
  201. package/dist/server/WorkspacePlugins.js +43 -0
  202. package/dist/server/WorkspacePlugins.js.map +1 -0
  203. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  204. package/dist/server/dispatch-as-plurnk.js +2 -1
  205. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  206. package/dist/server/envelope.js +1 -1
  207. package/dist/server/envelope.js.map +1 -1
  208. package/dist/server/loop-model.d.ts.map +1 -1
  209. package/dist/server/loop-model.js +1 -2
  210. package/dist/server/loop-model.js.map +1 -1
  211. package/dist/server/module-discovery.d.ts +6 -0
  212. package/dist/server/module-discovery.d.ts.map +1 -1
  213. package/dist/server/module-discovery.js +46 -24
  214. package/dist/server/module-discovery.js.map +1 -1
  215. package/dist/server/skills-problems.d.ts +8 -0
  216. package/dist/server/skills-problems.d.ts.map +1 -0
  217. package/dist/server/skills-problems.js +18 -0
  218. package/dist/server/skills-problems.js.map +1 -0
  219. package/dist/service.d.ts.map +1 -1
  220. package/dist/service.js +55 -45
  221. package/dist/service.js.map +1 -1
  222. package/docs/env.md +7 -8
  223. package/docs/members.md +4 -3
  224. package/docs/skills.md +45 -22
  225. package/package.json +37 -35
  226. package/plurnk.service +29 -0
package/SPEC.md CHANGED
@@ -133,7 +133,7 @@ package map. The default installed composition is specified in {§bundled-set}.
133
133
 
134
134
  OpenTelemetry may observe PLURNK; it never becomes product state, failure transport, scheduler input, model teaching, or client protocol. Domain and client activity remain on AG-UI. Reusable packages depend on the OTel API only; the daemon constructs only the explicitly configured trace and metric providers. An unconfigured or standards-valid disabled process loads no SDK or exporter implementation and keeps the API's no-op behavior with bounded overhead. OTel Logs have no provider or initialization path.
135
135
 
136
- Configuration uses the standard `OTEL_*` environment: `OTEL_TRACES_EXPORTER` / `OTEL_METRICS_EXPORTER` select `otlp` or `console` per signal (a missing or `none` value keeps that signal off; no SDK default selects an exporter), `OTEL_SERVICE_NAME` names the service (default `plurnk-service`), case-insensitive `true` in `OTEL_SDK_DISABLED` turns the boundary off, and OTLP exporters honor `OTEL_EXPORTER_OTLP_*`. An unknown exporter name fails daemon boot; a typo never silently disables observation. OTel Logs and direct draft semantic-convention use are excluded. HTTP spans carry only an AG-UI-owned bounded route class, never an input pathname or query. Spans otherwise carry high-cardinality identifiers; metric labels stay low-cardinality. Prompts, reasoning, file bodies, arbitrary URLs, secrets, and plugin payloads are never recorded as attributes or metric values by default. Exporter failure cannot change product results or client lifecycle. Daemon, telemetry, and database teardown are independent reverse-ownership phases; every phase runs and aggregate failure preserves every cause.
136
+ Configuration uses the standard `OTEL_*` environment: `OTEL_TRACES_EXPORTER` / `OTEL_METRICS_EXPORTER` select `otlp` or `console` per signal (a missing or `none` value keeps that signal off; no SDK default selects an exporter), `OTEL_SERVICE_NAME` names the service (default `plurnk-service`), case-insensitive `true` in `OTEL_SDK_DISABLED` turns the boundary off, and OTLP exporters honor `OTEL_EXPORTER_OTLP_*`. An unknown exporter name makes observability unavailable with a configuration diagnostic ({§configuration-repair-path}); offline checking rejects the same input before loading an SDK. OTel Logs and direct draft semantic-convention use are excluded. HTTP spans carry only an AG-UI-owned bounded route class, never an input pathname or query. Spans otherwise carry high-cardinality identifiers; metric labels stay low-cardinality. Prompts, reasoning, file bodies, arbitrary URLs, secrets, and plugin payloads are never recorded as attributes or metric values by default. Exporter failure cannot change product results or client lifecycle. Daemon, telemetry, and database teardown are independent reverse-ownership phases; every phase runs and aggregate failure preserves every cause.
137
137
 
138
138
  §observability-genai-conventions **GenAI convention projection.** Provider
139
139
  request spans follow the [GenAI registry at `c88d504`](https://github.com/open-telemetry/semantic-conventions-genai/tree/c88d504ab3d9879f8e50d3cc87e69775e11db234),
@@ -188,12 +188,13 @@ an explicitly specified subpath, not in the frozen root barrel.
188
188
  ```mermaid
189
189
  flowchart LR
190
190
  LISTENER["Bind client listener<br/>unready: HTTP 503"] --> DB["Acquire daemon lock<br/>and admit SQLite schema"]
191
- DB --> PROVIDER["Resolve and verify<br/>selected provider"]
192
- PROVIDER --> DAEMON["Construct and start<br/>daemon composition"]
191
+ DB --> DAEMON["Construct and start<br/>daemon composition"]
193
192
  DAEMON --> CLIENT["Activate client transport"]
193
+ CLIENT --> SELECT["Worker selects or first uses a model"]
194
+ SELECT --> PROVIDER["Construct and verify<br/>selected provider"]
194
195
  LISTENER -. failure .-> FAIL["Fail startup<br/>durable state untouched"]
195
196
  DB -. failure .-> CLOSE_LISTENER["Close listener"] --> FAIL
196
- PROVIDER -. failure .-> CLOSE["Close database<br/>and release lock"] --> FAIL
197
+ PROVIDER -. failure .-> REPAIR["Return selection failure<br/>client remains available"]
197
198
  DAEMON -. failure .-> TEARDOWN["Close every started owner"] --> FAIL
198
199
  ```
199
200
 
@@ -220,14 +221,17 @@ address by URL, never by port (#641): AG-UI mounts `/` and `/agui`, A2A the
220
221
  well-known card and its endpoint path, on the same address.
221
222
 
222
223
  §startup-admission-order After listener ownership, database admission completes
223
- before provider or capability initialization can perform external work. Every
224
+ before capability initialization can perform external work. Provider construction
225
+ and endpoint verification occur on worker selection or first use, never merely
226
+ because a default selector is configured. Startup validates selectors without
227
+ provider I/O and retains their configuration diagnostics. Every
224
228
  later startup failure closes resources in reverse ownership order while
225
229
  preserving the originating failure: daemon, observability, database, listener.
226
230
 
227
231
  §startup-readiness-line **Readiness is one stdout line.** After the client interface is mounted
228
232
  the service prints exactly one line, `plurnk-service agui=<url> db=<json string> route=<json string>`:
229
233
  the URL brackets an IPv6 host, and the database path and the route (the active model route or
230
- `no model`) are JSON strings, so a path or route containing spaces is exact and a consumer parses
234
+ `no model`, or `invalid model configuration`) are JSON strings, so a path or route containing spaces is exact and a consumer parses
231
235
  the URL as a URL and the strings as JSON; nothing else the service prints on stdout before it has
232
236
  that prefix. Before the line the listener answers `503 service-starting`; after it,
233
237
  `discover` is the identity check a launcher uses to tell this daemon from any other listener. A bind
@@ -378,7 +382,7 @@ direct-entry-plus-directory count; `-1` enables the ordinary markerless page;
378
382
  unset / `0` disables previews. `log://` is absent because the current worker's
379
383
  log already renders in present mode.
380
384
 
381
- §worker-initialization-entry **Model-worker initialization is a real `_plurnk` turn.** A model worker's first loop begins with one packetless `{ producer="_plurnk", kind="initialization" }` turn submitted through {§turn-ops-admission-path}. Its reasoning and program are stored before execution. NOTEs from its reasoning and program, the orienting READ/FIND surveys, and the reasoning READ in {§reasoning-initial-read} execute under {§op-execution-order}. Its program is announced at `log:///1/1/1/emission` ({§emission-row}) and supplies the worked example as the first request's assistant message ({§packet-wire-envelope}); no program READ, actionless source row or simulated READ is added. Every orienting row is structurally classified `_plurnk` and `init`. The namespace surveys and their asides follow {§actor-boundary-catalog-preview}.
385
+ §worker-initialization-entry **Model-worker initialization is a real reasoning-only `_plurnk` turn.** A model worker's first loop begins with one packetless `{ producer="_plurnk", kind="initialization" }` turn submitted through {§turn-ops-admission-path}. Its authored reasoning contains the orientation NOTE, READ/FIND surveys and its own reasoning READ ({§reasoning-initial-read}); it is stored before those operations execute through {§reasoning-operations} and {§op-execution-order}. No content program or emission row is fabricated ({§emission-row}). The first request sees ordinary results and the reasoning READ, not an assistant-content copy of the surveys. Every orienting row is structurally classified `_plurnk` and `init`. The namespace surveys and their asides follow {§actor-boundary-catalog-preview}.
382
386
 
383
387
  Incoming messages publish once as inbound SEND rows in the first model turn
384
388
  ({§message-arrival}); initialization neither READs nor archives them. The turn
@@ -1049,7 +1053,7 @@ boundary.
1049
1053
  - §worker-lifecycle-wake-liveness **A stream conclusion always reaches its worker.** The stream first persists its terminal state. A worker **blocked on a 202 wait** for that stream ({§wait-obligation-matrix}) then **awakens that loop in place** — the blocked loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. An already-active worker needs no injected prompt or second wake because its next packet reads the durable terminal state. A concluded worker receives no synthetic loop from ambient stream closure. The result remains available in the stream's own state under every case.
1050
1054
  - §worker-lifecycle-child-wake **Each child task completion notifies its parent.** Terminal-task publication, including failure and cancellation of a parked task, notifies the direct parent without injecting a prompt. Other unfinished tasks or streams in that child remain independent obligations; they cannot suppress notification. The parent's eligible waits requeue in place under {§loop-wake-identity} and the bounded {§worker-optimistic-settlement} opportunity. Durable revisioning covers completion-before-park and restart; drain teardown and whole-worker quiescence are not completion identities.
1051
1055
  - §worker-optimistic-settlement **Asynchronous settlement receives one bounded worker-local opportunity before model dispatch.** An initiating turn lets only the streams it started settle before program completion; separately, a stream conclusion, direct-child conclusion or addressed reply persists and publishes immediately but holds eligible parked loops' `202→100` requeues while another stream or direct child remains live. Both use `PLURNK_SERVICE_OPTIMISTIC_WAIT_MS`, shipped at five seconds; zero disables the opportunity. The wake hold ends as soon as no sibling obligation remains, never extends its original deadline, and coalesces arrivals within that window into at most one requeue per eligible loop. With no sibling obligation the wake is immediate; at the deadline, surviving work follows the ordinary monitored lifecycle. An arrival after provider dispatch begins retains its next wake, while poll, new-request and operator wakes never open this hold. Only packet/provider dispatch waits: durable state, client events, cancellation and the replying program do not. One redaction-safe span records elapsed time, quiescence versus deadline, and arrival count without entering the packet.
1052
- - §worker-lifecycle-idle-is-concluded **Idle is not unanswered.** An empty WAIT continues; an eligible final response with answered messages, observed results and no held work concludes under {§wait-obligation-matrix}. A concluded worker retains durable history; a later addressed arrival starts a new loop.
1056
+ - §worker-lifecycle-idle-is-concluded **Idle is not completion.** An empty WAIT continues; an eligible parameterless KILL with observed arrivals and results and no held work concludes under {§wait-obligation-matrix}, with or without an answer body. A concluded worker retains durable history; a later addressed arrival starts a new loop.
1053
1057
  - §worker-lifecycle-no-lost-loop **A loop is never stranded by a drain's exit.** A drain relinquishes its registry slot only after a lock-held re-claim confirms the queue is empty; a loop enqueued during that teardown is either re-claimed by the exiting drain or claimed by a fresh drain that a later inject starts. The relinquish and the start are serialized, so neither the lost-loop hang nor a transient double-drain can occur.
1054
1058
  - §worker-lifecycle-durable-disposition **Durable disposition wins cancellation races.** At a turn boundary, the engine reads the loop's durable status before interpreting a process-local abort. A committed `202` park survives a later daemon-shutdown signal; only a loop still durably running at `102` can be terminalized by that cancellation. Wake selection rechecks shutdown and worker cancellation before requeuing each parked loop.
1055
1059
  - §worker-lifecycle-restart-recovery **Restart is owner-loss reconciliation, not replay.** Before opening client transports, the service holds an exclusive database-adjacent daemon lock; a second live owner fails before touching SQLite, while a dead-PID crash claim is replaced atomically without a timeout lease. Boot preserves accepted `100` loops and restores their drains. A `102` loop belonged to a vanished drain/provider call, so it settles `500` with the interruption on its durable row—never replayed across an unknown effect boundary. Every pending physical provider request first settles as an error with absent usage and explicitly unknown cost; then its logical model call closes. Recovery never fabricates zero evidence. Every durable proposed operation likewise lost its process-local resolution waiter and settles as a visible `500 owner_vanished` occurrence rather than an unresolvable interrupt ({§proposal-list}). A pending client interaction also lost its exact awaiting operation, so boot removes the orphan instead of replaying work or inventing a response ({§client-interactions}). Every durable-open subscription belonged to a vanished callable: active channels become errored and its row closes `500`. A `202` continuation requeues on an unseen completion or when no live obligation remains. Otherwise it stays parked on surviving children; the drain restores inherited stream observation through the same guarded scheduler ({§worker-wait-timing}). Child terminalization wakes its parked parent on every outcome, including provider exceptions, cancellation, and restart interruption, recursively through the durable parent edges. These operations are idempotent, so an interrupted recovery safely repeats.
@@ -1078,6 +1082,16 @@ plugin-authored operation turns; exposing that path must not introduce a
1078
1082
  parallel record or lifecycle. Producer and kind never change. Process-restart
1079
1083
  recovery completes any turn whose producer vanished.
1080
1084
 
1085
+ §turn-exception-outcome An exceptional inference exit completes every still-open
1086
+ turn it acquired, without overwriting completed turns or inventing provider evidence.
1087
+ The turn owner propagates the original exception unchanged.
1088
+
1089
+ | Exception cause | Turn status |
1090
+ |---|---|
1091
+ | The owning aborted signal's reason, directly or through an `Error.cause` chain | 499 for cancellation; 504 for the loop execution deadline. |
1092
+ | An unrelated exception, including an unrelated `AbortError` concurrent with cancellation | 500. An aborted signal alone does not prove causation. |
1093
+ | An `AggregateError` combining cancellation with other failures | 500; cancellation does not conceal another failure. |
1094
+
1081
1095
  §turn-ops-admission-path **Source acquisition varies; admitted-turn execution does not.**
1082
1096
  A provider response, deterministic `_plurnk` program, or future client/plugin
1083
1097
  program crosses one admission boundary into the same executor. That executor
@@ -1214,7 +1228,8 @@ A crossing terminal names the source that struck the crossing turn — `repetiti
1214
1228
  `no_operation`, then `operation` — in its detail, in that order when a turn matches more
1215
1229
  than one. The three are not interchangeable: a turn that authored no operation did not *fail*
1216
1230
  one, and reporting it as a failed turn misreads a model answering without the fence as a model
1217
- whose operations broke. This is the crossing turn's source, not the streak's composition; the
1231
+ whose operations broke. A cycle of empty turns names repeated responses without operations,
1232
+ not repeated operations or results. This is the crossing turn's source, not the streak's composition; the
1218
1233
  rail rules on the crossing and does not retain the kinds behind it. What the crossing turn
1219
1234
  actually said is cited, not discarded ({§terminal-evidence}). Naming the source is not the
1220
1235
  private accounting {§rail-accounting-private} withholds: the streak, the cycle verdict and
@@ -1277,7 +1292,7 @@ The parser owns its boundaries; core admits determinate work and exposes its fai
1277
1292
  | Bounded program, including malformed operations | Admit valid operations and record parser failures; with no authored operation, apply {§empty-turn}. |
1278
1293
  | Outside response text | Store it as the turn's `outside` source under {§outside-text}; never a row, never delivered, never completion. |
1279
1294
  | Lost boundary after a closed operation | Admit the closed operations and record the boundary diagnostic under {§unparsed-tail-boundary}. |
1280
- | Lost boundary before any closed operation | Reject the attempt; neither outside text nor a reasoning NOTE substitutes for a closed response operation. |
1295
+ | Lost boundary before any closed operation | Reject the attempt; neither outside text nor a reasoning NOTE substitutes for a closed operational statement. Reasoning FIND/READ count as such statements ({§reasoning-operations}). |
1281
1296
  | Outside text carrying a log-entry heading, other than an emission row's | Reject the attempt ({§fabricated-log-entry}). |
1282
1297
  | A response the provider stopped at a repeated line | Reject the attempt with the provider's sentence as its diagnostic ({§repetition-stop}); no provider recovery, notice, or problem row. |
1283
1298
 
@@ -1460,9 +1475,23 @@ sequences; native messages and workspace outputs use opaque identifiers rather t
1460
1475
  pretending to be turn coordinates. `worker://<name>` addresses the actor, not a
1461
1476
  historical execution. No address grants ownership or access restrictions.
1462
1477
 
1463
- §fs-namespace **The workspace is a mount namespace; `project_root` is the model's `/`.** A namespace *names*; it does not confine. Host paths do not exist in it, and no engine surface folds a host-absolute spelling onto a member — not because a wall refuses them, but because those coordinates have no meaning here. What the model can reach is exactly the mount table, which the operator composes: a membership overlay routinely mounts a path from above the root (`../house-policy.md` is an ordinary `include` grantor, {§fs-visibility-grantors}), and it arrives named in namespace coordinates like everything else. Plurnk is therefore not a sandbox and claims no containment — confinement is the host's job; what Plurnk owns is authority, consent and audit. The root is **fixed immutably at workspace creation** (headless is forever); the mount table changes only through the declared membership overlay ({§membership}), never by re-rooting. At `project_root = /` the namespace is the whole filesystem and every rule below degenerates to identity — the design's proof case, and the common benchmark topology.
1478
+ §fs-namespace **Filesystem coordinates are ordinary paths; internal addresses are project-relative.** `project_root` is the base directory, not a replacement for the operating-system `/`. It is fixed at workspace creation; a headless workspace has no implicit filesystem base. Absolute paths and paths emitted by executors name the same filesystem locations as they do on the host. Resolution never grants membership or creation authority ({§fs-visibility-grantors}, {§fs-write-surface}); an outside-root member retains its `../`-prefixed key. Plurnk is not a sandbox: confinement belongs to the host.
1479
+
1480
+ §fs-namei **Resolve, then relativize, through one pathname resolver.** Resolve relative input against `project_root` and absolute input from the operating-system root, normalize lexical `.`/`..` segments, then translate the result to a `project_root`-relative key before storage, comparison or canonical rendering. Physical membership and symlink checks remain separate. No failed absolute lookup retries as a project-relative spelling.
1481
+
1482
+ | Input with `project_root=/work/project` | Canonical address |
1483
+ |---|---|
1484
+ | `src/x.md`, `./src/x.md`, `/work/project/src/x.md` | `src/x.md` |
1485
+ | `/src/x.md` | `../../src/x.md` |
1486
+ | `../policy.md`, `/work/policy.md` | `../policy.md` |
1487
+ | `.`, `/work/project/` | The project collection, not a file entry |
1488
+ | `/` | `../../`, the operating-system root collection |
1489
+
1490
+ At `project_root=/`, `/src/x.md` and `src/x.md` resolve to the same key. Without a project root, absolute paths cannot be translated; no process CWD or home directory is substituted.
1491
+
1492
+ Folders and globs select members in those same filesystem coordinates. Parent-directory selectors may include in-root and outside-root members; they never scan or admit unrelated disk contents. Catalog paths and grouped subtree selectors remain project-relative.
1464
1493
 
1465
- §fs-namei **Resolution is namei over the mount table.** The model's CWD is permanently `/`, so `src/x.md` and `/src/x.md` are the same name — the slash rule is a corollary, never a legislated equivalence. Resolution is lexical: `.` and `..` resolve before anything touches storage (`..` is legal *during* traversal); the final name lands in the root subtree (a bare key), on a declared outside-root mount (a `../`-prefixed key — the git-style overlay), or names nothing (404 carrying the resolved form). Containment is the resolution semantics — there is no separate traversal check to forget.
1494
+ §file-path-normalization An authored model file operation using an absolute path receives a `scheme:file/path_normalized` warning Notice naming its project-relative address. COPY/MOVE cover each absolute operand. The Notice neither changes the operation result nor causes a strike, and does not repeat for automatic observations or already-relative paths. Root-mounted workspaces need no such notice. It never suggests that an unadmitted path has become a member.
1466
1495
 
1467
1496
  §fs-canonical-name **One canonical name, storage ≡ wire: the git pathspec.** Member keys follow gitformat-index(5) verbatim (reference edition: git 2.47.3): relative to the workspace `project_root`, without leading slash, `/`-separated, no trailing slash or NUL. Directories are never entries and the root needs no name. When `project_root` is below the containing repository's top level, Git members above it naturally use the same `../`-prefixed CWD-relative names that `git ls-files` emits without `--full-name`; these are not outside-repository mounts. The database stores that root-relative key directly because workspace identity is rooted at the access point. Every model spelling canonicalizes before storage or comparison.
1468
1497
 
@@ -1529,6 +1558,8 @@ invalid range, read-only authority, and occupied hidden state without guessing.
1529
1558
 
1530
1559
  §file-directory-target **A directory is named as a directory.** Inside the root, a READ (or other exact-path read), KILL or EDIT whose target is a directory on disk — with or without a trailing slash — is refused `path-is-directory`, never as a missing or non-member file, since admitting it is not what the model needs: READ and KILL answer 404, EDIT 403. The detail is `'<key>' is a directory, not a file; <OP> reads/removes/writes one file.` and the recovery names the listing that reaches its files, `` List its files with `FIND (<key>/)`, then READ one by its path. `` (KILL: `then KILL each by its path`; EDIT: `` Name a file inside it, as `EDIT (<key>/<file>)`; list its files with `FIND (<key>/)`. ``). Beyond the root the disk stays dark and {§membership-read-refusal} holds unchanged.
1531
1560
 
1561
+ §file-find-directory **FIND recognizes existing directories without requiring a trailing slash.** An exact target naming a directory inside the workspace root resolves to the same recursive collection as its slash-suffixed spelling, including content matching and result pagination. Only workspace members appear; an empty directory or one containing only non-members yields a successful empty survey. Exact files stay exact, genuinely absent paths retain their missing-entry error, and this resolution does not inspect disk paths beyond the root. Shared glob and non-file URI semantics are unchanged.
1562
+
1532
1563
  ### §scheme-manifest Manifest
1533
1564
 
1534
1565
  §scheme-manifest-manifest Per the framework-owned author contract ({§manifest}), each registered scheme exposes one closed `SchemeManifest`. `Manifest.of` validates the complete declaration and enforces that `manifest.name` matches `package.json#plurnk.name` before registration.
@@ -2171,11 +2202,12 @@ same transitions the dispatcher's atomic curation event makes, without the row.
2171
2202
 
2172
2203
  ### §reasoning-initial-read Initial reasoning observation
2173
2204
 
2174
- The initialization turn records a short `_plurnk`-authored rationale containing
2175
- a fenced NOTE. The shared reasoning extractor ({§reasoning-notes}) executes that
2176
- NOTE through ordinary dispatch, creating its log item and immutable source.
2177
- The program begins with its own NOTE and READs its reasoning,
2178
- demonstrating both NOTE placements and their ordinary results. The initial message arrives separately as an
2205
+ The initialization turn records its `_plurnk`-authored rationale and complete
2206
+ NOTE/FIND/READ program as one reasoning source before dispatch. The shared
2207
+ reasoning extractor ({§reasoning-operations}) admits every operation once, in
2208
+ source order. Its final READ observes that same reasoning source, including
2209
+ the READ itself; this is an ordinary read of already stored text, not recursion
2210
+ or a future-source subscription. The initial message arrives separately as an
2179
2211
  inbound SEND ({§message-arrival}). Neither initialization nor later turns
2180
2212
  manufacture a task inventory.
2181
2213
  `PLURNK_REASONING_VIEW_LINES` (alias-scoped, default in `.env.defaults`) selects this one READ's
@@ -2184,13 +2216,14 @@ bounds it to the first N lines. Source retention, deliberate READs, and client
2184
2216
  streaming are independent. The only other automatic reasoning READ follows an empty
2185
2217
  turn ({§reasoning-empty-turn-read}).
2186
2218
 
2187
- §reasoning-empty-turn-read **An empty turn's reasoning is read back to the model.** After a
2188
- turn admitted under {§empty-turn}, one runtime turn of the same loop
2219
+ §reasoning-empty-turn-read **Operation-free reasoning is read back for recovery.** After a
2220
+ turn admitted under {§empty-turn} with no admitted reasoning operations, one runtime turn of the same loop
2189
2221
  (`{ producer="_plurnk", kind="operation" }`) dispatches
2190
2222
  `READ (reasoning://<worker>/<loop>/<turn>) <!-- turn N emitted no OP -->` over that turn's stored
2191
2223
  reasoning source; its receipt renders in the next packet like any other log row.
2192
2224
  `PLURNK_REASONING_EMPTY_TURN_LINES` (alias-scoped, default in `.env.defaults`) selects the scope on the same
2193
- scale as `PLURNK_REASONING_VIEW_LINES`. No read follows a turn without reasoning, and none follows
2225
+ scale as `PLURNK_REASONING_VIEW_LINES`. Any admitted reasoning NOTE, FIND or READ suppresses
2226
+ this recovery readback. No read follows a turn without reasoning, and none follows
2194
2227
  a turn whose emission or reasoning carries a foreign tool-call grammar or leaked template token
2195
2228
  (`KnownToxins` names them); the strike and its error row are unchanged.
2196
2229
 
@@ -2370,7 +2403,7 @@ single line past the end, a reversed range, empty content, a command's log row
2370
2403
 
2371
2404
  ### §turn-ops-entry The admitted turn program
2372
2405
 
2373
- §turn-ops-log-curation A source-backed turn preserves its **exact admitted Plurnk program**, including ignored interstitial text, before dispatch. `turn_sources` records that source once, separately from the curatable log and optional provider evidence. Retention does not manufacture a log row; the one row an admitted emission gains is its announcement ({§emission-row}). Ordinary READ creates a receipt governed by {§log-readable-projection}; curation of either never changes the source.
2406
+ §turn-ops-log-curation A source-backed turn preserves its **exact content emission**, including ignored interstitial text, before dispatch. `turn_sources` records that source once, separately from reasoning, the curatable log and optional provider evidence. Retention does not manufacture a log row; the one row an admitted emission gains is its announcement ({§emission-row}). Ordinary READ creates a receipt governed by {§log-readable-projection}; curation of either never changes the source.
2374
2407
 
2375
2408
  ### §emission-row The emission row
2376
2409
 
@@ -2380,22 +2413,24 @@ like any other row.
2380
2413
 
2381
2414
  | Surface | Contract |
2382
2415
  |---|---|
2383
- | When | An inference turn that admitted at least one statement from its provider content, and turn zero's survey ({§worker-initialization-entry}). A programmatic batch, an empty turn ({§empty-turn}), a client operation and a rejected attempt ({§rejected-emission-entry}) announce nothing. |
2416
+ | When | An inference turn that admitted at least one content statement. Reasoning-only turns, including initialization ({§worker-initialization-entry}), a programmatic batch, an empty turn ({§empty-turn}), a client operation and a rejected attempt ({§rejected-emission-entry}) announce nothing. |
2384
2417
  | Place | After the turn's inputs (arrivals, deltas, open-path READs) and before its reasoning NOTEs and operations, written after the selection snapshot ({§turn-ops-selection-snapshot}): the emission sits between what the worker had seen and what it caused. |
2385
2418
  | Row | A `_plurnk` READ of the turn's own source, `ops://<worker>/L/T`, with `attrs.kind="emission"` and the canonical leaf `/emission`: `### log:///L/T/S/emission → ops://<worker>/L/T · N`. It renders its author, the turn's producer, as `origin`, so a model's row carries none. It is no operation: no receipt, tool call or strike, and outside the op mix. |
2386
- | Body | Frozen: the canonical rendering ({§statement-rendering}) of every statement the parser admitted, in order. Free text and unadmitted forms are absent; a recovered native call ({§native-tool-calls}) appears as the operation it was read as; an operation whose receipt failed stays. Turn zero's is its survey as authored. `ops://` keeps the exact source ({§turn-source-resources}). |
2419
+ | Body | Frozen at announcement: the canonical rendering ({§statement-rendering}) of admitted content statements, each in a closed fence. Each body appears within the shared preview bound ({§body-projection}), `PLURNK_SERVICE_PREVIEW_LINES` and `PLURNK_SERVICE_PREVIEW_CHARS`. A longer body keeps its head, and its closing fence carries `<!-- Automatically truncated op body: READ (ops://<worker>/L/T) to retrieve in full -->`; nothing the harness writes enters a fence. Absent and empty bodies remain empty. All heading operands, scopes, metadata, patterns and asides remain. Free text and unadmitted forms are absent; a recovered native call ({§native-tool-calls}) appears as the operation it was read as; an operation whose receipt failed stays. Reasoning operations retain their own source and normal receipts, never an assistant-content copy. No body text is inspected for nested operations. |
2420
+ | Sources and memory | Dispatch and immutable `ops://` sources retain complete bodies ({§turn-source-resources}); an explicit source READ returns them normally. The wire omits NOTE and WAIT blocks, whose own rows show them whole ({§body-projection}), so curating a NOTE row removes its text; an emission of only NOTE and WAIT delivers nothing, its row stands, and no assistant message follows it ({§packet-wire-envelope}); reasoning-only NOTEs never enter this projection. |
2421
+ | Stability | The projection is fixed from its first appearance, never aged or resized under budget pressure. Already frozen announcements and historical request captures are not rewritten. |
2387
2422
  | Presentation | Born folded: the record shows its header, and its body follows the record as the worker's assistant message. |
2388
- | Accounting | `logTokens` charges the record and the emission it delivers ({§packet-token-accounting}). |
2423
+ | Accounting | `logTokens` charges the record and the emission the wire delivers, truncation asides included, never the omitted NOTE and WAIT blocks or the cut remainder ({§packet-token-accounting}). An explicit source READ has its own ordinary charge. |
2389
2424
  | Curation | Curated whole ({§log-kill-scope}): KILL retires it, and so does a scope covering every line (`<1,-1>`); on its exact coordinate a narrower scope is 422 `emission-curated-whole`, and a sweep whose scope would only trim it leaves it intact. |
2390
2425
  | Schema | Migration 12 admits `kind="emission"` only on this shape: one per turn, the turn's newest row when written, frozen, and curated whole. A database from before version 12 keeps its rows and gains no announcement. FORK copies it with the inherited turns, still naming its writer. |
2391
- | Echoes | A worker that repeats the heading in its own text is tolerated ({§fabricated-log-entry}); the digest counts the echoes. |
2426
+ | Echoes | A worker that repeats the heading in its own text is tolerated ({§fabricated-log-entry}); the digest counts the echoes. A worker that copies the truncation aside onto its own closer keeps its body intact; the aside is outside text ({§outside-text}). |
2392
2427
 
2393
2428
  ### §turn-source-resources Immutable turn-source resources
2394
2429
 
2395
2430
  | Surface | Contract |
2396
2431
  |---|---|
2397
2432
  | Identity | `ops://<worker>/<loop>/<turn>`, `reasoning://<worker>/<loop>/<turn>`, and `note://<worker>/<loop>/<turn>/<item>` name a worker in the current workspace and its durable coordinates. A note's item is its dispatched NOTE ordinal. The `outside` source ({§outside-text}) has no address: no `outside://` scheme exists, and it is reached only through `outside/event`, FORK and the digest. The worker authority is required and case-sensitive; userinfo, ports, and queries are invalid. Source identity never depends on the reading worker. `log:///` remains local; READ, FIND and KILL reject log authorities, userinfo, ports and queries with 400, never substitute the caller's log. |
2398
- | Source | `ops` is exact admitted `text/vnd.plurnk`, and its turn's emission row carries the canonical rendering ({§emission-row}); `reasoning` is `text/plain` containing the selected original provider reasoning or a non-model producer's authored rationale. Producer identity comes from the owning turn; a harness rationale is not provider evidence. The turn decides existence and the source decides content: a turn that exists but has no source of that kind reads as the ordinary empty resource (204, empty body), never a fabricated one; a worker or turn that does not exist is 404. |
2433
+ | Source | `ops` is exact admitted `text/vnd.plurnk`, and its turn's emission row carries the preview-bounded projection ({§emission-row}); `reasoning` is `text/plain` containing the selected original provider reasoning or a non-model producer's authored rationale. Producer identity comes from the owning turn; a harness rationale is not provider evidence. The turn decides existence and the source decides content: a turn that exists but has no source of that kind reads as the ordinary empty resource (204, empty body), never a fabricated one; a worker or turn that does not exist is 404. |
2399
2434
  | Notes | Each dispatched NOTE stores its exact literal body as an immutable `text/plain` source and returns its worker-qualified address. There may be multiple notes in a turn, from reasoning, content, or another producer. A missing note is 404, not an empty invented note. Sharing its URI uses ordinary SEND; the receiver deliberately READs it. NOTE itself sends no ambient update. |
2400
2435
  | Retention | One ops source, one reasoning source and one outside source per turn; one note source per NOTE ordinal. An optional inference-call link records provenance. Source removal follows deletion of its owning turn, never log curation. |
2401
2436
  | Operations | Ordinary scoped READ, FIND, content search and COPY from any named worker's source within the workspace. FIND accepts authority and path patterns, retaining complete worker-qualified identities in results and folder selectors. READ returns data and never executes it. Sources are read-only for every actor and have no edit hashes. |
@@ -2631,7 +2666,7 @@ Log history preserved — `log_entries` stores path tuple as text, not FK to `en
2631
2666
  - §find-line-anchors **A FIND regex anchors each line**, as READ, EDIT and KILL do ({§read-pattern}, {§edit-pattern}): `^` and `$` are a line's ends in every FIND content match — over entries, log rows, turn sources and a binary channel's bytes — so ```` ```FIND (django/urls/resolvers.py) /^from|^import/ ```` locates the same import lines a READ with that pattern shows, never a false 204.
2632
2667
  - §find-candidate-containment **One candidate's crash is that candidate's problem** — arbitrary member content can crash a mimetype handler mid-match (an unbalanced template partial crashed Readability and killed a 1,916-file FIND as a blank 500, #449). `Matcher.matchCandidates` contains a per-candidate handler throw: the candidate drops out exactly like unsupported content, the cause goes to daemon stderr, and only a FIND whose every candidate crashed reports a 415 whose Problem names the first crashing member and handler. The operation's other candidates always answer.
2633
2668
 
2634
- - §find-scope-prefix-filter Filters entries within scope. A **bare** path is the exact entry; an explicit **shell glob**, classified once by {§path-glob}, expands to a scope. Path globs use segment semantics: `*` and `?` never cross `/`; `**` does — in every spelling: a `**` glued to a name (`**.go`, `src/**.ts`) is matched as `**/*.go` / `src/**/*.ts`, never demoted to a one-level `*` the way a native matcher reads it. Terminal `*` and `**` are structural catalog selectors and include dot-prefixed entries, so a complete map does not hide `.env.defaults` or `.github`; richer patterns retain native shell behavior. SQLite prefix queries may reduce the candidate set but never decide the match. A trailing slash is a recursive FIND scope only for a scheme whose manifest declares `folderScopes: true`; otherwise it is ordinary resource syntax. This is an explicit plugin contract, never inferred from URL punctuation.
2669
+ - §find-scope-prefix-filter Filters entries within scope. A **bare** path is the exact entry unless the file scheme resolves a directory under {§file-find-directory}; an explicit **shell glob**, classified once by {§path-glob}, expands to a scope. Path globs use segment semantics: `*` and `?` never cross `/`; `**` does — in every spelling: a `**` glued to a name (`**.go`, `src/**.ts`) is matched as `**/*.go` / `src/**/*.ts`, never demoted to a one-level `*` the way a native matcher reads it. Terminal `*` and `**` are structural catalog selectors and include dot-prefixed entries, so a complete map does not hide `.env.defaults` or `.github`; richer patterns retain native shell behavior. SQLite prefix queries may reduce the candidate set but never decide the match. A trailing slash is a recursive FIND scope only for a scheme whose manifest declares `folderScopes: true`; otherwise it is ordinary resource syntax. This is an explicit plugin contract, never inferred from URL punctuation.
2635
2670
 
2636
2671
  Resource-authority globs select authorities independently of the path scope.
2637
2672
  Matching resources retain their full addresses through pattern matching,
@@ -2648,7 +2683,7 @@ Log history preserved — `log_entries` stores path tuple as text, not FK to `en
2648
2683
  - §find-fulltext-selection Every matcher operates only over the candidate set selected by `(target)`; indexed matchers do not bypass that selection. `~query` passes the native FTS5 expression to SQLite and ranks matching candidates by ascending BM25, with resource identity breaking ties. Native BM25 uses the shared index's term statistics; candidate visibility, owner, channel and target filters determine which resources can be returned. The ordinary FIND pager selects resources for broad targets or match locations for exact targets: markerless search uses {§markerless-first-page}, `<N>` selects position N and `<N,M>` selects an inclusive range. Fractions are invalid result coordinates, not similarity thresholds. Results expose addressable matched text regions; neither cosine scores nor percentage similarity is invented. Native query-syntax failures return 400 with SQLite's diagnostic; database and implementation failures propagate.
2649
2684
  - §fts-word-phrase **A word with inner punctuation is the phrase of its tokens.** FTS5 barewords hold only letters, digits, `_` and non-ASCII, so before the query reaches SQLite each word outside a quoted string or `NEAR(…)` group that is not a bareword (with optional leading `^` and trailing `*`) is quoted as a phrase: `~inherited-members` searches `"inherited-members"` — the adjacent tokens `inherited members` — instead of failing as `no such column: members`, and `c++`, `x.y`, `a/b` likewise. FTS5's own syntax passes untouched: `AND`/`OR`/`NOT`/`NEAR`, `+`, quoted phrases, parentheses, and column filters (a word containing `:`, `{` or `}`, or opening with `-`). A column-filter failure keeps SQLite's diagnostic and its recovery says what the filter is and gives the bare-word and `NOT` forms: `` `members:` and `-members` are FTS5 column filters, and the index has one column; to search for a word write it bare, as `~members`, and to exclude one write `NOT` between terms, as `~a NOT members`. ``
2650
2685
  - §find-scoped-isolation Workspace + scheme scoped — no cross-workspace/cross-scheme leakage.
2651
- - §find-result-projection **The authored target shape determines the result unit; result cardinality never changes it** ({§find-result-unit}). Returns `FindResult { status, content, mimetype, results, range, matchingPathCount, matchLocationCount, itemsWeightTotal, returnedItemsWeightTotal }`:
2686
+ - §find-result-projection **The resolved target selection determines the result unit; result cardinality never changes it** ({§find-result-unit}). Returns `FindResult { status, content, mimetype, results, range, matchingPathCount, matchLocationCount, itemsWeightTotal, returnedItemsWeightTotal }`:
2652
2687
 
2653
2688
  | Target | Matcher | `range.unit` | Result rows |
2654
2689
  |---|---|---|---|
@@ -2734,10 +2769,9 @@ same durable liveness.
2734
2769
  | Fresh operation/parser failure, without an authored WAIT | Continue before any automatic parking. |
2735
2770
  | Neither an authored WAIT nor an eligible completion request ({§kill-conclusion}) | Continue, regardless of earlier replies or live work. |
2736
2771
  | Live work and either WAIT or an eligible completion request | Park the same loop; message arrival, child or stream settlement, or stream cadence wakes it. No final-answer body is delivered while joining. |
2737
- | WAIT without live work | Continue; never invent a future wake. The first such WAIT is an honest yield and its row says only `Nothing is in flight. Continuing.`; a second in the same loop is the model waiting on a wake nothing can send, so its own row instead names what WAIT is for and what to reach for — `WAIT doesn't wait unless there's a child worker or stream to wait on. Use schedule for specific timing decisions.` The correction rides the operation's own result, which is the surface the model is certain to read. |
2738
- | Unanswered messages | Continue. |
2772
+ | WAIT without live work | Continue; never invent a future wake. Its row says only `Nothing is in flight. Continuing.`, however often the loop yields this way. |
2739
2773
  | Unobserved operation results, failures, child results or stream conclusions | Continue; the next packet presents them. |
2740
- | Eligible completion request with no outstanding messages, live work or unobserved results | Conclude successfully. |
2774
+ | Eligible completion request with no unpublished arrivals, live work or unobserved results | Conclude successfully, whether or not it delivers an answer. |
2741
2775
 
2742
2776
  An empty emission is handled by {§empty-turn}. Ordinary strikes, cycles and
2743
2777
  execution limits remain independent. NOTE and successful targeted KILL do not themselves
@@ -2797,11 +2831,12 @@ accounting and model-visible failure evidence remain separately owned by
2797
2831
  outstanding condition. Valid sibling operations always execute. Only an admitted
2798
2832
  KILL delivers its literal body through {§send-response-receipt}; a deferred body
2799
2833
  remains forensic evidence, never a stored draft to replay automatically. An empty
2800
- KILL concludes without repeating an already-delivered answer, but cannot abandon an
2801
- unanswered message. SEND, NOTE and targeted KILL never request successful
2834
+ KILL concludes silently even when an observed message has no delivered answer. It neither
2835
+ invents delivery nor replaces an earlier answer; immutable input and reply history remain intact.
2836
+ SEND, NOTE and targeted KILL never request successful
2802
2837
  completion. New arrivals still guard the terminal transition atomically
2803
- ({§completion-defers-to-messages}); an arrival concurrent with an accepted reply
2804
- remains unanswered and keeps the loop running. No implicit successful exit exists.
2838
+ ({§completion-defers-to-messages}); an unobserved arrival concurrent with completion
2839
+ keeps the loop running. No implicit successful exit exists.
2805
2840
  - §outside-text **Text outside the operations is the turn's `outside` source: stored, weighed, never a row.**
2806
2841
  The spans {§response-text} supplies are stored verbatim as one immutable `outside` turn source
2807
2842
  per admitted emission, in source order joined by a blank line ({§turn-source-resources}); no
@@ -2821,15 +2856,16 @@ accounting and model-visible failure evidence remain separately owned by
2821
2856
  {§response-text} alone owns which bytes are operations, quotations or outside text.
2822
2857
  - §loop-answer **A loop's address is what it said.** READ `ops://<worker>/<loop>` resolves to
2823
2858
  the latest reply the loop gave to the message that started it: the body of a SEND
2824
- or accepted final KILL that answered that message. A running loop without one is 425; a loop that
2825
- ended without one is its terminal problem (404 when it ended 2xx) — and that problem cites what
2826
- the model last left unconcluded, so the loop's own address never reports silence from a loop that
2827
- spoke ({§terminal-evidence}). `ops://<worker>/<loop>/<turn>`
2859
+ or accepted final KILL that answered that message. A failed terminal takes precedence over
2860
+ an earlier reply and retains its exact Problem, including {§terminal-evidence}. A running loop
2861
+ without a reply is 425; a concluded loop without one returns its terminal outcome, including
2862
+ successful silence. Completion never fabricates an answer or a missing-resource failure.
2863
+ `ops://<worker>/<loop>/<turn>`
2828
2864
  remains that turn's emission. A concluded child's `loop_termination` row to its parent
2829
2865
  READs this same loop resource. Witness: `test/intg/loop-answer.test.ts`.
2830
2866
  - §empty-turn **No authored response operation is a recoverable turn, never completion.**
2831
- Count parsed response operations before reasoning NOTEs join them; neither they nor outside
2832
- text ({§outside-text}) enter the count. When none exist and no boundary was lost, retain the turn
2867
+ Count parsed content operations and reasoning FIND/READs ({§reasoning-operations}); neither
2868
+ reasoning NOTEs nor outside text ({§outside-text}) enter the count. When none exist and no boundary was lost, retain the turn
2833
2869
  and its raw sources and count one progress-contract strike, whether or not the turn carried text. The strike sends no notice of its own: the turn records one `_plurnk`
2834
2870
  error row, `422` `The turn performed no operation.`, which rides the next packet's errors like
2835
2871
  any failure ({§operation-result-uniform-error-channel}), and its reasoning is read back to the
@@ -2938,8 +2974,9 @@ target that cannot be read keeps the owning READ's failure identity (#163) and s
2938
2974
  the slot contract in its recovery — the resource is the program and the body its stdin;
2939
2975
  a command belongs beneath a targetless heading — without guessing which was meant (#425). The started receipt always
2940
2976
  names the working directory only when it is not the project root, and then in the
2941
- model's own project-relative form ({§fs-namespace}: the root is the model's `/`, so it
2942
- is never rendered, and no receipt or Problem carries a host-absolute path). The `(path)` is a program — a script for an interpreter, a tool name for a tool
2977
+ project-relative form ({§fs-namespace}); the default directory is omitted rather
2978
+ than repeated in every receipt. Native file addresses resolve from that same project
2979
+ directory, while absolute input retains its filesystem meaning. The `(path)` is a program — a script for an interpreter, a tool name for a tool
2943
2980
  family — and neither a command nor a working directory is ever a target. The default
2944
2981
  shell is written as its own fence, ```` ```sh ````; no runtime-less form exists.
2945
2982
 
@@ -3047,13 +3084,15 @@ the workspace snapshot. Installed siblings form the immutable base:
3047
3084
  they are discovered and probed at startup, and availability is cached.
3048
3085
  Workspace Functionality providers may atomically overlay additional names under
3049
3086
  {§module-workspace-capabilities}; a name has one owner within a workspace, while
3050
- independent workspaces may use the same name. An absent or empty tag selects
3051
- `sh`; a non-empty tag selects exactly that registered executable tool. Unknown
3052
- tags are refused 501 with the advertised catalogue and are never reinterpreted
3053
- as shell command words. The common mistaken `[shell]` alias is narrowly told to
3054
- omit that signal for the default shell; arbitrary unknown tags receive no guessed
3055
- alternative intent. An unavailable runtime is also 501 and carries the probe
3056
- `detail`.
3087
+ independent workspaces may use the same name. The fence name selects exactly
3088
+ that registered executable tool. Unknown tags are refused 400 with the
3089
+ advertised catalogue and are never reinterpreted as shell command words.
3090
+ A runtime unavailable after an ordinary probe is 501 with the probe `detail`.
3091
+ Typed configuration failures preserve the declaration, with no executor instance
3092
+ or output scheme; invocation returns the exact 503 configuration Problem
3093
+ ({§configuration-repair-path}). No executor, including `sh`, is required for
3094
+ daemon startup. Internal constructor defects remain failures, not unavailable
3095
+ configuration verdicts.
3057
3096
 
3058
3097
  For a family runtime, `ExecutorRegistry.toolRegistry(tag, workspaceId)`
3059
3098
  validates the one executor-owned snapshot used by packet presentation,
@@ -3230,8 +3269,8 @@ Each layer uses the same value and masking rules. A worker's list includes works
3230
3269
  defaults by reference with `origin: "workspace"`; worker overrides and masks remain
3231
3270
  worker-owned. Enabling an inherited entry clears this layer's mask, not a mask in a
3232
3271
  lower layer; an explicit local value can override that lower layer. A mask follows
3233
- the name even when its lower-layer origin changes. Removing an override reveals the lower entry disabled, as for a service
3234
- baseline. Forking copies only worker state, not the workspace defaults. Workspace edits
3272
+ the name even when its lower-layer origin changes. Removing an override restores the
3273
+ lower entry and its enabledness ({§configuration-definition-resolution}). Forking copies only worker state, not the workspace defaults. Workspace edits
3235
3274
  affect subsequent launches, not existing processes or other workspaces. A shared
3236
3275
  capability never acquires an invoking worker's overrides or ownership.
3237
3276
 
@@ -3297,7 +3336,7 @@ body prefixes.
3297
3336
 
3298
3337
  ## §proposal Proposals and client interactions
3299
3338
 
3300
- §proposal-202-pauses A side-effecting op does not execute on dispatch — it **proposes**. The scheme returns **202** (an execution on a `host` runtime {§exec}, an EDIT to a member file {§membership}); the engine writes the log row `state='proposed'`, registers a waiter keyed by `logEntryId`, and **pauses `dispatch`** awaiting a resolution. The provider exchange and emitted operation are already durable, while the turn remains open until dispatch settles; {§engine-rails} therefore sees the *resolved* status, never the provisional 202. On accept the status becomes 200 and the scheme's effect runs.
3339
+ §proposal-202-pauses A side-effecting op does not execute on dispatch — it **proposes**. The scheme returns **202** (an execution on a `host` runtime {§exec}, an EDIT to a member file {§membership}); the engine writes the log row `state='proposed'`, registers a waiter keyed by `logEntryId`, and **pauses `dispatch`** awaiting a resolution. The provider exchange and emitted operation are already durable, while the turn remains open until dispatch settles; {§engine-rails} therefore sees the *resolved* status, never the provisional 202. Acceptance runs the scheme's effect and settles with its result ({§proposal-accept-applies}).
3301
3340
 
3302
3341
  **Resolution arrives through one lifecycle:**
3303
3342
 
@@ -3309,7 +3348,7 @@ body prefixes.
3309
3348
 
3310
3349
  | decision | state | `status_rx` | default outcome | effect |
3311
3350
  |---------------------------------|---|---|---|---|
3312
- | §proposal-accept-applies accept | `resolved` | 200 | — | runs the scheme's **`applyResolution`** — the real side effect (disk write, exec spawn). An unavailable handler returns `410 handler-unavailable`, never success. A failing apply (≥400) downgrades to reject, carrying the apply's own outcome — e.g. a member EDIT's `edit_collision` from its write-back compare-and-swap ({§membership-edit-write-cas}) — or `apply_failed` when it names none. |
3351
+ | §proposal-accept-applies accept | `resolved`, or `failed` when application fails | applied result's status, otherwise 200 | — | runs the scheme's **`applyResolution`** — the real side effect (disk write, exec spawn). An unavailable handler returns `410 handler-unavailable`, never success. A failing apply (≥400) preserves its result and marks the row failed without changing the client's decision; its outcome is retained, or `apply_failed` when it names none. |
3313
3352
  | §proposal-reject-fails reject | `failed` | 400 | `rejected` | none — the action did not occur. |
3314
3353
  | §proposal-cancel-aborts cancel | `cancelled` | 499 | `loop_aborted` | none — the loop is abandoning. |
3315
3354
 
@@ -3431,11 +3470,19 @@ was said ({§methods-loop-run-fold-consistency}). `LoopPolicies.compose` then ma
3431
3470
  once. `PLURNK_SERVICE_ATTENDED` answers an unstated attendance, and the attendance picks which
3432
3471
  knob answers an unstated disposition: `PLURNK_SERVICE_PROPOSALS` for an attended loop,
3433
3472
  `PLURNK_SERVICE_UNATTENDED_PROPOSALS` for an unattended one, whose vocabulary has no `review`.
3434
- Every panel state is therefore lawful, and an invalid knob fails boot by its name. No code,
3473
+ Every valid panel state is therefore lawful; an invalid knob is diagnosed at startup
3474
+ and refuses a loop that needs it ({§configuration-repair-path}). No code,
3435
3475
  schema or column holds a default ({§operator-config-only-home}): `loops.policy` and
3436
- `loops.max_turns` carry none, so every insert states both, and an administrative loop — a
3437
- client's direct statements, the runtime's own narration — states the panel's policy like any
3438
- other loop whose creator said nothing.
3476
+ `loops.max_turns` carry none, so every insert states both. Client-authored administrative
3477
+ loops use the same composition.
3478
+
3479
+ §runtime-bookkeeping-policy **Runtime bookkeeping has no reviewer and cannot acquire
3480
+ new authority.** Its administrative loops explicitly state
3481
+ `{ attended: false, proposals: "reject" }`; this is a runtime invariant, not an
3482
+ interactive default. Generated reference publication and audit narration therefore
3483
+ do not depend on client policy configuration. Runtime-authored proposals do not
3484
+ use effect-policy auto-admission; bookkeeping proposals settle as failures through
3485
+ the ordinary proposal lifecycle, never wait for a client or auto-accept.
3439
3486
 
3440
3487
  §loop-policy-effective-read `loops.policy` persists one complete immutable
3441
3488
  `LoopPolicy`; every runtime policy read validates that snapshot before use.
@@ -3538,7 +3585,7 @@ No generator. SQLite-optimal: STRICT (3.37+), `INTEGER PRIMARY KEY` aliasing, ex
3538
3585
  | §db-maintenance-optimize Statistics at shutdown | The daemon's last database step before the caller closes SQLite is `PRAGMA optimize` on the writer (`maintenance_optimize`), so `sqlite_stat1` reflects tables the connection planned against, bounded by SQLite's own analysis limit; a failure there is a reported shutdown error, never silent. Retention runs before it under the operator's policy ({§retention-policy}) and ends with a WAL truncation ({§db-space-reclamation}); no periodic `ANALYZE` runs. |
3539
3586
  | §db-space-reclamation The daemon keeps its own file healthy | `PLURNK_SERVICE_AUTO_VACUUM` (`incremental` or `none`) names the mode the daemon keeps its file in. At start, before any drain, a database in another mode is converted (set the mode, one `VACUUM`, which rewrites the file and needs free disk about its size) and the journal says so with page counts before and after. Under `incremental`, every retention pass ends by stepping `PRAGMA incremental_vacuum` to completion once free pages reach `PLURNK_SERVICE_RECLAIM_MIN_FREE_BYTES` (0 = every pass), and reports `reclaimedPages`; below the floor, free pages stay for SQLite to reuse. Under `none` the file never shrinks and freed pages are reused. No operator step is involved beyond the knobs. The WAL stays bounded by SQLite's automatic checkpoint (#764). |
3540
3587
  | §content-store Every body is stored once | `contents` holds each settled body once, addressed by its SHA-256, however many channels, workspaces, forks or derivations carry it; rows are immutable. `entry_channel_rows` points a settled channel at its body and keeps an active stream's body as a private buffer until it settles, when it is interned. Every reader and writer uses the `entry_channels` view, whose `INSTEAD OF` triggers intern bodies, refuse a bound `content_hash` that is not the content's, and write each column group only when it changed, so a search attachment is never a representation write. SQLite counts no changes for a view, so a write that must know whether its channel exists returns the channel's name; an outer join cannot flatten the view, so the two statements that need one read `entry_channel_rows` and `contents` directly. `derivation_fts` is an external-content index over `derivation_texts` (a derivation joined to its body); `derivations.content_id` names the indexed text, and the triggers in `_entry-fts.sql` move the index with it and forget it on delete. A body no channel holds and no derivation indexes is collected by retention under `PLURNK_SERVICE_COLLECT_CONTENTS`. Witnesses: `test/intg/retention.test.ts`, `test/intg/entries.test.ts`, `test/intg/fulltext-index.test.ts`. |
3541
- | §retention-policy Retention is the operator's policy; information is kept by default | `Retention` (`src/server/Retention.ts`, statements in `Retention.sql`) reads ten knobs from `.env.defaults` once at daemon construction (the two storage knobs are {§db-space-reclamation}) and runs four set statements in dependency order — on `PLURNK_SERVICE_RETENTION_INTERVAL_MS` cadence while the daemon runs (0 = shutdown only) and once more at shutdown before `PRAGMA optimize`. `PLURNK_SERVICE_RETAIN_PACKET_TURNS` (-1 = every packet) and `PLURNK_SERVICE_RETAIN_PACKET_MS` (-1 = no age limit) retire a completed turn's packet composition (`turn_sections`, {§packet-items}) once it is beyond the newest N packet-bearing turns of its loop or older than the age; the turn, its bag, its log rows and its accounting stay, and an open turn is never retired. `PLURNK_SERVICE_COLLECT_PACKET_ITEMS` collects items no composition references. `PLURNK_SERVICE_COLLECT_CONTENTS` collects stored bodies nothing holds ({§content-store}), after the collectors that release them. `PLURNK_SERVICE_COLLECT_DERIVATIONS` collects derivations no channel, turn source, or log row cites — superseded editions — with their symbols (cascade) and their full-text shadow (`derivations_delete_fts`, a process trigger beside the FTS statements, on every delete path). `PLURNK_SERVICE_RETAIN_RESPONSE_TURNS` and `PLURNK_SERVICE_RETAIN_RESPONSE_MS` retire a settled call's response body (`model_call_responses`) once it is beyond the newest N body-bearing calls of its loop or its turn is older than the age; the call's identity, failure, capacity, admission and accounting stay, and the digest renders such a call request-only. The durable record is kept; packets and response bodies — transient evidence — age out, so a daemon left running for months stops growing (#788). A malformed knob refuses daemon construction. Witness: `test/intg/retention.test.ts`. |
3588
+ | §retention-policy Retention is the operator's policy; information is kept by default | `Retention` (`src/server/Retention.ts`, statements in `Retention.sql`) reads ten knobs from `.env.defaults` once at daemon start (the two storage knobs are {§db-space-reclamation}) and runs four set statements in dependency order — on `PLURNK_SERVICE_RETENTION_INTERVAL_MS` cadence while the daemon runs (0 = shutdown only) and once more at shutdown before `PRAGMA optimize`. `PLURNK_SERVICE_RETAIN_PACKET_TURNS` (-1 = every packet) and `PLURNK_SERVICE_RETAIN_PACKET_MS` (-1 = no age limit) retire a completed turn's packet composition (`turn_sections`, {§packet-items}) once it is beyond the newest N packet-bearing turns of its loop or older than the age; the turn, its bag, its log rows and its accounting stay, and an open turn is never retired. `PLURNK_SERVICE_COLLECT_PACKET_ITEMS` collects items no composition references. `PLURNK_SERVICE_COLLECT_CONTENTS` collects stored bodies nothing holds ({§content-store}), after the collectors that release them. `PLURNK_SERVICE_COLLECT_DERIVATIONS` collects derivations no channel, turn source, or log row cites — superseded editions — with their symbols (cascade) and their full-text shadow (`derivations_delete_fts`, a process trigger beside the FTS statements, on every delete path). `PLURNK_SERVICE_RETAIN_RESPONSE_TURNS` and `PLURNK_SERVICE_RETAIN_RESPONSE_MS` retire a settled call's response body (`model_call_responses`) once it is beyond the newest N body-bearing calls of its loop or its turn is older than the age; the call's identity, failure, capacity, admission and accounting stay, and the digest renders such a call request-only. The durable record is kept; packets and response bodies — transient evidence — age out, so a daemon left running for months stops growing (#788). An invalid policy withholds retention, including storage conversion and shutdown collection, and reports its configuration diagnostic without blocking the client ({§configuration-repair-path}). Storage I/O failures remain ordinary startup or shutdown failures. Witness: `test/intg/retention.test.ts`. |
3542
3589
 
3543
3590
  - DDL = storage truth; JSON Schemas = wire truth. They are allowed to differ where ergonomics demand.
3544
3591
  - §entry-identity-no-null **Identity components are never NULL.** `(workspace_id, scheme, authority, pathname)` is a unique key. `workspace_id` references the workspace directly with cascading deletion. Namespace schemes use empty authority; resource schemes retain their canonical authority. File members use nonempty `scheme="file"` and render as bare paths. Registration refuses `storedScheme: null`.
@@ -3684,11 +3731,14 @@ and is ignored rather than resolved against the working directory.
3684
3731
 
3685
3732
  | Class | Base | Plurnk member |
3686
3733
  |---|---|---|
3687
- | Configuration | `$XDG_CONFIG_HOME` (default `~/.config`) | `plurnk/.env`, `plurnk/AGENTS.md` |
3734
+ | Configuration | `$XDG_CONFIG_HOME` (default `~/.config`) | `plurnk/.env`, `plurnk/AGENTS.md`, plurnk-only MCP definitions `plurnk/mcp.json`, Agent Skills `plurnk/skills/<name>/SKILL.md`, and Agent Plugins `plurnk/plugins/<plugin>/` |
3688
3735
  | Durable user data | `$XDG_DATA_HOME` (default `~/.local/share`) | `plurnk/plurnk.db` and SQLite sidecars |
3689
3736
  | Persistent operational state | `$XDG_STATE_HOME` (default `~/.local/state`) | On-demand workspace/module directories ({§module-workspace-directory}). |
3690
3737
  | Reproducible cache | `$XDG_CACHE_HOME` (default `~/.cache`) | Reserved; no directory is created without an owned artifact. |
3691
3738
  | Shared global Agent Skills | User home | `.agents/skills/<name>/SKILL.md` |
3739
+ | Shared global MCP definitions | User home | `.agents/mcp.json` |
3740
+ | Shared global Agent Plugins | User home | `.agents/plugins/<plugin>/` ({§agent-plugins-hosting}) |
3741
+ | A plugin's `PLUGIN_DATA` | `$XDG_DATA_HOME` | `plurnk/plugins/<plugin>/` |
3692
3742
 
3693
3743
  §state-root **A private daemon has one root.** `PLURNK_SERVICE_STATE_ROOT` (absolute; a leading
3694
3744
  `~/` expands; a relative value fails hard by name) replaces the data, state, cache and runtime homes
@@ -3710,15 +3760,34 @@ ordinary precedence; XDG variables themselves require absolute paths.
3710
3760
  |---------:|------------------------------------|-----------------------------------------------------------|
3711
3761
  | 1 | Assembled package `.env.defaults` | Set-if-unset floor; one owner per key. |
3712
3762
  | 2 | `$XDG_CONFIG_HOME/plurnk/.env` | User-level ambient configuration. |
3713
- | 3 | `./.env` | Working-directory ambient configuration. |
3714
- | 4 | `--config=<path>` | Singular service-owned explicit file. |
3715
- | 5 | `--env-file*` | Repeatable explicit files; later selected files win. |
3716
- | 6 | Initial shell environment | Preserved over every file layer. |
3717
- | 7 | Derived service CLI flags | Assigned last. |
3763
+ | 3 | `--config=<path>` | Singular service-owned explicit file. |
3764
+ | 4 | `--env-file*` | Repeatable explicit files; later selected files win. |
3765
+ | 5 | Initial shell environment | Preserved over every file layer. |
3766
+ | 6 | Derived service CLI flags | Assigned last. |
3767
+
3768
+ A working directory's `.env` configures that directory's application, never plurnk (#926); a
3769
+ project's variables reach its commands through the workspace environment ({§workspace-env}).
3718
3770
 
3719
3771
  Node's pre-script env-file form and the executable's post-script form share the same later-file-wins ordering. `--env-file-if-exists` skips an absent file without changing the order of selected files.
3720
3772
 
3721
- §operator-config-env-defaults **Every package owns its knobs — `.env.defaults` is the standard.** Each package in the daemon's ecosystem — internal or third-party — ships a `.env.defaults` at its package root declaring its own knobs; the file is the package's configuration reference, traveling in the tarball and changing with the code that reads it. At boot the daemon assembles every installed member's file into one floor (membership = the `@plurnk/*` scope or a `plurnk` package.json field, gated by `PLURNK_PLUGINS_TRUSTED_ONLY` with discover()'s exact semantics) and applies it set-if-unset under every operator source. `plurnk-service config defaults` renders the same complete, owner-labelled aggregate to stdout on demand, preserving comments and optional declarations without persisting a second copy or exposing effective secret values. A key claimed by two packages fails boot naming both. With the reader-declares discipline, each key has one implementation and one defaults owner.
3773
+ §operator-config-env-defaults **Every package owns its knobs — one assembled floor.**
3774
+
3775
+ | Source | Panel | Admission |
3776
+ |---|---|---|
3777
+ | Platform capability package | `.env.defaults` at the package root | `@plurnk/*` or a `plurnk` package field; {§plugin-trust-boundary} |
3778
+ | Agent Plugin native extension | `ai.plurnk/.env.defaults` | The winning daemon-wide plugin in {§agent-plugins-hosting}, a valid native declaration, and the same trust gate |
3779
+
3780
+ Root and trust flags apply before collection. A project plugin contributes no native panel.
3781
+ Non-module native panels follow their npm-only family discovery ({§plugin-manifest-read});
3782
+ a plain-folder declaration does not suppress an installed capability's panel.
3783
+ Linked packages resolve panels against the same canonical root as native code;
3784
+ an absent panel is optional, but a panel escaping that root is rejected.
3785
+ The file travels with its code and is its configuration reference. All admitted files compose
3786
+ one floor, applied set-if-unset beneath operator sources. `plurnk-service config defaults`
3787
+ renders those same owner-labelled files, preserving comments and optional declarations without
3788
+ persisting another copy or exposing effective values. Duplicate key ownership fails naming both
3789
+ owners. Invalid optional native panels are diagnosed and prevent that extension from loading;
3790
+ they do not block the remaining floor or the repair path ({§configuration-repair-path}).
3722
3791
 
3723
3792
  §operator-config-only-home **The cascading environment is the only home for a choice.** The principle and its reasons are ARCHITECTURE.md's (*Configuration authority*); this is what `scripts/env-surface-policy.mjs` enforces in `root:lint`, over the source Git tracks:
3724
3793
 
@@ -3750,6 +3819,34 @@ or provider request. The seeded `.env`, first-run diagnostic, service help, and
3750
3819
  missing-model recovery all signpost `plurnk-service config defaults` as the
3751
3820
  complete installed option catalog.
3752
3821
 
3822
+ §operator-config-offline-validation **`config check` and runtime use the same
3823
+ owning configuration readers, with different failure boundaries.** An offline
3824
+ check rejects invalid configuration with a nonzero exit. Runtime contains
3825
+ optional-family errors according to {§configuration-repair-path}. Failure
3826
+ names the offending variable or file/entry and retains its cause.
3827
+
3828
+ | Owner | Offline validation |
3829
+ |---|---|
3830
+ | Core | Model selection, file-creation/effect/loop policy, members definitions and controls, skill-fetch settings and root selection |
3831
+ | MCP | Whole definitions from the environment and selected files (cwd is the project for this check), future-alias controls, catalog settings, timeouts, retry pacing and registry URL |
3832
+ | A2A | Whole outbound definitions and controls, timeout/diagnostic bounds, configured inbound exposure |
3833
+ | Schedule | Whole definitions and controls, recurrence syntax, time zone and preview count |
3834
+ | Hooks | Command/argument/event configuration and delivery bounds |
3835
+
3836
+ Disabled definitions and controls without a resource are validated, not skipped.
3837
+ Checking creates no database, starts no process or listener, arms no schedule,
3838
+ and contacts no provider or endpoint. Symbolic credential references remain
3839
+ symbolic: availability belongs to workspace preparation, not offline validation.
3840
+
3841
+ First-run seeding publishes the complete private configuration directory atomically.
3842
+ Concurrent initializers adopt the winning seed; a failed initializer removes only
3843
+ its own staging directory. An existing operator directory is never reseeded.
3844
+
3845
+ §systemd-user-unit The service package ships `plurnk.service` as an example
3846
+ systemd user unit. Installation and enablement are explicit operator actions;
3847
+ package installation performs neither. The template documents executable-path
3848
+ and environment adjustments instead of introducing a service-management command.
3849
+
3753
3850
  Model selection uses one selector vocabulary in `ProviderRegistry` ({§provider-instantiation}). `PLURNK_MODEL_<alias>=<provider>/<model-id>` optionally declares a friendly route and tuning scope; `PLURNK_MODEL=<selector>` selects either that alias or an exact provider/model route. `PLURNK_MODEL_CHILD=<selector>` uses the same vocabulary for the default child provider; unset means inherit the spawning loop's provider. Operator selections and alias declarations live in `.env`, not `.env.defaults`.
3754
3851
 
3755
3852
  Each knob's value lives on its panel and nowhere else (`plurnk-service config defaults` prints them all); this table says what the service's knobs mean.
@@ -3870,7 +3967,7 @@ the policy renders in exactly one packet section. Every other tier runs the
3870
3967
  test cascade, so shipped-default regressions are otherwise invisible by
3871
3968
  construction.
3872
3969
 
3873
- §operator-config-real-model-profile **Real-model gate profile.** `plurnk-core/.env.test` is committed source and is the single shared profile for live, demo, and the candidate daemon used by benchlets. Live/demo load it after operator files; the candidate daemon loads it below its inherited environment. Direct shell/benchmark overrides win in both paths. Its exact allowlist is limited to gate-wide service posture that is identical on every machine: complete catalog orientation, automatic Git membership when the operator ceiling permits Git, ambient operator-file docs/packet notes cleared, ambient MCP selections and schedules disabled, and `PLURNK_EXECS_QUESTION=0` for unattended runs. The ordinary executor switch removes the question tool and its teaching; an explicit override can opt into an attended drill. Configuration with a narrower or variable owner stays outside it:
3970
+ §operator-config-real-model-profile **Real-model gate profile.** `plurnk-core/.env.test` is committed source and is the single shared profile for live, demo, and the candidate daemon used by benchlets. Live/demo load it after operator files; the candidate daemon loads it below its inherited environment. Direct shell/benchmark overrides win in both paths. Its exact allowlist is limited to gate-wide service posture that is identical on every machine: complete catalog orientation, automatic Git membership when the operator ceiling permits Git, ambient operator-file docs/packet notes cleared, the operator's installed skills and plugins left unread ({§agent-roots}), ambient MCP/A2A and schedules default disabled, and `PLURNK_EXECS_QUESTION=0` for unattended runs. The ordinary executor switch removes the question tool and its teaching; an explicit override can opt into an attended drill. The drivers also project per-alias `ENABLED=0` overrides for named MCP, A2A, schedule, membership, and skill resources in the operator's config file. Definitions remain inspectable; explicit shell/benchmark controls win. Mock-tier bootstrap clears these ambient families before loading its fixture floor. No operator file is rewritten. Configuration with a narrower or variable owner stays outside it:
3874
3971
 
3875
3972
  | Owner | Configuration |
3876
3973
  |---|---|
@@ -3953,36 +4050,55 @@ proceeds. `setup` is the readiness boundary for every capability registered
3953
4050
  with Core: recovery may demand a workspace provider before `start`. For a
3954
4051
  pre-bound client interface, requests remain unavailable until `start`; every
3955
4052
  other module opens its module-owned exterior ingress only after recovery. No
3956
- registered capability may depend on exterior ingress. Shutdown begins started
3957
- and self-closing module closure in reverse order and surfaces aggregated close
3958
- failures.
3959
-
3960
- §module-discovery **Third-party daemon-module composition is manifest
3961
- discovery.** A package declares `plurnk: { kind: "module", module:
3962
- "<export-subpath>" }`; the export is one DaemonModule (an object, or a no-arg
3963
- factory returning one). At boot, core scans installed packages under the
3964
- executor family's discovery and trust rules ({§plugin-discovery}) and
3965
- registers every trusted declaring module before any module setup runs, in
3966
- package-name order. The service's explicit composition — the AG-UI,
3967
- hooks, and MCP modules — carries init options and is wired in service.ts;
3968
- discovery never duplicates those packages. An untrusted declaring package is
3969
- skipped with a boot warning, never executed. A module export that is neither
3970
- an object nor a no-arg factory, a factory returning a non-object, or an object
3971
- with a non-function lifecycle member fails boot loudly.
4053
+ registered capability may depend on exterior ingress. A module, and any distinct
4054
+ lifetime object returned by `start`, may implement the following phases:
4055
+
4056
+ | Phase | Obligation |
4057
+ |---|---|
4058
+ | `stop()` | Reject new ingress, stop timers and other producers, and settle owned work that can emit core events. Keep event subscriptions and resources needed for settlement alive. |
4059
+ | `close()` | Unsubscribe observers and release remaining resources after producer settlement; await admitted notification deliveries. Do not start new core work. |
4060
+
4061
+ Both phases are optional and idempotent; repeated calls join the same work.
4062
+ Core tracks a module before `setup` so partially acquired resources are released
4063
+ even if setup fails. A returned object identical to its module is tracked once.
4064
+
4065
+ §module-discovery **Daemon modules compose through their shared lifecycle.**
4066
+
4067
+ | Source | Declaration | Lifetime |
4068
+ |---|---|---|
4069
+ | Platform capability package | `package.json#plurnk` with `kind: "module"` and `module` | Daemon-wide |
4070
+ | Agent Plugin | `plugin.json#extensions.ai.plurnk` with `kind: "module"` and a `module` path under `ai.plurnk/` | Daemon-wide; npm and selected user roots only |
4071
+ | Project Agent Plugin | Portable components only | Workspace-scoped; native code is not imported |
4072
+
4073
+ The export is one DaemonModule object or no-argument factory. Standard bundles follow
4074
+ {§agent-plugins-hosting} source order, then other installed module packages load in package-name
4075
+ order. All trusted modules register before setup. The service's explicit AG-UI, hooks and MCP
4076
+ composition is never duplicated. Untrusted modules are reported and not imported. Invalid
4077
+ declarations, unavailable module files and configuration errors during construction are diagnosed
4078
+ at the affected native extension; healthy siblings remain available. A factory validates startup
4079
+ configuration before `setup` acquires resources. Failures after registration begins follow
4080
+ {§module-lifecycle} cleanup, not a partial-registration fallback.
4081
+ An invalid module object, factory result or lifecycle member is an implementation contract failure,
4082
+ not configuration, and fails loudly. Native capabilities register through their owning public
4083
+ interfaces and release registrations during {§module-lifecycle} resource teardown.
3972
4084
 
3973
4085
  §module-shutdown-order `Daemon.stop()` first rejects new capability demand and
3974
- aborts proposals, branches, derivations, and worker scopes. It simultaneously
3975
- begins every module closer in reverse registration order, allowing exterior
3976
- listeners to stop accepting work while active requests observe those
3977
- cancellations. It then settles branches, drains, module closers, streaming
3978
- producers, derivations, mimetypes, and schemes before its final worker-settlement
3979
- barrier. The supervisor owns each asynchronous cancellation and wake task from
4086
+ aborts proposals, branches, derivations, and worker scopes. It begins module
4087
+ `stop()` calls in reverse registration order without serially awaiting them,
4088
+ so every producer is asked to stop even if another stalls. Core settles drains,
4089
+ capability publications, module producers, streaming producers, derivations,
4090
+ mimetypes, schemes, and the final worker-settlement barrier while observers
4091
+ remain subscribed. Only then does it begin and join module `close()` calls in
4092
+ reverse order. Failures do not skip later phases and join one shutdown aggregate.
4093
+ The supervisor owns each asynchronous cancellation and wake task from
3980
4094
  acceptance through settlement, including immediately acknowledged and explicitly
3981
4095
  awaited cancellation; a task failure participates in the shutdown aggregate.
3982
4096
  After asynchronous selection, the supervisor rechecks shutdown before creating
3983
4097
  a drain or installing a timer; parked-loop wake mutations also recheck worker
3984
4098
  cancellation under {§worker-lifecycle-durable-disposition}.
3985
- The database may be released only after the final settlement barrier resolves.
4099
+ The database remains available through observer closure and final maintenance.
4100
+ The shared deadline bounds every phase, including observer delivery; forced
4101
+ shutdown may therefore lose notifications and reports the unfinished phase.
3986
4102
 
3987
4103
  §crash-only-stop The settle sequence is deadline-bounded
3988
4104
  (`PLURNK_SERVICE_STOP_TIMEOUT_MS`, default 30000): past the deadline each wait
@@ -3996,21 +4112,22 @@ backstop, not the exit.
3996
4112
  ```mermaid
3997
4113
  flowchart LR
3998
4114
  stop[Begin stop] --> abort[Abort core producers]
3999
- stop --> moduleClose[Begin reverse module closure]
4115
+ stop --> moduleStop[Begin reverse module stop]
4000
4116
  abort --> drains[Settle worker drains]
4001
- drains --> joined[Settle module closures]
4002
- moduleClose --> joined
4117
+ drains --> joined[Settle module producers]
4118
+ moduleStop --> joined
4003
4119
  joined --> producers[Settle streaming producers]
4004
4120
  producers --> resources[Dispose derivations,<br/>mimetypes, and schemes]
4005
4121
  resources --> settlement[Settle cancellations and wakes]
4006
- settlement --> database[Release database]
4122
+ settlement --> observers[Close observers and resources]
4123
+ observers --> database[Maintain and release database]
4007
4124
  ```
4008
4125
 
4009
4126
  | Setup function | Contract |
4010
4127
  |---|---|
4011
4128
  | `registerRuntimes([{ decl, executor, availability, scheme? }, ...])` | Validates the complete canonical tag set under {§executor-runtime-declaration}, then publishes every process-wide executor and optional claimed scheme facet atomically. |
4012
4129
  | `registerScheme(name, handler)` | Adds one process-wide addressable scheme handler; scheme readiness and model-facing capability publication remain core-owned. |
4013
- | §module-action-registration `registerModuleAction({ name, scope, inputSchema, outputSchema, handler })` | Adds one non-empty, extension-unique action with resolvable JSON Schemas. `scope` is exactly `worldless`, `workspace`, or `worker`; the handler receives schema-validated params and a separate matching context. Scoped contexts contain trusted bound identifiers, never client parameters. A client-interface module decides whether and how the name becomes public, validates successful output, and owns collisions with its built-ins. |
4130
+ | §module-action-registration `registerModuleAction({ name, scope, residency, inputSchema, outputSchema, handler })` | Adds one non-empty, extension-unique action with resolvable JSON Schemas. `scope` is exactly `worldless`, `workspace`, or `worker`; the handler receives schema-validated params and a separate matching context. `residency` is explicitly `required` or `none`: only the former acquires workspace capabilities and reconciles worker documents. Worldless actions require `none`. Scoped contexts contain trusted bound identifiers, never client parameters. A client-interface module decides whether and how the name becomes public, validates successful output, and owns collisions with its built-ins. |
4014
4131
  | §module-workspace-provider `registerWorkspaceCapabilityProvider(namespaceOwner, provider)` | Registers one extension-unique Functionality provider. `activate({ workspaceId, retain })` reconstructs the workspace snapshot; idempotent `deactivate({ workspaceId })` releases process resources. Core coalesces demand and supplies residency leases for work that outlives its caller. |
4015
4132
  | §module-workspace-state `readWorkspaceModuleState(workspaceId, namespaceOwner)` | Reads one nullable JSON state value per workspace and provider. Core owns storage and lifecycle; the provider owns its schema. Store symbolic credential references, not copied secrets. A worker-scoped family's coordinator reads and replaces the same shape per worker in `worker_module_state` ({§functionality-scope}). |
4016
4133
  | `readWorkspaceEnvironment(workspaceId)` | Captures the workspace env layer ({§workspace-env}) and returns its composer. No argument uses admitted host values; a supplied environment supplies a module's reference-resolution context. Both apply the same captured values and masks, without worker overrides. |
@@ -4060,7 +4177,7 @@ A conflicting alias is rejected explicitly; it never produces a hidden second
4060
4177
  definition for the submitting client or worker.
4061
4178
 
4062
4179
  §module-workspace-residency **Persistence is not residency.** Model execution,
4063
- capability-aware operations, scoped module actions, and retained provider work
4180
+ capability-aware operations, module actions declaring required residency, and retained provider work
4064
4181
  lease the workspace's Functionality. Boot, workspace or worker creation,
4065
4182
  attachment, listing, naming, idle clients, and parked state alone do not.
4066
4183
  After the last lease releases, `PLURNK_SERVICE_WORKSPACE_WARM_MS` and
@@ -4090,10 +4207,10 @@ registry. Deleting the workspace cascades its state; worker lifecycle does not.
4090
4207
  ## Workspace Functionality
4091
4208
 
4092
4209
  §functionality-coordinator **One coordinator owns the common lifecycle.**
4093
- Agent Skills, MCP, outbound A2A agents, and membership are adapters beneath
4210
+ Functionality families are adapters beneath
4094
4211
  `list | discover | add | enable | disable | remove`. State and mutations
4095
- serialize per workspace and family. Client actions
4096
- `workspace.<family>.<verb>` and model manager executors invoke the same
4212
+ serialize per workspace and family. Scope-bound client actions
4213
+ `workspace.<family>.<verb>` / `worker.<family>.<verb>` and model manager executors invoke the same
4097
4214
  coordinator. Families do not invent another management grammar, proposal
4098
4215
  policy, or hotload path.
4099
4216
 
@@ -4101,12 +4218,98 @@ Retryability describes the actual failed condition, not its numeric status.
4101
4218
 
4102
4219
  | Verb | Common contract |
4103
4220
  |---|---|
4104
- | `list` | Project definitions, origin, enabledness, and preparation outcome: disabled, active, unavailable with its Problem, or authorization-required. No credential values. |
4221
+ | `list` | Project definitions, ownership (`origin`), winning configuration source (`provenance`), enabledness, and published preparation outcome: disabled, dormant, active, unavailable with its Problem, or authorization-required. No credential values. |
4105
4222
  | `discover` | Return inert candidates. Never install, persist, enable, or execute them. |
4106
- | `add` | Admit and persist a workspace definition, prepare it, and enable it atomically. It may override the service baseline. Reapplying the same workspace definition enables it idempotently (200); a different definition for that alias fails 409 without replacing it. |
4223
+ | `add` | Admit and persist a local definition, prepare it, and enable it atomically. It may override an inherited definition. Reapplying the same local definition enables it idempotently (200); a different local definition for that alias fails 409 without replacing it. |
4107
4224
  | `enable` | Publish an available definition; retry preparation if unavailable. |
4108
4225
  | `disable` | Withdraw live capability; retain its definition and saved results. |
4109
- | `remove` | Disable and forget the workspace definition. A same-alias service baseline reappears disabled. Service definitions are disable-only. Saved results remain. |
4226
+ | `remove` | Forget the locally owned definition and its enabledness override. Restore any inherited definition with its inherited enabledness. Inherited definitions cannot be removed at this scope. Saved results remain. |
4227
+
4228
+ §configuration-definition-resolution **Named resource definitions replace whole;
4229
+ independent behavior controls remain independent.** Source readers and scope
4230
+ overlays apply the same boundary:
4231
+
4232
+ | Value | Resolution |
4233
+ |---|---|
4234
+ | Named definition | Select the complete definition from the highest-precedence source or scope declaring that alias. Omitted fields, arrays and nested objects never inherit from a lower definition. |
4235
+ | Environment resource declaration | Use {§resource-environment}: absence inherits, an empty definition is invalid, and an explicit enabledness switch disables without erasing the definition. |
4236
+ | Definition validity | Validate the selected definition against its family's schema. Missing required fields are errors, not requests to fill from a lower definition; rejected live changes preserve the previous publication. |
4237
+ | Independently declared behavior control | Resolve its own value through its cascade. An enabledness override does not copy or patch the definition it controls. |
4238
+ | Local definition removal | Remove this scope's definition and enabledness override. Restore the current inherited definition and enabledness, or leave no entry if none exists. Do not persist a replacement or disabling mask; subsequent inherited changes remain effective. Restoration follows the same preparation and publication failure policy as other mutations ({§functionality-publication}). |
4239
+
4240
+ §configuration-provenance **Inspection names the winning definition's input, not
4241
+ its owner or runtime.** `provenance` uses the same `{kind, source, reference?}`
4242
+ shape as discovery candidates. Source readers contribute it; the coordinator
4243
+ preserves it through inheritance, enabledness changes, and every readiness state.
4244
+
4245
+ | Definition source | Inspection |
4246
+ |---|---|
4247
+ | Assembled environment | `kind: environment`, `source`: exact definition key; never its value or an inferred dotenv filename. |
4248
+ | Discovered skill root | `kind: file`, `source`: the winning `SKILL.md` path. |
4249
+ | Standalone MCP file | `kind: file`, `source`: the winning `mcp.json` path; `reference`: its entry's JSON Pointer. |
4250
+ | Local workspace/worker definition or host-provided tree with no configuration input | No fabricated provenance; `origin` identifies ownership and the family definition describes the resource. |
4251
+ | Local override | Replaces inherited provenance with the local definition; removal restores the current inherited provenance. |
4252
+
4253
+ Only the winning definition's source is reported. Shadowed definitions, secrets,
4254
+ and environment-file loading history are not tracked. Source metadata is derived
4255
+ on inspection, not persisted in the local overlay or used as runtime identity.
4256
+
4257
+ §configuration-repair-path **Invalid optional configuration cannot remove the
4258
+ agent's repair environment.** Capability owners reject typed operator input
4259
+ errors. The launcher and shared coordinator contain them at their respective
4260
+ composition boundaries, not arbitrary exceptions:
4261
+
4262
+ | Boundary | Outcome |
4263
+ |---|---|
4264
+ | Optional startup integration (hooks, hosted A2A, observability) cannot be configured | Withhold that integration and retain its exact configuration diagnostic. Activate the client interface and unrelated capabilities; never invent a replacement setting. |
4265
+ | Invalid default model or child selector | Retain its diagnostic at startup; keep client inspection and explicit selection available. A request relying on the invalid selector fails with `daemon:configuration/configuration-invalid` (503), naming its key. Never substitute another model or silently inherit a child model. |
4266
+ | Model construction or endpoint verification fails | Reject selection/use before inference or committing the selection. Keep the client available; a later selection can retry or choose another route. |
4267
+ | Invalid effect, file-creation, or loop-default policy | Retain the startup diagnostic. Reject the affected operation or unresolved loop policy as `daemon:configuration/configuration-invalid` (503); no guessed admission policy, external effect, or orphan approval wait. Independent operations and explicitly supplied valid loop policies remain usable. |
4268
+ | Invalid retention configuration | Withhold collection and automatic storage conversion, including shutdown collection. Preserve stored evidence and expose the diagnostic; do not substitute a deletion policy. |
4269
+ | Invalid packet configuration or retired capacity knobs | Retain the startup diagnostic and keep client inspection available. Reject affected packet construction before inference with `daemon:configuration/configuration-invalid` (503). Never guess a capacity or projection setting. |
4270
+ | Invalid executor construction/probe configuration | Keep the installed declaration and its diagnostic, but no executable instance or output scheme. Other executors and ordinary READ/EDIT remain usable. Invoking the unavailable tag returns its exact 503 configuration Problem. |
4271
+ | Invalid execution scheduling, input, or scratch configuration | Diagnose at startup and validate on the affected execution path before admission, stream creation, or external effects. Scratch configuration applies only to resource-backed execution; inline programs remain independent. Never substitute concurrency, timeout, or directory settings. |
4272
+ | Invalid model-alias catalog | Fail catalog inspection explicitly; omit its unavailable snapshot rather than report an empty catalog. Exact provider/model selection does not depend on aliases. |
4273
+ | Offline `config check` | Validate the same inputs without activating integrations; an invalid setting remains a nonzero failure. |
4274
+ | Family configuration cannot be resolved | Keep its manager available, identify the configuration failure in its generated documentation, and preserve durable definitions. Do not publish the family's operational capabilities or pretend its catalog is empty. Other families and ordinary model work remain usable. |
4275
+ | Inspection or mutation of an unresolved family | Return the exact configuration Problem, naming the key and required correction, through both client and model paths. No silent source fallback or change to stored settings. |
4276
+ | Invalid live mutation | Reject atomically and preserve the preceding publication. |
4277
+ | Previously valid family becomes invalid | Withdraw its operational capabilities at normal publication, then release the old snapshot. Keep the manager and diagnostic. |
4278
+ | Configuration source is corrected or removed | Inspection resolves the current source immediately, even in the repairing turn; a previous source-resolution error cannot override a successful read. Not-yet-published definitions remain dormant. Normal publication restores capabilities; inspection does not activate them. |
4279
+ | Preparation configuration is corrected | The published preparation failure remains until ordinary preparation succeeds. Successful source inspection alone does not establish runtime readiness. Environment-file edits follow their ordinary process lifetime, not an implicit reload. |
4280
+ | Internal invariant, state, or implementation failure | Preserve the exception; never reclassify it as an operator configuration error. |
4281
+
4282
+ Client discovery and passive synchronization report startup diagnostics through the
4283
+ existing Notice channel, even without a usable model. The first turn of a drain, and a changed diagnostic
4284
+ thereafter, reports unresolved configuration to both client and model. An
4285
+ unchanged diagnostic is not repeated every turn. Operation failures remain Problems.
4286
+
4287
+ §functionality-inspection **Inspection is not demand.** `list` and `discover` do not
4288
+ acquire residency, join preparation, reconcile worker documents, or extend warm
4289
+ retention. An enabled definition no resident publication has prepared is `dormant`: every one while
4290
+ the family is cold, and one that arrived or changed out of band until the next turn publishes it
4291
+ ({§functionality-hotload}). During replacement the preceding publication remains authoritative;
4292
+ the candidate is never presented as active. A published outcome belongs to the
4293
+ complete definition prepared, not merely its alias. Cooling leaves durable definitions
4294
+ inspectable. Mutations and protocol continuations retain their residency rules.
4295
+
4296
+ §functionality-preparation-visibility **Preparation is workspace activity, not
4297
+ model context.** The coordinator owns a current `FunctionalityPreparationActivity`
4298
+ per preparing family. `workspacePreparationStatus(workspaceId)` returns that same
4299
+ state without demand; `workspace/preparation` broadcasts
4300
+ `{ workspaceId, preparation: [...] }` whenever it changes.
4301
+
4302
+ | Boundary | Visible state |
4303
+ |---|---|
4304
+ | Preparation begins | Family, `phase: preparing`, `alias: null`, UTC `since` |
4305
+ | Adapter calls `progress(alias)` | Enabled alias being prepared; a fresh `since` |
4306
+ | Prepared candidate enters publication | `phase: publishing`, `alias: null` |
4307
+ | Commit, rejection, or rollback settles | Family removed; empty array means no preparation |
4308
+
4309
+ Preparation reports neither definitions nor credentials, does not alter the
4310
+ publication contract, and creates no log entries or model Notices. Published
4311
+ failures retain their exact Problems in `list`. Concurrent consumers share the
4312
+ workspace activity; a client disconnect does not clear another consumer's work.
4110
4313
 
4111
4314
  §functionality-adapter **An adapter owns protocol truth.** It declares its
4112
4315
  family, namespace owner, definition schema, contributed defaults, discovery,
@@ -4116,16 +4319,66 @@ family declares, at admission, in the service projection, and on persisted
4116
4319
  state, so an environment variable's name is an alias exactly as a skill name
4117
4320
  is. Admission distinguishes explicit client
4118
4321
  actions from model operations where the family contract requires it
4119
- ({§members-model-scope}). Preparation returns runtimes, documents, per-alias
4322
+ ({§members-model-scope}). Preparation receives each complete definition with optional
4323
+ adapter-owned interpretation context. Context is source semantics, not policy or provenance;
4324
+ it participates in runtime identity and hot-load comparisons, is never projected as configuration
4325
+ or persisted into a workspace override, and cannot survive replacement by a local definition.
4326
+ Removing that override restores the current inherited definition and context together.
4327
+ Descriptive provenance alone does not change runtime identity. Preparation returns runtimes, documents, per-alias
4120
4328
  outcomes, and a snapshot with `commit`/`abort`. Successful publication commits;
4121
4329
  failure aborts; cooling tears down. Protocol continuations remain ordinary
4122
4330
  module actions. Optional `forget` releases an installed or provisioned
4123
- definition before removal; failure rejects removal ({§skills-remove}). The
4331
+ definition before removal; failure rejects removal. The
4124
4332
  seam's shapes — the identity a verb acts under, its options, definition
4125
4333
  sources, outcomes, preparation, the prepared result and the family handle —
4126
4334
  are declared once in `plurnk-contracts` and imported by core and every
4127
4335
  module; core adds only its own face of the seam, the runtime registration a
4128
4336
  resident family prepares and the scheme facet it may expose.
4337
+ An adapter may expose current partial-source `configurationNotices`; these join the ordinary
4338
+ workspace diagnostics without preventing independently valid definitions from preparing.
4339
+
4340
+ §functionality-hotload **Out-of-band state is admitted before the next turn.** An adapter whose
4341
+ `available` reads state that changes outside the daemon, such as skill roots ({§skills-hotload}) or
4342
+ installed plugins ({§agent-plugins-hosting}), implements `refreshIfChanged`. Turn admission calls it
4343
+ for every family under the workspace gate before packet assembly. The family handle's `refresh` with
4344
+ `ifChanged` republishes a resident family only when the enabled definitions it would prepare differ
4345
+ from the ones its publication prepared. The coordinator makes that comparison because it alone knows
4346
+ what it published, so a change `list` saw first is still published at the next turn. A family whose
4347
+ definitions do not capture its published content, such as a skill's files, republishes
4348
+ unconditionally when that content changed. An unchanged family dispatches nothing.
4349
+
4350
+ §agent-plugins-hosting **Installed Agent Plugins are found like skills.** A workspace's plugins are
4351
+ the immediate child directories of its project's `.agents/plugins`, then
4352
+ `$XDG_CONFIG_HOME/plurnk/plugins` (plurnk alone), then `~/.agents/plugins` (every agent), loaded and
4353
+ validated by `@plurnk/plurnk-agent-plugins` ({§agent-plugins-roots}), followed by standard plugin
4354
+ bundles in the installed npm graph. An earlier source shadows a later plugin of the same manifest
4355
+ name, regardless of distribution or directory name. Native discovery uses that same cascade with
4356
+ the project root omitted; workspace discovery includes it. A plugin's `PLUGIN_DATA` is
4357
+ `$XDG_DATA_HOME/plurnk/plugins/<name>/<sha256(canonical-root)>`, kept across in-place updates and
4358
+ moved with a state root ({§state-root}). Distinct installations never share data by name alone;
4359
+ workspaces referencing the same canonical installation share its data. Modules receive a workspace's plugins,
4360
+ in precedence order, through the setup seam's `readWorkspacePlugins`, with one signature that changes
4361
+ exactly when a plugin, its manifest, its MCP configuration, or its skills change
4362
+ ({§functionality-hotload}), and the roots the workspace has. This read-only source loader does
4363
+ not install or delete plugins. MCP's own lifecycle is independent ({§mcp-definitions}).
4364
+
4365
+ Portable components use each family's existing management and publication path. Within a source
4366
+ scope, standalone definitions precede bundled components; nearer scopes precede farther scopes,
4367
+ with npm last. Complete environment definitions override those inputs, followed by workspace
4368
+ definitions and enabledness. Inspection names the winning component file as plugin provenance.
4369
+ Removing a workspace override restores inheritance; it never deletes the installed bundle.
4370
+ Current plugin-source diagnostics join the workspace's configuration notices before inference.
4371
+
4372
+ §agent-roots **A daemon reads the roots `PLURNK_SERVICE_ROOTS` names.** A comma list drawn from
4373
+ `project`, `plurnk` and `global`, nearest first, selects which Agent Skills, Agent Plugins and standalone MCP roots a
4374
+ daemon reads; the default names all three. The real-model gate profile selects
4375
+ its discovery roots explicitly ({§operator-config-real-model-profile}), so
4376
+ operator roots do not shape a gate. Root selection does not restrict explicit
4377
+ source definitions. Skills mutations change workspace bindings, not these roots
4378
+ ({§skills-functionality}); MCP mutations likewise remain workspace-owned. The module setup seam's
4379
+ `workspaceConfigurationDirectories` supplies selected `<project>/.agents`,
4380
+ `$XDG_CONFIG_HOME/plurnk`, and `~/.agents` directories in precedence order, omitting
4381
+ project when no project is bound. Modules own their file formats; core owns discovery roots.
4129
4382
 
4130
4383
  An adapter may expose a `scheme` facet beneath its family's runtime namespace
4131
4384
  ({§runtime-resource-binding}). A facet claims a path subtree and is the scheme's
@@ -4145,9 +4398,10 @@ operator's ceiling ({§exec-env-scoped}, service origin) precede workspace defau
4145
4398
  worker overrides ({§workspace-env}). `add` takes
4146
4399
  the name as the alias and `{ "value": "…" }` as the definition, used verbatim with no
4147
4400
  interpolation. `disable` withholds a name in the selected scope while retaining it;
4148
- `remove` forgets a locally-owned entry and a same-name lower baseline reappears
4149
- disabled, so removal never silently changes what the next spawn sees. Definitions
4150
- from a lower layer are disable-only in the current scope.
4401
+ `remove` forgets a locally-owned entry and restores any same-name inherited value
4402
+ and enabledness ({§configuration-definition-resolution}). Use `disable` to keep
4403
+ an inherited name out of subsequent launches. Definitions from a lower layer
4404
+ cannot be removed in the current scope.
4151
4405
 
4152
4406
  `list` projects effective values with their origin. Values are shown: the ceiling is the security
4153
4407
  boundary, not the projection, and any admitted name is already readable by every command the
@@ -4550,12 +4804,14 @@ adding a loop to it. LOOK text anchors resolve through the same
4550
4804
 
4551
4805
  | Event | Payload | When fired |
4552
4806
  |--------------------------------------------------------------|---------|------------|
4807
+ | §notifications-operation-event `operation/event` | `ApplicationOperationEvent`: `{ workerId, loopId, turnId, sequence, origin, projectRoot, statement, phase, result? }` | One admitted dispatch starts before capability/proposal admission and settles after its durable receipt(s). Only the settled phase has `result`, the exact returned operation result. READ fan-out is one dispatch; BARE starts before prompt preparation and settles after ordered receipt persistence. Automatic stream observations and log writes are not dispatches. An asynchronous executor's successful dispatch does not assert process exit. An internal exception that prevents settlement has no invented result event. |
4553
4808
  | §notifications-log-entry-notify `log/entry` | `{ entry: LogEntry }` | A non-proposed `log_entries` row is committed, or a proposed row reaches terminal settlement under {§proposal-proposed-hidden}. Delivery completes before a later event may terminate the owning Loop. |
4554
4809
  | §notifications-loop-terminated `loop/terminated` | `{ workerId, loopId, result, hitMaxTurns, turnIds, usage: { accounting, curationWeight, curationBudget, contextTokens, contextCapacity, meta }, attributions }` | One loop reaches a terminal state. `result` is the exact universal operation result, including its RFC 9457 Problem Details on failure. `accounting` is the loop's contracts-owned {§provider-accounting}; the two curation facts and two physical-context facts follow {§tokenomics-client-gauge}; `meta` is that turn's opaque provider bag. `attributions` is the sorted union of exact provider-request evidence ({§attribution}), separate from accounting. Worker and loop are an inseparable owning coordinate. |
4555
4810
  | §notifications-loop-packet `loop/packet` | `{ workerId, loopId, packetCount }` | One provider packet becomes durable. `packetCount` is the exact count of packet-bearing turns in that Loop; packetless producer turns and physical provider retries never contribute. |
4556
4811
  | §notifications-loop-proposal `loop/proposal` | contracts-owned `ProposalProjection` | Dispatch pauses on a durable 202 proposal. `disposition` is the sole authority for whether a client presents review UI; live and reconnect share {§proposal-projection}. |
4557
4812
  | §notifications-loop-interaction `loop/interaction` | contracts-owned `ClientInteractionProjection` | An operation is paused on client input. Live delivery and reconnect discovery share {§client-interactions}; workspace scope remains the event envelope. |
4558
4813
  | §notifications-workspace-created `workspace/created` | `{ id, name, projectRoot }` | A workspace is created. This is the only current global event. |
4814
+ | `workspace/preparation` | `{ workspaceId, preparation: FunctionalityPreparationActivity[] }` | Workspace capability preparation changes; snapshot and clearing semantics follow {§functionality-preparation-visibility}. |
4559
4815
  | §notifications-stream-event-on-channel-change `stream/event` | `{ entryId, workerId, target, channel, state, contentLength, mimetype?, loop_seq?, turn_seq?, sequence? }` | Channel content grows or channel state transitions. `workerId` is the initiating actor used for conversation routing, never entry ownership or access control. `target` is the canonical resource URI. Optional numeric coordinates identify the causal log item, independently of that URI. Core-managed channel writes include the current stored `mimetype`, which may change per call ({§channel-mimetype}); the generic plugin notification capability does not require it. It carries metadata, not content; consumers read bytes by canonical workspace address. |
4560
4816
  | §notifications-stream-concluded `stream/concluded` | `{ entryId, workerId, target, subscriptionId, scheme, result, summary, wakeAction, loop_seq?, turn_seq?, sequence? }` | A subscription closes. `workerId` identifies the initiating actor; `target` is the canonical resource URI. Optional numeric fields identify the causal log item, never parsed from `target`. Exact result truth is preserved. `wakeAction` reports `wake-pending` before settlement, `no-op-active-loop` when work is already executing, `no-loop`, or `skipped-aborted`/`skipped-cancelled` for an aborted worker scope. A pending wake predicts neither execution nor recipient count; subsequent ordinary loop events report actual progress and completion. |
4561
4817
  | §notifications-notice-event `notice/event` | `{ workerId, loopId, notice: Notice }` | A transient observation or progress notice occurs. `workerId` owns loop activity; only workspace derivation progress uses `null` with `loopId=0`. It cannot alter durable history, scheduling, recovery, or model-visible failure truth. |
@@ -4618,16 +4874,17 @@ The packet reaches the provider as a transcript, under the roles the model was t
4618
4874
  | Message | Role | Content |
4619
4875
  |:--|:--|:--|
4620
4876
  | 1 | `system` | the system slot, as rendered |
4621
- | 2, 4, … | `user` | the user slot's text up to and including the next placed emission row's record ({§emission-row}); the first opens with `## Log` |
4622
- | 3, 5, … | `assistant` | that row's emission, as the worker's own message |
4623
- | last | `user` | the records after the last placed emission, then the remaining user sections in {§packet-cache-monotone} order; native parts ride here ({§packet-attachment-parts}) |
4877
+ | 2, 4, … | `user` | the user slot's text up to and including the record of the next placed emission that delivers text ({§emission-row}); the first opens with `## Log` |
4878
+ | 3, 5, … | `assistant` | that row's frozen emission projection less its NOTE and WAIT blocks, as the worker's own message ({§emission-row}) |
4879
+ | last | `user` | the records after the last emission delivered, then the remaining user sections in {§packet-cache-monotone} order; native parts ride here ({§packet-attachment-parts}) |
4624
4880
 
4625
4881
  Only role boundaries are added: joined by blank lines, the user messages are the user slot's
4626
4882
  bytes, in record order. An emission is placed exactly when its row is present in the final log
4627
4883
  section, so curation governs the transcript: a KILLed emission row, or one a trusted transform
4628
4884
  removed ({§packet-plugin-transform}), takes its emission with it, and a log without emission rows
4629
- is one user message. The Worker block and the status clump always follow the log, so a request
4630
- never ends on an emission, and the projection refuses one that would. The digest's packet
4885
+ is one user message. An emission of only NOTE and WAIT delivers nothing, so its record runs on
4886
+ into the next user message. The Worker block and the status clump always follow the log, so a
4887
+ request never ends on an emission, and the projection refuses one that would. The digest's packet
4631
4888
  artifacts record the sections, and `.wire.json` the messages ({§share-packet-names}).
4632
4889
 
4633
4890
  ### §packet-cache-monotone Default order and cache locality
@@ -4936,7 +5193,7 @@ source independently from this optional model-exchange record; a request-only
4936
5193
  turn receives a note instead of a fabricated response.
4937
5194
 
4938
5195
  §digest-turn-artifact-identity **Digest packet artifacts project durable turns.**
4939
- After selectors are applied, digest retains every turn with exact program source, a
5196
+ After selectors are applied, digest retains every turn with exact content or reasoning source, a
4940
5197
  valid stored provider request, or malformed stored packet evidence; orders those
4941
5198
  turns by durable chronology; and names each by its log coordinate ({§share-packet-names}). The
4942
5199
  producer does not affect projection.
@@ -4951,6 +5208,7 @@ consumer reconstructs a name. A name that cannot be a file name, or two turns sh
4951
5208
  | Artifact | Present when | Authority |
4952
5209
  |----------|--------------|-----------|
4953
5210
  | `<stem>.assistant.md` | The turn has an `ops` source | Exact `turn_sources.content`, independent of log rows |
5211
+ | `<stem>.reasoning.md` | The turn has a `reasoning` source | Exact `turn_sources.content`, without relabeling it as content |
4954
5212
  | `<stem>.system.md`, `<stem>.user.md` | The turn stored a provider request | Stored text sections projected through `PacketWire`; native parts are not Markdown |
4955
5213
  | `<stem>.wire.json` | The turn stored a provider request | The request's text messages in order, its worker's emission rows placed ({§packet-wire-envelope}); `<stem>.wire.invalid.json` names a stored log that cannot be projected |
4956
5214
  | `digest.json` turn `attachments` | Every turn | Stored native attachment descriptors; `[]` means a request without attachments, `null` means no valid stored request. Selection is not proof of provider acceptance. |
@@ -4959,8 +5217,8 @@ consumer reconstructs a name. A name that cannot be a file name, or two turns sh
4959
5217
  | `<stem>.packet.raw.txt` | The stored packet fails typed validation | Exact stored packet text |
4960
5218
  | `<stem>.packet.invalid.json` | The stored packet fails typed validation | Turn identity and complete validation error chain |
4961
5219
 
4962
- A source-backed turn without provider participation therefore produces only
4963
- `assistant.md`; a request-only turn produces no fabricated assistant. A
5220
+ A source-backed turn without provider participation produces only its source-channel
5221
+ artifacts; a request-only turn produces no fabricated assistant. A
4964
5222
  source-less programmatic turn with no provider request has no forensic payload
4965
5223
  to project and writes no files.
4966
5224
 
@@ -5074,7 +5332,7 @@ ordered set exactly once. Recovery retries complete the same queued loop and nev
5074
5332
  mint duplicate work. Output withholding preserves readable arrival rows; explicit
5075
5333
  KILL follows the ordinary log contract.
5076
5334
 
5077
- §completion-defers-to-messages **Conclusion does not cross an unanswered arrival.** The end-of-program check includes messages that arrived during inference. The final database transition rechecks unanswered messages atomically. An arrival that wins the race continues the current loop; one admitted after conclusion belongs to a new loop. Orphan recovery preserves messages accepted before an independently forced termination.
5335
+ §completion-defers-to-messages **Conclusion does not cross an unobserved arrival.** The end-of-program check includes messages that arrived during inference. The final database transition atomically requires every inbox message to be published and the loop's observed wake revision to equal its worker's current revision. Answer delivery is not this barrier. An arrival that wins the race continues the current loop; one admitted after conclusion belongs to a new loop. Orphan recovery preserves messages accepted before an independently forced termination.
5078
5336
 
5079
5337
  §packet-catalog **Catalogs are query results, not packet state.** The packet
5080
5338
  stores no materialized manifest. Complete and one-level entry directories,
@@ -5172,7 +5430,21 @@ retain distinct contracts and lifetimes.
5172
5430
 
5173
5431
  §digest-wire-line **Wire health aggregated.** Each worker summary renders a `Wire:` line — total physical provider requests, error-outcome count, and the error percentage when nonzero. Provider-level failures are absorbed by retries below the packet stream, so without this aggregate a rate-limit storm is invisible in every summary while the model's experience stays clean.
5174
5432
 
5175
- §digest-cache-ledger **Cacheable versus cached, per request.** For every physical provider request the digest computes its cacheable prefix: the longest common prefix, in characters, between the prompt its turn stored (the request's messages in order, each role above its content — the messages `.wire.json` carries) and the prompt of the previous provider request in the same loop, taken as that prefix's share of the whole prompt in the packet's own token estimate ({§tokenomics-agnostic-ruler}) and applied to the provider's reported input count, so `cacheableTokens` sits in the same units as the cache read beside it; a loop's first request has 0, and a request whose turn stores no valid packet or whose provider reported no input count has none. Beside it sit the provider's reported cache read as `cachedTokens` (`provider_requests.usage_input_cache_read`) and `inputTokens` (`usage_input`). `digest.json` carries the three on every provider-request row; an inference turn line carries `cache=<cached>/<cacheable>` summed over the turn's requests; each workspace heading is followed by `Cache: <cached> of <cacheable> cacheable tokens reported (<pct>%) over <n> requests`. A provider that reported no cache field at all (null, not 0) renders `?` on the turn line and is counted apart on the workspace line (`· <k> unreported (cached=?)`), outside both sums and the percentage; a request without a stored packet or without a reported input count is likewise counted apart. The prefix measures what the daemon kept identical between consecutive requests; it is no claim about the provider's tokenization or cache-block alignment, so a provider that under-caches an identical prefix reads as such, apart from a prefix the daemon itself broke.
5433
+ §digest-cache-ledger **Measured cache reuse and estimated prompt overlap are separate.**
5434
+
5435
+ | Projection | Meaning |
5436
+ |---|---|
5437
+ | `digest.json` provider-request `cachedTokens`, `inputTokens` | Exact provider-reported cache reads and input tokens; absent quantities remain `null`, reported zero remains zero. Every physical request counts, including retries and first requests of new loops. Stored packet availability is irrelevant to these counters. |
5438
+ | Turn `cache=<cached>/<input>` | Sum each measured quantity over that turn's requests. If any request omits a quantity, that sum is `?`. |
5439
+ | Workspace `Cache: <cached> of <input> reported input tokens read from cache (<pct>%) over <n> requests` | Sum only requests reporting both counters. Percentage is cache reads / input, rounded to one decimal; zero input is `n/a`. Requests missing either counter are counted separately as `missing input or cache usage (excluded)`. |
5440
+ | `digest.json` provider-request `adjacentPrefixTokensEstimate` | Optional loop-local diagnostic: the longest common character prefix with the preceding request, weighted under {§tokenomics-agnostic-ruler} as a share of the current stored prompt, multiplied by reported input tokens. First request: `0`; missing current/preceding packet or current input: `null`. Empty prompts have zero overlap. |
5441
+
5442
+ The prefix estimate uses the stored emission packet's wire message order, roles
5443
+ and content. A BARE request's input is not that packet; its prefix estimate and
5444
+ the following request's comparison are unknown. The estimate is
5445
+ neither provider tokenization nor a cache ceiling, and never supplies a cache-ratio
5446
+ denominator. Caching across loops or against other provider-resident prefixes
5447
+ does not make the measured counters inconsistent.
5176
5448
 
5177
5449
  §digest-edit-census **Every model EDIT by the form it authored, how it landed, and whether it came back.** For each worker the digest reads every model-authored EDIT row and classifies the form from the row's stored marker and the durable statement's pattern: `hash` (one anchor), `line` (one line number), `range` (two marks), `insert` (the zero-width `<L,1,L,1>` form, {§zero-width-column-one-insert}), `column` (any other four-mark region), `prepend` / `append` (`<0>` / `<-1>`), `offset` (a tolerated anchor offset, {§anchor-offset}), `pattern` (a selection matcher), `whole` (no marker: a creation when it lands 201). It counts the EDITs, those refused (status ≥ 400), and the *revisits*: an EDIT of a path the same worker had edited within its previous two model turns — the shape of a repair without the claim of one. Each worker summary renders `EDITs: <n> · <form>=<count>… · refused=<k> · revisits=<r>` (`(no edits)` for none); `digest.json` carries the census as `edit_census` on every worker and stamps every EDIT log entry with its `edit_form` and `edit_revisit`. A form is a fact about what was written, never about intent; the bench sheet reads the counts as friction and leaves the judgement to the reader.
5178
5450
 
@@ -5293,8 +5565,10 @@ enable | disable | remove`, `workspace.members.<verb>` for the client,
5293
5565
  ```` ```members (<verb>) ```` for the model — for what the model may see, exactly as they do for skills and
5294
5566
  MCP servers. A definition is one gitignore-style glob, `{ glob }`, relative to the project
5295
5567
  root; a leading `!` excludes matching members, and an exclusion wins over every inclusion.
5296
- The coordinator's provenance (`service-configuration`, `client-action`, `model-proposal`)
5297
- rides the definition; its alias is a short name, suggested from the glob. `list` shows each
5568
+ Admission provenance (`service-configuration`, `client-action`, `model-proposal`)
5569
+ rides the definition to enforce {§members-model-scope}; configuration-source
5570
+ provenance belongs to the shared inspection projection ({§configuration-provenance}).
5571
+ The alias is a short name, suggested from the glob. `list` shows each
5298
5572
  definition with what it resolved to — `include` or `exclude`, the pattern, the members it
5299
5573
  admits or removes (count and a bounded sample), and for a model's inclusion the matches the
5300
5574
  repository's ignore rules refused — so the model sees what its glob did and adapts.
@@ -5303,10 +5577,12 @@ repository's ignore rules refused — so the model sees what its glob did and ad
5303
5577
  untracked, absent); a glob previews what `add` would include or exclude. Names only, never
5304
5578
  content; nothing is added.
5305
5579
 
5306
- §members-configuration *Available definitions.* The operator's `PLURNK_MEMBERS_<ALIAS>=<glob>`
5307
- (`!glob` excludes) and `PLURNK_MEMBERS_ENABLED=[…]` (`[]` enables none) are the
5308
- service-origin definitions, the shape `PLURNK_MCP_*` already has; an empty glob, a bare `!`,
5309
- or an unknown enabled alias fails the daemon at boot.
5580
+ §members-configuration *Available definitions.* `PLURNK_MEMBERS_<alias>=<glob>`
5581
+ declares one service-origin rule (`!glob` excludes). The shared naming and
5582
+ enabledness dialect is {§resource-environment}; `PLURNK_MEMBERS_ENABLED` supplies
5583
+ the panel default and `PLURNK_MEMBERS_<alias>_ENABLED` overrides one rule.
5584
+ An empty glob or a bare `!` fails validation. Controls may precede their rule;
5585
+ they are validated without creating a definition ({§resource-environment}).
5310
5586
 
5311
5587
  §members-model-scope *The model's authority.* A model's `add` is admitted against
5312
5588
  `PLURNK_SERVICE_MEMBERS_MODEL_SCOPE` in the file-creation lattice `none < root <
@@ -5338,53 +5614,98 @@ verbs are these verbs.
5338
5614
  §skills-functionality **Agent Skills are one workspace Functionality family.**
5339
5615
  Core registers the `skills` family with the coordinator ({§functionality-coordinator});
5340
5616
  its adapter owns protocol truth for standard Agent Skills and nothing else. A
5341
- definition is `SkillDefinition` — the standard skill `name`, its source
5342
- `scope` (`project` = `<projectRoot>/.agents/skills`, `global` =
5343
- `~/.agents/skills`, `service` = a host-provided resource tree), and for a workspace-installed skill the standard installer
5344
- `source` that provides it. Plurnk seeds no universal root and mutates none
5345
- absent an explicit `add`/`remove`.
5617
+ definition is `SkillDefinition`: the standard skill `name`, its `source`, and
5618
+ optional Git `ref`/resolved `commit` ({§skills-sources}). Only a host-provided
5619
+ resource tree omits `source`. Source location is not mutation ownership: standard
5620
+ project/global roots are read-only configuration inputs; live changes belong to
5621
+ the workspace. Plurnk neither installs into nor deletes from those roots.
5346
5622
 
5347
5623
  *Available definitions.* The filesystem is the only truth about installation:
5348
- every `<root>/<name>/SKILL.md` directory under the project then the global
5349
- root is one service-origin definition, enabled by default, project shadowing
5350
- global and then host-provided trees by name; when the standard installer's `skills-lock.json` records a
5351
- source it rides the definition. The workspace's durable state owns enablement
5352
- ({§functionality-state}); a disabled skill stays client-visible and leaves no
5353
- model-facing trace.
5354
-
5355
- *Discovery is inert.* `discover {query}` searches the ecosystem registry
5356
- (`PLURNK_SERVICE_SKILLS_REGISTRY_URL`; empty disables it
5357
- with 501 `registry-not-configured`) and returns one candidate per hit with
5358
- `registry` provenance and the exact `owner/repo` source. `discover {source}`
5359
- lists the skills one standard package reference contains with `source`
5360
- provenance. Neither installs, persists, or enables. Client `configuration`
5361
- contributes nothing and is refused with 400.
5362
-
5363
- *Admission.* `add {alias, definition}` requires `alias = name`, a `source`,
5364
- and a project root when `scope` is `project`; the workspace definition may
5365
- shadow a service skill of the same name. The family's aliases use the standard
5366
- skill-name grammar ({§agent-skills-name}), including digit-leading and Unicode
5367
- names, rather than the coordinator's generic default.
5624
+ every `<root>/<name>/SKILL.md` directory under the project, plurnk, then global
5625
+ root is one service-origin definition, a nearer root shadowing a farther one and
5626
+ all of them shadowing host-provided trees by name. Each filesystem definition
5627
+ names its actual source directory. The workspace's durable state owns
5628
+ enablement ({§functionality-state}); a disabled skill stays client-visible and
5629
+ leaves no model-facing trace.
5630
+
5631
+ §skills-configuration **Skills use the shared definition cascade.**
5632
+
5633
+ | Layer, low to high | Definition source |
5634
+ |---|---|
5635
+ | Service | Host-provided trees |
5636
+ | Standard locations | Global, plurnk, then project roots selected by {§agent-roots} |
5637
+ | Cascading environment | `PLURNK_SKILLS_<name>={"name":"<name>","source":"…","ref"?:"…"}` replaces the complete definition |
5638
+ | Live workspace | `skills (add)` creates a workspace definition through {§functionality-coordinator} |
5639
+
5640
+ `PLURNK_SKILLS_ENABLED` supplies default enabledness; `<name>_ENABLED` overrides
5641
+ it independently ({§resource-environment}). The decoded environment alias must
5642
+ equal the standard skill name, including digit-leading and Unicode names.
5643
+ Blank definitions are invalid; controls may precede a definition. Environment
5644
+ validation checks shape, names, remote URL rules, and Git-only `ref` without
5645
+ fetching or opening a source; `commit` is service-recorded, not an input.
5646
+
5647
+ *Discovery is inert.* `discover {source}` lists the standard skills one source
5648
+ carries, each a candidate with `source` provenance and the exact definition to
5649
+ add; it never installs, persists, or enables. Agent Skills have no standard
5650
+ registry, so `discover {query}` is 400 `query-unsupported`, naming the source
5651
+ forms. Client `configuration` contributes nothing and is refused with 400.
5652
+
5653
+ *Admission.* `add {alias, definition}` requires `alias = name` and a `source`;
5654
+ the workspace definition may shadow a service skill of the same name. Relative
5655
+ sources require a project root; absolute sources work in headless workspaces.
5656
+ A local source is recorded as its absolute path.
5657
+ A git source records the `commit` its `ref` names at admission, or its default
5658
+ branch's when no `ref` is given; `ref` belongs to git sources, and a supplied
5659
+ `commit` is refused because the service records it. The family's aliases use
5660
+ the standard skill-name grammar ({§agent-skills-name}), including digit-leading
5661
+ and Unicode names, rather than the coordinator's generic default.
5368
5662
 
5369
5663
  *Preparation.* For each enabled alias the adapter selects the host-provided
5370
- tree for `service` scope or locates the directory at the filesystem scope;
5371
- a workspace definition whose directory is absent is installed
5372
- through the standard CLI (`PLURNK_SERVICE_SKILLS_CLI`, invoked as
5373
- `<cli> add <source> --agent universal --skill <name> --yes [--global]`, run with
5374
- `HOME` set to the service's user home so the installer's `~` is the global
5375
- root) and the installed `SKILL.md` — never the installer's output — is the
5376
- evidence.
5664
+ tree or resolves the complete source definition ({§skills-sources}). Local
5665
+ folders and their `SKILL.md` files remain live references, including supporting
5666
+ resources and symlink retargeting. Git/archive sources are materialized only
5667
+ inside {§module-workspace-directory}; different workspaces and complete source
5668
+ definitions cannot reuse each other's materializations accidentally.
5669
+ The first fetched Git/archive copy remains stable across enable, cooling, and
5670
+ restart until the complete source definition changes. In particular, an
5671
+ operator-configured symbolic Git ref is not an implicit update subscription.
5377
5672
  Each admitted skill requires standard `name` and `description` frontmatter
5378
5673
  with `name` matching its directory. A missing, uninstallable, or invalid skill
5379
- is `unavailable` with its exact Problem (`skill-missing`, `install-failed`,
5380
- `skill-invalid`) under the coordinator's failure policy
5381
- ({§functionality-model-mutation}); one bad skill never fails the family.
5674
+ is `unavailable` with its exact Problem ({§problems-functionality}) under the
5675
+ coordinator's failure policy ({§functionality-model-mutation}); one bad skill
5676
+ never fails the family.
5677
+
5678
+ Removal follows {§skills-remove}.
5382
5679
 
5383
- Installer provenance follows the upstream lock locations: project
5384
- `skills-lock.json`; global `$XDG_STATE_HOME/skills/.skill-lock.json` when
5385
- configured, otherwise `~/.agents/.skill-lock.json`. A lock's source belongs to
5386
- its scope, never a same-named installation in another root. Missing locks mean
5387
- unknown provenance; malformed or unreadable locks surface their cause.
5680
+ §skills-sources **A source is a git remote, a folder, or a file, read with standard tools.**
5681
+ Fetching runs nothing it fetched. Materialized copies cannot contain references
5682
+ outside their skill; live resources retain {§agent-skills-directory} containment.
5683
+
5684
+ | Source | How it is read |
5685
+ |---|---|
5686
+ | Git remote: a full `https://` or `ssh://` URL, or `user@host:path` | `git ls-remote` resolves the ref at admission; preparation shallow-clones it with hooks and submodules off, and a checkout at any other commit is 409 `source-moved` |
5687
+ | Folder: absolute, `~/`, or relative to the project root | Live reference; standard directory/name matching applies |
5688
+ | A file named `SKILL.md` | Live reference to its skill directory and supporting resources |
5689
+ | A `.zip`, `.tar`, `.tgz`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst` archive | Unpacked into private staging with `unzip` or `tar`; a lone top-level directory is the source's root |
5690
+
5691
+ Any other scheme, plain `http`, `owner/repo` shorthand, and an https URL carrying
5692
+ credentials are refused with `source-invalid` or `source-missing`: shorthand names no
5693
+ forge, and a recorded source is listed to every client. Git runs with the
5694
+ operator's configuration, credentials, and SSH agent, never plurnk's secrets, and
5695
+ never prompts; `PLURNK_SERVICE_SKILLS_FETCH_TIMEOUT_MS` bounds each fetch. The
5696
+ retired vendor-installer knobs (`PLURNK_SERVICE_SKILLS_CLI`, `_CLI_TIMEOUT_MS`,
5697
+ `_REGISTRY_URL`, `_REGISTRY_LIMIT`, `_REGISTRY_TIMEOUT_MS`) make the skills family
5698
+ unavailable when set, each naming what replaced it ({§configuration-repair-path}).
5699
+
5700
+ A source's skills are the directories holding a `SKILL.md`, found by walking
5701
+ from its root without entering `.git` or a skill already found. A fetched skill at
5702
+ the root is named by its frontmatter ({§agent-skills-name}); local references and
5703
+ directories below the root use the standard folder rule. A source with `plugin.json` at its root is an
5704
+ Agent Plugin and is refused with 422 `source-is-plugin`, so its skills keep the
5705
+ plugin's identity. Materialization copies the named skill beside its workspace-owned destination
5706
+ and renames it to `<root>/<name>`; a copy that holds a link out of the skill, or
5707
+ anything but files, directories, and inward links, is refused with 422
5708
+ `source-unsafe` and leaves nothing behind.
5388
5709
 
5389
5710
  §skills-resources **A skill is a resource tree, not a rewritten document.**
5390
5711
  The family exposes enabled, available {§agent-skills-tree} sources through
@@ -5401,13 +5722,13 @@ serialized URI address the same resource, not separate skill identities.
5401
5722
  | `READ (skill://<name>/SKILL.md)` | Original frontmatter and Markdown, unchanged; relative links remain relative to the source layout. |
5402
5723
  | `READ` / `FIND` below the authority | References, scripts, and assets retain their source paths and ordinary pattern, channel, byte, and multimodal semantics. Acquisition observes current source contents, including disappearance. |
5403
5724
  | ```` ```runtime (skill://<name>/scripts/program.ext) ```` | Ordinary resource execution and proposal policy; preserve the native file and its siblings under {§exec-source-temporary}. Discovery and READ never execute scripts. |
5404
- | Model mutation | Read-only; no EDIT, SEND, or KILL of installed resources. Manage installation and enablement through ```` ```skills ````. |
5725
+ | Model mutation | Read-only; no EDIT, SEND, or KILL of skill resources. Manage definitions and enablement through ```` ```skills ````. |
5405
5726
  | Disable / unavailable / remove | Withdraw the authority from new resource access and discovery. Existing log receipts remain historical evidence. |
5406
5727
  | WORK / FORK | Use the same workspace Functionality, not copied definitions or resource caches. |
5407
5728
 
5408
5729
  Explicit skill URIs address these resources; bare operation paths still address
5409
5730
  project files, with no implicit current-skill directory. Source resolution follows
5410
- {§agent-skills-directory}, including installer symlinks and containment of references.
5731
+ {§agent-skills-directory}, including symlinked skill directories and containment of references.
5411
5732
  An uninstalled Git skill is not manufactured by repository detection.
5412
5733
 
5413
5734
  §plurnk-skill **Plurnk's own reference is an ordinary service-provided skill.**
@@ -5420,24 +5741,22 @@ same {§operator-config-env-defaults} renderer as the operator command, never th
5420
5741
  effective environment. Native chapter files retain their owners and locations;
5421
5742
  runtime-generated bytes have no invented disk location. Disable/enable,
5422
5743
  shared workspace visibility, and project/global shadowing use the ordinary Skills lifecycle.
5423
- Service-provided skills are not installer targets; service definitions are
5424
- disable-only under {§skills-remove}.
5425
-
5426
- §skills-remove **`remove` uninstalls the workspace definition's installation.** Before the
5427
- coordinator forgets a workspace-origin skill definition the adapter removes that
5428
- skill from the definition's scope through the standard CLI (`remove <name>
5429
- --yes [--global]`), verified by the directory's absence; a failed removal
5430
- rejects the mutation. A same-named skill at a lower-precedence root is then
5431
- revealed as a service definition, disabled ({§functionality-coordinator}).
5432
- Service definitions are disable-only.
5433
-
5434
- §skills-hotload **Out-of-band installers are admitted at the next turn.** The
5435
- family keeps one signature of both installed roots, source locations, frontmatter
5436
- sources, and installer provenance per resident workspace; turn
5437
- admission recomputes it under the workspace gate before packet assembly and
5438
- republishes the family through the coordinator when it changed, so a skill
5439
- installed or removed by any other tool is discoverable in the first subsequent
5440
- model turn while an unchanged set dispatches nothing. The model manages skills
5744
+ Service-provided skills are not installer targets; removal follows {§skills-remove}.
5745
+
5746
+ §skills-remove **`remove` forgets the workspace binding, not the source.**
5747
+ It withdraws that definition and restores any inherited definition and enabledness
5748
+ ({§functionality-coordinator}). External folders are never deleted. Fetched
5749
+ materializations remain workspace-owned operational state, reusable only for the
5750
+ same complete definition; they confer no availability without a definition.
5751
+ Inherited definitions cannot be removed here; their enabledness can be overridden.
5752
+
5753
+ §skills-hotload **Skills placed out of band are admitted at the next turn** ({§functionality-hotload}). The
5754
+ family keeps one signature of the discovered roots, configured definitions, source locations, and `SKILL.md` sources
5755
+ per resident workspace, read before a publication loads the skills it describes. Turn admission
5756
+ recomputes it under the workspace gate before packet assembly: a changed signature republishes the
5757
+ family, and an unchanged one republishes only when the skills the coordinator would publish differ
5758
+ from the published ones. A skill installed, edited or removed by any other tool is therefore
5759
+ discoverable in the first subsequent model turn, while an unchanged set dispatches nothing. The model manages skills
5441
5760
  only through the generated ```` ```skills ```` family
5442
5761
  ({§functionality-model-projection}); it is never taught a package manager.
5443
5762
 
@@ -5896,6 +6215,7 @@ Every Problem code core mints is named here under its family ({§problem-error-c
5896
6215
  |---|---:|---|
5897
6216
  | `service-starting` | 503 | The PLURNK service owns this listener but has not admitted its client interface yet. |
5898
6217
  | `configuration-unsupported` | 400 | Environment discovery reads this installation's declared configuration; client configuration contributes nothing. |
6218
+ | `configuration-invalid` | 503 | The owning configuration reader's diagnostic, naming the invalid key. Recovery: Correct the named configuration input. Other capabilities remain available. |
5899
6219
  | `name-reserved` | 400 | '*alias*' is plurnk's own: PLURNK_* configuration and provider credential names never reach a subprocess. |
5900
6220
  | `value-invalid` | 400 | '*alias*' needs a string value. |
5901
6221
  | `env-invalid` | 400 | `env` must be an object of string values; '*name*' is not a name a shell can export. |
@@ -5903,14 +6223,19 @@ Every Problem code core mints is named here under its family ({§problem-error-c
5903
6223
  | `headless` | 409 | The workspace has no project root, so there are no file members. Recovery: Open the workspace on a project root. |
5904
6224
  | `definition-invalid` | 400 | A members definition is { glob }: a gitignore-style pattern, `!glob` to exclude; a skill definition names an installable skill. |
5905
6225
  | `model-scope` | 403 | The model may not change membership here: the members scope is none. Recovery: `git add` the file so git tracks it, or ask the operator to add it (/members add) or raise PLURNK_SERVICE_MEMBERS_MODEL_SCOPE. |
5906
- | `registry-unreachable` | 502 | Skills registry *url* could not be reached. |
5907
- | `registry-rejected` | 502 | Skills registry *url* answered *status*. |
5908
- | `registry-invalid` | 502 | Skills registry *url* returned no skills array. |
5909
- | `discover-failed` | 502 | Agent Skills source '*source*' could not be listed: *cause*. |
6226
+ | `query-unsupported` | 400 | Agent Skills have no standard registry to search; discover takes a source: a git remote as a full https or ssh URL, a folder, a lone SKILL.md, or a zip or tar archive. |
6227
+ | `source-invalid` | 400 | '*source*' is not a valid git remote URL; an https source carries no credentials (git's credential helper supplies them); is not a source: a git remote is a full https or ssh URL; is relative, and this workspace has no project root to resolve it against; or is neither a folder, a SKILL.md, nor a zip or tar archive. |
6228
+ | `source-missing` | 404 | No folder or file is at '*source*' (; owner/repo shorthand names no forge, so give the repository's full https or ssh URL). |
6229
+ | `source-unreadable` | 422 | '*source*' cannot be read: *cause*; or '*path*' could not be unpacked: *reason*. |
6230
+ | `source-unreachable` | 502 | git could not reach '*remote*', or fetch it (at '*ref*'): *reason*. |
6231
+ | `ref-missing` | 404 | '*remote*' has no branch or tag '*ref*', or names no default branch. |
6232
+ | `source-moved` | 409 | '*source*' *ref* now names *current*; this skill was added at *commit*. Recovery: Remove the skill and add it again to take the current commit. |
6233
+ | `source-is-plugin` | 422 | '*source*' is an Agent Plugin; install it as a plugin, so its skills keep the plugin's identity and servers. |
6234
+ | `source-unsafe` | 422 | '*path*' links outside its skill, or is neither a file, a directory, nor an inward link. |
6235
+ | `skill-not-found` | 404 | '*source*' carries no Agent Skill named '*name*'. |
6236
+ | `skill-ambiguous` | 409 | '*source*' carries *count* skills named '*name*'. |
5910
6237
  | `alias-mismatch` | 400 | Alias '*alias*' must equal the skill name '*name*'. |
5911
- | `scope-not-installable` | 400 | Service-provided skills can be enabled or disabled; adding a skill requires project or global scope. Recovery: Add it with scope "global" or open a workspace rooted in a project. |
5912
- | `source-required` | 400 | Adding '*alias*' requires the standard installer source that provides it. |
5913
- | `uninstall-failed` | 502 | Agent Skill '*name*' could not be removed from its *scope* root: *cause*. |
6238
+ | `source-required` | 400 | Adding '*alias*' requires the source that provides it. |
5914
6239
  | `workspace-not-found` | 404 | Workspace *id* does not exist. |
5915
6240
  | `state-not-json` | 400 | Worker module state is not JSON-serializable. |
5916
6241
  | `workspace-busy` | 409 | Workspace *id* is running an operation or another capability change. Recovery: Settle the current operation and retry the capability change. |
@@ -5923,9 +6248,8 @@ Every Problem code core mints is named here under its family ({§problem-error-c
5923
6248
  | `loop-policy-invalid` | 400 | An unattended loop cannot hold a proposal for review: nobody is present to answer. Recovery: State proposals accept or reject, or attend the loop. |
5924
6249
  | `scope-cancelled` | 499 | The worker scope was cancelled: *reason*. |
5925
6250
  | `range-not-satisfiable` | 416 | `Range <0,-1>` starts at 0, which is not a line; lines are numbered from 1. Recovery: Write `<1,-1>` to trim every line of the body; `KILL (log:///…/READ)` with no scope retires the item. |
5926
- | `registry-not-configured` | 501 | Skills registry search is disabled; PLURNK_SERVICE_SKILLS_REGISTRY_URL is empty. |
5927
- | `install-failed` | 502 | Agent Skill '*name*' could not be installed from '*source*': *cause* (or the installer reported it but its SKILL.md does not exist). |
5928
- | `skill-missing` | 404 | Agent Skill '*alias*' is not installed under its *scope* root, or is not provided by this service. |
6251
+ | `install-failed` | 500 | Agent Skill '*name*' could not be placed under *root*: *cause*. |
6252
+ | `skill-missing` | 404 | Agent Skill '*alias*' is not provided by this service. |
5929
6253
  | `skill-invalid` | 422 | Agent Skill '*alias*' is not a valid standard skill: *cause*. |
5930
6254
 
5931
6255
  §pinned-wording-core **Pinned wording.** Verbatim sentences tests pin: each is contract, and a change here is a change of contract.