diff --git a/README.md b/README.md index ae20514..262ceda 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ are in without remembering flags. | `/slopo-analyze-ignore` | Triages the whole codebase's clusters, writing the obvious false positives into the ignore file. | | `/slopo-analyze-one` | Reports one cluster at a time — optionally by hash — and judges whether it is worth acting on. | | the `slopo` tool | Resolves the CLI, materialises the config, runs the work, and turns slopo's terse failures into something actionable. | -| the `dsh-slopo` settings namespace | The embedding backend and per-workspace state, registered on the Host. | -| the settings card | A form for all of it, in Settings → Plugins → Plugin configuration. | +| the `dsh-slopo` configuration | The embedding backend and per-workspace state. It is the Host plugin's own `Config`, so the deployment's schema and the form can never disagree. | +| the settings card | A form for all of it, on the **Plugins** page under the `dsh-slopo` bundle. | + All three skills are **user-invocable and deliberately not model-invocable**, matching slopo's own agent configs. Typing `/slopo-review` anywhere in a message injects that skill; the model will not reach for one on its own. Every one of @@ -41,23 +42,31 @@ allowlist a build before the first install can succeed. ## Settings -The plugin contributes a card to **Settings → Plugins → Plugin configuration**, -keyed on the `dsh-slopo` settings namespace the Host registers. Every setting in -the table below is editable there, overridden fields are marked, and Reset -returns a field to the deployment default rather than to empty. +The plugin ships a configuration page on the **Plugins** page — the sidebar +entry that lists bundles, rows and official plugins — reached by opening the +`dsh-slopo` bundle and pressing **Configure**. Its key is the bundle's package +name, which is also the namespace: the Loader entry id the bundle's patch +declares. -The card behaves the way the other plugin cards do: it starts collapsed, its -header is a button that discloses the controls in place, the header marks it when -it holds unsaved edits — visible even while collapsed, since staged edits outlive -being folded away — and a save that lands collapses it again. +That the namespace is the entry id is the point. Since dsh 0.1.7-rc.2 the +framework projects this plugin's exported `Config` schema onto the entry that +composed it, so there is one source of truth for what is configurable. Every +field is marked `volatile()`, which is what puts it on the page, and +`embeddingApiKey` additionally carries `role('secret')`, so the browser is told +whether a key is stored and never receives the key itself. + +Every setting in the table below is editable there; overridden fields are +marked, a draft that is not a valid value blocks the save, and Reset returns a +field to the deployment default rather than to empty. A save is one +revision-fenced write, so a second editor cannot be silently overwritten. The card is the bundle's second half — the browser half. It needs no build step and ships as plain ESM in the module-loader wrapper, but the platform snapshots client bundles at activation, so **a client half only appears after the profile -restarts.** Without it the namespace still works — it is editable in -`$DSH_HOME/settings.yaml` and through the tool's `configure` action — there is -simply no form drawn for it, because the tab renders the intersection of served -namespaces and registered cards. +restarts.** Without it the namespace still works — it is editable in the +profile's patch layer and through the tool's `configure` action — there is +simply no form drawn for it, because the page only renders a bundle that +registers a configuration entry for itself. ## Configure it @@ -84,7 +93,9 @@ Then set it up: } ``` -Values land in the `dsh-slopo` section of `$DSH_HOME/settings.yaml`. +Values land in the `config` of the `dsh-slopo` row in the profile's patch layer +(and, from the browser, in the same row through the settings domain — the card +and the tool write the same place). | Setting | Meaning | | --- | --- | @@ -228,6 +239,29 @@ Every action indexes first so the gate runs before anything expensive, and ## Things worth knowing +**The harness seam moved under this plugin once, and the failure was silent.** +dsh 0.1.7-rc.2 replaced three things this bundle used: + +| was | is | +| --- | --- | +| `ctx.settings.installSection(ctx, ns, Config, config, …)` | the exported `Config` itself, projected onto the Loader entry; `ctx.settings.configure({ auto: false })` claims the entry's own page | +| a plugin-chosen settings namespace + a `settings.plugin.item` card | the entry id, plus a `plugins.bundle.config` card keyed by the bundle's package name | +| `ctx.shell.run(spec)` | `await ctx.shell.execute(ctx.shell.resolve(request)).result()` | + +All three broke quietly rather than loudly. `installSection` was missing, so the +call threw into a `try/catch` that logged a warning nobody reads; the card's slot +and the `settingsScope` service it asked for no longer existed, so the card +simply never rendered; and `shell.run` threw only on the paths that actually run +a command. On top of that, a `Config` field that is not marked `volatile()` is +invisible to every settings UI, and the `jobs` registry wants `exec.agent.id` +(a session id) as its owner — handing it the agent object registered a job no +session could collect. + +Two habits follow. First, this bundle's tests bind to the *installed* harness's +own slot catalog and package exports, so a rename fails a test instead of +quietly emptying a page. Second, the browser half keeps its slot and service +names in exactly one place each, so the next rename is a one-line edit. + **Five settings are frozen at first index.** `source_dir`, `embedding_model`, `embedding_dimensions` and both node-count thresholds are written into `slopo.db` on the first `index`, and every later run refuses to mix generations: @@ -296,18 +330,38 @@ that silently does nothing. Two real bugs were caught this way: a config renderer that would write the API key to disk if handed one, and `--config-version` placed before the subcommand, which slopo rejects with a usage error. +The mock's seams are shaped like the harness's, deliberately, because that is +what makes this file a contract test. `ctx.shell` exposes `resolve`/`execute` +with a handle whose `result()` settles the run — there is no `run(spec)`, which +is exactly the method dsh 0.1.7-rc.2 removed. `ctx.settings` has no +`installSection` either; it has `configure` and `update`, and the tests assert +that the plugin claims its own page and writes to the entry id the bundle patch +declares. The `jobs` fake records the spec, so the test can check that a +background job is owned by a **session id** and that its `done` resolves a +`JobOutcome` carrying `result`. + The browser half has no browser to run in, so its tests evaluate the bundle the way the real module loader does — inside `window.__ModuleLoader__.load` — with a -fake `require` that answers `react` and throws on anything else. That last part is -deliberate: it checks the card stays inside the platform's seed table, which is -the constraint a hand-written client half is most likely to break. The card is -then rendered and its handlers driven, which is not a substitute for looking at -it but does cover the wiring it cannot work without. +fake `require` that answers `react` and `@deepseek-ai/dsh-client-ui-primitives` +and throws on anything else. That last part is deliberate: it checks the card +stays inside the platform's seed table, which is the constraint a hand-written +client half is most likely to break. The card is then rendered and its handlers +driven — including the field specs, run through a copy of the shipped +`SettingsFormModel` staging semantics — which is not a substitute for looking at +it but does cover the wiring it cannot work without. Three further tests read the +*installed* harness instead: that `plugins.bundle.config` is a declared slot and +`settings.plugin.item` is gone, that the slot is dispatched by bundle package +name, and that every primitive the card requires is still exported. They skip +when no global dsh installation is present. `node_modules/@deepseek-ai` is a local symlink to the installed harness so the `@deepseek-ai/*` imports resolve during tests. It is gitignored and is not a -declared dependency — an out-of-tree bundle that depended on an in-box package -would install a second copy that drifts from the host's. +declared dependency — `peerDependencies` states the compatibility contract +instead, and an out-of-tree bundle that depended on an in-box package would +install a second copy that drifts from the host's. The declared peers are what +the harness's compatibility gate reads: a plugin whose `@deepseek-ai/dsh-*` +ranges do not admit the running version is refused at startup rather than +half-loaded. ## Releases and updating @@ -321,14 +375,20 @@ would install a second copy that drifts from the host's. `vX.Y.Z`; 4. creates the Gitea release with those notes as the body. -The bump is a patch unless a commit in the release says otherwise: +The bump is read from the commit subjects in the release, so the intent travels +with the change instead of living in a file someone has to remember to update: | in a commit subject | effect | |---|---| -| `[minor]` | release as a minor version | -| `[major]` | release as a major version | +| `feat`, as its own word (`feat:`, `feat(x):`) | minor — `0.1.0` → `0.2.0` | +| `feat!:` / `fix(x)!:` / `BREAKING` | major — `0.1.0` → `1.0.0` | +| `fix:`, `docs:`, prose, anything else | patch — `0.1.0` → `0.1.1` | +| `[minor]` / `[major]` | force the bump when the subject cannot carry it | | `[skip release]` | this push publishes nothing | +Only the **subject** line is read, and `feat` has to be a word — `Fix the feature +flag leak` is a patch, not a minor. + With no tags yet, the version already in `package.json` is what gets tagged — the first release should match what the code says, not invent a number. @@ -338,8 +398,16 @@ commits carry one; otherwise the notes are a flat list. Prose commit subjects ge no invented headings. The logic lives in `.gitea/scripts/release.sh` so it runs without a runner, and -`npm run test:release` exercises it — 31 checks, including an empty range, a -repository with no tags yet, and a patch number already at 9. +`npm run test:release` exercises it — an empty range, a repository with no tags +yet, a patch number already at 9, a `feat` that is only a word. `npm run +release:plan` says whether the current HEAD would release, and `npm run +release:notes` prints the notes it would publish, both without releasing. + +The runner needs nothing but Gitea's own job token (`secrets.GITEA_TOKEN`), which +`permissions: contents: write` in the workflow is what unlocks. Two caveats worth +knowing before the first push: Actions has to be enabled on the repository, and a +runner with the `ubuntu-latest` label has to be registered against the instance — +otherwise the run sits queued forever. ### Making `dsh plugin update` work diff --git a/client/client.js b/client/client.js index b223323..0e56b8b 100644 --- a/client/client.js +++ b/client/client.js @@ -1,26 +1,37 @@ /** - * dsh-slopo — browser half: the card in Settings → Plugins → Plugin configuration. + * dsh-slopo — browser half: the bundle's configuration page on the Plugins page. * - * The Host half registers a `dsh-slopo` settings namespace. This file registers - * a card under that exact namespace key. Both are required: the tab renders the - * INTERSECTION of the namespaces the Host serves and the cards the browser - * claims, so a served namespace with no card draws nothing at all, and a card - * whose namespace is not served is never dispatched. + * Where the card lives, and why it moved. Up to dsh 0.1.6 the settings surfaces + * were: a settings namespace a plugin registered for itself, plus a + * `settings.plugin.item` card the plugin's client half registered under that + * namespace key, rendered inside Settings → Plugins → Plugin configuration. + * 0.1.7-rc.2 removed both halves of that contract: * - * The chrome follows the framework's `PluginCard`: the root is an `
  • ` (the - * tab renders every card into a `