Project-level conventions for changes under plugins/specode/. Read
this before opening a PR or cutting a release.
Any runtime code under plugins/specode/scripts/ MUST use only the
Python standard library. Plugin users install via the host CLI's
plugin install; they don't pip install -r requirements.txt.
Pulling third-party packages in either silently breaks for users
without them or forces a heavier install path.
Tests under plugins/specode/tests/ MAY use pytest (it's a dev
dependency, not runtime).
Every script under plugins/specode/scripts/ is a CLI invoked from
hook commands (hooks.json) or directly by the main agent. All
invocations MUST go through the run.sh wrapper with the full
$CLAUDE_PLUGIN_ROOT (fallback $CODEBUDDY_PLUGIN_ROOT) path:
sh "${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/scripts/run.sh" \
"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/scripts/<name>.py" \
<verb> <args...>Why:
run.shprobespython3 → python → pyso it works on any host with Python 3.8+ on PATH.- Both
CLAUDE_PLUGIN_ROOTandCODEBUDDY_PLUGIN_ROOTare platform-injected env vars; the:-fallback covers both Claude Code and CodeBuddy without forcing the user to pick one. - Bare
python3 resolve_root.py …calls fail in most cwds because the scripts are not on PATH and the agent doesn't know where it is. SeeSKILL.md(Iron Rules) for the hard rule.
hooks/hooks.json and the commands/spec.md invocation sections all
use this template — match them when adding new entry points.
Run the suite from the repo root:
python3 -m pytest plugins/specode/tests/ -vThe lite suite currently covers resolve_root.py: 3-tier specsRoot
resolution priority, set-root persistence + absolute-path rejection,
and list-specs behavior. There is no state machine, lock,
selector-drift, or template-lint test anymore — those mechanisms were
removed in 1.0.0.
When adding behavior, prefer:
- Unit tests that call the CLI script through
subprocess.runvia therun_scriptfixture (the scripts are CLIs, not importable modules). - Use the
fake_homefixture to redirect$HOME/XDG_CONFIG_HOMEand clearSPECODE_ROOT, keeping tests isolated from the real~/.config/specode/. - For hook tests, feed stdin payloads matching the host CLI hook schema
and assert against the JSON
additionalContext.
The single hook handler in spec_hooks.py (SessionStart) MUST:
- Catch all exceptions internally and return 0.
- Never
exit 2. It is advisory only. If you need to influence the model, injectadditionalContextJSON to stdout and stillexit 0. - Tolerate non-TTY / empty stdin (hook payload arrives via pipe). The script must not block when stdin is a TTY.
The plugin owns one persisted file:
~/.config/specode/config.json— currently holds onlyspecsRoot(the user's document directory, used verbatim as the specs root).
There is no per-session state file and no per-spec config/lock file
anymore — spec state is inferred from the documents on disk (which
fixed docs exist + - [ ] progress in design.md). Config writes use
tempfile + os.replace + fsync (_atomic_write_json in
resolve_root.py).
Public release procedure for plugin maintainers.
Two manifests carry version. They MUST match or the plugin tag
tooling refuses to operate:
plugins/specode/.claude-plugin/plugin.json→"version": "X.Y.Z".claude-plugin/marketplace.json→ the specode entry'sversion(plugins[0]; leave the task-swarm entry untouched)
"API surface" for semver purposes (1.0.0+) = the /spec subcommand set
(/spec <需求> / /spec continue <slug> / /spec list), the
SessionStart hook event, the persisted config.json.specsRoot field,
and the 3 fixed document filenames (requirements.md / design.md /
implementation-log.md) that users or future runtime code observe.
| Bump | When | Examples |
|---|---|---|
| major | A user feels a breaking change after a plugin update | rename / remove a /spec subcommand; rename a hook event; rename config.json.specsRoot; rename a fixed document filename |
| minor | Backwards-compatible new capability or evolution | new /spec subcommand; new optional config field; new selector option |
| patch | Bug fix / docs / internal refactor with no surface change | fix a typo in a prompt; clarify a reference; CI-only; remove dev-only files from the repo |
When in doubt, bump higher.
# 1. Bump both manifests to the new version
$EDITOR plugins/specode/.claude-plugin/plugin.json
$EDITOR .claude-plugin/marketplace.json
# 2. Land CHANGELOG.md: rename `## Unreleased` → `## X.Y.Z (YYYY-MM-DD)`,
# then add a fresh empty `## Unreleased` above it for the next cycle
$EDITOR CHANGELOG.md
# 3. Run the test suite one more time
python3 -m pytest plugins/specode/tests/ -q
# 4. Commit + push
git commit -am "Bump to X.Y.Z: <summary>"
git push
# 5. Dry-run the tag first
claude plugin tag --dry-run plugins/specode
# (or codebuddy plugin tag --dry-run plugins/specode — pick whichever
# host CLI is installed; both wrap the same git operations)
# 6. Create + push the annotated tag
claude plugin tag plugins/specode --pushTag format: specode--v{version} (annotated, message
specode {version}). The plugin is not packaged into a tarball
or registry artifact — host CLIs fetch the marketplace manifest
directly from GitHub and resolve plugins by git tag. Pushing the
tag IS the release.
Only safe if no user has installed it yet:
git tag -d specode--vX.Y.Z
git push --delete origin specode--vX.Y.Z
claude plugin tag plugins/specode --push # re-createOnce a release is in user hands, prefer a new patch version.
# Adjust the CLI name for whichever host you use (claude / codebuddy).
claude plugin marketplace update qxbyte
claude plugin install specode@qxbyte # or `update`
claude plugin list | grep specode # confirm new versionUsers on a different host follow the same procedure with their host's
CLI name (codebuddy plugin …).