@plurnk/plurnk-service 0.6.0 → 0.8.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 (258) hide show
  1. package/SPEC.md +125 -74
  2. package/bin/plurnk-service.ts +132 -0
  3. package/dist/Paths.d.ts +8 -0
  4. package/dist/Paths.d.ts.map +1 -0
  5. package/dist/Paths.js +47 -0
  6. package/dist/Paths.js.map +1 -0
  7. package/dist/content/index.d.ts +9 -0
  8. package/dist/content/index.d.ts.map +1 -0
  9. package/dist/content/index.js +10 -0
  10. package/dist/content/index.js.map +1 -0
  11. package/dist/content/line-marker.d.ts +26 -0
  12. package/dist/content/line-marker.d.ts.map +1 -0
  13. package/dist/content/line-marker.js +323 -0
  14. package/dist/content/line-marker.js.map +1 -0
  15. package/dist/content/matcher.d.ts +15 -0
  16. package/dist/content/matcher.d.ts.map +1 -0
  17. package/dist/content/matcher.js +112 -0
  18. package/dist/content/matcher.js.map +1 -0
  19. package/dist/content/mimetype-binary.d.ts +9 -0
  20. package/dist/content/mimetype-binary.d.ts.map +1 -0
  21. package/dist/content/mimetype-binary.js +86 -0
  22. package/dist/content/mimetype-binary.js.map +1 -0
  23. package/dist/content/path-mimetype.d.ts +6 -0
  24. package/dist/content/path-mimetype.d.ts.map +1 -0
  25. package/dist/content/path-mimetype.js +49 -0
  26. package/dist/content/path-mimetype.js.map +1 -0
  27. package/dist/content/read-resolve.d.ts +20 -0
  28. package/dist/content/read-resolve.d.ts.map +1 -0
  29. package/dist/content/read-resolve.js +60 -0
  30. package/dist/content/read-resolve.js.map +1 -0
  31. package/dist/core/ChannelWrite.d.ts +33 -30
  32. package/dist/core/ChannelWrite.d.ts.map +1 -1
  33. package/dist/core/ChannelWrite.js +43 -41
  34. package/dist/core/ChannelWrite.js.map +1 -1
  35. package/dist/core/Engine.d.ts +14 -10
  36. package/dist/core/Engine.d.ts.map +1 -1
  37. package/dist/core/Engine.js +272 -94
  38. package/dist/core/Engine.js.map +1 -1
  39. package/dist/core/EnvFlags.d.ts +6 -3
  40. package/dist/core/EnvFlags.d.ts.map +1 -1
  41. package/dist/core/EnvFlags.js +62 -60
  42. package/dist/core/EnvFlags.js.map +1 -1
  43. package/dist/core/PluginLoader.d.ts +6 -3
  44. package/dist/core/PluginLoader.d.ts.map +1 -1
  45. package/dist/core/PluginLoader.js +77 -73
  46. package/dist/core/PluginLoader.js.map +1 -1
  47. package/dist/core/ProviderInstantiate.d.ts +4 -2
  48. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  49. package/dist/core/ProviderInstantiate.js +23 -22
  50. package/dist/core/ProviderInstantiate.js.map +1 -1
  51. package/dist/core/SchemeRegistry.d.ts +1 -1
  52. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  53. package/dist/core/SchemeRegistry.js +3 -3
  54. package/dist/core/SchemeRegistry.js.map +1 -1
  55. package/dist/core/git-membership.d.ts +8 -0
  56. package/dist/core/git-membership.d.ts.map +1 -0
  57. package/dist/core/git-membership.js +125 -0
  58. package/dist/core/git-membership.js.map +1 -0
  59. package/dist/core/packet-wire.d.ts +47 -6
  60. package/dist/core/packet-wire.d.ts.map +1 -1
  61. package/dist/core/packet-wire.js +376 -312
  62. package/dist/core/packet-wire.js.map +1 -1
  63. package/dist/core/resolveForLoop.d.ts +14 -0
  64. package/dist/core/resolveForLoop.d.ts.map +1 -0
  65. package/dist/core/resolveForLoop.js +34 -0
  66. package/dist/core/resolveForLoop.js.map +1 -0
  67. package/dist/core/results.d.ts +40 -0
  68. package/dist/core/results.d.ts.map +1 -0
  69. package/dist/core/results.js +52 -0
  70. package/dist/core/results.js.map +1 -0
  71. package/dist/core/scheme-types.d.ts +6 -3
  72. package/dist/core/scheme-types.d.ts.map +1 -1
  73. package/dist/core/scheme-types.js +4 -4
  74. package/dist/core/scheme-types.js.map +1 -1
  75. package/dist/core/types.d.ts +27 -0
  76. package/dist/core/types.d.ts.map +1 -0
  77. package/dist/core/types.js +17 -0
  78. package/dist/core/types.js.map +1 -0
  79. package/dist/index.d.ts +1 -6
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4 -43
  82. package/dist/index.js.map +1 -1
  83. package/dist/schemes/Exec.d.ts +1 -0
  84. package/dist/schemes/Exec.d.ts.map +1 -1
  85. package/dist/schemes/Exec.js +101 -94
  86. package/dist/schemes/Exec.js.map +1 -1
  87. package/dist/schemes/File.d.ts +0 -1
  88. package/dist/schemes/File.d.ts.map +1 -1
  89. package/dist/schemes/File.js +32 -66
  90. package/dist/schemes/File.js.map +1 -1
  91. package/dist/schemes/Known.d.ts.map +1 -1
  92. package/dist/schemes/Known.js +13 -13
  93. package/dist/schemes/Known.js.map +1 -1
  94. package/dist/schemes/Log.d.ts.map +1 -1
  95. package/dist/schemes/Log.js +8 -52
  96. package/dist/schemes/Log.js.map +1 -1
  97. package/dist/schemes/Plurnk.d.ts.map +1 -1
  98. package/dist/schemes/Plurnk.js +13 -13
  99. package/dist/schemes/Plurnk.js.map +1 -1
  100. package/dist/schemes/Skill.d.ts.map +1 -1
  101. package/dist/schemes/Skill.js +13 -13
  102. package/dist/schemes/Skill.js.map +1 -1
  103. package/dist/schemes/Unknown.d.ts.map +1 -1
  104. package/dist/schemes/Unknown.js +13 -13
  105. package/dist/schemes/Unknown.js.map +1 -1
  106. package/dist/schemes/_entry-crud.d.ts +5 -3
  107. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  108. package/dist/schemes/_entry-crud.js +55 -50
  109. package/dist/schemes/_entry-crud.js.map +1 -1
  110. package/dist/schemes/_entry-find.d.ts +10 -3
  111. package/dist/schemes/_entry-find.d.ts.map +1 -1
  112. package/dist/schemes/_entry-find.js +99 -77
  113. package/dist/schemes/_entry-find.js.map +1 -1
  114. package/dist/schemes/_entry-manifest.d.ts +6 -0
  115. package/dist/schemes/_entry-manifest.d.ts.map +1 -0
  116. package/dist/schemes/_entry-manifest.js +45 -0
  117. package/dist/schemes/_entry-manifest.js.map +1 -0
  118. package/dist/schemes/_entry-ops.d.ts +7 -4
  119. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  120. package/dist/schemes/_entry-ops.js +198 -316
  121. package/dist/schemes/_entry-ops.js.map +1 -1
  122. package/dist/schemes/_entry-send.d.ts +4 -1
  123. package/dist/schemes/_entry-send.d.ts.map +1 -1
  124. package/dist/schemes/_entry-send.js +57 -55
  125. package/dist/schemes/_entry-send.js.map +1 -1
  126. package/dist/server/ClientConnection.js +3 -3
  127. package/dist/server/ClientConnection.js.map +1 -1
  128. package/dist/server/Daemon.d.ts +5 -5
  129. package/dist/server/Daemon.d.ts.map +1 -1
  130. package/dist/server/Daemon.js +221 -176
  131. package/dist/server/Daemon.js.map +1 -1
  132. package/dist/server/clientTurn.d.ts +4 -1
  133. package/dist/server/clientTurn.d.ts.map +1 -1
  134. package/dist/server/clientTurn.js +19 -17
  135. package/dist/server/clientTurn.js.map +1 -1
  136. package/dist/server/dsl.d.ts +19 -16
  137. package/dist/server/dsl.d.ts.map +1 -1
  138. package/dist/server/dsl.js +127 -105
  139. package/dist/server/dsl.js.map +1 -1
  140. package/dist/server/envelope.d.ts +22 -19
  141. package/dist/server/envelope.d.ts.map +1 -1
  142. package/dist/server/envelope.js +116 -102
  143. package/dist/server/envelope.js.map +1 -1
  144. package/dist/server/logEntry.d.ts +4 -1
  145. package/dist/server/logEntry.d.ts.map +1 -1
  146. package/dist/server/logEntry.js +41 -39
  147. package/dist/server/logEntry.js.map +1 -1
  148. package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
  149. package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
  150. package/dist/server/methods/_dispatchAsClient.js +31 -29
  151. package/dist/server/methods/_dispatchAsClient.js.map +1 -1
  152. package/dist/server/methods/discover.d.ts +3 -1
  153. package/dist/server/methods/discover.d.ts.map +1 -1
  154. package/dist/server/methods/discover.js +8 -6
  155. package/dist/server/methods/discover.js.map +1 -1
  156. package/dist/server/methods/entry_read.d.ts +4 -1
  157. package/dist/server/methods/entry_read.d.ts.map +1 -1
  158. package/dist/server/methods/entry_read.js +54 -52
  159. package/dist/server/methods/entry_read.js.map +1 -1
  160. package/dist/server/methods/log_read.d.ts +4 -1
  161. package/dist/server/methods/log_read.d.ts.map +1 -1
  162. package/dist/server/methods/log_read.js +38 -36
  163. package/dist/server/methods/log_read.js.map +1 -1
  164. package/dist/server/methods/loop_cancel.d.ts +3 -1
  165. package/dist/server/methods/loop_cancel.d.ts.map +1 -1
  166. package/dist/server/methods/loop_cancel.js +19 -17
  167. package/dist/server/methods/loop_cancel.js.map +1 -1
  168. package/dist/server/methods/loop_resolve.d.ts +3 -1
  169. package/dist/server/methods/loop_resolve.d.ts.map +1 -1
  170. package/dist/server/methods/loop_resolve.js +42 -40
  171. package/dist/server/methods/loop_resolve.js.map +1 -1
  172. package/dist/server/methods/loop_run.d.ts +3 -1
  173. package/dist/server/methods/loop_run.d.ts.map +1 -1
  174. package/dist/server/methods/loop_run.js +104 -102
  175. package/dist/server/methods/loop_run.js.map +1 -1
  176. package/dist/server/methods/op_copy.d.ts +3 -1
  177. package/dist/server/methods/op_copy.d.ts.map +1 -1
  178. package/dist/server/methods/op_copy.js +25 -23
  179. package/dist/server/methods/op_copy.js.map +1 -1
  180. package/dist/server/methods/op_dispatch.d.ts +3 -1
  181. package/dist/server/methods/op_dispatch.d.ts.map +1 -1
  182. package/dist/server/methods/op_dispatch.js +18 -16
  183. package/dist/server/methods/op_dispatch.js.map +1 -1
  184. package/dist/server/methods/op_edit.d.ts +3 -1
  185. package/dist/server/methods/op_edit.d.ts.map +1 -1
  186. package/dist/server/methods/op_edit.js +23 -21
  187. package/dist/server/methods/op_edit.js.map +1 -1
  188. package/dist/server/methods/op_exec.d.ts +3 -1
  189. package/dist/server/methods/op_exec.d.ts.map +1 -1
  190. package/dist/server/methods/op_exec.js +20 -18
  191. package/dist/server/methods/op_exec.js.map +1 -1
  192. package/dist/server/methods/op_find.d.ts +3 -1
  193. package/dist/server/methods/op_find.d.ts.map +1 -1
  194. package/dist/server/methods/op_find.js +23 -21
  195. package/dist/server/methods/op_find.js.map +1 -1
  196. package/dist/server/methods/op_hide.d.ts +3 -1
  197. package/dist/server/methods/op_hide.d.ts.map +1 -1
  198. package/dist/server/methods/op_hide.js +23 -21
  199. package/dist/server/methods/op_hide.js.map +1 -1
  200. package/dist/server/methods/op_move.d.ts +3 -1
  201. package/dist/server/methods/op_move.d.ts.map +1 -1
  202. package/dist/server/methods/op_move.js +23 -21
  203. package/dist/server/methods/op_move.js.map +1 -1
  204. package/dist/server/methods/op_parse.d.ts +3 -1
  205. package/dist/server/methods/op_parse.d.ts.map +1 -1
  206. package/dist/server/methods/op_parse.js +24 -22
  207. package/dist/server/methods/op_parse.js.map +1 -1
  208. package/dist/server/methods/op_read.d.ts +3 -1
  209. package/dist/server/methods/op_read.d.ts.map +1 -1
  210. package/dist/server/methods/op_read.js +23 -21
  211. package/dist/server/methods/op_read.js.map +1 -1
  212. package/dist/server/methods/op_send.d.ts +3 -1
  213. package/dist/server/methods/op_send.d.ts.map +1 -1
  214. package/dist/server/methods/op_send.js +22 -20
  215. package/dist/server/methods/op_send.js.map +1 -1
  216. package/dist/server/methods/op_show.d.ts +3 -1
  217. package/dist/server/methods/op_show.d.ts.map +1 -1
  218. package/dist/server/methods/op_show.js +23 -21
  219. package/dist/server/methods/op_show.js.map +1 -1
  220. package/dist/server/methods/ping.d.ts +3 -1
  221. package/dist/server/methods/ping.d.ts.map +1 -1
  222. package/dist/server/methods/ping.js +8 -6
  223. package/dist/server/methods/ping.js.map +1 -1
  224. package/dist/server/methods/providers_list.d.ts +3 -1
  225. package/dist/server/methods/providers_list.d.ts.map +1 -1
  226. package/dist/server/methods/providers_list.js +19 -17
  227. package/dist/server/methods/providers_list.js.map +1 -1
  228. package/dist/server/methods/session_attach.d.ts +3 -1
  229. package/dist/server/methods/session_attach.d.ts.map +1 -1
  230. package/dist/server/methods/session_attach.js +43 -41
  231. package/dist/server/methods/session_attach.js.map +1 -1
  232. package/dist/server/methods/session_create.d.ts +3 -1
  233. package/dist/server/methods/session_create.d.ts.map +1 -1
  234. package/dist/server/methods/session_create.js +51 -49
  235. package/dist/server/methods/session_create.js.map +1 -1
  236. package/dist/server/methods/session_list.d.ts +3 -1
  237. package/dist/server/methods/session_list.d.ts.map +1 -1
  238. package/dist/server/methods/session_list.js +9 -7
  239. package/dist/server/methods/session_list.js.map +1 -1
  240. package/dist/server/methods/session_runs.d.ts +3 -1
  241. package/dist/server/methods/session_runs.d.ts.map +1 -1
  242. package/dist/server/methods/session_runs.js +19 -17
  243. package/dist/server/methods/session_runs.js.map +1 -1
  244. package/dist/server/methods/session_set_persona.d.ts +3 -1
  245. package/dist/server/methods/session_set_persona.d.ts.map +1 -1
  246. package/dist/server/methods/session_set_persona.js +28 -26
  247. package/dist/server/methods/session_set_persona.js.map +1 -1
  248. package/dist/server/methods/session_set_root.d.ts +3 -1
  249. package/dist/server/methods/session_set_root.d.ts.map +1 -1
  250. package/dist/server/methods/session_set_root.js +31 -29
  251. package/dist/server/methods/session_set_root.js.map +1 -1
  252. package/dist/server/yolo.d.ts +3 -1
  253. package/dist/server/yolo.d.ts.map +1 -1
  254. package/dist/server/yolo.js +15 -13
  255. package/dist/server/yolo.js.map +1 -1
  256. package/package.json +77 -32
  257. package/bin/plurnk-service.js +0 -112
  258. /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
package/SPEC.md CHANGED
@@ -90,6 +90,27 @@ Three independent axes on entries and channels. Confusion across them is a recur
90
90
 
91
91
  ## §1 Architecture
92
92
 
93
+ ### §1.1 Ecosystem
94
+
95
+ The plurnk project is a modular monorepo-of-repos in the `@plurnk/*` npm namespace. Each repo has one published package and one agent who owns it; cross-repo coordination happens through issues, not shared code. This service sits in the middle of that ecosystem and is its **runtime substrate** — the daemon other repos plug into.
96
+
97
+ Dependency direction (from root to leaf):
98
+
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
+ - **Framework siblings** consume grammar and define their own author-facing contracts:
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}`, …
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
+ - `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 (`bin/plurnk-service.js` over WebSocket + JSON-RPC), and projects packets to the wire per `Packet.json`. Most of the substantive runtime work lives here.
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
+
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.
109
+
110
+ **This service's central role:** sole consumer of every author-facing framework contract (one set of integrations across the ecosystem), sole producer of the engine's runtime behavior (one canonical implementation of dispatch, log, packet wire), and sole orchestrator of cross-scheme operations (COPY/MOVE flow through engine-mediated `readEntry` / `writeEntry` / `deleteEntry`, never scheme-to-scheme). Most cross-repo coordination flows through us — we file the consumer-need issues at upstream repos, adopt their decisions, document the surface in SPEC.
111
+
112
+ ### §1.2 In-process architecture
113
+
93
114
  Engine library + admin CLI + daemon. Four plug points:
94
115
 
95
116
  - **Providers** (§2) — LLM transports. Engine sends a turn's messages, receives raw content + usage; engine parses the content into `PlurnkStatement[]`.
@@ -113,23 +134,23 @@ Author-facing contract: [plurnk-providers#1](https://github.com/plurnk/plurnk-pr
113
134
 
114
135
  Three entry points:
115
136
 
116
- - `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`.
117
- - `provider.countTokens(text)` — synchronous, called at write-time (§14.2) and render-time. Non-negative integer.
118
- - `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`.
137
+ - `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`. {§2.1-generate}
138
+ - `provider.countTokens(text)` — synchronous, called at write-time (§14.2) and render-time. Non-negative integer. {§2.1-counttokens}
139
+ - `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`. {§2.1-costfor}
119
140
 
120
- Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info") and `provider.model`.
141
+ Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info"), read by the budget {§2.1-identity}; and `provider.model` — the instance identity the deferred model-switch recompute compares (§14.2-hot-switch-recompute), exposed but not yet consumed here.
121
142
 
122
143
  ### §2.2 Engine → provider guarantees
123
144
 
124
145
  - `messages` is a complete prompt (`system_definition`, `persona`, `index`, `log`, `prompt`, `telemetry`, `system_requirements` pre-assembled). Provider does not reorder.
125
- - `signal` is wired to the run's AbortController.
126
- - `generate` is single-call per turn. No parallel calls on the same instance.
127
- - `assistantRaw` is opaque to the engine (forensics-only).
146
+ - `signal` is wired to the run's AbortController. {§2.2-signal-wired}
147
+ - `generate` is single-call per turn. No parallel calls on the same instance. {§2.2-single-call}
148
+ - `assistantRaw` is opaque to the engine (forensics-only). {§2.2-assistantraw-opaque}
128
149
  - `countTokens` is cheap by contract; engine calls frequently.
129
150
 
130
151
  ### §2.3 Provider instantiation
131
152
 
132
- Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
153
+ Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). {§2.3-alias-resolution} Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
133
154
 
134
155
  ```
135
156
  PLURNK_MODEL_gemma=openai/macher.gguf
@@ -139,9 +160,9 @@ PLURNK_MODEL=gemma
139
160
 
140
161
  First path segment = provider plugin; rest = provider's own model id.
141
162
 
142
- ### §2.4 In-tree Mock provider
163
+ ### §2.4 Mock provider (sibling fixture)
143
164
 
144
- `Mock` (exported from `@plurnk/plurnk-providers`) — intg fixture + reference implementation. `{ contextSize, responses }` constructor; `generate` shifts from the queue. `MockResponse.assistant.ops?: PlurnkStatement[]` is a pre-parsed escape hatch the engine consumes directly when present; production providers don't expose this.
165
+ `Mock` (exported from `@plurnk/plurnk-providers`) — intg fixture + reference implementation. `{ contextSize, responses }` constructor; `generate` shifts from the queue. `MockResponse.assistant.ops?: PlurnkStatement[]` is a pre-parsed escape hatch the engine consumes directly when present; production providers don't expose this. {§2.4-mock-fixture}
145
166
 
146
167
  ---
147
168
 
@@ -151,15 +172,15 @@ Author-facing contract: [plurnk-schemes#1](https://github.com/plurnk/plurnk-sche
151
172
 
152
173
  ### §3.1 Manifest
153
174
 
154
- Per author contract. Each scheme declares a `static manifest: SchemeManifest` with `name`, `channels`, `defaultChannel`, `category`, `scope`, `writableBy`, `volatile`, `modelVisible`, optional `flags`. Identity match enforced at plugin load: `manifest.name` must equal `package.json#plurnk.name`.
175
+ Per author contract. Each scheme declares a `static manifest: SchemeManifest` with `name`, `channels`, `defaultChannel`, `category`, `scope`, `writableBy`, `volatile`, `modelVisible`, optional `flags`. {§3.1-manifest} Identity match enforced at plugin load: `manifest.name` must equal `package.json#plurnk.name`.
155
176
 
156
177
  ### §3.2 CRUD primitives
157
178
 
158
- Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives cross-scheme COPY/MOVE/SEND[410] through these. Each method is one SQL transaction; engine owns the outer transaction for orchestrations.
179
+ Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives cross-scheme COPY/MOVE/SEND[410] through these — the orchestration and its 404/409/415 semantics are anchored under §6.4/§6.5. Each method is one SQL transaction; engine owns the outer transaction for orchestrations.
159
180
 
160
181
  ### §3.3 Op methods
161
182
 
162
- Per author contract (`edit`/`read`/`show`/`hide`/`find`/`send`/`exec?`). Engine dispatches by `PlurnkStatement.op`. COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives.
183
+ Per author contract (`edit`/`read`/`show`/`hide`/`find`/`send`/`exec?`). Engine dispatches by `PlurnkStatement.op`. {§3.3-op-dispatch} COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives (§6.4/§6.5).
163
184
 
164
185
  ### §3.4 Cross-scheme orchestration
165
186
 
@@ -179,17 +200,17 @@ move(source_path, dest_path, signal_tags, ctx):
179
200
  src_scheme.deleteEntry(source_pathname, ctx)
180
201
  ```
181
202
 
182
- Same- and cross-scheme operations share the orchestrator. Same-scheme COPY is not a special case.
203
+ Same- and cross-scheme operations share the orchestrator. Same-scheme COPY is not a special case. Orchestration behavior — 404/409/415, `move` = `copy` + `deleteEntry` — is anchored under §6.4/§6.5.
183
204
 
184
205
  ### §3.5 SEND dispatch (status-code-as-verb)
185
206
 
186
207
  Directed SEND (non-null path) routes to scheme's `send`. Status = intent:
187
208
 
188
209
  - `SEND[200](path)` — write body into resource (WS message, exec stdin).
189
- - `SEND[410](path)` — delete the resource. {§3.5-410-deletes-resource}
190
- - `SEND[410](path#fragment)` — delete the named channel only. {§3.5-410-fragment-channel-delete}
191
210
  - `SEND[499](path)` — cancel active subscription (§7).
192
211
 
212
+ `SEND[410](path[#fragment])` also deletes the target entry/channel — an implemented side-effect, NOT taught to the model and with no live/demo surface. The model-facing delete idiom is MOVE to `/dev/null` (§6.5).
213
+
193
214
  Other status codes return 501 from entry-bearing schemes by default. {§3.5-entry-schemes-501-on-non-410}
194
215
 
195
216
  Null-path SEND is broadcast (§6.7), engine-handled.
@@ -219,11 +240,11 @@ Engine → scheme guarantees:
219
240
 
220
241
  - `ctx` is fresh per call. No mutation across calls.
221
242
  - `ctx.writer` reflects the actual writer at this dispatch.
222
- - `manifest.writableBy` checked BEFORE invocation; engine returns 403 directly on exclusion.
223
- - `ctx.signal` is wired to the run's AbortController.
224
- - Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `packet.user.telemetry.errors[]` (§15.1).
243
+ - `manifest.writableBy` checked BEFORE invocation; engine returns 403 directly on exclusion. {§3.6-writableby-403}
244
+ - `ctx.signal` is wired to the run's AbortController (§2.2-signal-wired).
245
+ - Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `packet.user.telemetry.errors[]` (§15.1). {§3.6-exception-500}
225
246
 
226
- **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`. Raw DB writes bypass tokenization — out of API scope.
247
+ **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` (§14.2-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
227
248
 
228
249
  ---
229
250
 
@@ -392,16 +413,17 @@ Per-op semantics. AST shapes from `@plurnk/plurnk-grammar`'s `PlurnkStatement`.
392
413
  AST: `{ op: "EDIT", target, body: string | null, signal: tags | null, lineMarker? }`.
393
414
 
394
415
  - Resolves target channel from fragment (§5.5); unknown channel → 400; undeclared in manifest → engine crash (§5.3).
395
- - Writes body; `body: null` clears.
396
- - Sets `indexed=1` for written channel in current run.
397
- - Returns `{ status: 201, entryId }` for new entries; `{ status: 200, entryId }` for updates.
398
- - Tags from `signal[]` apply additively via `entry_tags` (scheme may vary).
416
+ - Writes body; `body: null` clears. {§6.1-null-clears}
417
+ - Sets `indexed=1` for written channel in current run. {§6.1-indexed}
418
+ - Returns `{ status: 201, entryId }` for new entries; `{ status: 200, entryId }` for content updates. {§6.1-status-201-200}
419
+ - A write that changes nothing identical content and no new tag — returns `{ status: 304, entryId }`, mirroring SHOW/HIDE's no-op (§6.3). {§6.1-noop-304}
420
+ - Tags from `signal[]` apply additively via `entry_tags` (scheme may vary). {§6.1-tags-additive}
399
421
 
400
422
  ### §6.2 READ
401
423
 
402
424
  AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineMarker? }`.
403
425
 
404
- - Returns channel content + mimetype, or 404.
426
+ - Returns channel content + mimetype {§6.2-read-content}, or 404 {§6.2-read-404}.
405
427
  - `lineMarker` slices per §16.3.
406
428
  - `body` matcher dispatches through `Mimetypes.query` per §16.1 (all four dialects wired).
407
429
 
@@ -410,7 +432,7 @@ AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineM
410
432
  AST: `{ op: "SHOW"|"HIDE", target, body: MatcherBody | null, signal: tags | null, lineMarker? }`.
411
433
 
412
434
  - Flips `visibility.indexed` for the targeted channel(s) per §5.5 rules.
413
- - Returns 200 on transition, 304 on no-op, 404 if entry absent.
435
+ - Returns 200 on transition {§6.3-flip-200}, 304 on no-op {§6.3-noop-304}, 404 if entry absent {§6.3-absent-404}.
414
436
 
415
437
  ### §6.4 COPY (engine-orchestrated)
416
438
 
@@ -419,7 +441,7 @@ AST: `{ op: "COPY", target (source), body (destination), signal: tags | null, li
419
441
  Engine orchestrates over CRUD primitives (§3.2, §3.4):
420
442
 
421
443
  1. `src_scheme.readEntry` → 404 if missing. {§6.4-missing-source-404}
422
- 2. `dst_scheme.readEntry` → if exists, 409 (no overwrite). {§6.4-conflict-409}
444
+ 2. `dst_scheme.readEntry` → conflict verdict, deferred until the written content is known (step 5): exists with identical content + tags → 304 (no-op, mirrors EDIT §6.1) {§6.4-noop-304}; exists with different content → 409 (no overwrite) {§6.4-conflict-409}; absent → proceed.
423
445
  3. Mimetype compat — channels' mimetypes must be accepted by `dst_scheme.manifest.channels`. Mismatch → 415.
424
446
  4. Tags: `signal` non-null replaces source tags {§6.4-signal-replaces-source-tags}; null/empty carries source tags {§6.4-no-signal-carries-source-tags}.
425
447
  5. `dst_scheme.writeEntry({channels, tags})`.
@@ -432,7 +454,7 @@ Returns 201 on success. Same- and cross-scheme COPY share the orchestrator. {§6
432
454
  AST: `{ op: "MOVE", target (source), body: dest | null, signal: tags | null, lineMarker? }`.
433
455
 
434
456
  - **Relocation** (`body` non-null): COPY (§6.4) + `src_scheme.deleteEntry` in one transaction. 201 on success. {§6.5-relocation-deletes-source} Cross-scheme same as same-scheme. {§6.5-cross-scheme-move}
435
- - **Deletion** (`body: null`): `src_scheme.deleteEntry` directly. {§6.5-null-body-deletes} 200 on success, 404 if absent. {§6.5-missing-source-404}
457
+ - **Deletion** (`body: null`, or `body` = `/dev/null`): `src_scheme.deleteEntry` directly. {§6.5-null-body-deletes} 200 on success, 404 if absent. {§6.5-missing-source-404} `/dev/null` is the model-facing delete idiom the grammar teaches; a null body is its programmatic equivalent. {§6.5-dev-null-deletes}
436
458
 
437
459
  Log history preserved — `log_entries` stores path tuple as text, not FK to `entries.id`.
438
460
 
@@ -441,7 +463,7 @@ Log history preserved — `log_entries` stores path tuple as text, not FK to `en
441
463
  AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal: tags | null, lineMarker? }`.
442
464
 
443
465
  - Filters entries within scope (scheme + pathname prefix). {§6.6-scope-prefix-filter}
444
- - `body` matcher operates on pathname (glob). {§6.6-glob-filter-on-pathname} Content matchers (xpath/jsonpath/regex over body) live on READ.
466
+ - `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. {§6.6-glob-filter-on-content}
445
467
  - `signal` is a tag filter; entries match if they have ALL listed tags. {§6.6-tag-filter-and-semantics}
446
468
  - Session + scheme scoped — no cross-session/cross-scheme leakage. {§6.6-scoped-isolation}
447
469
  - Returns `{ status: 200, results: string }` (newline-separated matching paths, `text/plain`).
@@ -610,7 +632,7 @@ Plugin discovery (§9) registers whatever's in `node_modules/@plurnk/*`.
610
632
  - Render budget per channel — `PLURNK_ENTRY_SIZE_DEFAULT_TOKENS` (§12); tokenization per §14.2.
611
633
  - Backpressure caps — none (§7.8).
612
634
  - Stream cancel — `SEND[499]` (§7.7).
613
- - Delete — `SEND[410]` (§3.5, §6.5).
635
+ - Delete — MOVE to `/dev/null` (§6.5); `SEND[410]` also deletes as a side-effect (§3.5).
614
636
  - Per-loop flags — `loops.flags` JSON column; `yolo` end-to-end today, others scheduled.
615
637
  - Default-channel wire rendering — §5.5.
616
638
 
@@ -690,7 +712,7 @@ registry.register("loop.run", {
690
712
  - `description`: one-liner surfaced by `discover`.
691
713
  - `params`: `"type — meaning"` per param; `?` suffix = optional. Self-documenting, not enforced.
692
714
  - `requiresInit`: rejects until a session is attached.
693
- - `longRunning`: exempt from `PLURNK_RPC_TIMEOUT`.
715
+ - `longRunning`: exempt from `PLURNK_RPC_TIMEOUT`. {§13.3-register}
694
716
 
695
717
  ### §13.4 Discovery
696
718
 
@@ -716,7 +738,7 @@ registry.register("loop.run", {
716
738
  }
717
739
  ```
718
740
 
719
- `capabilities` lists registered plug-ins by `(kind, name)`. Cold clients call `discover` first. No hardcoded method names or capability lists in any client.
741
+ `capabilities` lists registered plug-ins by `(kind, name)`. Cold clients call `discover` first. No hardcoded method names or capability lists in any client. {§13.4-discover}
720
742
 
721
743
  ### §13.5 Core method set
722
744
 
@@ -733,18 +755,18 @@ registry.register("loop.run", {
733
755
  |------------------------|---------------------|-------------------|-------|
734
756
  | `session.create` | `name?: string`, `projectRoot?: string`, `persona?: string` | `{ id, name, projectRoot, persona }` | Creates new session; auto-name if unprovided. Optional `projectRoot` pins the workspace (null/omitted = headless). Optional `persona` sets the session-level persona override. |
735
757
  | `session.list` | none | `{ sessions: Session[] }` | Lists all sessions. |
736
- | `session.attach` | `id: number`, `runId?: number`, `runName?: string`, `persona?: string` | `{ id, name, runId, runName }` | Binds this connection to an existing session. Optional `runId` resumes that specific run (must belong to the session). Optional `runName` reuses-or-creates by name within the session. Both omitted → new auto-named run. Optional `persona` sets run-level persona only when a NEW run is created. |
758
+ | `session.attach` | `id: number`, `runId?: number`, `runName?: string`, `persona?: string` | `{ id, name, runId, runName }` | Binds this connection to an existing session. Optional `runId` resumes that specific run (must belong to the session). Optional `runName` reuses-or-creates by name within the session. Both omitted → new auto-named run. Optional `persona` sets run-level persona only when a NEW run is created. {§13.5-session-attach} |
737
759
  | `session.runs` | `id?: number` | `{ runs: Run[] }` | Lists runs in a session (defaults to attached session); most-recent first. |
738
760
  | `session.set_root` | `projectRoot: string \| null` | `{ projectRoot }` | Update the workspace pointer on the attached session. Null reverts to headless. |
739
761
  | `session.set_persona` | `persona: string \| null` | `{ persona }` | Update the session-level persona. Null clears the override (falls through to PLURNK_PERSONA file). |
740
762
 
741
- **Auto-envelope.** Clients calling a `requiresInit: true` method without first attaching get auto-created session → run → client loop. Records persist normally; auto-created ≠ auto-deleted. Cleanup is a future `session.delete` / `session.archive` endpoint.
763
+ **Auto-envelope.** Clients calling a `requiresInit: true` method without first attaching get auto-created session → run → client loop. Records persist normally; auto-created ≠ auto-deleted. Cleanup is a future `session.delete` / `session.archive` endpoint. {§13.5-auto-envelope}
742
764
 
743
765
  **Loops (model-driven)**
744
766
 
745
767
  | Method | Params | Result | Notes |
746
768
  |-------------------|-------------------------------------|------------------------|-------|
747
- | `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags`, `persona?: string` | `{ loopId, turnIds, finalStatus, hitMaxTurns, reason }` | Model-driven loop. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (currently `{yolo?: boolean}`; more arrive as wired — see §0.5). Optional `persona` sets the loop-level persona (highest precedence in the cascade). Streams `log/entry` and `loop/proposal` notifications during. `longRunning: true`. |
769
+ | `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags`, `persona?: string` | `{ loopId, turnIds, finalStatus, hitMaxTurns, reason }` | Model-driven loop. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (currently `{yolo?: boolean}`; more arrive as wired — see §0.5). Optional `persona` sets the loop-level persona (highest precedence in the cascade). Streams `log/entry` and `loop/proposal` notifications during. `longRunning: true`. {§13.5-loop-run} |
748
770
  | `loop.resolve` | `logEntryId: number`, `decision: "accept" \| "reject" \| "cancel"`, `body?: string`, `outcome?: string` | `{ status, logEntryId }` | Resolve a pending proposal (status=202 log entry). Engine.dispatch unpauses on resolution. |
749
771
  | `providers.list` | none | `{ aliases: ProviderAlias[] }` | Lists configured `PLURNK_MODEL_<alias>` entries with `{alias, provider, model, active}`. Clients use to populate model-selection UI. |
750
772
 
@@ -752,12 +774,12 @@ registry.register("loop.run", {
752
774
 
753
775
  | Method | Params | Result | Notes |
754
776
  |---------------|-------------------------------------|------------------------|-------|
755
- | `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. |
756
- | `log.read` | `loopId?: number`, … | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. |
777
+ | `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. {§13.5-entry-read} |
778
+ | `log.read` | `loopId?: number`, … | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. {§13.5-log-read} |
757
779
 
758
780
  **DSL operations (client-driven, mirror grammar)**
759
781
 
760
- Per the **Speak in DSL, not plumbing** rule (AGENTS.md): `op.*` methods construct DSL statements internally and dispatch through `Engine.dispatch`. Param shapes are ergonomic (semantic names, not HEREDOC slots); semantics are the DSL's.
782
+ Per the **Speak in DSL, not plumbing** rule (AGENTS.md): `op.*` methods construct DSL statements internally and dispatch through `Engine.dispatch`. {§13.5-op-mirror} Param shapes are ergonomic (semantic names, not HEREDOC slots); semantics are the DSL's.
761
783
 
762
784
  Each `op.*` call creates a turn in the connection's client loop (§13.7), dispatches, fires `log/entry`, returns the dispatch result.
763
785
 
@@ -787,7 +809,7 @@ Server-initiated events on the same WebSocket.
787
809
 
788
810
  | Notification | Params | When fired |
789
811
  |--------------------|-------------------------------------|------------|
790
- | `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. |
812
+ | `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. {§13.6-log-entry-notify} |
791
813
  | `loop/terminated` | `{ loopId, finalStatus, hitMaxTurns }` | Loop reaches terminal status. |
792
814
  | `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). |
793
815
  | `session/created` | `{ id, name, projectRoot, persona }` | Any client creates a session. |
@@ -853,7 +875,7 @@ Plurnk-specific (`-32000` to `-32099`):
853
875
  | -32006 | Mimetype unavailable |
854
876
  | -32007 | Timeout |
855
877
 
856
- Error responses MAY include `data: {…}` with structured context (404'd path, timed-out method, etc.).
878
+ Error responses MAY include `data: {…}` with structured context (404'd path, timed-out method, etc.). {§13.8-error-codes}
857
879
 
858
880
  ### §13.9 Versioning
859
881
 
@@ -875,43 +897,73 @@ Each entry: question, answer, rationale, migration path.
875
897
 
876
898
  **Migration path.** If a plugin needs to inject a packet section, grow a single `packet.augment` hook called after `#buildIndex`; plugins return system/user augmentation objects merged into the packet. Additive — engine-direct base stays.
877
899
 
878
- ### §14.2 Tokenomics: real provider tokens, stored at write, summed at render
900
+ ### §14.2 Tokenomics: real provider tokens, render-weight budget, per-scheme balance
901
+
902
+ **Question.** How does plurnk track token costs accurately enough to ground the model's SHOW/HIDE/compose decisions? Accuracy is the whole game — a budget that smells wrong is one the model stops trusting and curating against.
903
+
904
+ **Two measures, never conflated:**
879
905
 
880
- **Question.** How does plurnk track token costs accurately enough for model SHOW/HIDE/compose decisions, without re-tokenizing every turn?
906
+ - **render-weight** the tokens the model actually processes this turn (rendered previews + meta + fences). The budget is about this.
907
+ - **content-depth** — an entry's full content size (`entry_channels.tokens`). The manifest's `tokens` is this.
881
908
 
882
- **Decision.** Provider tokenizers authoritative; counts computed at write time and stored; render-time accounting is SQL `SUM` plus tokenization of a small set of wrapper strings. {§14.2-tokenomics-v0}
909
+ **Built.**
883
910
 
884
- Lattice:
911
+ - **Provider tokens, stored at write.** `provider.countTokens` is the source of truth; `entry_channels.tokens` (via `_entry-crud`) and `log_entries.tokens` (via `Engine.#writeLog`) are populated at write as a write-time snapshot. A `ceil(len/DIVISOR)` fallback (the divisor tripwire) applies only when no provider tokenizer is wired. {§14.2-tokens-stored-at-write}
912
+ - **Render-weight budget.** The budget headline — `ceiling`, `tokenUsage`, `tokensFree` — is measured from the *assembled packet* (placeholders substituted after measuring), so it reflects what the model actually receives. A `SUM` of stored content-depth would mis-price previews; render-weight is the accurate measure. {§14.2-render-weight-budget}
913
+ - **Per-scheme balance.** A markdown table groups the model's context by scheme — `indexed`/`archived` counts and render-weight `tokens` — anchored `repo, known, unknown, log`, tail sorted by tokens. The model sees at a glance what's eating its window. {§14.2-per-scheme-balance}
914
+ - **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. {§14.2-context-percent}
915
+ - **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. {§14.2-depth-render-fresh}
916
+ - **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. {§14.2-over-budget-floor}
885
917
 
886
- - **`provider.countTokens` is the source of truth.** No chars/DIVISOR approximation. Per-provider tokenizer (§2) is the foundation.
887
- - **Stored at write time.** `entry_channels.tokens` populated by `_entry-crud.ts` on INSERT/UPDATE. `log_entries.tokens` populated by `Engine.#writeLog`. Write helper IS the contract sister modules see.
888
- - **Render-time SUM.** `Engine.#buildIndex` adds per-scheme `SUM(tokens)`. Only wrapper strings re-tokenize each render — bounded.
889
- - **Hot model switch is a feature.** Session model change walks `entry_channels` + `log_entries` and recomputes against new tokenizer. One-time cost at switch boundary.
890
- - **Budget table self-reference via placeholder substitution.** Render with `{{tokenUsage}}`/`{{tokensFree}}`, measure, substitute. ±1–2 token drift accepted.
891
- - **Telemetry shape.** Per-scheme breakdown table in `packet.user.telemetry.budget` with `tokenCeiling`/`tokenUsage`/`tokensFree` headline; markdown table groups by scheme with indexed-count, archived-count, tokens.
918
+ **Rejected / obviated.**
892
919
 
893
- **Rationale.** Rummy used chars/DIVISOR + compute-at-SELECT because its sync-only SQL function couldn't call provider tokenizers. plurnk's async `countTokens` is potentially expensive; pay once at write, never re-pay at render. Approximation can't ground SHOW/HIDE decisions — the model curating its own context only works if numbers are accurate.
920
+ - **Hot model-switch recompute** *obviated* by render-fresh depth (above). There's no cross-loop cache to recompute: the manifest re-tokenizes at build, the budget always did. A model change between loops can't stale a number nothing caches.
921
+ - **Reasoning-token surfacing** — *rejected* for the model-facing budget: reasoning is *output*, not window-context, and the model can't HIDE it. The thinking-vs-output distinction is cost-forensics (the usage breakdown is stored on every packet), not a curation signal.
894
922
 
895
- **Migration path.** If `countTokens` cost becomes prohibitive, retreat to chars/DIVISOR per provider, async-refined to real count when budget permits. Schema unchanged.
923
+ **Rationale.** Rummy used chars/DIVISOR + compute-at-SELECT only because its sync-only SQL couldn't call a tokenizer. plurnk has real `countTokens`: store content tokens once at write (the depth), measure the small rendered output for the budget (the weight). Approximation can't ground curation — the model only curates against numbers it trusts.
924
+
925
+ **Migration path.** None on cost — SQLite, JS, and a local tokenizer are negligible against the model's token budget, the only thing worth economizing. The fallback divisor is a correctness tripwire (no provider tokenizer wired), not a performance retreat. Schema unchanged.
896
926
 
897
927
  ### §14.3 Workspace identity, membership, disk co-location
898
928
 
899
929
  **Question.** How does plurnk represent the project a session works on? Where does file membership come from? Does writing an entry imply writing to disk?
900
930
 
901
- **Decision lattice.** {§14.3-workspace-phase-f}
931
+ **The boundary is the client's.** The client owns the model's filesystem access in both directions: reads are membership-gated (a file is invisible to the model unless it is a member), and writes are proposals the client accepts or rejects (`yolo` auto-accepts). Writing an entry never implies writing to disk — entries are canonical in the store; disk only moves when the client accepts a side-effecting proposal, and only where `project_root` is set (null = headless, client owns materialization).
932
+
933
+ **Built — git-substrate membership.** {§14.3-git-membership}
934
+
935
+ - **Identity on the session.** No `projects` table; `sessions.project_root TEXT` (nullable = headless). `entries.scope` unchanged (`∈ {'agent','session'}`). Workspace = session; no users/auth/multi-tenant.
936
+ - **git-ls-files membership.** git present → tracked files (`git ls-files`) are members with no explicit `add` — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership).
937
+ - **EMI eager + relevance-bounded.** Materializes only the run's indexed members (`visibility.indexed=1`) at prompt-composition, re-reading disk so a divergent member reflects current content.
938
+ - **Disk-read-first edits.** Disk writes route through `File.edit`, which reads disk before diffing — an untouched member's baseline is its real content, never empty. Silent overwrite is structurally prevented.
902
939
 
903
- - **D1. Workspace identity lives on the session.** No `projects` table. `sessions.project_root TEXT` (nullable = headless). Constraints scoped via `session_constraints`. plurnk has no users/auth/multi-tenant; workspace = session.
904
- - **D2. No new `entries.scope` value.** Stays `∈ {'agent', 'session'}`.
905
- - **D3. Disk co-location optional.** `sessions.project_root` set → disk side-effects on accept; null → canonical entries land, no disk write (client owns materialization).
906
- - **D4. Membership without git: no fs-walk.** git present → ls-files + constraint overlay. git absent → `effect='add'` constraints only.
907
- - **D5. EMI is eager + relevance-bounded.** Fires at prompt-composition time; checks only entries the current run will include (`visibility.indexed=1`). Divergent → re-read disk + emit synthetic log entry.
908
- - **D6. Edit-on-untouched-file: disk-read-first.** EDIT targeting a member with no existing entry reads disk first to populate canonical baseline, then diffs. Prevents silent overwrite.
909
- - **D7. Constraint pattern matching via `node:path.matchesGlob`.** No dep. Helper wrapper for swap-out optionality.
940
+ **Deferred promised, not yet built.** Each carries an anchor with a deliberately-red test so the deferral is tracked, never silently covered.
910
941
 
911
- **Rationale.** No users/tenants. Session is the right scope unit. git+constraints membership keeps model out of fs-walk territory. EMI's eager-relevance-bounded firing prevents stale previews without per-turn full-repo cost.
942
+ - **Constraint overlay — the client supersede.** {§14.3-constraint-overlay} A `session_constraints` table layering *add* (members git misses), *ignore* (drop ones it tracks), and *read-only* (member for read, writes rejected) over the git substrate; glob matching via `node:path.matchesGlob`. git-absent, these `effect='add'` constraints are the *sole* membership source so this overlay is not merely the override knob, it is the entire membership mechanism without git.
943
+ - **EMI divergence signal.** {§14.3-emi-divergence-signal} When EMI re-reads a member whose disk content changed out-of-band, emit a synthetic log entry so the model sees the change (the re-read is built; only the signal is deferred).
944
+
945
+ **Rationale.** No users/tenants. Session is the right scope unit. git+constraints membership keeps the model out of fs-walk territory. EMI's eager-relevance-bounded firing prevents stale previews without per-turn full-repo cost.
912
946
 
913
947
  **Migration path.** Tenancy / cross-session shared workspaces require a `workspaces` table joining sessions to workspace id, with constraints lifted off `session_constraints`. Disk co-location semantics unchanged.
914
948
 
949
+ ### §14.4 Budget enforcement: the grinder
950
+
951
+ **Question.** §14.2 surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Three states defeat self-regulation, none of them the model's doing: a jumbo prompt, a jumbo repo (the index overflows before the model acts), an unexpectedly large read. What enforces the ceiling when the signal isn't enough?
952
+
953
+ **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 (§14.2) is measured against the ceiling. At or under → the packet ships untouched; the grinder never trims speculatively or "helpfully." {§14.4-overflow-only} On overflow it reclaims window in two passes, re-measuring between:
954
+
955
+ - **Prior-turn rollback.** The immediately-prior turn's log entries — the latest emissions, the ones that pushed the packet over — are hidden (`indexed=0`, the same flag the model's own HIDE uses); the prior turn fit by induction, so reverting it usually lands back under. Hidden, not deleted: rows and bodies persist and are re-SHOWable, so log *history* is preserved while the render shrinks. {§14.4-layer1-rollback}
956
+ - **Index collapse.** If clearing replays isn't enough, every catalog entry except `plurnk://manifest.json` is hidden (`indexed=0`); the manifest stays as the lifeline the model reads to pull anything back by path. The index is engine-built scaffolding — a derived, previews-only view regenerated each turn — never the model's context, so collapsing it removes no capability. {§14.4-layer2-index-collapse}
957
+ - **Hard stop.** If the packet still overflows with only the manifest left, the loop abandons at 499 (`engine_loop_cancel`) — the path `maxTurns` and the strike threshold already use. No third pass. {§14.4-hard-413-abort}
958
+
959
+ **Strike coupling.** A grinder fire bumps the engine's `turnErrors` — the same internal counter cycle detection feeds — so an overflow counts toward the strike streak that ends a runaway loop at 499. This is the pressure that keeps self-curation the path of least resistance. {§14.4-strike-coupling} **Turn 0/1 is exempt:** the first turn's overflow precedes any model action — it's the environment, not the model — so it never strikes. {§14.4-soft-turn-0-1}
960
+
961
+ **What the model sees.** A `budget_overflow` telemetry event (§15.1), 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 per-scheme budget table (§14.2) 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. {§14.4-event-model-terms} Per the gamification policy (§15.1), the *strike* the overflow triggers stays engine-internal; the model sees the hidden entries, never the accounting.
962
+
963
+ **Rationale.** The model owns curation (§14.2); the grinder is the exceptional backstop. It only *hides* — reversibly — the prior turn's render, then the index scaffolding; nothing is deleted, so the model can SHOW any of it back and log history stays intact. Rummy's §1316 spec described clearing log *bodies*, but its code instead hid the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it (and the §1316 index-collapse pass that doc never caught up to).
964
+
965
+ **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.
966
+
915
967
  ---
916
968
 
917
969
  ## §15 Packet shape
@@ -941,6 +993,8 @@ type Packet = {
941
993
 
942
994
  **Prompt as a first-class entry.** Each loop's prompt is written on loop start as a system-origin `EDIT` against `plurnk://prompt/<loop_id>` (indexable, body channel, text/markdown). At render time the current loop's entry is foisted out of `packet.system.index` into `packet.user.prompt` — no duplicate rendering. Previous loops' prompts remain in the index, addressable for READ/HIDE. {§15-current-prompt-foist}
943
995
 
996
+ **The entry catalog.** `plurnk://manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's indexed, READable, and queryable. Body is `application/json`: a flat array, one item per entry across all schemes (hidden ones included — the model sees what it could pull in), each `{ path, shown, channels: { <name>: { mimetype, tokens, lines } } }`. `shown` is whether the entry is in this turn's index — `[?(@.shown==false)]` gives the hidden inventory to SHOW; `tokens` is the provider's write-time count (budget depth), `lines` the content extent from `Mimetypes.process().totalLines`. The engine counts neither. It does not list itself. {§15-manifest-catalog}
997
+
944
998
  ### §15.1 user.telemetry — model-facing runtime telemetry
945
999
 
946
1000
  Slot for telemetry the model MUST react to immediately. Rendered at the bottom of the user section. Errors are transient — appear on the turn AFTER the failure, clear once seen. `packet.system.log[]` is the durable audit; `telemetry.errors[]` is the **alert**.
@@ -965,6 +1019,7 @@ Slot for telemetry the model MUST react to immediately. Rendered at the bottom o
965
1019
  | `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`) |
966
1020
  | `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). |
967
1021
  | `max_commands_exceeded` | Single emission exceeded `PLURNK_MAX_COMMANDS` cap; overflow ops dropped without dispatch | `source: "engine:rail"`, `kind`, `emitted`, `dropped` |
1022
+ | `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) |
968
1023
 
969
1024
  Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping are all engine-internal — they 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://`. {§15.1-no-error-scheme}
970
1025
 
@@ -1052,12 +1107,12 @@ Body: JSON array of `{line, matched, matching?}`. Empty → 204. Mimetype = `app
1052
1107
 
1053
1108
  ### §16.4 Structural EDIT on JSON
1054
1109
 
1055
- When effective mimetype is `application/json`, EDIT dispatches through `applyJsonItemEdit`. Body shape rule (parse-then-discriminate):
1110
+ When effective mimetype is `application/json`, EDIT dispatches through `applyJsonItemEdit`. {§16.4-structural-json-edit} Body shape rule (parse-then-discriminate):
1056
1111
 
1057
1112
  - Body parses as JSON array → items to splice
1058
1113
  - Body parses as non-array JSON → single item to splice
1059
1114
  - Empty body → delete the selection
1060
- - Body fails JSON parse → 400 (path-extension declares intent; honor strictly)
1115
+ - Body fails JSON parse → 400 (path-extension declares intent; honor strictly) {§16.4-json-parse-fail-400}
1061
1116
 
1062
1117
  **Array source marker × body:**
1063
1118
 
@@ -1088,14 +1143,14 @@ When effective mimetype is `application/json`, EDIT dispatches through `applyJso
1088
1143
  - `known://config.yaml` → `application/yaml`
1089
1144
  - `known://users` (no suffix) → `text/markdown` (Known manifest default)
1090
1145
 
1091
- Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype is stored in `entry_channels.mimetype` on write and drives `<L>` and matcher dispatch on read.
1146
+ Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype is stored in `entry_channels.mimetype` on write and drives `<L>` and matcher dispatch on read. {§16.5-extension-mimetype}
1092
1147
 
1093
1148
  ### §16.6 Render rule (mimetype-driven)
1094
1149
 
1095
1150
  `packet-wire` log render branches on `isLineNavigableMimetype`:
1096
1151
 
1097
- - **Line-navigable** (text/markdown, text/plain, csv, source code, yaml, toml) → `N:\t` line-number prefix per line
1098
- - **Tree-navigable** (application/json, application/xml, text/html, +json/+xml suffixes) → verbatim body (no `N:\t` — outer line numbers would collide with structural navigation like jsonpath/xpath)
1152
+ - **Line-navigable** (text/markdown, text/plain, csv, source code, yaml, toml) → `N:\t` line-number prefix per line {§16.6-line-navigable-prefix}
1153
+ - **Tree-navigable** (application/json, application/xml, text/html, +json/+xml suffixes) → verbatim body (no `N:\t` — outer line numbers would collide with structural navigation like jsonpath/xpath) {§16.6-tree-navigable-verbatim}
1099
1154
 
1100
1155
  The `N:\t` prefix is presentation/reference per plurnk.md ("not part of the source"); stripped before any matcher operation on the log entry.
1101
1156
 
@@ -1103,16 +1158,12 @@ The `N:\t` prefix is presentation/reference per plurnk.md ("not part of the sour
1103
1158
 
1104
1159
  Auto-derived text mimetypes anywhere in plurnk-service normalize to `text/markdown`:
1105
1160
 
1106
- - `<L>` slice on line-navigable source → `text/markdown`
1161
+ - `<L>` slice on line-navigable source → `text/markdown` {§16.7-text-markdown-normalize}
1107
1162
  - File scheme extension fallback → `text/markdown`
1108
1163
  - `Mimetypes.detect()` returning `text/plain` → normalized via `normalizeAutoTextMimetype`
1109
1164
 
1110
1165
  `text/plain` survives only where a scheme explicitly declares it (exec stdout/stderr — subprocess byte-streams aren't markdown). The model never auto-encounters `text/plain` from defaults.
1111
1166
 
1112
- ### §16.8 EDIT response surfaces unified diff
1113
-
1114
- Every EDIT@200/201 carries `diff` (unified diff format) on the result. Both `_entry-ops.editSessionEntry` and `File.edit` produce it. `packet-wire` log render emits the diff under the entry's fence so the model sees what changed without a follow-up READ — wrong-marker mistakes (e.g., `<1>` when `<-1>` was meant) become visible on the next turn. Omitted on no-op edits (content unchanged). {§16.8-edit-diff}
1115
-
1116
1167
  ### §16.9 Op-level invariants and resolved ambiguities
1117
1168
 
1118
1169
  Carried from the contract walk; durable.
@@ -1122,9 +1173,9 @@ Carried from the contract walk; durable.
1122
1173
  - **EDIT `<L>` on non-existent entry** → body becomes content; `<L>` is positional-only on existing content.
1123
1174
  - **COPY `<L>`** → source range, symmetric with READ `<L>`.
1124
1175
  - **READ rx** prefixes each line with `N:\t` per §16.6. `sliceLinesRaw` (used by COPY) returns the lines without prefix.
1125
- - **FIND body matcher** applies to pathname. xpath/jsonpath content matchers dispatch through `Mimetypes.query`.
1126
- - **SHOW/HIDE** has a single-entry path (exact pathname, no slots) and a multi-entry path (any of body/signal/`<L>` present). Multi-entry path treats target as pathname glob scope, applies body matcher to pathnames (regex/glob), filters by `signal` tags, paginates with `<L>`, and flips visibility on the resulting set.
1127
- - **SEND[410]** with `#fragment` deletes that channel only; without fragment, deletes the whole entry. **SEND[499]** is owned by the streaming scheme that holds the subscription.
1176
+ - **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).
1177
+ - **SHOW/HIDE** has a single-entry path (exact pathname, no slots) and a multi-entry path (any of body/signal/`<L>` present). Multi-entry path treats target as pathname glob scope, applies the body matcher to entry content (shared with FIND via `matchPathnames`), filters by `signal` tags, paginates with `<L>`, and flips visibility on the resulting set.
1178
+ - **SEND[410]** deletes as a side-effect (not the model idiom; §6.5): with `#fragment`, that channel only; without, the whole entry. **SEND[499]** is owned by the streaming scheme that holds the subscription.
1128
1179
  - **File scheme** reads disk content with mimetype detected via `Mimetypes.detect({ path })` (plumbed through `PlurnkSchemeContext.mimetypes`). Binary mimetypes → 415 on READ and EDIT.
1129
1180
 
1130
1181
  ### §16.10 Directed-SEND status code policy
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, resolve } from "node:path";
7
+ import SqlRite from "@possumtech/sqlrite";
8
+ import type { Db } from "../src/core/Db.ts";
9
+ import Daemon from "../src/server/Daemon.ts";
10
+ import EnvFlags from "../src/core/EnvFlags.ts";
11
+ import ProviderInstantiate from "../src/core/ProviderInstantiate.ts";
12
+ import { resolveActiveAlias } from "@plurnk/plurnk-providers";
13
+
14
+ export default class Cli {
15
+ static #projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+
17
+ static #die(code: number, message: string): never {
18
+ process.stderr.write(`${message}\n`);
19
+ process.exit(code);
20
+ }
21
+
22
+ static #loadEnv(path: string, required: boolean): void {
23
+ if (existsSync(path)) {
24
+ try { process.loadEnvFile(path); }
25
+ catch (cause) { Cli.#die(64, `failed to load ${path}: ${cause instanceof Error ? cause.message : String(cause)}`); }
26
+ } else if (required) {
27
+ Cli.#die(64, `--config: ${path} does not exist`);
28
+ }
29
+ }
30
+
31
+ // The .env cascade always populates these from .env.example, so absence is
32
+ // a broken config, not a runtime branch — fail hard rather than `?? ""`.
33
+ static #requireEnv(name: string): string {
34
+ const value = process.env[name];
35
+ if (value === undefined || value.length === 0) Cli.#die(78, `missing required env ${name} (declare it in .env.example)`);
36
+ return value;
37
+ }
38
+
39
+ static async #openDb(dbPath: string): Promise<Db> {
40
+ const db = await SqlRite.open({
41
+ path: dbPath,
42
+ dir: [resolve(Cli.#projectRoot, "migrations"), resolve(Cli.#projectRoot, "src")],
43
+ });
44
+ return db as unknown as Db;
45
+ }
46
+
47
+ static async #migrate(): Promise<void> {
48
+ const dbPath = Cli.#requireEnv("PLURNK_DB_PATH");
49
+ const db = await Cli.#openDb(dbPath);
50
+ try { process.stdout.write(`migrated: ${dbPath}\n`); }
51
+ finally { await db.close(); }
52
+ }
53
+
54
+ static async #start(): Promise<void> {
55
+ const dbPath = Cli.#requireEnv("PLURNK_DB_PATH");
56
+ const host = Cli.#requireEnv("PLURNK_HOST");
57
+ const port = Number(Cli.#requireEnv("PLURNK_PORT"));
58
+
59
+ const db = await Cli.#openDb(dbPath);
60
+ const alias = resolveActiveAlias();
61
+ const provider = alias === null ? null : await ProviderInstantiate.loadActiveProvider();
62
+ const daemon = new Daemon({ db, provider });
63
+ const addr = await daemon.start({ host, port });
64
+ const aliasStr = alias === null ? "no model" : `${alias.alias}=${alias.provider}/${alias.model}`;
65
+ process.stdout.write(`plurnk-service ws://${addr.host}:${addr.port} db=${dbPath} ${aliasStr}\n`);
66
+
67
+ const shutdown = async (): Promise<void> => { await daemon.stop(); await db.close(); process.exit(0); };
68
+ process.on("SIGINT", shutdown);
69
+ process.on("SIGTERM", shutdown);
70
+ }
71
+
72
+ static async main(): Promise<void> {
73
+ // Env cascade: .env.example (shipped defaults) < .env (project) <
74
+ // .env.<config> (--config) < shell. process.loadEnvFile is set-if-unset,
75
+ // so loading in low→high precedence order yields the right effective env
76
+ // (highest precedence loads FIRST — first write wins).
77
+ const configFlagIndex = process.argv.findIndex((a) => a === "--config" || a.startsWith("--config="));
78
+ const configFile = ((): string | null => {
79
+ if (configFlagIndex === -1) return null;
80
+ const arg = process.argv[configFlagIndex];
81
+ if (arg.includes("=")) return arg.slice(arg.indexOf("=") + 1);
82
+ return process.argv[configFlagIndex + 1] ?? null;
83
+ })();
84
+
85
+ if (configFile !== null) Cli.#loadEnv(configFile, true);
86
+ Cli.#loadEnv(".env", false);
87
+ Cli.#loadEnv(resolve(Cli.#projectRoot, ".env.example"), false);
88
+
89
+ const flagDescriptors = await EnvFlags.parseEnvExample(resolve(Cli.#projectRoot, ".env.example"));
90
+ const flagOptions: Record<string, { type: "string" }> = {};
91
+ for (const f of flagDescriptors) {
92
+ flagOptions[f.flagName.replace(/^--/, "")] = { type: "string" };
93
+ }
94
+
95
+ const usage = `usage: plurnk-service [options] [migrate]
96
+
97
+ ${EnvFlags.formatFlagsHelp(flagDescriptors)}
98
+
99
+ --config=<path> layer additional env from <path>
100
+ -h, --help show this help
101
+ `;
102
+
103
+ const { positionals, values } = parseArgs({
104
+ allowPositionals: true,
105
+ strict: false,
106
+ options: { help: { type: "boolean", short: "h" }, config: { type: "string" }, ...flagOptions },
107
+ });
108
+
109
+ for (const f of flagDescriptors) {
110
+ const key = f.flagName.replace(/^--/, "");
111
+ const v = values[key];
112
+ if (typeof v === "string") process.env[f.envName] = v;
113
+ }
114
+
115
+ if (values.help) { process.stdout.write(usage); process.exit(0); }
116
+
117
+ const dispatch: Record<string, () => Promise<void>> = { migrate: Cli.#migrate, start: Cli.#start };
118
+ const subcommand = typeof positionals[0] === "string" ? positionals[0] : "start";
119
+ const handler = dispatch[subcommand];
120
+ if (handler === undefined) Cli.#die(64, `unknown subcommand: ${subcommand}\n\n${usage}`);
121
+ if (positionals.length > 1) Cli.#die(64, `unexpected arguments: ${positionals.slice(1).join(" ")}`);
122
+
123
+ try { await handler(); }
124
+ catch (cause) {
125
+ process.stderr.write(`${subcommand}: ${cause instanceof Error ? cause.message : String(cause)}\n`);
126
+ if (cause instanceof Error && cause.cause) process.stderr.write(` cause: ${cause.cause instanceof Error ? cause.cause.message : String(cause.cause)}\n`);
127
+ process.exit(1);
128
+ }
129
+ }
130
+ }
131
+
132
+ await Cli.main();
@@ -0,0 +1,8 @@
1
+ export default class Paths {
2
+ #private;
3
+ static migrations: string;
4
+ static instructionsSystem: string;
5
+ static defaultPersona: string;
6
+ static defaultRequirements: string;
7
+ }
8
+ //# sourceMappingURL=Paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Paths.d.ts","sourceRoot":"","sources":["../src/Paths.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,OAAO,OAAO,KAAK;;IAItB,MAAM,CAAC,UAAU,SAA8C;IAC/D,MAAM,CAAC,kBAAkB,SAA6C;IAKtE,MAAM,CAAC,cAAc,SAAkC;IAIvD,MAAM,CAAC,mBAAmB,SAAuC;CAuBpE"}