harness-alchemist 0.1.0 → 0.1.2

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.
Files changed (26) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/dist/deepseek.d.ts +2 -1
  4. package/dist/deepseek.d.ts.map +1 -1
  5. package/dist/deepseek.js +23 -2
  6. package/dist/deepseek.js.map +1 -1
  7. package/dist/opencode.d.ts +19 -1
  8. package/dist/opencode.d.ts.map +1 -1
  9. package/dist/opencode.js +54 -1
  10. package/dist/opencode.js.map +1 -1
  11. package/lib/validate.mjs +122 -5
  12. package/package.json +5 -3
  13. package/skills/harness-alchemist/SKILL.md +9 -0
  14. package/skills/harness-alchemist/references/compatibility.md +9 -0
  15. package/skills/harness-alchemist/scripts/main.mjs +22 -0
  16. package/skills/harness-alchemist/scripts/main.py +28 -0
  17. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/SKILL.md.tpl +5 -2
  18. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/references/compatibility.md.tpl +29 -0
  19. package/templates/v0.1.0/universal-typescript/package.json.tpl +1 -0
  20. package/templates/v0.1.0/universal-typescript/skills/shared-skill/SKILL.md.tpl +18 -4
  21. package/templates/v0.1.0/universal-typescript/skills/shared-skill/references/tool-contract.md.tpl +35 -0
  22. package/templates/v0.1.0/universal-typescript/skills/shared-skill/scripts/main.mjs.tpl +22 -0
  23. package/templates/v0.1.0/universal-typescript/skills/shared-skill/scripts/main.py.tpl +28 -0
  24. package/templates/v0.1.0/universal-typescript/src/deepseek.ts.tpl +31 -2
  25. package/templates/v0.1.0/universal-typescript/src/opencode.ts.tpl +61 -1
  26. package/templates/v0.1.0/universal-typescript/tests/runtimes.test.mjs.tpl +35 -2
@@ -2,13 +2,13 @@
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.0",
5
+ "version": "0.1.2",
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"
9
9
  },
10
- "homepage": "https://github.com/lunarmoon26/harness-alchemist",
11
- "repository": "https://github.com/lunarmoon26/harness-alchemist",
10
+ "homepage": "git+https://github.com/lunarmoon26/harness-alchemist.git",
11
+ "repository": "git+https://github.com/lunarmoon26/harness-alchemist.git",
12
12
  "license": "MIT",
13
13
  "skills": "./skills/"
14
14
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "harness-alchemist",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
7
- "url": "https://github.com/lunarmoon26/harness-alchemist"
7
+ "url": "git+https://github.com/lunarmoon26/harness-alchemist.git"
8
8
  },
9
- "homepage": "https://github.com/lunarmoon26/harness-alchemist",
10
- "repository": "https://github.com/lunarmoon26/harness-alchemist",
9
+ "homepage": "git+https://github.com/lunarmoon26/harness-alchemist.git",
10
+ "repository": "git+https://github.com/lunarmoon26/harness-alchemist.git",
11
11
  "license": "MIT",
12
12
  "skills": "./skills/"
13
13
  }
@@ -1,4 +1,5 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  export declare const name = "harness-alchemist";
3
- export declare function apply(_context: Context): void;
3
+ export declare function runSkillScript(script: string, payload: string): unknown;
4
+ export declare function apply(context: Context): void;
4
5
  //# sourceMappingURL=deepseek.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC,wBAAgB,KAAK,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAE7C"}
1
+ {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAQvC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAcvE;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK5C"}
package/dist/deepseek.js CHANGED
@@ -1,5 +1,26 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  export const name = "harness-alchemist";
2
- export function apply(_context) {
3
- // Register Cordis services, tools, or lifecycle effects here.
5
+ const scriptsDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "skills/harness-alchemist/scripts");
6
+ export function runSkillScript(script, payload) {
7
+ const runtime = script.endsWith(".py") ? "python3" : process.execPath;
8
+ const result = spawnSync(runtime, [join(scriptsDirectory, script)], {
9
+ input: payload,
10
+ encoding: "utf8",
11
+ });
12
+ if (result.error) {
13
+ throw new Error(`Could not run ${script} via ${runtime}: ${result.error.message}`);
14
+ }
15
+ if (result.status !== 0) {
16
+ const diagnostic = result.stderr.trim() || `${script} exited with code ${result.status}`;
17
+ throw new Error(diagnostic);
18
+ }
19
+ return JSON.parse(result.stdout);
20
+ }
21
+ export function apply(context) {
22
+ context.provide(name, {
23
+ run: (payload, runtime = "node") => runSkillScript(runtime === "python" ? "main.py" : "main.mjs", payload),
24
+ });
4
25
  }
5
26
  //# sourceMappingURL=deepseek.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,UAAU,KAAK,CAAC,QAAiB;IACrC,8DAA8D;AAChE,CAAC"}
1
+ {"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAIxC,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,gBAAgB,GAAG,IAAI,CAC3B,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,IAAI,EACJ,kCAAkC,CACnC,CAAA;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,OAAe;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAA;IACrE,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,EAAE;QAClE,KAAK,EAAE,OAAO;QACd,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,MAAM,QAAQ,OAAO,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,qBAAqB,MAAM,CAAC,MAAM,EAAE,CAAA;QACxF,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAA;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,OAAgB;IACpC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;QACpB,GAAG,EAAE,CAAC,OAAe,EAAE,UAA6B,MAAM,EAAE,EAAE,CAC5D,cAAc,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC;KACzE,CAAC,CAAA;AACJ,CAAC"}
@@ -1,3 +1,21 @@
1
- declare const plugin: () => Promise<{}>;
1
+ export declare function runSkillScript(script: string, payload: string): Promise<string>;
2
+ declare const plugin: () => Promise<{
3
+ tool: {
4
+ harness_alchemist_run: {
5
+ description: string;
6
+ args: {
7
+ input: import("zod").ZodString;
8
+ runtime: import("zod").ZodOptional<import("zod").ZodEnum<{
9
+ node: "node";
10
+ python: "python";
11
+ }>>;
12
+ };
13
+ execute(args: {
14
+ input: string;
15
+ runtime?: "node" | "python" | undefined;
16
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
17
+ };
18
+ };
19
+ }>;
2
20
  export default plugin;
3
21
  //# sourceMappingURL=opencode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM,mBAEO,CAAA;AAEnB,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAaA,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B/E;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;EAuBO,CAAA;AAEnB,eAAe,MAAM,CAAA"}
package/dist/opencode.js CHANGED
@@ -1,5 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { tool } from "@opencode-ai/plugin";
5
+ const scriptsDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "skills/harness-alchemist/scripts");
6
+ export function runSkillScript(script, payload) {
7
+ return new Promise((resolveResult, rejectResult) => {
8
+ const runtime = script.endsWith(".py") ? "python3" : process.execPath;
9
+ const child = spawn(runtime, [join(scriptsDirectory, script)], {
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ });
12
+ let stdout = "";
13
+ let stderr = "";
14
+ child.stdout.on("data", (chunk) => {
15
+ stdout += chunk;
16
+ });
17
+ child.stderr.on("data", (chunk) => {
18
+ stderr += chunk;
19
+ });
20
+ child.on("error", (error) => {
21
+ rejectResult(new Error(`Could not run ${script} via ${runtime}: ${error.message}`));
22
+ });
23
+ child.on("close", (code) => {
24
+ if (code !== 0) {
25
+ rejectResult(new Error(stderr.trim() || `${script} exited with code ${code}`));
26
+ }
27
+ else {
28
+ resolveResult(stdout);
29
+ }
30
+ });
31
+ child.stdin.end(payload);
32
+ });
33
+ }
1
34
  const plugin = (async () => {
2
- return {};
35
+ return {
36
+ tool: {
37
+ harness_alchemist_run: tool({
38
+ description: "Run the Harness Alchemist shared-skill workflow. Delegates to skills/harness-alchemist/scripts.",
39
+ args: {
40
+ input: tool.schema
41
+ .string()
42
+ .describe("JSON object passed on stdin to the skill entrypoint"),
43
+ runtime: tool.schema
44
+ .enum(["node", "python"])
45
+ .optional()
46
+ .describe("Entrypoint runtime; defaults to node"),
47
+ },
48
+ execute: async (args) => {
49
+ const script = args.runtime === "python" ? "main.py" : "main.mjs";
50
+ const output = await runSkillScript(script, args.input);
51
+ return JSON.stringify(JSON.parse(output));
52
+ },
53
+ }),
54
+ },
55
+ };
3
56
  });
4
57
  export default plugin;
5
58
  //# sourceMappingURL=opencode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;IACzB,OAAO,EAAE,CAAA;AACX,CAAC,CAAkB,CAAA;AAEnB,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"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"}
package/lib/validate.mjs CHANGED
@@ -31,6 +31,11 @@ Validates a universal Claude, Codex, OpenCode, Antigravity, and DeepSeek
31
31
  plugin scaffold. By default the project is resolved from the current working
32
32
  directory or from the script's containing generated project.
33
33
 
34
+ Product skills are checked against the Agent Skills specification and their
35
+ scripts against the shared tool contract. When the optional 'pyodide'
36
+ devDependency is installed, Python entrypoints are also syntax-checked and
37
+ smoke-executed in a WebAssembly CPython sandbox.
38
+
34
39
  Options:
35
40
  --external Run installed platform validators, currently Claude Code.
36
41
  --json Print a machine-readable result.
@@ -230,6 +235,110 @@ function exportTarget(exports, key, field = "import") {
230
235
  return undefined
231
236
  }
232
237
 
238
+ const PYTHON_SMOKE_PROGRAM = `
239
+ import contextlib
240
+ import io
241
+ import json
242
+ import sys
243
+
244
+ sys.stdin = io.StringIO(__ha_payload__)
245
+ _buffer = io.StringIO()
246
+ try:
247
+ with contextlib.redirect_stdout(_buffer):
248
+ exec(compile(__ha_source__, "<skill>", "exec"), {"__name__": "__main__"})
249
+ except SystemExit as exit_code:
250
+ if exit_code.code not in (0, None):
251
+ raise RuntimeError(f"script exited with code {exit_code.code}")
252
+ _buffer.getvalue()
253
+ `
254
+
255
+ async function smokePythonScripts(scripts, errors, warnings) {
256
+ let loadPyodide
257
+ try {
258
+ ;({ loadPyodide } = await import("pyodide"))
259
+ } catch {
260
+ warnings.push("pyodide is not installed; skipped WebAssembly Python validation")
261
+ return
262
+ }
263
+
264
+ let instance
265
+ try {
266
+ instance = await loadPyodide()
267
+ } catch (error) {
268
+ warnings.push(`Pyodide failed to initialize (${error.message}); skipped WebAssembly Python validation`)
269
+ return
270
+ }
271
+
272
+ for (const script of scripts) {
273
+ const source = await readFile(script, "utf8")
274
+ instance.globals.set("__ha_source__", source)
275
+ instance.globals.set("__ha_payload__", "{}")
276
+ let output
277
+ try {
278
+ output = instance.runPython(PYTHON_SMOKE_PROGRAM)
279
+ } catch (error) {
280
+ errors.push(`${script}: WebAssembly Python check failed (${String(error).split("\n").at(-2) ?? String(error)})`)
281
+ continue
282
+ }
283
+ try {
284
+ const parsed = JSON.parse(output)
285
+ if (parsed?.ok !== true || typeof parsed.plugin !== "string") {
286
+ errors.push(`${script}: unexpected tool-contract result ${output.trim()}`)
287
+ }
288
+ } catch {
289
+ errors.push(`${script}: did not print a JSON object per the tool contract`)
290
+ }
291
+ }
292
+
293
+ instance.globals.delete("__ha_source__")
294
+ instance.globals.delete("__ha_payload__")
295
+ }
296
+
297
+ async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts) {
298
+ if ((frontmatter.description?.length ?? 0) > 1024) {
299
+ errors.push(`${skillFile}: description must be at most 1024 characters`)
300
+ }
301
+ if ((frontmatter.compatibility?.length ?? 0) > 500) {
302
+ errors.push(`${skillFile}: compatibility must be at most 500 characters`)
303
+ }
304
+
305
+ const skillDirectory = dirname(skillFile)
306
+ const references = new Set(
307
+ [...content.matchAll(/(?:scripts|references|assets)\/[A-Za-z0-9][A-Za-z0-9._/-]*/g)].map(
308
+ (match) => match[0].replace(/[.,;:)\]]+$/, ""),
309
+ ),
310
+ )
311
+ for (const reference of references) {
312
+ if (!existsSync(join(skillDirectory, ...reference.split("/")))) {
313
+ errors.push(`${skillFile}: referenced path '${reference}' does not exist`)
314
+ }
315
+ }
316
+
317
+ const scriptsDirectory = join(skillDirectory, "scripts")
318
+ if (!existsSync(scriptsDirectory)) return
319
+
320
+ const entries = await readdir(scriptsDirectory)
321
+ const pythonBases = new Set(
322
+ entries.filter((entry) => entry.endsWith(".py")).map((entry) => entry.slice(0, -3)),
323
+ )
324
+ const mjsBases = new Set(
325
+ entries.filter((entry) => entry.endsWith(".mjs")).map((entry) => entry.slice(0, -4)),
326
+ )
327
+ for (const base of pythonBases) {
328
+ if (!mjsBases.has(base)) {
329
+ errors.push(`${skillFile}: scripts/${base}.py is missing its scripts/${base}.mjs twin`)
330
+ }
331
+ }
332
+ for (const base of mjsBases) {
333
+ if (!pythonBases.has(base)) {
334
+ errors.push(`${skillFile}: scripts/${base}.mjs is missing its scripts/${base}.py twin`)
335
+ }
336
+ }
337
+ for (const entry of entries.filter((entry) => entry.endsWith(".py"))) {
338
+ pythonScripts.add(join(scriptsDirectory, entry))
339
+ }
340
+ }
341
+
233
342
  export async function validateProject(projectRoot, options = {}) {
234
343
  const root = resolve(projectRoot)
235
344
  const errors = []
@@ -350,14 +459,15 @@ export async function validateProject(projectRoot, options = {}) {
350
459
  errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
351
460
  }
352
461
 
353
- const skillFiles = [
354
- ...(await collectSkillFiles(root, "skills")),
355
- ...(await collectSkillFiles(root, ".agents/skills")),
356
- ]
462
+ const maintenanceSkillFiles = await collectSkillFiles(root, ".agents/skills")
463
+ const productSkillFiles = await collectSkillFiles(root, "skills")
464
+ const skillFiles = [...productSkillFiles, ...maintenanceSkillFiles]
357
465
  if (skillFiles.length < 2) errors.push("Expected a shared skill and a project development skill")
358
466
 
467
+ const pythonScripts = new Set()
359
468
  for (const skillFile of skillFiles) {
360
- const frontmatter = parseFrontmatter(await readFile(skillFile, "utf8"))
469
+ const content = await readFile(skillFile, "utf8")
470
+ const frontmatter = parseFrontmatter(content)
361
471
  const directoryName = basename(dirname(skillFile))
362
472
  if (!frontmatter) errors.push(`${skillFile}: missing YAML frontmatter`)
363
473
  else {
@@ -368,9 +478,16 @@ export async function validateProject(projectRoot, options = {}) {
368
478
  if (!validName.test(frontmatter.name ?? "") || (frontmatter.name?.length ?? 0) > 64) {
369
479
  errors.push(`${skillFile}: invalid Agent Skill name`)
370
480
  }
481
+ if (productSkillFiles.includes(skillFile)) {
482
+ await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts)
483
+ }
371
484
  }
372
485
  }
373
486
 
487
+ if (pythonScripts.size > 0) {
488
+ await smokePythonScripts([...pythonScripts], errors, warnings)
489
+ }
490
+
374
491
  for (const path of await scanForTokens(root)) {
375
492
  errors.push(`${path}: unresolved scaffold token`)
376
493
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "harness-alchemist",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": {
7
- "harness-alchemist": "./bin/harness-alchemist.mjs"
7
+ "harness-alchemist": "bin/harness-alchemist.mjs"
8
8
  },
9
9
  "main": "./dist/opencode.js",
10
10
  "types": "./dist/opencode.d.ts",
@@ -70,13 +70,15 @@
70
70
  "devDependencies": {
71
71
  "@deepseek-ai/cordis": "^4.0.1",
72
72
  "@opencode-ai/plugin": "^1.18.21",
73
+ "@types/node": "^26.2.0",
74
+ "pyodide": "^314.0.5",
73
75
  "typescript": "^5.9.3"
74
76
  },
75
77
  "author": "Haochuan Zhang",
76
78
  "license": "MIT",
77
79
  "repository": {
78
80
  "type": "git",
79
- "url": "https://github.com/lunarmoon26/harness-alchemist"
81
+ "url": "git+https://github.com/lunarmoon26/harness-alchemist.git"
80
82
  },
81
83
  "dsh": {
82
84
  "bundle": {
@@ -56,3 +56,12 @@ npm pack --dry-run
56
56
  ```
57
57
 
58
58
  Static validation is structural evidence, not proof that remote services or host-specific lifecycle events work.
59
+
60
+ ## Skill Runtime
61
+
62
+ `scripts/main.mjs` (Node/Bun) and `scripts/main.py` (CPython 3.10+) are
63
+ behavioral twins demonstrating this repository's own tool contract: one JSON
64
+ object on stdin, one JSON result plus newline on stdout, non-zero exit with a
65
+ stderr diagnostic on failure. The `src/` adapters delegate to them; keep any
66
+ contract change mirrored in both twins and in
67
+ [references/compatibility.md](references/compatibility.md).
@@ -12,6 +12,9 @@ project/
12
12
  ├── .claude-plugin/plugin.json
13
13
  ├── .codex-plugin/plugin.json
14
14
  ├── skills/<name>/SKILL.md
15
+ ├── skills/<name>/scripts/main.mjs
16
+ ├── skills/<name>/scripts/main.py
17
+ ├── skills/<name>/references/tool-contract.md
15
18
  ├── src/opencode.ts
16
19
  ├── src/deepseek.ts
17
20
  ├── cordis.patch.yml
@@ -19,6 +22,12 @@ project/
19
22
  └── package.json
20
23
  ```
21
24
 
25
+ Product skills own their runtime: `skills/<name>/scripts/` holds behavioral
26
+ `.mjs`/`.py` twins (one JSON object in on stdin, one JSON result out on
27
+ stdout), and `src/opencode.ts` and `src/deepseek.ts` stay thin adapters that
28
+ spawn those scripts. See the generated project's tool contract reference for
29
+ details.
30
+
22
31
  ## Capability Matrix
23
32
 
24
33
  | Artifact | Claude | Codex | OpenCode | Antigravity | DeepSeek |
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // Harness Alchemist shared-skill entrypoint (Node/Bun twin of main.py).
3
+ // Reads one JSON payload from stdin and writes one JSON result to stdout.
4
+ // See references/compatibility.md for the full contract.
5
+
6
+ const chunks = []
7
+ for await (const chunk of process.stdin) chunks.push(chunk)
8
+
9
+ let request
10
+ try {
11
+ request = JSON.parse(Buffer.concat(chunks).toString("utf8"))
12
+ } catch (error) {
13
+ console.error(`Invalid JSON payload: ${error.message}`)
14
+ process.exit(1)
15
+ }
16
+
17
+ if (typeof request !== "object" || request === null || Array.isArray(request)) {
18
+ console.error("Payload must be a JSON object")
19
+ process.exit(1)
20
+ }
21
+
22
+ process.stdout.write(`${JSON.stringify({ ok: true, plugin: "harness-alchemist", echo: request })}\n`)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ """Harness Alchemist shared-skill entrypoint (Python twin of main.mjs).
3
+
4
+ Reads one JSON payload from stdin and writes one JSON result to stdout.
5
+ See references/compatibility.md for the full contract.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+
11
+
12
+ def main() -> int:
13
+ try:
14
+ request = json.load(sys.stdin)
15
+ except json.JSONDecodeError as error:
16
+ print(f"Invalid JSON payload: {error}", file=sys.stderr)
17
+ return 1
18
+
19
+ if not isinstance(request, dict):
20
+ print("Payload must be a JSON object", file=sys.stderr)
21
+ return 1
22
+
23
+ print(json.dumps({"ok": True, "plugin": "harness-alchemist", "echo": request}))
24
+ return 0
25
+
26
+
27
+ if __name__ == "__main__":
28
+ sys.exit(main())
@@ -13,6 +13,8 @@ describe development of the Harness Alchemist CLI or its canonical templates.
13
13
 
14
14
  - `skills/{{NAME}}/SKILL.md` is the plugin's end-user workflow. Replace the
15
15
  starter procedure with the capability's domain-specific instructions.
16
+ - `skills/{{NAME}}/scripts/` owns the skill runtime as `.mjs`/`.py` twins;
17
+ see `skills/{{NAME}}/references/tool-contract.md` before changing them.
16
18
  - `.agents/skills/develop-{{NAME}}/` is this repository's maintenance guidance
17
19
  and validation tooling.
18
20
  - `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, root `plugin.json`,
@@ -24,8 +26,9 @@ describe development of the Harness Alchemist CLI or its canonical templates.
24
26
  ## Maintenance
25
27
 
26
28
  1. Read `references/compatibility.md` before changing manifests or entrypoints.
27
- 2. Put portable end-user workflows in `skills/` and host-specific runtime code
28
- only in its corresponding entrypoint.
29
+ 2. Put portable end-user workflows and skill-owned scripts in `skills/` and
30
+ host-specific runtime code, kept to thin delegation adapters, in its
31
+ corresponding entrypoint.
29
32
  3. Run `npm run verify` before reporting completion. Before publishing, inspect
30
33
  `npm pack --dry-run` and run `claude plugin validate . --strict` when Claude
31
34
  Code is installed.
@@ -16,6 +16,35 @@
16
16
  | Antigravity | `plugin.json` | Skills use nested `<name>/SKILL.md`. |
17
17
  | DeepSeek | `src/deepseek.ts`, `cordis.patch.yml` | Function plugin uses named exports and no default export. |
18
18
 
19
+ ## Skill Script Contract
20
+
21
+ - `skills/{{NAME}}/scripts/` owns the skill runtime. Entrypoints come in
22
+ behavioral `.mjs` and `.py` twins with identical base names.
23
+ - Contract: one JSON object on stdin, one JSON result plus newline on stdout,
24
+ non-zero exit with a stderr diagnostic on failure. See
25
+ `skills/{{NAME}}/references/tool-contract.md`.
26
+ - `src/opencode.ts` and `src/deepseek.ts` are thin adapters that spawn these
27
+ scripts. Workflow logic never lives in the adapters.
28
+ - Maintenance scripts under `.agents/skills/develop-{{NAME}}/scripts/` are
29
+ maintainer tooling and are exempt from the twin rule.
30
+
31
+ ## Validation Tiers
32
+
33
+ - Tier B (always): Agent Skills frontmatter compliance, SKILL.md relative-path
34
+ resolution, and twin parity for product skills.
35
+ - Tier A (when the optional `pyodide` package is installed locally): Python
36
+ entrypoints are additionally compiled and smoke-executed in a WebAssembly
37
+ CPython sandbox without requiring native Python. This repository does not
38
+ declare pyodide as a dependency; install it ad hoc for deeper checks.
39
+
40
+ ## DeepSeek Harness Volatility
41
+
42
+ DeepSeek Harness (`dsh`) is a developer preview built on the Cordis kernel.
43
+ The generated contract pins only the verified surface: function-form named-
44
+ export plugins, `cordis.patch.yml` insert entries resolving npm module names,
45
+ and the `dsh.bundle.patch` package field. Re-verify against official dsh docs
46
+ before adopting deeper Cordis services.
47
+
19
48
  ## Metadata
20
49
 
21
50
  `package.json` owns version, description, author, repository, license, and npm package name. `scripts/sync-metadata.mjs` propagates these values without replacing harness-specific fields.
@@ -54,6 +54,7 @@
54
54
  "devDependencies": {
55
55
  "@deepseek-ai/cordis": "^4.0.1",
56
56
  "@opencode-ai/plugin": "^1.18.21",
57
+ "@types/node": "^26.2.0",
57
58
  "typescript": "^5.9.3"
58
59
  },
59
60
  "author": {{AUTHOR_JSON}},
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: {{NAME}}
3
3
  description: {{SHARED_SKILL_DESCRIPTION_JSON}}
4
+ compatibility: Requires Node.js 22+ or Bun 1.2+ for scripts/main.mjs; optional Python 3.10+ for scripts/main.py.
5
+ metadata:
6
+ plugin: {{NAME}}
4
7
  ---
5
8
 
6
9
  # {{DISPLAY_NAME}}
@@ -10,9 +13,20 @@ Apply the {{DISPLAY_NAME}} workflow to the user's request.
10
13
  ## Workflow
11
14
 
12
15
  1. Confirm the requested outcome and inspect the relevant project context.
13
- 2. Perform the smallest complete change that satisfies the request.
14
- 3. Use harness-provided tools only when they are needed for the workflow.
15
- 4. Verify the result with the project's available checks.
16
- 5. Report the outcome and any unresolved external dependency.
16
+ 2. Run the skill entrypoint to perform the smallest complete change:
17
+
18
+ ```
19
+ echo '{"request": "..."}' | scripts/main.mjs
20
+ ```
21
+
22
+ When Python 3.10+ is available, `scripts/main.py` behaves identically.
23
+ 3. Interpret the JSON result; both entrypoints return `{"ok": true, ...}` on
24
+ success and exit non-zero with a stderr diagnostic on failure.
25
+ 4. Use harness-provided tools only when they are needed for the workflow.
26
+ 5. Verify the result with the project's available checks.
27
+ 6. Report the outcome and any unresolved external dependency.
28
+
29
+ See [the tool contract](references/tool-contract.md) before changing or adding
30
+ entrypoints, and keep the `.mjs` and `.py` twins behaviorally identical.
17
31
 
18
32
  Replace this starter workflow with the plugin's domain-specific procedure as the capability develops.
@@ -0,0 +1,35 @@
1
+ # {{DISPLAY_NAME}} Tool Contract
2
+
3
+ The shared skill owns its runtime. Harness entrypoints are thin adapters that
4
+ delegate to the scripts in this directory; they must not contain workflow logic.
5
+
6
+ ## Entrypoints
7
+
8
+ | File | Runtime | Requirement |
9
+ | --- | --- | --- |
10
+ | `scripts/main.mjs` | Node.js 22+ or Bun 1.2+ | Zero npm dependencies |
11
+ | `scripts/main.py` | CPython 3.10+ | Standard library only |
12
+
13
+ `main.mjs` and `main.py` are behavioral twins. Any change to one must be
14
+ mirrored in the other, and both must keep the same name so validators can pair
15
+ them.
16
+
17
+ ## I/O Contract
18
+
19
+ 1. Read exactly one JSON object from stdin.
20
+ 2. Write exactly one JSON result followed by a newline to stdout.
21
+ 3. On success: `{"ok": true, "plugin": "<plugin-name>", ...}` with exit code 0.
22
+ 4. On failure: write a diagnostic to stderr, write nothing to stdout, and exit
23
+ with a non-zero code.
24
+
25
+ ## Delegation Rules
26
+
27
+ - Claude Code, Codex, and Antigravity agents invoke these scripts directly via
28
+ a shell, guided by `SKILL.md`.
29
+ - The OpenCode adapter (`src/opencode.ts`) registers a tool that spawns the
30
+ scripts and returns their JSON output.
31
+ - The DeepSeek/Cordis adapter (`src/deepseek.ts`) provides a service that
32
+ spawns the scripts; `cordis.patch.yml` loads it from the published package.
33
+
34
+ Prefer `.mjs` when only a JavaScript runtime is guaranteed; use `.py` when
35
+ Python is available or the workflow needs Python-only libraries.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // {{DISPLAY_NAME}} shared-skill entrypoint (Node/Bun twin of main.py).
3
+ // Reads one JSON payload from stdin and writes one JSON result to stdout.
4
+ // See references/tool-contract.md for the full contract.
5
+
6
+ const chunks = []
7
+ for await (const chunk of process.stdin) chunks.push(chunk)
8
+
9
+ let request
10
+ try {
11
+ request = JSON.parse(Buffer.concat(chunks).toString("utf8"))
12
+ } catch (error) {
13
+ console.error(`Invalid JSON payload: ${error.message}`)
14
+ process.exit(1)
15
+ }
16
+
17
+ if (typeof request !== "object" || request === null || Array.isArray(request)) {
18
+ console.error("Payload must be a JSON object")
19
+ process.exit(1)
20
+ }
21
+
22
+ process.stdout.write(`${JSON.stringify({ ok: true, plugin: "{{NAME}}", echo: request })}\n`)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ """{{DISPLAY_NAME}} shared-skill entrypoint (Python twin of main.mjs).
3
+
4
+ Reads one JSON payload from stdin and writes one JSON result to stdout.
5
+ See references/tool-contract.md for the full contract.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+
11
+
12
+ def main() -> int:
13
+ try:
14
+ request = json.load(sys.stdin)
15
+ except json.JSONDecodeError as error:
16
+ print(f"Invalid JSON payload: {error}", file=sys.stderr)
17
+ return 1
18
+
19
+ if not isinstance(request, dict):
20
+ print("Payload must be a JSON object", file=sys.stderr)
21
+ return 1
22
+
23
+ print(json.dumps({"ok": True, "plugin": "{{NAME}}", "echo": request}))
24
+ return 0
25
+
26
+
27
+ if __name__ == "__main__":
28
+ sys.exit(main())
@@ -1,7 +1,36 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import { dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
1
5
  import type { Context } from "@deepseek-ai/cordis"
2
6
 
3
7
  export const name = {{NAME_JSON}}
4
8
 
5
- export function apply(_context: Context): void {
6
- // Register Cordis services, tools, or lifecycle effects here.
9
+ const scriptsDirectory = join(
10
+ dirname(fileURLToPath(import.meta.url)),
11
+ "..",
12
+ "skills/{{NAME}}/scripts",
13
+ )
14
+
15
+ export function runSkillScript(script: string, payload: string): unknown {
16
+ const runtime = script.endsWith(".py") ? "python3" : process.execPath
17
+ const result = spawnSync(runtime, [join(scriptsDirectory, script)], {
18
+ input: payload,
19
+ encoding: "utf8",
20
+ })
21
+ if (result.error) {
22
+ throw new Error(`Could not run ${script} via ${runtime}: ${result.error.message}`)
23
+ }
24
+ if (result.status !== 0) {
25
+ const diagnostic = result.stderr.trim() || `${script} exited with code ${result.status}`
26
+ throw new Error(diagnostic)
27
+ }
28
+ return JSON.parse(result.stdout)
29
+ }
30
+
31
+ export function apply(context: Context): void {
32
+ context.provide(name, {
33
+ run: (payload: string, runtime: "node" | "python" = "node") =>
34
+ runSkillScript(runtime === "python" ? "main.py" : "main.mjs", payload),
35
+ })
7
36
  }
@@ -1,7 +1,67 @@
1
+ import { spawn } from "node:child_process"
2
+ import { dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ import { tool } from "@opencode-ai/plugin"
1
6
  import type { Plugin } from "@opencode-ai/plugin"
2
7
 
8
+ const scriptsDirectory = join(
9
+ dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ "skills/{{NAME}}/scripts",
12
+ )
13
+
14
+ export function runSkillScript(script: string, payload: string): Promise<string> {
15
+ return new Promise((resolveResult, rejectResult) => {
16
+ const runtime = script.endsWith(".py") ? "python3" : process.execPath
17
+ const child = spawn(runtime, [join(scriptsDirectory, script)], {
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ })
20
+ let stdout = ""
21
+ let stderr = ""
22
+ child.stdout.on("data", (chunk) => {
23
+ stdout += chunk
24
+ })
25
+ child.stderr.on("data", (chunk) => {
26
+ stderr += chunk
27
+ })
28
+ child.on("error", (error) => {
29
+ rejectResult(new Error(`Could not run ${script} via ${runtime}: ${error.message}`))
30
+ })
31
+ child.on("close", (code) => {
32
+ if (code !== 0) {
33
+ rejectResult(new Error(stderr.trim() || `${script} exited with code ${code}`))
34
+ } else {
35
+ resolveResult(stdout)
36
+ }
37
+ })
38
+ child.stdin.end(payload)
39
+ })
40
+ }
41
+
3
42
  const plugin = (async () => {
4
- return {}
43
+ return {
44
+ tool: {
45
+ "{{NAME}}_run": tool({
46
+ description:
47
+ "Run the {{DISPLAY_NAME}} shared-skill workflow. Delegates to skills/{{NAME}}/scripts.",
48
+ args: {
49
+ input: tool.schema
50
+ .string()
51
+ .describe("JSON object passed on stdin to the skill entrypoint"),
52
+ runtime: tool.schema
53
+ .enum(["node", "python"])
54
+ .optional()
55
+ .describe("Entrypoint runtime; defaults to node"),
56
+ },
57
+ execute: async (args) => {
58
+ const script = args.runtime === "python" ? "main.py" : "main.mjs"
59
+ const output = await runSkillScript(script, args.input)
60
+ return JSON.stringify(JSON.parse(output))
61
+ },
62
+ }),
63
+ },
64
+ }
5
65
  }) satisfies Plugin
6
66
 
7
67
  export default plugin
@@ -1,11 +1,22 @@
1
1
  import assert from "node:assert/strict"
2
+ import { spawnSync } from "node:child_process"
2
3
  import test from "node:test"
3
4
 
4
5
  import opencodePlugin from "../dist/opencode.js"
5
6
  import * as deepseekPlugin from "../dist/deepseek.js"
6
7
 
7
- test("OpenCode entrypoint returns an inert hooks object", async () => {
8
- assert.deepEqual(await opencodePlugin({}), {})
8
+ const pythonAvailable = spawnSync("python3", ["--version"], { encoding: "utf8" }).status === 0
9
+
10
+ test("OpenCode tool delegates to the shared skill script", async () => {
11
+ const hooks = await opencodePlugin({})
12
+ const entry = hooks.tool?.["{{NAME}}_run"]
13
+ assert.equal(typeof entry?.execute, "function")
14
+ const result = await entry.execute({ input: '{"hello":"world"}' })
15
+ assert.deepEqual(JSON.parse(result), {
16
+ ok: true,
17
+ plugin: "{{NAME}}",
18
+ echo: { hello: "world" },
19
+ })
9
20
  })
10
21
 
11
22
  test("DeepSeek entrypoint exposes a Cordis namespace plugin", () => {
@@ -13,3 +24,25 @@ test("DeepSeek entrypoint exposes a Cordis namespace plugin", () => {
13
24
  assert.equal(typeof deepseekPlugin.apply, "function")
14
25
  assert.equal("default" in deepseekPlugin, false)
15
26
  })
27
+
28
+ test("DeepSeek adapter delegates to the shared skill script", () => {
29
+ const result = deepseekPlugin.runSkillScript("main.mjs", '{"hello":"world"}')
30
+ assert.deepEqual(result, {
31
+ ok: true,
32
+ plugin: "{{NAME}}",
33
+ echo: { hello: "world" },
34
+ })
35
+ })
36
+
37
+ test(
38
+ "Python twin honors the same tool contract",
39
+ { skip: !pythonAvailable },
40
+ () => {
41
+ const result = deepseekPlugin.runSkillScript("main.py", '{"hello":"world"}')
42
+ assert.deepEqual(result, {
43
+ ok: true,
44
+ plugin: "{{NAME}}",
45
+ echo: { hello: "world" },
46
+ })
47
+ },
48
+ )