harness-alchemist 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +104 -100
- package/alchemy.schema.json +54 -0
- package/bin/harness-alchemist.mjs +1 -1
- package/lib/create.mjs +4 -0
- package/lib/validate.mjs +196 -76
- package/package.json +2 -1
- package/skills/harness-alchemist/SKILL.md +5 -1
- package/skills/harness-alchemist/references/codex.md +5 -1
- package/skills/harness-alchemist/references/compatibility.md +14 -0
- package/skills/harness-alchemist/references/deepseek-harness.md +6 -0
- package/skills/harness-alchemist/references/opencode.md +5 -1
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/references/compatibility.md.tpl +10 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/check-package.mjs.tpl +1 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/sync-metadata.mjs.tpl +9 -0
- package/templates/v0.1.0/universal-typescript/README.md.tpl +30 -10
- package/templates/v0.1.0/universal-typescript/alchemy.json.tpl +8 -0
- package/templates/v0.1.0/universal-typescript/package.json.tpl +1 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "harness-alchemist",
|
|
4
4
|
"displayName": "Harness Alchemist",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.6",
|
|
6
6
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Haochuan Zhang"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-alchemist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Haochuan Zhang",
|
package/README.md
CHANGED
|
@@ -1,140 +1,144 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/lunarmoon26/harness-alchemist">
|
|
3
|
+
<picture>
|
|
4
|
+
<source srcset="assets/harness-alchemist_dark.svg" media="(prefers-color-scheme: dark)">
|
|
5
|
+
<source srcset="assets/harness-alchemist_light.svg" media="(prefers-color-scheme: light)">
|
|
6
|
+
<img src="assets/harness-alchemist_light.svg" alt="Harness Alchemist logo" height="110">
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">The universal scaffold for coding-agent plugins. One repository, five harnesses, skills that own their runtime.</p>
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/harness-alchemist" target="blank">
|
|
14
|
+
<img src="https://img.shields.io/npm/v/harness-alchemist?style=flat-square" alt="Npm package for Harness Alchemist">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="./LICENSE">
|
|
17
|
+
<img alt="License: MIT" src="https://img.shields.io/github/license/lunarmoon26/harness-alchemist?style=flat-square" />
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://github.com/lunarmoon26/harness-alchemist/actions/workflows/ci.yml" target="blank">
|
|
20
|
+
<img src="https://img.shields.io/github/actions/workflow/status/lunarmoon26/harness-alchemist/ci.yml?branch=main&style=flat-square&label=CI" alt="CI status for Harness Alchemist">
|
|
21
|
+
</a>
|
|
22
|
+
<br /><br />
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
Follow <a href="https://x.com/haochuanzero">@haochuanzero on X</a> for updates · Start from the <a href="https://blog.haochuanz.net/harness-alchemist/">one-page tour</a>.
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Harness Alchemist scaffolds one TypeScript plugin repository that installs natively into **Claude Code**, **Codex/ChatGPT**, **OpenCode**, **Google Antigravity**, and **DeepSeek Harness/Cordis**. Product skills ship behavioral `.mjs`/`.py` script twins; every harness entrypoint is a thin adapter that delegates to them.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
10
34
|
|
|
11
35
|
```bash
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
--repository example/my-plugin
|
|
36
|
+
npm install -g harness-alchemist
|
|
37
|
+
# or run it without installing:
|
|
38
|
+
npx harness-alchemist@latest create my-plugin --help
|
|
16
39
|
```
|
|
17
40
|
|
|
18
|
-
|
|
41
|
+
Requires Node.js 22.20+ (Bun 1.2+ also supported). Zero runtime dependencies.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
19
44
|
|
|
20
45
|
```bash
|
|
21
|
-
|
|
46
|
+
npx harness-alchemist@latest create my-plugin \
|
|
22
47
|
--description "What the plugin does" \
|
|
23
48
|
--author "Example Team" \
|
|
24
49
|
--repository example/my-plugin
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The destination must be missing or empty. Harness Alchemist renders into a staging directory, validates the result, and moves it into place without replacing existing content.
|
|
28
|
-
|
|
29
|
-
## Commands
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
harness-alchemist create <directory> [options]
|
|
33
|
-
harness-alchemist validate [directory] [--external] [--json]
|
|
34
|
-
harness-alchemist templates
|
|
35
|
-
harness-alchemist version
|
|
51
|
+
cd my-plugin && npm install && npm run verify
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
New projects use the unreleased canonical `v0.1.0` template. Unknown versions
|
|
41
|
-
fail instead of silently selecting a different layout.
|
|
42
|
-
|
|
43
|
-
## Generated Layout
|
|
44
|
-
|
|
45
|
-
```text
|
|
46
|
-
.agents/
|
|
47
|
-
plugins/ Codex repository marketplace
|
|
48
|
-
skills/develop-<name>/ Project-local maintenance skill and scripts
|
|
49
|
-
.claude-plugin/ Claude plugin and marketplace manifests
|
|
50
|
-
.codex-plugin/ Codex/ChatGPT plugin manifest
|
|
51
|
-
skills/<name>/SKILL.md Shared installable Agent Skill
|
|
52
|
-
src/opencode.ts OpenCode npm plugin entrypoint
|
|
53
|
-
src/deepseek.ts Cordis plugin entrypoint
|
|
54
|
-
cordis.patch.yml DeepSeek Harness bundle layer
|
|
55
|
-
plugin.json Antigravity plugin manifest
|
|
56
|
-
package.json Canonical package metadata
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Generated repositories start at version `0.1.0` with inert OpenCode and Cordis entrypoints. Add only the runtime hooks or services the plugin actually needs.
|
|
60
|
-
|
|
61
|
-
The recursive part is intentional: Harness Alchemist itself uses this layout, and every generated repository receives `.agents/skills/develop-<name>/` with its own sync, validation, package-payload, and compatibility workflow.
|
|
54
|
+
Creation only writes to a missing or empty destination. The generated project passes its own gate out of the box: TypeScript checks, runtime delegation tests, scaffold validation, and an npm-payload audit.
|
|
62
55
|
|
|
63
|
-
##
|
|
56
|
+
## Existing monorepos and SDK packages
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[docs/skill-tiers.md](docs/skill-tiers.md) before moving guidance between them.
|
|
58
|
+
`validate` also supports an adapted plugin package inside an existing repository.
|
|
59
|
+
Add `alchemy.json` at the repository root:
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"$schema": "https://unpkg.com/harness-alchemist/alchemy.schema.json",
|
|
64
|
+
"pluginRoot": "packages/my-sdk",
|
|
65
|
+
"opencodeExport": "./server"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
The manifest is JSON-Schema-validated; the schema ships in the npm package and
|
|
70
|
+
is referenced through `$schema`, so editors autocomplete and check every field.
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
`pluginRoot` contains the canonical plugin manifests, shared skills, Cordis
|
|
73
|
+
patch, adapter sources, and publishable package metadata. Repository marketplace
|
|
74
|
+
manifests remain at the project root and point at that package directory.
|
|
75
|
+
`opencodeExport: "./server"` preserves an SDK at the package root while exposing
|
|
76
|
+
the OpenCode adapter through the modern server entrypoint. Omitting the file
|
|
77
|
+
retains the strict generated single-package layout.
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
The optional `runtime` field selects what the adapted package must contain:
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
npm
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
- `"npm"` (default) — the full generated contract: npm metadata, OpenCode and
|
|
82
|
+
Cordis adapters, Cordis patch, and `.mjs`/`.py` script twins.
|
|
83
|
+
- `"skills"` — skills and harness manifests only. No npm package, adapters, or
|
|
84
|
+
Cordis patch are required, and single-language scripts are allowed, so
|
|
85
|
+
Python, Go, Rust, Java, C#, or Swift repositories can expose their workflows
|
|
86
|
+
to Claude Code, Codex, Antigravity, and DeepSeek's filesystem skill roots
|
|
87
|
+
without adopting a JavaScript runtime.
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
npm run verify
|
|
92
|
-
```
|
|
89
|
+
Generated projects include a `alchemy.json` manifest recording their
|
|
90
|
+
`runtime`, canonical `template` version, `generator`, `generatorVersion`, and
|
|
91
|
+
`createdAt`; `npm run sync` keeps `generatorVersion` aligned with the package
|
|
92
|
+
version.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
## What you get
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
| Surface | Purpose |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `skills/<name>/` | Agent Skills spec skill with `.mjs`/`.py` script twins under `scripts/` and a tool-contract reference |
|
|
99
|
+
| `src/opencode.ts` | OpenCode plugin registering tools that spawn the skill scripts |
|
|
100
|
+
| `src/deepseek.ts` + `cordis.patch.yml` | Cordis function plugin providing a service over the same scripts |
|
|
101
|
+
| `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, `plugin.json` | Native manifests for Claude Code, Codex, and Antigravity marketplaces |
|
|
102
|
+
| `.agents/skills/develop-<name>/` | Repository-maintenance skill so agents can develop the project recursively |
|
|
103
|
+
| `.github/workflows/` | CI plus tag-triggered publish (`vX.Y.Z` → verify → npm provenance) |
|
|
99
104
|
|
|
100
|
-
##
|
|
105
|
+
## Supported harnesses
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
- Shared Agent Skill: `skills/harness-alchemist/SKILL.md`
|
|
104
|
-
- OpenCode package-root export: `harness-alchemist`
|
|
105
|
-
- Cordis export: `harness-alchemist/deepseek`
|
|
106
|
-
- DeepSeek bundle patch: `harness-alchemist/cordis.patch.yml`
|
|
107
|
+
Install paths below are verified against real CLIs before shipping in the template.
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
| Harness | Skills | Runtime | Install |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| Claude Code | bundled natively | — | `claude plugin install <name>@<marketplace>` |
|
|
112
|
+
| Codex/ChatGPT | bundled natively | — | `codex plugin add <name>@<marketplace>` |
|
|
113
|
+
| OpenCode | via `~/.agents/skills/` | npm package hooks | `"plugin": ["<package>"]` in `opencode.json` |
|
|
114
|
+
| Google Antigravity | nested bundle | — | `agy plugin validate . && agy plugin install .` |
|
|
115
|
+
| DeepSeek Harness | profile filesystem roots | Cordis service plugin | `dsh plugin --profile demo add <package-or-path>` |
|
|
109
116
|
|
|
110
|
-
##
|
|
117
|
+
## Skill script contract
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
Product skills own their logic; adapters never do.
|
|
113
120
|
|
|
114
121
|
```bash
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
echo '{"request": "hello"}' | node skills/<name>/scripts/main.mjs
|
|
123
|
+
# {"ok":true,"plugin":"my-plugin","echo":{"request":"hello"}}
|
|
117
124
|
```
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
- One JSON object on stdin, one JSON result plus newline on stdout.
|
|
127
|
+
- Non-zero exit with a stderr diagnostic on failure.
|
|
128
|
+
- `scripts/main.py` is a stdlib-only behavioral twin of `scripts/main.mjs`.
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
codex plugin marketplace add lunarmoon26/harness-alchemist
|
|
123
|
-
```
|
|
130
|
+
## Validation tiers
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
npx skills add lunarmoon26/harness-alchemist --agent opencode
|
|
129
|
-
```
|
|
132
|
+
`npm run validate` always enforces Agent Skills frontmatter compliance, SKILL.md reference resolution, and twin parity. With the optional `pyodide` devDependency installed, Python entrypoints are additionally compiled and smoke-executed inside a WebAssembly CPython sandbox — no native Python required.
|
|
130
133
|
|
|
131
|
-
|
|
134
|
+
## Release automation
|
|
132
135
|
|
|
133
136
|
```bash
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
# bump package.json version, then:
|
|
138
|
+
npm run sync && npm run verify && git commit -am "Release vX.Y.Z" && git push
|
|
139
|
+
git tag vX.Y.Z && git push origin vX.Y.Z # publishes automatically
|
|
136
140
|
```
|
|
137
141
|
|
|
138
142
|
## License
|
|
139
143
|
|
|
140
|
-
MIT
|
|
144
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://unpkg.com/harness-alchemist/alchemy.schema.json",
|
|
4
|
+
"title": "Harness Alchemist layout manifest",
|
|
5
|
+
"description": "Declares how a repository adapts the Harness Alchemist universal plugin layout. Omitting the file selects the strict generated single-package layout.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "URL of this schema."
|
|
12
|
+
},
|
|
13
|
+
"runtime": {
|
|
14
|
+
"enum": ["npm", "skills"],
|
|
15
|
+
"default": "npm",
|
|
16
|
+
"description": "'npm' requires the full generated contract (npm package, OpenCode and Cordis adapters, Cordis patch, .mjs/.py script twins). 'skills' requires skills and harness manifests only and allows single-language scripts."
|
|
17
|
+
},
|
|
18
|
+
"pluginRoot": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"default": ".",
|
|
21
|
+
"pattern": "^(?!\\.{2}(/|\\\\))[^\\u0000]+",
|
|
22
|
+
"description": "Repository-relative directory containing the plugin manifests, product skills, runtime sources, Cordis patch, and npm metadata. Defaults to the repository root."
|
|
23
|
+
},
|
|
24
|
+
"opencodeExport": {
|
|
25
|
+
"enum": [".", "./server"],
|
|
26
|
+
"default": ".",
|
|
27
|
+
"description": "Package export key that exposes the OpenCode adapter. Only meaningful when runtime is 'npm'."
|
|
28
|
+
},
|
|
29
|
+
"template": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$",
|
|
32
|
+
"description": "Canonical template version the project was generated from."
|
|
33
|
+
},
|
|
34
|
+
"generator": {
|
|
35
|
+
"const": "harness-alchemist",
|
|
36
|
+
"description": "Tool that generated the project."
|
|
37
|
+
},
|
|
38
|
+
"generatorVersion": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$",
|
|
41
|
+
"description": "Version of the generating harness-alchemist CLI."
|
|
42
|
+
},
|
|
43
|
+
"createdAt": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"format": "date-time",
|
|
46
|
+
"description": "ISO 8601 creation timestamp written by the generator."
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"if": {
|
|
50
|
+
"properties": { "opencodeExport": { "const": "./server" } },
|
|
51
|
+
"required": ["opencodeExport"]
|
|
52
|
+
},
|
|
53
|
+
"then": { "properties": { "runtime": { "const": "npm" } } }
|
|
54
|
+
}
|
|
@@ -35,7 +35,7 @@ if (!command || command === "help" || command === "--help" || command === "-h")
|
|
|
35
35
|
} else if (["create", "init", "new"].includes(command)) {
|
|
36
36
|
process.exitCode = await runCreate(args)
|
|
37
37
|
} else if (command === "validate") {
|
|
38
|
-
process.exitCode = await runValidate(args
|
|
38
|
+
process.exitCode = await runValidate(args)
|
|
39
39
|
} else {
|
|
40
40
|
console.error(`Unknown command: ${command}\n`)
|
|
41
41
|
console.error(usage())
|
package/lib/create.mjs
CHANGED
|
@@ -237,6 +237,7 @@ export async function runCreate(argv) {
|
|
|
237
237
|
const projectRoot = dirname(dirname(scriptPath))
|
|
238
238
|
const canonicalRoot = join(projectRoot, "templates", options.templateVersion)
|
|
239
239
|
const templateRoot = join(canonicalRoot, "universal-typescript")
|
|
240
|
+
const generatorVersion = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8")).version
|
|
240
241
|
const state = await destinationState(options.output)
|
|
241
242
|
if (state === "not-directory" || state === "non-empty") {
|
|
242
243
|
console.error(`Destination must be missing or empty: ${options.output}`)
|
|
@@ -250,6 +251,9 @@ export async function runCreate(argv) {
|
|
|
250
251
|
DISPLAY_NAME_JSON: JSON.stringify(options.displayName),
|
|
251
252
|
DESCRIPTION: options.description,
|
|
252
253
|
DESCRIPTION_JSON: JSON.stringify(options.description),
|
|
254
|
+
TEMPLATE_VERSION: options.templateVersion,
|
|
255
|
+
GENERATOR_VERSION: generatorVersion,
|
|
256
|
+
CREATED_AT: new Date().toISOString(),
|
|
253
257
|
SHARED_SKILL_DESCRIPTION_JSON: JSON.stringify(
|
|
254
258
|
`${options.description} Use when the user requests ${options.displayName} workflows or explicitly asks to use the ${options.name} plugin.`,
|
|
255
259
|
),
|
package/lib/validate.mjs
CHANGED
|
@@ -6,24 +6,32 @@ import { basename, dirname, join, resolve, sep } from "node:path"
|
|
|
6
6
|
import { fileURLToPath } from "node:url"
|
|
7
7
|
import { spawnSync } from "node:child_process"
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const PROJECT_REQUIRED_FILES = [
|
|
10
10
|
".agents/plugins/marketplace.json",
|
|
11
11
|
".claude-plugin/marketplace.json",
|
|
12
|
-
".claude-plugin/plugin.json",
|
|
13
|
-
".codex-plugin/plugin.json",
|
|
14
12
|
".github/workflows/npm-publish.yml",
|
|
15
13
|
".gitignore",
|
|
16
14
|
"AGENTS.md",
|
|
17
15
|
"LICENSE",
|
|
18
16
|
"README.md",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const PLUGIN_REQUIRED_FILES = [
|
|
20
|
+
".claude-plugin/plugin.json",
|
|
21
|
+
".codex-plugin/plugin.json",
|
|
22
|
+
"plugin.json",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const NPM_PLUGIN_REQUIRED_FILES = [
|
|
19
26
|
"cordis.patch.yml",
|
|
20
27
|
"package.json",
|
|
21
|
-
"plugin.json",
|
|
22
28
|
"src/deepseek.ts",
|
|
23
29
|
"src/opencode.ts",
|
|
24
30
|
"tsconfig.json",
|
|
25
31
|
]
|
|
26
32
|
|
|
33
|
+
const RUNTIME_MODES = ["npm", "skills"]
|
|
34
|
+
|
|
27
35
|
function usage() {
|
|
28
36
|
return `Usage: harness-alchemist validate [project-directory] [--external] [--json]
|
|
29
37
|
|
|
@@ -44,16 +52,19 @@ Options:
|
|
|
44
52
|
|
|
45
53
|
function findProjectRoot(start) {
|
|
46
54
|
let current = resolve(start)
|
|
55
|
+
let conventionalRoot
|
|
47
56
|
while (true) {
|
|
57
|
+
if (existsSync(join(current, "alchemy.json"))) return current
|
|
48
58
|
if (
|
|
59
|
+
!conventionalRoot &&
|
|
49
60
|
existsSync(join(current, "package.json")) &&
|
|
50
61
|
existsSync(join(current, ".claude-plugin")) &&
|
|
51
62
|
existsSync(join(current, ".codex-plugin"))
|
|
52
63
|
) {
|
|
53
|
-
|
|
64
|
+
conventionalRoot = current
|
|
54
65
|
}
|
|
55
66
|
const parent = dirname(current)
|
|
56
|
-
if (parent === current) return
|
|
67
|
+
if (parent === current) return conventionalRoot
|
|
57
68
|
current = parent
|
|
58
69
|
}
|
|
59
70
|
}
|
|
@@ -294,7 +305,7 @@ async function smokePythonScripts(scripts, errors, warnings) {
|
|
|
294
305
|
instance.globals.delete("__ha_payload__")
|
|
295
306
|
}
|
|
296
307
|
|
|
297
|
-
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts) {
|
|
308
|
+
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime) {
|
|
298
309
|
if ((frontmatter.description?.length ?? 0) > 1024) {
|
|
299
310
|
errors.push(`${skillFile}: description must be at most 1024 characters`)
|
|
300
311
|
}
|
|
@@ -325,12 +336,12 @@ async function checkProductSkillRuntime(skillFile, content, frontmatter, errors,
|
|
|
325
336
|
entries.filter((entry) => entry.endsWith(".mjs")).map((entry) => entry.slice(0, -4)),
|
|
326
337
|
)
|
|
327
338
|
for (const base of pythonBases) {
|
|
328
|
-
if (!mjsBases.has(base)) {
|
|
339
|
+
if (runtime === "npm" && !mjsBases.has(base)) {
|
|
329
340
|
errors.push(`${skillFile}: scripts/${base}.py is missing its scripts/${base}.mjs twin`)
|
|
330
341
|
}
|
|
331
342
|
}
|
|
332
343
|
for (const base of mjsBases) {
|
|
333
|
-
if (!pythonBases.has(base)) {
|
|
344
|
+
if (runtime === "npm" && !pythonBases.has(base)) {
|
|
334
345
|
errors.push(`${skillFile}: scripts/${base}.mjs is missing its scripts/${base}.py twin`)
|
|
335
346
|
}
|
|
336
347
|
}
|
|
@@ -344,25 +355,107 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
344
355
|
const errors = []
|
|
345
356
|
const warnings = []
|
|
346
357
|
|
|
347
|
-
|
|
358
|
+
const layoutPath = join(root, "alchemy.json")
|
|
359
|
+
const parsedLayout = existsSync(layoutPath) ? await readJson(layoutPath, errors) : {}
|
|
360
|
+
if (parsedLayout === undefined) return { root, errors, warnings }
|
|
361
|
+
if (
|
|
362
|
+
typeof parsedLayout !== "object" ||
|
|
363
|
+
parsedLayout === null ||
|
|
364
|
+
Array.isArray(parsedLayout)
|
|
365
|
+
) {
|
|
366
|
+
errors.push("alchemy.json must contain a JSON object")
|
|
367
|
+
return { root, errors, warnings }
|
|
368
|
+
}
|
|
369
|
+
const layout = parsedLayout
|
|
370
|
+
const knownLayoutFields = [
|
|
371
|
+
"pluginRoot",
|
|
372
|
+
"opencodeExport",
|
|
373
|
+
"runtime",
|
|
374
|
+
"$schema",
|
|
375
|
+
"template",
|
|
376
|
+
"generator",
|
|
377
|
+
"generatorVersion",
|
|
378
|
+
"createdAt",
|
|
379
|
+
]
|
|
380
|
+
const unknownLayoutFields = Object.keys(layout).filter(
|
|
381
|
+
(key) => !knownLayoutFields.includes(key),
|
|
382
|
+
)
|
|
383
|
+
if (unknownLayoutFields.length) {
|
|
384
|
+
errors.push(`alchemy.json has unknown fields: ${unknownLayoutFields.join(", ")}`)
|
|
385
|
+
}
|
|
386
|
+
if (layout.$schema !== undefined && typeof layout.$schema !== "string") {
|
|
387
|
+
errors.push("alchemy.json $schema must be a string")
|
|
388
|
+
}
|
|
389
|
+
if (layout.template !== undefined && !/^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(layout.template)) {
|
|
390
|
+
errors.push("alchemy.json template must be a 'vX.Y.Z' version")
|
|
391
|
+
}
|
|
392
|
+
if (layout.generator !== undefined && layout.generator !== "harness-alchemist") {
|
|
393
|
+
errors.push("alchemy.json generator must be 'harness-alchemist'")
|
|
394
|
+
}
|
|
395
|
+
if (
|
|
396
|
+
layout.generatorVersion !== undefined &&
|
|
397
|
+
!/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(layout.generatorVersion)
|
|
398
|
+
) {
|
|
399
|
+
errors.push("alchemy.json generatorVersion must be an 'X.Y.Z' version")
|
|
400
|
+
}
|
|
401
|
+
if (layout.createdAt !== undefined) {
|
|
402
|
+
const createdAt = new Date(layout.createdAt)
|
|
403
|
+
if (typeof layout.createdAt !== "string" || Number.isNaN(createdAt.getTime())) {
|
|
404
|
+
errors.push("alchemy.json createdAt must be an ISO 8601 timestamp")
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const runtime = layout.runtime ?? "npm"
|
|
408
|
+
if (!RUNTIME_MODES.includes(runtime)) {
|
|
409
|
+
errors.push(`alchemy.json runtime must be one of: ${RUNTIME_MODES.join(", ")}`)
|
|
410
|
+
}
|
|
411
|
+
const pluginRootRelative = layout.pluginRoot ?? "."
|
|
412
|
+
if (typeof pluginRootRelative !== "string" || !pluginRootRelative.trim()) {
|
|
413
|
+
errors.push("alchemy.json pluginRoot must be a non-empty relative path")
|
|
414
|
+
}
|
|
415
|
+
const pluginRoot = resolve(root, typeof pluginRootRelative === "string" ? pluginRootRelative : ".")
|
|
416
|
+
if (!(pluginRoot === root || pluginRoot.startsWith(`${root}${sep}`))) {
|
|
417
|
+
errors.push("alchemy.json pluginRoot escapes the project root")
|
|
418
|
+
} else if (existsSync(pluginRoot)) {
|
|
419
|
+
const [canonicalRoot, canonicalPluginRoot] = await Promise.all([
|
|
420
|
+
realpath(root),
|
|
421
|
+
realpath(pluginRoot),
|
|
422
|
+
])
|
|
423
|
+
if (!(canonicalPluginRoot === canonicalRoot || canonicalPluginRoot.startsWith(`${canonicalRoot}${sep}`))) {
|
|
424
|
+
errors.push("alchemy.json pluginRoot resolves through a symlink outside the project root")
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const opencodeExport = layout.opencodeExport ?? "."
|
|
428
|
+
if (![".", "./server"].includes(opencodeExport)) {
|
|
429
|
+
errors.push("alchemy.json opencodeExport must be '.' or './server'")
|
|
430
|
+
}
|
|
431
|
+
if (opencodeExport !== "." && runtime !== "npm") {
|
|
432
|
+
errors.push("alchemy.json opencodeExport requires runtime 'npm'")
|
|
433
|
+
}
|
|
348
434
|
if (errors.length > 0) return { root, errors, warnings }
|
|
349
435
|
|
|
350
|
-
const
|
|
351
|
-
const
|
|
436
|
+
for (const file of PROJECT_REQUIRED_FILES) requirePath(root, file, errors)
|
|
437
|
+
const pluginRequiredFiles = runtime === "npm"
|
|
438
|
+
? [...PLUGIN_REQUIRED_FILES, ...NPM_PLUGIN_REQUIRED_FILES]
|
|
439
|
+
: PLUGIN_REQUIRED_FILES
|
|
440
|
+
for (const file of pluginRequiredFiles) {
|
|
441
|
+
requirePath(root, join(pluginRootRelative, file), errors)
|
|
442
|
+
}
|
|
443
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
444
|
+
|
|
445
|
+
const claudePlugin = await readJson(join(pluginRoot, ".claude-plugin/plugin.json"), errors)
|
|
352
446
|
const claudeMarketplace = await readJson(
|
|
353
447
|
join(root, ".claude-plugin/marketplace.json"),
|
|
354
448
|
errors,
|
|
355
449
|
)
|
|
356
|
-
const codexPlugin = await readJson(join(
|
|
450
|
+
const codexPlugin = await readJson(join(pluginRoot, ".codex-plugin/plugin.json"), errors)
|
|
357
451
|
const codexMarketplace = await readJson(
|
|
358
452
|
join(root, ".agents/plugins/marketplace.json"),
|
|
359
453
|
errors,
|
|
360
454
|
)
|
|
361
|
-
const antigravityPlugin = await readJson(join(
|
|
455
|
+
const antigravityPlugin = await readJson(join(pluginRoot, "plugin.json"), errors)
|
|
362
456
|
|
|
363
457
|
if (errors.length > 0) return { root, errors, warnings }
|
|
364
458
|
|
|
365
|
-
const packageBase = packageJson.name?.split("/").at(-1)
|
|
366
459
|
const pluginName = claudePlugin.name
|
|
367
460
|
const validName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
368
461
|
|
|
@@ -372,9 +465,6 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
372
465
|
if ((pluginName?.length ?? 0) > 56) {
|
|
373
466
|
errors.push("Plugin name must be at most 56 characters")
|
|
374
467
|
}
|
|
375
|
-
if (packageBase !== pluginName) {
|
|
376
|
-
errors.push(`npm package basename '${packageBase}' must match plugin name '${pluginName}'`)
|
|
377
|
-
}
|
|
378
468
|
|
|
379
469
|
for (const [label, value] of [
|
|
380
470
|
["Codex plugin", codexPlugin.name],
|
|
@@ -383,64 +473,104 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
383
473
|
if (value !== pluginName) errors.push(`${label} name '${value}' does not match '${pluginName}'`)
|
|
384
474
|
}
|
|
385
475
|
|
|
386
|
-
|
|
387
|
-
["
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
476
|
+
if (runtime === "skills") {
|
|
477
|
+
for (const [label, manifest] of [["Codex plugin", codexPlugin]]) {
|
|
478
|
+
if (manifest.version !== claudePlugin.version) {
|
|
479
|
+
errors.push(`${label} version does not match the Claude plugin`)
|
|
480
|
+
}
|
|
481
|
+
if (manifest.description !== claudePlugin.description) {
|
|
482
|
+
errors.push(`${label} description does not match the Claude plugin`)
|
|
483
|
+
}
|
|
392
484
|
}
|
|
393
|
-
if (
|
|
394
|
-
errors.push(
|
|
485
|
+
if (antigravityPlugin.description !== claudePlugin.description) {
|
|
486
|
+
errors.push("Antigravity description does not match the Claude plugin")
|
|
395
487
|
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
errors.
|
|
399
|
-
}
|
|
488
|
+
} else {
|
|
489
|
+
const packageJson = await readJson(join(pluginRoot, "package.json"), errors)
|
|
490
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
400
491
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
[
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
492
|
+
const packageBase = packageJson.name?.split("/").at(-1)
|
|
493
|
+
if (packageBase !== pluginName) {
|
|
494
|
+
errors.push(`npm package basename '${packageBase}' must match plugin name '${pluginName}'`)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const [label, manifest] of [
|
|
498
|
+
["Claude plugin", claudePlugin],
|
|
499
|
+
["Codex plugin", codexPlugin],
|
|
500
|
+
]) {
|
|
501
|
+
if (manifest.version !== packageJson.version) {
|
|
502
|
+
errors.push(`${label} version does not match package.json`)
|
|
503
|
+
}
|
|
504
|
+
if (manifest.description !== packageJson.description) {
|
|
505
|
+
errors.push(`${label} description does not match package.json`)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (antigravityPlugin.description !== packageJson.description) {
|
|
509
|
+
errors.push("Antigravity description does not match package.json")
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (packageJson.type !== "module") errors.push("package.json type must be 'module'")
|
|
513
|
+
if (opencodeExport === "." && packageJson.engines?.node !== ">=22.20.0") {
|
|
514
|
+
errors.push("package.json engines.node must be '>=22.20.0'")
|
|
515
|
+
}
|
|
516
|
+
const requiredExports = [
|
|
517
|
+
[opencodeExport, "import", "./dist/opencode.js"],
|
|
518
|
+
[opencodeExport, "types", "./dist/opencode.d.ts"],
|
|
519
|
+
["./deepseek", "import", "./dist/deepseek.js"],
|
|
520
|
+
["./deepseek", "types", "./dist/deepseek.d.ts"],
|
|
521
|
+
["./cordis.patch.yml", "import", "./cordis.patch.yml"],
|
|
522
|
+
]
|
|
523
|
+
for (const [key, field, expected] of requiredExports) {
|
|
524
|
+
const target = exportTarget(packageJson.exports, key, field)
|
|
525
|
+
if (target !== expected) {
|
|
526
|
+
errors.push(`package.json export '${key}' ${field} target must be '${expected}'`)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const entry of [
|
|
530
|
+
"dist",
|
|
531
|
+
"skills",
|
|
532
|
+
"alchemy.json",
|
|
533
|
+
"cordis.patch.yml",
|
|
534
|
+
".claude-plugin/plugin.json",
|
|
535
|
+
".codex-plugin/plugin.json",
|
|
536
|
+
"plugin.json",
|
|
537
|
+
]) {
|
|
538
|
+
if (!packageJson.files?.includes(entry)) errors.push(`package.json files is missing '${entry}'`)
|
|
539
|
+
}
|
|
540
|
+
if (packageJson.dsh?.bundle?.patch !== "./cordis.patch.yml") {
|
|
541
|
+
errors.push("package.json dsh.bundle.patch must be './cordis.patch.yml'")
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const patch = await readFile(join(pluginRoot, "cordis.patch.yml"), "utf8")
|
|
545
|
+
const expectedModule = `${packageJson.name}/deepseek`
|
|
546
|
+
const cordisEntries = parseCordisInsertEntries(patch)
|
|
547
|
+
.filter((entry) => entry.id === pluginName)
|
|
548
|
+
const cordisEntry = cordisEntries[0]
|
|
549
|
+
if (cordisEntries.length !== 1 || !cordisEntry?.valid) {
|
|
550
|
+
errors.push(`cordis.patch.yml must contain a valid insert entry for '${pluginName}'`)
|
|
551
|
+
} else if (cordisEntry.name !== expectedModule) {
|
|
552
|
+
errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
|
|
416
553
|
}
|
|
417
|
-
}
|
|
418
|
-
for (const entry of [
|
|
419
|
-
"dist",
|
|
420
|
-
"skills",
|
|
421
|
-
"cordis.patch.yml",
|
|
422
|
-
".claude-plugin/plugin.json",
|
|
423
|
-
".codex-plugin/plugin.json",
|
|
424
|
-
"plugin.json",
|
|
425
|
-
]) {
|
|
426
|
-
if (!packageJson.files?.includes(entry)) errors.push(`package.json files is missing '${entry}'`)
|
|
427
|
-
}
|
|
428
|
-
if (packageJson.dsh?.bundle?.patch !== "./cordis.patch.yml") {
|
|
429
|
-
errors.push("package.json dsh.bundle.patch must be './cordis.patch.yml'")
|
|
430
554
|
}
|
|
431
555
|
|
|
432
|
-
await requireManifestPath(
|
|
433
|
-
await requireManifestPath(
|
|
556
|
+
await requireManifestPath(pluginRoot, claudePlugin.skills, "Claude skills", errors)
|
|
557
|
+
await requireManifestPath(pluginRoot, codexPlugin.skills, "Codex skills", errors)
|
|
558
|
+
|
|
559
|
+
const marketplaceSource = pluginRoot === root
|
|
560
|
+
? "./"
|
|
561
|
+
: `./${pluginRootRelative.replaceAll("\\", "/").replace(/^\.\//, "")}`
|
|
434
562
|
|
|
435
563
|
const claudeEntry = claudeMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
436
564
|
if (!claudeEntry) errors.push("Claude marketplace is missing the plugin entry")
|
|
437
|
-
else if (claudeEntry.source !==
|
|
565
|
+
else if (claudeEntry.source !== marketplaceSource) {
|
|
566
|
+
errors.push(`Claude marketplace source must be '${marketplaceSource}'`)
|
|
567
|
+
}
|
|
438
568
|
|
|
439
569
|
const codexEntry = codexMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
440
570
|
if (!codexEntry) errors.push("Codex marketplace is missing the plugin entry")
|
|
441
571
|
else {
|
|
442
|
-
if (codexEntry.source?.source !== "local" || codexEntry.source?.path !==
|
|
443
|
-
errors.push(
|
|
572
|
+
if (codexEntry.source?.source !== "local" || codexEntry.source?.path !== marketplaceSource) {
|
|
573
|
+
errors.push(`Codex marketplace source must be a local '${marketplaceSource}' path`)
|
|
444
574
|
}
|
|
445
575
|
if (!codexEntry.policy?.installation || !codexEntry.policy?.authentication) {
|
|
446
576
|
errors.push("Codex marketplace entry requires installation and authentication policies")
|
|
@@ -448,19 +578,9 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
448
578
|
if (!codexEntry.category) errors.push("Codex marketplace entry requires a category")
|
|
449
579
|
}
|
|
450
580
|
|
|
451
|
-
const patch = await readFile(join(root, "cordis.patch.yml"), "utf8")
|
|
452
|
-
const expectedModule = `${packageJson.name}/deepseek`
|
|
453
|
-
const cordisEntries = parseCordisInsertEntries(patch)
|
|
454
|
-
.filter((entry) => entry.id === pluginName)
|
|
455
|
-
const cordisEntry = cordisEntries[0]
|
|
456
|
-
if (cordisEntries.length !== 1 || !cordisEntry?.valid) {
|
|
457
|
-
errors.push(`cordis.patch.yml must contain a valid insert entry for '${pluginName}'`)
|
|
458
|
-
} else if (cordisEntry.name !== expectedModule) {
|
|
459
|
-
errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
|
|
460
|
-
}
|
|
461
581
|
|
|
462
582
|
const maintenanceSkillFiles = await collectSkillFiles(root, ".agents/skills")
|
|
463
|
-
const productSkillFiles = await collectSkillFiles(
|
|
583
|
+
const productSkillFiles = await collectSkillFiles(pluginRoot, "skills")
|
|
464
584
|
const skillFiles = [...productSkillFiles, ...maintenanceSkillFiles]
|
|
465
585
|
if (skillFiles.length < 2) errors.push("Expected a shared skill and a project development skill")
|
|
466
586
|
|
|
@@ -479,7 +599,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
479
599
|
errors.push(`${skillFile}: invalid Agent Skill name`)
|
|
480
600
|
}
|
|
481
601
|
if (productSkillFiles.includes(skillFile)) {
|
|
482
|
-
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts)
|
|
602
|
+
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime)
|
|
483
603
|
}
|
|
484
604
|
}
|
|
485
605
|
}
|
|
@@ -493,7 +613,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
493
613
|
}
|
|
494
614
|
|
|
495
615
|
if (options.external) {
|
|
496
|
-
const result = spawnSync("claude", ["plugin", "validate",
|
|
616
|
+
const result = spawnSync("claude", ["plugin", "validate", pluginRoot, "--strict"], {
|
|
497
617
|
encoding: "utf8",
|
|
498
618
|
})
|
|
499
619
|
if (result.error?.code === "ENOENT") warnings.push("Claude CLI not found; skipped external validation")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-alchemist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"lib",
|
|
26
26
|
"templates",
|
|
27
27
|
"skills",
|
|
28
|
+
"alchemy.schema.json",
|
|
28
29
|
"cordis.patch.yml",
|
|
29
30
|
".claude-plugin/plugin.json",
|
|
30
31
|
".codex-plugin/plugin.json",
|
|
@@ -16,7 +16,11 @@ replace the generated repository's local maintenance skill.
|
|
|
16
16
|
2. Read [references/compatibility.md](references/compatibility.md) before choosing paths or manifests.
|
|
17
17
|
3. For a new project, run `harness-alchemist create --help` and use the CLI instead of recreating the structure manually.
|
|
18
18
|
4. After creation, load `.agents/skills/develop-<name>/` in the generated project before maintaining its files.
|
|
19
|
-
5.
|
|
19
|
+
5. For an existing monorepo or SDK package, use the explicit
|
|
20
|
+
`alchemy.json` adaptation described in
|
|
21
|
+
[references/compatibility.md](references/compatibility.md); do not replace the
|
|
22
|
+
product package root merely to match the generated layout.
|
|
23
|
+
6. Run `harness-alchemist validate` and the generated package's `verify` script before reporting completion.
|
|
20
24
|
|
|
21
25
|
## Create A Project
|
|
22
26
|
|
|
@@ -27,9 +27,13 @@ The rich `interface` object is recommended for public presentation but is not re
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
codex plugin marketplace add owner/repo
|
|
30
|
+
codex plugin add <plugin-name>@<marketplace-name>
|
|
31
|
+
codex plugin list
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
The `@marketplace` suffix is required for `codex plugin add`; a bare plugin
|
|
35
|
+
name is rejected. Headless install works without opening `/plugins`, then
|
|
36
|
+
start a new session. Codex npm marketplace sources download packages without running lifecycle scripts, so published packages must already contain built runtime files.
|
|
33
37
|
|
|
34
38
|
Official references:
|
|
35
39
|
|
|
@@ -28,6 +28,20 @@ stdout), and `src/opencode.ts` and `src/deepseek.ts` stay thin adapters that
|
|
|
28
28
|
spawn those scripts. See the generated project's tool contract reference for
|
|
29
29
|
details.
|
|
30
30
|
|
|
31
|
+
## Existing repositories
|
|
32
|
+
|
|
33
|
+
Existing monorepos may declare `pluginRoot` and an optional `./server` OpenCode
|
|
34
|
+
export in root `alchemy.json`. The repository root keeps marketplaces
|
|
35
|
+
and maintenance guidance; the plugin package keeps product skills, host
|
|
36
|
+
manifests, adapter sources, Cordis patch, and npm metadata. Generated repositories
|
|
37
|
+
remain single-package and need no configuration file.
|
|
38
|
+
|
|
39
|
+
The optional `runtime` field adapts the contract to the repository's language:
|
|
40
|
+
`"npm"` (default) requires the full generated package; `"skills"` accepts
|
|
41
|
+
skills and harness manifests alone — no npm package, adapters, Cordis patch,
|
|
42
|
+
or `.mjs`/`.py` twins — so polyglot repositories can publish workflows to
|
|
43
|
+
Claude Code, Codex, Antigravity, and DeepSeek without a JavaScript runtime.
|
|
44
|
+
|
|
31
45
|
## Capability Matrix
|
|
32
46
|
|
|
33
47
|
| Artifact | Claude | Codex | OpenCode | Antigravity | DeepSeek |
|
|
@@ -49,6 +49,12 @@ dsh plugin --profile demo add @scope/my-plugin
|
|
|
49
49
|
dsh --profile demo --dump-config
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
`dsh plugin add` forwards to pnpm in the profile directory, so an unpublished checkout can be installed by absolute path and stays linked to the working tree:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
dsh plugin --profile demo add /absolute/path/to/my-plugin
|
|
56
|
+
```
|
|
57
|
+
|
|
52
58
|
Bundle membership changes require a profile restart. Profile patch edits may hot-reload.
|
|
53
59
|
|
|
54
60
|
Shared Agent Skills are discovered from project `.agents/skills` and other configured filesystem roots. Package-relative skill assets are not automatically activated by a bundle; install repository skills separately when needed.
|
|
@@ -33,12 +33,16 @@ OpenCode accepts npm package names in `opencode.json`:
|
|
|
33
33
|
|
|
34
34
|
OpenCode installs npm plugins with Bun at startup. For local development, register an absolute `file://` URL to compiled JavaScript or a direct TypeScript file with all of its dependencies available.
|
|
35
35
|
|
|
36
|
-
Skills are
|
|
36
|
+
Skills are discovered from `~/.agents/skills/` (and project `.agents/skills/`), not from the OpenCode config directory, and symlinked skill directories are skipped. Install them from the Git repository or a checkout:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npx skills add owner/repo --agent opencode
|
|
40
|
+
# or, from a local clone:
|
|
41
|
+
cp -R skills/<name> ~/.agents/skills/
|
|
40
42
|
```
|
|
41
43
|
|
|
44
|
+
Verify discovery with `opencode debug skill`.
|
|
45
|
+
|
|
42
46
|
## Extension Rules
|
|
43
47
|
|
|
44
48
|
- Return `{}` when no hooks are registered.
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
- `.agents/skills/develop-{{NAME}}/` is repository-local development guidance and is discoverable by Vercel Skills.
|
|
7
7
|
- OpenCode and DeepSeek users install shared skills separately from the npm runtime plugin.
|
|
8
8
|
|
|
9
|
+
## Layout Manifest
|
|
10
|
+
|
|
11
|
+
`alchemy.json` records this repository's layout: `runtime: "npm"`,
|
|
12
|
+
the canonical `template` version, `generator`, `generatorVersion`, and
|
|
13
|
+
`createdAt`. It is validated against the published JSON Schema referenced by
|
|
14
|
+
`$schema`. `npm run sync` refreshes `generatorVersion` from `package.json`.
|
|
15
|
+
If this project ever moves into a monorepo, add `pluginRoot` (and optionally
|
|
16
|
+
`opencodeExport: "./server"` for SDK packages); `runtime: "skills"` adapts
|
|
17
|
+
non-JavaScript repositories to skills-only validation.
|
|
18
|
+
|
|
9
19
|
## Harness-Specific Contract
|
|
10
20
|
|
|
11
21
|
| Harness | Files | Rule |
|
|
@@ -121,4 +121,13 @@ for (let index = 0; index < cordisLines.length; index += 1) {
|
|
|
121
121
|
if (!updatedCordisEntry) throw new Error(`Could not find Cordis entry '${pluginName}' to update`)
|
|
122
122
|
await writeFile(cordisPath, cordisLines.join("\n"))
|
|
123
123
|
|
|
124
|
+
const layoutPath = join(root, "alchemy.json")
|
|
125
|
+
if (existsSync(layoutPath)) {
|
|
126
|
+
const layout = await readJson(layoutPath)
|
|
127
|
+
if (layout.generatorVersion !== packageJson.version) {
|
|
128
|
+
layout.generatorVersion = packageJson.version
|
|
129
|
+
await writeJson(layoutPath, layout)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
console.log(`Synchronized plugin manifests from ${packagePath}`)
|
|
@@ -19,7 +19,7 @@ cordis.patch.yml DeepSeek Harness bundle layer
|
|
|
19
19
|
plugin.json Antigravity plugin manifest
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The
|
|
22
|
+
The runtime entrypoints are thin adapters that delegate to skill scripts; see `skills/{{NAME}}/references/tool-contract.md` before extending them.
|
|
23
23
|
|
|
24
24
|
## Skill Boundaries
|
|
25
25
|
|
|
@@ -54,13 +54,13 @@ npm pack --dry-run
|
|
|
54
54
|
|
|
55
55
|
## GitHub Release Publishing
|
|
56
56
|
|
|
57
|
-
`.github/workflows/npm-publish.yml` publishes when a
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`.github/workflows/npm-publish.yml` publishes when a `vX.Y.Z` tag is pushed
|
|
58
|
+
(or a GitHub release with a semver tag such as `vX.Y.Z-rc.1` is published);
|
|
59
|
+
the workflow applies that version, synchronizes manifests,
|
|
60
60
|
verifies the package, and publishes with npm provenance.
|
|
61
61
|
|
|
62
62
|
Configure the repository `NPM_TOKEN` secret with an npm publish token before
|
|
63
|
-
|
|
63
|
+
pushing the tag.
|
|
64
64
|
|
|
65
65
|
## Claude Code
|
|
66
66
|
|
|
@@ -80,9 +80,11 @@ claude plugin validate . --strict
|
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
codex plugin marketplace add {{REPOSITORY_SOURCE}}
|
|
83
|
+
codex plugin add {{NAME}}@{{MARKETPLACE}}
|
|
84
|
+
codex plugin list
|
|
83
85
|
```
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
The `@{{MARKETPLACE}}` suffix is required; a bare plugin name is rejected.
|
|
86
88
|
|
|
87
89
|
## OpenCode
|
|
88
90
|
|
|
@@ -95,12 +97,24 @@ After publishing `{{PACKAGE_NAME}}`, add it to `opencode.json`:
|
|
|
95
97
|
}
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
For an unpublished checkout, point the entry at the built adapter instead:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"plugin": ["file:///absolute/path/to/{{NAME}}/dist/opencode.js"]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Skills are discovered from `~/.agents/skills/`, not from the OpenCode config
|
|
109
|
+
directory, and symlinked directories are skipped. Copy each shared skill you
|
|
110
|
+
need:
|
|
99
111
|
|
|
100
112
|
```bash
|
|
101
|
-
|
|
113
|
+
cp -R skills/{{NAME}} ~/.agents/skills/
|
|
102
114
|
```
|
|
103
115
|
|
|
116
|
+
Verify with `opencode debug skill`.
|
|
117
|
+
|
|
104
118
|
## Google Antigravity
|
|
105
119
|
|
|
106
120
|
Clone the repository and install its root as a plugin:
|
|
@@ -120,12 +134,18 @@ dsh plugin --profile demo add {{PACKAGE_NAME}}
|
|
|
120
134
|
dsh --profile demo --dump-config
|
|
121
135
|
```
|
|
122
136
|
|
|
123
|
-
|
|
137
|
+
For an unpublished checkout, `dsh plugin add` forwards to pnpm, so a local
|
|
138
|
+
path works and stays linked to your working tree:
|
|
124
139
|
|
|
125
140
|
```bash
|
|
126
|
-
|
|
141
|
+
dsh plugin --profile demo add /absolute/path/to/{{NAME}}
|
|
127
142
|
```
|
|
128
143
|
|
|
144
|
+
The bundle loads `{{PACKAGE_NAME}}/deepseek` through `package.json`'s
|
|
145
|
+
`dsh.bundle.patch` entry. Verify composition with
|
|
146
|
+
`dsh --profile demo --dump-config`, which should list an insert for
|
|
147
|
+
`{{NAME}}`.
|
|
148
|
+
|
|
129
149
|
## License
|
|
130
150
|
|
|
131
151
|
{{LICENSE}}
|