Release v1.5.3 — devtronic installs for every project, and stops writing over what it cannot read - #56
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-helptold correctly installed projects they had no skills. It enumerated.claude/skills/with afind, which plugin mode leaves empty, and fell into its "No devtronic skills detected" branch.doc-synccounted the same empty directory and reported documentation drift every time.architecture-checkerlooked for.claude/architecture-rules.md;initgenerates.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.checkpoint.shcreatedthoughts/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.Stopgate 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 thehooks.jsonthat ships.readClaudeSettingsanswers 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.jsonwith devtronic's two keys — permissions, hooks and env with it.updateaccounted for four.Added
devtronic init --global— registers the marketplace in~/.claude/settings.json(orCLAUDE_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 asresolve(options.path || '.').userConfigon the published plugin —profileandafkreach hooks asCLAUDE_PLUGIN_OPTION_<NAME>, giving them a home at user scope.Notes for review
Stopgate,version-check.sh, andcheckpoint.shagain — found while writing this). They are now generated from the template's own text, so they cannot drift by construction, and thehooks.jsonpair gained a whole-object comparison.undefinedtoundefined: green, and verifying nothing.1.5.3is 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.