@holdyourvoice/hyv 2.9.25 → 2.9.27

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable CLI changes. Also mirrored to [holdyourvoice.com/changelog](https://holdyourvoice.com/changelog) for user-facing releases.
4
4
 
5
+ ## [2.9.27] — 2026-07-15
6
+
7
+ ### Fixed
8
+ - **MCP scan hang** — `hyv_scan`, `hyv_fix`, `hyv_check`, `hyv_score`, `hyv_diff`, and `hyv_validate` no longer block on account profile sync; they use local `~/.hyv` cache immediately (matching `hyv scan` CLI) and hydrate profiles in the background
9
+
10
+ ## [2.9.26] — 2026-07-09
11
+
12
+ ### Improved
13
+ - **Tier 1 structural detection** — cross-sentence binary reframes (`this is not X. it is Y.`), sermon anaphora, and founder-carousel metaphors now sort first in scan output and rewrite prompts
14
+ - **Rewrite prompts** split tier 1 structural hits vs other patterns so the model fixes architecture before vocabulary slop
15
+ - **18 new structural cousin rules** synced in local scan engine (`scan.ts`, `tier-one.ts`)
16
+
5
17
  ## [2.9.25] — 2026-07-04
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -96,7 +96,7 @@ hyv doctor --fix-agents
96
96
  | tool | description |
97
97
  |------|-------------|
98
98
  | `hyv_welcome` / `hyv_demo` | onboarding + live free scan demo |
99
- | `hyv_scan` | fast local scan (free, offline) |
99
+ | `hyv_scan` | fast local scan (free, offline; uses `~/.hyv` cache, no network wait) |
100
100
  | `hyv_analyze` | hybrid local + server analysis (paid when online) |
101
101
  | `hyv_clean` | scan → fix → validate → rewrite prompt |
102
102
  | `hyv_fix` / `hyv_check` / `hyv_score` / `hyv_diff` | local utilities |
@@ -209,6 +209,10 @@ HYV_API_URL=https://staging.holdyourvoice.com hyv init
209
209
  - [blog](https://holdyourvoice.com/blog)
210
210
  - [community](https://holdyourvoice.com/community)
211
211
 
212
+ ## documentation ownership
213
+
214
+ Changes under `cli/` update this README, the more-specific owner README when applicable, and the root repository README. Package-relative paths, bin names, published files, and postinstall behavior are contracts; validate them with `npm run validate:publish` before merging.
215
+
212
216
  ## license
213
217
 
214
218
  UNLICENSED — [holdyourvoice.com](https://holdyourvoice.com)
@@ -0,0 +1,20 @@
1
+ # `cli/agents/`
2
+
3
+ ## Purpose
4
+
5
+ `cli/agents/` contains agent resources included in the published CLI package.
6
+
7
+ ## Contracts
8
+
9
+ The directory is included by the `files` list in `cli/package.json`. Changes must preserve package inclusion and postinstall discovery behavior.
10
+
11
+ ## Validation
12
+
13
+ ```bash
14
+ cd cli
15
+ npm run validate:publish
16
+ ```
17
+
18
+ ## Documentation ownership
19
+
20
+ Changes update this README, `cli/README.md`, and the root `README.md`.
@@ -0,0 +1,20 @@
1
+ # `cli/assets/`
2
+
3
+ ## Purpose
4
+
5
+ `cli/assets/` contains assets included in the published CLI package.
6
+
7
+ ## Contracts
8
+
9
+ The directory is included by the `files` list in `cli/package.json`. Keep package paths and postinstall consumers intact.
10
+
11
+ ## Validation
12
+
13
+ ```bash
14
+ cd cli
15
+ npm run validate:publish
16
+ ```
17
+
18
+ ## Documentation ownership
19
+
20
+ Changes update this README, `cli/README.md`, and the root `README.md`.
@@ -21,6 +21,16 @@ patterns, and uniform sentence rhythm within paragraphs.
21
21
  - "It's not X, it's Y" / "The point isn't X, it's Y"
22
22
  - "You don't need X, you need Y"
23
23
  - "Not just X, but Y" / "Not only... but also"
24
+ - **structural cousins (top priority):** cross-sentence forms the vocabulary scan misses:
25
+ - "This is not X. It is Y." / "Is not X. It is Y."
26
+ - "You are not X. You are Y." / "You are no longer X. You are Y."
27
+ - "You stop X. You start Y."
28
+ - "They don't need X. They need Y."
29
+ - "They call it X. They call it Y." (staccato anaphora)
30
+ - Parallel sermon: "X because… / they X because…" and "endure through… / they endure through…"
31
+ - Founder-carousel metaphors: "competing with the void", "architect of a new reality", "friction into momentum", "cognitive architecture", "defines what good looks like", "the hidden goal", "quiet promise", "simple but uncomfortable:"
32
+
33
+ **Tier 1 priority:** rules with ids `structural_*` and `founder_*` are primary flags. They sort first in scan output, win hit-budget slots in the editor, weigh heavier in cleanliness scoring, lead rewrite prompts, and gate rewrite output before vocabulary slop.
24
34
 
25
35
  ### Staccato drama (#2, #95, #96)
26
36
  - Short punchy sentence runs: "No X. No Y. Just Z."
package/dist/index.js CHANGED
@@ -11316,6 +11316,23 @@ var init_patterns = __esm({
11316
11316
  { id: "struct.lesson-setup", category: "structure", severity: "yellow", regex: /\bhere'?s what .{3,60} taught\b/gi, suggestion: "just share the lesson" },
11317
11317
  // THE BIG ONE — antithesis negation pattern (Voice DNA: FATAL)
11318
11318
  { id: "struct.this-isnt-x-this-is-y", category: "structure", severity: "red", regex: /\bthis isn'?t .{2,40}\.?\s*(?:this is|it'?s) .{2,40}/gi, suggestion: "FATAL: delete the negation, just state the positive claim" },
11319
+ { id: "struct.this-is-not-it-is", category: "structure", severity: "red", regex: /\bthis\s+is\s+not\b.{3,80}\.\s*it\s+is\b/gi, suggestion: 'FATAL: "this is not X. it is Y" \u2014 state what it is directly' },
11320
+ { id: "struct.is-not-it-is", category: "structure", severity: "red", regex: /\bis\s+not\b.{3,80}\.\s*it\s+is\b/gi, suggestion: "FATAL: negation-then-reveal \u2014 drop the denial" },
11321
+ { id: "struct.you-are-not-you-are", category: "structure", severity: "red", regex: /\byou\s+are\s+not\b.{3,80}\.\s*you\s+are\b/gi, suggestion: "FATAL: identity reframe carousel \u2014 say what they are" },
11322
+ { id: "struct.you-no-longer-you-are", category: "structure", severity: "red", regex: /\byou\s+are\s+no\s+longer\b.{3,80}\.\s*you\s+are\b/gi, suggestion: "FATAL: escalation reframe \u2014 one grounded sentence" },
11323
+ { id: "struct.you-stop-you-start", category: "structure", severity: "red", regex: /\byou\s+stop\b.{3,50}\.\s*you\s+start\b/gi, suggestion: "FATAL: pivot cadence \u2014 describe the behavior change" },
11324
+ { id: "struct.need-negation-pivot", category: "structure", severity: "red", regex: /\b(?:do\s+not|don'?t)\s+need\b.{3,50}\.\s*(?:they|you)\s+need\b/gi, suggestion: "FATAL: need antithesis \u2014 say what they need" },
11325
+ { id: "struct.they-call-it-anaphora", category: "structure", severity: "red", regex: /\bthey\s+call\s+it\b[^.!?]{2,50}[.!?]\s*they\s+call\s+it\b/gi, suggestion: "FATAL: staccato anaphora \u2014 name it once" },
11326
+ { id: "struct.parallel-because", category: "structure", severity: "red", regex: /\b(\w+)\s+because\b[^.!?]{3,60}[.!?]\s*they\s+\1\s+because\b/gi, suggestion: "FATAL: parallel sermon rhythm \u2014 merge into one sentence" },
11327
+ { id: "struct.parallel-endure", category: "structure", severity: "red", regex: /\bendure\s+through\b[^.!?]{3,60}[.!?]\s*they\s+endure\s+through\b/gi, suggestion: "FATAL: parallel maxim \u2014 say it once" },
11328
+ { id: "struct.simple-but-uncomfortable", category: "structure", severity: "red", regex: /\bsimple\s+but\s+uncomfortable\s*:/gi, suggestion: "truth-posture colon reveal \u2014 state the mechanism" },
11329
+ { id: "struct.hidden-goal", category: "structure", severity: "red", regex: /\bthe\s+hidden\s+goal\b/gi, suggestion: "reveal framing \u2014 say the goal directly" },
11330
+ { id: "struct.quiet-promise", category: "structure", severity: "red", regex: /\bquiet\s+promise\b/gi, suggestion: "profundity metaphor \u2014 name the actual commitment" },
11331
+ { id: "struct.competing-void", category: "structure", severity: "red", regex: /\bcompeting\s+with\s+the\s+void\b/gi, suggestion: "screenshot profundity \u2014 name the real competitor" },
11332
+ { id: "struct.architect-reality", category: "structure", severity: "red", regex: /\barchitect\s+of\s+(?:a\s+)?new\s+reality\b/gi, suggestion: "thought-leader grandiosity \u2014 describe what you build" },
11333
+ { id: "struct.friction-momentum", category: "structure", severity: "red", regex: /\b(?:turns?\s+)?friction\s+into\s+momentum\b/gi, suggestion: "transformation buzzphrase \u2014 explain the mechanism" },
11334
+ { id: "struct.cognitive-architecture", category: "structure", severity: "red", regex: /\bcognitive\s+architecture\b/gi, suggestion: "abstract systems metaphor \u2014 use a concrete scene" },
11335
+ { id: "struct.defines-good", category: "structure", severity: "red", regex: /\bdefines\s+what\s+good\s+looks\s+like\b/gi, suggestion: "consultant language \u2014 show an example of good" },
11319
11336
  { id: "struct.not-x-y", category: "structure", severity: "red", regex: /\bnot .{2,30}\.?\s*.{2,30}\b/gi, suggestion: 'the "Not X. Y." pattern is an AI tell \u2014 just state Y' },
11320
11337
  { id: "struct.forget-x", category: "structure", severity: "red", regex: /\bforget .{2,40}\.?\s*(?:this is|it'?s|you need)/gi, suggestion: "don't negate \u2014 just state what you mean" }
11321
11338
  ];
@@ -11375,6 +11392,34 @@ var init_patterns = __esm({
11375
11392
  }
11376
11393
  });
11377
11394
 
11395
+ // src/lib/tier-one.ts
11396
+ function isTierOneRule(ruleId) {
11397
+ const id = String(ruleId || "");
11398
+ if (TIER_ONE_PREFIXES.some((p) => id.startsWith(p)))
11399
+ return true;
11400
+ return TIER_ONE_SIGNAL_IDS.has(id);
11401
+ }
11402
+ function tierSortKey(ruleId) {
11403
+ return isTierOneRule(ruleId) ? 0 : 1;
11404
+ }
11405
+ var TIER_ONE_PREFIXES, TIER_ONE_SIGNAL_IDS;
11406
+ var init_tier_one = __esm({
11407
+ "src/lib/tier-one.ts"() {
11408
+ "use strict";
11409
+ TIER_ONE_PREFIXES = ["structural_", "founder_"];
11410
+ TIER_ONE_SIGNAL_IDS = /* @__PURE__ */ new Set([
11411
+ "struct.this-isnt-x-this-is-y",
11412
+ "struct.not-x-y",
11413
+ "struct.negation-cascade",
11414
+ "struct.which-is-another",
11415
+ "struct.in-other-words",
11416
+ "struct.moment-becomes",
11417
+ "struct.same-better",
11418
+ "struct.generic-buyer"
11419
+ ]);
11420
+ }
11421
+ });
11422
+
11378
11423
  // src/lib/scan.ts
11379
11424
  function words(text) {
11380
11425
  return (text || "").match(/[a-zA-Z][a-zA-Z0-9']*/g) || [];
@@ -11485,6 +11530,9 @@ function scanText(text) {
11485
11530
  }
11486
11531
  }
11487
11532
  return hits.sort((a, b) => {
11533
+ const tier = tierSortKey(a.rule) - tierSortKey(b.rule);
11534
+ if (tier !== 0)
11535
+ return tier;
11488
11536
  if (a.line !== b.line)
11489
11537
  return a.line - b.line;
11490
11538
  return a.rule.localeCompare(b.rule);
@@ -11495,6 +11543,7 @@ var init_scan = __esm({
11495
11543
  "src/lib/scan.ts"() {
11496
11544
  "use strict";
11497
11545
  init_patterns();
11546
+ init_tier_one();
11498
11547
  AI_PATTERN_RULES = [
11499
11548
  {
11500
11549
  id: "ai_antithesis",
@@ -11555,6 +11604,75 @@ var init_scan = __esm({
11555
11604
  {
11556
11605
  id: "founder_cadence_same_better",
11557
11606
  pattern: /\bsame\s+[^.!?\n]{1,35}[.!?]\s*(?:better|nicer|cleaner|calmer|safer)\s+[^.!?\n]{1,35}[.!?]?/i
11607
+ },
11608
+ // structural cousins — cross-sentence binary reframing (top priority)
11609
+ {
11610
+ id: "structural_this_is_not_it_is",
11611
+ pattern: /\bthis\s+is\s+not\b.{3,80}\.\s*it\s+is\b/i
11612
+ },
11613
+ {
11614
+ id: "structural_is_not_it_is",
11615
+ pattern: /\bis\s+not\b.{3,80}\.\s*it\s+is\b/i
11616
+ },
11617
+ {
11618
+ id: "structural_you_are_not_you_are",
11619
+ pattern: /\byou\s+are\s+not\b.{3,80}\.\s*you\s+are\b/i
11620
+ },
11621
+ {
11622
+ id: "structural_you_no_longer_you_are",
11623
+ pattern: /\byou\s+are\s+no\s+longer\b.{3,80}\.\s*you\s+are\b/i
11624
+ },
11625
+ {
11626
+ id: "structural_you_stop_you_start",
11627
+ pattern: /\byou\s+stop\b.{3,50}\.\s*you\s+start\b/i
11628
+ },
11629
+ {
11630
+ id: "structural_need_negation_pivot",
11631
+ pattern: /\b(?:do\s+not|don'?t)\s+need\b.{3,50}\.\s*(?:they|you)\s+need\b/i
11632
+ },
11633
+ {
11634
+ id: "structural_they_call_it_anaphora",
11635
+ pattern: /\bthey\s+call\s+it\b[^.!?]{2,50}[.!?]\s*they\s+call\s+it\b/i
11636
+ },
11637
+ {
11638
+ id: "structural_parallel_verb_because",
11639
+ pattern: /\b(\w+)\s+because\b[^.!?]{3,60}[.!?]\s*they\s+\1\s+because\b/i
11640
+ },
11641
+ {
11642
+ id: "structural_parallel_endure_through",
11643
+ pattern: /\bendure\s+through\b[^.!?]{3,60}[.!?]\s*they\s+endure\s+through\b/i
11644
+ },
11645
+ {
11646
+ id: "structural_simple_but_uncomfortable",
11647
+ pattern: /\bsimple\s+but\s+uncomfortable\s*:/i
11648
+ },
11649
+ {
11650
+ id: "structural_hidden_goal",
11651
+ pattern: /\bthe\s+hidden\s+goal\b/i
11652
+ },
11653
+ {
11654
+ id: "structural_quiet_promise",
11655
+ pattern: /\bquiet\s+promise\b/i
11656
+ },
11657
+ {
11658
+ id: "founder_competing_with_void",
11659
+ pattern: /\bcompeting\s+with\s+the\s+void\b/i
11660
+ },
11661
+ {
11662
+ id: "founder_architect_of_reality",
11663
+ pattern: /\barchitect\s+of\s+(?:a\s+)?new\s+reality\b/i
11664
+ },
11665
+ {
11666
+ id: "founder_friction_into_momentum",
11667
+ pattern: /\b(?:turns?\s+)?friction\s+into\s+momentum\b/i
11668
+ },
11669
+ {
11670
+ id: "founder_cognitive_architecture",
11671
+ pattern: /\bcognitive\s+architecture\b/i
11672
+ },
11673
+ {
11674
+ id: "founder_defines_good_looks_like",
11675
+ pattern: /\bdefines\s+what\s+good\s+looks\s+like\b/i
11558
11676
  }
11559
11677
  ];
11560
11678
  ABSTRACT_STYLE_WORDS = /* @__PURE__ */ new Set([
@@ -16372,6 +16490,8 @@ var path11 = __toESM(require("path"));
16372
16490
  init_config();
16373
16491
  init_signals();
16374
16492
  init_profile_parse();
16493
+ init_scan();
16494
+ init_tier_one();
16375
16495
  function loadPromptTemplate() {
16376
16496
  const templatePath = path11.join(CACHE_DIR2, "prompt-template.md");
16377
16497
  try {
@@ -16420,6 +16540,7 @@ Rules:
16420
16540
  - "Not X. Y." is banned. Just state Y.
16421
16541
  - "Forget X. This is Y." is banned. Just state Y.
16422
16542
  - ANY sentence that negates one framing then asserts a corrected one fails.
16543
+ - Tier 1 structural patterns (binary reframes, negation pivots, parallel sermon cadence) outrank vocabulary fixes. Kill the architecture first.
16423
16544
 
16424
16545
  Voice profile:
16425
16546
  {profile_block}
@@ -16427,8 +16548,11 @@ Voice profile:
16427
16548
  Extra constraints:
16428
16549
  {constraints_block}
16429
16550
 
16430
- Flagged lines:
16431
- {issue_lines}
16551
+ Tier 1 \u2014 structural (fix first):
16552
+ {tier_one_lines}
16553
+
16554
+ Other flagged patterns:
16555
+ {other_lines}
16432
16556
 
16433
16557
  Draft with line numbers ({draft_name}):
16434
16558
  {numbered_draft}
@@ -16488,6 +16612,38 @@ function buildProfileBlock(fullProfile, fallbackName) {
16488
16612
  }
16489
16613
  return { block: "(no voice profile supplied \u2014 use generic voice rules)", name: null };
16490
16614
  }
16615
+ function collectPromptIssues(draftContent, fullProfile) {
16616
+ const issues = [];
16617
+ const seen = /* @__PURE__ */ new Set();
16618
+ const addIssue = (line, rule, detail) => {
16619
+ const key = `${line}|${rule}|${detail.slice(0, 80)}`;
16620
+ if (seen.has(key))
16621
+ return;
16622
+ seen.add(key);
16623
+ issues.push({ line, rule, detail, tierOne: isTierOneRule(rule) });
16624
+ };
16625
+ for (const signal of extractSignals(draftContent, fullProfile).signals) {
16626
+ addIssue(signal.line, signal.id, signal.suggestion);
16627
+ }
16628
+ for (const hit of scanText(draftContent)) {
16629
+ addIssue(hit.line, hit.rule, hit.phrase || hit.text || "");
16630
+ }
16631
+ issues.sort((a, b) => {
16632
+ const tier = tierSortKey(a.rule) - tierSortKey(b.rule);
16633
+ if (tier !== 0)
16634
+ return tier;
16635
+ if (a.line !== b.line)
16636
+ return a.line - b.line;
16637
+ return a.rule.localeCompare(b.rule);
16638
+ });
16639
+ return issues;
16640
+ }
16641
+ function formatIssueLines(issues, tierOneOnly) {
16642
+ const filtered = issues.filter((i) => i.tierOne === tierOneOnly);
16643
+ if (!filtered.length)
16644
+ return "- none found";
16645
+ return filtered.map((i) => `- line ${i.line} [${i.rule}]: ${i.detail}`).join("\n");
16646
+ }
16491
16647
  function buildRewritePrompt(options) {
16492
16648
  const {
16493
16649
  draftPath,
@@ -16498,16 +16654,19 @@ function buildRewritePrompt(options) {
16498
16654
  skipScan = false
16499
16655
  } = options;
16500
16656
  const { block: profileBlock, name: profileUsed } = buildProfileBlock(fullProfile, profileName);
16501
- const signals = skipScan ? [] : extractSignals(draftContent, fullProfile).signals;
16502
- const issueLines = signals.length > 0 ? signals.map((s) => `- line ${s.line} [${s.id}]: ${s.suggestion}`).join("\n") : "- none found by deterministic scan";
16657
+ const issues = skipScan ? [] : collectPromptIssues(draftContent, fullProfile);
16658
+ const tierOneLines = formatIssueLines(issues, true);
16659
+ const otherLines = formatIssueLines(issues, false);
16503
16660
  const draftLines = draftContent.split("\n");
16504
16661
  const numberedDraft = draftLines.map((line, i) => `${i + 1}: ${line}`).join("\n");
16505
16662
  const template = loadPromptTemplate();
16506
- const prompt = template.replace("{profile_block}", profileBlock).replace("{constraints_block}", constraints || "(none)").replace("{issue_lines}", issueLines).replace("{draft_name}", draftPath).replace("{numbered_draft}", numberedDraft);
16663
+ const prompt = template.replace("{profile_block}", profileBlock).replace("{constraints_block}", constraints || "(none)").replace("{tier_one_lines}", tierOneLines).replace("{other_lines}", otherLines).replace("{draft_name}", draftPath).replace("{numbered_draft}", numberedDraft);
16507
16664
  return {
16508
- prompt,
16665
+ prompt: skipScan ? `${prompt}
16666
+
16667
+ none found by deterministic scan` : prompt,
16509
16668
  profileUsed,
16510
- issuesFound: signals.length
16669
+ issuesFound: issues.length
16511
16670
  };
16512
16671
  }
16513
16672
 
@@ -17781,6 +17940,7 @@ var MCP_SERVER_ARGS = ["mcp", "--stdio"];
17781
17940
  // src/lib/cli-entry.ts
17782
17941
  function resolveCliEntry() {
17783
17942
  const candidates = [
17943
+ typeof __filename !== "undefined" ? path17.resolve(__filename) : "",
17784
17944
  path17.resolve(process.argv[1] || ""),
17785
17945
  path17.resolve(__dirname, "index.js"),
17786
17946
  path17.resolve(__dirname, "..", "dist", "index.js")
@@ -17792,7 +17952,7 @@ function mcpServerCommand() {
17792
17952
  if (entry) {
17793
17953
  return { command: process.execPath, args: [entry, ...MCP_SERVER_ARGS] };
17794
17954
  }
17795
- return { command: "hyv", args: [...MCP_SERVER_ARGS] };
17955
+ throw new Error("could not resolve hyv CLI entry \u2014 reinstall @holdyourvoice/hyv or run via node dist/index.js");
17796
17956
  }
17797
17957
  function mcpServerSnippet() {
17798
17958
  const { command, args: args2 } = mcpServerCommand();
@@ -19670,6 +19830,10 @@ async function resolveProfile(slug) {
19670
19830
  }
19671
19831
  return profile;
19672
19832
  }
19833
+ async function loadLocalProfileForTool(slug) {
19834
+ void ensureMcpProfilesHydrated();
19835
+ return loadProfileForCommand(slug);
19836
+ }
19673
19837
  function jsonRpcOk(id, result) {
19674
19838
  return JSON.stringify({ jsonrpc: "2.0", id, result });
19675
19839
  }
@@ -19727,7 +19891,7 @@ async function toolScan(args2) {
19727
19891
  return "Error: no text or file provided";
19728
19892
  try {
19729
19893
  let scanText2 = text;
19730
- const profile = await resolveProfile(args2.profile);
19894
+ const profile = await loadLocalProfileForTool(args2.profile);
19731
19895
  if (filePath) {
19732
19896
  try {
19733
19897
  scanText2 = readScanFile(filePath);
@@ -19775,7 +19939,7 @@ async function toolValidate(args2) {
19775
19939
  if (!text.trim())
19776
19940
  return "Error: no text provided";
19777
19941
  try {
19778
- const profile = await resolveProfile(profileSlug);
19942
+ const profile = await loadLocalProfileForTool(profileSlug);
19779
19943
  if (!profile)
19780
19944
  return "Error: no voice profile available. Run hyv init or use hyv scan for free generic checks.";
19781
19945
  const result = validateRewrite(text, text, profile);
@@ -19798,7 +19962,7 @@ async function toolFix(args2) {
19798
19962
  if (!text.trim())
19799
19963
  return "Error: no text provided";
19800
19964
  try {
19801
- const profile = await resolveProfile(args2.profile);
19965
+ const profile = await loadLocalProfileForTool(args2.profile);
19802
19966
  const result = runPipeline(text, profile, true);
19803
19967
  if (result.changes.length === 0)
19804
19968
  return "No auto-fixable issues found.";
@@ -19817,7 +19981,7 @@ async function toolCheck(args2) {
19817
19981
  if (!text.trim())
19818
19982
  return "Error: no text provided";
19819
19983
  try {
19820
- const profile = await resolveProfile(args2.profile);
19984
+ const profile = await loadLocalProfileForTool(args2.profile);
19821
19985
  const result = runPipeline(text, profile, false);
19822
19986
  if (result.stats.totalSignals === 0)
19823
19987
  return `Clean \u2014 score: ${result.score}/100. No AI patterns found.`;
@@ -19835,7 +19999,7 @@ async function toolScore(args2) {
19835
19999
  if (!text.trim())
19836
20000
  return "Error: no text provided";
19837
20001
  try {
19838
- const profile = await resolveProfile(args2.profile);
20002
+ const profile = await loadLocalProfileForTool(args2.profile);
19839
20003
  const result = runPipeline(text, profile, false);
19840
20004
  return result.score.toString();
19841
20005
  } catch (err) {
@@ -19847,7 +20011,7 @@ async function toolDiff(args2) {
19847
20011
  if (!text.trim())
19848
20012
  return "Error: no text provided";
19849
20013
  try {
19850
- const profile = await resolveProfile(args2.profile);
20014
+ const profile = await loadLocalProfileForTool(args2.profile);
19851
20015
  const result = runPipeline(text, profile, true);
19852
20016
  if (result.changes.length === 0)
19853
20017
  return "No auto-fixable changes available.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.25",
3
+ "version": "2.9.27",
4
4
  "description": "Free local AI writing scan for cursor & claude. MCP server, 220+ pattern detection, voice profiles. npx @holdyourvoice/hyv welcome",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,21 @@
1
+ # `cli/scripts/`
2
+
3
+ ## Purpose
4
+
5
+ `cli/scripts/` contains package install, publish validation, smoke-test, and supporting CLI scripts.
6
+
7
+ ## Contracts
8
+
9
+ Scripts are referenced by `cli/package.json` and the npm package `files` list. Package-relative paths must remain valid when the package is published.
10
+
11
+ ## Validation
12
+
13
+ ```bash
14
+ cd cli
15
+ npm run validate:publish
16
+ npm run test:smoke
17
+ ```
18
+
19
+ ## Documentation ownership
20
+
21
+ Changes under this folder update this README, `cli/README.md`, and the root `README.md`.
@@ -0,0 +1,20 @@
1
+ # `cli/skills/`
2
+
3
+ ## Purpose
4
+
5
+ `cli/skills/` contains skills included in the published CLI package.
6
+
7
+ ## Contracts
8
+
9
+ The directory is included by the `files` list in `cli/package.json`. Changes must preserve package inclusion and any postinstall or agent configuration paths that reference these files.
10
+
11
+ ## Validation
12
+
13
+ ```bash
14
+ cd cli
15
+ npm run validate:publish
16
+ ```
17
+
18
+ ## Documentation ownership
19
+
20
+ Changes update this README, `cli/README.md`, and the root `README.md`.