A dsh plugin installed as a bare git URL is pinned to a commit in the profile
lockfile. With no tags in the repository there was nothing for an update to move
to, which is why updating appeared to do nothing and a reinstall was the only way
through.
Gitea action on push to master: work out the range since the last tag, decide
whether the push releases at all, bump package.json, prepend a CHANGELOG section,
commit, tag vX.Y.Z, and publish the release with those notes as the body.
The bump is a patch unless a commit says [minor] or [major]; [skip release]
publishes nothing. With no tags yet the declared version is what gets tagged, so
the first release matches the code. Notes come from commit subjects, grouped under
headings only when the range actually uses conventional prefixes.
The logic is a script rather than inline YAML so it runs without a runner:
npm run test:release covers 31 cases, including an empty range, a repository with
no tags, and a patch number already at 9. The empty-range guard is load-bearing:
the release commit is itself a push to master.
Two bugs the tests caught: an empty note section made '[ -n x ] && printf' return
non-zero, which under set -e aborted the whole release, and a bullet beginning
with '-' was read by grep as an option.
README documents the #semver: spec, which tracks releases rather than every
commit, and the two things that look like a failed update: an unfetchable remote,
and the profile restart client bundle bytes require.
The settings namespace was registered but nothing drew a form for it, because
the Plugin configuration tab renders the INTERSECTION of the namespaces the Host
serves and the cards a browser half claims. A served namespace with no card
renders nothing at all. This is the missing card.
Plain ESM in the module-loader wrapper, no build step, requiring only `react` —
which is in the platform seed table. Anything outside that table would have to be
bundled into this file or declared in `dsh.client.external`, and the tests fail
loudly on any other specifier.
It stages edits and writes them on Save; a cleared field is an `unset` rather
than a write of "", so the value re-inherits the composition layer instead of
becoming empty, and the user layer's presence is what marks a field overridden.
Reset clears every override. A namespace this client cannot reach renders
nothing, so a deployment without the Host half shows no trace.
package.json gains an `exports` map (the loader reads `exports["./client"]`; a
bare `main` would not be enough) and the `dsh.client` declaration.
The integration tests load the real plugin module against a thin mock Cordis
context and drive the tool through a real shell into the real slopo binary. The
mock implements only the seams the plugin touches, so a wrong assumption about a
dsh service fails a test instead of producing a plugin that silently does
nothing at runtime.
Two real bugs were caught this way and are now pinned:
* renderSlopoConfig would write the embedding API key to disk if it were
handed one. It cannot now.
* --config-version was placed before the subcommand, which slopo rejects with
exit code 2. It is declared on the command, not the callback.
The end-to-end test is opt-in (DSH_SLOPO_E2E=1) because it needs a reachable
embedding backend; it indexes a real repository, embeds it, and asserts a real
duplicate cluster comes back.
The three SKILL.md files are ports of the configs slopo 0.8.0 ships for
Claude Code, not new prose. What changed is the mechanics:
* name + description frontmatter, both required by dsh
* disable-model-invocation: true kept verbatim -- dsh has the same key,
and slopo makes that choice for a reason (every one of these commands
can spend minutes embedding)
* allowed-tools / argument-hint / arguments dropped, not dsh keys
* the !`slopo ...` shell-injection directive replaced with an
instruction to call the slopo tool plus an explicit failure check,
because dsh skill bodies do not execute commands
The guidance prose is quoted from slopo; NOTICE.md records the mapping and
the licence obligation that follows from it (AGPL-3.0-or-later, matching
upstream and the LICENSE file fetched from it).