Skip to content

Add devin plugin format and marketplace tree - #153

Merged
jothimani-rajendran merged 2 commits into
mainfrom
claude/devin-plugin-format
Sep 22, 2026
Merged

jothimani-rajendran merged 2 commits into
mainfrom
claude/devin-plugin-format

Conversation

@jothimani-rajendran

@jothimani-rajendran jothimani-rajendran commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

Step 2 of the chock-devin-plugins distribution repo work (org-plan plan/devin-plugins-repo.md §3 PR 2): teaches chock's emitter --format devin and --tree devin, pinned to agentseam==0.3.2 (on PyPI today), which records Devin's native plugin layout.

Per-file changes

  • pyproject.toml, requirements/brand-assets.in/.txt — bump agentseam==0.3.1 → 0.3.2. requirements/brand-assets.txt regenerated with pip-compile --generate-hashes (never hand-edited), diff is the version line + hashes only.
  • src/chock/plugin/devin.py (new) — mirrors src/chock/plugin/codex.py function 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, skills skills/{name}/SKILL.md, hooks at the plugin root as hooks.json (not hooks/hooks.json). command/executable/plugin_root are all unrecorded (packaging.plugin_root("devin") is None), and agentseam's declares convention (the "./skills" codex/cursor use) is also None for this vendor, so the manifest's skills field 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) — no author/license/repository/keywords/interface, and no hooks key (Devin auto-discovers hooks.json at the plugin root; nothing declares it, unlike codex/cursor). hooks_map_file("devin", ...) from in_agent.py is reused, now taught to read the vendor's own bare flag (see the fix note below) — devin's is True, so no top-level hooks wrapper is emitted, and vendors.shell_matcher("devin") correctly returns None (unrecorded shell vocabulary) so no matcher key is emitted either.
  • src/chock/plugin/posture.py — added enforced_devin() beside enforced_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_ROOT expansion inside a hook command is documented as an environment variable, not witnessed. No enforcement-tier word (enforced, block) appears in the returned text — pinned by test_devin_posture_never_claims_enforcement.
  • src/chock/plugin/cli.py — FORMATS and HOOK_FORMATS gain "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 extension build_store_plugin/store_plugin_differences) now also recognizes an owned_subtrees entry that names a plain file, not only a directory. Every existing store's entries are directories (hooks/, scripts/); Devin's hooks.json sits at the plugin root instead, so without this a policy that lost its guard script would leave a stale hooks.json behind forever (caught by test_devin_losing_a_guard_removes_the_root_hook, which mirrors the existing test_losing_a_guard_removes_the_hook for 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 single marketplace.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 empty index_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 --url option, required (and refused with a clear message, never guessed from git remote) when --tree devin.
  • docs/cli-reference.md — plugin build's format list and prose gain devin; marketplace build's tree list and prose gain devin + --url. Tightened wording elsewhere in the same file (the sync, check and compliance report sections) to bring the file back to 299 lines, under the 300-line budget — never raised.
  • CHANGELOG.md — one entry under a new ## Unreleased heading, in the voice of the 0.4.0 codex/cursor-support entries. No version bump.
  • Runtime goldens (tests/fixtures/runtime_goldens/*.py) — all ten regenerated via CHOCK_REGEN_GOLDENS=1 pytest tests/test_runtime_goldens.py (never hand-edited): version-stamp-only diff (agentseam 0.3.1 → 0.3.2 in 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 own sys.executable path, unrelated to this change and not something test_compiled_freshness.py checks).
  • tests/test_devin_plugin.py (new) — shaped like tests/test_cursor_codex_plugin.py's codex sections: built file set, manifest content (only documented fields, no interface/hooks key, no assets/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: --url required, root meta-plugin written (not an index file), --check catches 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.md covers chock sync's own compiled surfaces — pre-tool-use/stop/etc — where Devin was already listed, since agentseam 0.3.2 already made it a chock sync in-agent vendor before this PR; policy-spec.md and ci.yml don't reference formats at all), and no test forced a change to either.

Fix commit: hooks.json must be the bare event map

A 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 for claude_code/codex_cli/vscode_copilot (none of them record a bare flag), but agentseam records Devin's own hook_entry as {bare: true, matcher: true, wrapper: hooks_map} — agentseam.adapters.get("devin").hook_config(...) returns the bare {"PreToolUse": [...]} map, no top-level hooks key, matching the vendor's own hooks-page example. A wrapped native hooks.json risks Devin silently never loading the hook — the exact failure this plugin format exists to avoid. My original PR body claim that hooks_map_file "fit" as-is was wrong on this point.

Fixed by teaching chock.vendors.hook_entry_bare(vendor) (new) to read that flag, and hooks_map_file() to omit the wrapper when it's set. codex_cli/claude_code/vscode_copilot output is unchanged — their tests, and every runtime/emitter-stability golden, are byte-identical before and after (verified; nothing needed regenerating, since the pre-existing chock sync path for devin already called agentseam's own hook_config directly and was already correct — only the new plugin-format path via hooks_map_file had the bug). New test test_devin_hooks_shape_matches_agentseams_own_hook_config pins the emitted shape against agentseam's own hook_config output directly.

Design decisions

1. The hook command. packaging.executable_ref("devin", path) returns None because agentseam records no plugin-root token for Devin (plugin_root is ()): the vendor documents only that hook commands receive a DEVIN_PLUGIN_ROOT environment variable (CLI changelog: "Hooks contributed by a plugin also get DEVIN_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 in devin.py's comments and in posture.enforced_devin()'s text: Devin runs hooks in "the same format as Claude Code hooks," which executes type: command entries through a shell, so $VAR expansion should work the same way — but that expansion inside a hooks.json command string is not itself witnessed. I checked store.py, build.py and cli.py for a helper that refuses a None executable ref before writing devin.py; none exists (codex/cursor/claude never hit that path since their plugin_root is non-empty), so there was nothing to silently bypass — devin._hook_command builds the string directly and is exercised by test_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 into marketplace_core.py + marketplace_devin.py), not folding it into plugin build. Devin has no marketplace index file — devin plugins install owner/repo reads a repo's root .devin-plugin/plugin.json as a meta-plugin whose optionalPlugins/requiredPlugins list each plugin as a {source: git-subdir, url, path} entry (CognitionAI/team-marketplace-template is 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, so plugin build (which only ever writes into <out-dir>/<format>/<id>/) was the wrong place for it — marketplace build --tree devin writes the root manifest over what plugin build --format devin already produced, which is exactly what marketplace build already does for every other tree, just with a different output shape. chock-market.lock and PLUGINS.md stayed on the existing generic code paths (already parametrized by tree; the only devin-specific patch needed there was the per-tree hook-path lookup for PLUGINS.md's posture column, described above).

Verification

$ python -m pytest -q
1404 passed, 2 skipped in 295.67s (0:04:55)

$ python -m ruff check .
All checks passed!

$ python -m ruff format --check .
291 files already formatted

$ pytest acceptance/ -c acceptance/pytest.ini --rootdir=acceptance -q
21 passed, 106 warnings in 52.59s

$ chock check --repo .        # exit 0
$ chock sync --repo . --check # Compiled artifacts match their manifests.

Sample run against this repo's own .agents/policies (rebuilt after the fix):

$ chock plugin build --repo . --format devin --out-dir /tmp/d
Packaged 23 policies (format: devin)
  .devin-plugin/plugin.json + hooks.json (PreToolUse, best-effort) per guard policy
  Skills are advisory in any client. Repo-level enforcement still needs `chock sync`.

$ cat /tmp/d/devin/block-destructive-commands/hooks.json
{
  "PreToolUse": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "python3 \"$DEVIN_PLUGIN_ROOT/scripts/devin.py\" --guard \"$DEVIN_PLUGIN_ROOT/scripts/block-destructive.sh\"",
          "timeout": 30
        }
      ]
    }
  ]
}

$ chock marketplace build --dist /tmp/d --tree devin --name chock-devin --url https://github.com/open-coder-ai/chock-devin-plugins
Wrote .devin-plugin/plugin.json: a meta-plugin referencing 23 plugins
Wrote chock-market.lock: sha256 per published plugin directory
Wrote PLUGINS.md: 23 plugins with their posture

$ cat /tmp/d/.devin-plugin/plugin.json
{
  "name": "chock-devin",
  "description": "Chock policies packaged as installable plugins. Generated from the chock-catalog; each plugin states whether it is best-effort in this client or advisory.",
  "optionalPlugins": [
    { "source": "git-subdir", "url": "https://github.com/open-coder-ai/chock-devin-plugins", "path": "devin/agent-discipline" },
    { "source": "git-subdir", "url": "https://github.com/open-coder-ai/chock-devin-plugins", "path": "devin/block-destructive-commands" },
    { "source": "git-subdir", "url": "https://github.com/open-coder-ai/chock-devin-plugins", "path": "devin/block-invisible-unicode" },
    { "source": "git-subdir", "url": "https://github.com/open-coder-ai/chock-devin-plugins", "path": "devin/block-no-verify" }
    // ... 19 more, same shape
  ]
}

$ chock marketplace build --dist /tmp/d --tree devin --name chock-devin --url https://github.com/open-coder-ai/chock-devin-plugins --check
Marketplace index matches the plugin tree (23 plugins).

🤖 Generated with Claude Code

jothimani-rajendran and others added 2 commits September 21, 2026 23:48
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>
@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review September 22, 2026 00:58
@jothimani-rajendran
jothimani-rajendran merged commit 22d721c into main Sep 22, 2026
19 checks passed
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