@holdyourvoice/hyv 3.3.5 → 3.4.1
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 +66 -116
- 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 +464 -317
- package/dist/cli.test.js +76 -0
- package/dist/fact-linter.js +107 -98
- package/dist/mcp.js +43 -192
- package/dist/rebuild-task.test.js +1 -1
- package/dist/release-audit.test.js +5 -1
- package/dist/stage1-evaluation.js +74 -48
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { loadAll, loadAllFrom, validateAll, validateId, sortedIds } from './load.js';
|
|
7
|
+
import { AGENT_SCHEMA_VERSION } from './types.js';
|
|
8
|
+
function makePackage(dir, id, overrides = {}) {
|
|
9
|
+
const packageDir = join(dir, 'skills', id);
|
|
10
|
+
mkdirSync(join(packageDir, 'agents'), { recursive: true });
|
|
11
|
+
const descriptor = {
|
|
12
|
+
schema_version: AGENT_SCHEMA_VERSION,
|
|
13
|
+
id,
|
|
14
|
+
title: id,
|
|
15
|
+
description: 'test agent',
|
|
16
|
+
instruction_file: 'SKILL.md',
|
|
17
|
+
role: 'Tester',
|
|
18
|
+
workflow_phase: 'test',
|
|
19
|
+
input: { required: ['draft'], optional: [] },
|
|
20
|
+
output: { required: ['report'], optional: [] },
|
|
21
|
+
evidence_requirements: ['preserve evidence'],
|
|
22
|
+
permissions: ['read_repository'],
|
|
23
|
+
stop_conditions: ['stop when evidence missing'],
|
|
24
|
+
tool_free_mode: {
|
|
25
|
+
available: true,
|
|
26
|
+
behavior: 'report NOT_AVAILABLE',
|
|
27
|
+
unavailable_statuses: ['NOT_AVAILABLE', 'NOT_RUN'],
|
|
28
|
+
},
|
|
29
|
+
handoff_to: [],
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
writeFileSync(join(packageDir, 'agent.json'), JSON.stringify(descriptor));
|
|
33
|
+
writeFileSync(join(packageDir, 'SKILL.md'), `# ${id}\n\nUsage: hyv ${id} draft.md\n`);
|
|
34
|
+
writeFileSync(join(packageDir, 'agents', 'openai.yaml'), `interface:\n display_name: "${id}"\n short_description: "test agent"\n default_prompt: "Use ${id}"\n`);
|
|
35
|
+
return packageDir;
|
|
36
|
+
}
|
|
37
|
+
function makeTempRoot() {
|
|
38
|
+
return mkdtempSync(join(tmpdir(), 'hyv-agents-'));
|
|
39
|
+
}
|
|
40
|
+
test('loads all valid packages and rejects unknown schema version', () => {
|
|
41
|
+
const root = makeTempRoot();
|
|
42
|
+
try {
|
|
43
|
+
const first = makePackage(root, 'hyv-alpha');
|
|
44
|
+
makePackage(root, 'hyv-beta', { handoff_to: ['hyv-alpha'] });
|
|
45
|
+
const packages = loadAllFrom(root);
|
|
46
|
+
assert.deepEqual(sortedIds(packages), ['hyv-alpha', 'hyv-beta']);
|
|
47
|
+
assert.equal(packages.get('hyv-alpha').directory, first);
|
|
48
|
+
assert.match(packages.get('hyv-alpha').instructions, /^# hyv-alpha/);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
rmSync(root, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test('rejects a package whose id does not match its directory name', () => {
|
|
55
|
+
const root = makeTempRoot();
|
|
56
|
+
try {
|
|
57
|
+
makePackage(root, 'hyv-alpha', { id: 'hyv-mismatch' });
|
|
58
|
+
assert.throws(() => loadAllFrom(root), /id must match skill directory/);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('rejects a handoff target that does not exist', () => {
|
|
65
|
+
const root = makeTempRoot();
|
|
66
|
+
try {
|
|
67
|
+
makePackage(root, 'hyv-alpha', { handoff_to: ['hyv-ghost'] });
|
|
68
|
+
assert.throws(() => loadAllFrom(root), /hands off to unknown agent/);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
test('rejects an unsupported permission and an unsupported status', () => {
|
|
75
|
+
const root = makeTempRoot();
|
|
76
|
+
try {
|
|
77
|
+
makePackage(root, 'hyv-alpha', { permissions: ['definitely-not-real'] });
|
|
78
|
+
assert.throws(() => loadAllFrom(root), /unsupported permission/);
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
const root2 = makeTempRoot();
|
|
81
|
+
makePackage(root2, 'hyv-beta', {
|
|
82
|
+
tool_free_mode: { available: true, behavior: 'x', unavailable_statuses: ['NOPE'] },
|
|
83
|
+
});
|
|
84
|
+
assert.throws(() => loadAllFrom(root2), /tool_free_mode requires/);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
rmSync(root, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('validateAll and validateId report PASS or unknown id', () => {
|
|
91
|
+
const root = makeTempRoot();
|
|
92
|
+
try {
|
|
93
|
+
makePackage(root, 'hyv-alpha');
|
|
94
|
+
const packages = loadAllFrom(root);
|
|
95
|
+
validateAll(packages);
|
|
96
|
+
validateId(packages, 'hyv-alpha');
|
|
97
|
+
assert.throws(() => validateId(packages, 'hyv-missing'), /unknown agent/);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
test('rejects a malformed agent.json', () => {
|
|
104
|
+
const root = makeTempRoot();
|
|
105
|
+
try {
|
|
106
|
+
const packageDir = join(root, 'skills', 'hyv-alpha');
|
|
107
|
+
mkdirSync(join(packageDir, 'agents'), { recursive: true });
|
|
108
|
+
writeFileSync(join(packageDir, 'agent.json'), '{ not json');
|
|
109
|
+
writeFileSync(join(packageDir, 'SKILL.md'), '# x');
|
|
110
|
+
assert.throws(() => loadAllFrom(root), /not valid JSON/);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
test('rejects unknown contract fields and invalid OpenAI interface metadata', () => {
|
|
117
|
+
const root = makeTempRoot();
|
|
118
|
+
try {
|
|
119
|
+
makePackage(root, 'hyv-alpha', { unsupported: true });
|
|
120
|
+
assert.throws(() => loadAllFrom(root), /supported contract fields/);
|
|
121
|
+
rmSync(root, { recursive: true, force: true });
|
|
122
|
+
const root2 = makeTempRoot();
|
|
123
|
+
const packageDir = makePackage(root2, 'hyv-beta');
|
|
124
|
+
rmSync(join(packageDir, 'agents', 'openai.yaml'));
|
|
125
|
+
assert.throws(() => loadAllFrom(root2), /openai\.yaml/);
|
|
126
|
+
rmSync(root2, { recursive: true, force: true });
|
|
127
|
+
const root3 = makeTempRoot();
|
|
128
|
+
const invalidPackageDir = makePackage(root3, 'hyv-gamma');
|
|
129
|
+
writeFileSync(join(invalidPackageDir, 'agents', 'openai.yaml'), 'interface:\n display_name: "unterminated\n short_description: "test"\n default_prompt: "Use hyv-gamma"\n');
|
|
130
|
+
assert.throws(() => loadAllFrom(root3), /supported interface metadata/);
|
|
131
|
+
rmSync(root3, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
rmSync(root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
test('published packages declare command execution and required writes', () => {
|
|
138
|
+
const packages = loadAll();
|
|
139
|
+
const writers = new Set(['hyv-profile', 'hyv-hygiene', 'hyv-apply-hidden-text-policy', 'hyv-prepare-rewrite', 'hyv-prepare-judgment', 'hyv-prepare-rebuild', 'hyv-rebuild-writer-request', 'hyv-lifecycle', 'hyv-learning']);
|
|
140
|
+
for (const [id, pkg] of packages) {
|
|
141
|
+
assert.ok(pkg.descriptor.permissions.includes('execute_commands'), `${id} must require command execution`);
|
|
142
|
+
assert.equal(pkg.descriptor.permissions.includes('write_repository'), writers.has(id), `${id} write permission mismatch`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
test('published instructions document command-specific argument requirements', () => {
|
|
146
|
+
const packages = loadAll();
|
|
147
|
+
assert.match(packages.get('hyv-reduce-judgment').instructions, /hyv reduce-judgment envelope\.json envelope\.json envelope\.json \[envelope\.json\.\.\.\]/);
|
|
148
|
+
assert.match(packages.get('hyv-learning').instructions, /record-approved/);
|
|
149
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const AGENT_SCHEMA_VERSION = '1.0.0';
|
|
2
|
+
export const VALID_PERMISSIONS = new Set([
|
|
3
|
+
'read_repository',
|
|
4
|
+
'write_repository',
|
|
5
|
+
'execute_commands',
|
|
6
|
+
'network',
|
|
7
|
+
'browser_automation',
|
|
8
|
+
'git_write',
|
|
9
|
+
'pull_request_write',
|
|
10
|
+
'subagents',
|
|
11
|
+
]);
|
|
12
|
+
export const VALID_UNAVAILABLE_STATUSES = new Set([
|
|
13
|
+
'NOT_AVAILABLE',
|
|
14
|
+
'NOT_CONFIGURED',
|
|
15
|
+
'NOT_RUN',
|
|
16
|
+
'STALE',
|
|
17
|
+
'ERROR',
|
|
18
|
+
]);
|