@plurnk/plurnk-service 0.55.0 → 0.57.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 (117) hide show
  1. package/.env.example +9 -14
  2. package/SPEC.md +133 -99
  3. package/dist/content/matcher.d.ts +4 -0
  4. package/dist/content/matcher.d.ts.map +1 -1
  5. package/dist/content/matcher.js +73 -92
  6. package/dist/content/matcher.js.map +1 -1
  7. package/dist/core/ChannelWrite.d.ts +7 -1
  8. package/dist/core/ChannelWrite.d.ts.map +1 -1
  9. package/dist/core/ChannelWrite.js +8 -2
  10. package/dist/core/ChannelWrite.js.map +1 -1
  11. package/dist/core/ChannelWrite.sql +12 -2
  12. package/dist/core/Engine.d.ts +9 -0
  13. package/dist/core/Engine.d.ts.map +1 -1
  14. package/dist/core/Engine.js +345 -104
  15. package/dist/core/Engine.js.map +1 -1
  16. package/dist/core/Engine.sql +44 -2
  17. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  18. package/dist/core/SchemeRegistry.js +6 -1
  19. package/dist/core/SchemeRegistry.js.map +1 -1
  20. package/dist/core/fork.d.ts.map +1 -1
  21. package/dist/core/fork.js +20 -4
  22. package/dist/core/fork.js.map +1 -1
  23. package/dist/core/fork.sql +29 -0
  24. package/dist/core/git-membership.d.ts.map +1 -1
  25. package/dist/core/git-membership.js +40 -4
  26. package/dist/core/git-membership.js.map +1 -1
  27. package/dist/core/packet-inject.d.ts +2 -0
  28. package/dist/core/packet-inject.d.ts.map +1 -1
  29. package/dist/core/packet-inject.js +28 -1
  30. package/dist/core/packet-inject.js.map +1 -1
  31. package/dist/core/packet-wire.d.ts.map +1 -1
  32. package/dist/core/packet-wire.js +32 -28
  33. package/dist/core/packet-wire.js.map +1 -1
  34. package/dist/core/session-settings.d.ts +1 -6
  35. package/dist/core/session-settings.d.ts.map +1 -1
  36. package/dist/core/session-settings.js +4 -13
  37. package/dist/core/session-settings.js.map +1 -1
  38. package/dist/schemes/Exec.d.ts.map +1 -1
  39. package/dist/schemes/Exec.js +41 -7
  40. package/dist/schemes/Exec.js.map +1 -1
  41. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  42. package/dist/schemes/ExecOutputScheme.js +2 -1
  43. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  44. package/dist/schemes/File.d.ts.map +1 -1
  45. package/dist/schemes/File.js +9 -6
  46. package/dist/schemes/File.js.map +1 -1
  47. package/dist/schemes/Log.d.ts +4 -0
  48. package/dist/schemes/Log.d.ts.map +1 -1
  49. package/dist/schemes/Log.js +35 -19
  50. package/dist/schemes/Log.js.map +1 -1
  51. package/dist/schemes/Log.sql +14 -11
  52. package/dist/schemes/Run.d.ts +7 -1
  53. package/dist/schemes/Run.d.ts.map +1 -1
  54. package/dist/schemes/Run.js +49 -9
  55. package/dist/schemes/Run.js.map +1 -1
  56. package/dist/schemes/_entry-find.d.ts +16 -1
  57. package/dist/schemes/_entry-find.d.ts.map +1 -1
  58. package/dist/schemes/_entry-find.js +84 -50
  59. package/dist/schemes/_entry-find.js.map +1 -1
  60. package/dist/schemes/_entry-find.sql +23 -0
  61. package/dist/schemes/_entry-graph.d.ts +6 -1
  62. package/dist/schemes/_entry-graph.d.ts.map +1 -1
  63. package/dist/schemes/_entry-graph.js +35 -22
  64. package/dist/schemes/_entry-graph.js.map +1 -1
  65. package/dist/schemes/_entry-graph.sql +9 -7
  66. package/dist/schemes/_entry-manifest.d.ts +1 -1
  67. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  68. package/dist/schemes/_entry-manifest.js +38 -7
  69. package/dist/schemes/_entry-manifest.js.map +1 -1
  70. package/dist/schemes/_entry-ops.d.ts +5 -0
  71. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  72. package/dist/schemes/_entry-ops.js +14 -0
  73. package/dist/schemes/_entry-ops.js.map +1 -1
  74. package/dist/schemes/_entry-semantic.d.ts +1 -1
  75. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  76. package/dist/schemes/_entry-semantic.js +15 -12
  77. package/dist/schemes/_entry-semantic.js.map +1 -1
  78. package/dist/schemes/exec-abort.d.ts +4 -0
  79. package/dist/schemes/exec-abort.d.ts.map +1 -1
  80. package/dist/schemes/exec-abort.js +6 -0
  81. package/dist/schemes/exec-abort.js.map +1 -1
  82. package/dist/server/Daemon.d.ts.map +1 -1
  83. package/dist/server/Daemon.js +133 -19
  84. package/dist/server/Daemon.js.map +1 -1
  85. package/dist/server/MethodRegistry.d.ts +2 -0
  86. package/dist/server/MethodRegistry.d.ts.map +1 -1
  87. package/dist/server/drain.sql +17 -5
  88. package/dist/server/dsl.d.ts +9 -1
  89. package/dist/server/dsl.d.ts.map +1 -1
  90. package/dist/server/dsl.js +31 -4
  91. package/dist/server/dsl.js.map +1 -1
  92. package/dist/server/envelope.d.ts.map +1 -1
  93. package/dist/server/envelope.js +0 -9
  94. package/dist/server/envelope.js.map +1 -1
  95. package/dist/server/logEntry.d.ts.map +1 -1
  96. package/dist/server/logEntry.js +3 -1
  97. package/dist/server/logEntry.js.map +1 -1
  98. package/dist/server/methods/log_read.d.ts.map +1 -1
  99. package/dist/server/methods/log_read.js +7 -1
  100. package/dist/server/methods/log_read.js.map +1 -1
  101. package/dist/server/methods/log_read.sql +17 -7
  102. package/dist/server/methods/op_look.d.ts +5 -0
  103. package/dist/server/methods/op_look.d.ts.map +1 -0
  104. package/dist/server/methods/op_look.js +30 -0
  105. package/dist/server/methods/op_look.js.map +1 -0
  106. package/dist/server/methods/op_parse.d.ts.map +1 -1
  107. package/dist/server/methods/op_parse.js +7 -2
  108. package/dist/server/methods/op_parse.js.map +1 -1
  109. package/dist/server/methods/session_constraints.js +1 -1
  110. package/dist/server/methods/session_constraints.js.map +1 -1
  111. package/dist/server/methods/session_create.d.ts.map +1 -1
  112. package/dist/server/methods/session_create.js +7 -14
  113. package/dist/server/methods/session_create.js.map +1 -1
  114. package/docs/run.md +3 -1
  115. package/migrations/0000-00-00.01_schema.sql +21 -1
  116. package/package.json +10 -10
  117. package/requirements.md +4 -0
package/SPEC.md CHANGED
@@ -14,14 +14,14 @@ Canonical meanings. When a doc, comment, test name, or commit message uses one o
14
14
 
15
15
  | Term | Meaning |
16
16
  |---|---|
17
- | **agent** | The plurnk runtime singleton. Owns agent-scoped state (default scheme registry, agent-wide entries). One per process. |
17
+ | **agent** | The plurnk runtime. Acts in-session as the reserved `plurnk` run (§actor-boundary self-hosting), never a privileged singleton owning its own entries — the old *agent scope* is retired (entry scope is now `session` / `run`, §machine-processes). |
18
18
  | **session** | Durable user-named workspace. Persists across runs and process restarts. Identity: `sessions.id` + unique `sessions.name`. |
19
19
  | **run** | A stretch of work within a session. Multiple runs per session. May fork from another run via `parent_run_id`. Owns the log entries. |
20
- | **loop** | One model-driven or client-driven iteration within a run. Status ∈ {100 pending · 102 running · 200 done · 413 budget-overflow · 429 turn-ceiling · 499 cancelled · 500 failed · 508 runaway}. Many loops per run. The model runs inside a loop; each client RPC has its own loop. |
20
+ | **loop** | One model-driven or client-driven iteration within a run. Status ∈ {100 pending · 102 running · 200 done · 202 parked (resumable, §send) · 413 budget-overflow · 429 turn-ceiling · 499 cancelled · 500 failed · 508 runaway}. Many loops per run. The model runs inside a loop; each client RPC has its own loop. |
21
21
  | **turn** | One round-trip with the LLM (or one client RPC dispatch). One assembled prompt sent, one parsed response handled. Many turns per loop. Identity: `(loop_id, sequence)`. |
22
22
  | **op** | One DSL operation the model emits. Parsed into a `PlurnkStatement`. Examples: `EDIT`, `READ`, `SEND`, `FIND`, `COPY`, `MOVE`, `OPEN`, `FOLD`, `EXEC`. One turn produces zero or more ops. |
23
23
  | **statement** | Synonym for parsed op. The AST shape `PlurnkStatement` from `@plurnk/plurnk-grammar`. |
24
- | **action** | One executed op. Action and op are the same thing in different states (op = parsed; action = executed). The execution produces a log_entries row at `log:///<L>/<T>/<S>/<op>`. |
24
+ | **action** | One executed op. Action and op are the same thing in different states (op = parsed; action = executed). The execution produces a log_entries row at `log:///<L>/<T>/<S>/<op>`. (The log also holds an *actionless* `op='error'` row — a model emission that failed to parse, §telemetry — so a failure is curatable like any row.) |
25
25
  | **dispatch** | The engine routing a statement to its scheme's op handler. |
26
26
 
27
27
  ### §storage-terms Storage terms
@@ -30,7 +30,7 @@ Canonical meanings. When a doc, comment, test name, or commit message uses one o
30
30
  |---|---|
31
31
  | **entry** | The unit of canonical state. Identity: `(scope, scheme, pathname)`. Holds one or more `channels` of content plus `tags` and `attributes`. |
32
32
  | **channel** | A named content buffer on an entry. Examples: `body`, `stdout`, `stderr`, `headers`, `symbols`. Each channel has `content`, `mimetype`, `tokens`, `state`. |
33
- | **scope** | `"agent"` or `"session"`. Determines who reads. Agent-scope entries visible to every run; session-scope entries to that session's runs. |
33
+ | **scope** | `"session"` or `"run"`. Determines who reads: session-scope entries are the shared world (every run in the session), run-scope entries are a run's private scratch (§machine-processes). *Agent scope is retired.* |
34
34
  | **scheme** | A URI prefix + handler. `known`, `unknown`, `file`, `https`, `exec`. The scheme handler interprets paths under its prefix and implements the op surface. Consumption surface §scheme-surface; author contract: [plurnk-schemes](https://github.com/plurnk/plurnk-schemes). |
35
35
  | **mimetype** | A channel's content type. Drives the handler that produces the structural projections (`symbols`, `deepJson`, `deepXml`). Consumption surface §mimetype-surface; author contract: [plurnk-mimetypes](https://github.com/plurnk/plurnk-mimetypes). |
36
36
  | **provider** | An LLM transport. Implements `generate({messages, signal})` against a wire protocol. Consumption surface §provider; author contract: [plurnk-providers](https://github.com/plurnk/plurnk-providers). |
@@ -43,7 +43,7 @@ Independent axes on entries and channels. Confusion across them is a recurring s
43
43
  |---|---|---|
44
44
  | **status** | HTTP int | Outcome of an operation. Carried on `log_entries.status_rx`, returned from op handlers. Per the catalogue (§send-dispatch). |
45
45
  | **channel state** | `static \| active \| closed \| errored` | Streaming lifecycle of a channel's content. Metadata, not gating — engine renders content regardless of state. |
46
- | **entry state** | `proposed \| resolved \| cancelled` | Proposal lifecycle. `proposed` = pending client accept; `resolved` = side effect happened; `cancelled` = client rejected. Distinct from channel state. |
46
+ | **entry state** | `proposed \| resolved \| failed \| cancelled` | Proposal lifecycle (`log_entries.state`). `proposed` = pending client accept; `resolved` = accepted, side effect happened; `failed` = rejected (no effect); `cancelled` = the proposal was cancelled (loop abandoning). Distinct from channel state. |
47
47
  | **outcome** | `string \| null` | Short reason for `failed`/`cancelled` (`"permission:403"`, `"aborted"`, `"not_found"`). Opaque to most callers. |
48
48
 
49
49
  ### §authority-terms Writer / authority
@@ -64,8 +64,8 @@ Independent axes on entries and channels. Confusion across them is a recurring s
64
64
  | **sudden death** | The last `MAX_STRIKES` turns of a loop's `MAX_LOOP_TURNS` window emit soft 429 warnings so the model can wrap up cleanly. `soft=true`: no strike, no streak increment. |
65
65
  | **mode** | `"ask" \| "act"`. Per-loop. Ask = read-only (no side-effecting ops); act = full surface. |
66
66
  | **flag** | Per-loop boolean shaping the active toolset: `yolo` (auto-accept proposals), `noWeb`, `noInteraction`, `noProposals`. |
67
- | **proposal** | A deferred side-effecting action awaiting client accept/reject (full lifecycle §proposal). State machine: `proposed → resolved` or `proposed cancelled`. `yolo` short-circuits to immediate. |
68
- | **resolution** | Client's accept/reject of a proposal via `op.resolve` RPC. |
67
+ | **proposal** | A deferred side-effecting action awaiting client accept/reject (full lifecycle §proposal). State machine: `proposed → resolved` (accept), `→ failed` (reject), or `→ cancelled` (cancel). `yolo` short-circuits to immediate. |
68
+ | **resolution** | Client's accept / reject / cancel of a proposal via the `loop.resolve` RPC (§methods). |
69
69
 
70
70
  ### §packet-terms Packet terms
71
71
 
@@ -99,10 +99,10 @@ Dependency direction (from root to leaf):
99
99
  - **`plurnk-grammar`** — root. Owns the JSON-Schema contracts (Packet, TelemetryEvent, AST shapes), the ANTLR parser that turns model output into `PlurnkStatement[]`, and `PlurnkParseError` with its `toTelemetryEvent()` helper. Nothing in the ecosystem can speak the DSL without it; everything else pins it exactly.
100
100
  - **Framework siblings** consume grammar and define their own author-facing contracts:
101
101
  - `plurnk-providers` — Provider/Alias types, `parseAliasesFromEnv`, `resolveActiveAlias`, `Mock`, `ProviderUsage` (currency-aware, includes `reasoning`). Vendor-specific implementations are children: `plurnk-providers-openai`, `-google`, `-ollama`, `-openrouter`, `-cloudflare`, `-xai`.
102
- - `plurnk-mimetypes` — handler base classes, discovery, fitting algorithm, matcher dispatch. Handler children are per-mimetype: `plurnk-mimetypes-text-{python,typescript,markdown,html,csv,plain}`, `plurnk-mimetypes-application-{json,yaml,toml,pdf}`, …
102
+ - `plurnk-mimetypes` — handler base classes, discovery, the fitting algorithm, and the match primitives (`queryGlob`/`queryRegex`/`queryJsonpathObject`/`queryXpathString`) the service's matcher dispatches over (§matcher-dispatch). Handler children are per-mimetype: `plurnk-mimetypes-text-{python,typescript,markdown,html,csv,plain}`, `plurnk-mimetypes-application-{json,yaml,toml,pdf}`, …
103
103
  - `plurnk-schemes` — scheme-author types (`SchemeManifest`, `WriterTier`, `LoopFlags`), result-shape contracts (`EntryResult` / `ProposalResult` / `PassthroughResult`), slicing primitives, matcher helpers, `schemeError(...)` constructor. Future scheme children: `plurnk-schemes-http`, `plurnk-schemes-git`, …
104
104
  - `plurnk-execs` — `BaseExecutor`, `SubprocessExecutor`, runtime resolver, discovery. Children declare runtimes: `plurnk-execs-sh`, future `plurnk-execs-search`, `plurnk-execs-node`, …
105
- - **`plurnk-service`** (this repo) — consumes all of the above. Implements the engine, dispatches ops through scheme handlers, hosts the in-tree set of schemes (`plurnk`, `log`, `exec`, `known`, `unknown`, `skill`, `file`), discovers installed mimetype handlers + provider vendors + executor siblings at boot, hosts the daemon (`src/service.ts` over WebSocket + JSON-RPC), and projects packets to the wire per `Packet.json`. Most of the substantive runtime work lives here.
105
+ - **`plurnk-service`** (this repo) — consumes all of the above. Implements the engine, dispatches ops through scheme handlers, hosts the in-tree set of schemes (`plurnk`, `log`, `exec`, `known`, `unknown`, `skill`, `file`), discovers installed mimetype handlers + provider vendors + executor siblings at boot, hosts the daemon (`src/service.ts` over WebSocket + JSON-RPC), and projects packets to the wire (the packet shape is service-owned since grammar 0.67 deleted `Packet.json`, §packet). Most of the substantive runtime work lives here.
106
106
  - **`plurnk`** (client) — terminal UI consuming the daemon's RPC surface. Renders `telemetry/event` notifications, subscribes to log/stream/proposal events. No engine logic of its own.
107
107
 
108
108
  The grammar is the contract. The frameworks consume the contract and add author-facing surfaces. The service consumes the frameworks and runs the engine. The client consumes the service and renders to humans. Each tier is its own published package; each tier's evolution happens in its own repo.
@@ -138,27 +138,27 @@ Server posture: this package is the runtime. User-facing CLI lives in `plurnk` a
138
138
 
139
139
  **Wild west — no mutual exclusion.** Runs share the manifest without locks. Coordination is cooperative (tags + the shared workspace convention) and softly fenced (the §membership `read-only` overlay, a session policy, bounds every run's writable surface uniformly — §machine-processes); a conflict *surfaces* as a delta rather than being prevented. Inform, never override. {§actor-boundary-no-mutex}
140
140
 
141
- **Passive wake.** An idle run wakes on exactly two events, both *directed at the run*: a prompt injected into it — the **voice door** (a user/system `loop.inject`, and once `run://` lands a sibling's `SEND(run:///self)`) — or a **stream-status transition** on a subscription it opened (§channel-state). Everything ambient is a delta — a sibling's edit to a shared entry, an out-of-band disk change — and a delta **never** wakes; it queues and drains at the next turn one of those two events produces (§env-delta). {§actor-boundary-passive-wake}
141
+ **Passive wake.** An idle run wakes on exactly two events, both *directed at the run*: a prompt injected into it — the **voice door** (a user/system `loop.inject`, and once `run://` lands a sibling's `SEND(run://<name>)`) — or a **stream-status transition** on a subscription it opened (§channel-state). Everything ambient is a delta — a sibling's edit to a shared entry, an out-of-band disk change — and a delta **never** wakes; it queues and drains at the next turn one of those two events produces (§env-delta). {§actor-boundary-passive-wake}
142
142
 
143
143
  **Self-hosting — the runtime is an actor, not a back channel.** Runtime-initiated work (fs reconciliation §membership, git auto-add) is an **ephemeral `plurnk` run** firing ordinary ops, seen by other runs through the environment door like any actor's — not a privileged engine pathway. The engine keeps only the irreducible kernel runs stand on (spawn, dispatch, packet assembly, the budget rails §grinder, the fs-watch); everything expressible as ops on session entries is a run doing ops, through the same `op.*` surface (§methods) the service offers clients. Dogfooding is the architecture, not a test mode. {§actor-boundary-self-hosting}
144
144
 
145
- **Migration path.** Largely realized: `Engine.dispatch` is origin-agnostic; client ops run in a per-connection client loop (`_dispatchAsClient`); plurnk EDITs already carry `origin=plurnk`. The keystone is **built** — `dispatchAsPlurnk` spawns the session's reserved `plurnk` run and fires ops through dispatch, mirroring `_dispatchAsClient`. What remains is *repatriation* — the inline plurnk dispatches still bolted into the model's loop (the §env-delta materialization, the manifest build, git auto-add) move onto it.
145
+ **Migration path.** Largely realized: `Engine.dispatch` is origin-agnostic; client ops run in a per-connection client loop (`_dispatchAsClient`); plurnk EDITs already carry `origin=plurnk`. The keystone is **built** — `dispatchAsPlurnk` spawns the session's reserved `plurnk` run and fires ops through dispatch, mirroring `_dispatchAsClient`; its uses so far (operator docs below; the fs-divergence narration) land in the plurnk run's log. The line that remains is one of *kind*, not a list of pending dispatches: work **expressible as an op** belongs on the keystone; work that is **not** stays kernel. Disk→entry materialization is the latter *ingestion* is the inverse of an EDIT (which proposes egress to disk, §membership-edit-membership-gate), so it has no actor op and remains fs-watch kernel, paired with the plurnk run's filtered `source=file` narration (§env-delta) so a sibling pulls only true divergences, not every re-sync; the manifest build is likewise the per-turn derivation pump — packet-assembly kernel, not an entry-creating op. The one outstanding *expressible* piece is **git auto-add** a model-created file surfaced as a plurnk-run op — gated on the §membership repo-overlay still being built.
146
146
 
147
147
  **The keystone's first use: operator reference docs.** `PLURNK_MD_<ALIAS>=<path>` (§operator-config) materializes `<path>` as a `plurnk:///<ALIAS>.md` entry — a `dispatchAsPlurnk` EDIT in the plurnk run, **not** the model's — and the model's turn-0 foists a READ of it. The model reads the doc inline while the materializing EDIT stays out of its log: idiomatic context injection, an ordinary entry + READ rather than a bespoke packet section. The same `PLURNK_MD_*` convention cascades to clients. {§actor-boundary-doc-injection}
148
148
 
149
- **Catalog preview.** `PLURNK_MANIFEST_ITEMS` foists a turn-0 `FIND(scheme:///**)` — one per scheme that holds entries — into the run's first turn, the same plurnk-origin foist as the docs, so a run opens with its catalog instead of blank. `-1` foists each scheme's whole catalog; a positive `N` caps each to its first `N` rows (FIND's `<L>`, clamped to the scheme's count so the strict marker never 416s); unset / `0` foists nothing. `log://` is absent — it is present-mode (the `# Log` section), not a catalog scheme. {§actor-boundary-manifest-preview}
149
+ **Catalog preview.** `PLURNK_FILES_ITEMS` foists a turn-0 `FIND(scheme:///**)` per scheme into the run's first turn (the same plurnk-origin foist as the docs), so a run opens with its catalog instead of blank. The model's own surface — `known`/`unknown` (memory), `run` (scratch), `plurnk` (docs) — always foists **full**; the first-`N` cap applies **only to `file`** (`FIND(file:///**)`, the external, arbitrarily-large tracked-file tree), so the model's own memory is never truncated (a partial view of memory reads as withheld). `-1` = everything full; a positive `N` = the file list capped to its first `N` (FIND's `<L>`, clamped so the strict marker never 416s; memory still full); unset / `0` = no preview (the model FINDs on demand). `log://` is absent — present-mode (the `# Log` section), not a catalog scheme. {§actor-boundary-catalog-preview}
150
150
 
151
151
  ### §machine-processes The machine and its processes: session, run, fork
152
152
 
153
153
  **Question.** §actor-boundary isolates runs and lets the runtime self-host, but it stands on an ownership model it never states: what does a *session* own versus a *run*; what is shared versus private; and what does a fork carry? Unstated, the downstream questions — which run `log.read` reads, what a fork copies, where a per-client window onto the workspace would live — grow subtle, then metastasize. Drawn once, they vanish.
154
154
 
155
- **Decision — the session is the world; a run is a log on it.** A **session** is the world: one shared filesystem — the `session`-scoped entries, surfaced as `plurnk:///manifest.json` (§packet) — under one membership overlay (§membership). Exactly one filesystem and one overlay per session; neither is per-run. A **run** is a process whose private memory is its **log** (§lifecycle-terms) — its loops, turns, and rows, each row carrying its own content, attribution (`origin`/`source`, §env-delta), and fold-state (`expanded`). A run owns **no membership**; even its visibility is not a possession but a bit on its own rows. It is a *history over the shared world, not a world*.
155
+ **Decision — the session is the world; a run is a log on it.** A **session** is the world: one shared filesystem — the `session`-scoped entries, surfaced as the per-scheme catalog (`FIND(scheme:///**)`, §packet) — under one membership overlay (§membership). Exactly one filesystem and one overlay per session; neither is per-run. A **run** is a process whose private memory is its **log** (§lifecycle-terms) — its loops, turns, and rows, each row carrying its own content, attribution (`origin`/`source`, §env-delta), and fold-state (`expanded`). A run owns **no membership**; even its visibility is not a possession but a bit on its own rows. It is a *history over the shared world, not a world*.
156
156
 
157
157
  **One filesystem.** The entries are the session's: `entries.session_id`, never a run. A write by any run is a write to the one filesystem every run reads; there is no per-run entry set. {§machine-processes-one-filesystem}
158
158
 
159
159
  **One overlay.** Membership — `git ls-files ∪ pick − hide` with `view` read-only (§membership) — is the session's: `session_constraints.session_id`, never a run. It is workspace *curation*, and the workspace *is* the session; two runs are two conversations about one curated workspace and see the same one. Divergent membership is a different session, never a per-run overlay. {§machine-processes-one-overlay}
160
160
 
161
- **A run is its log — and nothing beside.** The run-private state is the log and only the log. *What I am looking at* (OPEN/FOLD) is `log_entries.expanded`, a bit on the run's own rows, toggled by ordinary `log:///` ops — not a second store, and never membership (§open-fold). *What I last saw* needs no shadow either: a run learns its world moved through log entries (§env-delta) — a sibling's write broadcast into its log, an out-of-band disk change detected against the entry's own content and broadcast the same way — never through a per-run snapshot the run cannot see. The log is the whole of a run's memory. {§machine-processes-run-is-its-log}
161
+ **A run's memory of the world is its log — no shadow beside it.** A run's view of the shared world is the log and only the log — never a per-run snapshot. *What I am looking at* (OPEN/FOLD) is `log_entries.expanded`, a bit on the run's own rows, toggled by ordinary `log:///` ops — not a second store, and never membership (§open-fold). *What I last saw* needs no shadow either: a run learns its world moved through log entries (§env-delta) — a sibling's write broadcast into its log, an out-of-band disk change detected against the entry's own content and broadcast the same way — never through a per-run snapshot the run cannot see. (Its private **scratch** run-scope entries, §run-scheme — is the run's own evolving workspace, owned not shadowed: a store it writes and reads deliberately, not a hidden mirror of the shared world. The doctrine is *no shadow of the world*, not *no private state*.) {§machine-processes-run-is-its-log}
162
162
 
163
163
  **A run's log is private to packets, not to the session.** Isolation (§actor-boundary) governs what an *actor* sees — its own run, never a sibling's. It does not wall off the *wire*: any connection may read any run's log in its session by id — `log.read({ runId })`, ownership-verified, defaulting to the connection's own run. This is how a conversation client reads the **model** run, where the conversation lives: `loop.run` returns its `modelRunId`, and `session.runs` enumerates a session's runs for a connection that did not drive it live. The read is observation, never packet membership — no actor sees it. {§machine-processes-model-run-readable}
164
164
 
@@ -172,21 +172,24 @@ Server posture: this package is the runtime. User-facing CLI lives in `plurnk` a
172
172
 
173
173
  **Migration path.** Mostly stating what the schema already carries: `runs.parent_run_id` and the parentless `sessions` exist (§lifecycle-terms); `session_constraints` is session-level (§membership); §env-delta already makes a run's timeline self-contained, so a fork's log copy suffices. Additive: `run.fork` over the wire (the engine fork is built). Two repatriations: §actor-boundary's "read-only overlay scopes a run's writable surface" becomes a *session* policy bounding every run uniformly; and the §env-delta environment door has shed its per-run snapshot — a run's only memory is its log, so drift is pulled from the shared log (other actors' edits since the run's last turn) and the filesystem narrates its own through the `plurnk` run, both already log entries, never a per-run shadow.
174
174
 
175
- ### §run-scheme The run:// scheme — spawn, irc, fork, terminate, cap, collect
175
+ ### §run-scheme The run:// scheme — control (spawn, irc, fork, terminate, cap, collect) and run-scope scratch
176
176
 
177
- The run:// scheme makes §machine-processes addressable: a `run://` target is a sister run in the session — `run:///.` the current run, `run:///<name>` a session-scoped sibling (`runs.name`). Same-session only; a run never addresses another session's runs (§actor-boundary). Three ops, fire-and-forget — the child runs independently, lineage in `runs.parent_run_id`:
177
+ The run:// scheme makes §machine-processes addressable: a `run://` target is a sister run in the session — `run://self` the current run, `run://<name>` a session-scoped sibling (`runs.name`). `self` is the reserved current-run sentinel; empty authority (`run:///`) is invalid (400). Same-session only; a run never addresses another session's runs (§actor-boundary). The path discriminates the two faces: **path-absent is control** on the run-as-actor — the NAME is the authority (`run://<name>`, two slashes, no path) — while **path-present is run-scope storage**: `run://<owner>/<path>` (`run://self/<path>` for self) addresses the owner's private scratch (Scratch + Perspective, below). The control ops are three, fire-and-forget — the child runs independently, lineage in `runs.parent_run_id`:
178
178
 
179
- - **Spawn** — `EDIT(run:///<name>):prompt` creates a new sister (empty log) and starts it with `prompt` on its first loop; self cannot be spawned (400). {§run-scheme-spawn}
180
- - **irc** — `SEND(run:///<name>):msg` delivers `msg` to an existing sister, the **voice door** (§actor-boundary-two-doors): an active sister folds it into its next turn, an idle one wakes (§actor-boundary-passive-wake); a name with no run in the session is 404. {§run-scheme-irc}
181
- - **Fork** — `COPY(run:///<src>):prompt` branches `src` (self when `.`): the source's log is deep-copied into a new sister (§machine-processes-fork-copies-the-log), which continues with `prompt`; the world is shared, never copied (§machine-processes-fork-shares-the-world). {§run-scheme-fork}
179
+ - **Spawn** — `EDIT(run://<name>):prompt` creates a new sister (empty log) and starts it with `prompt` on its first loop; self cannot be spawned (400). {§run-scheme-spawn}
180
+ - **irc** — `SEND(run://<name>):msg` delivers `msg` to an existing sister, the **voice door** (§actor-boundary-two-doors): an active sister folds it into its next turn, an idle one wakes (§actor-boundary-passive-wake); a name with no run in the session is 404. {§run-scheme-irc}
181
+ - **Fork** — `COPY(run://<src>):prompt` branches `src` (self is `run://self`): the source's log is deep-copied into a new sister (§machine-processes-fork-copies-the-log), which continues with `prompt`; the world is shared, never copied (§machine-processes-fork-shares-the-world). A fork ALSO inherits the source's run-scope **scratch** — its private workspace deep-copied with the owner remapped (source → branch) — so the branch opens with the parent's notes and diverges on its own edits: *fork = everything-in-common-but-name*. {§run-scheme-fork} {§run-scheme-fork-scratch}
182
182
 
183
183
  All three ride one engine seam — the daemon's inject (active→fold, idle→enqueue+drain) — so the handler creates/branches/resolves the run and hands off; the daemon owns provider + system prompt. COPY's body here is the fork's seed prompt, not a destination path: the engine routes a run:// source away from the entry-copy path before parsing the body.
184
184
 
185
185
  Beyond the three creation ops:
186
186
 
187
- - **Terminate** — `KILL(run:///<name>)` aborts a run by address (self when `.`): its active loop closes 499 and its subscriptions tear down; a name with no run is 404. The override to the fire-and-forget default not a parent-power, whoever holds the address may end it; a run left alone simply ends at its own `SEND[200]`. {§run-scheme-terminate}
187
+ - **Scratch (storage)** — `run://<owner>/<path>` is run-scope entry storage (`scope='run'`, the owner is the run name folded into the path; `run://self` is self). A run EDITs only its own scratch a cross-run write is **403** ("read a sister's notes, never write them") but READs and FINDs any sister's by name (cross-run read is open; scratch is perspective-private, not ACL-private). {§run-scheme-scratch}
188
+ - **Scratch KILL (delete)** — `KILL(run://<owner>/<path>)` with an entry **path present** deletes that scratch entry (200; 404 if absent), self-only like EDIT (a cross-run delete is **403**) — the model's curation lever over its own workspace, distinct from the path-ABSENT `KILL(run://<name>)` which terminates the run (§run-scheme-terminate). The discriminator is the entry path, never the op. {§run-scheme-scratch-kill}
189
+ - **Perspective** — a run's own scratch is catalogued in **its** manifest alone — `Manifest(run) = session-scope ∪ this-run's-run-scope`, foisted as `FIND(run://self/**)` at turn 0 — so a sibling reaches it only by explicit `FIND(run://<name>/**)` and never sees it in its own perspective; isolation is structural (`scope='run'` is excluded from every session query, the owner opted back in only on its own read paths). {§run-scheme-find-perspective}
190
+ - **Terminate** — `KILL(run://<name>)` aborts a run by address (self is `run://self`): its active loop closes 499 and its subscriptions tear down; a name with no run is 404. The override to the fire-and-forget default — not a parent-power, whoever holds the address may end it; a run left alone simply ends at its own `SEND[200]`. {§run-scheme-terminate}
188
191
  - **Cap** — `PLURNK_SESSION_RUNS_MAX_ACTIVE` ceilings the *concurrent* active runs per session (a run with a non-terminal loop); a spawn or fork past it fails hard (508 — no queue, no retry), irc exempt; `-1` disables it. The fork-bomb brake, sized for sessions that live for months. {§run-scheme-cap}
189
- - **Collect** — a run's loop reaching a terminal status surfaces to its sisters as an ambient FOLDED delta (§env-delta): a `SEND` from `run:///<name>` carrying the loop's deliverable — the `SEND[200]` body, or for an abandonment the reason. Every death-path is stamped uniformly, so no termination is silent; collection is the shared world moving, never a verb. {§run-scheme-collect}
192
+ - **Collect** — a run's loop reaching a terminal status surfaces to its sisters as an ambient FOLDED delta (§env-delta): a `SEND` from `run://<name>` carrying the loop's deliverable — the `SEND[200]` body, or for an abandonment the reason. Every death-path is stamped uniformly, so no termination is silent; collection is the shared world moving, never a verb. {§run-scheme-collect}
190
193
 
191
194
  ### §run-lifecycle Run lifecycle: the drain, the reap, the passive wake
192
195
 
@@ -197,6 +200,8 @@ A run is a **log plus a cancellation scope** — one `AbortController` per run,
197
200
  - **A stream's kill binds to the scope it captured at spawn.** A stream captures the run's cancellation scope as it registers and wires its kill to it, re-checking `aborted` AFTER wiring — no check-then-listen gap can drop an abort that lands mid-registration. Because the scope is replaced only once aborted, a captured-then-replaced scope is necessarily already aborted, so replacement never strands a live stream. {§run-lifecycle-exec-epoch-bound}
198
201
  - **A cancelled run is not resurrected by its own torn-down work.** A stream conclusion delivered to a cancelled, idle run starts no fresh drain: an aborted (499) conclusion is skipped, and a straggler that concluded cleanly surfaces its deliverable as an environment delta (§env-delta), never a revived loop. The cancel was deliberate; only an explicit `loop.run` resumes the run. {§run-lifecycle-no-resurrection}
199
202
  - **A stream conclusion always reaches its run.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active run folds the conclusion into its next turn; a run parked at a **slept loop** (`SEND[202]`) **awakens that loop in place** — the slept loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. The result is never lost: a parked loop sleeps rather than ending, and the stream's status-transition is the OPEN event (§actor-boundary-passive-wake) that wakes it; on resume it reads the concluded stream's own state, not a synthetic prompt. {§run-lifecycle-wake-liveness}
203
+ - **A child run concluding wakes a parent parked on it — the topology join.** `run://` spawn/fork records `parent_run_id` (§lifecycle-terms). When a run's drain exits having **concluded** — no parked `202` loop, no open stream — the daemon resumes its parent **in place** if the parent is parked (`#onDrainExit` → the shared `#wakeParkedRun`, the same 202→100 resume a stream conclusion uses). So a parent that spawns work and `SEND[202]`s is woken the moment its child finishes; on resume it reads the child's deliverable from the §run-scheme-collect delta in its own log — a control edge, **never an injected prompt**. The wake recurses upward via the parent's own drain-exit. A child still running — or itself parked at 202 — is not *concluded*, so it does not wake the parent (it's still a live thing the subtree holds). This is the structured-concurrency join: streams and child runs are the same kind of "live thing a run holds," driving premature-terminate (§send-premature-terminate), the wake edge, and the collect delta identically. {§run-lifecycle-child-wake}
204
+ - **A 202 whose subtree is idle emits `loop/quiesced` — a soft signal, not a terminal.** When a loop parks at `SEND[202]` and its subtree is idle (no open stream, no non-terminal child), the daemon broadcasts `loop/quiesced` (§notifications) — the client's honest *"nothing is running under this run right now."* The loop **stays at 202 and is reawakable**: a later `irc`/`loop.run` resumes it (and it re-quiesces, re-firing). It is **never a terminal code** — in a topology where any sibling can `irc` any run, true finality below the session doesn't exist, so quiescence (not finality) is the honest "done." A subtree with any live thing emits nothing — that thing's conclusion is the wake edge, not a quiesce. This is also the dead-park resolution: a wake-edge-less 202 no longer hangs silently; it announces idleness while staying resumable. {§run-lifecycle-quiesced}
200
205
  - **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. {§run-lifecycle-no-lost-loop}
201
206
 
202
207
  ---
@@ -209,12 +214,14 @@ Author-facing contract: [plurnk-providers#1](https://github.com/plurnk/plurnk-pr
209
214
 
210
215
  Three entry points:
211
216
 
212
- - `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`. {§provider-surface-generate}
217
+ - `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw, meta? }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`. {§provider-surface-generate}
213
218
  - `provider.countTokens(text)` — synchronous, called at write-time (§tokenomics) and render-time. Non-negative integer. {§provider-surface-counttokens}
214
219
  - `provider.costFor(usage)` — once per completed turn; pico-USD. Engine writes to `turns.usage_cost_pico`; triggers cascade to `runs.cost_pico` / `sessions.cost_pico`. {§provider-surface-costfor}
215
220
 
216
221
  Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info"), read by the budget {§provider-surface-identity}; and `provider.model` — the instance identity the deferred model-switch recompute compares (§tokenomics), exposed but not yet consumed here.
217
222
 
223
+ **Metadata passthrough (provider → client).** `generate` may return an open `meta: Record<string, unknown>` bag — e.g. a hosted provider's running `balancePico`. The service stores it **unenforced** per turn (`turns.meta`, `json_valid` only — no schema) and forwards the latest turn's blob to the client on the loop usage payload (`loop.run` result / `loop/terminated`, §methods). **The service never reads a field within it.** The canonical-field contract — which fields exist and their shapes — is the *provider framework's* (it normalizes raw vendor data into the agreed set) and the *client's* (it renders that set); a provider and client can ship a feature with **zero service change** as long as the blob flows. Absent → `{}` (the client renders nothing; never fabricated). The mirror direction (client → provider, the self-identified `client` id) rides `generate({client})` (§attribution). {§meta-passthrough}
224
+
218
225
  ### §provider-guarantees Engine → provider guarantees
219
226
 
220
227
  - `messages` is a complete prompt (the section list, pre-assembled into the system + user messages). Provider does not reorder.
@@ -267,7 +274,7 @@ Every op targets a URI; the entry key is `(scope, scheme, pathname)`. The URI pa
267
274
 
268
275
  - A **registered** scheme is a plurnk namespace: its authority is a leading path segment, folded into the pathname (`Engine.#extractTarget` → `foldAuthorityIntoPath`). So `known://x`, `known:///x`, and pathname `/x` are the same entry — the authority is never a host, and the two-slash and three-slash forms are not distinct resources. {§scheme-address-namespace-fold}
269
276
  - A **foreign** scheme (unregistered — `http`/`https`) is a real web host: its authority stays in `hostname`, never folded.
270
- - `file` persists `scheme = NULL`; a relative path resolves against the workspace root to the namespace-absolute `/rel` key (RFC §5 reference resolution), and a path escaping the root is 403 (§membership).
277
+ - `file` persists `scheme = NULL`; a relative path resolves against the workspace root to the namespace-absolute `/rel` key (RFC 3986 §5 reference resolution), and a path escaping the root is 403 (§membership).
271
278
 
272
279
  Storage keys on the resolved `(scheme, pathname)` **verbatim** — the leading slash is the namespace origin, not a filesystem absolute, and is never re-normalized at the storage boundary.
273
280
 
@@ -327,15 +334,20 @@ interface PlurnkSchemeContext {
327
334
  readonly runId: number;
328
335
  readonly loopId: number;
329
336
  readonly turnId: number;
330
- readonly writer: "model" | "client" | "plurnk" | "plugin";
337
+ readonly writer: "model" | "client" | "plurnk" | "plugin"; // WriterTier
331
338
  readonly signal: AbortSignal | undefined;
332
339
  readonly streamEventNotify?: StreamEventNotify;
333
340
  readonly wakeRunNotify?: WakeRunNotify;
341
+ readonly injectRun?: InjectRunNotify; // run:// spawn/fork/irc loop-start (§run-scheme)
334
342
  readonly mimetypes?: Mimetypes;
343
+ readonly executors?: ExecutorRegistry; // boot-discovered EXEC runtimes (§exec)
344
+ readonly tokenize?: (text: string) => number; // write-time tokenizer (§tokenomics)
345
+ readonly defaultChannelFor?: (scheme: string | null) => string;
346
+ readonly pushTelemetry?: (event: TelemetryEvent) => void; // → next packet errors[] + telemetry/event (§telemetry)
335
347
  }
336
348
  ```
337
349
 
338
- Notifier fields populated by the Daemon; absent in test fixtures.
350
+ The optional engine-/daemon-populated capabilities (the notifiers, `injectRun`, `executors`, `tokenize`, `defaultChannelFor`, `pushTelemetry`) are absent in bare test fixtures; a handler that needs one **fail-hards** rather than silently degrading (no default runtime, no silent zero-token write).
339
351
 
340
352
  Engine → scheme guarantees:
341
353
 
@@ -345,7 +357,7 @@ Engine → scheme guarantees:
345
357
  - `ctx.signal` is wired to the run's AbortController (§provider-guarantees-signal-wired).
346
358
  - Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `errors` section (§telemetry). {§scheme-surface-exception-500}
347
359
 
348
- **Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.provider.countTokens` (§tokenomics-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
360
+ **Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.tokenize` (§tokenomics-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
349
361
 
350
362
  ---
351
363
 
@@ -409,7 +421,7 @@ new Mimetypes({
409
421
 
410
422
  Fallback heuristic is a boot-before-provider-resolved tripwire.
411
423
 
412
- **Manifest build.** `EntryManifest.buildManifestBody` is the engine-side packet-assembly pass (the §mimetype firing point) that walks **every** entry. It calls `process({ content, hint })` per channel for the catalog's `lines` (`totalLines`) and, for the body channel, pulls `symbols`+`references` from the *same* call to (re)build the `@graph` symbol index (`symbol_defs`/`symbol_refs`) via `EntryGraph.populateFrom` — one parse, two projections:
424
+ **Derivation pump.** `EntryManifest.maintainDerivations` is the per-turn engine-side pass (the §mimetype firing point) that walks **every** entry. It calls `process({ content, hint })` per channel for the catalog's `lines` (`totalLines`) and, for the body channel, pulls `symbols`+`references` from the *same* call to (re)build the `@graph` symbol index (`symbol_defs`/`symbol_refs`) via `EntryGraph.populateFrom` — one parse, two projections:
413
425
 
414
426
  ```ts
415
427
  const result = await mimetypes.process({ content: r.content, hint: r.mimetype }, { channels: ["symbols", "references"] });
@@ -419,6 +431,8 @@ if (isBody) await EntryGraph.populateFrom(db, sessionId, r.entry_id, result.symb
419
431
 
420
432
  `hint` short-circuits detection. The service consumes `totalLines` (extent), `symbols`/`references` (the `@graph` index), and `deepJson`/`deepXml` (matcher dispatch); never a rendered preview — content reaches the model on READ. Because this pass runs every assembly over every entry, any content change — by any writer — is reflected in the next packet's index. The `@graph` index is NOT engine *ranking* (the anti-pattern): it's a complete, unranked index the model queries via `FIND @<sym`, the manifest paradigm applied to structure, uniform across schemes (`file:///` is the primary case).
421
433
 
434
+ The body channel's embedding vectors derive in the same pass (`EntrySemantic.deriveEmbeddings`): content is tiled into token-budgeted chunks, then embedded in **one data-parallel batch** (`mimetypes.embedBatch`) rather than a per-chunk loop — bit-identical vectors (no re-embed), ~6× on a multi-core box. The pump computes the changed-entry worklist up front so the corpus total is known; a multi-entry pass (the initial ingest, which otherwise looks frozen) emits a throttled `embed_progress` NOTICE (below), and a 0-1 entry steady-state turn stays silent.
435
+
422
436
  **Conformance.** Mimetype-specific behavioral tests live in each handler's own surface. plurnk-service intg covers integration: the engine routes through `Mimetypes.process` with the right hint and the catalog reflects `totalLines`; tests use auto-discovery (production handler set); a custom-handler test injects a stub `BaseHandler` via `loader + discovery`.
423
437
 
424
438
  ---
@@ -437,7 +451,7 @@ Schemes MAY declare multiple channels (`exec`: stdout/stderr/stdin; `http`: body
437
451
 
438
452
  ### §no-visibility Entries carry no visibility
439
453
 
440
- Every entry is uniformly listed in `plurnk:///manifest.json` (§packet) and READable — entries have no per-run open/folded state. Context curation is the model's, on the **log** (via OPEN/FOLD, §open-fold), never on entries.
454
+ Every entry is uniformly listed in the catalog (`FIND(scheme:///**)`, §packet) and READable — entries have no per-run open/folded state. Context curation is the model's, on the **log** (via OPEN/FOLD, §open-fold), never on entries.
441
455
 
442
456
  ### §channel-mimetype Mimetype is a (scheme, channel) property — never a default
443
457
 
@@ -460,8 +474,8 @@ Rules:
460
474
  |---|---|
461
475
  | `known:///france/capital` | body (default) |
462
476
  | `known:///france/capital#symbols` | symbols |
463
- | `exec:///sh/1/1/2#stdout` | stdout |
464
- | `exec:///sh/1/1/2#stderr` | stderr |
477
+ | `sh:///1/1/2#stdout` | stdout |
478
+ | `sh:///1/1/2#stderr` | stderr |
465
479
  | `sse://feed/y#data` | data |
466
480
  | `log:///N/T/A` | (no channel concept; atomic log row) |
467
481
 
@@ -476,8 +490,8 @@ RPC params carry fragments inline via the `target` string (`{ target: "known:///
476
490
 
477
491
  ```
478
492
  <<notes.md:...:notes.md — file scheme (bare)
479
- <<exec:///sh/1/1/2:...:exec:///sh/1/1/2 — exec default (stdout)
480
- <<exec:///sh/1/1/2#stderr:...:exec:///sh/1/1/2#stderr — non-default
493
+ <<sh:///1/1/2:...:sh:///1/1/2 — exec output default (stdout)
494
+ <<sh:///1/1/2#stderr:...:sh:///1/1/2#stderr — non-default
481
495
  <<log:///1/1/0:...:log:///1/1/0 — atomic log row
482
496
  ```
483
497
 
@@ -517,7 +531,7 @@ AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineM
517
531
 
518
532
  - Returns channel content + mimetype {§read-read-content}, or 404 {§read-read-404}.
519
533
  - `lineMarker` slices per §slice-semantics.
520
- - `body` matcher dispatches through `Mimetypes.query` per §matcher-dispatch (all four dialects wired).
534
+ - `body` matcher dispatches through the in-tree `Matcher` per §matcher-dispatch (all four content dialects wired).
521
535
 
522
536
  ### §open-fold OPEN / FOLD
523
537
 
@@ -525,6 +539,12 @@ AST: `{ op: "OPEN"|"FOLD", target, body: MatcherBody | null, signal: tags | null
525
539
 
526
540
  OPEN/FOLD operate on the **log** (`log:///`) — the model's context-curation surface (§packet). FOLD collapses a log row to its path; OPEN restores its body. Non-destructive: rows and bodies persist, re-OPENable. Entries carry no visibility (§no-visibility), so OPEN/FOLD against an entry scheme returns 501.
527
541
 
542
+ ### §model-entry The model's own emission, mirrored back
543
+
544
+ A `model` log row is the model's **verbatim prior emission**, mirrored back so it can finally SEE its own behavior — and reason through its own syntax errors (the parser reports by line; the row renders line-numbered like all content). Actionless, like an `op='error'` row (§telemetry): no target, no op executed; `tx` is empty and the emission lives in `rx.content`, typed `text/vnd.plurnk`. **Born OPEN on a turn that erred** — a parse error or a content-offset NOTICE (`grammar_unenforced`) — so the model resolves the reported line against its own emission, no embedded snippet; **born FOLDED otherwise** (budget-neutral until the model OPENs it). OPEN/FOLD/KILL-able like any log row (the model curates its own history). The engine writes one at the end of each turn that produced output; a struck/empty turn mirrors nothing.
545
+
546
+ The run's **first** model row is exceptional: a born-OPEN turn-0 **exemplar** — a minimal worked example (`PLAN` → environment `FIND`s → `SEND[102]`) the model always opens on, so the grammar can stay thin (the example teaches the syntax, not a heavy grammar). {§model-entry}
547
+
528
548
  ### §copy COPY (engine-orchestrated)
529
549
 
530
550
  AST: `{ op: "COPY", target (source), body (destination), signal: tags | null, lineMarker? }`.
@@ -552,11 +572,11 @@ Log history preserved — `log_entries` stores path tuple as text, not FK to `en
552
572
 
553
573
  AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal: tags | null, lineMarker? }`.
554
574
 
555
- - Filters entries within scope (scheme + pathname prefix). {§find-scope-prefix-filter}
575
+ - Filters entries within scope. The target's GLOB-ness sets it: a **bare** path is the exact entry, a **trailing-slash folder** (incl. the scheme root `/`) or an explicit **glob** expands to a scope (the `*` is folderhood, not a blanket prefix), `#regex#` filters by pathname. Same target contract as READ — bare = the entry, folder/glob = a scope to fan out (#286). {§find-scope-prefix-filter}
556
576
  - `body` matcher operates on entry content (glob/regex/jsonpath/xpath), per grammar plurnk.md §"Body matcher dispatch"; the path-glob lives in the (target), not the body. {§find-glob-filter-on-content}
557
577
  - `signal` is a tag filter; entries match if they have ALL listed tags. {§find-tag-filter-and-semantics}
558
578
  - Session + scheme scoped — no cross-session/cross-scheme leakage. {§find-scoped-isolation}
559
- - Returns `FindResult { status, content, mimetype, results: CatalogEntry[] }`. FIND resolves to the scheme's **catalog rows** the very rows the manifest catalogsfiltered to the statement's matches and kept in match order. A **catalog row** is `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }`: the addressable entry path, its per-channel `{mimetype, tokens, lines}` keyed by addressable URI (default channel → the bare path, non-default → `path#channel`), plus the entry's `tags` and a live `seconds` stream age. The matcher (glob/regex/jsonpath/xpath, `~`semantic, `@`graph) decides WHICH entries appear and in what order a content hit **includes** the entry, a miss **excludes** it; it never reshapes the row. There is no per-match extent: the match LOCATION (which line or symbol) is a `READ` concern, not a FIND field. `content` is the rows as a JSON array (`application/json`); a body-less `FIND(scheme:///**)` yields the scheme's whole catalog — the manifest's per-scheme slice. {§find-result-catalog-rows}
579
+ - Returns `FindResult { status, content, mimetype, results: MatchItem[], matches, pathnames }`. The matcher sets the unit (#286). A **body-less** FIND is the **catalog**: one item per *entry* — `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` (the addressable path, per-channel `{mimetype, tokens, lines}` keyed by URI default channel → the bare path, non-default → `path#channel` plus `tags` and a live `seconds` stream age), the manifest's per-scheme slice. A **matcher** FIND resolves to one item per *match*: the entry's catalog row plus the `matchSpan` `{lineStart, lineEnd}` it hit. **A file with N matches yields N items** — the same row repeated, one span each; there is no `matchLines` array. The unit is uniform across every dialect — glob/regex/jsonpath/xpath select line spans, `~`semantic the ranked chunk's span, `@`graph the matched symbol's span all `(file, span)`, all real content lines (the old "the extent of ~semantic/@graph is not a content line" carve-out was false: a chunk span and a symbol span are line ranges). Order is match order (rank for `~`semantic, source order otherwise); a miss contributes nothing; identical spans dedup. `content` is the items as a JSON array (`application/json`). {§find-result-catalog-rows}
560
580
 
561
581
  ### §send SEND
562
582
 
@@ -579,23 +599,25 @@ The engine's failure terminals — **500** (strike threshold) and **508** (cycle
579
599
  **Two engine error states verify the claim.** Neither is a status code the model learns; both are engine machinery (§engine-rails), pushed to the model as a steering hint on the next packet and **never** as the strike itself (the model sees errors that happened, never the engine's accounting — the gamification policy, §engine-rails). Each strikes (`turnErrors`) and lets the loop continue so the model can correct; a model that ignores the hint and keeps offending spins out to the engine's 500, seeing only the repeated hint, never the count. (Both live at `Engine.runLoop`'s turn close.)
580
600
 
581
601
  - **Idle turn** {§send-idle-turn} — a continuing turn (102) whose ops are only PLAN/SEND — no work op. The model continued with nothing to do. The steer, verbatim: *"If the turn's work is complete, terminate with 200. If awaiting a stream or run trigger, terminate with 202 to hibernate."*
582
- - **Premature terminate** {§send-premature-terminate} — a `SEND[200]` while the run holds a live stream or spawn (§subscriptions, §run-lifecycle-total-reap). The model declared done with work still running. The engine **downgrades the 200 to 102** — the turn continues *and the SEND's body is preserved* (it dispatches as a continue, never discarded) — and steers, verbatim: *"Attempted termination with active streams. Terminate with 202 to hibernate until stream completion, KILL(path) with 200 again to clean up, or 499 to fail."*
602
+ - **Premature terminate** {§send-premature-terminate} — a `SEND[200]` while the run holds a **live thing**: an open stream/spawn (§subscriptions, §run-lifecycle-total-reap) **or a non-terminal child run** (§run-lifecycle — children and streams are the same kind of live thing a run holds). The model declared done with work still running. The engine **downgrades the 200 to 102** — the turn continues *and the SEND's body is preserved* (it dispatches as a continue, never discarded) — and steers, verbatim: *"Attempted termination with active streams or child runs. Terminate with 202 to hibernate until they complete, KILL(path) with 200 again to clean up, or 499 to fail."*
583
603
 
584
- **Dead-park caveat — read before changing 202.** A 202 is a real hibernation only when a wake edge exists; a 202 with *no* wake edge sleeps forever. Today's guard is **preventive** un-advertising 202 plus the two steering states keep the model from declaring a spurious 202 **not** a daemon-side wake-edge check that terminates a wake-edge-less 202. That check is the known backstop, **deferred**. Until it lands, a model that emits 202 with nothing to wake it *can* hang; do not assume a wake-edge guarantee exists.
604
+ **Dead-park caveat — read before changing 202.** A 202 is a real hibernation only when a **wake edge** exists. Two edges exist: a **stream conclusion** (§run-lifecycle-wake-liveness) and an EXEC **poll cadence** `<T,P>` (§exec-poll — a per-run timer resumes the slept loop every P). grammar 0.74.20 now **advertises** `SEND[202]` on the hot path, so the old "preventive" guard (un-advertising it) no longer holds. The remaining gap is the **wake-edge-*less* 202** a model that parks with no open stream and no poll: it has nothing to resume it and *can* hang. The daemon-side check that detects and resolves a wake-edge-less 202 is the known backstop, still **deferred**; until it lands, do not assume a wake-edge guarantee for a 202 emitted with nothing running.
585
605
 
586
606
  ### §exec EXEC
587
607
 
588
- AST: `{ op: "EXEC", target (cwd), body: string | null (command), signal: string | null (runtime tag) }`.
608
+ AST: `{ op: "EXEC", target (cwd), body: string | null (command), signal: string | null (runtime tag), lineMarker (timeout/poll) }`.
589
609
 
590
610
  Engine routes unconditionally to `exec` scheme (path slot is `cwd`, not a URI). The runtime slot (`signal`) selects an executor, resolved against the boot-time `ExecutorRegistry` — siblings discovered and probed at startup, availability cached, default `sh`. Unknown or unavailable runtime → 501 carrying the probe `detail`. {§exec-registry-resolves}
591
611
 
592
- **Effect-gating.** Each executor declares an `effect` (`pure` | `read` | `host`); the service maps it to policy (`EffectPolicy`). A `host` runtime (subprocess; file-backed sqlite) mutates the host**propose** (lifecycle §proposal): the run waits for a human gate, then spawns and writes stdout/stderr to channels of an `exec:///<runtime>/<loop>/<turn>/<seq>` entry (the executor is the URI authority; the coordinate that follows matches the op's log-row coordinate, e.g. `exec:///sh/1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` → `closed`/`errored`) drive what the model sees at subsequent turn boundarieschannel-state). {§exec-host-proposes}
612
+ **Timeout and poll — `<T,P>` on the `<L>` slot (grammar 0.74.20).** EXEC repurposes the line-marker slot as `<timeout, poll>` in **seconds** (consistent with the `seconds=` stream-age render). `T` (mark[0]) caps the spawn's lifetime: at `T>0` the service aborts it (a bounded reap — polite signal then SIGKILL after `PLURNK_EXEC_KILL_GRACE_MS`) and stamps the stream **504**, distinct from a deliberate kill (499) or a clean exit (200). `-1` / absentunbounded (loop-life bounded), the background-stream behavior. **`0` → turn-scoped**: the stream is reaped at the run's *next pre-turn* (via the registry abort, before the turn's own spawns), so it never survives into the subsequent turn; its terminal output surfaces born-OPEN like any close (§exec-stream). {§exec-timeout} `P` (mark[1]) is the hibernation **poll cadence**, stored on the subscription: while the loop is *parked* at `SEND[202]`, the daemon arms a per-run timer for the tightest open poll cadence and resumes the slept loop every P seconds (floored by `PLURNK_EXEC_WAIT_MS` so it can't tick faster than a turn settles) to inspect progress (the same 202100 resume a stream conclusion uses, §run-lifecycle). It does **nothing while the loop is active** an active loop already gets the ambient folded stream deltas exec-stream), so the poll-wake matters only across hibernation. A 202 with no polled stream gets no timer (it sleeps until a conclusion wakes it). {§exec-poll}
613
+
614
+ **Effect-gating.** Each executor declares an `effect` (`pure` | `read` | `host`); the service maps it to policy (`EffectPolicy`). A `host` runtime (subprocess; file-backed sqlite) mutates the host → **propose** (lifecycle §proposal): the run waits for a human gate, then spawns and writes stdout/stderr to channels of a `<runtime>:///<loop>/<turn>/<seq>` entry (the runtime tag is the URI scheme, §exec/#240; the coordinate matches the op's log-row coordinate, e.g. `sh:///1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` → `closed`/`errored`) drive what the model sees at subsequent turn boundaries (§channel-state). {§exec-host-proposes}
593
615
 
594
616
  A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run**: no proposal, no human gate, no notification. It skips the gate a host command faces, but it does NOT resolve in-band — like every exec it backgrounds and streams, its output reaching the model through the environment-observation injector (a foisted READ of the stream's new bytes each turn, §exec-stream), never a same-turn receipt. {§exec-readpure-ungated}
595
617
 
596
618
  **Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector — the same machine §env-delta rides — reads each of the run's open channels from a per-channel byte cursor and foists the new bytes as an `origin=plurnk` READ at `<runtime>:///<coord>#<channel>`, then advances the cursor — each delta carries the `startLine` that cursor implies, so a stream spanning turns numbers into one continuous sequence (lines 1–k, then k+1–m), not a fresh `1:` each turn. The delta is **folded** while the channel streams and auto-**opened** on the terminal one (the channel closed): a model ignores a chatty background run but always SEES a finished one. It never types these READs — it consumes them. The EXEC row itself renders the *command* it ran, `:::`-fenced and line-numbered per §render-rule so the model can line-reference its own code — the input, distinct from the stream above (the output). This is exec as an instance of one ambient machine, env-delta as another (sibling edits, timestamp cursor, always folded). {§exec-stream}
597
619
 
598
- `SEND[499](exec:///<runtime>/<loop>/<turn>/<seq>)` cancels in-flight subprocess via subscription registry's stored AbortController (§stream-control).
620
+ `SEND[499](exec:///<loop>/<turn>/<seq>)` cancels the in-flight subprocess via the subscription registry's stored `AbortController` — the coordinate addresses the spawn (`exec://` is the process-control face); the `<runtime>://` output entry delegates the same KILL to the one `Exec` handler that owns the abort state (§stream-control).
599
621
 
600
622
  **Scoped environment.** An EXEC subprocess inherits the *project's* environment — its `.env`, the standard shell vars — so the model's commands run as the project expects; but never plurnk's own secrets: the provider API keys and `PLURNK_*` config are stripped before the spawn, so a model-run command can't `printenv` the engine's keys. The service owns the scoping policy (the denylist); the executor spawns with the env it is handed. {§exec-env-scoped}
601
623
 
@@ -656,7 +678,7 @@ Model sees lifecycle events in the `log` section per turn.
656
678
 
657
679
  ### §stream-constraints Engine constraints
658
680
 
659
- ONE engine-level constraint: **100 MiB char-length cap per channel body**. `CHECK (length(content) <= 104857600)` on `entry_channels.content` (migrations/001_schema.sql). Violations → SQLITE_CONSTRAINT; action-entry captures rejection at status 500.
681
+ ONE engine-level constraint: **100 MiB char-length cap per channel body**. `CHECK (length(content) <= 104857600)` on `entry_channels.content` (the genesis schema migration, `migrations/0000-00-00.01_schema.sql`). Violations → SQLITE_CONSTRAINT; action-entry captures rejection at status 500.
660
682
 
661
683
  All other limits are extrinsic — providers (request size, model context, fetch timeouts), schemes (per-call validation), mimetypes (render budgets). Engine does not throttle, batch, rate-limit, or cap anything else. {§stream-constraints-engine-one-cap}
662
684
 
@@ -676,8 +698,9 @@ SQLite (`node:sqlite`) with WAL mode and STRICT tables. Hand-written DDL; CI-ali
676
698
 
677
699
  No generator. SQLite-optimal: STRICT (3.37+), `INTEGER PRIMARY KEY` aliasing, explicit `NOT NULL`, indexed query paths, deliberate FK `ON DELETE`/`ON UPDATE`, `WITHOUT ROWID` where access pattern warrants, generated columns, FTS5.
678
700
 
679
- - One migration file per cohesive concern. Numbered, deterministic apply order.
680
- - `migrate` CLI: idempotent; skips applied markers in `applied_migrations`.
701
+ - One `.sql` file per cohesive concern under `migrations/`, **date-prefixed and basename-sorted** for deterministic apply order (`0000-00-00.01_schema.sql` is the genesis).
702
+ - DDL lives in `-- INIT: <name>` blocks (`CREATE TABLE IF NOT EXISTS`) that `@possumtech/sqlrite` runs idempotently at DB open — re-running is a no-op by construction, so today there is no separate `migrate` CLI or applied-marker table. A fresh DB is the recovery story while the project is greenfield-solo.
703
+ - **Forward — apply-once evolution.** The idempotent-`INIT` model creates a fresh schema but does not *evolve* a populated one. When persisted state outlives a "just nuke it" reset, the date-prefixed ordering is the foundation a real migration policy layers onto: ordered `ALTER` files applied once and recorded in a marker table, not recreated idempotently. The current scheme is chosen so that transition is **additive** — a marker table plus apply-once dispatch — never a restructuring of how DDL is authored or ordered.
681
704
  - **Schema-alignment test**: loads `@plurnk/plurnk-grammar/schema/*.json`, parses DDL via `node:sqlite` introspection, asserts every required schema field has a corresponding `NOT NULL` column. Grammar drift fails CI.
682
705
  - DDL = storage truth; JSON Schemas = wire truth. Tested-aligned, allowed to differ where ergonomics demand.
683
706
 
@@ -727,7 +750,7 @@ Env vars configure installed plugins; never declare existence. Filesystem is the
727
750
 
728
751
  Plugin discovery (§plugin-discovery) registers whatever's in `node_modules/@plurnk/*`.
729
752
 
730
- **Providers in-tree (`src/providers/`):** `Mock` (intg-only test fixture + worked example).
753
+ **Providers in-tree:** none. `Mock` (the intg-only test fixture + worked example) is a sibling, `@plurnk/plurnk-providers` (§mock-provider); `ProviderInstantiate.ts` dynamically imports the selected provider package.
731
754
 
732
755
  **Mimetypes in-tree:** none. Framework + handlers are all siblings.
733
756
 
@@ -763,7 +786,7 @@ Plugin discovery (§plugin-discovery) registers whatever's in `node_modules/@plu
763
786
  - Channel state (`active`/`closed`/`errored`) — subscription registry, not on `ChannelContent`.
764
787
  - Backpressure caps — none (§stream-constraints).
765
788
  - Stream cancel — `SEND[499]` (§stream-control).
766
- - Delete — MOVE to `/dev/null` (§move); `SEND[410]` also deletes as a side-effect (§send-dispatch).
789
+ - Delete — `KILL` (entry-KILL, the canonical delete; the MOVE→`/dev/null` idiom is retired, §move); `SEND[410]` also deletes as a side-effect (§send-dispatch).
767
790
  - Per-loop flags — `loops.flags` JSON column; `yolo` end-to-end today, others scheduled.
768
791
  - Default-channel wire rendering — §channel-selection.
769
792
 
@@ -788,9 +811,9 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
788
811
  | `PLURNK_MIN_CYCLES` | `3` | enforced | Min repetitions before cycle detection fires (§engine-rails). |
789
812
  | `PLURNK_MAX_CYCLE_PERIOD` | `4` | enforced | Max period length cycle detection examines (§engine-rails). |
790
813
  | `PLURNK_MD_<ALIAS>` | (unset) | enforced | Operator reference doc: materializes `<path>` as `plurnk:///<ALIAS>.md`, auto-READ into every model run's turn 0 (§actor-boundary). `~` expands to home. |
791
- | `PLURNK_MANIFEST_ITEMS` | `0` | enforced | Turn-0 manifest preview foisted into the model's first turn. `-1` = full `plurnk:///manifest.json`; positive `N` = the first N items (jsonpath slice); `0` / unset = off (§actor-boundary-manifest-preview). |
814
+ | `PLURNK_FILES_ITEMS` | `-1` | enforced | Turn-0 catalog preview, one `FIND(scheme:///**)` per scheme. Memory/scratch/docs always full; the first-`N` cap applies **only** to the `file` list. `-1` = all full; positive `N` = file list first-N (memory still full); `0` / unset = off (§actor-boundary-catalog-preview). |
792
815
  | `PLURNK_PROPOSAL_TIMEOUT_MS` | `300000` | enforced | ms wait for a proposed entry (status=202) to be resolved before timing out. |
793
- | `PLURNK_PROVIDERS_REASON_LEVEL` | `0` | enforced | Reasoning **magnitude** sent to the providers: `0` = none, positive = effort/budget the provider module translates to wire format (o-series tiers, Anthropic `budget_tokens`). The on/off is the `PLURNK_PROVIDERS_REASONING` gate. |
816
+ | `PLURNK_PROVIDERS_REASONING_BUDGET` | `-1` | enforced | The single provider reasoning gate (REQUIRED — fail-hard if unset). `0` = native reasoning off (in-DSL PLAN does it), `-1` = adaptive / no cap, `N` = capped at N tokens; provider modules translate per model family (reasoning_effort tiers, Anthropic `budget_tokens`). Floor (gemma): `0`. |
794
817
  | `PLURNK_FETCH_TIMEOUT` | `600000` | enforced | Service-wide ms ceiling on any outbound request (providers, future http schemes). Module-specific overrides are allowed below the ceiling. |
795
818
  | `PLURNK_DEBUG` | `0` | reserved | Schema-validation toggle. Not yet enforced. |
796
819
  | `PLURNK_LOG_LEVEL` | `info` | reserved | Stdout banner verbosity. Not yet enforced. |
@@ -798,7 +821,7 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
798
821
  **enforced** = engine reads and acts on the value. **reserved** = shipped in `.env.example` (forward-spec) but no-op until wired.
799
822
 
800
823
  **Two override semantics — ceiling vs default.** Which kind a var is determines what "override" means across the cascade:
801
- - **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-session constraint, not a per-call RPC arg. `PLURNK_GIT_ENABLED` (`=0` flatly denies git service-wide, §membership), `PLURNK_BUDGET_CEILING` (§tokenomics), `PLURNK_MAX_COMMANDS`, `PLURNK_MAX_STRIKES`, `PLURNK_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
824
+ - **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-session constraint, not a per-call RPC arg. `PLURNK_GIT_ALLOWED` (`=0` flatly denies git service-wide, §membership), `PLURNK_BUDGET_CEILING` (§tokenomics), `PLURNK_MAX_COMMANDS`, `PLURNK_MAX_STRIKES`, `PLURNK_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
802
825
  - **Default** (explicit-wins) — a fallback the most-specific setter replaces freely: `PLURNK_MODEL` (a `loop.run({alias})` overrides it), `PLURNK_REQUIREMENTS` (the per-call requirements default), and the config-time vars (`HOST` / `PORT` / `DB_PATH`).
803
826
 
804
827
  Enforcement is per-use-site — no central most-restrictive pass; each ceiling is checked where it bites. `PLURNK_MAX_TURNS` ships **off** (`-1` = no cap; the loop ends via SEND, budget, strikes, or cycle detection) and, when an operator sets a positive value, the per-call request is `min()`-capped against it. {§operator-config-max-turns-ceiling}
@@ -806,7 +829,7 @@ Enforcement is per-use-site — no central most-restrictive pass; each ceiling i
806
829
  **Client open-context (per session).** `session.create({settings})` carries per-session overrides, persisted on `sessions.settings` and composed against env at each knob's read-site. Two families, kept distinct so neither semantic leaks into the other; operator-arcane knobs stay env-only — this is the narrow client surface.
807
830
 
808
831
  *Defaults — explicit-wins (the client replaces/merges freely):*
809
- - `settings.manifestItems` (number) **replaces** `PLURNK_MANIFEST_ITEMS` for the session: a one-shot opens clean (`0`), a workspace full (`-1`) or capped (`N`). A single scalar — the client value wins outright. {§operator-config-session-manifest-items}
832
+ - `settings.filesItems` (number) **replaces** `PLURNK_FILES_ITEMS` for the session: a one-shot opens clean (`0`, no preview), a workspace full (`-1`), or with the file list capped (`N`, memory still full). A single scalar — the client value wins outright. {§operator-config-session-files-items}
810
833
  - `settings.mdDocs` (`[{alias, content}]`) **unions** with the server's `PLURNK_MD_*` docs, keyed by alias — a client adds its own repo docs atop the operator's systemwide policy doc. On alias collision the client wins (a deliberate shadow), but by default the policy doc rides into every session. The client sends content (it owns the file), not a path. {§operator-config-session-md-docs}
811
834
 
812
835
  *Ceilings — most-restrictive-wins (the client may only narrow, never widen):*
@@ -843,16 +866,16 @@ Success response: `{ "jsonrpc": "2.0", "id": …, "result": … }`. Failure: `{
843
866
  ### §method-registration Method registration
844
867
 
845
868
  ```ts
846
- registry.register("loop.run", {
869
+ registry.registerMethod("loop.run", {
847
870
  handler: async (params, ctx) => { /* ... */ },
848
871
  description: "Run a model-driven loop with a prompt.",
849
872
  params: {
850
873
  prompt: "string — the user prompt for the loop",
851
- sessionId: "number? — defaults to current attached session",
852
874
  maxTurns: "number? — defaults to PLURNK_MAX_TURNS",
875
+ alias: "string? — overrides the boot-time PLURNK_MODEL",
853
876
  },
854
877
  requiresInit: true,
855
- longRunning: true,
878
+ longRunning: false, // returns immediately (finalStatus:100); the loop runs async, §methods
856
879
  });
857
880
  ```
858
881
 
@@ -938,7 +961,7 @@ registry.register("loop.run", {
938
961
  | Method | Params | Result | Notes |
939
962
  |---------------|-------------------------------------|------------------------|-------|
940
963
  | `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. {§methods-entry-read} |
941
- | `log.read` | `loopId?: number`, | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. {§methods-log-read} |
964
+ | `log.read` | `loopId?`, `turnId?`, `loopSeq?`, `turnSeq?`, `sequence?`, `sinceId?`, `limit?` | `{ entries: LogEntry[] }` | Read recent log entries from the attached session. A full display coordinate (`loopSeq`+`turnSeq`+`sequence`) resolves the single entry behind an `L/T/S` waterfall line — full shape (tx + rx), server-side, no client fetch-all+match (#271). {§methods-log-read} |
942
965
 
943
966
  **Log coordinate.** Every `LogEntry` — from `log.read` and the `log/entry` notification alike — carries `loop_seq`/`turn_seq`, the loop+turn ordinals, beside the `loop_id`/`turn_id` DB keys, so a client renders the logical coordinate (e.g. `01/02/03`) without resolving ids. {§methods-log-coordinate}
944
967
 
@@ -963,9 +986,12 @@ Naming: `target` = URI the op acts on; `scope` for FIND; `source`/`destination`
963
986
  | `op.exec` | `cwd?: string`, `runtime?: string`, `command?: string` | Mirrors `<<EXEC>>`. |
964
987
  | `op.dispatch` | `statement: PlurnkStatement` | Low-level path for clients that have a parsed AST already (e.g. the TUI when the user types raw HEREDOC at the prompt). |
965
988
  | `op.parse` | `text: string` | Convenience: daemon parses raw DSL text via the grammar, dispatches each statement as actions of one turn, returns `{ results: DispatchResult[] }`. |
989
+ | `op.look` | `text: string` | Non-logging READ: resolves the target via READ's full scheme resolver and returns its content, writing **no** log entry. The client's off-run inspection primitive — forward `<<LOOK>>` with the op token rewritten `LOOK`→`READ`. READ-only. {§op-look} |
966
990
 
967
991
  All `op.*` return `{ status, ...op-specific }`. All `requiresInit: true`. None `longRunning`.
968
992
 
993
+ **`op.look` is the exception** to the "creates a turn, fires `log/entry`" rule above (§methods-op-mirror): it runs READ's full resolver (every scheme, full grammar — the client stays grammar-blind, forwarding its `<<LOOK>>` text with the op token swapped to `READ`) but mints **no turn and writes no `log_entries` row** — the read leaves no trace the model can see, the human-side counterpart to membership-gated model reads (§operator-config, "the boundary is the client's"). It resolves against the connection's client loop so run-relative coordinates (`log:///<L>/<T>/<S>`) resolve correctly. Where `entry.read`/`log.read` leave no row but are scheme-limited, and `op.read` resolves everything but logs, `op.look` resolves everything **and** doesn't log. A non-READ statement is rejected. {§op-look}
994
+
969
995
  Future: `subscription.list`, `subscription.cancel` (the latter is `op.send({status: 499, recipient})` today).
970
996
 
971
997
  ### §notifications Notifications
@@ -976,6 +1002,7 @@ Server-initiated events on the same WebSocket.
976
1002
  |--------------------|-------------------------------------|------------|
977
1003
  | `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. {§notifications-log-entry-notify} |
978
1004
  | `loop/terminated` | `{ loopId, finalStatus, hitMaxTurns }` | Loop reaches terminal status. |
1005
+ | `loop/quiesced` | `{ loopId, runId, status: 202 }` | A loop parked at `SEND[202]` reached subtree-quiescence (no open stream, no non-terminal child) — idle/complete-for-now but **reawakable**, distinct from `loop/terminated` (§run-lifecycle-quiesced). |
979
1006
  | `loop/proposal` | `{ logEntryId, sessionId, runId, loopId, turnId, op, target, body, attrs, flags }` | Dispatch pauses on status=202. Carries `flags` so server-YOLO clients can suppress review UI. Client responds with `loop.resolve` (or `PLURNK_PROPOSAL_TIMEOUT_MS` fires). |
980
1007
  | `session/created` | `{ id, name, projectRoot }` | Any client creates a session. |
981
1008
  | `stream/event` | `{ entryId, channel, state, contentLength }` | Channel content grows or state transitions. {§notifications-stream-event-on-channel-change} |
@@ -1015,7 +1042,7 @@ Server-initiated events on the same WebSocket.
1015
1042
 
1016
1043
  **The client's run.** A client connection is an actor (§machine-processes); its `op.*` write to its **own run** — `origin = "client"`, one loop per connection — and `log.read` reads that run. Disconnect closes the loop's status; rows persist. Multiple connections each get their own client run.
1017
1044
 
1018
- `loop.run` and `inject` target the **model's run** — a separate run holding the conversation, `origin = "model"`. Both runs share the session's one filesystem (§machine-processes); the packet renders only the model's run, so the client's ops are structurally absent from it — no origin filter (§actor-boundary-isolation). *Migration:* the daemon today opens both loops in the connection's one run (the conflation §machine-processes corrects); the build gives the client and the model their separate runs.
1045
+ `loop.run` and `inject` target the **model's run** — a separate run holding the conversation, `origin = "model"`. Both runs share the session's one filesystem (§machine-processes); the packet renders only the model's run, so the client's ops are structurally absent from it — no origin filter (§actor-boundary-isolation). The model run (`Envelope.ensureModelRun`) and the connection's client run are distinct, each lazily allocated on first use the §machine-processes conflation is corrected.
1019
1046
 
1020
1047
  ### §errors Errors
1021
1048
 
@@ -1058,7 +1085,7 @@ Each entry: question, answer, rationale, migration path.
1058
1085
 
1059
1086
  **Question.** Rummy uses priority-ordered filter chains for packet assembly. Plurnk builds a default ordered section list directly in `Engine.#buildRequestPacket`, then lets trusted plugins rewrite it.
1060
1087
 
1061
- **Decision.** Two stages. (1) The engine builds the default section list — the kernel sections `definition`, `tools`, `schemes`, `log` (system slot), then `prompt`, `budget`, `errors`, `git`, `requirements` (user slot). (2) `SchemeRegistry.transformSections` pipes that list through every registered scheme that implements `transformSections(sections) → sections`, in registration order, before the engine measures. A plugin returns whatever list it wants — add, remove, reorder. {§packet-plugin-transform}
1088
+ **Decision.** Two stages. (1) The engine builds the default section list — the system-slot sections `definition`, `tools`, `schemes`, the policy sections `system-policy`/`project-policy`, and `budget` (budget is law — a privileged constraint kept in the lean system zone), then the user-slot sections `prompt`, `errors`, `log`, `git`, `requirements` (the log is short-term memory — data, rendered at the action point, not a privileged rule). (2) `SchemeRegistry.transformSections` pipes that list through every registered scheme that implements `transformSections(sections) → sections`, in registration order, before the engine measures. A plugin returns whatever list it wants — add, remove, reorder. {§packet-plugin-transform}
1062
1089
 
1063
1090
  **Why a whole-list transform, not a per-section hook.** It is the legible, fork-avoiding seam: a plugin that can reshape the packet to its needs never has a reason to fork the engine (§ecosystem). And it is **strictly in-process and trusted** (behind `PLURNK_PLUGINS_TRUSTED_ONLY`) — the client/RPC wire never reaches the packet, because handing an untrusted connection the model's entire context is exactly the actor-boundary violation the engine exists to prevent. Pure list-in/list-out; no context is handed to plugins.
1064
1091
 
@@ -1081,7 +1108,7 @@ Each entry: question, answer, rationale, migration path.
1081
1108
  - **Heaviest entries.** A second table lists the ten heaviest log entries by render-weight, each by its `log:///<coord>/<op>` handle — the FOLD targets behind the turn weight. The handle carries the turn, so the two tables interlock. {§tokenomics-largest-entries}
1082
1109
  - **Context-window percent.** The headline carries usage as a percent of the ceiling — `usage Y (P%)` — a fullness gauge beside the absolutes. Reads the ceiling already in hand; no extra provider call. {§tokenomics-context-percent}
1083
1110
  - **Depth re-counted at render.** The manifest re-tokenizes each entry's `tokens` through the live provider at build — never the write-time snapshot — so a model change between loops can't stale the catalog. Every token figure in the packet is render-fresh, manifest and budget alike; nothing trusts a cross-loop cached total.
1084
- - **Over-budget is honest.** When usage exceeds the ceiling, `free` floors at 0 and the percent passes 100 the readout shows the overshoot rather than a negative free, so the model knows it's over and curates down. {§tokenomics-over-budget-floor}
1111
+ - **The delivered packet is never over budget.** The readout shows the state of the packet the model actually has, and the grinder (§grinder) folds any over-ceiling packet back under *before* it is sent — so a delivered budget headline is always usage ≤ ceiling, percent 100, free ≥ 0. The percent is of the **post-fold** packet; the pre-fold overshoot is engine trivia the model never sees. A packet that can't be folded under even fully collapsed is the corner case: the loop **hard-413s** rather than deliver an over-budget packet. Its STORED failure record renders the overshoot honestly — `free` floors at 0 (never negative), the percent passes 100 — never clamped to hide the degenerate state, but never the model's reasoning surface either. {§tokenomics-over-budget-floor}
1085
1112
 
1086
1113
  **Rejected / obviated.**
1087
1114
 
@@ -1100,7 +1127,7 @@ Each entry: question, answer, rationale, migration path.
1100
1127
 
1101
1128
  **Tier — session is the world; permissions are the session's.** Membership, the overlay, and the git flags are **session-tier** (`session_constraints.session_id`, service/session config) — never per-run. Every run in a session shares one world (§machine-processes: one filesystem, one overlay); a run is a *log* — a perspective over that world — owning no membership of its own. A declaration reshapes the one world for every run, never per-connection. `runs.origin` is attribution (whose perspective), not a permission.
1102
1129
 
1103
- **Workspace identity.** No `projects` table; `sessions.project_root TEXT` (nullable = headless) anchors the workspace. `entries.scope` unchanged (`∈ {'agent','session'}`). Workspace = session; no users/auth/multi-tenant.
1130
+ **Workspace identity.** No `projects` table; `sessions.project_root TEXT` (nullable = headless) anchors the workspace. `entries.scope {'session','run'}` (agent-scope retired). Workspace = session; no users/auth/multi-tenant.
1104
1131
 
1105
1132
  **git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files`) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source.
1106
1133
 
@@ -1131,11 +1158,11 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1131
1158
 
1132
1159
  **Rationale.** Session is the right scope unit; membership *is* the curation, outsourced and tiered: git bounds it by tracking, the client supersedes by overlay, the model curates its own render by READ/FOLD — the engine curates nothing. The forest falls out of "session = world": one workspace can be many repos, so membership is their union, declared not guessed (the scan and its security are the client's). Exhaustiveness is a property of *coverage*, not *work*: every member is checked every turn so no drift hides, but unchanged members cost only a detect — the full-repo cost is git's to bound (what it tracks) and the client's to bound (`hide`), never the engine's to pay re-reading what hasn't moved.
1133
1160
 
1134
- **Migration path.** `session_constraints.effect` gains `repo`; the three renames (`add`→`pick`, `ignore`→`hide`, `read-only`→`view`) are wire-surface changes on `session.constrain`. Forest resolution iterates declared repos (was one `ls-files` at root). The change-detect adds a per-member stored signal — mtime+size or content hash, and *that choice is the EMI reliability bound* — gating the existing materialize. `PLURNK_GIT_ALLOWED` replaces `PLURNK_GIT_ENABLED`; `PLURNK_GIT_AUTO` is new. Tenancy / cross-session shared workspaces still require a `workspaces` table lifting constraints off `session_constraints`.
1161
+ **Migration path.** `session_constraints.effect` gains `repo`; the three renames (`add`→`pick`, `ignore`→`hide`, `read-only`→`view`) are wire-surface changes on `session.constrain`. Forest resolution iterates declared repos (was one `ls-files` at root). The change-detect adds a per-member stored signal — mtime+size or content hash, and *that choice is the EMI reliability bound* — gating the existing materialize. `PLURNK_GIT_ALLOWED` (the hard ceiling) and `PLURNK_GIT_AUTO` (the default declaration) are the git flags (§membership-git-flags). Tenancy / cross-session shared workspaces still require a `workspaces` table lifting constraints off `session_constraints`.
1135
1162
 
1136
1163
  ### §grinder Budget enforcement: the grinder
1137
1164
 
1138
- **Question.** §tokenomics surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Two states defeat self-regulation, neither the model's doing: a jumbo prompt (the turn-0 environment), and an unexpectedly large read. (A jumbo repo is no longer its own case — with no index nothing auto-renders the repo; it surfaces only as a large `manifest.json` READ, which the model chunks like any big read.) What enforces the ceiling when the signal isn't enough?
1165
+ **Question.** §tokenomics surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Two states defeat self-regulation, neither the model's doing: a jumbo prompt (the turn-0 environment), and an unexpectedly large read. (A jumbo repo is no longer its own case — with no index nothing auto-renders the repo; it surfaces only as a large catalog `FIND`, which the model pages like any big result.) What enforces the ceiling when the signal isn't enough?
1139
1166
 
1140
1167
  **Decision — a pre-LLM grinder, fired only on actual overflow.** In `Engine.runTurn`, after the packet is assembled (`#buildRequestPacket`) and before `provider.generate`, the assembled render-weight (§tokenomics) is measured against the ceiling. At or under → the packet ships untouched; the grinder never trims speculatively or "helpfully." {§grinder-overflow-only} On overflow it reverts the prior turn, then hard-stops if that isn't enough:
1141
1168
 
@@ -1146,7 +1173,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1146
1173
 
1147
1174
  **What the model sees.** A `budget_overflow` telemetry event (§telemetry), in the model's own terms: which of its entries left the window, by scheme. No mechanism vocabulary — no "layer," no "grinder," no "reclaim" — and no advice. The engine reports *what happened to the model's world*; the budget readout (§tokenomics) — its turn and entry weights — is the diagnostic surface, and the model — which can see what changed in its repo, its reads, its turn — diagnoses the cause the engine can't attribute. {§grinder-event-model-terms} Per the gamification policy (§telemetry), the *strike* the overflow triggers stays engine-internal; the model sees the hidden entries, never the accounting.
1148
1175
 
1149
- **Rationale.** The model owns curation (§tokenomics); the grinder is the exceptional backstop. It only *folds* — reversibly — the prior turn's render; nothing is deleted, so the model can OPEN it back and log history stays intact. Rummy's §1316 spec described clearing log *bodies*, but its code instead folded the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it.
1176
+ **Rationale.** The model owns curation (§tokenomics); the grinder is the exceptional backstop. It only *folds* — reversibly — the prior turn's render; nothing is deleted, so the model can OPEN it back and log history stays intact. Rummy's own spec described clearing log *bodies*, but its code instead folded the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it.
1150
1177
 
1151
1178
  **Migration path.** None on mechanism. Speculative or non-overflow trimming is a different feature, deliberately excluded — the grinder fires only in response to actual overflow.
1152
1179
 
@@ -1203,7 +1230,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1203
1230
 
1204
1231
  ```ts
1205
1232
  type PacketSection = {
1206
- name: string; // stable id: definition, tools, schemes, log, prompt, budget, errors, git, requirements — or a plugin's own
1233
+ name: string; // stable id: definition, tools, schemes, system-policy, project-policy, budget, prompt, errors, log, git, requirements — or a plugin's own
1207
1234
  slot: "system" | "user"; // the prompt-cache boundary; system-slot sections build the cache-stable system message
1208
1235
  header: string | null; // "## Plurnk System X", or null (definition renders verbatim)
1209
1236
  content: string; // rendered markdown — what the model saw
@@ -1222,11 +1249,16 @@ The wire projection (`PacketWire.renderSlot`) groups sections by slot into the s
1222
1249
 
1223
1250
  **Prompt as a first-class entry.** Each loop's prompt is written on loop start as a plurnk-origin `EDIT` against `plurnk:///prompt/<loop_id>` (indexable, body channel, text/markdown). At render time the current loop's prompt body materializes into the `prompt` section; the entry itself stays READ/FOLD-able like any other. The foisted `EDIT`'s **log row is folded by default** (`expanded=0`): the prompt body already lives in the `prompt` section, so the log keeps the action for forensics while collapsing the duplicate body, re-OPENable like any fold (§open-fold). {§prompt-fold}
1224
1251
 
1225
- **The entry catalog.** The catalog is the **complete, unranked directory** of what a session holds, served by `FIND(scheme:///**)` — one per-scheme array, queried on demand, not a single materialized entry (there is no `plurnk:///manifest.json`; the per-scheme arrays replaced it). Built in the schemes layer (`_entry-manifest.catalogRowsFor`); a per-turn derivation pump (`maintainDerivations`) refreshes the deep channels the rows report. A scheme's array is **every entry it holds, in no relevance order**, each `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` — every channel keyed by the URI the model READs (the default channel by the bare path, a non-default by `path#channel`), so it reaches a channel without guessing. `tags` is present only when the entry carries `entry_tags` — its own categorization, surfaced so the model can `FIND` by tag. The model ranks and filters the catalog itself by querying it (task-aware); the catalog never ranks for it — the instant it did, it would be an index again. `tokens` is the provider's live count recounted at render, `lines` the content extent from `Mimetypes.process().totalLines`. The catalog never lists itself. {§packet-manifest-catalog}
1252
+ **The entry catalog.** The catalog is the **complete, unranked directory** of what a session holds, served by `FIND(scheme:///**)` — one per-scheme array, queried on demand, not a single materialized entry (there is no `plurnk:///manifest.json`; the per-scheme arrays replaced it). Built in the schemes layer (`_entry-manifest.catalogRowsFor`); a per-turn derivation pump (`maintainDerivations`) refreshes the deep channels the rows report. A scheme's array is **every entry it holds, in no relevance order**, each `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` — every channel keyed by the URI the model READs (the default channel by the bare path, a non-default by `path#channel`), so it reaches a channel without guessing. `tags` is present only when the entry carries `entry_tags` — its own categorization, surfaced so the model can `FIND` by tag. The model ranks and filters the catalog itself by querying it (task-aware); the catalog never ranks for it — the instant it did, it would be an index again. `tokens` is the provider's live count recounted at render, `lines` the content extent from `Mimetypes.process().totalLines`. The catalog never lists itself. {§packet-catalog}
1226
1253
 
1227
1254
  ### §telemetry user.telemetry — model-facing runtime telemetry
1228
1255
 
1229
- Telemetry the model MUST react to immediately. Errors render in their own `errors` section (§packet-assembly unbundled them from a single telemetry block) — transient, appearing on the turn AFTER the failure, clearing once seen. The `log` section is the durable audit; the `errors` section (rendered from `packet.telemetryErrors`) is the **alert**.
1256
+ The model's runtime alert surface, with two sources by lifetime:
1257
+
1258
+ - **Errors are LOG ITEMS.** A model FAILURE — a parse failure (an actionless `op='error'` row) or an action that returned `status_rx ≥ 400` — is a durable `log_entries` row. It folds, kills, and budgets like any log entry (§open-fold), so the model recalls or curates its own mistakes and the grinder's prior-turn rollback can reclaim them — ONE budget surface, the log. The `errors` section is a derived POINTER INDEX over the recent `status_rx ≥ 400` rows (status + `log:///<coord>`), aiming the model at them; it holds no bodies and no fold/budget state of its own. Durable: an error persists until the model folds or kills it, not "cleared once seen."
1259
+ - **Engine NOTICES are telemetry.** Ephemeral events the engine emits while steering or narrating — a provider's `grammar_unenforced`, a `max_commands_exceeded` truncation, a `budget_overflow` fold, the premature-terminate/idle steers. Transient: they appear on the turn AFTER the event and drain once seen (`packet.telemetryErrors` is their only home). They are NOT the model's failures.
1260
+
1261
+ The `log` section is the durable audit; the `errors` section surfaces both — the error pointers (durable, in the log) and the notices (ephemeral).
1230
1262
 
1231
1263
  **Grammar contract:**
1232
1264
 
@@ -1236,27 +1268,28 @@ Telemetry the model MUST react to immediately. Errors render in their own `error
1236
1268
  **Plurnk-service rendering:**
1237
1269
 
1238
1270
  - `budget` per §tokenomics: turn-weight and heaviest-entries tables with `tokenCeiling`/`tokenUsage`/`tokensFree`.
1239
- - `errors[]` from previous turn's dispatch. Required: `kind` discriminator. Additional kind-specific fields are flat on the element NO nested `detail`. Canonical-JSON serialization sorts keys for prefix-cache friendliness.
1240
- - Wire: one `* {canonical-JSON}` line per error under `## Plurnk System Errors`, push order. Buffer drains on read. {§telemetry-drain-on-read}
1271
+ - **Error pointers** — derived from the previous turn's `log_entries WHERE status_rx ≥ 400`: `kind: "action_failure"`, `coordinate` (`<L>/<T>/<S>`), `op` (`error` for a parse failure, the real op for a failed action), `status`, `target`, and a terse `error`. The full body (message, snippet) lives on the foldable log row, not here.
1272
+ - **Notices** one `* {canonical-JSON}` line per event under `## Plurnk System Errors`, push order; flat kind-specific fields (NO nested `detail`); canonical-JSON sorts keys for prefix-cache friendliness. The notice buffer drains on read — each appears on exactly one packet. {§telemetry-drain-on-read}
1241
1273
  - **No prose `message` field.** Errors carry structured facts. The `kind` is the alert; the named fields are the data. Guidance, advice, hints, and exhortation MUST NOT appear in telemetry. Letting the model infer what to do from facts (and the log) beats handing it instructions it will second-guess.
1242
1274
  - **Gamification policy (rummy precedent, plugins/error/error.js).** The model sees errors that **happened** — its actions failed, its emission didn't parse, its ops were truncated. The model does NOT see the engine's accounting *about* errors: strike streaks, cycle detection, sudden-death thresholds, no-ops bookkeeping. Surfacing internal state creates a gamification surface where the model optimizes for engine metrics (manufacturing a clean turn to reset the strike counter, e.g.) instead of the task. Engine bookkeeping drives abandonment silently; the model just sees its actual failures.
1243
1275
 
1244
- **Kinds emitted by plurnk-service:**
1276
+ **The error pointer + the engine NOTICE kinds:**
1245
1277
 
1246
1278
  | `kind` | Source | Required fields |
1247
1279
  |---|---|---|
1248
- | `parse_error` | Grammar parser failed mid-statement | `source: "grammar"`, `kind`, `message`, `position` (content-offset), `snippet` (model's offending line, N:\t-prefixed), `parserSource` (`lexer`/`parser`/`visitor`) |
1249
- | `action_failure` | Log entry with `status_rx 400` from previous turn | `kind`, `coordinate` (`<L>/<T>/<S>`), `op`, `status`, `target` (URI or null). May carry scheme-emitted `error` (a terse fact, not guidance). |
1280
+ | `action_failure` | The DERIVED error pointer any `log_entries` row with `status_rx ≥ 400` from the previous turn: a parse failure's actionless `op='error'` row, or a failed action | `kind`, `coordinate` (`<L>/<T>/<S>`), `op`, `status`, `target` (URI or null). May carry a terse `error` fact. The full message/snippet lives on the foldable log row. |
1281
+ | `grammar_unenforced` | (provider, forwarded) GBNF-filter divergence the model's bytes diverged from the transported grammar | `source: "provider:*"`, `kind`, `message`, `position` (content-offset) |
1250
1282
  | `max_commands_exceeded` | Single emission exceeded `PLURNK_MAX_COMMANDS` cap; overflow ops dropped without dispatch | `source: "engine:rail"`, `kind`, `emitted`, `dropped` |
1251
1283
  | `budget_overflow` | Assembled packet exceeded the budget ceiling; entries moved out of the window to fit | `source: "engine:rail"`, `kind`, `hidden` (per-scheme `[{scheme, count}]` — entries removed from the window) |
1284
+ | `embed_progress` | The derivation pump (§mimetype-surface) is embedding a multi-entry corpus pass; throttled to ~10 milestones, silent for a 0-1 entry turn | `source: "engine:derivation"`, `kind`, `completed`, `total`, `message` |
1252
1285
 
1253
- Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping are all engine-internalthey drive abandonment silently per the gamification policy above. Action-bound failures (handler returned 4xx/5xx or threw) mirror as `action_failure` kind on the next packet. Full detail queryable via `log:///`. {§telemetry-no-error-scheme}
1286
+ **Severity on the wire (`level`, required grammar 0.74.29+).** Every `TelemetryEvent` carries `level: "error" | "warn" | "info"`, set by the **producer** at the emit site severity is meaning the producer owns, not something the client re-derives by pattern-matching the open `kind` vocabulary. Service mappings: provider errors and `max_commands_exceeded` → `error`; engine steers (`idle_turn`, `premature_terminate`) and `budget_overflow` → `warn`; `embed_progress` → `info`. Forwarded provider/executor events carry the producer's own `level` (the service defaults it only for a producer predating the field). Clients color straight off `level`. {§telemetry-event-level}
1254
1287
 
1255
- **No `error://` scheme.** Actionless failures route to telemetry, not a queryable scheme namespace.
1288
+ Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping stay engine-internal — they drive abandonment silently per the gamification policy. Both error kinds — a failed action AND an actionless parse failure — are LOG ITEMS (`log:///<coord>`, `op='error'` for the latter, `status_rx ≥ 400`), foldable and re-OPENable, with full detail (message, snippet) on the row. The `errors` section surfaces a derived pointer to each. There is **no bespoke `error://` scheme**: errors live in the log, addressable + curatable like any row — not a separate queryable namespace. {§telemetry-no-error-scheme}
1256
1289
 
1257
- **Client surface: `telemetry/event` notification.** Every event the engine pushes to the loop's telemetry buffer also broadcasts live via the `telemetry/event` WS notification. Same envelope on both sides `{ source, kind, message?, position?, …kind-specific }` per the grammar's `TelemetryEvent` schema. The model sees the event on the NEXT packet's `errors` section (drains on read); the client sees it the moment it lands. Client uses cases: render parse errors in a debug panel (the `snippet` field is content the model emitted), surface strike/sudden_death as "loop is degrading" toasts, log everything to a session timeline. Scoped to the loop's session. {§telemetry-telemetry-event-notify}
1290
+ **Client surface.** Engine NOTICES broadcast live via the `telemetry/event` WS notification same envelope as the model's drained copy (`{ source, kind, level, message?, position?, …kind-specific }` per the grammar's `TelemetryEvent` schema), the moment they land, scoped to the loop's session (a `grammar_unenforced` snippet in a debug panel, a session timeline). ERRORS do not broadcast on this surface: they are log rows, and the client reads them the same way the model curates them `log.read` / the `log/entry` notification, the durable log. {§telemetry-telemetry-event-notify}
1258
1291
 
1259
- **Content-offset snippet rendering.** When telemetry carries `position: { type: "content-offset", line, column }`, plurnk-service extracts a ±N-line slice from the model's own prior `assistant.content` and renders it as an `N:\t`-prefixed heredoc under an `error://<line>` fence, immediately following the event meta line. Without the snippet, the model gets "invalid xpath at 1:0" with no way to trace what it wrote at 1:0 and tends to regenerate the same broken emission. With it, recovery is direct (canonical case: the edit-todo demo where a READ body starting with `//` got xpath-dispatched). The snippet field is stripped from the meta JSON so it appears once, in the body block. {§telemetry-content-offset-snippet}
1292
+ **Content-offset position.** An emission-level error carries a `position: { type: "content-offset", line, column }` into the model's own emission a parse-error LOG ROW (op='error', §model-entry) and a content-offset NOTICE (e.g. a provider's `grammar_unenforced`) both report the line, not the bytes. The model resolves it against its own emission: the turn that erred has its `model` row born OPEN (§model-entry), so the line-numbered emission sits in the log and the model reads the cited line directly. No snippet is embedded that would duplicate the emission the model already holds. {§telemetry-content-offset-pointer}
1260
1293
 
1261
1294
  ### §tools user.tools — the capability sheet
1262
1295
 
@@ -1270,7 +1303,11 @@ A `## Plurnk System Schemes` section renders in the system slot **after the defi
1270
1303
 
1271
1304
  ### §inject system.inject — the operator injection
1272
1305
 
1273
- When `PLURNK_PACKET_INJECT` names a readable markdown file, its content renders as a `## Plurnk Operator Notes` section in the system slot **right after the teaching** (definition → tools → schemes → inject), before the log — part of the cached prefix. Read per-turn so the operator's edits take effect live; a set-but-unreadable path fails the turn hard (a deliberate setting with a broken path is a misconfig, surfaced not hidden). `~/` expands to home. It's the operator-side complement to the plugin section hook — a pressure valve so reshaping the packet edits operator content, never the core. Unset → no section. {§packet-inject}
1306
+ When `PLURNK_PACKET_INJECT` names a readable markdown file, its content renders as a `## Plurnk Operator Notes` section in the system slot **right after the teaching** (definition → tools → schemes → inject), ahead of the policy sections and budget — part of the cached prefix. Read per-turn so the operator's edits take effect live; a set-but-unreadable path fails the turn hard (a deliberate setting with a broken path is a misconfig, surfaced not hidden). `~/` expands to home. It's the operator-side complement to the plugin section hook — a pressure valve so reshaping the packet edits operator content, never the core. Unset → no section. {§packet-inject}
1307
+
1308
+ ### §policy system.policy — the client's policy injection
1309
+
1310
+ Two sections ride the system slot **below the operator notes, above budget**: `## Plurnk System Policy` from `PLURNK_POLICY` (default `~/.plurnk/AGENTS.md`) and `## Project Policy` from `PLURNK_PROJECT` (default `<projectRoot>/AGENTS.md`, resolved relative to the session root). AGENTS.md is **policy** — the client's authoritative rules promoted into the privileged zone — NOT a curatable, foldable, READ-able entry; the model cannot FOLD it away. A default-absent path is silent (the section is omitted); an explicit override (env set) that fails to read fails the turn hard — a deliberate setting with a broken path is a misconfig, surfaced not hidden. Read per-turn so edits take effect live. Reference/scratch docs are NOT policy — they ride `PLURNK_MD_*` (materialized as READ-able entries, §operator-config), which is where the dev-notes AGENTS.md used to hold belong. {§policy-sections}
1274
1311
 
1275
1312
  **The scheme self-doc contract.** `example` is the hot-path one-liner; `documentation` is the deep doc — the exact shape execs already use (`example` + `documentation`). `SchemeRegistry.teach()` renders the directory; `docEntries()` materializes the docs (per loop.run, alongside the operator docs). `documentation` rides a service-side `SchemeManifest` extension until plurnk-schemes#25 lands it in the contract.
1276
1313
 
@@ -1288,42 +1325,39 @@ Rendered at the END of the user packet under `## Plurnk System Requirements` {§
1288
1325
 
1289
1326
  Body matchers and `<L>` both dispatch on entry mimetype. Body matcher: leading-char classification (`//` xpath, `/` regex, `$` jsonpath, otherwise glob). `<L>`: line-navigable → by line, structured → by item.
1290
1327
 
1291
- ### §matcher-dispatch Matcher dispatch (delegated to `Mimetypes.query`)
1328
+ ### §matcher-dispatch Matcher dispatch (service-owned, over daughter primitives)
1292
1329
 
1293
- `matchAgainstContent` (exported from `@plurnk/plurnk-schemes`) is an adapter over `Mimetypes.query(input, expression)`. Framework parses leading prefix, resolves per-mimetype handler, returns `QueryMatch[]`. Adapter maps typed errors:
1330
+ `Matcher.matchAgainstContent` (in-tree, `src/content/matcher.ts`) is the **service's own** dialect dispatch — `Mimetypes.query` is NOT consumed (§mimetype-methods). It handles the **content dialects** and switches on each, calling the daughter's individual primitives: `glob → queryGlob` and `regex → queryRegex` over the raw content; `jsonpath → queryJsonpathObject` over the `deepJson` projection and `xpath → queryXpathString` over `deepXml` (both pulled from `mimetypes.process({channels})`, so a structural dialect works over any source type), returning `QueryMatch[]` rendered as `<source-line>:\t<line>`. `~semantic` and `@graph` are **relation dialects, not content matchers** — FIND resolves them upstream to `(file, span)` items (`~`semantic via `rankSemantic`, `@`graph via `EntryGraph`), so they never reach `matchAgainstContent` (a fail-hard invariant guards the impossible routing). Status mapping (content dialects):
1294
1331
 
1295
- | Framework error | HTTP status |
1332
+ | Result | HTTP status |
1296
1333
  |---|---|
1297
- | `UnsupportedDialectError` | 415 |
1298
- | `InvalidExpressionError` | 400 |
1299
- | `QueryParseFailureError` | 203 (soft fallback: raw content as `text/markdown` with `reason`) |
1300
- | Empty match array | 204 |
1301
1334
  | Match array | 200 |
1335
+ | Empty match array | 204 |
1336
+ | Malformed matcher expression | 400 |
1337
+ | Source unparseable for its mimetype | 203 (soft fallback: raw content as text with `reason`) |
1302
1338
 
1303
1339
  203 is HTTP-creative ("Non-Authoritative Information"). On parse failure, returns raw bytes as text primitive with `reason` so the model can fall back to regex/visual parsing or fix source. {§matcher-dispatch-203-soft-fallback}
1304
1340
 
1305
1341
  Glob anchoring (`TODO*` starts-with, `*TODO*` contains, `*.log` ends-with, `[Tt]odo*` char class) lives in framework's `BaseHandler`.
1306
1342
 
1307
- ### §matcher-result Matcher result shape (uniform across dialects)
1343
+ ### §matcher-result Matcher result shape READ returns matching LINES, uniformly
1308
1344
 
1309
- Body: one match per line as `<line>:\t<value>` — the same `N:\t` form READ emits, so `<L>` can page the result set. Empty 204. Mimetype = `text/markdown` regardless of source dialect.
1345
+ The contract is the grammar's: **plurnk.md §"`<Line> / <Result>`""FIND returns rows of results, READ returns lines of content"**, and READ "prefixes every line with line numbers and a hard tab, `N:\t`" (one source-line number, not part of the source). This section documents the service's implementation of that.
1310
1346
 
1311
- - `<line>`1-indexed source line, shifted back to source coordinates when matching inside an `<L>` slice.
1312
- - `<value>` — the extracted match, rendered bare when it is a single-line string, else JSON-encoded (preserving the one-match-per-line invariant). Polymorphic per dialect:
1313
- - **bare regex** → string (full match)
1314
- - **anon captures** → array `[c1, c2, …]`
1315
- - **named captures** → object `{name: v, …}`. Mixed anon+named uses positional keys `"1"`, `"2"` alongside names.
1316
- - **glob** → string (matching source line)
1317
- - **jsonpath** → JSON value at the path
1318
- - **xpath text/attr** → string
1319
- - **xpath node** → serialized XML
1347
+ **A matcher selects locations; it never extracts a value.** Every dialect identifies *where* in the source it matches; READ returns the **source line(s)** at those locations, faithfully one shape for every dialect: `<line>:\t<line-content>`, prefixed with the single source-line number per plurnk.md (shifted back to source coordinates inside an `<L>` slice), never double-numbered. Empty → 204; mimetype `text/markdown` regardless of source. The model reads the line and adapts whatever it needs out of it — READ never pre-chews a match down to a bare value. {§matcher-result-read-returns-lines}
1320
1348
 
1321
- | Dialect | Extracts | Natural use |
1349
+ | Dialect | Selects | Natural use |
1322
1350
  |---|---|---|
1323
- | regex `/pat/` | substring (or captures) | extract the value after X: |
1324
- | glob `pat` | whole matching lines | show lines containing TODO |
1325
- | jsonpath `$.path` | JSON values (parsed value for JSON-shaped mimetypes; bare-leaves outline for markdown/HTML/source) | get the host field / jump to Installation |
1326
- | xpath `//sel` | XML nodes/text/attrs (text/html only) | get the h1 contents |
1351
+ | regex `/pat/` | the lines the pattern occurs in (it *matches*, never captures-and-extracts) | the lines mentioning X |
1352
+ | glob `pat` | the lines the glob matches | the lines containing TODO |
1353
+ | jsonpath `$.path` | the line(s) where the structural path resolves | the line defining `host` |
1354
+ | xpath `//sel` | the line(s) of the selected node (text/html) | the line(s) of the h1 |
1355
+ | `~`semantic `~q` | the line span of each ranked chunk (a relation, resolved by FIND) | the section about X |
1356
+ | `@`graph `@<sym` | the line span of each matched symbol occurrence (a relation, resolved by FIND) | where X is referenced |
1357
+
1358
+ **READ honors FIND.** A READ that resolves to more than the single exact entry — a glob/folder scope, OR any matcher — fans out: the engine runs the scheme's FIND, then writes **one log row per MATCH** (not per file), each delivering that match's content — READ is the content retrieval over FIND's survey (§find-result-catalog-rows). A file with N matches → N rows. It costs **one command** (the model emitted one READ) yet writes N rows, each its own concrete `(file, span)` — individually foldable/killable/re-READable. A matcher row carries the source LINES at the match's span, delivered via a **raw line-slice** so a structural mimetype's item-index `<L>` never mis-slices a span that is, by construction, source lines; a body-less folder/glob row carries the whole entry. A **bare entry, body-less** is the single direct read. Zero matches writes a single `204` row (never silence). {§read-multi-file-fanout}
1359
+
1360
+ > **Source-line provenance (shipped, every dialect).** Each hit carries a source-line span: regex/glob over raw content; jsonpath/xpath over the parsed `deepJson`/`deepXml` projection (the mimetypes daughter reports each hit's line span); `~`semantic the ranked chunk's span; `@`graph the symbol occurrence's span. So the per-match `(file, span)` item is well-defined for every dialect, and READ returns the line uniformly.
1327
1361
 
1328
1362
  ### §slice-semantics `<L>` semantics by source mimetype
1329
1363
 
@@ -1347,7 +1381,7 @@ Body: one match per line as `<line>:\t<value>` — the same `N:\t` form READ emi
1347
1381
  - `<0>` / `<-1>` → `[]` for READ
1348
1382
  - Out-of-range → 416; malformed JSON → 400
1349
1383
 
1350
- **Killer composition.** `<<READ(log:///N/M/K)<P>::READ` picks the P-th match from a prior matcher result matcher rx is `application/json`, structural `<L>` selects the P-th element. {§slice-semantics-compose-pattern}
1384
+ **Compose by addressing the match.** Under per-match fan-out a matcher READ writes one row per match, so the **N-th match IS `log:///<l>/<t>/N`** its own addressable row, read directly. There is no `<P>`-slice of a combined blob (no blob exists). To process a match further, READ its row and apply a matcher/`<L>` to that content (the body-less compose-chain). {§slice-semantics-compose-pattern}
1351
1385
 
1352
1386
  ### §json-edit Structural EDIT on JSON
1353
1387
 
@@ -1419,9 +1453,9 @@ Carried from the contract walk; durable.
1419
1453
  - **Dialect/mimetype mismatch** → 415 (xpath on text/plain → 415; jsonpath on JSON-shapeless mimetypes → 204 because outline is empty, not 415).
1420
1454
  - **Binary entries** → 415 across the board for READ/EDIT/OPEN/FOLD.
1421
1455
  - **EDIT `<L>` on non-existent entry** → body becomes content; `<L>` is positional-only on existing content.
1422
- - **COPY `<L>`** → source range, symmetric with READ `<L>`.
1456
+ - **COPY/MOVE `<L>`** → slices the SOURCE range into the destination (every channel), symmetric with READ `<L>` but WITHOUT the `N:\t` prefix (`sliceLinesRaw`); an out-of-range marker → 416. MOVE `<L>` copies the slice, then deletes the whole source (relocation of a fragment). Binary channels can't be sliced (the binary→415 rule above). {§copy-l-source-range}
1423
1457
  - **READ rx** prefixes each line with `N:\t` per §render-rule. `sliceLinesRaw` (used by COPY) returns the lines without prefix.
1424
- - **FIND body matcher** applies to entry content (all dialects), per-candidate via `Matcher.matchAgainstContent` `Mimetypes.query` (status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
1458
+ - **FIND body matcher** applies to entry content (all dialects), per-candidate via the in-tree `Matcher.matchAgainstContent` (§matcher-dispatch; status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
1425
1459
  - **OPEN/FOLD** operate on the **log** (`log:///`), not entries (§open-fold) — FOLD collapses a log row to its path, OPEN restores its body. Aimed at an entry scheme they return 501.
1426
1460
  - **SEND[410]** deletes as a side-effect (not the model idiom; §move): with `#fragment`, that channel only; without, the whole entry. **SEND[499]** is owned by the streaming scheme that holds the subscription.
1427
1461
  - **File scheme** reads disk content with mimetype detected via `Mimetypes.detect({ path })` (plumbed through `PlurnkSchemeContext.mimetypes`). Binary mimetypes → 415 on READ and EDIT.