Add devin plugin format and marketplace tree - #153
Merged
Merged
Conversation
Teaches chock's emitter `--format devin` (a native `.devin-plugin/` package: `plugin.json`, `skills/<id>/SKILL.md`, and a root-level `hooks.json`, mirroring codex.py/cursor.py function for function) and `chock marketplace build --tree devin`, pinned to agentseam 0.3.2, which records Devin's native plugin layout. Devin's manifest documents no plugin-root token for hook commands, only that they receive a `$DEVIN_PLUGIN_ROOT` environment variable, so the hook command is chock's own shell expansion of that variable rather than a vendor-recorded token. The package claims no enforcement tier: `posture.enforced_devin()` states the vendor's own fail-open, best- effort caveat instead. Devin has no marketplace index file, so `--tree devin` writes a root `.devin-plugin/plugin.json` meta-plugin whose `optionalPlugins` point `git-subdir` entries at each built plugin, gated on a new `--url` option. Split `marketplace.py` into `marketplace_core.py` (tree-generic index/ lock/catalog logic) and `marketplace_devin.py` (the root-manifest path) to keep every file under the repo's 300-line review budget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
agentseam 0.3.2 records Devin's hook_entry as {bare: true, matcher: true,
wrapper: hooks_map}: its own hook_config("devin", ...) returns the bare
{"PreToolUse": [...]} map, with no top-level "hooks" key. codex_cli and
claude_code carry no `bare` flag, so their hooks.json is wrapped in
{"hooks": {...}} -- the shape hooks_map_file() hardcoded for every
vendor. Reusing it for devin unconditionally wrapped its native
hooks.json too, which risks the hooks silently never loading in Devin,
the exact failure mode this plugin format exists to avoid.
hooks_map_file() now reads a vendor's own `hook_entry.bare` flag
(chock.vendors.hook_entry_bare(), new) and omits the wrapper when set.
codex_cli/claude_code/vscode_copilot output is unchanged (verified
byte-identical: their tests, and the runtime/emitter-stability goldens,
are untouched by this commit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
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
Step 2 of the
chock-devin-pluginsdistribution repo work (org-planplan/devin-plugins-repo.md§3 PR 2): teaches chock's emitter--format devinand--tree devin, pinned toagentseam==0.3.2(on PyPI today), which records Devin's native plugin layout.Per-file changes
pyproject.toml,requirements/brand-assets.in/.txt— bumpagentseam==0.3.1→0.3.2.requirements/brand-assets.txtregenerated withpip-compile --generate-hashes(never hand-edited), diff is the version line + hashes only.src/chock/plugin/devin.py(new) — mirrorssrc/chock/plugin/codex.pyfunction for function:build_devin_manifest,devin_plugin_files,stale_devin_files,build_devin_plugin,devin_plugin_differences,_LAYOUT = packaging.layout("devin"). Native layout confirmed from agentseam 0.3.2: manifest.devin-plugin/plugin.json, skillsskills/{name}/SKILL.md, hooks at the plugin root ashooks.json(nothooks/hooks.json).command/executable/plugin_rootare all unrecorded (packaging.plugin_root("devin")isNone), and agentseam'sdeclaresconvention (the"./skills"codex/cursor use) is alsoNonefor this vendor, so the manifest'sskillsfield is the plainest value that satisfies the vendor's own doc ("plugin-root-relative, no.."): a bare"skills". Manifest fields are restricted to what the vendor documents (name,version,description,skills) — noauthor/license/repository/keywords/interface, and nohookskey (Devin auto-discovershooks.jsonat the plugin root; nothing declares it, unlike codex/cursor).hooks_map_file("devin", ...)fromin_agent.pyis reused, now taught to read the vendor's ownbareflag (see the fix note below) — devin's isTrue, so no top-levelhookswrapper is emitted, andvendors.shell_matcher("devin")correctly returnsNone(unrecorded shell vocabulary) so nomatcherkey is emitted either.src/chock/plugin/posture.py— addedenforced_devin()besideenforced_codex(). Carries the vendor's own fail-open caveat near-verbatim ("currently best effort and fail open -- if a hook fails to load or run, the session continues without it -- so don't rely on them for crucial guardrails yet"), scoped to "local Devin sessions (the CLI and Devin Desktop)" only, and states that$DEVIN_PLUGIN_ROOTexpansion inside a hook command is documented as an environment variable, not witnessed. No enforcement-tier word (enforced,block) appears in the returned text — pinned bytest_devin_posture_never_claims_enforcement.src/chock/plugin/cli.py—FORMATSandHOOK_FORMATSgain"devin";HOOK_EMITTERS["devin"] = (devin_plugin_differences, build_devin_plugin); import added beside the codex import; the printed format list gains a devin line naming.devin-plugin/plugin.json + hooks.json.src/chock/plugin/store.py—stale_store_files(and by extensionbuild_store_plugin/store_plugin_differences) now also recognizes anowned_subtreesentry that names a plain file, not only a directory. Every existing store's entries are directories (hooks/,scripts/); Devin'shooks.jsonsits at the plugin root instead, so without this a policy that lost its guard script would leave a stalehooks.jsonbehind forever (caught bytest_devin_losing_a_guard_removes_the_root_hook, which mirrors the existingtest_losing_a_guard_removes_the_hookfor codex/cursor).src/chock/plugin/data/stores/devin.json(new) —{"owned_subtrees": ["scripts", "hooks.json"]}.src/chock/plugin/marketplace.py/marketplace_core.py(new) /marketplace_devin.py(new) — split out of the original singlemarketplace.py(see design decision 2 below) purely to keep every file under the repo's CI-enforced 300-line budget; no behavior change to the existing claude/cursor/codex trees.TREES["devin"]added with an emptyindex_paths(no index-file format).render_catalog_page's hook-detection is now per-tree (packaging.supports(CHOCK_AGENT[tree], packaging.HOOKS)instead of a hardcoded"hooks"/"hooks.json", since Devin's hook file is at the plugin root) and its vocabulary ("enforces" vs "best-effort") is now per-tree so the page never claims "enforce"/"block" for Devin.main()gains a--urloption, required (and refused with a clear message, never guessed fromgit remote) when--tree devin.docs/cli-reference.md—plugin build's format list and prose gaindevin;marketplace build's tree list and prose gaindevin+--url. Tightened wording elsewhere in the same file (thesync,checkandcompliance reportsections) to bring the file back to 299 lines, under the 300-line budget — never raised.CHANGELOG.md— one entry under a new## Unreleasedheading, in the voice of the 0.4.0 codex/cursor-support entries. No version bump.tests/fixtures/runtime_goldens/*.py) — all ten regenerated viaCHOCK_REGEN_GOLDENS=1 pytest tests/test_runtime_goldens.py(never hand-edited): version-stamp-only diff (agentseam 0.3.1→0.3.2in two comment lines each), same for this repo's own committed.chock/bin/*.py(this repo dogfoods chock;chock sync --repo .regenerated them the same way — I deliberately reverted the other files that command touched, since they only bake in this sandbox's ownsys.executablepath, unrelated to this change and not somethingtest_compiled_freshness.pychecks).tests/test_devin_plugin.py(new) — shaped liketests/test_cursor_codex_plugin.py's codex sections: built file set, manifest content (only documented fields, nointerface/hookskey, noassets/icon.svg), the exact hook command string, posture text present with no enforcement-tier word, CLI build +--check, and the root-hooks-file staleness case.tests/test_marketplace.py— added devin-tree tests:--urlrequired, root meta-plugin written (not an index file),--checkcatches missing/stale/hand-edited.spec/policy-spec.md,.github/workflows/ci.yml,docs/enforcement-surfaces.md— not changed. None of them enumerate plugin-package formats (enforcement-surfaces.mdcoverschock sync's own compiled surfaces — pre-tool-use/stop/etc — where Devin was already listed, since agentseam 0.3.2 already made it achock syncin-agent vendor before this PR;policy-spec.mdandci.ymldon't reference formats at all), and no test forced a change to either.Fix commit:
hooks.jsonmust be the bare event mapA second commit fixes a real defect a review caught:
hooks_map_file()unconditionally wrapped its output in{"hooks": {...}}, the claude-plugin shape. That's correct forclaude_code/codex_cli/vscode_copilot(none of them record abareflag), but agentseam records Devin's ownhook_entryas{bare: true, matcher: true, wrapper: hooks_map}—agentseam.adapters.get("devin").hook_config(...)returns the bare{"PreToolUse": [...]}map, no top-levelhookskey, matching the vendor's own hooks-page example. A wrapped nativehooks.jsonrisks Devin silently never loading the hook — the exact failure this plugin format exists to avoid. My original PR body claim thathooks_map_file"fit" as-is was wrong on this point.Fixed by teaching
chock.vendors.hook_entry_bare(vendor)(new) to read that flag, andhooks_map_file()to omit the wrapper when it's set.codex_cli/claude_code/vscode_copilotoutput is unchanged — their tests, and every runtime/emitter-stability golden, are byte-identical before and after (verified; nothing needed regenerating, since the pre-existingchock syncpath for devin already called agentseam's ownhook_configdirectly and was already correct — only the new plugin-format path viahooks_map_filehad the bug). New testtest_devin_hooks_shape_matches_agentseams_own_hook_configpins the emitted shape against agentseam's ownhook_configoutput directly.Design decisions
1. The hook command.
packaging.executable_ref("devin", path)returnsNonebecause agentseam records no plugin-root token for Devin (plugin_rootis()): the vendor documents only that hook commands receive aDEVIN_PLUGIN_ROOTenvironment variable (CLI changelog: "Hooks contributed by a plugin also getDEVIN_PLUGIN_ROOT"), not a${...}expansion inside the command string. I did not extend agentseam or invent a token. Instead I emit the native layout and build the command in chock as a shell expansion of the documented variable:python3 "$DEVIN_PLUGIN_ROOT/scripts/devin.py" --guard "$DEVIN_PLUGIN_ROOT/scripts/<guard>.sh". This rests on an inference, stated as such indevin.py's comments and inposture.enforced_devin()'s text: Devin runs hooks in "the same format as Claude Code hooks," which executestype: commandentries through a shell, so$VARexpansion should work the same way — but that expansion inside ahooks.jsoncommand string is not itself witnessed. I checkedstore.py,build.pyandcli.pyfor a helper that refuses aNoneexecutable ref before writingdevin.py; none exists (codex/cursor/claude never hit that path since theirplugin_rootis non-empty), so there was nothing to silently bypass —devin._hook_commandbuilds the string directly and is exercised bytest_devin_guard_policy_layout_and_hook, which pins the exact command.2. The marketplace shape. I took the plan's first option: a second code path in
marketplace.py(now split for line budget intomarketplace_core.py+marketplace_devin.py), not folding it intoplugin build. Devin has no marketplace index file —devin plugins install owner/reporeads a repo's root.devin-plugin/plugin.jsonas a meta-plugin whoseoptionalPlugins/requiredPluginslist each plugin as a{source: git-subdir, url, path}entry (CognitionAI/team-marketplace-templateis the vendor's own template). That's a fundamentally different shape from every other tree's "index file over a built tree," not a parametrization of it, soplugin build(which only ever writes into<out-dir>/<format>/<id>/) was the wrong place for it —marketplace build --tree devinwrites the root manifest over whatplugin build --format devinalready produced, which is exactly whatmarketplace buildalready does for every other tree, just with a different output shape.chock-market.lockandPLUGINS.mdstayed on the existing generic code paths (already parametrized bytree; the only devin-specific patch needed there was the per-tree hook-path lookup forPLUGINS.md's posture column, described above).Verification
Sample run against this repo's own
.agents/policies(rebuilt after the fix):🤖 Generated with Claude Code