@holdyourvoice/hyv 3.3.4 → 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.
Files changed (98) hide show
  1. package/Readme.md +85 -365
  2. package/dist/agents/catalog.js +118 -0
  3. package/dist/agents/catalog.test.js +60 -0
  4. package/dist/agents/emit.js +42 -0
  5. package/dist/agents/emit.test.js +64 -0
  6. package/dist/agents/index.js +4 -0
  7. package/dist/agents/load.js +184 -0
  8. package/dist/agents/load.test.js +149 -0
  9. package/dist/agents/types.js +18 -0
  10. package/dist/approval-capability.js +1 -4
  11. package/dist/approval-context.js +1 -1
  12. package/dist/cli.js +111 -2
  13. package/dist/cli.test.js +76 -0
  14. package/dist/copy-spec.js +1 -6
  15. package/dist/editorial-packs.js +1 -3
  16. package/dist/hidden-text.js +2 -12
  17. package/dist/internal.js +30 -0
  18. package/dist/judgment-task.js +1 -13
  19. package/dist/lifecycle-adapter.js +2 -1
  20. package/dist/mcp-tools.js +2 -1
  21. package/dist/pipeline.js +4 -12
  22. package/dist/profile.js +1 -3
  23. package/dist/rebuild-task.js +1 -16
  24. package/dist/rebuild-task.test.js +1 -1
  25. package/dist/release-audit.test.js +5 -1
  26. package/dist/rewrite-task.js +1 -5
  27. package/dist/semantic-review.js +1 -2
  28. package/dist/version.js +1 -1
  29. package/package.json +3 -3
  30. package/skills/hyv-analyze/SKILL.md +24 -0
  31. package/skills/hyv-analyze/agent.json +49 -0
  32. package/skills/hyv-analyze/agents/openai.yaml +4 -0
  33. package/skills/hyv-apply-hidden-text-policy/SKILL.md +24 -0
  34. package/skills/hyv-apply-hidden-text-policy/agent.json +49 -0
  35. package/skills/hyv-apply-hidden-text-policy/agents/openai.yaml +4 -0
  36. package/skills/hyv-apply-rebuild/SKILL.md +24 -0
  37. package/skills/hyv-apply-rebuild/agent.json +49 -0
  38. package/skills/hyv-apply-rebuild/agents/openai.yaml +4 -0
  39. package/skills/hyv-apply-rewrite/SKILL.md +24 -0
  40. package/skills/hyv-apply-rewrite/agent.json +48 -0
  41. package/skills/hyv-apply-rewrite/agents/openai.yaml +4 -0
  42. package/skills/hyv-batch-analyze/SKILL.md +24 -0
  43. package/skills/hyv-batch-analyze/agent.json +45 -0
  44. package/skills/hyv-batch-analyze/agents/openai.yaml +4 -0
  45. package/skills/hyv-fact-lint/SKILL.md +24 -0
  46. package/skills/hyv-fact-lint/agent.json +50 -0
  47. package/skills/hyv-fact-lint/agents/openai.yaml +4 -0
  48. package/skills/hyv-final-check/SKILL.md +24 -0
  49. package/skills/hyv-final-check/agent.json +43 -0
  50. package/skills/hyv-final-check/agents/openai.yaml +4 -0
  51. package/skills/hyv-hygiene/SKILL.md +24 -0
  52. package/skills/hyv-hygiene/agent.json +50 -0
  53. package/skills/hyv-hygiene/agents/openai.yaml +4 -0
  54. package/skills/hyv-inspect-hidden-text/SKILL.md +24 -0
  55. package/skills/hyv-inspect-hidden-text/agent.json +48 -0
  56. package/skills/hyv-inspect-hidden-text/agents/openai.yaml +4 -0
  57. package/skills/hyv-learning/SKILL.md +24 -0
  58. package/skills/hyv-learning/agent.json +48 -0
  59. package/skills/hyv-learning/agents/openai.yaml +4 -0
  60. package/skills/hyv-lifecycle/SKILL.md +24 -0
  61. package/skills/hyv-lifecycle/agent.json +48 -0
  62. package/skills/hyv-lifecycle/agents/openai.yaml +4 -0
  63. package/skills/hyv-logic-lint/SKILL.md +24 -0
  64. package/skills/hyv-logic-lint/agent.json +47 -0
  65. package/skills/hyv-logic-lint/agents/openai.yaml +4 -0
  66. package/skills/hyv-mcp/SKILL.md +24 -0
  67. package/skills/hyv-mcp/agent.json +43 -0
  68. package/skills/hyv-mcp/agents/openai.yaml +4 -0
  69. package/skills/hyv-patterns/SKILL.md +24 -0
  70. package/skills/hyv-patterns/agent.json +43 -0
  71. package/skills/hyv-patterns/agents/openai.yaml +4 -0
  72. package/skills/hyv-prepare-judgment/SKILL.md +24 -0
  73. package/skills/hyv-prepare-judgment/agent.json +52 -0
  74. package/skills/hyv-prepare-judgment/agents/openai.yaml +4 -0
  75. package/skills/hyv-prepare-rebuild/SKILL.md +24 -0
  76. package/skills/hyv-prepare-rebuild/agent.json +54 -0
  77. package/skills/hyv-prepare-rebuild/agents/openai.yaml +4 -0
  78. package/skills/hyv-prepare-rewrite/SKILL.md +24 -0
  79. package/skills/hyv-prepare-rewrite/agent.json +51 -0
  80. package/skills/hyv-prepare-rewrite/agents/openai.yaml +4 -0
  81. package/skills/hyv-profile/SKILL.md +24 -0
  82. package/skills/hyv-profile/agent.json +50 -0
  83. package/skills/hyv-profile/agents/openai.yaml +4 -0
  84. package/skills/hyv-rebuild-writer-request/SKILL.md +24 -0
  85. package/skills/hyv-rebuild-writer-request/agent.json +47 -0
  86. package/skills/hyv-rebuild-writer-request/agents/openai.yaml +4 -0
  87. package/skills/hyv-reduce-judgment/SKILL.md +24 -0
  88. package/skills/hyv-reduce-judgment/agent.json +45 -0
  89. package/skills/hyv-reduce-judgment/agents/openai.yaml +4 -0
  90. package/skills/hyv-rewrite-prompt/SKILL.md +24 -0
  91. package/skills/hyv-rewrite-prompt/agent.json +48 -0
  92. package/skills/hyv-rewrite-prompt/agents/openai.yaml +4 -0
  93. package/skills/hyv-verify/SKILL.md +24 -0
  94. package/skills/hyv-verify/agent.json +49 -0
  95. package/skills/hyv-verify/agents/openai.yaml +4 -0
  96. package/skills/hyv-verify-spec/SKILL.md +24 -0
  97. package/skills/hyv-verify-spec/agent.json +50 -0
  98. package/skills/hyv-verify-spec/agents/openai.yaml +4 -0
@@ -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
+ }
@@ -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
+ ]);
@@ -1,14 +1,11 @@
1
1
  import { createHash, createPublicKey, verify } from 'node:crypto';
2
2
  import { parseCanonicalJson } from './canonical-json.js';
3
+ import { exactKeys, isPlainObject as plain } from './internal.js';
3
4
  const DIGEST = /^[a-f0-9]{64}$/;
4
5
  const BASE64URL = /^[A-Za-z0-9_-]+$/;
5
6
  const CLAIM_KEYS = ['version', 'purpose', 'issuer', 'audience', 'subjectArtifactFingerprint', 'sourceHash', 'candidateHash', 'profileId', 'profileRevisionDigest', 'keyId', 'issuedAt', 'notBefore', 'expiresAt', 'nonce'];
6
7
  const STORE_KEYS = ['version', 'audience', 'maxCapabilityLifetimeSeconds', 'keys'];
7
8
  function fail(error) { return { ok: false, error }; }
8
- function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
9
- function exactKeys(value, required, optional = []) {
10
- return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
11
- }
12
9
  function bounded(value) { return typeof value === 'string' && value.length > 0 && value.length <= 128; }
13
10
  function safeTime(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
14
11
  function validClaims(value) {