harness-alchemist 0.1.6 → 0.1.8
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 +23 -0
- package/bin/harness-alchemist.mjs +5 -0
- package/lib/install-check.mjs +363 -0
- package/lib/validate.mjs +21 -7
- package/package.json +1 -1
- package/skills/harness-alchemist/references/publishing.md +11 -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.8",
|
|
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.8",
|
|
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
|
@@ -127,6 +127,29 @@ echo '{"request": "hello"}' | node skills/<name>/scripts/main.mjs
|
|
|
127
127
|
- Non-zero exit with a stderr diagnostic on failure.
|
|
128
128
|
- `scripts/main.py` is a stdlib-only behavioral twin of `scripts/main.mjs`.
|
|
129
129
|
|
|
130
|
+
## Install-level verification
|
|
131
|
+
|
|
132
|
+
`install-check` drives your local harness CLIs against the project and asserts
|
|
133
|
+
each one can discover the plugin — the same checks a user's install would
|
|
134
|
+
perform, automated and cleaned up afterwards:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx harness-alchemist@latest install-check /path/to/project
|
|
138
|
+
npx harness-alchemist@latest install-check . --harness agy --json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Harness | Verified by | Isolation |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| Claude Code | marketplace add → install → `plugin details` skill inventory | user scope, auto-removed |
|
|
144
|
+
| Codex | marketplace add → plugin add → `plugin list` enabled | plugin cache, auto-removed |
|
|
145
|
+
| Antigravity | `plugin validate` → install → `plugin list` | staged, auto-uninstalled |
|
|
146
|
+
| OpenCode | skills discovery via `debug skill` + plugin startup | temp `XDG_CONFIG_HOME` |
|
|
147
|
+
| DeepSeek | Cordis bundle composed into profile (`--dump-config`) | temp `DSH_HOME` |
|
|
148
|
+
|
|
149
|
+
Claude, Codex, and Antigravity run in both runtimes; the OpenCode plugin leg
|
|
150
|
+
and the DeepSeek Cordis check require npm mode with built adapters
|
|
151
|
+
(`npm run build` first). Missing CLIs are reported as skipped, not failures.
|
|
152
|
+
|
|
130
153
|
## Validation tiers
|
|
131
154
|
|
|
132
155
|
`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.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFile } from "node:fs/promises"
|
|
4
4
|
|
|
5
5
|
import { listTemplates, runCreate } from "../lib/create.mjs"
|
|
6
|
+
import { runInstallCheck } from "../lib/install-check.mjs"
|
|
6
7
|
import { runValidate } from "../lib/validate.mjs"
|
|
7
8
|
|
|
8
9
|
const packageJson = JSON.parse(
|
|
@@ -17,6 +18,8 @@ Usage: harness-alchemist <command> [options]
|
|
|
17
18
|
Commands:
|
|
18
19
|
create <directory> Create a universal coding-agent plugin repository.
|
|
19
20
|
validate [directory] Validate a generated repository.
|
|
21
|
+
install-check Install the plugin into local harness CLIs and verify
|
|
22
|
+
discovery (claude, codex, agy, opencode, dsh).
|
|
20
23
|
templates List bundled canonical templates.
|
|
21
24
|
version Print the CLI version.
|
|
22
25
|
help Show this help.
|
|
@@ -36,6 +39,8 @@ if (!command || command === "help" || command === "--help" || command === "-h")
|
|
|
36
39
|
process.exitCode = await runCreate(args)
|
|
37
40
|
} else if (command === "validate") {
|
|
38
41
|
process.exitCode = await runValidate(args)
|
|
42
|
+
} else if (command === "install-check") {
|
|
43
|
+
process.exitCode = await runInstallCheck(args)
|
|
39
44
|
} else {
|
|
40
45
|
console.error(`Unknown command: ${command}\n`)
|
|
41
46
|
console.error(usage())
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
4
|
+
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { dirname, join, resolve } from "node:path"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
import { spawnSync } from "node:child_process"
|
|
9
|
+
|
|
10
|
+
const TIMEOUT_MS = 300_000
|
|
11
|
+
|
|
12
|
+
function findProjectRoot(start) {
|
|
13
|
+
let current = resolve(start)
|
|
14
|
+
let conventionalRoot
|
|
15
|
+
while (true) {
|
|
16
|
+
if (existsSync(join(current, "alchemy.json"))) return current
|
|
17
|
+
if (
|
|
18
|
+
!conventionalRoot &&
|
|
19
|
+
existsSync(join(current, "package.json")) &&
|
|
20
|
+
existsSync(join(current, ".claude-plugin")) &&
|
|
21
|
+
existsSync(join(current, ".codex-plugin"))
|
|
22
|
+
) {
|
|
23
|
+
conventionalRoot = current
|
|
24
|
+
}
|
|
25
|
+
const parent = dirname(current)
|
|
26
|
+
if (parent === current) return conventionalRoot
|
|
27
|
+
current = parent
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function run(command, args, options = {}) {
|
|
32
|
+
const result = spawnSync(command, args, {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
timeout: TIMEOUT_MS,
|
|
35
|
+
...options,
|
|
36
|
+
})
|
|
37
|
+
if (result.error?.code === "ENOENT") return { missing: true }
|
|
38
|
+
return {
|
|
39
|
+
ok: result.status === 0,
|
|
40
|
+
status: result.status,
|
|
41
|
+
stdout: result.stdout ?? "",
|
|
42
|
+
stderr: result.stderr ?? "",
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function firstLine(text) {
|
|
47
|
+
return text.trim().split("\n")[0] ?? ""
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function usage() {
|
|
51
|
+
return `Usage: harness-alchemist install-check [project-directory] [options]
|
|
52
|
+
|
|
53
|
+
Install-level verification: drives the local harness CLIs (claude, codex,
|
|
54
|
+
agy, opencode, dsh) against the project's plugin package and asserts each
|
|
55
|
+
harness can discover it. Static validation is a prerequisite; run
|
|
56
|
+
'harness-alchemist validate' first.
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
--harness <id> Limit to one harness (claude, codex, agy, opencode, dsh).
|
|
60
|
+
Repeatable.
|
|
61
|
+
--keep Keep installed plugins and marketplaces after the check.
|
|
62
|
+
--json Print a machine-readable result.
|
|
63
|
+
--help Show this help.`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const options = { harnesses: [], keep: false, json: false }
|
|
68
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
69
|
+
const arg = argv[index]
|
|
70
|
+
if (arg === "--help" || arg === "-h") return { help: true }
|
|
71
|
+
if (arg === "--keep") options.keep = true
|
|
72
|
+
else if (arg === "--json") options.json = true
|
|
73
|
+
else if (arg === "--harness") {
|
|
74
|
+
const value = argv[index + 1]
|
|
75
|
+
if (!value || value.startsWith("--")) throw new Error("--harness requires a value")
|
|
76
|
+
if (!["claude", "codex", "agy", "opencode", "dsh"].includes(value)) {
|
|
77
|
+
throw new Error(`Unknown harness '${value}'. Choose from claude, codex, agy, opencode, dsh`)
|
|
78
|
+
}
|
|
79
|
+
options.harnesses.push(value)
|
|
80
|
+
index += 1
|
|
81
|
+
} else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`)
|
|
82
|
+
else if (options.project) throw new Error("Only one project directory may be supplied")
|
|
83
|
+
else options.project = arg
|
|
84
|
+
}
|
|
85
|
+
return options
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readJson(path, problems) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(await readFile(path, "utf8"))
|
|
91
|
+
} catch (error) {
|
|
92
|
+
problems.push(`${path}: invalid JSON (${error.message})`)
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function skillNames(skillsDirectory) {
|
|
98
|
+
if (!existsSync(skillsDirectory)) return []
|
|
99
|
+
return readdirSync(skillsDirectory)
|
|
100
|
+
.filter((entry) => existsSync(join(skillsDirectory, entry, "SKILL.md")))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function runInstallCheck(argv) {
|
|
104
|
+
let options
|
|
105
|
+
try {
|
|
106
|
+
options = parseArgs(argv)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(error.message)
|
|
109
|
+
console.error(usage())
|
|
110
|
+
return 2
|
|
111
|
+
}
|
|
112
|
+
if (options.help) {
|
|
113
|
+
console.log(usage())
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
118
|
+
const root = options.project
|
|
119
|
+
? resolve(options.project)
|
|
120
|
+
: findProjectRoot(process.cwd()) ?? findProjectRoot(scriptDirectory)
|
|
121
|
+
if (!root) {
|
|
122
|
+
console.error("Could not find a universal plugin project. Pass its directory explicitly.")
|
|
123
|
+
return 2
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const problems = []
|
|
127
|
+
const layoutPath = join(root, "alchemy.json")
|
|
128
|
+
const layout = existsSync(layoutPath) ? (await readJson(layoutPath, problems)) ?? {} : {}
|
|
129
|
+
const pluginRoot = resolve(root, layout.pluginRoot ?? ".")
|
|
130
|
+
const runtime = layout.runtime ?? "npm"
|
|
131
|
+
|
|
132
|
+
const claudePlugin = await readJson(join(pluginRoot, ".claude-plugin/plugin.json"), problems)
|
|
133
|
+
const claudeMarketplace = await readJson(join(root, ".claude-plugin/marketplace.json"), problems)
|
|
134
|
+
if (problems.length > 0) {
|
|
135
|
+
for (const problem of problems) console.error(problem)
|
|
136
|
+
return 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pluginName = claudePlugin.name
|
|
140
|
+
const marketplaceName = claudeMarketplace.name
|
|
141
|
+
const skills = skillNames(join(pluginRoot, "skills"))
|
|
142
|
+
const selected = options.harnesses.length > 0
|
|
143
|
+
? options.harnesses
|
|
144
|
+
: ["claude", "codex", "agy", "opencode", "dsh"]
|
|
145
|
+
|
|
146
|
+
const results = []
|
|
147
|
+
const record = (harness, status, details) => {
|
|
148
|
+
results.push({ harness, status, details })
|
|
149
|
+
if (options.json) return
|
|
150
|
+
const marker = status === "pass" ? "✔" : status === "skip" ? "○" : "✖"
|
|
151
|
+
console.log(`${marker} ${harness}: ${status}`)
|
|
152
|
+
for (const detail of details) console.log(` ${detail}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const adapterPath = join(pluginRoot, "dist/opencode.js")
|
|
156
|
+
|
|
157
|
+
for (const harness of selected) {
|
|
158
|
+
if (harness === "claude") {
|
|
159
|
+
const details = []
|
|
160
|
+
let status = "pass"
|
|
161
|
+
const steps = [
|
|
162
|
+
["validate", pluginRoot, "--strict"],
|
|
163
|
+
["marketplace", "add", root],
|
|
164
|
+
["install", `${pluginName}@${marketplaceName}`],
|
|
165
|
+
["details", pluginName],
|
|
166
|
+
]
|
|
167
|
+
for (const args of steps) {
|
|
168
|
+
const result = run("claude", ["plugin", ...args])
|
|
169
|
+
if (result.missing) { status = "skip"; details.push("claude CLI not found"); break }
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
status = "fail"
|
|
172
|
+
details.push(`\`${command} ${args.join(" ")}\` failed: ${firstLine(result.stderr || result.stdout)}`)
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
if (args[0] === "details") {
|
|
176
|
+
for (const skill of skills) {
|
|
177
|
+
if (!result.stdout.includes(skill)) {
|
|
178
|
+
status = "fail"
|
|
179
|
+
details.push(`skill '${skill}' missing from plugin details`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
details.push(`${skills.length} skills registered`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!options.keep) {
|
|
186
|
+
run("claude", ["plugin", "uninstall", `${pluginName}@${marketplaceName}`])
|
|
187
|
+
run("claude", ["plugin", "marketplace", "remove", marketplaceName])
|
|
188
|
+
}
|
|
189
|
+
record(harness, status, details)
|
|
190
|
+
} else if (harness === "codex") {
|
|
191
|
+
const details = []
|
|
192
|
+
let status = "pass"
|
|
193
|
+
const add = run("codex", ["plugin", "marketplace", "add", root])
|
|
194
|
+
if (add.missing) {
|
|
195
|
+
record(harness, "skip", ["codex CLI not found"])
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (!add.ok) {
|
|
199
|
+
record(harness, "fail", [`marketplace add failed: ${firstLine(add.stderr || add.stdout)}`])
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const install = run("codex", ["plugin", "add", `${pluginName}@${marketplaceName}`])
|
|
203
|
+
if (!install.ok) {
|
|
204
|
+
status = "fail"
|
|
205
|
+
details.push(`plugin add failed: ${firstLine(install.stderr || install.stdout)}`)
|
|
206
|
+
} else {
|
|
207
|
+
const list = run("codex", ["plugin", "list"])
|
|
208
|
+
if (!list.ok || !list.stdout.includes(`${pluginName}@${marketplaceName}`)) {
|
|
209
|
+
status = "fail"
|
|
210
|
+
details.push("plugin not listed as installed")
|
|
211
|
+
} else if (!list.stdout.includes("installed, enabled")) {
|
|
212
|
+
status = "fail"
|
|
213
|
+
details.push("plugin installed but not enabled")
|
|
214
|
+
} else {
|
|
215
|
+
details.push("installed and enabled")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!options.keep) {
|
|
219
|
+
run("codex", ["plugin", "remove", `${pluginName}@${marketplaceName}`])
|
|
220
|
+
run("codex", ["plugin", "marketplace", "remove", marketplaceName])
|
|
221
|
+
}
|
|
222
|
+
record(harness, status, details)
|
|
223
|
+
} else if (harness === "agy") {
|
|
224
|
+
const details = []
|
|
225
|
+
const validate = run("agy", ["plugin", "validate", pluginRoot])
|
|
226
|
+
if (validate.missing) {
|
|
227
|
+
record(harness, "skip", ["agy CLI not found"])
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
if (!validate.ok) {
|
|
231
|
+
record(harness, "fail", [`validate failed: ${firstLine(validate.stderr || validate.stdout)}`])
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
const install = run("agy", ["plugin", "install", pluginRoot])
|
|
235
|
+
if (!install.ok) {
|
|
236
|
+
record(harness, "fail", [`install failed: ${firstLine(install.stderr || install.stdout)}`])
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
const list = run("agy", ["plugin", "list"])
|
|
240
|
+
if (!list.ok || !list.stdout.includes(pluginName)) {
|
|
241
|
+
record(harness, "fail", ["plugin missing from agy plugin list"])
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
details.push("validated, installed, and listed")
|
|
245
|
+
if (!options.keep) run("agy", ["plugin", "uninstall", pluginName])
|
|
246
|
+
record(harness, "pass", details)
|
|
247
|
+
} else if (harness === "opencode") {
|
|
248
|
+
if (runtime === "npm" && !existsSync(adapterPath)) {
|
|
249
|
+
record(harness, "fail", [
|
|
250
|
+
`npm runtime requires a built adapter; missing ${adapterPath}`,
|
|
251
|
+
"run `npm run build` in the plugin package first",
|
|
252
|
+
])
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const isolation = await mkdtemp(join(tmpdir(), "ha-opencode-"))
|
|
256
|
+
const configDirectory = join(isolation, "opencode")
|
|
257
|
+
const skillsDirectory = join(configDirectory, "skills")
|
|
258
|
+
await cp(join(pluginRoot, "skills"), skillsDirectory, { recursive: true })
|
|
259
|
+
const config = { $schema: "https://opencode.ai/config.json" }
|
|
260
|
+
if (runtime === "npm") {
|
|
261
|
+
config.plugin = [`file://${adapterPath}`]
|
|
262
|
+
}
|
|
263
|
+
await writeFile(join(configDirectory, "opencode.json"), `${JSON.stringify(config, null, 2)}\n`)
|
|
264
|
+
const environment = {
|
|
265
|
+
...process.env,
|
|
266
|
+
XDG_CONFIG_HOME: isolation,
|
|
267
|
+
HOME: isolation,
|
|
268
|
+
}
|
|
269
|
+
const skillCheck = run("opencode", ["debug", "skill"], { env: environment })
|
|
270
|
+
if (skillCheck.missing) {
|
|
271
|
+
await rm(isolation, { recursive: true, force: true })
|
|
272
|
+
record(harness, "skip", ["opencode CLI not found"])
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
const details = []
|
|
276
|
+
let status = "pass"
|
|
277
|
+
const missing = skills.filter((skill) => !skillCheck.stdout.includes(`"name": "${skill}"`))
|
|
278
|
+
if (missing.length > 0) {
|
|
279
|
+
status = "fail"
|
|
280
|
+
details.push(`skills not discovered: ${missing.join(", ")}`)
|
|
281
|
+
} else {
|
|
282
|
+
details.push(`${skills.length} skills discovered`)
|
|
283
|
+
}
|
|
284
|
+
if (runtime === "npm" && status === "pass") {
|
|
285
|
+
const startup = run("opencode", ["debug", "startup"], { env: environment })
|
|
286
|
+
if (!startup.ok) {
|
|
287
|
+
status = "fail"
|
|
288
|
+
details.push(`startup failed: ${firstLine(startup.stderr || startup.stdout)}`)
|
|
289
|
+
} else {
|
|
290
|
+
details.push("plugin loaded at startup")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
await rm(isolation, { recursive: true, force: true })
|
|
294
|
+
record(harness, status, details)
|
|
295
|
+
} else if (harness === "dsh") {
|
|
296
|
+
if (runtime !== "npm") {
|
|
297
|
+
record(harness, "skip", ["skills runtime has no Cordis bundle; install skills via filesystem roots instead"])
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
if (!existsSync(adapterPath)) {
|
|
301
|
+
record(harness, "fail", [
|
|
302
|
+
`npm runtime requires built adapters; missing ${join(pluginRoot, "dist/deepseek.js")}`,
|
|
303
|
+
"run `npm run build` in the plugin package first",
|
|
304
|
+
])
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const dshHome = await mkdtemp(join(tmpdir(), "ha-dsh-"))
|
|
308
|
+
const environment = { ...process.env, DSH_HOME: dshHome }
|
|
309
|
+
let dshCommand = "dsh"
|
|
310
|
+
let dshPrefix = []
|
|
311
|
+
if (spawnSync("dsh", ["--version"], { encoding: "utf8" }).error?.code === "ENOENT") {
|
|
312
|
+
dshCommand = "npx"
|
|
313
|
+
dshPrefix = ["-y", "@deepseek-ai/dsh"]
|
|
314
|
+
}
|
|
315
|
+
const add = run(dshCommand, [...dshPrefix, "plugin", "--profile", "install-check", "add", pluginRoot], { env: environment })
|
|
316
|
+
if (add.missing) {
|
|
317
|
+
await rm(dshHome, { recursive: true, force: true })
|
|
318
|
+
record(harness, "skip", ["dsh CLI not found and npx unavailable"])
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
if (!add.ok) {
|
|
322
|
+
await rm(dshHome, { recursive: true, force: true })
|
|
323
|
+
record(harness, "fail", [`profile add failed: ${firstLine(add.stderr || add.stdout)}`])
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
const dump = run(dshCommand, [...dshPrefix, "--profile", "install-check", "--dump-config"], { env: environment })
|
|
327
|
+
const details = []
|
|
328
|
+
let status = "pass"
|
|
329
|
+
if (!dump.ok || !dump.stdout.includes(`- id: ${pluginName}`)) {
|
|
330
|
+
status = "fail"
|
|
331
|
+
details.push("cordis insert entry missing from composed profile")
|
|
332
|
+
} else {
|
|
333
|
+
details.push("cordis bundle composed into isolated profile")
|
|
334
|
+
}
|
|
335
|
+
await rm(dshHome, { recursive: true, force: true })
|
|
336
|
+
record(harness, status, details)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const failed = results.filter((entry) => entry.status === "fail")
|
|
341
|
+
const skipped = results.filter((entry) => entry.status === "skip")
|
|
342
|
+
if (options.json) {
|
|
343
|
+
console.log(JSON.stringify({
|
|
344
|
+
project: root,
|
|
345
|
+
plugin: pluginName,
|
|
346
|
+
runtime,
|
|
347
|
+
results,
|
|
348
|
+
summary: {
|
|
349
|
+
pass: results.length - failed.length - skipped.length,
|
|
350
|
+
fail: failed.length,
|
|
351
|
+
skip: skipped.length,
|
|
352
|
+
},
|
|
353
|
+
}, null, 2))
|
|
354
|
+
} else {
|
|
355
|
+
console.log(
|
|
356
|
+
failed.length === 0
|
|
357
|
+
? `Install check passed for ${pluginName} (${results.length - skipped.length} verified, ${skipped.length} skipped)`
|
|
358
|
+
: `Install check failed for ${pluginName}: ${failed.length} of ${results.length} harnesses failed`,
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return failed.length > 0 ? 1 : 0
|
|
363
|
+
}
|
package/lib/validate.mjs
CHANGED
|
@@ -9,7 +9,6 @@ import { spawnSync } from "node:child_process"
|
|
|
9
9
|
const PROJECT_REQUIRED_FILES = [
|
|
10
10
|
".agents/plugins/marketplace.json",
|
|
11
11
|
".claude-plugin/marketplace.json",
|
|
12
|
-
".github/workflows/npm-publish.yml",
|
|
13
12
|
".gitignore",
|
|
14
13
|
"AGENTS.md",
|
|
15
14
|
"LICENSE",
|
|
@@ -263,7 +262,7 @@ except SystemExit as exit_code:
|
|
|
263
262
|
_buffer.getvalue()
|
|
264
263
|
`
|
|
265
264
|
|
|
266
|
-
async function smokePythonScripts(scripts, errors, warnings) {
|
|
265
|
+
async function smokePythonScripts(scripts, errors, warnings, execSmoke = true) {
|
|
267
266
|
let loadPyodide
|
|
268
267
|
try {
|
|
269
268
|
;({ loadPyodide } = await import("pyodide"))
|
|
@@ -280,17 +279,26 @@ async function smokePythonScripts(scripts, errors, warnings) {
|
|
|
280
279
|
return
|
|
281
280
|
}
|
|
282
281
|
|
|
282
|
+
if (!execSmoke) {
|
|
283
|
+
warnings.push("skills runtime: Python entrypoints are syntax-checked without execution")
|
|
284
|
+
}
|
|
285
|
+
|
|
283
286
|
for (const script of scripts) {
|
|
284
287
|
const source = await readFile(script, "utf8")
|
|
285
288
|
instance.globals.set("__ha_source__", source)
|
|
286
|
-
instance.globals.set("__ha_payload__", "{}")
|
|
287
289
|
let output
|
|
288
290
|
try {
|
|
289
|
-
|
|
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
|
+
}
|
|
290
297
|
} catch (error) {
|
|
291
298
|
errors.push(`${script}: WebAssembly Python check failed (${String(error).split("\n").at(-2) ?? String(error)})`)
|
|
292
299
|
continue
|
|
293
300
|
}
|
|
301
|
+
if (!execSmoke) continue
|
|
294
302
|
try {
|
|
295
303
|
const parsed = JSON.parse(output)
|
|
296
304
|
if (parsed?.ok !== true || typeof parsed.plugin !== "string") {
|
|
@@ -301,8 +309,13 @@ async function smokePythonScripts(scripts, errors, warnings) {
|
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
319
|
}
|
|
307
320
|
|
|
308
321
|
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime) {
|
|
@@ -434,6 +447,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
434
447
|
if (errors.length > 0) return { root, errors, warnings }
|
|
435
448
|
|
|
436
449
|
for (const file of PROJECT_REQUIRED_FILES) requirePath(root, file, errors)
|
|
450
|
+
if (runtime === "npm") requirePath(root, ".github/workflows/npm-publish.yml", errors)
|
|
437
451
|
const pluginRequiredFiles = runtime === "npm"
|
|
438
452
|
? [...PLUGIN_REQUIRED_FILES, ...NPM_PLUGIN_REQUIRED_FILES]
|
|
439
453
|
: PLUGIN_REQUIRED_FILES
|
|
@@ -605,7 +619,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
605
619
|
}
|
|
606
620
|
|
|
607
621
|
if (pythonScripts.size > 0) {
|
|
608
|
-
await smokePythonScripts([...pythonScripts], errors, warnings)
|
|
622
|
+
await smokePythonScripts([...pythonScripts], errors, warnings, runtime === "npm")
|
|
609
623
|
}
|
|
610
624
|
|
|
611
625
|
for (const path of await scanForTokens(root)) {
|
package/package.json
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Before Publishing
|
|
4
4
|
|
|
5
|
+
Run the full verification ladder before distributing:
|
|
6
|
+
|
|
7
|
+
1. `harness-alchemist validate <project>` — static contract for all harnesses.
|
|
8
|
+
2. The project's own `verify` script — build plus runtime delegation tests.
|
|
9
|
+
3. `harness-alchemist install-check <project>` — drives the local harness CLIs
|
|
10
|
+
(claude, codex, agy, opencode, dsh) and asserts real discovery. Claude,
|
|
11
|
+
Codex, and Antigravity verify in both runtimes; the OpenCode plugin leg and
|
|
12
|
+
DeepSeek Cordis check need npm mode with built adapters. Missing CLIs are
|
|
13
|
+
reported as skipped. Use `--keep` to leave installs in place for manual
|
|
14
|
+
inspection, and `--harness <id>` to target one harness.
|
|
15
|
+
|
|
5
16
|
Run:
|
|
6
17
|
|
|
7
18
|
```bash
|