@holdyourvoice/hyv 3.3.5 → 3.4.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/Readme.md +12 -1
- package/dist/agents/catalog.js +118 -0
- package/dist/agents/catalog.test.js +60 -0
- package/dist/agents/emit.js +42 -0
- package/dist/agents/emit.test.js +64 -0
- package/dist/agents/index.js +4 -0
- package/dist/agents/load.js +184 -0
- package/dist/agents/load.test.js +149 -0
- package/dist/agents/types.js +18 -0
- package/dist/cli.js +110 -1
- package/dist/cli.test.js +76 -0
- package/dist/rebuild-task.test.js +1 -1
- package/dist/release-audit.test.js +5 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/skills/hyv-analyze/SKILL.md +24 -0
- package/skills/hyv-analyze/agent.json +49 -0
- package/skills/hyv-analyze/agents/openai.yaml +4 -0
- package/skills/hyv-apply-hidden-text-policy/SKILL.md +24 -0
- package/skills/hyv-apply-hidden-text-policy/agent.json +49 -0
- package/skills/hyv-apply-hidden-text-policy/agents/openai.yaml +4 -0
- package/skills/hyv-apply-rebuild/SKILL.md +24 -0
- package/skills/hyv-apply-rebuild/agent.json +49 -0
- package/skills/hyv-apply-rebuild/agents/openai.yaml +4 -0
- package/skills/hyv-apply-rewrite/SKILL.md +24 -0
- package/skills/hyv-apply-rewrite/agent.json +48 -0
- package/skills/hyv-apply-rewrite/agents/openai.yaml +4 -0
- package/skills/hyv-batch-analyze/SKILL.md +24 -0
- package/skills/hyv-batch-analyze/agent.json +45 -0
- package/skills/hyv-batch-analyze/agents/openai.yaml +4 -0
- package/skills/hyv-fact-lint/SKILL.md +24 -0
- package/skills/hyv-fact-lint/agent.json +50 -0
- package/skills/hyv-fact-lint/agents/openai.yaml +4 -0
- package/skills/hyv-final-check/SKILL.md +24 -0
- package/skills/hyv-final-check/agent.json +43 -0
- package/skills/hyv-final-check/agents/openai.yaml +4 -0
- package/skills/hyv-hygiene/SKILL.md +24 -0
- package/skills/hyv-hygiene/agent.json +50 -0
- package/skills/hyv-hygiene/agents/openai.yaml +4 -0
- package/skills/hyv-inspect-hidden-text/SKILL.md +24 -0
- package/skills/hyv-inspect-hidden-text/agent.json +48 -0
- package/skills/hyv-inspect-hidden-text/agents/openai.yaml +4 -0
- package/skills/hyv-learning/SKILL.md +24 -0
- package/skills/hyv-learning/agent.json +48 -0
- package/skills/hyv-learning/agents/openai.yaml +4 -0
- package/skills/hyv-lifecycle/SKILL.md +24 -0
- package/skills/hyv-lifecycle/agent.json +48 -0
- package/skills/hyv-lifecycle/agents/openai.yaml +4 -0
- package/skills/hyv-logic-lint/SKILL.md +24 -0
- package/skills/hyv-logic-lint/agent.json +47 -0
- package/skills/hyv-logic-lint/agents/openai.yaml +4 -0
- package/skills/hyv-mcp/SKILL.md +24 -0
- package/skills/hyv-mcp/agent.json +43 -0
- package/skills/hyv-mcp/agents/openai.yaml +4 -0
- package/skills/hyv-patterns/SKILL.md +24 -0
- package/skills/hyv-patterns/agent.json +43 -0
- package/skills/hyv-patterns/agents/openai.yaml +4 -0
- package/skills/hyv-prepare-judgment/SKILL.md +24 -0
- package/skills/hyv-prepare-judgment/agent.json +52 -0
- package/skills/hyv-prepare-judgment/agents/openai.yaml +4 -0
- package/skills/hyv-prepare-rebuild/SKILL.md +24 -0
- package/skills/hyv-prepare-rebuild/agent.json +54 -0
- package/skills/hyv-prepare-rebuild/agents/openai.yaml +4 -0
- package/skills/hyv-prepare-rewrite/SKILL.md +24 -0
- package/skills/hyv-prepare-rewrite/agent.json +51 -0
- package/skills/hyv-prepare-rewrite/agents/openai.yaml +4 -0
- package/skills/hyv-profile/SKILL.md +24 -0
- package/skills/hyv-profile/agent.json +50 -0
- package/skills/hyv-profile/agents/openai.yaml +4 -0
- package/skills/hyv-rebuild-writer-request/SKILL.md +24 -0
- package/skills/hyv-rebuild-writer-request/agent.json +47 -0
- package/skills/hyv-rebuild-writer-request/agents/openai.yaml +4 -0
- package/skills/hyv-reduce-judgment/SKILL.md +24 -0
- package/skills/hyv-reduce-judgment/agent.json +45 -0
- package/skills/hyv-reduce-judgment/agents/openai.yaml +4 -0
- package/skills/hyv-rewrite-prompt/SKILL.md +24 -0
- package/skills/hyv-rewrite-prompt/agent.json +48 -0
- package/skills/hyv-rewrite-prompt/agents/openai.yaml +4 -0
- package/skills/hyv-verify/SKILL.md +24 -0
- package/skills/hyv-verify/agent.json +49 -0
- package/skills/hyv-verify/agents/openai.yaml +4 -0
- package/skills/hyv-verify-spec/SKILL.md +24 -0
- package/skills/hyv-verify-spec/agent.json +50 -0
- package/skills/hyv-verify-spec/agents/openai.yaml +4 -0
package/Readme.md
CHANGED
|
@@ -120,22 +120,32 @@ The tool never edits your draft. You decide which findings are valid, apply the
|
|
|
120
120
|
| `hyv profile <profile.json> <sample...>` | Two or more text files | Profile JSON |
|
|
121
121
|
| `hyv analyze <draft> <profile.json>` | Draft and profile | Analysis JSON |
|
|
122
122
|
| `hyv hygiene <draft> [--fix] [--output=path]` | Draft | Hygiene report or cleaned copy plus receipt |
|
|
123
|
+
| `hyv inspect-hidden-text <draft> [policy.json]` | Draft and optional policy | Hidden-text inspection report |
|
|
124
|
+
| `hyv apply-hidden-text-policy <draft> <policy.json> <output.md>` | Draft and approved policy | Sanitized output plus receipt |
|
|
123
125
|
| `hyv final-check <path\|->` | Any final text | Accepted text on stdout or a withheld-output report |
|
|
126
|
+
| `hyv logic-lint <draft\|-> [writing-brief.json]` | Draft and optional brief | Deterministic logic-lint report |
|
|
124
127
|
| `hyv rewrite-prompt <draft> <profile.json>` | Draft and profile | Markdown editing brief |
|
|
125
128
|
| `hyv prepare-rewrite <draft> <profile.json> <task.json>` | Draft and profile | Versioned task file |
|
|
126
129
|
| `hyv apply-rewrite <task.json> <response.json> <profile.json>` | Task, response, profile | Candidate evaluation JSON |
|
|
127
130
|
| `hyv prepare-judgment <pre-edit\|post-candidate> <kind> <draft> <profile.json> <task.json> [candidate.md]` | Draft, profile, optional candidate | Versioned judgment task |
|
|
128
131
|
| `hyv reduce-judgment <envelope.json> ...` | Signed judgment envelopes | Recommendation JSON |
|
|
129
132
|
| `hyv prepare-rebuild <draft> <profile.json> <reduction.json> <copy-spec.json> <task.json> [--recomposition-policy policy.json]` | Draft, recommendation, CopySpec, capability, optional policy | Versioned rebuild task |
|
|
133
|
+
| `hyv rebuild-writer-request <task.json> <writer-request.json>` | Rebuild task | Writer-only rebuild request |
|
|
130
134
|
| `hyv apply-rebuild <task.json> <response.json> <profile.json>` | Task, response, profile, capability | Candidate evaluation JSON |
|
|
131
135
|
| `hyv verify <original> <candidate> <profile.json>` | Original, candidate, profile | Verification JSON and exit code |
|
|
132
136
|
| `hyv verify-spec <original> <candidate> <profile.json> <copy-spec.json>` | Original, candidate, profile, CopySpec | Verification JSON with hard claim gate |
|
|
133
|
-
| `hyv learning <show\|inspect\|add\|record\|ratify\|supersede\|migrate\|clear> ...` | Profile, operation, bounded metadata | Preferences or a text-free receipt |
|
|
137
|
+
| `hyv learning <show\|inspect\|add\|record\|record-approved\|ratify\|supersede\|migrate\|clear> ...` | Profile, operation, bounded metadata | Preferences or a text-free receipt |
|
|
134
138
|
| `hyv lifecycle <prepare-semantic\|submit-verdict\|inspect\|validate-final-approval\|finalize> ...` | Versioned lifecycle artifacts | Lifecycle artifact or metadata |
|
|
135
139
|
| `hyv patterns` | None | Ruleset JSON |
|
|
140
|
+
| `hyv mcp` | None | Local MCP server on stdio |
|
|
141
|
+
| `hyv agent list\|validate\|describe\|emit <id> [--host HOST] [--mode prompt\|json] [--output FILE]` | Optional agent id | Portable agent contract (see below) |
|
|
136
142
|
|
|
137
143
|
Every file argument can be `-` when the command accepts input on standard input. Use `npx @holdyourvoice/hyv <command>` if you have not installed the CLI globally.
|
|
138
144
|
|
|
145
|
+
## Portable agents
|
|
146
|
+
|
|
147
|
+
The 23 writing and runtime commands are also model-neutral portable agent packages under `skills/hyv-*/` (an `agent.json` contract, a `SKILL.md`, and an `agents/openai.yaml` interface), mirroring the clean-code portable-agent pattern. `hyv agent list` prints every package; `hyv agent validate [id]` checks the contract schema; `hyv agent describe <id> --host HOST` resolves permissions against a host catalog; and `hyv agent emit <id> --mode prompt|json --host HOST` emits a host-aware contract. The subcommand stays local. `emit --output` creates a new contract file and refuses an existing target. Read the [portable agents guide](docs/wiki/Portable-Agents.md) for the package contract, host model, and examples.
|
|
148
|
+
|
|
139
149
|
## Privacy
|
|
140
150
|
|
|
141
151
|
Your samples, drafts, profiles, and candidates stay on your machine. Verification is read-only. Learning commands can write text-free local events under `~/.hyv/learning/` — profile fingerprint, finding IDs, counts, and an opaque digest. No writing text is uploaded, and the package makes no runtime network requests.
|
|
@@ -151,6 +161,7 @@ Keep writing samples, edit histories, and client text out of public commits unle
|
|
|
151
161
|
| [Prompt contract](docs/PROMPT-CONTRACT.md) | The tier order and editing constraints. |
|
|
152
162
|
| [VoiceDNA](docs/VOICE-DNA.md) | The 13 profile elements. |
|
|
153
163
|
| [Fact linter](docs/wiki/Fact-Linter.md) | The source-consistency checker. |
|
|
164
|
+
| [Portable agents](docs/wiki/Portable-Agents.md) | Load or emit a host-aware contract for one HYV command. |
|
|
154
165
|
| [Support](SUPPORT.md) | Funding without a feature gate. |
|
|
155
166
|
|
|
156
167
|
## Contribute
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const CATALOG = {
|
|
2
|
+
generic: {
|
|
3
|
+
id: 'generic', display_name: 'Generic coding environment', native_skills: false, subagents: false,
|
|
4
|
+
hooks: false, blocking_approvals: false, file_edits: false, command_execution: false, browser_automation: false,
|
|
5
|
+
background_tasks: false, cli: true, repository_read: false,
|
|
6
|
+
context_capacity: 'host-defined', filesystem_mode: 'host-defined', network_policy: 'host-defined',
|
|
7
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
8
|
+
integration: 'portable Markdown instructions and standalone CLI',
|
|
9
|
+
},
|
|
10
|
+
codex: {
|
|
11
|
+
id: 'codex', display_name: 'Codex', native_skills: true, subagents: true,
|
|
12
|
+
hooks: false, blocking_approvals: true, file_edits: true, command_execution: true, browser_automation: false,
|
|
13
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
14
|
+
context_capacity: 'host-defined', filesystem_mode: 'sandboxed', network_policy: 'approval-gated',
|
|
15
|
+
subagent_isolation: true, session_reset: true, structured_output: true,
|
|
16
|
+
integration: 'native skills with AGENTS.md fallback',
|
|
17
|
+
},
|
|
18
|
+
'claude-code': {
|
|
19
|
+
id: 'claude-code', display_name: 'Claude Code', native_skills: true, subagents: true,
|
|
20
|
+
hooks: true, blocking_approvals: true, file_edits: true, command_execution: true, browser_automation: false,
|
|
21
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
22
|
+
context_capacity: 'host-defined', filesystem_mode: 'host-defined', network_policy: 'approval-gated',
|
|
23
|
+
subagent_isolation: true, session_reset: false, structured_output: true,
|
|
24
|
+
integration: 'native skills and agent instructions',
|
|
25
|
+
},
|
|
26
|
+
cursor: {
|
|
27
|
+
id: 'cursor', display_name: 'Cursor', native_skills: false, subagents: false,
|
|
28
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: true, browser_automation: false,
|
|
29
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
30
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
31
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
32
|
+
integration: 'generated rules and portable instructions',
|
|
33
|
+
},
|
|
34
|
+
copilot: {
|
|
35
|
+
id: 'copilot', display_name: 'GitHub Copilot', native_skills: false, subagents: false,
|
|
36
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: false, browser_automation: false,
|
|
37
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
38
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
39
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
40
|
+
integration: 'repository instructions and custom agent definitions',
|
|
41
|
+
},
|
|
42
|
+
'gemini-cli': {
|
|
43
|
+
id: 'gemini-cli', display_name: 'Gemini CLI', native_skills: false, subagents: false,
|
|
44
|
+
hooks: false, blocking_approvals: true, file_edits: true, command_execution: true, browser_automation: false,
|
|
45
|
+
background_tasks: true, cli: true, repository_read: true,
|
|
46
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'approval-gated',
|
|
47
|
+
subagent_isolation: false, session_reset: false, structured_output: true,
|
|
48
|
+
integration: 'terminal-agent instructions and standalone CLI',
|
|
49
|
+
},
|
|
50
|
+
'ide-agent': {
|
|
51
|
+
id: 'ide-agent', display_name: 'IDE coding agent', native_skills: false, subagents: false,
|
|
52
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: false, browser_automation: false,
|
|
53
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
54
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
55
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
56
|
+
integration: 'generated rules or portable instructions',
|
|
57
|
+
},
|
|
58
|
+
windsurf: {
|
|
59
|
+
id: 'windsurf', display_name: 'Windsurf', native_skills: false, subagents: false,
|
|
60
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: true, browser_automation: false,
|
|
61
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
62
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
63
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
64
|
+
integration: 'generated workspace rules and standalone CLI',
|
|
65
|
+
},
|
|
66
|
+
cline: {
|
|
67
|
+
id: 'cline', display_name: 'Cline', native_skills: false, subagents: false,
|
|
68
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: true, browser_automation: false,
|
|
69
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
70
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
71
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
72
|
+
integration: 'generated workspace rules and standalone CLI',
|
|
73
|
+
},
|
|
74
|
+
'roo-code': {
|
|
75
|
+
id: 'roo-code', display_name: 'Roo Code', native_skills: false, subagents: false,
|
|
76
|
+
hooks: false, blocking_approvals: false, file_edits: true, command_execution: true, browser_automation: false,
|
|
77
|
+
background_tasks: false, cli: true, repository_read: true,
|
|
78
|
+
context_capacity: 'host-defined', filesystem_mode: 'workspace', network_policy: 'host-defined',
|
|
79
|
+
subagent_isolation: false, session_reset: false, structured_output: false,
|
|
80
|
+
integration: 'generated workspace rules and standalone CLI',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
export function resolveHost(id) {
|
|
84
|
+
return CATALOG[id] ?? CATALOG.generic;
|
|
85
|
+
}
|
|
86
|
+
export function listHosts() {
|
|
87
|
+
return Object.values(CATALOG).sort((a, b) => a.id.localeCompare(b.id));
|
|
88
|
+
}
|
|
89
|
+
function hostSupports(host, permission) {
|
|
90
|
+
switch (permission) {
|
|
91
|
+
case 'read_repository': return host.repository_read;
|
|
92
|
+
case 'write_repository': return host.file_edits;
|
|
93
|
+
case 'execute_commands': return host.command_execution;
|
|
94
|
+
case 'network': return false;
|
|
95
|
+
case 'browser_automation': return host.browser_automation;
|
|
96
|
+
case 'git_write':
|
|
97
|
+
case 'pull_request_write': return host.command_execution;
|
|
98
|
+
case 'subagents': return host.subagents;
|
|
99
|
+
default: return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function runtime(agent, hostID) {
|
|
103
|
+
const host = resolveHost(hostID);
|
|
104
|
+
const available = [];
|
|
105
|
+
const unavailable = [];
|
|
106
|
+
for (const permission of agent.permissions) {
|
|
107
|
+
if (hostSupports(host, permission))
|
|
108
|
+
available.push(permission);
|
|
109
|
+
else
|
|
110
|
+
unavailable.push({ capability: permission, status: 'NOT_AVAILABLE' });
|
|
111
|
+
}
|
|
112
|
+
let mode = 'procedural';
|
|
113
|
+
if (unavailable.length > 0)
|
|
114
|
+
mode = 'prompt-only';
|
|
115
|
+
if (host.native_skills && unavailable.length === 0)
|
|
116
|
+
mode = 'native';
|
|
117
|
+
return { agent, host, available_capabilities: available, unavailable_capabilities: unavailable, execution_mode: mode };
|
|
118
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { resolveHost, listHosts, runtime } from './catalog.js';
|
|
4
|
+
import { AGENT_SCHEMA_VERSION } from './types.js';
|
|
5
|
+
function minimalDescriptor(id, permissions) {
|
|
6
|
+
return {
|
|
7
|
+
schema_version: AGENT_SCHEMA_VERSION,
|
|
8
|
+
id,
|
|
9
|
+
title: id,
|
|
10
|
+
description: 'test',
|
|
11
|
+
instruction_file: 'SKILL.md',
|
|
12
|
+
role: 'Tester',
|
|
13
|
+
workflow_phase: 'test',
|
|
14
|
+
input: { required: ['draft'], optional: [] },
|
|
15
|
+
output: { required: ['report'], optional: [] },
|
|
16
|
+
evidence_requirements: ['evidence'],
|
|
17
|
+
permissions,
|
|
18
|
+
stop_conditions: ['stop'],
|
|
19
|
+
tool_free_mode: { available: true, behavior: 'report', unavailable_statuses: ['NOT_AVAILABLE'] },
|
|
20
|
+
handoff_to: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
test('resolves known hosts and falls back to generic for unknown ids', () => {
|
|
24
|
+
assert.equal(resolveHost('codex').id, 'codex');
|
|
25
|
+
assert.equal(resolveHost('future-ide').id, 'generic');
|
|
26
|
+
assert.equal(resolveHost('future-ide').native_skills, false);
|
|
27
|
+
assert.equal(resolveHost('future-ide').cli, true);
|
|
28
|
+
});
|
|
29
|
+
test('catalog has unique host ids and includes generic', () => {
|
|
30
|
+
const hosts = listHosts();
|
|
31
|
+
const ids = hosts.map((host) => host.id);
|
|
32
|
+
assert.ok(ids.includes('generic'));
|
|
33
|
+
assert.equal(new Set(ids).size, ids.length);
|
|
34
|
+
});
|
|
35
|
+
test('runtime marks unsupported permissions unavailable and switches to prompt-only', () => {
|
|
36
|
+
const descriptor = minimalDescriptor('hyv-verify', ['read_repository', 'network', 'subagents']);
|
|
37
|
+
const resolved = runtime(descriptor, 'generic');
|
|
38
|
+
assert.deepEqual(resolved.available_capabilities, []);
|
|
39
|
+
assert.deepEqual(resolved.unavailable_capabilities, [
|
|
40
|
+
{ capability: 'read_repository', status: 'NOT_AVAILABLE' },
|
|
41
|
+
{ capability: 'network', status: 'NOT_AVAILABLE' },
|
|
42
|
+
{ capability: 'subagents', status: 'NOT_AVAILABLE' },
|
|
43
|
+
]);
|
|
44
|
+
assert.equal(resolved.execution_mode, 'prompt-only');
|
|
45
|
+
});
|
|
46
|
+
test('runtime uses native mode when the host supports every permission', () => {
|
|
47
|
+
const descriptor = minimalDescriptor('hyv-verify', ['read_repository', 'execute_commands']);
|
|
48
|
+
const resolved = runtime(descriptor, 'codex');
|
|
49
|
+
assert.deepEqual(resolved.available_capabilities, ['read_repository', 'execute_commands']);
|
|
50
|
+
assert.deepEqual(resolved.unavailable_capabilities, []);
|
|
51
|
+
assert.equal(resolved.execution_mode, 'native');
|
|
52
|
+
});
|
|
53
|
+
test('network is never available on any host', () => {
|
|
54
|
+
for (const host of listHosts()) {
|
|
55
|
+
const resolved = runtime(minimalDescriptor('hyv-verify', ['network']), host.id);
|
|
56
|
+
assert.deepEqual(resolved.available_capabilities, []);
|
|
57
|
+
assert.deepEqual(resolved.unavailable_capabilities, [{ capability: 'network', status: 'NOT_AVAILABLE' }]);
|
|
58
|
+
assert.equal(resolved.execution_mode, 'prompt-only');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runtime } from './catalog.js';
|
|
2
|
+
export function describe(pkg, hostID) {
|
|
3
|
+
return runtime(pkg.descriptor, hostID);
|
|
4
|
+
}
|
|
5
|
+
export function emitJson(pkg, hostID) {
|
|
6
|
+
return JSON.stringify(describe(pkg, hostID), null, 2);
|
|
7
|
+
}
|
|
8
|
+
function printable(values) {
|
|
9
|
+
return values.length === 0 ? 'none' : values.join(', ');
|
|
10
|
+
}
|
|
11
|
+
function printableUnavailable(values) {
|
|
12
|
+
return values.length === 0 ? 'none' : values.map(({ capability, status }) => `${capability} (${status})`).join(', ');
|
|
13
|
+
}
|
|
14
|
+
function runtimeSummary(descriptor) {
|
|
15
|
+
const host = descriptor.host;
|
|
16
|
+
return `Context capacity: ${host.context_capacity}; filesystem mode: ${host.filesystem_mode}; network policy: ${host.network_policy}; browser/UI: ${host.browser_automation}; subagent isolation: ${host.subagent_isolation}; session reset: ${host.session_reset}; structured output: ${host.structured_output}.`;
|
|
17
|
+
}
|
|
18
|
+
export function emitPrompt(pkg, hostID) {
|
|
19
|
+
const descriptor = pkg.descriptor;
|
|
20
|
+
const resolved = describe(pkg, hostID);
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(`# ${descriptor.title}`, '');
|
|
23
|
+
lines.push(`Role: ${descriptor.role}`);
|
|
24
|
+
lines.push(`Phase: ${descriptor.workflow_phase}`);
|
|
25
|
+
lines.push(`Execution mode: ${resolved.execution_mode}`, '');
|
|
26
|
+
lines.push('## Contract', '');
|
|
27
|
+
lines.push(`Required input: ${printable(descriptor.input.required)}`);
|
|
28
|
+
lines.push(`Required output: ${printable(descriptor.output.required)}`);
|
|
29
|
+
lines.push(`Evidence: ${printable(descriptor.evidence_requirements)}`);
|
|
30
|
+
lines.push(`Stop conditions: ${printable(descriptor.stop_conditions)}`, '');
|
|
31
|
+
lines.push('## Runtime descriptor', '');
|
|
32
|
+
lines.push(runtimeSummary(resolved), '');
|
|
33
|
+
lines.push('## Capability boundary', '');
|
|
34
|
+
lines.push(`Available: ${printable(resolved.available_capabilities)}`);
|
|
35
|
+
lines.push(`Unavailable: ${printableUnavailable(resolved.unavailable_capabilities)}`, '');
|
|
36
|
+
lines.push(`When a capability is unavailable, ${descriptor.tool_free_mode.behavior} Status must be one of: ${printable(descriptor.tool_free_mode.unavailable_statuses)}.`, '');
|
|
37
|
+
lines.push('## Handoff', '');
|
|
38
|
+
lines.push(`Next agents: ${printable(descriptor.handoff_to)}`, '');
|
|
39
|
+
lines.push('## Instructions', '');
|
|
40
|
+
lines.push(pkg.instructions.trim());
|
|
41
|
+
return `${lines.join('\n')}\n`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { describe, emitJson, emitPrompt } from './emit.js';
|
|
4
|
+
import { AGENT_SCHEMA_VERSION } from './types.js';
|
|
5
|
+
function makePackage(id) {
|
|
6
|
+
return {
|
|
7
|
+
directory: '/pkg',
|
|
8
|
+
instructions: 'Usage: hyv verify original.md candidate.md profile.json',
|
|
9
|
+
descriptor: {
|
|
10
|
+
schema_version: AGENT_SCHEMA_VERSION,
|
|
11
|
+
id,
|
|
12
|
+
title: 'hyv-verify',
|
|
13
|
+
description: 'verify a candidate',
|
|
14
|
+
instruction_file: 'SKILL.md',
|
|
15
|
+
role: 'Verifier',
|
|
16
|
+
workflow_phase: 'verify',
|
|
17
|
+
input: { required: ['original', 'candidate'], optional: ['writing brief'] },
|
|
18
|
+
output: { required: ['verification report'], optional: [] },
|
|
19
|
+
evidence_requirements: ['preserve revision evidence'],
|
|
20
|
+
permissions: ['read_repository', 'network'],
|
|
21
|
+
stop_conditions: ['stop when evidence is stale'],
|
|
22
|
+
tool_free_mode: {
|
|
23
|
+
available: true,
|
|
24
|
+
behavior: 'report the decision and exact unavailable capability',
|
|
25
|
+
unavailable_statuses: ['NOT_AVAILABLE', 'NOT_CONFIGURED', 'NOT_RUN', 'STALE', 'ERROR'],
|
|
26
|
+
},
|
|
27
|
+
handoff_to: ['hyv-lifecycle'],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
test('describe resolves a host and returns a runtime descriptor', () => {
|
|
32
|
+
const resolved = describe(makePackage('hyv-verify'), 'codex');
|
|
33
|
+
assert.equal(resolved.agent.id, 'hyv-verify');
|
|
34
|
+
assert.equal(resolved.host.id, 'codex');
|
|
35
|
+
assert.ok(resolved.available_capabilities.includes('read_repository'));
|
|
36
|
+
assert.deepEqual(resolved.unavailable_capabilities, [{ capability: 'network', status: 'NOT_AVAILABLE' }]);
|
|
37
|
+
assert.equal(resolved.execution_mode, 'prompt-only');
|
|
38
|
+
});
|
|
39
|
+
test('emitJson renders a valid JSON runtime descriptor', () => {
|
|
40
|
+
const body = emitJson(makePackage('hyv-verify'), 'generic');
|
|
41
|
+
const parsed = JSON.parse(body);
|
|
42
|
+
assert.equal(parsed.agent.id, 'hyv-verify');
|
|
43
|
+
assert.equal(parsed.host.id, 'generic');
|
|
44
|
+
assert.ok(Array.isArray(parsed.unavailable_capabilities));
|
|
45
|
+
assert.deepEqual(parsed.unavailable_capabilities, [
|
|
46
|
+
{ capability: 'read_repository', status: 'NOT_AVAILABLE' },
|
|
47
|
+
{ capability: 'network', status: 'NOT_AVAILABLE' },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
test('emitPrompt includes instructions, capability boundary, and required statuses', () => {
|
|
51
|
+
const prompt = emitPrompt(makePackage('hyv-verify'), 'codex');
|
|
52
|
+
assert.match(prompt, /# hyv-verify/);
|
|
53
|
+
assert.match(prompt, /Role: Verifier/);
|
|
54
|
+
assert.match(prompt, /Usage: hyv verify original\.md candidate\.md profile\.json/);
|
|
55
|
+
assert.match(prompt, /Available: read_repository/);
|
|
56
|
+
assert.match(prompt, /Unavailable: network \(NOT_AVAILABLE\)/);
|
|
57
|
+
assert.match(prompt, /NOT_AVAILABLE, NOT_CONFIGURED, NOT_RUN, STALE, ERROR/);
|
|
58
|
+
assert.match(prompt, /Next agents: hyv-lifecycle/);
|
|
59
|
+
});
|
|
60
|
+
test('emitPrompt reports no available capabilities for a fully unavailable host', () => {
|
|
61
|
+
const prompt = emitPrompt(makePackage('hyv-verify'), 'generic');
|
|
62
|
+
assert.match(prompt, /Available: none/);
|
|
63
|
+
assert.match(prompt, /Execution mode: prompt-only/);
|
|
64
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AGENT_SCHEMA_VERSION, VALID_PERMISSIONS, VALID_UNAVAILABLE_STATUSES, } from './types.js';
|
|
2
|
+
export { resolveHost, listHosts, runtime } from './catalog.js';
|
|
3
|
+
export { loadAll, loadAllFrom, sortedIds, validateAll, validateId } from './load.js';
|
|
4
|
+
export { describe, emitJson, emitPrompt } from './emit.js';
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { AGENT_SCHEMA_VERSION, VALID_PERMISSIONS, VALID_UNAVAILABLE_STATUSES, } from './types.js';
|
|
5
|
+
const MAX_MANIFEST_BYTES = 1 << 20;
|
|
6
|
+
export const SKILLS_DIRECTORY = 'skills';
|
|
7
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
|
8
|
+
const DESCRIPTOR_KEYS = new Set(['schema_version', 'id', 'title', 'description', 'instruction_file', 'role', 'workflow_phase', 'input', 'output', 'evidence_requirements', 'permissions', 'stop_conditions', 'tool_free_mode', 'handoff_to']);
|
|
9
|
+
const IO_KEYS = new Set(['required', 'optional']);
|
|
10
|
+
const TOOL_FREE_MODE_KEYS = new Set(['available', 'behavior', 'unavailable_statuses']);
|
|
11
|
+
function isAgentDirectory(name) {
|
|
12
|
+
return name.startsWith('hyv-') && !name.startsWith('hyv-mcpb');
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function hasOnlyKeys(value, keys) {
|
|
18
|
+
return Object.keys(value).every((key) => keys.has(key));
|
|
19
|
+
}
|
|
20
|
+
function stringArray(value) {
|
|
21
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.length > 0);
|
|
22
|
+
}
|
|
23
|
+
function validIO(value) {
|
|
24
|
+
return isRecord(value) && hasOnlyKeys(value, IO_KEYS) && stringArray(value.required) && stringArray(value.optional) && value.required.length > 0;
|
|
25
|
+
}
|
|
26
|
+
function validToolFreeMode(value) {
|
|
27
|
+
return isRecord(value)
|
|
28
|
+
&& hasOnlyKeys(value, TOOL_FREE_MODE_KEYS)
|
|
29
|
+
&& value.available === true
|
|
30
|
+
&& typeof value.behavior === 'string' && value.behavior.length > 0
|
|
31
|
+
&& stringArray(value.unavailable_statuses) && value.unavailable_statuses.length > 0
|
|
32
|
+
&& value.unavailable_statuses.every((status) => VALID_UNAVAILABLE_STATUSES.has(status));
|
|
33
|
+
}
|
|
34
|
+
function validateDescriptor(value) {
|
|
35
|
+
if (!isRecord(value) || !hasOnlyKeys(value, DESCRIPTOR_KEYS))
|
|
36
|
+
return 'manifest must contain only the supported contract fields';
|
|
37
|
+
const descriptor = value;
|
|
38
|
+
if (descriptor.schema_version !== AGENT_SCHEMA_VERSION)
|
|
39
|
+
return `unsupported schema_version "${descriptor.schema_version}"`;
|
|
40
|
+
if (![descriptor.id, descriptor.title, descriptor.description, descriptor.instruction_file, descriptor.role, descriptor.workflow_phase].every((value) => typeof value === 'string' && value.length > 0)) {
|
|
41
|
+
return 'id, title, description, role, and workflow_phase are required';
|
|
42
|
+
}
|
|
43
|
+
if (descriptor.instruction_file !== 'SKILL.md')
|
|
44
|
+
return 'instruction_file must be SKILL.md';
|
|
45
|
+
if (!validIO(descriptor.input) || !validIO(descriptor.output)) {
|
|
46
|
+
return 'input and output require at least one entry in required';
|
|
47
|
+
}
|
|
48
|
+
if (!stringArray(descriptor.evidence_requirements) || !stringArray(descriptor.stop_conditions) || !stringArray(descriptor.permissions) || !stringArray(descriptor.handoff_to)) {
|
|
49
|
+
return 'evidence_requirements and stop_conditions require at least one entry';
|
|
50
|
+
}
|
|
51
|
+
if (!descriptor.evidence_requirements.length || !descriptor.stop_conditions.length)
|
|
52
|
+
return 'evidence_requirements and stop_conditions require at least one entry';
|
|
53
|
+
for (const permission of descriptor.permissions) {
|
|
54
|
+
if (!VALID_PERMISSIONS.has(permission))
|
|
55
|
+
return `unsupported permission "${permission}"`;
|
|
56
|
+
}
|
|
57
|
+
if (!validToolFreeMode(descriptor.tool_free_mode)) {
|
|
58
|
+
return 'tool_free_mode requires available and behavior';
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function validOpenAiInterface(value) {
|
|
63
|
+
const lines = value.trimEnd().split('\n');
|
|
64
|
+
const quotedString = (line, prefix) => {
|
|
65
|
+
if (!line?.startsWith(prefix))
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(line.slice(prefix.length));
|
|
69
|
+
return typeof parsed === 'string' && parsed.length > 0;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return lines.length === 4
|
|
76
|
+
&& lines[0] === 'interface:'
|
|
77
|
+
&& quotedString(lines[1], ' display_name: ')
|
|
78
|
+
&& quotedString(lines[2], ' short_description: ')
|
|
79
|
+
&& quotedString(lines[3], ' default_prompt: ');
|
|
80
|
+
}
|
|
81
|
+
function loadPackage(directory) {
|
|
82
|
+
const manifestPath = join(directory, 'agent.json');
|
|
83
|
+
const stat = lstatSync(manifestPath);
|
|
84
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
85
|
+
throw new Error(`inspect manifest: ${manifestPath} must be a regular file`);
|
|
86
|
+
if (stat.size > MAX_MANIFEST_BYTES)
|
|
87
|
+
throw new Error(`inspect manifest: ${manifestPath} exceeds ${MAX_MANIFEST_BYTES} bytes`);
|
|
88
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
89
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_MANIFEST_BYTES)
|
|
90
|
+
throw new Error(`inspect manifest: ${manifestPath} exceeds ${MAX_MANIFEST_BYTES} bytes`);
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`parse manifest: ${manifestPath} is not valid JSON`);
|
|
97
|
+
}
|
|
98
|
+
const error = validateDescriptor(parsed);
|
|
99
|
+
if (error)
|
|
100
|
+
throw new Error(`validate manifest: ${manifestPath}: ${error}`);
|
|
101
|
+
const descriptor = parsed;
|
|
102
|
+
const instructionPath = join(directory, descriptor.instruction_file);
|
|
103
|
+
const instructionStat = lstatSync(instructionPath);
|
|
104
|
+
if (instructionStat.isSymbolicLink() || !instructionStat.isFile()) {
|
|
105
|
+
throw new Error(`inspect instructions: ${instructionPath} must be a regular file`);
|
|
106
|
+
}
|
|
107
|
+
const instructions = readFileSync(instructionPath, 'utf8');
|
|
108
|
+
const interfacePath = join(directory, 'agents', 'openai.yaml');
|
|
109
|
+
const interfaceStat = lstatSync(interfacePath);
|
|
110
|
+
if (interfaceStat.isSymbolicLink() || !interfaceStat.isFile() || interfaceStat.size > MAX_MANIFEST_BYTES) {
|
|
111
|
+
throw new Error(`inspect OpenAI interface: ${interfacePath} must be a regular file within ${MAX_MANIFEST_BYTES} bytes`);
|
|
112
|
+
}
|
|
113
|
+
const openai = readFileSync(interfacePath, 'utf8');
|
|
114
|
+
if (Buffer.byteLength(openai, 'utf8') > MAX_MANIFEST_BYTES || !validOpenAiInterface(openai)) {
|
|
115
|
+
throw new Error(`validate OpenAI interface: ${interfacePath} must contain the supported interface metadata`);
|
|
116
|
+
}
|
|
117
|
+
return { descriptor, directory, instructions };
|
|
118
|
+
}
|
|
119
|
+
function locateSkillsRoot(start) {
|
|
120
|
+
let current = start;
|
|
121
|
+
for (;;) {
|
|
122
|
+
const candidate = join(current, SKILLS_DIRECTORY);
|
|
123
|
+
try {
|
|
124
|
+
const stat = lstatSync(candidate);
|
|
125
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// continue upward
|
|
130
|
+
}
|
|
131
|
+
const parent = resolve(current, '..');
|
|
132
|
+
if (parent === current)
|
|
133
|
+
break;
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`locate HYV package root: no ${SKILLS_DIRECTORY} directory found from ${start}`);
|
|
137
|
+
}
|
|
138
|
+
export function loadAllFrom(root) {
|
|
139
|
+
const skillsRoot = locateSkillsRoot(root);
|
|
140
|
+
const entries = readdirSync(skillsRoot, { withFileTypes: true });
|
|
141
|
+
const packages = new Map();
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.isDirectory() || !isAgentDirectory(entry.name))
|
|
144
|
+
continue;
|
|
145
|
+
const loaded = loadPackage(join(skillsRoot, entry.name));
|
|
146
|
+
if (loaded.descriptor.id !== entry.name) {
|
|
147
|
+
throw new Error(`load ${entry.name}: id must match skill directory`);
|
|
148
|
+
}
|
|
149
|
+
if (packages.has(loaded.descriptor.id))
|
|
150
|
+
throw new Error(`duplicate agent id "${loaded.descriptor.id}"`);
|
|
151
|
+
packages.set(loaded.descriptor.id, loaded);
|
|
152
|
+
}
|
|
153
|
+
for (const loaded of packages.values()) {
|
|
154
|
+
for (const target of loaded.descriptor.handoff_to) {
|
|
155
|
+
if (!packages.has(target))
|
|
156
|
+
throw new Error(`agent "${loaded.descriptor.id}" hands off to unknown agent "${target}"`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return packages;
|
|
160
|
+
}
|
|
161
|
+
export function loadAll(start = PACKAGE_ROOT) {
|
|
162
|
+
return loadAllFrom(start);
|
|
163
|
+
}
|
|
164
|
+
export function sortedIds(packages) {
|
|
165
|
+
return [...packages.keys()].sort();
|
|
166
|
+
}
|
|
167
|
+
export function validateAll(packages) {
|
|
168
|
+
for (const loaded of packages.values()) {
|
|
169
|
+
const error = validateDescriptor(loaded.descriptor);
|
|
170
|
+
if (error)
|
|
171
|
+
throw new Error(`validate manifest: ${loaded.directory}: ${error}`);
|
|
172
|
+
for (const target of loaded.descriptor.handoff_to) {
|
|
173
|
+
if (!packages.has(target))
|
|
174
|
+
throw new Error(`agent "${loaded.descriptor.id}" hands off to unknown agent "${target}"`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function validateId(packages, id) {
|
|
179
|
+
if (!packages.has(id))
|
|
180
|
+
throw new Error(`unknown agent "${id}"`);
|
|
181
|
+
}
|
|
182
|
+
export function isAbsolutePath(value) {
|
|
183
|
+
return isAbsolute(value);
|
|
184
|
+
}
|