@oharness/engine 0.2.0 → 0.3.0
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/dist/agent/skills.d.ts +10 -2
- package/dist/agent/skills.js +38 -12
- package/dist/agent/skills.js.map +1 -1
- package/dist/auth/manager.d.ts +13 -0
- package/dist/auth/manager.js +22 -0
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/providers/openai-codex.d.ts +46 -0
- package/dist/auth/providers/openai-codex.js +201 -0
- package/dist/auth/providers/openai-codex.js.map +1 -0
- package/dist/harness.js +14 -3
- package/dist/harness.js.map +1 -1
- package/dist/mcp/builtin.d.ts +13 -0
- package/dist/mcp/builtin.js +93 -0
- package/dist/mcp/builtin.js.map +1 -0
- package/dist/mcp/client.d.ts +5 -1
- package/dist/mcp/client.js +11 -4
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +30 -2
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/secrets.d.ts +15 -0
- package/dist/mcp/secrets.js +61 -0
- package/dist/mcp/secrets.js.map +1 -1
- package/dist/providers/builtin.js +17 -4
- package/dist/providers/builtin.js.map +1 -1
- package/dist/providers/catalog.d.ts +27 -3
- package/dist/providers/catalog.js +10 -0
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/catalog.json +41 -5
- package/dist/providers/piai.d.ts +1 -1
- package/dist/providers/piai.js +10 -1
- package/dist/providers/piai.js.map +1 -1
- package/dist/providers/registry.js +4 -2
- package/dist/providers/registry.js.map +1 -1
- package/dist/runtime-plugins/builtin/media.d.ts +33 -0
- package/dist/runtime-plugins/builtin/media.js +315 -0
- package/dist/runtime-plugins/builtin/media.js.map +1 -0
- package/dist/runtime-plugins/host.d.ts +2 -0
- package/dist/runtime-plugins/host.js +3 -2
- package/dist/runtime-plugins/host.js.map +1 -1
- package/dist/runtime-plugins/index.d.ts +1 -0
- package/dist/runtime-plugins/index.js.map +1 -1
- package/dist/runtime-plugins/types.d.ts +5 -0
- package/package.json +23 -2
- package/skills/media-generation/SKILL.md +96 -0
- package/skills/plugin-creator/SKILL.md +120 -0
- package/skills/plugin-creator/scripts/create-plugin.mjs +76 -0
- package/skills/plugin-creator/scripts/validate-plugin.mjs +46 -0
- package/skills/plugin-creator/scripts/validate-runtime-plugin.mjs +28 -0
- package/skills/skill-authoring/SKILL.md +4 -1
- package/skills/skill-creator/SKILL.md +98 -0
- package/skills/skill-creator/scripts/create-skill.mjs +75 -0
- package/skills/skill-creator/scripts/validate-runtime-skill.mjs +36 -0
- package/skills/skill-creator/scripts/validate-skill.mjs +40 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plugin-creator
|
|
3
|
+
description: Creating or updating native OHarness plugin bundles with skills, subagents, hooks, MCP servers, project or user installation, and manifest validation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create an OHarness plugin
|
|
7
|
+
|
|
8
|
+
Choose between two native OHarness formats. A declarative bundle packages
|
|
9
|
+
Skills, Subagents, Hooks, and MCP servers without importing JavaScript. A
|
|
10
|
+
Runtime Plugin is trusted executable code that can register tools, Runtime
|
|
11
|
+
Skills, models, authentication, hooks, audit sinks, prompts, pages, and
|
|
12
|
+
background lifecycle work.
|
|
13
|
+
|
|
14
|
+
## Choose the installation scope
|
|
15
|
+
|
|
16
|
+
- Default to `<project>/.oharness/plugins/<name>/` for a repository-specific
|
|
17
|
+
plugin.
|
|
18
|
+
- For a local/global installation available to every project, use
|
|
19
|
+
`$OHARNESS_HOME/plugins/<name>/` when set,
|
|
20
|
+
`$XDG_CONFIG_HOME/oharness/plugins/<name>/` on configured Linux systems, or
|
|
21
|
+
otherwise `~/.oharness/plugins/<name>/`.
|
|
22
|
+
- Respect an explicit directory. Inspect an existing plugin before editing it
|
|
23
|
+
and never overwrite unrelated files.
|
|
24
|
+
|
|
25
|
+
Both the Web and CLI frontends use the same plugin discovery in `Harness`, so a
|
|
26
|
+
user-installed plugin is available to either frontend on its next Harness
|
|
27
|
+
startup. Project and configured plugin directories override earlier plugins
|
|
28
|
+
with the same name.
|
|
29
|
+
|
|
30
|
+
Runtime Plugins are different: project configuration cannot activate executable
|
|
31
|
+
modules. They must be supplied by the embedding application or enabled in the
|
|
32
|
+
user `plugins.json` runtime-plugin list. Changes take effect on the next Harness
|
|
33
|
+
startup.
|
|
34
|
+
|
|
35
|
+
## Native bundle format
|
|
36
|
+
|
|
37
|
+
Create only the parts the requested plugin needs:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
my-plugin/
|
|
41
|
+
oharness.plugin.json
|
|
42
|
+
skills/example/SKILL.md
|
|
43
|
+
agents/reviewer.md
|
|
44
|
+
scripts/audit.sh
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The manifest needs only `name`. Supported optional fields are `version`,
|
|
48
|
+
`description`, `mcpServers`, `hooks`, and inline `subagents`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"name": "my-plugin",
|
|
53
|
+
"version": "1.0.0",
|
|
54
|
+
"description": "Release and review workflows",
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"github": { "type": "http", "url": "https://example.com/mcp" }
|
|
57
|
+
},
|
|
58
|
+
"hooks": {
|
|
59
|
+
"PreToolUse": [{ "matcher": "mcp__github__*", "command": "./scripts/audit.sh" }]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Skills under `skills/` and Markdown agents under `agents/` are discovered
|
|
65
|
+
automatically and do not need manifest entries. A plugin can alternatively be
|
|
66
|
+
added through `plugins.dirs` in configuration. OHarness can consume compatible
|
|
67
|
+
Claude Code plugin bundles, but create the native `oharness.plugin.json` format
|
|
68
|
+
unless the user explicitly requests cross-product compatibility.
|
|
69
|
+
|
|
70
|
+
For a deterministic skeleton, run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
node <skill-base>/scripts/create-plugin.mjs <name> [--description "<text>"] [--version 1.0.0] [--with skills,agents,scripts] [--project <directory> | --user]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Resolve `<skill-base>` from the base directory shown when this skill loads; do
|
|
77
|
+
not assume the current working directory is the built-in skill directory.
|
|
78
|
+
The command refuses to replace an existing manifest unless `--force` is given.
|
|
79
|
+
Use that flag only after inspecting the target and intentionally replacing it.
|
|
80
|
+
Populate requested hooks and MCP declarations after scaffolding; do not invent
|
|
81
|
+
commands, endpoints, credentials, or permissions.
|
|
82
|
+
|
|
83
|
+
## Runtime Plugin mode
|
|
84
|
+
|
|
85
|
+
When the request needs executable in-process behavior, add `--runtime`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node <skill-base>/scripts/create-plugin.mjs <name> --runtime [--description "<text>"] [--version 1.0.0] [--project <directory> | --user]
|
|
89
|
+
node <skill-base>/scripts/validate-runtime-plugin.mjs <path-to-runtime-plugin>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Implement only the requested capabilities through `setup(ctx)`. Register all
|
|
93
|
+
contributions during setup, keep mutable state per Harness instance, honor
|
|
94
|
+
`ctx.signal`, register cleanup immediately with `ctx.onDispose`, and make
|
|
95
|
+
`ctx.onStart` return after starting background work rather than awaiting an
|
|
96
|
+
infinite loop. Runtime Skill contributions use
|
|
97
|
+
`ctx.skills.register({ name, description, body, directory? })`; the optional
|
|
98
|
+
directory must be absolute.
|
|
99
|
+
|
|
100
|
+
When installation is requested, add an entry with the generated id and absolute
|
|
101
|
+
module path to the user `plugins.json` `runtimePlugins` list. Never enable
|
|
102
|
+
executable code from project configuration, and do not change user configuration
|
|
103
|
+
unless installation or enablement was explicitly requested.
|
|
104
|
+
|
|
105
|
+
Plugin hooks and stdio MCP servers execute local commands. Treat the bundle as
|
|
106
|
+
code, keep secrets as environment placeholders, and call out those executable
|
|
107
|
+
parts when handing the plugin back.
|
|
108
|
+
|
|
109
|
+
## Validate and activate
|
|
110
|
+
|
|
111
|
+
Run:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
node <skill-base>/scripts/validate-plugin.mjs <path-to-plugin>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Verify every referenced script or skill exists and test deterministic helpers.
|
|
118
|
+
Plugin discovery happens when `Harness` starts, so restart the local CLI/Web
|
|
119
|
+
process or begin a new session after installing a new bundle. Use `/plugins` to
|
|
120
|
+
confirm the effective plugin and `/mcp` to inspect contributed MCP connections.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function fail(message) {
|
|
7
|
+
console.error(`plugin-creator: ${message}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function userRoot() {
|
|
12
|
+
if (process.env.OHARNESS_HOME) return path.resolve(process.env.OHARNESS_HOME);
|
|
13
|
+
if (process.platform === "linux" && process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, "oharness");
|
|
14
|
+
return path.join(homedir(), ".oharness");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalize(value) {
|
|
18
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63).replace(/-+$/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const rawName = args.shift();
|
|
23
|
+
if (!rawName) fail("usage: create-plugin.mjs <name> [--runtime] [--description <text>] [--version <value>] [--with a,b] [--project <dir> | --user] [--force]");
|
|
24
|
+
|
|
25
|
+
let description;
|
|
26
|
+
let version;
|
|
27
|
+
let project = process.cwd();
|
|
28
|
+
let useUser = false;
|
|
29
|
+
let projectSpecified = false;
|
|
30
|
+
let runtime = false;
|
|
31
|
+
let force = false;
|
|
32
|
+
let companions = [];
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg === "--description") description = args[++i];
|
|
36
|
+
else if (arg === "--version") version = args[++i];
|
|
37
|
+
else if (arg === "--project") { project = path.resolve(args[++i] ?? fail("--project needs a directory")); projectSpecified = true; }
|
|
38
|
+
else if (arg === "--user") useUser = true;
|
|
39
|
+
else if (arg === "--runtime") runtime = true;
|
|
40
|
+
else if (arg === "--with") companions = (args[++i] ?? "").split(",").filter(Boolean);
|
|
41
|
+
else if (arg === "--force") force = true;
|
|
42
|
+
else fail(`unknown option: ${arg}`);
|
|
43
|
+
}
|
|
44
|
+
if (useUser && projectSpecified) fail("choose either --user or --project");
|
|
45
|
+
|
|
46
|
+
const name = normalize(rawName);
|
|
47
|
+
if (!name) fail("name must contain a letter or digit");
|
|
48
|
+
const allowed = new Set(["skills", "agents", "scripts"]);
|
|
49
|
+
for (const companion of companions) if (!allowed.has(companion)) fail(`unsupported companion directory: ${companion}`);
|
|
50
|
+
|
|
51
|
+
const parent = runtime
|
|
52
|
+
? (useUser ? path.join(userRoot(), "runtime-plugins") : path.join(project, ".oharness", "runtime-plugins"))
|
|
53
|
+
: (useUser ? path.join(userRoot(), "plugins") : path.join(project, ".oharness", "plugins"));
|
|
54
|
+
const directory = path.join(parent, name);
|
|
55
|
+
const file = path.join(directory, runtime ? "index.mjs" : "oharness.plugin.json");
|
|
56
|
+
if (!force) {
|
|
57
|
+
try {
|
|
58
|
+
await readFile(file, "utf8");
|
|
59
|
+
fail(`${file} already exists (pass --force only to replace it intentionally)`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code !== "ENOENT") throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await mkdir(directory, { recursive: true });
|
|
66
|
+
for (const companion of companions) await mkdir(path.join(directory, companion), { recursive: true });
|
|
67
|
+
const manifest = {
|
|
68
|
+
name,
|
|
69
|
+
...(version?.trim() ? { version: version.trim() } : {}),
|
|
70
|
+
...(description?.trim() ? { description: description.trim() } : {}),
|
|
71
|
+
};
|
|
72
|
+
const output = runtime
|
|
73
|
+
? `export default {\n id: ${JSON.stringify(name)},\n version: ${JSON.stringify(version?.trim() || "1.0.0")},\n apiVersion: 1,\n ${description?.trim() ? `description: ${JSON.stringify(description.trim())},\n ` : ""}parseConfig(raw) {\n if (raw !== undefined) throw new Error("This plugin takes no configuration");\n return {};\n },\n setup(_ctx) {\n // Register requested runtime capabilities here before returning.\n },\n};\n`
|
|
74
|
+
: `${JSON.stringify(manifest, null, 2)}\n`;
|
|
75
|
+
await writeFile(file, output, "utf8");
|
|
76
|
+
console.log(file);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const input = process.argv[2];
|
|
6
|
+
if (!input) {
|
|
7
|
+
console.error("usage: validate-plugin.mjs <plugin-directory-or-oharness.plugin.json>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const file = path.basename(input) === "oharness.plugin.json" ? path.resolve(input) : path.resolve(input, "oharness.plugin.json");
|
|
12
|
+
const directory = path.dirname(file);
|
|
13
|
+
const errors = [];
|
|
14
|
+
let manifest;
|
|
15
|
+
try {
|
|
16
|
+
manifest = JSON.parse(await readFile(file, "utf8"));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
errors.push(`cannot parse ${file}: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) errors.push("manifest must be a JSON object");
|
|
22
|
+
const name = manifest?.name;
|
|
23
|
+
if (typeof name !== "string" || !name.trim()) errors.push("manifest requires a non-empty name");
|
|
24
|
+
if (typeof name === "string" && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) errors.push("name must be lowercase hyphen-case");
|
|
25
|
+
if (typeof name === "string" && name.length >= 64) errors.push("name must be shorter than 64 characters");
|
|
26
|
+
if (typeof name === "string" && path.basename(directory) !== name) errors.push("directory name must match manifest name");
|
|
27
|
+
if (manifest?.mcpServers !== undefined && (!manifest.mcpServers || typeof manifest.mcpServers !== "object" || Array.isArray(manifest.mcpServers))) errors.push("mcpServers must be an object");
|
|
28
|
+
if (manifest?.hooks !== undefined && (!manifest.hooks || typeof manifest.hooks !== "object" || Array.isArray(manifest.hooks))) errors.push("hooks must be an object");
|
|
29
|
+
if (manifest?.subagents !== undefined && !Array.isArray(manifest.subagents)) errors.push("subagents must be an array");
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const skills = path.join(directory, "skills");
|
|
33
|
+
for (const entry of await readdir(skills, { withFileTypes: true })) {
|
|
34
|
+
if (!entry.isDirectory()) continue;
|
|
35
|
+
try { await readFile(path.join(skills, entry.name, "SKILL.md"), "utf8"); }
|
|
36
|
+
catch { errors.push(`skills/${entry.name} is missing SKILL.md`); }
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code !== "ENOENT") errors.push(`cannot inspect skills/: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (errors.length) {
|
|
43
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(`valid OHarness plugin: ${name}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const input = process.argv[2];
|
|
7
|
+
if (!input) {
|
|
8
|
+
console.error("usage: validate-runtime-plugin.mjs <runtime-plugin-directory-or-module>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const target = path.resolve(input);
|
|
13
|
+
const file = (await stat(target)).isDirectory() ? path.join(target, "index.mjs") : target;
|
|
14
|
+
const errors = [];
|
|
15
|
+
let plugin;
|
|
16
|
+
try { plugin = (await import(`${pathToFileURL(file).href}?validate=${Date.now()}`)).default; }
|
|
17
|
+
catch (error) { errors.push(`cannot import ${file}: ${error.message}`); }
|
|
18
|
+
if (!plugin || typeof plugin !== "object") errors.push("module must default-export a plugin object");
|
|
19
|
+
if (typeof plugin?.id !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(plugin.id)) errors.push("plugin id is invalid");
|
|
20
|
+
if (typeof plugin?.version !== "string" || !plugin.version) errors.push("plugin version is required");
|
|
21
|
+
if (plugin?.apiVersion !== 1) errors.push("apiVersion must be 1");
|
|
22
|
+
if (typeof plugin?.parseConfig !== "function") errors.push("parseConfig is required");
|
|
23
|
+
if (typeof plugin?.setup !== "function") errors.push("setup is required");
|
|
24
|
+
if (errors.length) {
|
|
25
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log(`valid OHarness runtime plugin: ${plugin.id}`);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-authoring
|
|
3
|
-
description:
|
|
3
|
+
description: Explaining or reviewing the OHarness SKILL.md format, discovery order, and description matching. For creating or installing a skill, use skill-creator.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Writing a skill
|
|
7
7
|
|
|
8
|
+
This is the format reference. Use the built-in `skill-creator` for a complete
|
|
9
|
+
creation or installation workflow with scaffolding and validation.
|
|
10
|
+
|
|
8
11
|
A skill is packaged instructions loaded on demand. Its **description** sits in
|
|
9
12
|
the system prompt of every request; its **body** is only loaded when a task
|
|
10
13
|
matches. That split is the whole point, and it drives every rule below.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-creator
|
|
3
|
+
description: Creating or updating reusable OHarness skills, including project-local and user-wide installation, SKILL.md authoring, supporting files, and validation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create an OHarness skill
|
|
7
|
+
|
|
8
|
+
Create a small, discoverable skill that changes the agent's decisions for a
|
|
9
|
+
specific workflow. Do not turn generic knowledge into a permanent system-prompt
|
|
10
|
+
cost.
|
|
11
|
+
|
|
12
|
+
## Choose the installation scope
|
|
13
|
+
|
|
14
|
+
- Default to `<project>/.oharness/skills/<name>/` when the skill belongs to the
|
|
15
|
+
current repository or the user did not ask for a global installation.
|
|
16
|
+
- Use the user skills directory when the user asks to install the skill locally,
|
|
17
|
+
globally, for every project, or for their account. This is
|
|
18
|
+
`$OHARNESS_HOME/skills/<name>/` when `OHARNESS_HOME` is set,
|
|
19
|
+
`$XDG_CONFIG_HOME/oharness/skills/<name>/` on Linux when XDG is configured,
|
|
20
|
+
and otherwise `~/.oharness/skills/<name>/`.
|
|
21
|
+
- Respect an explicit destination. Do not write outside the configured
|
|
22
|
+
workspace roots without the required approval.
|
|
23
|
+
|
|
24
|
+
Choose a persisted skill unless the user explicitly asks for a Runtime Skill.
|
|
25
|
+
A Runtime Skill is trusted JavaScript registered in memory by a Runtime Plugin;
|
|
26
|
+
it is useful when the instructions or lifecycle must be supplied
|
|
27
|
+
programmatically. It is not a second executable escape hatch for ordinary
|
|
28
|
+
markdown skills.
|
|
29
|
+
|
|
30
|
+
Project skills override user and built-in skills with the same name. Inspect an
|
|
31
|
+
existing destination before changing it and preserve unrelated resources.
|
|
32
|
+
|
|
33
|
+
## Author the skill
|
|
34
|
+
|
|
35
|
+
Use a lowercase hyphenated name under 64 characters. Every skill is a directory
|
|
36
|
+
with one required `SKILL.md`:
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
---
|
|
40
|
+
name: deploy-service
|
|
41
|
+
description: Deploying this service to Railway, including its preflight checks and rollback boundary.
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Deploy the service
|
|
45
|
+
|
|
46
|
+
Run `scripts/preflight.sh` before changing the deployment.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `description` is the only part visible before selection. Name the concrete
|
|
50
|
+
request that should activate the skill; avoid descriptions such as "helper" or
|
|
51
|
+
"utilities". The body should contain only non-obvious workflow, constraints,
|
|
52
|
+
decision criteria, and references to supporting files.
|
|
53
|
+
|
|
54
|
+
Add `scripts/`, `reference/`, or `assets/` only when the workflow needs them.
|
|
55
|
+
Relative paths in the body resolve from the skill directory. Test executable
|
|
56
|
+
helpers rather than assuming they work.
|
|
57
|
+
|
|
58
|
+
For a deterministic initial file, run the bundled scaffold with the destination
|
|
59
|
+
scope and complete instructions:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node <skill-base>/scripts/create-skill.mjs <name> --description "<selection description>" --instructions "<skill instructions>" [--project <directory> | --user]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Resolve `<skill-base>` from the base directory shown when this skill loads; do
|
|
66
|
+
not assume the current working directory is the built-in skill directory.
|
|
67
|
+
Pass `--resources scripts,reference,assets` only for directories that will be
|
|
68
|
+
used. The command refuses to replace an existing skill unless `--force` is
|
|
69
|
+
given; use that flag only after inspecting the target and intentionally
|
|
70
|
+
replacing it.
|
|
71
|
+
|
|
72
|
+
For a Runtime Skill, add `--runtime`. The scaffold produces an `index.mjs`
|
|
73
|
+
whose Runtime Plugin calls `ctx.skills.register(...)`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
node <skill-base>/scripts/create-skill.mjs <name> --runtime --description "<selection description>" --instructions "<skill instructions>" [--project <directory> | --user]
|
|
77
|
+
node <skill-base>/scripts/validate-runtime-skill.mjs <path-to-runtime-skill>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Runtime Skills are trusted executable modules. They must be enabled through the
|
|
81
|
+
user `plugins.json` runtime-plugin list or supplied explicitly by an embedding
|
|
82
|
+
application; project configuration cannot activate them. When activation is
|
|
83
|
+
requested, use the generated plugin id `skill-<name>` and an absolute module
|
|
84
|
+
path. Do not edit user plugin configuration unless the user asked to install or
|
|
85
|
+
enable the Runtime Skill.
|
|
86
|
+
|
|
87
|
+
## Validate and finish
|
|
88
|
+
|
|
89
|
+
Run:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node <skill-base>/scripts/validate-skill.mjs <path-to-skill>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Also verify that the description matches realistic user wording, the body has
|
|
96
|
+
no unfinished placeholders, and referenced files exist. Writing a
|
|
97
|
+
`**/skills/**/SKILL.md` triggers OHarness skill discovery immediately, so the
|
|
98
|
+
new skill can be used in the same Web or CLI session without restarting.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function fail(message) {
|
|
7
|
+
console.error(`skill-creator: ${message}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function userRoot() {
|
|
12
|
+
if (process.env.OHARNESS_HOME) return path.resolve(process.env.OHARNESS_HOME);
|
|
13
|
+
if (process.platform === "linux" && process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, "oharness");
|
|
14
|
+
return path.join(homedir(), ".oharness");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalize(value) {
|
|
18
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63).replace(/-+$/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const rawName = args.shift();
|
|
23
|
+
if (!rawName) fail("usage: create-skill.mjs <name> --description <text> --instructions <text> [--runtime] [--project <dir> | --user] [--resources a,b] [--force]");
|
|
24
|
+
|
|
25
|
+
let description;
|
|
26
|
+
let instructions;
|
|
27
|
+
let project = process.cwd();
|
|
28
|
+
let useUser = false;
|
|
29
|
+
let projectSpecified = false;
|
|
30
|
+
let runtime = false;
|
|
31
|
+
let force = false;
|
|
32
|
+
let resources = [];
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg === "--description") description = args[++i];
|
|
36
|
+
else if (arg === "--instructions") instructions = args[++i];
|
|
37
|
+
else if (arg === "--project") { project = path.resolve(args[++i] ?? fail("--project needs a directory")); projectSpecified = true; }
|
|
38
|
+
else if (arg === "--user") useUser = true;
|
|
39
|
+
else if (arg === "--runtime") runtime = true;
|
|
40
|
+
else if (arg === "--resources") resources = (args[++i] ?? "").split(",").filter(Boolean);
|
|
41
|
+
else if (arg === "--force") force = true;
|
|
42
|
+
else fail(`unknown option: ${arg}`);
|
|
43
|
+
}
|
|
44
|
+
if (!description?.trim()) fail("--description is required");
|
|
45
|
+
if (!instructions?.trim()) fail("--instructions is required");
|
|
46
|
+
if (useUser && projectSpecified) fail("choose either --user or --project");
|
|
47
|
+
|
|
48
|
+
const name = normalize(rawName);
|
|
49
|
+
if (!name) fail("name must contain a letter or digit");
|
|
50
|
+
const allowedResources = new Set(["scripts", "reference", "assets"]);
|
|
51
|
+
for (const resource of resources) if (!allowedResources.has(resource)) fail(`unsupported resource directory: ${resource}`);
|
|
52
|
+
|
|
53
|
+
const parent = runtime
|
|
54
|
+
? (useUser ? path.join(userRoot(), "runtime-skills") : path.join(project, ".oharness", "runtime-skills"))
|
|
55
|
+
: (useUser ? path.join(userRoot(), "skills") : path.join(project, ".oharness", "skills"));
|
|
56
|
+
const directory = path.join(parent, name);
|
|
57
|
+
const file = path.join(directory, runtime ? "index.mjs" : "SKILL.md");
|
|
58
|
+
if (!force) {
|
|
59
|
+
try {
|
|
60
|
+
await readFile(file, "utf8");
|
|
61
|
+
fail(`${file} already exists (pass --force only to replace it intentionally)`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code !== "ENOENT") throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await mkdir(directory, { recursive: true });
|
|
68
|
+
for (const resource of resources) await mkdir(path.join(directory, resource), { recursive: true });
|
|
69
|
+
const title = name.split("-").map((word) => word[0]?.toUpperCase() + word.slice(1)).join(" ");
|
|
70
|
+
const cleanDescription = description.trim().replace(/\s+/g, " ");
|
|
71
|
+
const body = runtime
|
|
72
|
+
? `import { fileURLToPath } from "node:url";\n\nexport default {\n id: ${JSON.stringify(`skill-${name}`)},\n version: "1.0.0",\n apiVersion: 1,\n description: ${JSON.stringify(`Runtime skill: ${cleanDescription}`)},\n parseConfig(raw) {\n if (raw !== undefined) throw new Error("This runtime skill takes no configuration");\n return {};\n },\n setup(ctx) {\n ctx.skills.register({\n name: ${JSON.stringify(name)},\n description: ${JSON.stringify(cleanDescription)},\n body: ${JSON.stringify(instructions.trim())},\n directory: fileURLToPath(new URL(".", import.meta.url)),\n });\n },\n};\n`
|
|
73
|
+
: `---\nname: ${name}\ndescription: ${cleanDescription}\n---\n\n# ${title}\n\n${instructions.trim()}\n`;
|
|
74
|
+
await writeFile(file, body, "utf8");
|
|
75
|
+
console.log(file);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const input = process.argv[2];
|
|
7
|
+
if (!input) {
|
|
8
|
+
console.error("usage: validate-runtime-skill.mjs <runtime-skill-directory-or-module>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const file = (await stat(path.resolve(input))).isDirectory() ? path.resolve(input, "index.mjs") : path.resolve(input);
|
|
13
|
+
const errors = [];
|
|
14
|
+
let plugin;
|
|
15
|
+
try { plugin = (await import(`${pathToFileURL(file).href}?validate=${Date.now()}`)).default; }
|
|
16
|
+
catch (error) { errors.push(`cannot import ${file}: ${error.message}`); }
|
|
17
|
+
if (!plugin || typeof plugin !== "object") errors.push("module must default-export a plugin object");
|
|
18
|
+
if (plugin?.apiVersion !== 1 || typeof plugin?.id !== "string" || typeof plugin?.version !== "string") errors.push("runtime plugin requires id, version, and apiVersion: 1");
|
|
19
|
+
if (typeof plugin?.parseConfig !== "function" || typeof plugin?.setup !== "function") errors.push("runtime plugin requires parseConfig and setup functions");
|
|
20
|
+
|
|
21
|
+
let captured;
|
|
22
|
+
if (!errors.length) {
|
|
23
|
+
try {
|
|
24
|
+
const config = await plugin.parseConfig(undefined);
|
|
25
|
+
await plugin.setup({ skills: { register(skill) { captured = skill; } } }, config);
|
|
26
|
+
} catch (error) { errors.push(`runtime skill setup failed: ${error.message}`); }
|
|
27
|
+
}
|
|
28
|
+
if (!captured) errors.push("plugin setup must register one runtime skill");
|
|
29
|
+
if (captured && (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(captured.name) || !captured.description?.trim() || !captured.body?.trim())) errors.push("registered skill requires a valid name, description, and body");
|
|
30
|
+
if (captured?.directory !== undefined && !path.isAbsolute(captured.directory)) errors.push("runtime skill directory must be absolute");
|
|
31
|
+
|
|
32
|
+
if (errors.length) {
|
|
33
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
console.log(`valid OHarness runtime skill: ${captured.name}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const input = process.argv[2];
|
|
6
|
+
if (!input) {
|
|
7
|
+
console.error("usage: validate-skill.mjs <skill-directory-or-SKILL.md>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const file = path.basename(input) === "SKILL.md" ? path.resolve(input) : path.resolve(input, "SKILL.md");
|
|
12
|
+
const directory = path.dirname(file);
|
|
13
|
+
const errors = [];
|
|
14
|
+
let raw = "";
|
|
15
|
+
try {
|
|
16
|
+
if (!(await stat(file)).isFile()) errors.push("SKILL.md is not a file");
|
|
17
|
+
raw = await readFile(file, "utf8");
|
|
18
|
+
} catch (error) {
|
|
19
|
+
errors.push(`cannot read ${file}: ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(raw);
|
|
23
|
+
if (!match) errors.push("SKILL.md must start with closed YAML frontmatter");
|
|
24
|
+
const frontmatter = match?.[1] ?? "";
|
|
25
|
+
const body = match?.[2]?.trim() ?? "";
|
|
26
|
+
const name = /^name:\s*(.+)$/m.exec(frontmatter)?.[1]?.trim();
|
|
27
|
+
const description = /^description:\s*(.+)$/m.exec(frontmatter)?.[1]?.trim();
|
|
28
|
+
if (!name) errors.push("frontmatter requires name");
|
|
29
|
+
if (!description) errors.push("frontmatter requires description");
|
|
30
|
+
if (name && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) errors.push("name must be lowercase hyphen-case");
|
|
31
|
+
if (name && name.length >= 64) errors.push("name must be shorter than 64 characters");
|
|
32
|
+
if (name && path.basename(directory) !== name) errors.push("directory name must match frontmatter name");
|
|
33
|
+
if (body.length < 40) errors.push("skill body is too short to provide useful instructions");
|
|
34
|
+
if (/\bTODO\b|\[replace|<skill instructions>|<selection description>/i.test(raw)) errors.push("unfinished placeholder found");
|
|
35
|
+
|
|
36
|
+
if (errors.length) {
|
|
37
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log(`valid OHarness skill: ${name}`);
|