wolli 0.0.1 → 0.0.2

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.
@@ -0,0 +1,299 @@
1
+ # Plugins
2
+
3
+ A plugin is an npm-style package whose `package.json` carries a `"wolli"` manifest declaring the resources it contributes — extensions, integrations, skills, prompt templates, and/or themes. One install adds all of them to an agent at once, resolved in place from the single package. Plugins are how you share a [dual-half integration](./integrations.md#the-dual-half-package) (a transport plus its mapping extension), a bundle of [extensions](./extensions.md), or any mix of resource types between agents and across machines.
4
+
5
+ > **Per-agent, not global.** Wolli has no project scope. A plugin is installed for one agent and lands in that agent's own home (`~/.wolli/agents/<name>/`). The agent name precedes every verb: `wolli <agent> plugins install <source>`.
6
+
7
+ > **Security:** Plugins run with full host access. Extensions and integrations execute arbitrary code inside the agent process, and skills can instruct the model to take any action. Review a plugin's source before installing a third-party package.
8
+
9
+ ## Table of Contents
10
+
11
+ - [What is a Plugin](#what-is-a-plugin)
12
+ - [Where Plugins Install](#where-plugins-install)
13
+ - [The package.json `wolli` Manifest](#the-packagejson-wolli-manifest)
14
+ - [Authoring a Plugin](#authoring-a-plugin)
15
+ - [Publishing](#publishing)
16
+ - [Installing](#installing)
17
+ - [Configuring & Onboarding](#configuring--onboarding)
18
+ - [Listing, Updating, Removing](#listing-updating-removing)
19
+ - [How Resolution Works](#how-resolution-works)
20
+ - [Worked Example: Packaging the Telegram Integration](#worked-example-packaging-the-telegram-integration)
21
+
22
+ ## What is a Plugin
23
+
24
+ A plugin is a directory (or published package) with a `package.json` whose `"wolli"` field names the contribution files. Each listed path is a normal source module loaded by the agent's resource loader at launch:
25
+
26
+ - **integrations** — transport modules registered via `wolli.registerIntegration` (see [integrations.md](./integrations.md)).
27
+ - **extensions** — agent-owned behavior modules (see [extensions.md](./extensions.md)).
28
+ - **skills** — `SKILL.md` (or top-level `.md`) instruction files (see [skills.md](./skills.md)).
29
+ - **prompts** — `.md` prompt templates (see [prompt-templates.md](./prompt-templates.md)).
30
+ - **themes** — `.json` theme files (see [themes.md](./themes.md)).
31
+
32
+ A plugin may declare any subset. The common case is a single plugin that bundles both halves of an integration — a transport under `"integrations"` and its mapping extension under `"extensions"` — so they install and version as one unit; for a single agent you can instead place the two files directly in its `integrations/`/`extensions/` folders without a plugin (see [integrations.md](./integrations.md#integration-locations)). In a plugin, both resolve from the one install — the extension is **not** copied anywhere; it is loaded in place from the package (see [How Resolution Works](#how-resolution-works)).
33
+
34
+ ## Where Plugins Install
35
+
36
+ `install` copies/clones the package into the agent's managed plugin store under its home. The store is keyed by source scheme:
37
+
38
+ ```
39
+ ~/.wolli/agents/<name>/
40
+ ├── agent.json # the agent's settings override; records installed plugins in "plugins"[]
41
+ └── .plugins/
42
+ ├── npm/ # npm: sources — a private npm project; packages under node_modules/
43
+ │ ├── package.json
44
+ │ └── node_modules/<pkg>/
45
+ ├── git/<host>/<user>/<repo>/ # git: sources — a clone per repo
46
+ └── local/<slug>-<hash>/ # local sources — a recursive copy per origin path
47
+ ```
48
+
49
+ - `npm:` packages install via `npm install --prefix <store>/npm --legacy-peer-deps` (bun/pnpm equivalents use `--omit=peer` / `auto-install-peers=false`). They live under `<store>/npm/node_modules/<name>`.
50
+ - `git:` sources are cloned to `<store>/git/<host>/<user>/<repo>`; if the clone has a `package.json`, dependencies are installed there.
51
+ - Local sources are copied (not symlinked) to `<store>/local/<basename-slug>-<sha256-prefix>`, so the install travels even if the origin moves; dependencies install in the copy.
52
+
53
+ The agent's discovery dirs (`~/.wolli/agents/<name>/extensions/`, `integrations/`, `skills/`, etc.) are for hand-placed local resources. Installed plugins are **not** unpacked into those dirs — they stay in `.plugins/` and are resolved from there.
54
+
55
+ ## The package.json `wolli` Manifest
56
+
57
+ The plugin manager reads `package.json` and parses exactly the `"wolli"` object. Every key is an array of paths (relative to the package root) or glob patterns:
58
+
59
+ | Key | Loaded as | File pattern |
60
+ |----------------|-----------------|-------------------------------|
61
+ | `integrations` | integration modules | `.ts` / `.js` |
62
+ | `extensions` | extension modules | `.ts` / `.js` |
63
+ | `skills` | skills | `SKILL.md` / `.md` |
64
+ | `prompts` | prompt templates | `.md` |
65
+ | `themes` | themes | `.json` |
66
+
67
+ No other keys under `"wolli"` are read. (There is no gallery/preview metadata; wolli has no package registry of its own — see [Publishing](#publishing).)
68
+
69
+ Example manifest (modeled on the shipped Telegram plugin; see the [worked example](#worked-example-packaging-the-telegram-integration) for the verbatim file):
70
+
71
+ ```json
72
+ {
73
+ "name": "wolli-integration-telegram",
74
+ "type": "module",
75
+ "wolli": {
76
+ "integrations": ["./index.ts"],
77
+ "extensions": ["./telegram-chat.ts"]
78
+ },
79
+ "dependencies": {
80
+ "grammy": "1.44.0",
81
+ "@grammyjs/runner": "2.0.3"
82
+ },
83
+ "peerDependencies": {
84
+ "@opsyhq/wolli": "*"
85
+ }
86
+ }
87
+ ```
88
+
89
+ The simplest manifest lists one plain single-file path per key — a flat package with each contribution file at the package root:
90
+
91
+ ```json
92
+ {
93
+ "wolli": {
94
+ "integrations": ["./index.ts"],
95
+ "extensions": ["./x.ts"]
96
+ }
97
+ }
98
+ ```
99
+
100
+ Plain single-file path entries are **first-class**; globs and override prefixes are optional and only needed for multi-file or directory layouts. The advanced directory/glob/override-prefix semantics below layer on top of this base case.
101
+
102
+ **Notes:**
103
+
104
+ - Paths are relative to the package root and resolved against it. An entry is one of three things:
105
+ - a **plain path** — a single file (`./index.ts`) loaded as-is, or a **directory**, which is then collected for that resource type. Directories collect by the type's file pattern (`.md` for skills/prompts, `.json` for themes); for `integrations`/`extensions` the directory is collected with the same package-style discovery as a convention dir (an `index.ts`/`index.js` or nested `package.json` manifest per subdir, **not** a flat sweep of every `.ts`).
106
+ - a **glob** (contains `*` or `?`, e.g. `extensions/*.ts`) — expanded against the package root, then each match collected as above.
107
+ - an **override prefix** (`!exclude`, `+force-include`, `-force-exclude`) — not a source itself; it layers on top of the paths the plain/glob entries already produced. `!` removes matches, `+` adds an exact path back even if excluded, `-` removes an exact path even if force-included.
108
+ When an entry resolves to a directory (or a glob matches one), only files matching the resource type's pattern are picked up; a plain entry pointing straight at a single file is taken as-is, so list each file under its correct key.
109
+ - If no `"wolli"` manifest is present, the manager falls back to convention directories — `extensions/`, `integrations/`, `skills/`, `prompts/`, `themes/` — and auto-discovers files there. A bare file or a manifest-less directory with no convention dirs is treated as a single extension.
110
+ - Third-party runtime deps (here `grammy`; `croner` in the scheduler plugin) go in `dependencies` and are installed automatically when the plugin is fetched. `"dependencies"` is **optional** and may be omitted entirely when the transport relies only on Node globals — a transport that talks to a plain HTTP endpoint can call `fetch` directly with no bundled client (see [integrations.md › Available Imports](./integrations.md#available-imports), which states a plain-HTTP transport can use `fetch` directly and needs no bundled client). Bundling a client library is only needed for richer protocols.
111
+
112
+ ### Why peerDependencies on `@opsyhq/wolli`
113
+
114
+ Contribution modules import host types and APIs from `@opsyhq/wolli` (`IntegrationsAPI`, `ExtensionFactory`, etc.). The host process *provides* that package at runtime, so the plugin must not bundle its own copy. Declare it as a peer with a `"*"` range:
115
+
116
+ ```json
117
+ { "peerDependencies": { "@opsyhq/wolli": "*" } }
118
+ ```
119
+
120
+ Managed installs are run with peer resolution disabled (`--legacy-peer-deps` and equivalents), so the package manager does not try to install or solve this host-provided peer. The agent resolves it from the host at load time instead.
121
+
122
+ ## Authoring a Plugin
123
+
124
+ Lay the package out as a normal npm package. A dual-half integration plugin looks like:
125
+
126
+ ```
127
+ my-plugin/
128
+ ├── package.json # name, type: "module", "wolli" manifest, deps, peerDependencies
129
+ ├── index.ts # the integration transport (listed under "integrations")
130
+ ├── my-chat.ts # the mapping extension (listed under "extensions")
131
+ └── README.md
132
+ ```
133
+
134
+ 1. **Write the contribution files.** Author the transport half per [integrations.md](./integrations.md) and the mapping/behavior half per [extensions.md](./extensions.md). This doc does not duplicate their authoring guidance; it only packages them.
135
+ 2. **Declare them in the manifest.** List each file under the matching `"wolli"` key (above).
136
+ 3. **Set `"type": "module"`** so `.ts`/`.js` modules load as ESM.
137
+ 4. **Put runtime deps in `dependencies`** and **`@opsyhq/wolli` in `peerDependencies`** with `"*"`.
138
+
139
+ That is the whole contract. There is no build step or registration call beyond the manifest — the agent's resource loader imports the listed files at launch.
140
+
141
+ ## Publishing
142
+
143
+ A plugin is shared as an ordinary package. Wolli installs from three source kinds and nothing else — there is no wolli-hosted registry or installer. Pick the distribution that matches the source scheme you want users to install with:
144
+
145
+ | Distribution | How users install |
146
+ |------------------|-----------------------------------------------------------|
147
+ | npm registry | `npm publish`, then `wolli <agent> plugins install npm:<name>` |
148
+ | git repository | push to a host, then `... plugins install git:<host>/<user>/<repo>` |
149
+ | local path | hand someone the directory, then `... plugins install ./path` |
150
+
151
+ - **npm:** publish the package to any registry the user's npm client can reach. Versioned specs (`npm:pkg@1.2.3`) install pinned and are skipped by `update`.
152
+
153
+ > **Publishing requirements for the npm path.** A registry-publishable plugin must **not** set `"private": true`. The shipped Telegram/Scheduler examples set it only because they are in-repo packages never published; `npm publish` refuses a `"private": true` package. npm also requires a `"version"` field — it is mandatory for `npm publish` and for any versioned spec (`npm:pkg@1.2.3`). The in-repo examples carry `"version": "1.0.0"` for this reason; the abbreviated manifest under [The package.json `wolli` Manifest](#the-packagejson-wolli-manifest) omits both fields to focus on the `"wolli"` block, not because they are optional for publishing.
154
+ - **git:** any reachable repo works; HTTPS and SSH are both supported, and a pinned ref (`@tag`/`@commit`) freezes the checkout.
155
+ - **local:** for development or private sharing — no registry needed. The path is copied into the agent's store on install.
156
+
157
+ Wolli does not run `npm publish` for you; it only *consumes* one of these three source forms.
158
+
159
+ ## Installing
160
+
161
+ The agent name precedes the verb. Each source scheme maps to a distinct install path:
162
+
163
+ ```bash
164
+ wolli <agent> plugins install npm:@scope/pkg # npm: registry package
165
+ wolli <agent> plugins install npm:@scope/pkg@1.2.3 # pinned version
166
+ wolli <agent> plugins install git:github.com/user/repo # git: shorthand
167
+ wolli <agent> plugins install git:github.com/user/repo@v1 # pinned ref
168
+ wolli <agent> plugins install ./path/to/plugin # local path
169
+ ```
170
+
171
+ Source-scheme rules (as parsed by the plugin manager):
172
+
173
+ - **`npm:`** — everything after the prefix is the npm spec; a trailing `@version` marks it pinned.
174
+ - **`git:`** — accepts shorthand (`github.com/user/repo`, `git@github.com:user/repo`) and protocol URLs (`https://`, `ssh://`). Without the `git:` prefix, only explicit protocol URLs are recognized as git.
175
+ - **local** — a path starting with `./`, `../`, `/`, or `~`; also the fallback for any source that is neither `npm:` nor a recognized git URL.
176
+
177
+ `install` routes to the agent's daemon, which is the single writer of the plugin store and reloads itself after the change. Install:
178
+
179
+ 1. fetches the source into the agent's `.plugins/` store,
180
+ 2. records the source in the agent's settings (`agent.json` `"plugins"[]`),
181
+ 3. runs onboarding if applicable (below).
182
+
183
+ Local sources are normalized to an agent-relative form before being persisted, so they round-trip on the next launch.
184
+
185
+ Settings hold **one entry per plugin identity**, not per spec. Identity ignores the version/ref: npm by package name, git by `host/path` (so an SSH and an HTTPS URL for the same repo are the same plugin), local by resolved absolute origin. For local sources this means `remove ./path` / `update ./path` match by **resolved absolute origin**, not by the literal install spelling — `getPackageIdentity` keys on `local:<resolved-origin>` and `getLocalInstallPath` resolves the same way (`plugin-manager.ts`). So any relative spelling that resolves to the same directory matches the installed entry; reusing the identical spelling is sufficient but **not** required. Re-installing the same plugin at a different version or ref — e.g. after publishing a new `npm:pkg@2.0.0` over an existing `npm:pkg@1.0.0` — updates that single entry in place rather than appending a duplicate. This is also why a pinned npm entry whose installed copy no longer matches its pin is re-fetched at resolve time on the next launch.
186
+
187
+ ## Configuring & Onboarding
188
+
189
+ If a contributed integration declares an `onboard` step (see [integrations.md › Onboarding](./integrations.md#onboarding)), guided setup runs over the daemon's UI round-trip, rendered in a startup TUI:
190
+
191
+ ```bash
192
+ wolli <agent> plugins install ./packages/wolli/plugins/telegram # installs, then auto-onboards if on a TTY
193
+ wolli <agent> plugins configure ./packages/wolli/plugins/telegram # re-run guided setup on demand
194
+ ```
195
+
196
+ - **On install, on an interactive terminal (TTY):** onboarding runs immediately. Each onboarded service prints one result: `connected` (then a hint to run `wolli <agent>` to use it), `cancelled` (you dismissed a prompt), `not-found` (the integration is not installed for the agent), `no-onboard` (it declares no guided setup), or an `error` with its message. Any non-`connected`/`cancelled` result makes the command exit non-zero.
197
+ - **On install, non-interactive (no TTY):** install completes but skips setup and points you at `wolli <agent> plugins configure <source>` to finish later.
198
+ - **`configure`** re-runs the guided setup even if the account already exists. It requires an interactive terminal and is rejected early when headless. If the plugin has no guided setup, it reports `No guided setup available for this plugin.`
199
+
200
+ Onboarding writes account credentials to the per-agent `integrations.json`, separate from the plugin store — removing the plugin does not by itself touch saved accounts.
201
+
202
+ ## Listing, Updating, Removing
203
+
204
+ | Verb | What it does | Daemon |
205
+ |----------------------------|-------------------------------------------------------|--------|
206
+ | `plugins list` | print installed plugins and the integrations they add | no |
207
+ | `plugins update` | update all installed plugins | yes |
208
+ | `plugins update <source>` | update only the matching plugin | yes |
209
+ | `plugins remove <source>` | remove the plugin and drop it from settings | yes |
210
+
211
+ ```bash
212
+ wolli <agent> plugins list # read installed plugins from settings (local, no daemon)
213
+ wolli <agent> plugins update # update all installed plugins
214
+ wolli <agent> plugins update <source> # update only the matching plugin
215
+ wolli <agent> plugins remove <source> # remove the plugin and drop it from settings
216
+ ```
217
+
218
+ - **`list`** reads the agent's settings on disk directly (no daemon spawn). It prints each configured source, its on-disk install path, a `(filtered)` marker when the entry uses the object form, and then the integrations those plugins contribute. Listing the integrations calls `resolve()`, which self-heals a missing install (see [How Resolution Works](#how-resolution-works)) — so `list` is not strictly read-only: a configured-but-uninstalled npm/git source can trigger a fetch here unless `WOLLI_OFFLINE=1` is set.
219
+ - **`update`** routes to the daemon. With a source, only the matching plugin updates; without one, all do. Pinned npm versions are fixed and skipped. Git sources reconcile an existing clone to the configured ref. Local sources are re-copied from their origin. Set `WOLLI_OFFLINE=1` to skip network fetches (local re-copy still runs).
220
+ - **`remove`** routes to the daemon, deletes the plugin from the store, and removes its source from settings. It errors if no configured plugin matches the source.
221
+
222
+ A plugin entry in settings can be a bare string (load everything) or an object that filters which contributions load:
223
+
224
+ ```json
225
+ {
226
+ "plugins": [
227
+ "npm:wolli-integration-scheduler",
228
+ {
229
+ "source": "git:github.com/user/repo",
230
+ "extensions": ["*.ts", "!legacy.ts"],
231
+ "skills": []
232
+ }
233
+ ]
234
+ }
235
+ ```
236
+
237
+ Omit a key to load all of that type; `[]` loads none; `!pattern` excludes; `+path`/`-path` force-include/exclude exact paths. Filters narrow what the manifest already allows.
238
+
239
+ ## How Resolution Works
240
+
241
+ At each launch the resource loader calls the plugin manager's `resolve()`, which:
242
+
243
+ 1. reads the agent's `"plugins"[]` from settings,
244
+ 2. for each source, self-heals a missing install (re-fetches if the store entry is gone but the origin still exists),
245
+ 3. resolves the contributions **in place** from the install — manifest paths first, then convention dirs, then the single-extension fallback,
246
+ 4. applies any per-entry filter and name-collision precedence,
247
+ 5. hands the enabled paths to the integration loader and extension loader.
248
+
249
+ Crucially, a dual-half package's integration and its paired extension both resolve from the *same* install directory in `.plugins/`. The extension is never copied into `<agent>/extensions/`. The integration arm loads first so the producer runner exists before the extension wires `getIntegration(...)`.
250
+
251
+ Because `install`/`remove`/`update` go through the daemon (the single writer), the running agent reloads itself after the change — installed contributions become active without a manual restart. Onboarding-gated mapping extensions activate once their account is configured.
252
+
253
+ ## Worked Example: Packaging the Telegram Integration
254
+
255
+ The shipped Telegram plugin (`packages/wolli/plugins/telegram/`) is the canonical dual-half plugin. Its layout:
256
+
257
+ ```
258
+ telegram/
259
+ ├── package.json # "wolli": { integrations: ["./index.ts"], extensions: ["./telegram-chat.ts"] }
260
+ ├── index.ts # transport: long-polls grammY, holds the bot token, emits a `message` event,
261
+ │ # exposes sendMessage / sendChatAction / setCommands, declares onboard
262
+ └── telegram-chat.ts # mapping extension: routes each message into a per-chat Wolli session,
263
+ # ships the reply back through the transport
264
+ ```
265
+
266
+ Its manifest (verbatim):
267
+
268
+ ```json
269
+ {
270
+ "name": "wolli-integration-telegram",
271
+ "private": true,
272
+ "version": "1.0.0",
273
+ "type": "module",
274
+ "wolli": {
275
+ "integrations": ["./index.ts"],
276
+ "extensions": ["./telegram-chat.ts"]
277
+ },
278
+ "dependencies": {
279
+ "grammy": "1.44.0",
280
+ "@grammyjs/runner": "2.0.3"
281
+ },
282
+ "peerDependencies": {
283
+ "@opsyhq/wolli": "*"
284
+ }
285
+ }
286
+ ```
287
+
288
+ Install it into an agent and onboard the bot token in one step (on a TTY):
289
+
290
+ ```bash
291
+ wolli my-agent plugins install ./packages/wolli/plugins/telegram
292
+ # -> copies the package into ~/.wolli/agents/my-agent/.plugins/local/telegram-<hash>/
293
+ # -> records "plugins": ["packages/wolli/plugins/telegram"] in agent.json (agent-relative)
294
+ # -> runs onboard: prompts for the bot token, writes the account to integrations.json
295
+ ```
296
+
297
+ Both `index.ts` and `telegram-chat.ts` resolve from that one copy. The transport starts; once the account is configured, the mapping extension activates and bidirectional chat is live. To package it for others, publish the same directory to npm (`wolli-integration-telegram`) or a git repo and have them install with `npm:` / `git:` instead of the local path.
298
+
299
+ The shipped scheduler plugin (`packages/wolli/plugins/scheduler/`) has the identical shape — `"integrations": ["./index.ts"]`, `"extensions": ["./scheduler-chat.ts"]`, one runtime dep (`croner`), and the same `@opsyhq/wolli` peer — confirming the dual-half pattern is the convention, not Telegram-specific. The only manifest differences are the package name, the dependency, and the extension filename.
@@ -0,0 +1,92 @@
1
+ # Prompt Templates
2
+
3
+ Prompt templates are Markdown snippets that expand into full prompts. Type `/name` in the editor to invoke a template, where `name` is the filename without `.md`.
4
+
5
+ ## Locations
6
+
7
+ Wolli scopes prompt templates to each agent. It loads them from:
8
+
9
+ - The agent's home: `~/.wolli/agents/<name>/prompts/*.md`
10
+ - Project-local (under the agent's home): `.wolli/prompts/*.md`
11
+ - Settings: `prompts` array with files or directories
12
+ - CLI: `--prompt-template <path>` (repeatable)
13
+
14
+ Disable discovery with `--no-prompt-templates`.
15
+
16
+ ## Format
17
+
18
+ ```markdown
19
+ ---
20
+ description: Review staged git changes
21
+ ---
22
+ Review the staged changes (`git diff --cached`). Focus on:
23
+ - Bugs and logic errors
24
+ - Security issues
25
+ - Error handling gaps
26
+ ```
27
+
28
+ - The filename becomes the command name. `review.md` becomes `/review`.
29
+ - `description` is optional. If missing, the first non-empty line is used.
30
+ - `argument-hint` is optional. When set, the hint is displayed before the description in the autocomplete dropdown.
31
+
32
+ ### Argument Hints
33
+
34
+ Use `argument-hint` in frontmatter to show expected arguments in autocomplete. Use `<angle brackets>` for required arguments and `[square brackets]` for optional ones:
35
+
36
+ ```markdown
37
+ ---
38
+ description: Review PRs from URLs with structured issue and code analysis
39
+ argument-hint: "<PR-URL>"
40
+ ---
41
+ ```
42
+
43
+ This renders in the autocomplete dropdown as:
44
+
45
+ ```
46
+ → pr <PR-URL> — Review PRs from URLs with structured issue and code analysis
47
+ is <issue> — Analyze GitHub issues (bugs or feature requests)
48
+ wr [instructions] — Finish the current task end-to-end
49
+ cl — Audit changelog entries before release
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ Type `/` followed by the template name in the editor. Autocomplete shows available templates with descriptions.
55
+
56
+ ```
57
+ /review # Expands review.md
58
+ /component Button # Expands with argument
59
+ /component Button "click handler" # Multiple arguments
60
+ ```
61
+
62
+ ## Arguments
63
+
64
+ Templates support positional arguments, defaults, and simple slicing:
65
+
66
+ - `$1`, `$2`, ... positional args
67
+ - `$@` or `$ARGUMENTS` for all args joined
68
+ - `${1:-default}` uses arg 1 when present/non-empty, otherwise `default`
69
+ - `${@:N}` for args from the Nth position (1-indexed)
70
+ - `${@:N:L}` for `L` args starting at N
71
+
72
+ Example:
73
+
74
+ ```markdown
75
+ ---
76
+ description: Create a component
77
+ ---
78
+ Create a React component named $1 with features: $@
79
+ ```
80
+
81
+ Default values are useful for optional arguments:
82
+
83
+ ```markdown
84
+ Summarize the current state in ${1:-7} bullet points.
85
+ ```
86
+
87
+ Usage: `/component Button "onClick handler" "disabled support"`
88
+
89
+ ## Loading Rules
90
+
91
+ - Template discovery in `prompts/` is non-recursive.
92
+ - If you want templates in subdirectories, add them explicitly via `prompts` settings.