@holdyourvoice/hyv 3.6.0 → 4.0.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 +93 -154
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +23 -33
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +88 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/io.js +141 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +45 -27
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +77 -71
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +10 -19
- package/dist/mcp.js +2 -267
- package/dist/pipeline.js +18 -105
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +18 -52
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +73 -98
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/dist/voice-dna.js
CHANGED
|
@@ -16,12 +16,18 @@ function top(items, limit) {
|
|
|
16
16
|
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
17
17
|
return [...counts].sort((left, right) => right[1] - left[1]).slice(0, limit).map(([item]) => item);
|
|
18
18
|
}
|
|
19
|
+
function measureWriting(text, tokens) {
|
|
20
|
+
const mapped = sentences(text);
|
|
21
|
+
return { text, mapped, tokens: tokens ?? words(text), sentenceWords: mapped.map((sentence) => words(sentence.text)) };
|
|
22
|
+
}
|
|
19
23
|
export function profileMetrics(text) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
24
|
+
return metricsFor(measureWriting(text));
|
|
25
|
+
}
|
|
26
|
+
function metricsFor(writing) {
|
|
27
|
+
const { text, mapped: draftSentences, tokens: draftWords, sentenceWords } = writing;
|
|
28
|
+
const lengths = sentenceWords.map((tokens) => tokens.length);
|
|
29
|
+
const starters = sentenceWords.map((tokens) => tokens[0]?.toLowerCase() ?? '');
|
|
30
|
+
const structures = sentenceWords.map((tokens) => tokens.slice(0, 3).map((word) => word.toLowerCase()).join(' '));
|
|
25
31
|
const lowerWords = draftWords.map((word) => word.toLowerCase());
|
|
26
32
|
const nonStop = lowerWords.filter((word) => word.length > 3 && !STOP_WORDS.has(word));
|
|
27
33
|
const first = lowerWords.filter((word) => ['i', 'we', 'my', 'our', 'us'].includes(word)).length;
|
|
@@ -52,17 +58,18 @@ export function profileMetrics(text) {
|
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
export function measureFounderFingerprint(text) {
|
|
55
|
-
|
|
61
|
+
return fingerprintFor(measureWriting(text, words(text)));
|
|
62
|
+
}
|
|
63
|
+
function fingerprintFor(writing) {
|
|
64
|
+
const { text, mapped: draftSentences, tokens: draftWords, sentenceWords } = writing;
|
|
56
65
|
const wordDenominator = Math.max(1, draftWords.length);
|
|
57
66
|
const contractionCount = draftWords.filter((word) => CONTRACTIONS.has(word.toLowerCase().replaceAll('’', "'"))).length;
|
|
58
|
-
const draftSentences = sentences(text);
|
|
59
67
|
const sentenceDenominator = Math.max(1, draftSentences.length);
|
|
60
68
|
const buckets = { short: 0, medium: 0, long: 0 };
|
|
61
|
-
for (const
|
|
62
|
-
|
|
63
|
-
if (length <= 8)
|
|
69
|
+
for (const tokens of sentenceWords) {
|
|
70
|
+
if (tokens.length <= 8)
|
|
64
71
|
buckets.short += 1;
|
|
65
|
-
else if (length <= 20)
|
|
72
|
+
else if (tokens.length <= 20)
|
|
66
73
|
buckets.medium += 1;
|
|
67
74
|
else
|
|
68
75
|
buckets.long += 1;
|
|
@@ -81,16 +88,21 @@ export function measureFounderFingerprint(text) {
|
|
|
81
88
|
enDashRate: (text.match(/–/g) ?? []).length / wordDenominator,
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
function measureSamples(samples, avoid) {
|
|
85
92
|
if (samples.length < 2)
|
|
86
93
|
throw new Error('Provide at least two local writing samples.');
|
|
87
94
|
if (samples.some((sample) => !words(sample).length))
|
|
88
95
|
throw new Error('Every local writing sample must contain writing.');
|
|
89
|
-
|
|
96
|
+
const writing = measureWriting(samples.join('\n\n'));
|
|
97
|
+
const profile = { version: '2', sampleCount: samples.length, metrics: metricsFor(writing), avoid };
|
|
98
|
+
return { writing, profile };
|
|
99
|
+
}
|
|
100
|
+
export function buildProfile(samples, avoid = []) {
|
|
101
|
+
return measureSamples(samples, avoid).profile;
|
|
90
102
|
}
|
|
91
103
|
export function buildProfileV3(samples, id, channel, avoid = [], tone) {
|
|
92
|
-
const base =
|
|
93
|
-
const
|
|
104
|
+
const { profile: base, writing } = measureSamples(samples, avoid);
|
|
105
|
+
const ruleAllowances = deriveRuleAllowances(samples);
|
|
94
106
|
const fixtureIds = samples.map((_, index) => 'sample.' + String(index + 1).padStart(3, '0'));
|
|
95
107
|
const unsigned = {
|
|
96
108
|
version: '3',
|
|
@@ -101,10 +113,10 @@ export function buildProfileV3(samples, id, channel, avoid = [], tone) {
|
|
|
101
113
|
avoid: base.avoid,
|
|
102
114
|
provenance: { source: 'local-author-owned-samples', rights: 'author-owned', createdAt: new Date().toISOString() },
|
|
103
115
|
rulePolicy: {},
|
|
104
|
-
...(Object.keys(
|
|
116
|
+
...(Object.keys(ruleAllowances).length ? { ruleAllowances } : {}),
|
|
105
117
|
channel,
|
|
106
118
|
...(tone ? { tone } : {}),
|
|
107
|
-
fingerprint:
|
|
119
|
+
fingerprint: fingerprintFor(writing),
|
|
108
120
|
tolerances: {
|
|
109
121
|
contractionRate: { absolute: 0.1, calibrated: false },
|
|
110
122
|
sentenceLengthDistribution: { absolute: 0.15, calibrated: false },
|
|
@@ -121,11 +133,12 @@ function finding(id, severity, sentence, excerpt, reason, suggestion) {
|
|
|
121
133
|
return { engine: 'voice_dna', id, severity, sentence, excerpt, reason, suggestion };
|
|
122
134
|
}
|
|
123
135
|
export function analyzeVoiceDna(text, profile) {
|
|
124
|
-
const
|
|
136
|
+
const writing = measureWriting(text);
|
|
137
|
+
const metrics = metricsFor(writing);
|
|
125
138
|
const findings = [];
|
|
126
139
|
const tolerance = Math.max(8, profile.metrics.sentenceVariation * 2.2);
|
|
127
|
-
for (const sentence of
|
|
128
|
-
const count =
|
|
140
|
+
for (const [index, sentence] of writing.mapped.entries()) {
|
|
141
|
+
const count = writing.sentenceWords[index].length;
|
|
129
142
|
if (Math.abs(count - profile.metrics.sentenceLength) > tolerance)
|
|
130
143
|
findings.push(finding('dna.sentence-length', 'yellow', sentence.index, sentence.text, `Sentence length (${count}) is outside the profile band around ${profile.metrics.sentenceLength}.`, 'Restore the writer’s usual sentence length where it improves clarity.'));
|
|
131
144
|
for (const banned of profile.avoid)
|
|
@@ -139,7 +152,7 @@ export function analyzeVoiceDna(text, profile) {
|
|
|
139
152
|
if (metrics.pointOfView !== profile.metrics.pointOfView && profile.metrics.pointOfView !== 'mixed')
|
|
140
153
|
findings.push(finding('dna.point-of-view', 'yellow', 1, text.slice(0, 160), `Draft point of view is ${metrics.pointOfView}; profile is ${profile.metrics.pointOfView}.`, 'Restore the writer’s normal narrative distance.'));
|
|
141
154
|
if (profile.version === '3') {
|
|
142
|
-
const measured =
|
|
155
|
+
const measured = fingerprintFor(writing);
|
|
143
156
|
const fingerprintChecks = [
|
|
144
157
|
['contraction-rate', measured.contractionRate, profile.fingerprint.contractionRate, profile.tolerances.contractionRate],
|
|
145
158
|
['sentence-length-distribution', Math.max(Math.abs(measured.sentenceLengthDistribution.short - profile.fingerprint.sentenceLengthDistribution.short), Math.abs(measured.sentenceLengthDistribution.medium - profile.fingerprint.sentenceLengthDistribution.medium), Math.abs(measured.sentenceLengthDistribution.long - profile.fingerprint.sentenceLengthDistribution.long)), 0, profile.tolerances.sentenceLengthDistribution],
|
package/dist/writing-examples.js
CHANGED
|
@@ -31,19 +31,14 @@ function validate(inputs) {
|
|
|
31
31
|
function indexExamples(inputs) {
|
|
32
32
|
const entries = inputs.flatMap((input) => sentences(input.text).map((sentence, position) => ({ source: input.basename, position, text: sentence.text, terms: tokens(sentence.text) })));
|
|
33
33
|
const postings = new Map();
|
|
34
|
-
const documentFrequency = new Map();
|
|
35
34
|
for (const [entryIndex, entry] of entries.entries()) {
|
|
36
|
-
const seen = new Set();
|
|
37
35
|
for (const term of entry.terms) {
|
|
38
36
|
const posting = postings.get(term) ?? new Map();
|
|
39
37
|
posting.set(entryIndex, (posting.get(entryIndex) ?? 0) + 1);
|
|
40
38
|
postings.set(term, posting);
|
|
41
|
-
seen.add(term);
|
|
42
39
|
}
|
|
43
|
-
for (const term of seen)
|
|
44
|
-
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
45
40
|
}
|
|
46
|
-
return { entries, postings
|
|
41
|
+
return { entries, postings };
|
|
47
42
|
}
|
|
48
43
|
/** Finds redacted local examples in supplied memory only; it never creates a corpus or index on disk. */
|
|
49
44
|
export function findWritingExamples(query, inputs, limit = 3) {
|
|
@@ -58,7 +53,7 @@ export function findWritingExamples(query, inputs, limit = 3) {
|
|
|
58
53
|
const posting = index.postings.get(term);
|
|
59
54
|
if (!posting)
|
|
60
55
|
continue;
|
|
61
|
-
const idf = Math.log(1 + (index.entries.length -
|
|
56
|
+
const idf = Math.log(1 + (index.entries.length - posting.size + 0.5) / (posting.size + 0.5));
|
|
62
57
|
for (const [entryIndex, frequency] of posting) {
|
|
63
58
|
const entry = index.entries[entryIndex];
|
|
64
59
|
const denominator = frequency + 1.2 * (1 - 0.75 + 0.75 * entry.terms.length / Math.max(1, averageLength));
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holdyourvoice/hyv",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Local writing checks, voice profiles, and verified editing workflows for humans and agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hyv": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"!dist/**/*.test.js",
|
|
11
12
|
"skills",
|
|
12
13
|
"Readme.md",
|
|
13
14
|
"LICENSE"
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"bundle:claude": "esbuild src/mcp.ts --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js",
|
|
18
19
|
"build:claude": "npm run build && npm run bundle:claude",
|
|
19
20
|
"pack:claude": "npm run build:claude && node scripts/pack-claude.mjs",
|
|
21
|
+
"pack:chatgpt-plugin": "node scripts/pack-chatgpt-plugin.mjs",
|
|
20
22
|
"scorecard": "node scripts/public-scorecard.mjs benchmarks",
|
|
21
23
|
"validate:rule-contributions": "node scripts/validate-rule-contributions.mjs",
|
|
22
24
|
"stage1:evaluate": "node scripts/evaluate-rewrite-benchmark.mjs",
|
|
@@ -25,7 +27,8 @@
|
|
|
25
27
|
"stage2:human-packet": "npm run build && node scripts/run-stage2-human-packet.mjs",
|
|
26
28
|
"test": "npm run build && node --test \"dist/**/*.test.js\"",
|
|
27
29
|
"check:release": "node scripts/release-audit.mjs",
|
|
28
|
-
"prepack": "npm run check:release && npm test && npm run pack:claude"
|
|
30
|
+
"prepack": "npm run check:release && npm test && npm run pack:claude",
|
|
31
|
+
"check:compatibility": "node scripts/verify-compatibility.mjs"
|
|
29
32
|
},
|
|
30
33
|
"engines": {
|
|
31
34
|
"node": ">=20"
|
|
@@ -19,6 +19,10 @@ hyv prepare-judgment pre-edit|post-candidate kind draft.md profile.json task.jso
|
|
|
19
19
|
- Commands that write use an explicit output path. Confirm that path before running the command.
|
|
20
20
|
- This agent describes how to invoke the command and what it returns. It does not change command behavior or exit codes.
|
|
21
21
|
|
|
22
|
+
## Word economy review
|
|
23
|
+
|
|
24
|
+
For a post-candidate form judgment, review whether every word earns its place before final verification and delivery. Name the phrase and sentence range, propose a cut, and explain why meaning, evidence, clarity, and voice survive it. Preserve necessary uncertainty, attribution, emphasis, rhythm, and transitions. Do not use a word-count target. Record unresolved cuts as findings and use ESCALATE; do not return CLEAR merely because the candidate is shorter. Report concerns outside the authorized edit scope without changing text. The local command prepares the task; it does not perform this editorial judgment.
|
|
25
|
+
|
|
22
26
|
## Handoff
|
|
23
27
|
|
|
24
28
|
Run `hyv prepare-judgment` directly to execute the operation. Follow-on agents: hyv-reduce-judgment.
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
});
|
package/dist/agents/emit.test.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
});
|
package/dist/agents/load.test.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
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-ingest', '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
|
-
});
|