Skip to content

Release v1.5.3 — devtronic installs for every project, and stops writing over what it cannot read - #56

Merged
r-bart merged 7 commits into
mainfrom
fix/plugin-paths-and-global-install
Aug 26, 2026
Merged

Release v1.5.3 — devtronic installs for every project, and stops writing over what it cannot read#56
r-bart merged 7 commits into
mainfrom
fix/plugin-paths-and-global-install

Conversation

@r-bart

@r-bart r-bart commented Aug 26, 2026

Copy link
Copy Markdown
Owner

What this is

Two path bugs that have been broken since marketplace mode landed, two session hooks that were only safe inside a devtronic project, a published README that outlived the hooks it documented — and devtronic init --global, the install the CLI could never do.

Fixed

  • /devtronic-help told correctly installed projects they had no skills. It enumerated .claude/skills/ with a find, which plugin mode leaves empty, and fell into its "No devtronic skills detected" branch. doc-sync counted the same empty directory and reported documentation drift every time.
  • The architecture rules were read from a path the CLI writes nowhere. architecture-checker looked for .claude/architecture-rules.md; init generates .claude/rules/architecture.md. The agent worked by falling back to CLAUDE.md, and told users to hand-write a file devtronic had already generated elsewhere.
  • The session hooks touched repos that do not use devtronic. checkpoint.sh created thoughts/ on PreCompact anywhere it fired; SessionStart orientation was a Haiku call on every startup. Both matter far more once the plugin is installed at user scope, where they run in every repo you open.
  • The published README advertised the Stop gate for two releases after it was deleted — in a repo carrying a "do not send PRs here, this is auto-generated" notice. The notice was wrong: the sync never touched the README. Its hook list is now derived from the hooks.json that ships.
  • A settings file that could not be parsed was written over. readClaudeSettings answers unparseable JSON with {} so a read-only caller can carry on; six call sites reused it for a read-modify-write, so a trailing comma replaced .claude/settings.json with devtronic's two keys — permissions, hooks and env with it. update accounted for four.

Added

  • devtronic init --global — registers the marketplace in ~/.claude/settings.json (or CLAUDE_CONFIG_DIR) and writes nothing else. The plugin was always installable this way through Claude Code's own UI; the CLI was the one path that could not, because every command resolved its destination as resolve(options.path || '.').
  • userConfig on the published pluginprofile and afk reach hooks as CLAUDE_PLUGIN_OPTION_<NAME>, giving them a home at user scope.

Notes for review

  • The mirrored script pairs had drifted three times (the Stop gate, version-check.sh, and checkpoint.sh again — found while writing this). They are now generated from the template's own text, so they cannot drift by construction, and the hooks.json pair gained a whole-object comparison.
  • The parity suite asserts that neither side calls a model on SessionStart. Without it, dropping the prompt hook would have left the model-pinning check comparing undefined to undefined: green, and verifying nothing.
  • 1.5.3 is a patch containing an additive feature. Deliberate.

Tests: 1189 passing (+49). Verified from a clean npm ci, from the installed tarball, and by running the release sync against a fresh clone of the plugin repo.

r-bart added 7 commits August 26, 2026 20:20
When the CLI moved to marketplace mode, `init` stopped copying skills and
agents into `.claude/` — but the content kept describing the old layout, and
two things broke quietly and stayed broken.

`/devtronic-help` enumerated `.claude/skills/` with a `find`, got nothing in
plugin mode, and fell into its "No devtronic skills detected in this project"
branch — the discovery skill telling a correctly installed user their setup
was missing. It now reads its own `## Skill Categories` table, which is the
authority, and scans `.claude/` only for the user's *own* skills. `doc-sync`
counted the same empty directory and reported documentation drift every time.

`architecture-checker` read `.claude/architecture-rules.md`, a path the CLI
writes nowhere; what `init` generates is `.claude/rules/architecture.md`. The
personalized rule was never read by its real name, the agent worked by falling
back to CLAUDE.md, and its error message told the user to hand-write a file
devtronic had already generated somewhere else.

Both are prose, which is why neither had a test. The new one reads the
shipped templates and fails on the next such drift.
Both session hooks assumed they only ever ran where devtronic was installed.
That holds for a project install and stops holding the moment the plugin is
enabled at user scope, where the hooks fire in every repo the user opens.

`checkpoint.sh` did `mkdir -p thoughts/checkpoints` unconditionally on
PreCompact, so a repo that never asked for devtronic got a `thoughts/`
directory the first time a session compacted. It now returns early unless
`thoughts/` or `.ai-template/` is already there.

SessionStart orientation was a `type: prompt` hook pinned to Haiku: a model
call on every startup, to ask about a `thoughts/STATE.md` that usually does not
exist. It is now `orient.sh`, which reads the same file and emits the same
context on stdout — the mechanism the official Vercel plugin uses for exactly
this — and costs nothing where devtronic is absent.

Also adds the parity test these mirrored scripts never had. `checkpoint.sh` had
already drifted between the generator and the marketplace template (a comment
and one block's indentation): harmless, and the same shape as the two drifts
that were not — the `Stop` gate outliving its removal, and `version-check.sh`.
Both scripts are now generated by escaping the template's own text, so they
cannot drift by construction rather than by discipline.

The parity suite gains a whole-object comparison of the two `hooks.json`
copies. The existing checks compared event names and pinned models, which is
what let the details differ unnoticed; `generateHooks()` takes no arguments, so
there is nothing legitimate for the two to disagree about. It also asserts that
neither side calls a model on SessionStart — without that, dropping the prompt
hook would have left the model-pinning check comparing undefined to undefined:
green, and verifying nothing.
…ocuments

The plugin repo's README advertised "**Stop**: Quality gate before stopping"
for two releases after the Stop gate was deleted — in a repo that carries a
"do not send PRs here, this is auto-generated" notice. The notice was wrong:
the sync mirrors skills, agents, hooks and scripts, and never touched the
README. The hook list is now derived from the `hooks.json` that actually ships,
so the one claim in that file that could go stale cannot.

The script already refused to publish a hook pointing at a script the plugin
does not carry. It now gives the manifests the same treatment and runs
`claude plugin validate` over both, skipped with a notice where the CLI is not
on PATH — which is the case in CI, so this cannot break a release.

Also declares `userConfig` on the generated `plugin.json`. It is what gives
`profile` and `mode` a home at user scope: `devtronic mode` writes into a
project, which means nothing for a plugin installed globally. Values reach
hooks as `CLAUDE_PLUGIN_OPTION_<NAME>`. The schema takes type/title/description
and accepts only string|number|boolean|directory|file — there is no enum — so
the profile's values live in its description and mode is expressed as the
boolean `afk`, where the type does the constraining.
The plugin has always been installable for every project through Claude Code's
own UI — `/plugin install devtronic@devtronic`, which defaults to `--scope
user`. The CLI was the one path that could not: every command resolved its
destination as `resolve(options.path || '.')`, so the only scope that existed
was "the current project".

`devtronic init --global` registers the marketplace in `~/.claude/settings.json`
(or `CLAUDE_CONFIG_DIR`) and writes nothing else. No CLAUDE.md, no rules, no
`thoughts/` — there is no project here to derive them from, and a project-level
`init` stays the way to get them. It is a separate path rather than the same
700-line flow behind conditionals, because almost all of that flow exists to
analyze a project.

A project install that finds a user-scope one no longer re-registers the plugin,
and says so instead of claiming a registration it did not perform. `doctor`
learned the same rule: without it, it would report a correct global install as
a fault and `--fix` would write back exactly the entry `init` decided to leave
out — the two commands undoing each other.

Two things a user-scope target makes dangerous that a project target did not:

- The standalone-era hook sweep is now confined to project scope. It rests on
  "everything matching these signatures was written here by devtronic", which is
  true of a project and false of the user's own settings file, where the
  signatures (`${CLAUDE_PLUGIN_ROOT}/scripts/…`, `npx eslint --fix --quiet`)
  name no plugin in particular. It was deleting other plugins' hooks.
- Registering is a read-modify-write, and the read answers unparseable JSON with
  `{}` so callers can carry on. Landing that write on `~/.claude/settings.json`
  replaced the user's theme, model, permissions and autoMode — a file kept in no
  repository — over a trailing comma. It now refuses and names the file to fix.

`resolveScope` resolves user scope to `~/.claude`, not `~`: a caller that
prepends `.claude/` would otherwise produce `~/.claude/.claude/`, and a
CLAUDE.md written at `~` is read by nothing. `CLAUDE_CONFIG_DIR` wins where set,
so a multi-account setup does not get the wrong profile configured silently.
CI runs Node 20/22/24, matching `engines.node >=20`; the note still said
18/20/22.

The gotcha is the one lesson from this change that no test can catch: several
suites mock `utils/settings.js` with only the exports they happened to need, so
a new import arrives as `undefined` and kills the code under test at the call
site. The symptom surfaces far from the cause — an ENOENT on a file the aborted
function never reached.
`readClaudeSettings` answers unparseable JSON with `{}` so a read-only caller
can carry on. Six call sites reused it for a read-modify-**write**, so a
trailing comma — the commonest JSON typo there is — replaced
`.claude/settings.json` with devtronic's two keys, taking the project's
permissions, hooks and env with it. `update` alone accounted for four of them.

The guard now lives in the read every write path shares, so refusing is
structural rather than something each caller has to remember. Auditing the
writers rather than smoke-testing one command is what turned up
`unregisterPlugin`, which uninstall reached through and which still used the
tolerant read: removing devtronic is no licence to erase the file.

What each caller does on refusal depends on what it does next:

- `init` stops before its first write, so nothing is half-configured.
- `update` and `regenerate` skip the registration, say so loudly, and finish
  the rest — the remaining work is files devtronic owns, and aborting all of it
  over one unreadable file helps nobody.
- Both marketplace migrations abort. The steps after registration delete the
  standalone files and the local plugin directory, and doing that with the
  plugin unregistered leaves the project with neither.
- `doctor` reports the malformed file as its own failure, which is more useful
  than "plugin not registered" — that is merely what an unreadable file looks
  like — and marks it not fixable, since `--fix` would register into it.

The same defect at user scope was fixed with the `--global` install; this is
the project half, which predates it and which `update` puts in front of far
more people.
@r-bart
r-bart merged commit 3d7a0f7 into main Aug 26, 2026
3 checks passed
@r-bart
r-bart deleted the fix/plugin-paths-and-global-install branch August 26, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant