harness-alchemist 0.1.5 → 0.1.7
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 +217 -83
- package/package.json +2 -1
- package/skills/harness-alchemist/SKILL.md +5 -1
- package/skills/harness-alchemist/references/compatibility.md +14 -0
- 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/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.7",
|
|
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.7",
|
|
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,31 @@ 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
|
-
".github/workflows/npm-publish.yml",
|
|
15
12
|
".gitignore",
|
|
16
13
|
"AGENTS.md",
|
|
17
14
|
"LICENSE",
|
|
18
15
|
"README.md",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const PLUGIN_REQUIRED_FILES = [
|
|
19
|
+
".claude-plugin/plugin.json",
|
|
20
|
+
".codex-plugin/plugin.json",
|
|
21
|
+
"plugin.json",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const NPM_PLUGIN_REQUIRED_FILES = [
|
|
19
25
|
"cordis.patch.yml",
|
|
20
26
|
"package.json",
|
|
21
|
-
"plugin.json",
|
|
22
27
|
"src/deepseek.ts",
|
|
23
28
|
"src/opencode.ts",
|
|
24
29
|
"tsconfig.json",
|
|
25
30
|
]
|
|
26
31
|
|
|
32
|
+
const RUNTIME_MODES = ["npm", "skills"]
|
|
33
|
+
|
|
27
34
|
function usage() {
|
|
28
35
|
return `Usage: harness-alchemist validate [project-directory] [--external] [--json]
|
|
29
36
|
|
|
@@ -44,16 +51,19 @@ Options:
|
|
|
44
51
|
|
|
45
52
|
function findProjectRoot(start) {
|
|
46
53
|
let current = resolve(start)
|
|
54
|
+
let conventionalRoot
|
|
47
55
|
while (true) {
|
|
56
|
+
if (existsSync(join(current, "alchemy.json"))) return current
|
|
48
57
|
if (
|
|
58
|
+
!conventionalRoot &&
|
|
49
59
|
existsSync(join(current, "package.json")) &&
|
|
50
60
|
existsSync(join(current, ".claude-plugin")) &&
|
|
51
61
|
existsSync(join(current, ".codex-plugin"))
|
|
52
62
|
) {
|
|
53
|
-
|
|
63
|
+
conventionalRoot = current
|
|
54
64
|
}
|
|
55
65
|
const parent = dirname(current)
|
|
56
|
-
if (parent === current) return
|
|
66
|
+
if (parent === current) return conventionalRoot
|
|
57
67
|
current = parent
|
|
58
68
|
}
|
|
59
69
|
}
|
|
@@ -252,7 +262,7 @@ except SystemExit as exit_code:
|
|
|
252
262
|
_buffer.getvalue()
|
|
253
263
|
`
|
|
254
264
|
|
|
255
|
-
async function smokePythonScripts(scripts, errors, warnings) {
|
|
265
|
+
async function smokePythonScripts(scripts, errors, warnings, execSmoke = true) {
|
|
256
266
|
let loadPyodide
|
|
257
267
|
try {
|
|
258
268
|
;({ loadPyodide } = await import("pyodide"))
|
|
@@ -269,17 +279,26 @@ async function smokePythonScripts(scripts, errors, warnings) {
|
|
|
269
279
|
return
|
|
270
280
|
}
|
|
271
281
|
|
|
282
|
+
if (!execSmoke) {
|
|
283
|
+
warnings.push("skills runtime: Python entrypoints are syntax-checked without execution")
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
for (const script of scripts) {
|
|
273
287
|
const source = await readFile(script, "utf8")
|
|
274
288
|
instance.globals.set("__ha_source__", source)
|
|
275
|
-
instance.globals.set("__ha_payload__", "{}")
|
|
276
289
|
let output
|
|
277
290
|
try {
|
|
278
|
-
|
|
291
|
+
if (execSmoke) {
|
|
292
|
+
instance.globals.set("__ha_payload__", "{}")
|
|
293
|
+
output = instance.runPython(PYTHON_SMOKE_PROGRAM)
|
|
294
|
+
} else {
|
|
295
|
+
instance.runPython('compile(__ha_source__, "<skill>", "exec")\nNone')
|
|
296
|
+
}
|
|
279
297
|
} catch (error) {
|
|
280
298
|
errors.push(`${script}: WebAssembly Python check failed (${String(error).split("\n").at(-2) ?? String(error)})`)
|
|
281
299
|
continue
|
|
282
300
|
}
|
|
301
|
+
if (!execSmoke) continue
|
|
283
302
|
try {
|
|
284
303
|
const parsed = JSON.parse(output)
|
|
285
304
|
if (parsed?.ok !== true || typeof parsed.plugin !== "string") {
|
|
@@ -290,11 +309,16 @@ async function smokePythonScripts(scripts, errors, warnings) {
|
|
|
290
309
|
}
|
|
291
310
|
}
|
|
292
311
|
|
|
293
|
-
|
|
294
|
-
|
|
312
|
+
for (const key of ["__ha_source__", "__ha_payload__"]) {
|
|
313
|
+
try {
|
|
314
|
+
instance.globals.delete(key)
|
|
315
|
+
} catch {
|
|
316
|
+
// Key was never set in this session.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
295
319
|
}
|
|
296
320
|
|
|
297
|
-
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts) {
|
|
321
|
+
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime) {
|
|
298
322
|
if ((frontmatter.description?.length ?? 0) > 1024) {
|
|
299
323
|
errors.push(`${skillFile}: description must be at most 1024 characters`)
|
|
300
324
|
}
|
|
@@ -325,12 +349,12 @@ async function checkProductSkillRuntime(skillFile, content, frontmatter, errors,
|
|
|
325
349
|
entries.filter((entry) => entry.endsWith(".mjs")).map((entry) => entry.slice(0, -4)),
|
|
326
350
|
)
|
|
327
351
|
for (const base of pythonBases) {
|
|
328
|
-
if (!mjsBases.has(base)) {
|
|
352
|
+
if (runtime === "npm" && !mjsBases.has(base)) {
|
|
329
353
|
errors.push(`${skillFile}: scripts/${base}.py is missing its scripts/${base}.mjs twin`)
|
|
330
354
|
}
|
|
331
355
|
}
|
|
332
356
|
for (const base of mjsBases) {
|
|
333
|
-
if (!pythonBases.has(base)) {
|
|
357
|
+
if (runtime === "npm" && !pythonBases.has(base)) {
|
|
334
358
|
errors.push(`${skillFile}: scripts/${base}.mjs is missing its scripts/${base}.py twin`)
|
|
335
359
|
}
|
|
336
360
|
}
|
|
@@ -344,25 +368,108 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
344
368
|
const errors = []
|
|
345
369
|
const warnings = []
|
|
346
370
|
|
|
347
|
-
|
|
371
|
+
const layoutPath = join(root, "alchemy.json")
|
|
372
|
+
const parsedLayout = existsSync(layoutPath) ? await readJson(layoutPath, errors) : {}
|
|
373
|
+
if (parsedLayout === undefined) return { root, errors, warnings }
|
|
374
|
+
if (
|
|
375
|
+
typeof parsedLayout !== "object" ||
|
|
376
|
+
parsedLayout === null ||
|
|
377
|
+
Array.isArray(parsedLayout)
|
|
378
|
+
) {
|
|
379
|
+
errors.push("alchemy.json must contain a JSON object")
|
|
380
|
+
return { root, errors, warnings }
|
|
381
|
+
}
|
|
382
|
+
const layout = parsedLayout
|
|
383
|
+
const knownLayoutFields = [
|
|
384
|
+
"pluginRoot",
|
|
385
|
+
"opencodeExport",
|
|
386
|
+
"runtime",
|
|
387
|
+
"$schema",
|
|
388
|
+
"template",
|
|
389
|
+
"generator",
|
|
390
|
+
"generatorVersion",
|
|
391
|
+
"createdAt",
|
|
392
|
+
]
|
|
393
|
+
const unknownLayoutFields = Object.keys(layout).filter(
|
|
394
|
+
(key) => !knownLayoutFields.includes(key),
|
|
395
|
+
)
|
|
396
|
+
if (unknownLayoutFields.length) {
|
|
397
|
+
errors.push(`alchemy.json has unknown fields: ${unknownLayoutFields.join(", ")}`)
|
|
398
|
+
}
|
|
399
|
+
if (layout.$schema !== undefined && typeof layout.$schema !== "string") {
|
|
400
|
+
errors.push("alchemy.json $schema must be a string")
|
|
401
|
+
}
|
|
402
|
+
if (layout.template !== undefined && !/^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(layout.template)) {
|
|
403
|
+
errors.push("alchemy.json template must be a 'vX.Y.Z' version")
|
|
404
|
+
}
|
|
405
|
+
if (layout.generator !== undefined && layout.generator !== "harness-alchemist") {
|
|
406
|
+
errors.push("alchemy.json generator must be 'harness-alchemist'")
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
layout.generatorVersion !== undefined &&
|
|
410
|
+
!/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(layout.generatorVersion)
|
|
411
|
+
) {
|
|
412
|
+
errors.push("alchemy.json generatorVersion must be an 'X.Y.Z' version")
|
|
413
|
+
}
|
|
414
|
+
if (layout.createdAt !== undefined) {
|
|
415
|
+
const createdAt = new Date(layout.createdAt)
|
|
416
|
+
if (typeof layout.createdAt !== "string" || Number.isNaN(createdAt.getTime())) {
|
|
417
|
+
errors.push("alchemy.json createdAt must be an ISO 8601 timestamp")
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const runtime = layout.runtime ?? "npm"
|
|
421
|
+
if (!RUNTIME_MODES.includes(runtime)) {
|
|
422
|
+
errors.push(`alchemy.json runtime must be one of: ${RUNTIME_MODES.join(", ")}`)
|
|
423
|
+
}
|
|
424
|
+
const pluginRootRelative = layout.pluginRoot ?? "."
|
|
425
|
+
if (typeof pluginRootRelative !== "string" || !pluginRootRelative.trim()) {
|
|
426
|
+
errors.push("alchemy.json pluginRoot must be a non-empty relative path")
|
|
427
|
+
}
|
|
428
|
+
const pluginRoot = resolve(root, typeof pluginRootRelative === "string" ? pluginRootRelative : ".")
|
|
429
|
+
if (!(pluginRoot === root || pluginRoot.startsWith(`${root}${sep}`))) {
|
|
430
|
+
errors.push("alchemy.json pluginRoot escapes the project root")
|
|
431
|
+
} else if (existsSync(pluginRoot)) {
|
|
432
|
+
const [canonicalRoot, canonicalPluginRoot] = await Promise.all([
|
|
433
|
+
realpath(root),
|
|
434
|
+
realpath(pluginRoot),
|
|
435
|
+
])
|
|
436
|
+
if (!(canonicalPluginRoot === canonicalRoot || canonicalPluginRoot.startsWith(`${canonicalRoot}${sep}`))) {
|
|
437
|
+
errors.push("alchemy.json pluginRoot resolves through a symlink outside the project root")
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const opencodeExport = layout.opencodeExport ?? "."
|
|
441
|
+
if (![".", "./server"].includes(opencodeExport)) {
|
|
442
|
+
errors.push("alchemy.json opencodeExport must be '.' or './server'")
|
|
443
|
+
}
|
|
444
|
+
if (opencodeExport !== "." && runtime !== "npm") {
|
|
445
|
+
errors.push("alchemy.json opencodeExport requires runtime 'npm'")
|
|
446
|
+
}
|
|
447
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
448
|
+
|
|
449
|
+
for (const file of PROJECT_REQUIRED_FILES) requirePath(root, file, errors)
|
|
450
|
+
if (runtime === "npm") requirePath(root, ".github/workflows/npm-publish.yml", errors)
|
|
451
|
+
const pluginRequiredFiles = runtime === "npm"
|
|
452
|
+
? [...PLUGIN_REQUIRED_FILES, ...NPM_PLUGIN_REQUIRED_FILES]
|
|
453
|
+
: PLUGIN_REQUIRED_FILES
|
|
454
|
+
for (const file of pluginRequiredFiles) {
|
|
455
|
+
requirePath(root, join(pluginRootRelative, file), errors)
|
|
456
|
+
}
|
|
348
457
|
if (errors.length > 0) return { root, errors, warnings }
|
|
349
458
|
|
|
350
|
-
const
|
|
351
|
-
const claudePlugin = await readJson(join(root, ".claude-plugin/plugin.json"), errors)
|
|
459
|
+
const claudePlugin = await readJson(join(pluginRoot, ".claude-plugin/plugin.json"), errors)
|
|
352
460
|
const claudeMarketplace = await readJson(
|
|
353
461
|
join(root, ".claude-plugin/marketplace.json"),
|
|
354
462
|
errors,
|
|
355
463
|
)
|
|
356
|
-
const codexPlugin = await readJson(join(
|
|
464
|
+
const codexPlugin = await readJson(join(pluginRoot, ".codex-plugin/plugin.json"), errors)
|
|
357
465
|
const codexMarketplace = await readJson(
|
|
358
466
|
join(root, ".agents/plugins/marketplace.json"),
|
|
359
467
|
errors,
|
|
360
468
|
)
|
|
361
|
-
const antigravityPlugin = await readJson(join(
|
|
469
|
+
const antigravityPlugin = await readJson(join(pluginRoot, "plugin.json"), errors)
|
|
362
470
|
|
|
363
471
|
if (errors.length > 0) return { root, errors, warnings }
|
|
364
472
|
|
|
365
|
-
const packageBase = packageJson.name?.split("/").at(-1)
|
|
366
473
|
const pluginName = claudePlugin.name
|
|
367
474
|
const validName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
368
475
|
|
|
@@ -372,9 +479,6 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
372
479
|
if ((pluginName?.length ?? 0) > 56) {
|
|
373
480
|
errors.push("Plugin name must be at most 56 characters")
|
|
374
481
|
}
|
|
375
|
-
if (packageBase !== pluginName) {
|
|
376
|
-
errors.push(`npm package basename '${packageBase}' must match plugin name '${pluginName}'`)
|
|
377
|
-
}
|
|
378
482
|
|
|
379
483
|
for (const [label, value] of [
|
|
380
484
|
["Codex plugin", codexPlugin.name],
|
|
@@ -383,64 +487,104 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
383
487
|
if (value !== pluginName) errors.push(`${label} name '${value}' does not match '${pluginName}'`)
|
|
384
488
|
}
|
|
385
489
|
|
|
386
|
-
|
|
387
|
-
["
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
490
|
+
if (runtime === "skills") {
|
|
491
|
+
for (const [label, manifest] of [["Codex plugin", codexPlugin]]) {
|
|
492
|
+
if (manifest.version !== claudePlugin.version) {
|
|
493
|
+
errors.push(`${label} version does not match the Claude plugin`)
|
|
494
|
+
}
|
|
495
|
+
if (manifest.description !== claudePlugin.description) {
|
|
496
|
+
errors.push(`${label} description does not match the Claude plugin`)
|
|
497
|
+
}
|
|
392
498
|
}
|
|
393
|
-
if (
|
|
394
|
-
errors.push(
|
|
499
|
+
if (antigravityPlugin.description !== claudePlugin.description) {
|
|
500
|
+
errors.push("Antigravity description does not match the Claude plugin")
|
|
395
501
|
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
errors.
|
|
399
|
-
}
|
|
502
|
+
} else {
|
|
503
|
+
const packageJson = await readJson(join(pluginRoot, "package.json"), errors)
|
|
504
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
400
505
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
[
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
506
|
+
const packageBase = packageJson.name?.split("/").at(-1)
|
|
507
|
+
if (packageBase !== pluginName) {
|
|
508
|
+
errors.push(`npm package basename '${packageBase}' must match plugin name '${pluginName}'`)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (const [label, manifest] of [
|
|
512
|
+
["Claude plugin", claudePlugin],
|
|
513
|
+
["Codex plugin", codexPlugin],
|
|
514
|
+
]) {
|
|
515
|
+
if (manifest.version !== packageJson.version) {
|
|
516
|
+
errors.push(`${label} version does not match package.json`)
|
|
517
|
+
}
|
|
518
|
+
if (manifest.description !== packageJson.description) {
|
|
519
|
+
errors.push(`${label} description does not match package.json`)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (antigravityPlugin.description !== packageJson.description) {
|
|
523
|
+
errors.push("Antigravity description does not match package.json")
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (packageJson.type !== "module") errors.push("package.json type must be 'module'")
|
|
527
|
+
if (opencodeExport === "." && packageJson.engines?.node !== ">=22.20.0") {
|
|
528
|
+
errors.push("package.json engines.node must be '>=22.20.0'")
|
|
529
|
+
}
|
|
530
|
+
const requiredExports = [
|
|
531
|
+
[opencodeExport, "import", "./dist/opencode.js"],
|
|
532
|
+
[opencodeExport, "types", "./dist/opencode.d.ts"],
|
|
533
|
+
["./deepseek", "import", "./dist/deepseek.js"],
|
|
534
|
+
["./deepseek", "types", "./dist/deepseek.d.ts"],
|
|
535
|
+
["./cordis.patch.yml", "import", "./cordis.patch.yml"],
|
|
536
|
+
]
|
|
537
|
+
for (const [key, field, expected] of requiredExports) {
|
|
538
|
+
const target = exportTarget(packageJson.exports, key, field)
|
|
539
|
+
if (target !== expected) {
|
|
540
|
+
errors.push(`package.json export '${key}' ${field} target must be '${expected}'`)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const entry of [
|
|
544
|
+
"dist",
|
|
545
|
+
"skills",
|
|
546
|
+
"alchemy.json",
|
|
547
|
+
"cordis.patch.yml",
|
|
548
|
+
".claude-plugin/plugin.json",
|
|
549
|
+
".codex-plugin/plugin.json",
|
|
550
|
+
"plugin.json",
|
|
551
|
+
]) {
|
|
552
|
+
if (!packageJson.files?.includes(entry)) errors.push(`package.json files is missing '${entry}'`)
|
|
553
|
+
}
|
|
554
|
+
if (packageJson.dsh?.bundle?.patch !== "./cordis.patch.yml") {
|
|
555
|
+
errors.push("package.json dsh.bundle.patch must be './cordis.patch.yml'")
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const patch = await readFile(join(pluginRoot, "cordis.patch.yml"), "utf8")
|
|
559
|
+
const expectedModule = `${packageJson.name}/deepseek`
|
|
560
|
+
const cordisEntries = parseCordisInsertEntries(patch)
|
|
561
|
+
.filter((entry) => entry.id === pluginName)
|
|
562
|
+
const cordisEntry = cordisEntries[0]
|
|
563
|
+
if (cordisEntries.length !== 1 || !cordisEntry?.valid) {
|
|
564
|
+
errors.push(`cordis.patch.yml must contain a valid insert entry for '${pluginName}'`)
|
|
565
|
+
} else if (cordisEntry.name !== expectedModule) {
|
|
566
|
+
errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
|
|
416
567
|
}
|
|
417
568
|
}
|
|
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
|
-
}
|
|
431
569
|
|
|
432
|
-
await requireManifestPath(
|
|
433
|
-
await requireManifestPath(
|
|
570
|
+
await requireManifestPath(pluginRoot, claudePlugin.skills, "Claude skills", errors)
|
|
571
|
+
await requireManifestPath(pluginRoot, codexPlugin.skills, "Codex skills", errors)
|
|
572
|
+
|
|
573
|
+
const marketplaceSource = pluginRoot === root
|
|
574
|
+
? "./"
|
|
575
|
+
: `./${pluginRootRelative.replaceAll("\\", "/").replace(/^\.\//, "")}`
|
|
434
576
|
|
|
435
577
|
const claudeEntry = claudeMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
436
578
|
if (!claudeEntry) errors.push("Claude marketplace is missing the plugin entry")
|
|
437
|
-
else if (claudeEntry.source !==
|
|
579
|
+
else if (claudeEntry.source !== marketplaceSource) {
|
|
580
|
+
errors.push(`Claude marketplace source must be '${marketplaceSource}'`)
|
|
581
|
+
}
|
|
438
582
|
|
|
439
583
|
const codexEntry = codexMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
440
584
|
if (!codexEntry) errors.push("Codex marketplace is missing the plugin entry")
|
|
441
585
|
else {
|
|
442
|
-
if (codexEntry.source?.source !== "local" || codexEntry.source?.path !==
|
|
443
|
-
errors.push(
|
|
586
|
+
if (codexEntry.source?.source !== "local" || codexEntry.source?.path !== marketplaceSource) {
|
|
587
|
+
errors.push(`Codex marketplace source must be a local '${marketplaceSource}' path`)
|
|
444
588
|
}
|
|
445
589
|
if (!codexEntry.policy?.installation || !codexEntry.policy?.authentication) {
|
|
446
590
|
errors.push("Codex marketplace entry requires installation and authentication policies")
|
|
@@ -448,19 +592,9 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
448
592
|
if (!codexEntry.category) errors.push("Codex marketplace entry requires a category")
|
|
449
593
|
}
|
|
450
594
|
|
|
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
595
|
|
|
462
596
|
const maintenanceSkillFiles = await collectSkillFiles(root, ".agents/skills")
|
|
463
|
-
const productSkillFiles = await collectSkillFiles(
|
|
597
|
+
const productSkillFiles = await collectSkillFiles(pluginRoot, "skills")
|
|
464
598
|
const skillFiles = [...productSkillFiles, ...maintenanceSkillFiles]
|
|
465
599
|
if (skillFiles.length < 2) errors.push("Expected a shared skill and a project development skill")
|
|
466
600
|
|
|
@@ -479,13 +613,13 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
479
613
|
errors.push(`${skillFile}: invalid Agent Skill name`)
|
|
480
614
|
}
|
|
481
615
|
if (productSkillFiles.includes(skillFile)) {
|
|
482
|
-
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts)
|
|
616
|
+
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime)
|
|
483
617
|
}
|
|
484
618
|
}
|
|
485
619
|
}
|
|
486
620
|
|
|
487
621
|
if (pythonScripts.size > 0) {
|
|
488
|
-
await smokePythonScripts([...pythonScripts], errors, warnings)
|
|
622
|
+
await smokePythonScripts([...pythonScripts], errors, warnings, runtime === "npm")
|
|
489
623
|
}
|
|
490
624
|
|
|
491
625
|
for (const path of await scanForTokens(root)) {
|
|
@@ -493,7 +627,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
493
627
|
}
|
|
494
628
|
|
|
495
629
|
if (options.external) {
|
|
496
|
-
const result = spawnSync("claude", ["plugin", "validate",
|
|
630
|
+
const result = spawnSync("claude", ["plugin", "validate", pluginRoot, "--strict"], {
|
|
497
631
|
encoding: "utf8",
|
|
498
632
|
})
|
|
499
633
|
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.7",
|
|
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
|
|
|
@@ -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 |
|
|
@@ -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}`)
|