harness-alchemist 0.1.7 → 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.
@@ -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.7",
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.7",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-alchemist",
3
- "version": "0.1.7",
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
  "type": "module",
6
6
  "bin": {
@@ -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