@holdyourvoice/hyv 3.3.5 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/Readme.md +66 -116
  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/cli.js +464 -317
  11. package/dist/cli.test.js +76 -0
  12. package/dist/fact-linter.js +107 -98
  13. package/dist/mcp.js +43 -192
  14. package/dist/rebuild-task.test.js +1 -1
  15. package/dist/release-audit.test.js +5 -1
  16. package/dist/stage1-evaluation.js +74 -48
  17. package/dist/version.js +1 -1
  18. package/package.json +3 -3
  19. package/skills/hyv-analyze/SKILL.md +24 -0
  20. package/skills/hyv-analyze/agent.json +49 -0
  21. package/skills/hyv-analyze/agents/openai.yaml +4 -0
  22. package/skills/hyv-apply-hidden-text-policy/SKILL.md +24 -0
  23. package/skills/hyv-apply-hidden-text-policy/agent.json +49 -0
  24. package/skills/hyv-apply-hidden-text-policy/agents/openai.yaml +4 -0
  25. package/skills/hyv-apply-rebuild/SKILL.md +24 -0
  26. package/skills/hyv-apply-rebuild/agent.json +49 -0
  27. package/skills/hyv-apply-rebuild/agents/openai.yaml +4 -0
  28. package/skills/hyv-apply-rewrite/SKILL.md +24 -0
  29. package/skills/hyv-apply-rewrite/agent.json +48 -0
  30. package/skills/hyv-apply-rewrite/agents/openai.yaml +4 -0
  31. package/skills/hyv-batch-analyze/SKILL.md +24 -0
  32. package/skills/hyv-batch-analyze/agent.json +45 -0
  33. package/skills/hyv-batch-analyze/agents/openai.yaml +4 -0
  34. package/skills/hyv-fact-lint/SKILL.md +24 -0
  35. package/skills/hyv-fact-lint/agent.json +50 -0
  36. package/skills/hyv-fact-lint/agents/openai.yaml +4 -0
  37. package/skills/hyv-final-check/SKILL.md +24 -0
  38. package/skills/hyv-final-check/agent.json +43 -0
  39. package/skills/hyv-final-check/agents/openai.yaml +4 -0
  40. package/skills/hyv-hygiene/SKILL.md +24 -0
  41. package/skills/hyv-hygiene/agent.json +50 -0
  42. package/skills/hyv-hygiene/agents/openai.yaml +4 -0
  43. package/skills/hyv-inspect-hidden-text/SKILL.md +24 -0
  44. package/skills/hyv-inspect-hidden-text/agent.json +48 -0
  45. package/skills/hyv-inspect-hidden-text/agents/openai.yaml +4 -0
  46. package/skills/hyv-learning/SKILL.md +24 -0
  47. package/skills/hyv-learning/agent.json +48 -0
  48. package/skills/hyv-learning/agents/openai.yaml +4 -0
  49. package/skills/hyv-lifecycle/SKILL.md +24 -0
  50. package/skills/hyv-lifecycle/agent.json +48 -0
  51. package/skills/hyv-lifecycle/agents/openai.yaml +4 -0
  52. package/skills/hyv-logic-lint/SKILL.md +24 -0
  53. package/skills/hyv-logic-lint/agent.json +47 -0
  54. package/skills/hyv-logic-lint/agents/openai.yaml +4 -0
  55. package/skills/hyv-mcp/SKILL.md +24 -0
  56. package/skills/hyv-mcp/agent.json +43 -0
  57. package/skills/hyv-mcp/agents/openai.yaml +4 -0
  58. package/skills/hyv-patterns/SKILL.md +24 -0
  59. package/skills/hyv-patterns/agent.json +43 -0
  60. package/skills/hyv-patterns/agents/openai.yaml +4 -0
  61. package/skills/hyv-prepare-judgment/SKILL.md +24 -0
  62. package/skills/hyv-prepare-judgment/agent.json +52 -0
  63. package/skills/hyv-prepare-judgment/agents/openai.yaml +4 -0
  64. package/skills/hyv-prepare-rebuild/SKILL.md +24 -0
  65. package/skills/hyv-prepare-rebuild/agent.json +54 -0
  66. package/skills/hyv-prepare-rebuild/agents/openai.yaml +4 -0
  67. package/skills/hyv-prepare-rewrite/SKILL.md +24 -0
  68. package/skills/hyv-prepare-rewrite/agent.json +51 -0
  69. package/skills/hyv-prepare-rewrite/agents/openai.yaml +4 -0
  70. package/skills/hyv-profile/SKILL.md +24 -0
  71. package/skills/hyv-profile/agent.json +50 -0
  72. package/skills/hyv-profile/agents/openai.yaml +4 -0
  73. package/skills/hyv-rebuild-writer-request/SKILL.md +24 -0
  74. package/skills/hyv-rebuild-writer-request/agent.json +47 -0
  75. package/skills/hyv-rebuild-writer-request/agents/openai.yaml +4 -0
  76. package/skills/hyv-reduce-judgment/SKILL.md +24 -0
  77. package/skills/hyv-reduce-judgment/agent.json +45 -0
  78. package/skills/hyv-reduce-judgment/agents/openai.yaml +4 -0
  79. package/skills/hyv-rewrite-prompt/SKILL.md +24 -0
  80. package/skills/hyv-rewrite-prompt/agent.json +48 -0
  81. package/skills/hyv-rewrite-prompt/agents/openai.yaml +4 -0
  82. package/skills/hyv-verify/SKILL.md +24 -0
  83. package/skills/hyv-verify/agent.json +49 -0
  84. package/skills/hyv-verify/agents/openai.yaml +4 -0
  85. package/skills/hyv-verify-spec/SKILL.md +24 -0
  86. package/skills/hyv-verify-spec/agent.json +50 -0
  87. package/skills/hyv-verify-spec/agents/openai.yaml +4 -0
package/dist/mcp.js CHANGED
@@ -35,31 +35,33 @@ function failure(error) {
35
35
  function lifecycleResult(result) {
36
36
  return json(result.ok ? result.artifact : { error: result.error });
37
37
  }
38
+ function guardedJson(run, publicError) {
39
+ try {
40
+ return json(run());
41
+ }
42
+ catch (error) {
43
+ return failure(publicError ? new Error(publicError) : error);
44
+ }
45
+ }
46
+ function guardedLifecycle(run, publicError) {
47
+ try {
48
+ return lifecycleResult(run());
49
+ }
50
+ catch (error) {
51
+ return failure(publicError ? new Error(publicError) : error);
52
+ }
53
+ }
38
54
  const server = new McpServer({ name: 'hold-your-voice', version: HYV_VERSION });
39
55
  server.registerTool('hyv_build_profile', {
40
56
  description: 'Build a portable VoiceDNA profile from at least two writing samples. The samples stay in memory and are not saved.',
41
57
  inputSchema: { samples, avoid },
42
58
  annotations: { readOnlyHint: true },
43
- }, async ({ samples: writingSamples, avoid: phrases }) => {
44
- try {
45
- return json(buildProfileForMcp(writingSamples, phrases));
46
- }
47
- catch (error) {
48
- return failure(error);
49
- }
50
- });
59
+ }, async ({ samples: writingSamples, avoid: phrases }) => guardedJson(() => buildProfileForMcp(writingSamples, phrases)));
51
60
  server.registerTool('hyv_analyze', {
52
61
  description: 'Run separate VoiceDNA and AI Editor checks plus a non-scoring Unicode hygiene inspection against a draft using a portable profile JSON string.',
53
62
  inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
54
63
  annotations: { readOnlyHint: true },
55
- }, async ({ draft, profile_json, writing_brief_json }) => {
56
- try {
57
- return json(analyzeForMcp(draft, profile_json, writing_brief_json));
58
- }
59
- catch (error) {
60
- return failure(error);
61
- }
62
- });
64
+ }, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => analyzeForMcp(draft, profile_json, writing_brief_json)));
63
65
  server.registerTool('hyv_hygiene', {
64
66
  description: 'Inspect text for zero-width characters, bidirectional controls, Unicode tag characters, and unusual spaces without changing it or requiring a voice profile.',
65
67
  inputSchema: { draft: hygieneText },
@@ -82,50 +84,22 @@ server.registerTool('hyv_logic_lint', {
82
84
  description: 'Run the deterministic document-coherence gate. It detects configured topic drift, unanchored inference, and direct internal contradictions; it does not verify facts or approve publication.',
83
85
  inputSchema: { draft: writing, writing_brief_json: writingBriefJson.optional() },
84
86
  annotations: { readOnlyHint: true },
85
- }, async ({ draft, writing_brief_json }) => {
86
- try {
87
- return json(logicLintForMcp(draft, writing_brief_json));
88
- }
89
- catch (error) {
90
- return failure(error);
91
- }
92
- });
87
+ }, async ({ draft, writing_brief_json }) => guardedJson(() => logicLintForMcp(draft, writing_brief_json)));
93
88
  server.registerTool('hyv_rewrite_prompt', {
94
89
  description: 'Create a constrained editing brief. It does not rewrite the draft or call a model.',
95
90
  inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
96
91
  annotations: { readOnlyHint: true },
97
- }, async ({ draft, profile_json, writing_brief_json }) => {
98
- try {
99
- return json(rewritePromptForMcp(draft, profile_json, {}, writing_brief_json));
100
- }
101
- catch (error) {
102
- return failure(error);
103
- }
104
- });
92
+ }, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => rewritePromptForMcp(draft, profile_json, {}, writing_brief_json)));
105
93
  server.registerTool('hyv_prepare_rewrite', {
106
94
  description: 'Prepare a local, versioned rewrite task. The caller may forward it to a provider; doing so shares the draft and must be an explicit choice.',
107
95
  inputSchema: { draft: writing, profile_json: profileJson, copy_spec_json: copySpecJson.optional(), writing_brief_json: writingBriefJson.optional() },
108
96
  annotations: { readOnlyHint: true },
109
- }, async ({ draft, profile_json, copy_spec_json, writing_brief_json }) => {
110
- try {
111
- return json(prepareRewriteForMcp(draft, profile_json, copy_spec_json, writing_brief_json));
112
- }
113
- catch (error) {
114
- return failure(error);
115
- }
116
- });
97
+ }, async ({ draft, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => prepareRewriteForMcp(draft, profile_json, copy_spec_json, writing_brief_json)));
117
98
  server.registerTool('hyv_apply_rewrite', {
118
99
  description: 'Validate and apply a model response to a prepared task, then run the local gates. It never calls a provider or stores source or candidate text.',
119
100
  inputSchema: { task_json: z.string().min(1).max(250_000), response_json: z.string().min(1).max(100_000), profile_json: profileJson },
120
101
  annotations: { readOnlyHint: true },
121
- }, async ({ task_json, response_json, profile_json }) => {
122
- try {
123
- return json(applyRewriteForMcp(task_json, response_json, profile_json));
124
- }
125
- catch (error) {
126
- return failure(error);
127
- }
128
- });
102
+ }, async ({ task_json, response_json, profile_json }) => guardedJson(() => applyRewriteForMcp(task_json, response_json, profile_json)));
129
103
  server.registerTool('hyv_prepare_judgment', {
130
104
  description: 'Prepare a versioned pre-edit or post-candidate judgment task. It does not call a model.',
131
105
  inputSchema: {
@@ -136,62 +110,27 @@ server.registerTool('hyv_prepare_judgment', {
136
110
  candidate: writing.optional(),
137
111
  },
138
112
  annotations: { readOnlyHint: true },
139
- }, async ({ stage, kind, draft, profile_json, candidate }) => {
140
- try {
141
- return json(prepareJudgmentForMcp(stage, kind, draft, profile_json, candidate));
142
- }
143
- catch (error) {
144
- return failure(error);
145
- }
146
- });
113
+ }, async ({ stage, kind, draft, profile_json, candidate }) => guardedJson(() => prepareJudgmentForMcp(stage, kind, draft, profile_json, candidate)));
147
114
  server.registerTool('hyv_reduce_judgment', {
148
115
  description: 'Reduce bound judgment envelopes into SHIP, EDIT, REBUILD, CLEAR, or ESCALATE. It does not call a model.',
149
116
  inputSchema: { envelopes_json: z.string().min(1).max(250_000) },
150
117
  annotations: { readOnlyHint: true },
151
- }, async ({ envelopes_json }) => {
152
- try {
153
- return json(reduceJudgmentForMcp(envelopes_json));
154
- }
155
- catch (error) {
156
- return failure(error);
157
- }
158
- });
118
+ }, async ({ envelopes_json }) => guardedJson(() => reduceJudgmentForMcp(envelopes_json)));
159
119
  server.registerTool('hyv_verify', {
160
120
  description: 'Verify a revised candidate against an original draft and portable profile without changing learning state.',
161
121
  inputSchema: { original: writing, candidate: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
162
122
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
163
- }, async ({ original, candidate, profile_json, writing_brief_json }) => {
164
- try {
165
- return json(verifyForMcp(original, candidate, profile_json, writing_brief_json));
166
- }
167
- catch (error) {
168
- return failure(error);
169
- }
170
- });
123
+ }, async ({ original, candidate, profile_json, writing_brief_json }) => guardedJson(() => verifyForMcp(original, candidate, profile_json, writing_brief_json)));
171
124
  server.registerTool('hyv_verify_copy_spec', {
172
125
  description: 'Verify a candidate against the existing voice gates and a local CopySpec. Immutable claims remain verbatim unless atoms are supplied; then each declared atom must remain. Prohibited claims fail closed.',
173
126
  inputSchema: { original: writing, candidate: writing, profile_json: profileJson, copy_spec_json: copySpecJson, writing_brief_json: writingBriefJson.optional() },
174
127
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
175
- }, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => {
176
- try {
177
- return json(verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json));
178
- }
179
- catch (error) {
180
- return failure(error);
181
- }
182
- });
128
+ }, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json)));
183
129
  server.registerTool('hyv_batch_analyze', {
184
130
  description: 'Inspect two to one hundred drafts for repeated opening and closing sentences. It returns advisory batch findings and does not store the drafts.',
185
131
  inputSchema: { drafts: z.array(writing).min(2).max(100) },
186
132
  annotations: { readOnlyHint: true },
187
- }, async ({ drafts }) => {
188
- try {
189
- return json(analyzeBatchForMcp(drafts));
190
- }
191
- catch (error) {
192
- return failure(error);
193
- }
194
- });
133
+ }, async ({ drafts }) => guardedJson(() => analyzeBatchForMcp(drafts)));
195
134
  server.registerTool('hyv_patterns', {
196
135
  description: 'List the exact AI Editor rules that run in this extension.',
197
136
  inputSchema: {},
@@ -200,116 +139,56 @@ server.registerTool('hyv_patterns', {
200
139
  server.registerTool('hyv_learning_inspect', {
201
140
  description: 'Inspect profile-scoped learning receipts without returning stored instruction or draft text.',
202
141
  inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: true },
203
- }, async ({ profile_json }) => { try {
204
- return json(inspectLearningForMcp(profile_json));
205
- }
206
- catch (error) {
207
- return failure(error);
208
- } });
142
+ }, async ({ profile_json }) => guardedJson(() => inspectLearningForMcp(profile_json)));
209
143
  server.registerTool('hyv_learning_record', {
210
144
  description: 'Record an explicit profile-scoped learning instruction with authority and provenance metadata.',
211
145
  inputSchema: { profile_json: profileJson, instruction: z.string().min(1).max(240), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
- }, async (args) => { try {
213
- return json(recordLearningForMcp(args.profile_json, args.instruction, learningArgs(args)));
214
- }
215
- catch (error) {
216
- return failure(error);
217
- } });
146
+ }, async (args) => guardedJson(() => recordLearningForMcp(args.profile_json, args.instruction, learningArgs(args))));
218
147
  server.registerTool('hyv_learning_ratify', {
219
148
  description: 'Ratify a learning event for a Profile v3 revision.',
220
149
  inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
221
- }, async (args) => { try {
222
- return json(ratifyLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
223
- }
224
- catch (error) {
225
- return failure(error);
226
- } });
150
+ }, async (args) => guardedJson(() => ratifyLearningForMcp(args.profile_json, args.event_id, learningArgs(args))));
227
151
  server.registerTool('hyv_learning_supersede', {
228
152
  description: 'Supersede a learning event for a Profile v3 revision.',
229
153
  inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
230
- }, async (args) => { try {
231
- return json(supersedeLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
232
- }
233
- catch (error) {
234
- return failure(error);
235
- } });
154
+ }, async (args) => guardedJson(() => supersedeLearningForMcp(args.profile_json, args.event_id, learningArgs(args))));
236
155
  server.registerTool('hyv_learning_migrate', {
237
156
  description: 'Migrate Profile v2 learning into a Profile v3 identity.',
238
157
  inputSchema: { source_profile_json: profileJson, target_profile_json: profileJson, ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
239
- }, async (args) => { try {
240
- return json(migrateLearningForMcp(args.source_profile_json, args.target_profile_json, learningArgs(args)));
241
- }
242
- catch (error) {
243
- return failure(error);
244
- } });
158
+ }, async (args) => guardedJson(() => migrateLearningForMcp(args.source_profile_json, args.target_profile_json, learningArgs(args))));
245
159
  server.registerTool('hyv_learning_clear', {
246
160
  description: 'Delete all local learning state for a profile.',
247
161
  inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
248
- }, async ({ profile_json }) => { try {
249
- return json(clearLearningForMcp(profile_json));
250
- }
251
- catch (error) {
252
- return failure(error);
253
- } });
162
+ }, async ({ profile_json }) => guardedJson(() => clearLearningForMcp(profile_json)));
254
163
  server.registerTool('hyv_lifecycle_prepare_semantic', {
255
164
  description: 'Prepare a normal semantic-review task and its initial immutable lifecycle artifact.',
256
165
  inputSchema: { deterministic_json: lifecycleJson, binding_json: lifecycleJson, receipt_json: lifecycleJson, policy: z.literal('normal'), allowed_violations: z.array(semanticViolation).max(5) },
257
166
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
258
- }, async (args) => { try {
259
- return json(prepareLifecycleForMcp(args.deterministic_json, args.binding_json, args.receipt_json, args.policy, args.allowed_violations));
260
- }
261
- catch (error) {
262
- return failure(error);
263
- } });
167
+ }, async (args) => guardedJson(() => prepareLifecycleForMcp(args.deterministic_json, args.binding_json, args.receipt_json, args.policy, args.allowed_violations)));
264
168
  server.registerTool('hyv_lifecycle_submit_verdict', {
265
169
  description: 'Submit one normal-policy semantic verdict using the server-installed evaluator authorization context.',
266
170
  inputSchema: { artifact_json: lifecycleJson, task_json: lifecycleJson, evaluator_id: evaluatorId, verdict_json: lifecycleJson },
267
171
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
268
- }, async (args) => { try {
269
- return lifecycleResult(submitSemanticVerdictForMcp(args.artifact_json, args.task_json, args.evaluator_id, args.verdict_json, loadApprovalContext()));
270
- }
271
- catch (error) {
272
- return failure(error);
273
- } });
172
+ }, async (args) => guardedLifecycle(() => submitSemanticVerdictForMcp(args.artifact_json, args.task_json, args.evaluator_id, args.verdict_json, loadApprovalContext())));
274
173
  server.registerTool('hyv_lifecycle_inspect', {
275
174
  description: 'Validate and inspect an immutable lifecycle artifact without exposing bound source or candidate hashes.',
276
175
  inputSchema: { artifact_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
277
- }, async ({ artifact_json }) => { try {
278
- return json(inspectLifecycleForMcp(artifact_json));
279
- }
280
- catch (error) {
281
- return failure(error);
282
- } });
176
+ }, async ({ artifact_json }) => guardedJson(() => inspectLifecycleForMcp(artifact_json)));
283
177
  if (redactsSensitiveInputs) {
284
178
  server.registerTool('hyv_lifecycle_finalize', {
285
179
  description: 'Finalize an authorized human approval or rejection. Capability input requires host-guaranteed sensitive-input redaction.',
286
180
  inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson, capability_json: lifecycleJson.optional() },
287
181
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
288
- }, async (args) => { try {
289
- return lifecycleResult(finalizeLifecycleForMcp(args.artifact_json, args.decision_json, loadApprovalContext(), args.capability_json));
290
- }
291
- catch {
292
- return failure(new Error('Lifecycle finalization failed.'));
293
- } });
182
+ }, async (args) => guardedLifecycle(() => finalizeLifecycleForMcp(args.artifact_json, args.decision_json, loadApprovalContext(), args.capability_json), 'Lifecycle finalization failed.'));
294
183
  server.registerTool('hyv_lifecycle_validate_final_approval', {
295
184
  description: 'Validate a final-approval capability against the server-installed trust context.',
296
185
  inputSchema: { artifact_json: lifecycleJson, capability_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
297
- }, async (args) => { try {
298
- return json(validateFinalApprovalForMcp(args.artifact_json, args.capability_json, loadApprovalContext()));
299
- }
300
- catch {
301
- return failure(new Error('Capability validation failed.'));
302
- } });
186
+ }, async (args) => guardedJson(() => validateFinalApprovalForMcp(args.artifact_json, args.capability_json, loadApprovalContext()), 'Capability validation failed.'));
303
187
  server.registerTool('hyv_learning_record_approved', {
304
188
  description: 'Record one approval-revalidated, deterministic, text-free learning event.',
305
189
  inputSchema: { ready_json: lifecycleJson, approved_json: lifecycleJson, original: approvedLearningText, candidate: approvedLearningText, profile_json: profileJson, decision_json: lifecycleJson, capability_json: lifecycleJson, copy_spec_json: copySpecJson.optional(), writing_brief_json: writingBriefJson.optional() },
306
190
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
307
- }, async (args) => { try {
308
- return json({ status: recordApprovedLearningForMcp({ readyJson: args.ready_json, approvedJson: args.approved_json, source: args.original, candidate: args.candidate, profileJson: args.profile_json, decisionJson: args.decision_json, capabilityJson: args.capability_json, context: loadApprovalContext(), copySpecJson: args.copy_spec_json, writingBriefJson: args.writing_brief_json }) });
309
- }
310
- catch {
311
- return failure(new Error('Approved learning was not authorized.'));
312
- } });
191
+ }, async (args) => guardedJson(() => ({ status: recordApprovedLearningForMcp({ readyJson: args.ready_json, approvedJson: args.approved_json, source: args.original, candidate: args.candidate, profileJson: args.profile_json, decisionJson: args.decision_json, capabilityJson: args.capability_json, context: loadApprovalContext(), copySpecJson: args.copy_spec_json, writingBriefJson: args.writing_brief_json }) }), 'Approved learning was not authorized.'));
313
192
  server.registerTool('hyv_prepare_rebuild', {
314
193
  description: 'Prepare a rebuild task only after an upstream REBUILD recommendation, CopySpec, and signed rebuild-authorization capability. Capability input requires host-guaranteed sensitive-input redaction.',
315
194
  inputSchema: {
@@ -322,49 +201,21 @@ if (redactsSensitiveInputs) {
322
201
  recomposition_policy_json: lifecycleJson.optional(),
323
202
  },
324
203
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
325
- }, async (args) => {
326
- try {
327
- return json(prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json, args.recomposition_policy_json));
328
- }
329
- catch {
330
- return failure(new Error('Rebuild preparation failed.'));
331
- }
332
- });
204
+ }, async (args) => guardedJson(() => prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json, args.recomposition_policy_json), 'Rebuild preparation failed.'));
333
205
  server.registerTool('hyv_apply_rebuild', {
334
206
  description: 'Validate and evaluate a whole-document rebuild response against a prepared authorized rebuild task. Capability input requires host-guaranteed sensitive-input redaction. It never calls a provider.',
335
207
  inputSchema: { task_json: lifecycleJson, response_json: z.string().min(1).max(100_000), profile_json: profileJson, capability_json: lifecycleJson },
336
208
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
337
- }, async (args) => {
338
- try {
339
- return json(applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()));
340
- }
341
- catch {
342
- return failure(new Error('Rebuild application failed.'));
343
- }
344
- });
209
+ }, async (args) => guardedJson(() => applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()), 'Rebuild application failed.'));
345
210
  server.registerTool('hyv_rebuild_writer_request', {
346
211
  description: 'Create the writer-only payload for a prepared rebuild. It excludes source draft, capability, profile body, and validation evidence.',
347
212
  inputSchema: { task_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
348
- }, async ({ task_json }) => {
349
- try {
350
- return json(rebuildWriterRequestForMcp(task_json));
351
- }
352
- catch {
353
- return failure(new Error('Writer request could not be prepared.'));
354
- }
355
- });
213
+ }, async ({ task_json }) => guardedJson(() => rebuildWriterRequestForMcp(task_json), 'Writer request could not be prepared.'));
356
214
  }
357
215
  else {
358
216
  server.registerTool('hyv_lifecycle_finalize', {
359
217
  description: 'Record an authorized human rejection. Approval is unavailable because this host does not guarantee sensitive-input redaction.',
360
218
  inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
361
- }, async (args) => {
362
- try {
363
- return lifecycleResult(finalizeRejectionForMcp(args.artifact_json, args.decision_json, loadApprovalContext()));
364
- }
365
- catch {
366
- return failure(new Error('Only rejection is available without sensitive-input redaction.'));
367
- }
368
- });
219
+ }, async (args) => guardedLifecycle(() => finalizeRejectionForMcp(args.artifact_json, args.decision_json, loadApprovalContext()), 'Only rejection is available without sensitive-input redaction.'));
369
220
  }
370
221
  await server.connect(new StdioServerTransport());
@@ -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.5');
177
+ assert.equal(HYV_VERSION, '3.4.1');
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/);
@@ -432,77 +432,103 @@ export function sealRatings(packetInput, mappingInput, ratingsInput) {
432
432
  const base = { kind: 'hyv-stage1-ratings-seal', version: '1', protocolDigest: packet.protocolDigest, candidateDigest: packet.candidateDigest, packetDigest: packet.packetDigest, mappingDigest: packet.mappingDigest, ratingsDigest: sha256Canonical(ratings), recordCount: ratings.length };
433
433
  return { ...base, sealDigest: sha256Canonical(base) };
434
434
  }
435
- export function reduceEvaluation(protocolInput, runsInput, packetInput, mappingInput, ratingsInput, sealInput, releaseAuditInput) {
436
- const protocol = assertCommittedProtocol(protocolInput);
437
- const summary = validateRuns(protocol, runsInput);
438
- const packet = parsePacket(packetInput);
435
+ function validateEvaluationPacket(protocol, runs, summary, packet) {
439
436
  if (packet.protocolDigest !== protocol.protocolDigest || packet.candidateDigest !== candidateDigest(protocol) || packet.runsDigest !== summary.runsDigest)
440
437
  fail('packet_digest_chain_mismatch');
441
- const runRows = runsInput;
442
- const expectedReviewableCount = protocol.cases.filter((entry) => runRows.filter((run) => run.caseId === entry.caseId && effectiveRunOutcome(run) === 'completed').length === 2).length;
443
- if (packet.reviewableCount !== expectedReviewableCount || packet.nonReviewableCount !== protocol.cases.length - expectedReviewableCount)
438
+ const reviewableCount = protocol.cases.filter((entry) => runs.filter((run) => run.caseId === entry.caseId && effectiveRunOutcome(run) === 'completed').length === 2).length;
439
+ if (packet.reviewableCount !== reviewableCount || packet.nonReviewableCount !== protocol.cases.length - reviewableCount)
444
440
  fail('blind_packet_count_mismatch');
445
- const mapping = parseMapping(mappingInput);
446
- validateMappingOpening(protocol, mapping);
447
- const ratings = validateReviewerLog(protocol, packet, ratingsInput, true);
448
- const seal = sealRatings(packet, mapping, ratings);
449
- if (sha256Canonical(seal) !== sha256Canonical(sealInput))
450
- fail('ratings_seal_mismatch');
451
- const audit = record(releaseAuditInput);
441
+ }
442
+ function parseReleaseAudit(protocol, input) {
443
+ const audit = record(input);
452
444
  exactKeys(audit, ['kind', 'version', 'candidateCommit', 'protocolDigest', 'contractDigest', 'passed', 'digest']);
453
445
  const { digest: auditDigest, ...auditCore } = audit;
454
446
  if (auditDigest !== sha256Canonical(auditCore))
455
447
  fail('release_audit_digest_mismatch');
456
448
  if (audit.kind !== 'hyv-release-audit' || audit.version !== '1' || audit.candidateCommit !== protocol.stage1.sourceCommit || audit.protocolDigest !== protocol.protocolDigest || audit.contractDigest !== protocol.releaseAuditContractDigest || audit.passed !== true)
457
449
  fail('release_audit_binding_mismatch');
450
+ return audit;
451
+ }
452
+ function correctionCount(ratings, mapping, arm, result) {
453
+ const label = mapping.labels.A === arm ? 'A' : 'B';
454
+ return ratings.filter((rating) => rating.correctionVersusConfirm[label] === result).length;
455
+ }
456
+ function workflowForArm(runs, arm) {
457
+ const armRuns = runs.filter((run) => run.arm === arm);
458
+ return { completed: armRuns.filter((run) => effectiveRunOutcome(run) === 'completed').length, denominator: armRuns.length };
459
+ }
460
+ function calculateEvaluation(protocol, runs, ratings, mapping, summary) {
461
+ const completedRatings = ratings.filter((rating) => rating.workflow === 'completed');
462
+ const preferenceDenominator = protocol.intentToTreat.expectedReviewers * protocol.cases.length;
463
+ const preferred = completedRatings.reduce((total, rating) => total + (rating.preferredLabel === 'tie' ? 0.5 : mapping.labels[rating.preferredLabel] === 'stage1' ? 1 : 0), 0);
464
+ const preferenceRate = preferenceDenominator ? preferred / preferenceDenominator : 0;
465
+ const stage1Confirm = correctionCount(completedRatings, mapping, 'stage1', 'confirm');
466
+ const baselineConfirm = correctionCount(completedRatings, mapping, 'baseline', 'confirm');
467
+ const stage1Correction = correctionCount(completedRatings, mapping, 'stage1', 'correction');
468
+ const baselineCorrection = correctionCount(completedRatings, mapping, 'baseline', 'correction');
469
+ const baselineWorkflow = workflowForArm(runs, 'baseline');
470
+ const stage1Workflow = workflowForArm(runs, 'stage1');
471
+ const correctionMarginMet = (stage1Confirm - baselineConfirm) / preferenceDenominator >= protocol.analysis.margin;
472
+ const workflowRegressed = stage1Workflow.denominator === 0 || baselineWorkflow.denominator === 0 || stage1Workflow.completed / stage1Workflow.denominator < baselineWorkflow.completed / baselineWorkflow.denominator;
473
+ const metrics = {
474
+ preference: { numerator: preferred, denominator: preferenceDenominator, rate: preferenceRate, uncertainty95: wilson95(preferred, preferenceDenominator) },
475
+ correctionVersusConfirm: { stage1: { corrections: stage1Correction, confirms: stage1Confirm, denominator: preferenceDenominator, correctionRate: stage1Correction / preferenceDenominator, uncertainty95: wilson95(stage1Correction, preferenceDenominator) }, baseline: { corrections: baselineCorrection, confirms: baselineConfirm, denominator: preferenceDenominator, correctionRate: baselineCorrection / preferenceDenominator, uncertainty95: wilson95(baselineCorrection, preferenceDenominator) } },
476
+ completion: { numerator: completedRatings.length, denominator: preferenceDenominator, rate: completedRatings.length / preferenceDenominator, uncertainty95: wilson95(completedRatings.length, preferenceDenominator) },
477
+ abandonment: { numerator: ratings.length - completedRatings.length, denominator: preferenceDenominator, rate: (ratings.length - completedRatings.length) / preferenceDenominator, uncertainty95: wilson95(ratings.length - completedRatings.length, preferenceDenominator) },
478
+ providerRuns: { completed: summary.completed, abandoned: summary.abandonments, hardFailures: summary.hardFailures, timeouts: summary.timeouts, denominator: summary.denominator },
479
+ };
480
+ return { correctionMarginMet, metrics, preferenceDenominator, preferenceRate, workflowRegressed };
481
+ }
482
+ function mappingCustodyIsValid(protocol, packet, mapping) {
483
+ const { attestation, ...openingCore } = mapping;
484
+ try {
485
+ validateAttestation(attestation, sha256Canonical(openingCore), protocol.protocolDigest, packet.packetDigest);
486
+ return attestation.purpose === 'stage1-mapping-custody';
487
+ }
488
+ catch {
489
+ return false;
490
+ }
491
+ }
492
+ function evaluationBlockers(protocol, runs, packet, ratings, mapping, calculation) {
458
493
  const blockers = ['human_writer_evidence_deferred'];
459
- if (protocol.benchmark.synthetic || ratings.some((r) => r.evidenceClass === 'synthetic-dry-run'))
494
+ if (protocol.benchmark.synthetic || ratings.some((rating) => rating.evidenceClass === 'synthetic-dry-run'))
460
495
  blockers.push('synthetic_fixture_evidence');
461
496
  if (protocol.mode !== 'locked-human')
462
497
  blockers.push('locked_human_evidence_required');
463
498
  if (protocol.mode === 'locked-human') {
464
499
  blockers.push('reviewer_roster_verification_deferred');
465
- const { attestation, ...openingCore } = mapping;
466
- try {
467
- validateAttestation(attestation, sha256Canonical(openingCore), protocol.protocolDigest, packet.packetDigest);
468
- if (attestation.purpose !== 'stage1-mapping-custody')
469
- throw new Error('purpose');
470
- }
471
- catch {
500
+ if (!mappingCustodyIsValid(protocol, packet, mapping))
472
501
  blockers.push('mapping_custody_attestation_required');
473
- }
474
502
  }
475
- if (ratings.length !== protocol.intentToTreat.expectedReviewers * protocol.cases.length)
503
+ if (ratings.length !== calculation.preferenceDenominator)
476
504
  blockers.push('reviewer_denominator_mismatch');
477
505
  if (protocol.cases.length < protocol.analysis.minimumCases || ratings.length < protocol.analysis.minimumRatings)
478
506
  blockers.push('preregistered_minimum_not_met');
479
- if (runsInput.some((run) => run.hardGates && Object.values(run.hardGates).some((passed) => !passed)))
507
+ if (runs.some((run) => run.hardGates && Object.values(run.hardGates).some((passed) => !passed)))
480
508
  blockers.push('hard_gate_regression');
481
- const completedRatings = ratings.filter((rating) => rating.workflow === 'completed');
482
- const preferred = completedRatings.reduce((total, rating) => total + (rating.preferredLabel === 'tie' ? 0.5 : mapping.labels[rating.preferredLabel] === 'stage1' ? 1 : 0), 0);
483
- const preferenceDenominator = protocol.intentToTreat.expectedReviewers * protocol.cases.length;
484
- const preferenceRate = preferenceDenominator ? preferred / preferenceDenominator : 0;
485
- const stage1Confirm = completedRatings.filter((rating) => rating.correctionVersusConfirm[mapping.labels.A === 'stage1' ? 'A' : 'B'] === 'confirm').length;
486
- const baselineConfirm = completedRatings.filter((rating) => rating.correctionVersusConfirm[mapping.labels.A === 'baseline' ? 'A' : 'B'] === 'confirm').length;
487
- const correctionMarginMet = (stage1Confirm - baselineConfirm) / preferenceDenominator >= protocol.analysis.margin;
488
- if (preferenceRate < 0.5 + protocol.analysis.margin && !correctionMarginMet)
509
+ if (calculation.preferenceRate < 0.5 + protocol.analysis.margin && !calculation.correctionMarginMet)
489
510
  blockers.push('checkpoint_threshold_not_met');
490
- const workflowByArm = (arm) => ({ completed: runRows.filter((run) => run.arm === arm && effectiveRunOutcome(run) === 'completed').length, denominator: runRows.filter((run) => run.arm === arm).length });
491
- const baselineWorkflow = workflowByArm('baseline');
492
- const stage1Workflow = workflowByArm('stage1');
493
- if (stage1Workflow.denominator === 0 || baselineWorkflow.denominator === 0 || stage1Workflow.completed / stage1Workflow.denominator < baselineWorkflow.completed / baselineWorkflow.denominator)
511
+ if (calculation.workflowRegressed)
494
512
  blockers.push('workflow_regression');
495
- const stage1Correction = completedRatings.filter((rating) => rating.correctionVersusConfirm[mapping.labels.A === 'stage1' ? 'A' : 'B'] === 'correction').length;
496
- const baselineCorrection = completedRatings.filter((rating) => rating.correctionVersusConfirm[mapping.labels.A === 'baseline' ? 'A' : 'B'] === 'correction').length;
497
- const metrics = {
498
- preference: { numerator: preferred, denominator: preferenceDenominator, rate: preferenceRate, uncertainty95: wilson95(preferred, preferenceDenominator) },
499
- correctionVersusConfirm: { stage1: { corrections: stage1Correction, confirms: stage1Confirm, denominator: preferenceDenominator, correctionRate: stage1Correction / preferenceDenominator, uncertainty95: wilson95(stage1Correction, preferenceDenominator) }, baseline: { corrections: baselineCorrection, confirms: baselineConfirm, denominator: preferenceDenominator, correctionRate: baselineCorrection / preferenceDenominator, uncertainty95: wilson95(baselineCorrection, preferenceDenominator) } },
500
- completion: { numerator: completedRatings.length, denominator: preferenceDenominator, rate: completedRatings.length / preferenceDenominator, uncertainty95: wilson95(completedRatings.length, preferenceDenominator) },
501
- abandonment: { numerator: ratings.length - completedRatings.length, denominator: preferenceDenominator, rate: (ratings.length - completedRatings.length) / preferenceDenominator, uncertainty95: wilson95(ratings.length - completedRatings.length, preferenceDenominator) },
502
- providerRuns: { completed: summary.completed, abandoned: summary.abandonments, hardFailures: summary.hardFailures, timeouts: summary.timeouts, denominator: summary.denominator },
503
- };
504
- const intentToTreat = { ...summary, expectedRatings: preferenceDenominator, observedRatings: ratings.length, missingRatings: Math.max(0, preferenceDenominator - ratings.length), reconciled: summary.denominator === protocol.intentToTreat.expectedAssignments && ratings.length === preferenceDenominator };
505
- const base = { kind: 'hyv-stage1-evaluation-report', version: '1', protocolDigest: protocol.protocolDigest, candidateDigest: candidateDigest(protocol), runsDigest: summary.runsDigest, packetDigest: packet.packetDigest, mappingDigest: packet.mappingDigest, sealDigest: seal.sealDigest, releaseAuditDigest: audit.digest, intentToTreat, metrics, decision: blockers.length ? 'BLOCKED' : 'PASS', promotable: blockers.length === 0, blockers };
513
+ return blockers;
514
+ }
515
+ export function reduceEvaluation(protocolInput, runsInput, packetInput, mappingInput, ratingsInput, sealInput, releaseAuditInput) {
516
+ const protocol = assertCommittedProtocol(protocolInput);
517
+ const summary = validateRuns(protocol, runsInput);
518
+ const runs = runsInput;
519
+ const packet = parsePacket(packetInput);
520
+ validateEvaluationPacket(protocol, runs, summary, packet);
521
+ const mapping = parseMapping(mappingInput);
522
+ validateMappingOpening(protocol, mapping);
523
+ const ratings = validateReviewerLog(protocol, packet, ratingsInput, true);
524
+ const seal = sealRatings(packet, mapping, ratings);
525
+ if (sha256Canonical(seal) !== sha256Canonical(sealInput))
526
+ fail('ratings_seal_mismatch');
527
+ const audit = parseReleaseAudit(protocol, releaseAuditInput);
528
+ const calculation = calculateEvaluation(protocol, runs, ratings, mapping, summary);
529
+ const blockers = evaluationBlockers(protocol, runs, packet, ratings, mapping, calculation);
530
+ const intentToTreat = { ...summary, expectedRatings: calculation.preferenceDenominator, observedRatings: ratings.length, missingRatings: Math.max(0, calculation.preferenceDenominator - ratings.length), reconciled: summary.denominator === protocol.intentToTreat.expectedAssignments && ratings.length === calculation.preferenceDenominator };
531
+ const base = { kind: 'hyv-stage1-evaluation-report', version: '1', protocolDigest: protocol.protocolDigest, candidateDigest: candidateDigest(protocol), runsDigest: summary.runsDigest, packetDigest: packet.packetDigest, mappingDigest: packet.mappingDigest, sealDigest: seal.sealDigest, releaseAuditDigest: audit.digest, intentToTreat, metrics: calculation.metrics, decision: blockers.length ? 'BLOCKED' : 'PASS', promotable: blockers.length === 0, blockers };
506
532
  return { ...base, reportDigest: sha256Canonical(base) };
507
533
  }
508
534
  export function preflight(protocolInput) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const HYV_VERSION = '3.3.5';
1
+ export const HYV_VERSION = '3.4.1';
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "3.3.5",
3
+ "version": "3.4.1",
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.