@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
@@ -2,7 +2,7 @@ import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
2
2
  import { userInfo } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { parseApprovalTrustStore } from './approval-capability.js';
5
- const MAX_BYTES = 1024 * 1024;
5
+ import { MAX_JSON_BYTES as MAX_BYTES } from './internal.js';
6
6
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
7
  function readBounded(descriptor) {
8
8
  const chunks = [];
package/dist/cli.js CHANGED
@@ -13,13 +13,14 @@ import { evaluateRewriteResponse, parseRewriteTask, prepareRewriteTask } from '.
13
13
  import { parseJudgmentEnvelope, preparePostCandidateJudgment, preparePreEditJudgment, reducePostCandidate, reducePreEdit } from './judgment-task.js';
14
14
  import { evaluateRebuildResponse, parseRebuildTask, prepareRebuildTask, writerRequestForRebuild } from './rebuild-task.js';
15
15
  import { canonicalJson, parseCanonicalJson } from './canonical-json.js';
16
+ import { MAX_JSON_BYTES } from './internal.js';
16
17
  import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
17
18
  import { buildProfile } from './voice-dna.js';
18
19
  import { loadApprovalContext } from './approval-context.js';
19
20
  import { formatFactLintReport, lintFacts } from './fact-linter.js';
20
21
  import { lintLogic } from './logic-linter.js';
21
- const usage = 'Commands: profile, analyze, hygiene, inspect-hidden-text, apply-hidden-text-policy, final-check, fact-lint, logic-lint, batch-analyze, rewrite-prompt, prepare-rewrite, apply-rewrite, prepare-judgment, reduce-judgment, prepare-rebuild, rebuild-writer-request, apply-rebuild, verify, verify-spec, lifecycle, learning, patterns, mcp';
22
- const MAX_JSON_BYTES = 1024 * 1024;
22
+ import { loadAll, validateAll, validateId, sortedIds, describe, emitJson, emitPrompt } from './agents/index.js';
23
+ const usage = 'Commands: agent, profile, analyze, hygiene, inspect-hidden-text, apply-hidden-text-policy, final-check, fact-lint, logic-lint, batch-analyze, rewrite-prompt, prepare-rewrite, apply-rewrite, prepare-judgment, reduce-judgment, prepare-rebuild, rebuild-writer-request, apply-rebuild, verify, verify-spec, lifecycle, learning, patterns, mcp';
23
24
  function input(path) {
24
25
  return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
25
26
  }
@@ -269,8 +270,116 @@ function writeNewFileAtomically(path, text) {
269
270
  if (primaryError)
270
271
  throw primaryError;
271
272
  }
273
+ function agentFlags(args) {
274
+ const values = [];
275
+ let host = 'generic';
276
+ let mode;
277
+ let output;
278
+ for (let index = 0; index < args.length; index += 1) {
279
+ const argument = args[index];
280
+ if (argument === '--host') {
281
+ const value = args[index + 1];
282
+ if (!value || value.startsWith('--'))
283
+ throw new Error('Usage: hyv agent --host HOST requires a value.');
284
+ host = value;
285
+ index += 1;
286
+ }
287
+ else if (argument.startsWith('--host=')) {
288
+ const value = argument.slice('--host='.length);
289
+ if (!value)
290
+ throw new Error('Usage: hyv agent --host HOST requires a value.');
291
+ host = value;
292
+ }
293
+ else if (argument === '--mode') {
294
+ const value = args[index + 1];
295
+ if (value !== 'prompt' && value !== 'json')
296
+ throw new Error('Usage: hyv agent --mode prompt|json requires a mode.');
297
+ mode = value;
298
+ index += 1;
299
+ }
300
+ else if (argument.startsWith('--mode=')) {
301
+ const value = argument.slice('--mode='.length);
302
+ if (value !== 'prompt' && value !== 'json')
303
+ throw new Error('Usage: hyv agent --mode prompt|json requires a mode.');
304
+ mode = value;
305
+ }
306
+ else if (argument === '--output') {
307
+ const value = args[index + 1];
308
+ if (!value || value.startsWith('--'))
309
+ throw new Error('Usage: hyv agent --output FILE requires a value.');
310
+ output = value;
311
+ index += 1;
312
+ }
313
+ else if (argument.startsWith('--output=')) {
314
+ const value = argument.slice('--output='.length);
315
+ if (!value)
316
+ throw new Error('Usage: hyv agent --output FILE requires a value.');
317
+ output = value;
318
+ }
319
+ else {
320
+ values.push(argument);
321
+ }
322
+ }
323
+ return { values, host, mode, output };
324
+ }
325
+ function runAgent(args) {
326
+ const [subcommand, ...subargs] = args;
327
+ if (subcommand === 'list') {
328
+ if (subargs.length)
329
+ throw new Error('Usage: hyv agent list');
330
+ const packages = loadAll();
331
+ const ids = sortedIds(packages);
332
+ json(ids.map((id) => {
333
+ const descriptor = packages.get(id).descriptor;
334
+ return { id, role: descriptor.role, workflow_phase: descriptor.workflow_phase, description: descriptor.description };
335
+ }));
336
+ return 0;
337
+ }
338
+ if (subcommand === 'validate') {
339
+ if (subargs.length > 1)
340
+ throw new Error('Usage: hyv agent validate [id]');
341
+ const packages = loadAll();
342
+ const id = subargs[0];
343
+ if (id !== undefined)
344
+ validateId(packages, id);
345
+ validateAll(packages);
346
+ json({ schema_version: '1.0.0', status: 'PASS', agent: id ?? 'all' });
347
+ return 0;
348
+ }
349
+ if (subcommand === 'describe') {
350
+ const { values, host, mode, output } = agentFlags(subargs);
351
+ if (values.length !== 1 || mode !== undefined || output !== undefined)
352
+ throw new Error('Usage: hyv agent describe <id> [--host HOST]');
353
+ const packages = loadAll();
354
+ validateId(packages, values[0]);
355
+ json(describe(packages.get(values[0]), host));
356
+ return 0;
357
+ }
358
+ if (subcommand === 'emit') {
359
+ const { values, host, mode, output } = agentFlags(subargs);
360
+ if (values.length !== 1)
361
+ throw new Error('Usage: hyv agent emit <id> --mode prompt|json [--host HOST] [--output FILE]');
362
+ if (!mode)
363
+ throw new Error('Usage: hyv agent emit <id> --mode prompt|json [--host HOST] [--output FILE]');
364
+ const packages = loadAll();
365
+ validateId(packages, values[0]);
366
+ const pkg = packages.get(values[0]);
367
+ const body = mode === 'prompt' ? emitPrompt(pkg, host) : `${emitJson(pkg, host)}\n`;
368
+ if (output !== undefined) {
369
+ writeFileSync(output, body, { encoding: 'utf8', flag: 'wx' });
370
+ }
371
+ else {
372
+ process.stdout.write(body);
373
+ }
374
+ return 0;
375
+ }
376
+ throw new Error('Usage: hyv agent <list|validate|describe|emit> ...');
377
+ }
272
378
  export async function runCli(args) {
273
379
  const [command, ...rest] = args;
380
+ if (command === 'agent') {
381
+ return runAgent(rest);
382
+ }
274
383
  if (command === 'profile') {
275
384
  const { output, samples, avoid } = profileArguments(rest);
276
385
  writeFileSync(output, `${JSON.stringify(buildProfile(samples.map(input), avoid), null, 2)}\n`);
package/dist/cli.test.js CHANGED
@@ -556,3 +556,79 @@ test('prepares and applies an authorized rebuild through CLI', () => {
556
556
  rmSync(directory, { recursive: true, force: true });
557
557
  }
558
558
  });
559
+ test('agent list enumerates every command as one portable package', () => {
560
+ const result = run(['agent', 'list']);
561
+ assert.equal(result.status, 0, result.stderr);
562
+ const entries = JSON.parse(result.stdout);
563
+ assert.equal(entries.length, 23);
564
+ const ids = entries.map((entry) => entry.id);
565
+ assert.equal(new Set(ids).size, ids.length);
566
+ for (const id of ['hyv-profile', 'hyv-analyze', 'hyv-verify', 'hyv-mcp', 'hyv-patterns']) {
567
+ assert.ok(ids.includes(id), `missing ${id}`);
568
+ }
569
+ for (const entry of entries) {
570
+ assert.ok(entry.role);
571
+ assert.ok(entry.workflow_phase);
572
+ assert.ok(entry.description);
573
+ }
574
+ });
575
+ test('agent validate passes over the full package tree and rejects unknown ids', () => {
576
+ const result = run(['agent', 'validate']);
577
+ assert.equal(result.status, 0, result.stderr);
578
+ assert.equal(JSON.parse(result.stdout).status, 'PASS');
579
+ assert.equal(run(['agent', 'validate', 'hyv-verify']).status, 0);
580
+ const unknown = run(['agent', 'validate', 'hyv-does-not-exist']);
581
+ assert.equal(unknown.status, 1);
582
+ assert.match(unknown.stderr, /unknown agent/);
583
+ });
584
+ test('agent describe resolves a host and reports the runtime descriptor', () => {
585
+ const result = run(['agent', 'describe', 'hyv-verify', '--host', 'codex']);
586
+ assert.equal(result.status, 0, result.stderr);
587
+ const descriptor = JSON.parse(result.stdout);
588
+ assert.equal(descriptor.agent.id, 'hyv-verify');
589
+ assert.equal(descriptor.host.id, 'codex');
590
+ assert.ok(descriptor.available_capabilities.includes('read_repository'));
591
+ assert.ok(Array.isArray(descriptor.unavailable_capabilities));
592
+ });
593
+ test('agent emit writes a prompt or json contract to stdout or a named file', () => {
594
+ const prompt = run(['agent', 'emit', 'hyv-verify', '--mode=prompt', '--host=generic']);
595
+ assert.equal(prompt.status, 0, prompt.stderr);
596
+ assert.match(prompt.stdout, /# hyv-verify/);
597
+ assert.match(prompt.stdout, /Required input: original path, candidate path, profile path/);
598
+ const jsonOut = run(['agent', 'emit', 'hyv-verify', '--mode', 'json', '--host', 'generic']);
599
+ assert.equal(jsonOut.status, 0, jsonOut.stderr);
600
+ assert.equal(JSON.parse(jsonOut.stdout).agent.id, 'hyv-verify');
601
+ const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-agent-'));
602
+ try {
603
+ const target = join(directory, 'verify.json');
604
+ const emitted = run(['agent', 'emit', 'hyv-verify', '--mode', 'json', '--output', target]);
605
+ assert.equal(emitted.status, 0, emitted.stderr);
606
+ assert.equal(JSON.parse(readFileSync(target, 'utf8')).agent.id, 'hyv-verify');
607
+ }
608
+ finally {
609
+ rmSync(directory, { recursive: true, force: true });
610
+ }
611
+ });
612
+ test('agent commands locate the installed package tree outside a repository', () => {
613
+ const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-agent-cwd-'));
614
+ try {
615
+ mkdirSync(join(directory, 'skills'));
616
+ const result = spawnSync(process.execPath, [cli, 'agent', 'validate'], { cwd: directory, encoding: 'utf8' });
617
+ assert.equal(result.status, 0, result.stderr);
618
+ assert.equal(JSON.parse(result.stdout).status, 'PASS');
619
+ }
620
+ finally {
621
+ rmSync(directory, { recursive: true, force: true });
622
+ }
623
+ });
624
+ test('agent usage errors return exit 1 and never mutate', () => {
625
+ assert.equal(run(['agent']).status, 1);
626
+ assert.equal(run(['agent', 'emit', 'hyv-verify']).status, 1);
627
+ assert.equal(run(['agent', 'emit', 'hyv-verify', '--mode', 'bad']).status, 1);
628
+ assert.equal(run(['agent', 'describe']).status, 1);
629
+ assert.equal(run(['agent', 'describe', 'hyv-verify', '--mode', 'json']).status, 1);
630
+ assert.equal(run(['agent', 'describe', 'hyv-verify', '--output', 'ignored.json']).status, 1);
631
+ assert.equal(run(['agent', 'list', '--host', 'codex']).status, 1);
632
+ assert.equal(run(['agent', 'list', 'extra']).status, 1);
633
+ assert.equal(run(['agent', 'nope']).status, 1);
634
+ });
package/dist/copy-spec.js CHANGED
@@ -1,19 +1,14 @@
1
1
  import { sentences } from './text.js';
2
+ import { escaped, isText } from './internal.js';
2
3
  function normalized(value) {
3
4
  return value.toLowerCase().replace(/\s+/g, ' ').trim();
4
5
  }
5
- function escaped(value) {
6
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
- }
8
6
  function atomMatches(value, atom) {
9
7
  return new RegExp(`(?<![\\p{L}\\p{N}\\p{M}])${escaped(atom)}(?![\\p{L}\\p{N}\\p{M}])`, 'u').test(value);
10
8
  }
11
9
  function isAtom(value) {
12
10
  return isText(value, 500) && /[\p{L}\p{N}]/u.test(normalized(value));
13
11
  }
14
- function isText(value, limit) {
15
- return typeof value === 'string' && value.trim().length > 0 && value.length <= limit;
16
- }
17
12
  function isClaim(value) {
18
13
  if (!value || typeof value !== 'object')
19
14
  return false;
@@ -1,9 +1,7 @@
1
1
  import { paragraphs, sentences, words } from './text.js';
2
+ import { isText } from './internal.js';
2
3
  const formats = ['general', 'social', 'deck', 'outreach', 'blog', 'audit', 'website'];
3
4
  const evidenceStatuses = ['primary', 'attributed', 'internal', 'unverified'];
4
- function isText(value, limit) {
5
- return typeof value === 'string' && value.trim().length > 0 && value.length <= limit;
6
- }
7
5
  function isTerms(value) {
8
6
  return Array.isArray(value) && value.length <= 100 && value.every((term) => isText(term, 200));
9
7
  }
@@ -1,10 +1,8 @@
1
- import { createHash } from 'node:crypto';
2
- import { canonicalJson } from './canonical-json.js';
1
+ import { fingerprint as hash } from './internal.js';
3
2
  const ACKNOWLEDGEMENT = 'Removes only listed non-semantic controls; all other findings remain review-only.';
4
3
  export const minimalHiddenTextPolicy = {
5
4
  version: '1', name: 'minimal-text-control-cleanup', approvedRemovals: ['ascii_control', 'mid_document_bom'], acknowledgement: ACKNOWLEDGEMENT,
6
5
  };
7
- function hash(value) { return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex'); }
8
6
  function codepoint(value) { return `U+${value.toString(16).toUpperCase().padStart(4, '0')}`; }
9
7
  export function parseHiddenTextPolicy(value) {
10
8
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -50,15 +48,7 @@ export function inspectHiddenText(text, policy = minimalHiddenTextPolicy) {
50
48
  }
51
49
  export function applyHiddenTextPolicy(text, policy = minimalHiddenTextPolicy) {
52
50
  const report = inspectHiddenText(text, policy);
53
- const offsets = new Set(report.proposedChanges.map((item) => item.offset));
54
- let output = '';
55
- for (let offset = 0; offset < text.length;) {
56
- const value = text.codePointAt(offset);
57
- const character = String.fromCodePoint(value);
58
- if (!offsets.has(offset))
59
- output += character;
60
- offset += character.length;
61
- }
51
+ const output = applyOnce(text, policy);
62
52
  const remaining = inspectHiddenText(output, policy).findings;
63
53
  const again = applyOnce(output, policy);
64
54
  return { ...report, outputHash: hash(output), output, remaining, idempotent: again === output };
@@ -0,0 +1,30 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.js';
3
+ export const MAX_JSON_BYTES = 1024 * 1024;
4
+ export function sha256(value) {
5
+ return createHash('sha256').update(value).digest('hex');
6
+ }
7
+ export function digestCanonical(value) {
8
+ return sha256(canonicalJson(value));
9
+ }
10
+ export function fingerprint(value) {
11
+ return sha256(typeof value === 'string' ? value : canonicalJson(value));
12
+ }
13
+ export function profileIdentity(profile) {
14
+ if (profile.version === '3')
15
+ return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
16
+ const legacy = `legacy-v2:${digestCanonical(profile)}`;
17
+ return { profileId: legacy, profileRevisionDigest: legacy };
18
+ }
19
+ export function isPlainObject(value) {
20
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
21
+ }
22
+ export function exactKeys(value, required, optional = []) {
23
+ return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
24
+ }
25
+ export function isText(value, maximum = 256) {
26
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= maximum;
27
+ }
28
+ export function escaped(value) {
29
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }
@@ -1,21 +1,9 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { sentences } from './text.js';
4
3
  import { HYV_VERSION } from './version.js';
4
+ import { profileIdentity, sha256 as digest } from './internal.js';
5
5
  const PRE_EDIT_KINDS = ['triage', 'argument', 'form'];
6
6
  const POST_CANDIDATE_KINDS = ['argument', 'polarity', 'form', 'flatness', 'semantic'];
7
- function digest(value) {
8
- return createHash('sha256').update(value).digest('hex');
9
- }
10
- function digestCanonical(value) {
11
- return digest(canonicalJson(value));
12
- }
13
- function profileIdentity(profile) {
14
- if (profile.version === '3')
15
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
16
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
17
- return { profileId: legacy, profileRevisionDigest: legacy };
18
- }
19
7
  function fingerprintTask(task) {
20
8
  return digest(`hyv:judgment-task:v1\0${canonicalJson(task)}`);
21
9
  }
@@ -3,6 +3,7 @@ import { verifyApprovalCapability } from './approval-capability.js';
3
3
  import { createInitialLifecycleArtifact, isValidLifecycleArtifact, parseSemanticVerdict, prepareSemanticReviewTask, reduceRewriteLifecycle } from './semantic-review.js';
4
4
  import { verifyDeterministically } from './pipeline.js';
5
5
  import { recordVerifiedCandidate } from './learning.js';
6
+ import { MAX_JSON_BYTES } from './internal.js';
6
7
  export function prepareLifecycle(deterministic, binding, receipt, policy, allowedViolations) {
7
8
  const task = prepareSemanticReviewTask(binding, policy, receipt, allowedViolations);
8
9
  return { task, artifact: createInitialLifecycleArtifact(task, deterministic) };
@@ -58,7 +59,7 @@ export function finalizeLifecycle(artifact, decision, context, capability) {
58
59
  }
59
60
  export function recordApprovedLearning(request) {
60
61
  const { ready, approved, decision, capability, source, candidate, profile, context, copySpec, writingBrief } = request;
61
- if (Buffer.byteLength(source, 'utf8') > 1024 * 1024 || Buffer.byteLength(candidate, 'utf8') > 1024 * 1024)
62
+ if (Buffer.byteLength(source, 'utf8') > MAX_JSON_BYTES || Buffer.byteLength(candidate, 'utf8') > MAX_JSON_BYTES)
62
63
  throw new Error('Approved learning text exceeds the byte limit.');
63
64
  inspectLifecycle(ready);
64
65
  inspectLifecycle(approved);
package/dist/mcp-tools.js CHANGED
@@ -12,6 +12,7 @@ import { inspectHiddenText, applyHiddenTextPolicy, parseHiddenTextPolicy } from
12
12
  import { buildProfile } from './voice-dna.js';
13
13
  import { finalOutputCheck, inspectHygiene } from './hygiene.js';
14
14
  import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
15
+ import { MAX_JSON_BYTES } from './internal.js';
15
16
  function profileFromJson(profileJson) {
16
17
  try {
17
18
  return parseProfile(JSON.parse(profileJson));
@@ -103,7 +104,7 @@ export function logicLintForMcp(draft, writingBriefJson) {
103
104
  return lintLogic(draft, writingBriefFromJson(writingBriefJson));
104
105
  }
105
106
  function parsed(json, label) {
106
- if (Buffer.byteLength(json, 'utf8') > 1024 * 1024)
107
+ if (Buffer.byteLength(json, 'utf8') > MAX_JSON_BYTES)
107
108
  throw new Error(`${label} exceeds the byte limit.`);
108
109
  try {
109
110
  return JSON.parse(json);
package/dist/pipeline.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { HYV_VERSION } from './version.js';
4
3
  import { analyzeAiEditor } from './ai-editor.js';
@@ -6,10 +5,11 @@ import { verifyClaims } from './copy-spec.js';
6
5
  import { analyzeEditorial } from './editorial-packs.js';
7
6
  import { finalOutputCheck, inspectHygiene } from './hygiene.js';
8
7
  import { analyzeVoiceDna } from './voice-dna.js';
9
- import { legacySetPreservation } from './preservation.js';
8
+ import { legacySetPreservation, LEGACY_SET_PRESERVATION_VERSION } from './preservation.js';
10
9
  import { lintFacts } from './fact-linter.js';
11
10
  import { lintLogic } from './logic-linter.js';
12
11
  import { sentences } from './text.js';
12
+ import { digestCanonical, escaped as escapeRegex, profileIdentity, sha256 as digest } from './internal.js';
13
13
  export function analyze(text, profile, brief) {
14
14
  const voiceDna = analyzeVoiceDna(text, profile);
15
15
  const aiEditor = analyzeAiEditor(text, profile);
@@ -101,7 +101,7 @@ function verifyRequiredFacts(candidate, brief) {
101
101
  const reversed = brief.requiredFacts.filter((fact) => {
102
102
  const terms = fact.atoms?.length ? fact.atoms : [fact.text];
103
103
  return terms.some((term) => {
104
- const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ const escaped = escapeRegex(term);
105
105
  const quotedDenial = new RegExp(`${escaped}(?:["']|\\s)*(?:is|was|are|were)?\\s*(?:not|false|untrue|incorrect)`, 'i');
106
106
  return quotedDenial.test(candidate) || draftSentences.some((sentence, index) => sentence.text.toLowerCase().includes(term.toLowerCase()) && (/\b(?:not|false|untrue|incorrect)\b/i.test(sentence.text) || /^(?:that|this) (?:statement|claim|fact|assertion|point) (?:is|was) (?:not|false|untrue|incorrect)\b/i.test(draftSentences[index + 1]?.text.trim() ?? '')));
107
107
  });
@@ -153,19 +153,11 @@ export function verifyRebuildWithCopySpec(original, candidate, profile, spec, br
153
153
  passed: checked.passed && !regressions.some(isBlockingFinding) && claims.passed && finalCheck.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
154
154
  };
155
155
  }
156
- function digest(value) { return createHash('sha256').update(value).digest('hex'); }
157
- function digestCanonical(value) { return digest(canonicalJson(value)); }
158
- function profileIdentity(profile) {
159
- if (profile.version === '3')
160
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
161
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
162
- return { profileId: legacy, profileRevisionDigest: legacy };
163
- }
164
156
  function projectDeterministicVerificationArtifact(source, candidate, profile, verification, copySpec, writingBrief, verificationKind = 'claims' in verification ? 'copy_spec' : 'standard') {
165
157
  const identity = profileIdentity(profile);
166
158
  const base = {
167
159
  version: '1', verificationKind, passed: verification.passed, analysisVersion: verification.candidate.version,
168
- rulesetVersion: HYV_VERSION, preservationMetricVersion: 'legacy-set-v1', preservationScore: verification.preservationScore,
160
+ rulesetVersion: HYV_VERSION, preservationMetricVersion: LEGACY_SET_PRESERVATION_VERSION, preservationScore: verification.preservationScore,
169
161
  sourceHash: digest(source), candidateHash: digest(candidate), ...identity,
170
162
  ...(copySpec ? { copySpecHash: digestCanonical(copySpec) } : {}), ...(writingBrief ? { writingBriefHash: digestCanonical(writingBrief) } : {}),
171
163
  regressionKeys: verification.regressions.map((finding) => `${finding.engine}:${finding.id}:${finding.sentence}`).sort(),
package/dist/profile.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.js';
3
+ import { isPlainObject } from './internal.js';
3
4
  const METRICS_KEYS = ['sentenceLength', 'sentenceVariation', 'sentenceStructure', 'rhythm', 'paragraphLength', 'openingMoves', 'vocabulary', 'lexicalDensity', 'pointOfView', 'punctuation', 'caseStyle', 'questionRate', 'transitions'];
4
5
  const PROFILE_V3_KEYS = ['version', 'id', 'revision', 'revisionDigest', 'sampleCount', 'metrics', 'avoid', 'provenance', 'rulePolicy', 'fingerprint', 'tolerances', 'metricFixtures'];
5
6
  const FINGERPRINT_METRICS = ['contractionRate', 'sentenceLengthDistribution', 'bulletRate', 'enDashRate'];
6
7
  const STABLE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,127})$/;
7
- function isPlainObject(value) {
8
- return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
9
- }
10
8
  function hasKnownKeys(value, keys) {
11
9
  return Object.keys(value).every((key) => keys.includes(key)) && keys.every((key) => key in value);
12
10
  }
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { canonicalJson } from './canonical-json.js';
3
2
  import { parseCopySpec } from './copy-spec.js';
4
3
  import { parseWritingBrief } from './editorial-packs.js';
@@ -10,29 +9,15 @@ import { sentences } from './text.js';
10
9
  import { HYV_VERSION } from './version.js';
11
10
  import { buildRecompositionBrief, measureLexicalResidual, parseRecompositionPolicy } from './recomposition.js';
12
11
  import { provenanceStatusForRebuild, writerRequestForRebuild } from './provenance-status.js';
12
+ import { fingerprint, profileIdentity, sha256 as digest } from './internal.js';
13
13
  const MAX_RESPONSE_BYTES = 100_000;
14
14
  const MAX_CANDIDATE_CHARACTERS = 100_000;
15
- function fingerprint(value) {
16
- return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
17
- }
18
- function digest(value) {
19
- return createHash('sha256').update(value).digest('hex');
20
- }
21
- function digestCanonical(value) {
22
- return digest(canonicalJson(value));
23
- }
24
15
  function failure(code, message, path) {
25
16
  return { code, message, ...(path ? { path } : {}) };
26
17
  }
27
18
  function isFailure(value) {
28
19
  return typeof value === 'object' && value !== null && 'code' in value && 'message' in value;
29
20
  }
30
- function profileIdentity(profile) {
31
- if (profile.version === '3')
32
- return { profileId: profile.id, profileRevisionDigest: profile.revisionDigest };
33
- const legacy = `legacy-v2:${digestCanonical(profile)}`;
34
- return { profileId: legacy, profileRevisionDigest: legacy };
35
- }
36
21
  function parseJson(value) {
37
22
  if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
38
23
  return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
@@ -174,7 +174,7 @@ test('CLI and MCP rebuild helpers share fingerprints', () => {
174
174
  version: '1', audience: 'operators', intent: 'explain', format: 'outreach',
175
175
  });
176
176
  assert.match(briefTask.prompt, /# WritingBrief/);
177
- assert.equal(HYV_VERSION, '3.3.4');
177
+ assert.equal(HYV_VERSION, '3.4.0');
178
178
  });
179
179
  test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
180
180
  const reduction = rebuildRecommendation();
@@ -23,7 +23,7 @@ function fixture(files) {
23
23
  const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-audit-'));
24
24
  execFileSync('git', ['init', '--quiet'], { cwd: directory });
25
25
  const defaults = {
26
- 'package.json': JSON.stringify({ name: 'audit-fixture', license: 'MIT', files: ['dist', 'Readme.md', 'LICENSE'], scripts: { 'stage1:evaluate': 'node scripts/evaluate-rewrite-benchmark.mjs', 'stage1:dry-run': 'npm run build && node scripts/run-stage1-dry-run.mjs', 'stage1:human-packet': 'npm run build && node scripts/run-stage1-human-packet.mjs', 'stage2:human-packet': 'npm run build && node scripts/run-stage2-human-packet.mjs' }, version: '1.0.0', type: 'module', bin: { hyv: 'dist/cli.js' }, engines: { node: '>=20' } }),
26
+ 'package.json': JSON.stringify({ name: 'audit-fixture', license: 'MIT', files: ['dist', 'skills', 'Readme.md', 'LICENSE'], scripts: { 'stage1:evaluate': 'node scripts/evaluate-rewrite-benchmark.mjs', 'stage1:dry-run': 'npm run build && node scripts/run-stage1-dry-run.mjs', 'stage1:human-packet': 'npm run build && node scripts/run-stage1-human-packet.mjs', 'stage2:human-packet': 'npm run build && node scripts/run-stage2-human-packet.mjs' }, version: '1.0.0', type: 'module', bin: { hyv: 'dist/cli.js' }, engines: { node: '>=20' } }),
27
27
  'mcpb/manifest.json': JSON.stringify({ version: '1.0.0' }),
28
28
  'claude-plugin/.claude-plugin/plugin.json': JSON.stringify({ version: '1.0.0' }),
29
29
  '.claude-plugin/marketplace.json': JSON.stringify({ plugins: [{ name: 'hold-your-voice', version: '1.0.0' }] }),
@@ -36,6 +36,9 @@ function fixture(files) {
36
36
  ].join('\n'),
37
37
  'src/version.ts': "export const HYV_VERSION = '1.0.0';",
38
38
  'src/stage1-evaluation.ts': "const baseline = '4e6269121d551c008a34db73077e1e4fea41b3f9'; const stage1 = '550ea24f652291dca13757fdbd2f0fa0b5e3f621';",
39
+ 'skills/hyv-test/agent.json': '{}',
40
+ 'skills/hyv-test/SKILL.md': '# test',
41
+ 'skills/hyv-test/agents/openai.yaml': 'name: test',
39
42
  ...Object.fromEntries(stage1Files.map((file) => [file, '{}'])),
40
43
  };
41
44
  for (const [file, text] of Object.entries({ ...defaults, ...files })) {
@@ -146,6 +149,7 @@ test('requires the npm package runtime contract', () => {
146
149
  const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
147
150
  assert.notEqual(result.status, 0);
148
151
  assert.match(result.stderr, /npm package must include dist/);
152
+ assert.match(result.stderr, /npm package must include skills/);
149
153
  assert.match(result.stderr, /npm package must include Readme\.md/);
150
154
  assert.match(result.stderr, /npm package must use the ESM runtime contract/);
151
155
  assert.match(result.stderr, /npm hyv binary must point to dist\/cli\.js/);
@@ -1,15 +1,11 @@
1
- import { createHash } from 'node:crypto';
2
- import { canonicalJson } from './canonical-json.js';
3
1
  import { parseWritingBrief } from './editorial-packs.js';
4
2
  import { finalOutputCheck, hygieneSourceFindings } from './hygiene.js';
5
3
  import { analyze, deriveEditScope, renderRewritePrompt, verifyDeterministically } from './pipeline.js';
6
4
  import { sentences } from './text.js';
5
+ import { fingerprint } from './internal.js';
7
6
  const MAX_RESPONSE_BYTES = 100_000;
8
7
  const MAX_REPLACEMENTS = 100;
9
8
  const MAX_REPLACEMENT_CHARACTERS = 10_000;
10
- function fingerprint(value) {
11
- return createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
12
- }
13
9
  function failure(code, message, path) {
14
10
  return { code, message, ...(path ? { path } : {}) };
15
11
  }
@@ -1,13 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.js';
3
3
  import { verifyApprovalCapability } from './approval-capability.js';
4
+ import { exactKeys as exact, isPlainObject as plain } from './internal.js';
4
5
  const violations = new Set(['action_change', 'dropped_object', 'unsupported_claim', 'constraint_weakened', 'clarity_regression']);
5
6
  const HEX = /^[a-f0-9]{64}$/;
6
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
8
  function hash(domain, value) { return createHash('sha256').update(`${domain}\0`).update(canonicalJson(value)).digest('hex'); }
8
9
  function same(left, right) { return canonicalJson(left) === canonicalJson(right); }
9
- function exact(value, required, optional = []) { return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key)); }
10
- function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
11
10
  function bindingValid(binding) {
12
11
  return binding.schemaVersion === '1' && [binding.rewriteTaskFingerprint, binding.rewriteResponseFingerprint, binding.deterministicArtifactFingerprint, binding.sourceHash, binding.candidateHash].every((item) => HEX.test(item))
13
12
  && ID.test(binding.profileId) && ID.test(binding.profileRevisionDigest) && ID.test(binding.rulesetVersion);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const HYV_VERSION = '3.3.4';
1
+ export const HYV_VERSION = '3.4.0';
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "3.3.4",
3
+ "version": "3.4.0",
4
4
  "description": "A local-first dual-engine writing gate that protects voice and catches generic AI patterns.",
5
5
  "type": "module",
6
6
  "bin": { "hyv": "dist/cli.js" },
7
- "files": ["dist", "Readme.md", "LICENSE"],
8
- "scripts": { "build": "tsc -p tsconfig.json", "bundle:claude": "esbuild src/mcp.ts --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js", "build:claude": "npm run build && npm run bundle:claude", "pack:claude": "npm run build:claude && node scripts/pack-claude.mjs", "stage1:evaluate": "node scripts/evaluate-rewrite-benchmark.mjs", "stage1:dry-run": "npm run build && node scripts/run-stage1-dry-run.mjs", "stage1:human-packet": "npm run build && node scripts/run-stage1-human-packet.mjs", "stage2:human-packet": "npm run build && node scripts/run-stage2-human-packet.mjs", "test": "npm run build && node --test dist/**/*.test.js", "check:release": "node scripts/release-audit.mjs", "prepack": "npm run check:release && npm test" },
7
+ "files": ["dist", "skills", "Readme.md", "LICENSE"],
8
+ "scripts": { "build": "tsc -p tsconfig.json", "bundle:claude": "esbuild src/mcp.ts --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js", "build:claude": "npm run build && npm run bundle:claude", "pack:claude": "npm run build:claude && node scripts/pack-claude.mjs", "stage1:evaluate": "node scripts/evaluate-rewrite-benchmark.mjs", "stage1:dry-run": "npm run build && node scripts/run-stage1-dry-run.mjs", "stage1:human-packet": "npm run build && node scripts/run-stage1-human-packet.mjs", "stage2:human-packet": "npm run build && node scripts/run-stage2-human-packet.mjs", "test": "npm run build && node --test \"dist/**/*.test.js\"", "check:release": "node scripts/release-audit.mjs", "prepack": "npm run check:release && npm test" },
9
9
  "engines": { "node": ">=20" },
10
10
  "license": "MIT",
11
11
  "keywords": ["ai-writing", "cli", "editing", "voice", "writing"],
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: hyv-analyze
3
+ description: Run separate VoiceDNA and AI Editor checks plus a non-scoring Unicode hygiene inspection against a draft.
4
+ ---
5
+
6
+ # hyv-analyze
7
+
8
+ Run independent VoiceDNA and AI Editor reports plus a separate non-scoring Unicode hygiene report against a draft using a portable profile. An optional WritingBrief adds local audience, intent, and format context without changing the profile. Analysis is read-only; it never rewrites the draft or calls a model.
9
+
10
+ ## Usage
11
+
12
+ ```text
13
+ hyv analyze draft.md profile.json [writing-brief.json]
14
+ ```
15
+
16
+ ## Behavior
17
+
18
+ - Deterministic and local-first: this command never calls a provider and never sends drafts, samples, profiles, or telemetry to a service.
19
+ - Commands that write use an explicit output path. Confirm that path before running the command.
20
+ - This agent describes how to invoke the command and what it returns. It does not change command behavior or exit codes.
21
+
22
+ ## Handoff
23
+
24
+ Run `hyv analyze` directly to execute the operation. Follow-on agents: hyv-rewrite-prompt, hyv-prepare-rewrite.