Two buttons beside the dsh chat input: rewrite the draft with the current directory in view, and insert a role template at the caret.
allow-version Shared version floor
If your profile pins `@jayyuen66/dsh-plugin-shared` with a pnpm `overrides` entry, that pin must be at least the version this package declares. A lower pin installs a copy without the names imported at runtime, and the host then reports `N entries did not activate` together with `does not provide an export named ...` at boot - the package installs fine and never loads, and no per-repository CI run can see it.
##
Verify it is live
1. **Two controls appear** to the right of the chat input. If they do not, the bundle layer is not mounted - check `dsh --profile web --dump-config` for the `dir-prep-organize` layer.
- `--dump-config` is the only accepted "it is in the layer stack" signal, and it is **not read-only** - it rewrites that profile's root assembly file. Read layer lines from stdout **and** check stderr separately: an unmatched patch target appears only on stderr, so the exit code alone proves nothing.
- A layer line may read `# ==
What it does not provide
Worth knowing before you look for them:
- **It registers no model tools.** The host half only injects `settings`; it never touches the tool registry. There is no `organize_text` tool to call.
- **It opens no HTTP route of its own.** The data plane is the official Typert Remote namespace `dirPrepOrganize`, registered through an injected child fiber, reached through the host `/api` gateway. Authentication, Host/DNS-rebinding/Origin checks, the signed browser-session cookie and body limits are all the gateway's - this package keeps no CSRF token and no route table.
- **It adds no CLI subcommand.**
- **It reads no environment variable** and makes no outbound request of its own.
- What it *does* consume: the host `settings` service (hard-injected, so the entry does not activate without it), `llm.stream`, `agentDefaultModel`, and the session/agent/filesystem services. When the settings form itself is not ready, the card renders disabled with a status hint while the dropdown falls back to the built-in curated set.
##
Enabling it in dsh
- Installing contributes the bundle patch layer (`dsh.bundle.patch` → `./cordis.patch.yml`), which inserts `- id: dir-prep-organize` with `name` set to the package name.
- The **client half** registers two slots plus the settings card:
- `conversation.input.right` - the polish button (id `dir-prep-organize`, order 99) and the templates dropdown (id `dir-prep-organize-templates`, order 100).
- `plugins.bundle.config` - the settings card, keyed by the **bundle package name** `@jayyuen66/dsh-dir-prep-organize`. The host dispatches `entryKey: pkg.name` (source of truth: `dsh.profile.bundles` in the profile's `package.json`), so a bare entry id never matches and the card would render nowhere.
- The settings namespace and `configForms.get()` stay the **bare entry id** `dir-prep-organize` - deliberately not the same identifier as that slot key.
- The client half is `dsh.client.platform = web` with `immediately`.
- The card follows the official locale plugin's preference for its own labels and copy; the host half reads the preference per request, so switching language needs no restart.
- The plugin page reads its title, description and detail document from `locale/
The polish call
| Aspect | Fact |
|---|---|
| Transport | Host `llm.stream`: one **request-only** user text message (no `id`, no `source`) plus a `system` rule block |
| Model | The live agent's current selection first - `agent.model.current()` on current hosts, `agent.options` on older ones - then `agentDefaultModel.currentSelection()`. Its absence reports "No model selection is available" |
| Draft guard | The rewrite is written back only if the session id, input action face, draft revision and input phase (`plain`) are unchanged. Otherwise the call reports "The input box changed; the polished text was not applied" and leaves the draft alone |
| Role style | An optional `roleText` injected into the user message's `` fence (never into the system block - see the trust-order note above `buildOrganizeSystem`). It borrows terminology and tone only; it never executes the role's tasks |
| Finish reason | Anything other than `stop` - including a missing finish chunk - **fails** and never writes back to the draft |
| Timeout | `organizeTimeoutMs`, default 120000 ms, read per request via `.get()`. The client fetch timeout is that value + 5000 ms |
| Input caps | `maxSnippetBytes` (262144 = 256 KiB) per key file, a 2 MiB total snippet budget, and `readConcurrency` (8) for reads. A file over the per-file cap, or a snippet past the total budget, is skipped whole and counted in `skipped.tooLarge`. Directory listings, conversation text and role bodies are **never truncated** |
| No truncation, on purpose | Directory listings, conversation text and role bodies go in whole: a model context-overflow error is the feedback, and the user slims the input. A payload above the endpoint body cap gets a 413 instead |
##
Templates
| Aspect | Fact |
|---|---|
| Entry shape | `{ id, name, description, text, group, emoji }`, all strings. `description` / `group` / `emoji` default to empty strings |
| Validation on save | Entries with an empty `name` or `text` are dropped; `emoji` is cut to 12 characters; a missing or colliding `id` is deterministically replaced by `tpl-
Settings
Namespace `dir-prep-organize` equals the settings entry id. Fields projected into the settings form are the `.volatile()` ones.
| Field | Type | Default | Card control | Notes |
|---|---|---|---|---|
| `templates` | `{id,name,description,text,group,emoji}[]` | unset | **yes** | The user's own templates. Unset falls back to the 85 built-in entries |
| `importAllowRoots` | `string[]` | `[]` | **yes** | Extra allowed roots for the import endpoint. Non-strings and whitespace-only items are dropped; duplicates removed. With nothing registered, only the session cwd and the built-in role library are allowed |
| `organizeTimeoutMs` | natural, volatile | `120000` | **no** | The LLM abort deadline in ms. Values are truncated and clamped to `[1, 2147483647]`; non-finite values fall back to `120000`. `0` passes the schema and clamps to 1 ms, so it really does mean an immediate timeout. Past 2^31-1 Node's `setTimeout` prints a `TimeoutOverflowWarning` and silently uses **1 ms**, which turns "raise the timeout" into "fail instantly". The card has no control, so changing it means `settings.yaml` or the row's `config:` |
| `maxSnippetBytes` | natural, non-volatile | `262144` | **no** | Per-file cap for key-file snippets; a larger file, or one whose size is unknown, is skipped whole and counted in `skipped.tooLarge`. Import candidates use the same cap. Non-volatile means no form projection, hence no card row - deployment value only |
| `readConcurrency` | natural, non-volatile | `8` | **no** | Bounded read concurrency for snippets and directory imports. Clamped to `[1, 64]`: `0` or a negative value made the bounded map recurse forever until the host was killed by the OOM killer, and a huge value defeats the fd ceiling this field exists to hold |
Settings are persisted in `settings.yaml` under the dsh data directory.
##
Public interface
Official Typert Remote namespace **`dirPrepOrganize`**, registered through an injected child fiber. The descriptors are hand-written, parameters go through strict shape validation, results go through `src-json`.
| Method | Input | Success payload |
|---|---|---|
| `defaults` | - | `ok` / `templates[…]` (the curated set) |
| `context` | `{ sessionId }` | `ok` / `cwd` / `truncated` / `skipped` / `entries[{name,isDir,sizeBytes,snippet}]` |
| `model` | `{ sessionId }` | `ok` / `provider` / `model` / `reasoningEffort` |
| `organize` | `{ sessionId, prompt, entriesSummary, roleText? }` | `ok` / `content` |
| `importTemplates` | `{ path, sessionId }` (`path` exactly empty = whole built-in library) | `ok` / `count` / `truncated` / `skipped` / `entries` |
- Business failures stay `ok:false` with `error`. The import path-policy branches are the exception: an out-of-bounds path or a malformed `path` field also carries `status` 403 or 400.
- `model` is the preflight read of the default selection; `organize` prefers the live agent's selection as described under "The polish call".
- Result parsing on the client keeps defensive defaults: a non-object payload or a missing `ok:true` becomes a **visible error**, never an empty list.
##
Data and privacy
| Aspect | Fact |
|---|---|
| Network | No outbound requests of its own: the data plane is the host gateway's RPC channel. No environment variable is read |
| On disk | The only data written is `templates` and `importAllowRoots`, in the host settings file |
| What one polish call sends | The full draft, the session cwd path, top-level directory entries **with the full text of key text files**, the real user/assistant turns (framework-injected messages and tool results are filtered out), and the optional role body |
| Which files get a snippet | Only 20 code/document extensions (ts/tsx/js/jsx/mjs/cjs/py/rs/go/java/rb/json/toml/md/sql/graphql/proto/css/scss/less). `yaml` / `env` are **not** whitelisted |
| Sensitive file names | Any file whose name matches `credentials` / `secret(s)` / `id_rsa` / `pem` / `p12` / `pfx` / `keystore` / `env` / `key(s)` / `token(s)` / `auth` / `passwd` / `password` / `account(s)` **at a separator boundary** never gets a snippet. The boundary is deliberate: a naive substring match would drop `monkey.js` and `AUTHORS.md`, while a miss would send credential text to a third-party model |
| Caps and concurrency | A single file over `maxSnippetBytes`, or with unknown size, is skipped whole - never half-summarized. Collected snippets share a 2 MiB budget; once it is exhausted the rest are skipped whole and counted in `skipped.tooLarge`. Reads run with `readConcurrency` |
| Counters | Too-large / unreadable / deeper-level / non-role cases are counted in `skipped`, shown in the UI, and reflected honestly by `truncated` |
| Import endpoint | **Read-only**: no model call, no disk writes. Absolute paths are refused by default; only paths inside the session cwd, the package's `agents/` and `importAllowRoots` pass. `.` / `..` / hidden segments (such as `.ssh`) are always rejected, and an out-of-bounds path answers 403 echoing the resolved path |
##
Known boundaries
Registered honestly, one entry per class.
- **Nothing is truncated for you.** Directory listings, conversation text and role bodies go in whole. Snippets are omitted whole rather than shortened: one over the per-file cap or past the 2 MiB total budget is counted and left out. A context overflow is the model's error to report and yours to slim - this plugin will not silently drop half your draft.
- **The import card has no session context.** Its POST sends `sessionId` as an empty string, so an import must live inside the built-in `agents/` or a registered root, and must contain no `.` / `..` / hidden segments. The rejection names the allowed zones and echoes the resolved path.
- **Directory import reaches two levels.** The top level plus one sub-directory level; a third level is only **counted** as `deeper`, never imported.
- **A file whose name cannot be parsed from the first 20 frontmatter lines counts as `unnamed`** and is skipped rather than guessed.
- **Machine translation does not exist here.** The `agents/` role library and template bodies are user data; they are never translated, whichever interface language is active.
- **The polish path is best-effort about the model's answer.** A non-`stop` finish reason fails the whole call; a model that returns prose instead of a rewrite replaces your draft with that prose, unless the draft or input phase changed meanwhile and the write-back is refused.
##
FAQ
| Symptom | What happens |
|---|---|
| Nothing changed after pressing "Polish" | Failures never touch the draft; the button shows an error state for a few seconds with the reason in its tooltip. Usual causes: no session cwd (`sessionId` missing or session unknown), no model selection, a finish reason other than `stop`, the `organizeTimeoutMs` deadline, or the draft/selection changing while the request was in flight (the result is discarded) |
| The model reports a context overflow | Working as designed: input is deliberately not truncated, so the model error is the feedback. A payload above the endpoint body cap gets 413 instead |
| Importing local roles was refused | Absolute paths are refused by default and the card has no session context. The path must live inside the built-in `agents/` or a registered directory and contain no `.` / `..` / hidden segments |
| Fewer entries imported than expected | Read the four `skipped` counters: `deeper` (third level, counted only), `tooLarge` (over `maxSnippetBytes`), `unnamed` (no name in the first 20 frontmatter lines), `unreadable` (read failure) |
| Template edits not applied / wrong language | Edits stage locally and take effect for newly opened dropdowns after **Save**. Labels and copy follow the official locale preference; role and template **bodies** are user data and are never machine translated |
##
Development
```sh
pnpm install
pnpm run check # typecheck → lint → client build → host build → types → tests → format
pnpm run gen:defaults # regenerate the curated set from agents/ (dev-time only)
```
- **`test/gen-defaults.test.ts` is a drift lock.** It compares the generated file field by field against the role library, checks both mirror tables' key order, and requires **byte-level** identity with what the generator produces. Editing the parsing or extraction logic means editing `scripts/gen-defaults-lib.mjs` **and** re-running `pnpm run gen:defaults`; anything else shows up as a red test rather than a silently stale dropdown.
- The generator exits non-zero when a curated slug is missing or fails to parse, so a role can never disappear silently.
- The rest of the suite splits the classes of defect a stub cannot see: the host half, the client half, the Remote contract (`test/dir-prep-remote.test.ts`), the two build artifacts (`test/build-host.test.ts`, `test/build-client.test.ts`), and an integration layer.
- Coverage thresholds are 100% across four metrics, so a new branch needs a case that reaches it.
##
License
MIT. Source at [github.com/JayYuen666/dsh-dir-prep-organize](https://github.com/JayYuen666/dsh-dir-prep-organize). Issues go to that repository's tracker.
`agents/` is vendored from [`agency-agents-zh`](https://github.com/jnMetaCode/agency-agents-zh) (MIT); see [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md).