skillwiki 0.2.0-beta.26 → 0.2.0-beta.28

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-XM5IYZX7.js";
5
5
 
6
6
  // src/cli.ts
7
- import { readFileSync as readFileSync4 } from "fs";
7
+ import { readFileSync as readFileSync5 } from "fs";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // src/utils/output.ts
@@ -72,7 +72,8 @@ var ExitCode = {
72
72
  DRIFT_DETECTED: 32,
73
73
  RAW_DEDUP_DETECTED: 33,
74
74
  MIGRATION_APPLIED: 34,
75
- UNKNOWN_WIKI_PROFILE: 35
75
+ UNKNOWN_WIKI_PROFILE: 35,
76
+ DEDUP_APPLIED: 36
76
77
  };
77
78
 
78
79
  // ../shared/src/json-output.ts
@@ -796,6 +797,9 @@ var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
796
797
  function stripFences(body) {
797
798
  return body.replace(FENCE2, "").replace(INLINE_CODE, "");
798
799
  }
800
+ function stripFencedBlocks(body) {
801
+ return body.replace(FENCE2, "");
802
+ }
799
803
  function extractCitationMarkers(body) {
800
804
  const stripped = stripFences(body);
801
805
  const out = [];
@@ -806,7 +810,7 @@ function extractCitationMarkers(body) {
806
810
  return out;
807
811
  }
808
812
  function hasSourcesFooter(body) {
809
- return /^## Sources\s*$/m.test(stripFences(body));
813
+ return /^## Sources\s*$/m.test(stripFencedBlocks(body));
810
814
  }
811
815
  function isLegacyCitationStyle(body) {
812
816
  const markers = extractCitationMarkers(body);
@@ -898,7 +902,7 @@ async function runAudit(input) {
898
902
  return { ...m, resolved: false };
899
903
  }
900
904
  }));
901
- const sources = fm.data.sources ?? [];
905
+ const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
902
906
  const referenced = new Set(resolved.map((m) => m.target));
903
907
  const unused_sources = sources.filter((s) => !referenced.has(s));
904
908
  const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
@@ -1303,8 +1307,7 @@ async function runInit(input) {
1303
1307
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1304
1308
  });
1305
1309
  if (err22) return err22;
1306
- const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
1307
- const skipEnv = !!input.noEnv || isTempPath;
1310
+ const skipEnv = !!input.noEnv;
1308
1311
  let envWritten = "";
1309
1312
  if (!skipEnv) {
1310
1313
  try {
@@ -1600,6 +1603,8 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1600
1603
  }
1601
1604
 
1602
1605
  // src/commands/dedup.ts
1606
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
1607
+ import { join as join13 } from "path";
1603
1608
  async function runDedup(input) {
1604
1609
  const scan = await scanVault(input.vault);
1605
1610
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -1616,17 +1621,63 @@ async function runDedup(input) {
1616
1621
  else hashMap.set(sha, [raw.relPath]);
1617
1622
  }
1618
1623
  const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
1619
- const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
1624
+ const rewired = [];
1625
+ const removed = [];
1626
+ if (input.apply && duplicates.length > 0) {
1627
+ const replacements = /* @__PURE__ */ new Map();
1628
+ for (const group of duplicates) {
1629
+ const canonical = group.files[0];
1630
+ for (let i = 1; i < group.files.length; i++) {
1631
+ replacements.set(group.files[i], canonical);
1632
+ }
1633
+ }
1634
+ for (const page of scan.data.typedKnowledge) {
1635
+ const text = readFileSync(join13(input.vault, page.relPath), "utf-8");
1636
+ let updated = text;
1637
+ let changed = false;
1638
+ for (const [oldPath, newPath] of replacements) {
1639
+ const oldMarker = `^[${oldPath}]`;
1640
+ const newMarker = `^[${newPath}]`;
1641
+ if (updated.includes(oldMarker)) {
1642
+ updated = updated.replaceAll(oldMarker, newMarker);
1643
+ changed = true;
1644
+ }
1645
+ const oldFm = `- "^[${oldPath}]"`;
1646
+ const newFm = `- "^[${newPath}]"`;
1647
+ if (updated.includes(oldFm)) {
1648
+ updated = updated.replaceAll(oldFm, newFm);
1649
+ changed = true;
1650
+ }
1651
+ }
1652
+ if (changed) {
1653
+ writeFileSync(join13(input.vault, page.relPath), updated);
1654
+ rewired.push(page.relPath);
1655
+ }
1656
+ }
1657
+ for (const [oldPath] of replacements) {
1658
+ const fullPath = join13(input.vault, oldPath);
1659
+ try {
1660
+ unlinkSync(fullPath);
1661
+ removed.push(oldPath);
1662
+ } catch {
1663
+ }
1664
+ }
1665
+ }
1666
+ const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
1620
1667
  const hintLines = [`scanned: ${totalFiles} raw files`];
1621
1668
  if (duplicates.length > 0) {
1622
1669
  hintLines.push(`duplicates: ${duplicates.length}`);
1623
1670
  for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
1671
+ if (input.apply) {
1672
+ hintLines.push(`rewired: ${rewired.length} pages`);
1673
+ hintLines.push(`removed: ${removed.length} raw files`);
1674
+ }
1624
1675
  } else {
1625
1676
  hintLines.push("0 duplicates");
1626
1677
  }
1627
1678
  return {
1628
1679
  exitCode,
1629
- result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
1680
+ result: ok({ scanned: totalFiles, duplicates, rewired, removed, humanHint: hintLines.join("\n") })
1630
1681
  };
1631
1682
  }
1632
1683
 
@@ -1645,8 +1696,8 @@ function hasDuplicateFrontmatter(body) {
1645
1696
  return false;
1646
1697
  }
1647
1698
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1648
- var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
1649
- var INFO_ORDER = ["bridges", "low_confidence_single_source", "page_structure", "topic_map_recommended"];
1699
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
1700
+ var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
1650
1701
  async function runLint(input) {
1651
1702
  const buckets = {};
1652
1703
  const links = await runLinks({ vault: input.vault });
@@ -1690,20 +1741,31 @@ async function runLint(input) {
1690
1741
  const dedup = await runDedup({ vault: input.vault });
1691
1742
  if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
1692
1743
  const scan = await scanVault(input.vault);
1744
+ const slugs = scan.ok ? buildSlugMap(scan.data.typedKnowledge) : /* @__PURE__ */ new Map();
1693
1745
  if (scan.ok) {
1694
1746
  const legacyPages = [];
1695
1747
  const orphanedPages = [];
1696
1748
  const structFlags = [];
1697
1749
  const dupFrontmatter = [];
1698
1750
  const noOverview = [];
1751
+ const fmWikilinkFlags = [];
1699
1752
  for (const page of scan.data.typedKnowledge) {
1700
1753
  const text = await readPage(page);
1701
1754
  const split = splitFrontmatter(text);
1702
1755
  if (!split.ok) continue;
1703
1756
  const body = split.data.body;
1757
+ const rawFm = split.data.rawFrontmatter;
1704
1758
  if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
1705
1759
  if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
1706
1760
  if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
1761
+ const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
1762
+ for (const link of fmLinks) {
1763
+ const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
1764
+ const tail = target.split("/").pop();
1765
+ if (!slugs.has(tail.toLowerCase())) {
1766
+ fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
1767
+ }
1768
+ }
1707
1769
  const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
1708
1770
  const hasOverview = /^## Overview/m.test(body);
1709
1771
  if (!hasOverview) noOverview.push(page.relPath);
@@ -1723,6 +1785,7 @@ async function runLint(input) {
1723
1785
  if (structFlags.length > 0) buckets.page_structure = structFlags;
1724
1786
  if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
1725
1787
  if (noOverview.length > 0) buckets.missing_overview = noOverview;
1788
+ if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
1726
1789
  }
1727
1790
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1728
1791
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -1758,12 +1821,12 @@ async function runLint(input) {
1758
1821
  // src/commands/config.ts
1759
1822
  import { readFile as readFile12 } from "fs/promises";
1760
1823
  import { existsSync } from "fs";
1761
- import { join as join13 } from "path";
1824
+ import { join as join14 } from "path";
1762
1825
  function validateKey(key) {
1763
1826
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
1764
1827
  }
1765
1828
  function configPath(home) {
1766
- return join13(home, ".skillwiki", ".env");
1829
+ return join14(home, ".skillwiki", ".env");
1767
1830
  }
1768
1831
  async function runConfigGet(input) {
1769
1832
  if (!validateKey(input.key)) {
@@ -1817,13 +1880,13 @@ async function runConfigPath(input) {
1817
1880
  }
1818
1881
 
1819
1882
  // src/commands/doctor.ts
1820
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
1821
- import { join as join15 } from "path";
1883
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1884
+ import { join as join16 } from "path";
1822
1885
  import { execSync } from "child_process";
1823
1886
 
1824
1887
  // src/utils/auto-update.ts
1825
- import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
1826
- import { join as join14, dirname as dirname6 } from "path";
1888
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
1889
+ import { join as join15, dirname as dirname6 } from "path";
1827
1890
  import { spawn } from "child_process";
1828
1891
 
1829
1892
  // src/utils/update-consts.ts
@@ -1834,11 +1897,11 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
1834
1897
 
1835
1898
  // src/utils/auto-update.ts
1836
1899
  function cachePath(home) {
1837
- return join14(home, ".skillwiki", CACHE_FILENAME);
1900
+ return join15(home, ".skillwiki", CACHE_FILENAME);
1838
1901
  }
1839
1902
  function readCacheRaw(home) {
1840
1903
  try {
1841
- const raw = readFileSync(cachePath(home), "utf8");
1904
+ const raw = readFileSync2(cachePath(home), "utf8");
1842
1905
  return JSON.parse(raw);
1843
1906
  } catch {
1844
1907
  return null;
@@ -1854,7 +1917,7 @@ function readCache(home) {
1854
1917
  function writeCache(home, cache) {
1855
1918
  const p = cachePath(home);
1856
1919
  mkdirSync(dirname6(p), { recursive: true });
1857
- writeFileSync(p, JSON.stringify(cache, null, 2));
1920
+ writeFileSync2(p, JSON.stringify(cache, null, 2));
1858
1921
  }
1859
1922
  function latestFromCache(home, currentVersion) {
1860
1923
  const { cache } = readCache(home);
@@ -1937,9 +2000,9 @@ function checkVaultStructure(resolvedPath) {
1937
2000
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1938
2001
  }
1939
2002
  const missing = [];
1940
- if (!existsSync3(join15(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
2003
+ if (!existsSync3(join16(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1941
2004
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
1942
- if (!existsSync3(join15(resolvedPath, dir))) missing.push(dir + "/");
2005
+ if (!existsSync3(join16(resolvedPath, dir))) missing.push(dir + "/");
1943
2006
  }
1944
2007
  if (missing.length === 0) {
1945
2008
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -1947,7 +2010,7 @@ function checkVaultStructure(resolvedPath) {
1947
2010
  return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1948
2011
  }
1949
2012
  function checkSkillsInstalled(home) {
1950
- const skillsDir = join15(home, ".claude", "skills");
2013
+ const skillsDir = join16(home, ".claude", "skills");
1951
2014
  if (!existsSync3(skillsDir)) {
1952
2015
  return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1953
2016
  }
@@ -1968,12 +2031,12 @@ function checkNpmUpdate(home, currentVersion) {
1968
2031
  return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
1969
2032
  }
1970
2033
  function checkPluginVersionDrift(home, currentVersion) {
1971
- const pluginJsonPath = join15(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
2034
+ const pluginJsonPath = join16(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
1972
2035
  if (!existsSync3(pluginJsonPath)) {
1973
2036
  return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
1974
2037
  }
1975
2038
  try {
1976
- const content = readFileSync2(pluginJsonPath, { encoding: "utf8" });
2039
+ const content = readFileSync3(pluginJsonPath, { encoding: "utf8" });
1977
2040
  const pluginData = JSON.parse(content);
1978
2041
  const pluginVersion = pluginData.version;
1979
2042
  if (!pluginVersion) {
@@ -2015,7 +2078,7 @@ async function checkProfiles(home) {
2015
2078
  }
2016
2079
  async function checkProjectLocalOverride(cwd) {
2017
2080
  const dir = cwd ?? process.cwd();
2018
- const envPath = join15(dir, ".skillwiki", ".env");
2081
+ const envPath = join16(dir, ".skillwiki", ".env");
2019
2082
  if (existsSync3(envPath)) {
2020
2083
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
2021
2084
  }
@@ -2031,9 +2094,9 @@ function findSkillMd(dir) {
2031
2094
  }
2032
2095
  for (const entry of entries) {
2033
2096
  if (entry.isFile() && entry.name === "SKILL.md") {
2034
- results.push(join15(dir, entry.name));
2097
+ results.push(join16(dir, entry.name));
2035
2098
  } else if (entry.isDirectory()) {
2036
- results.push(...findSkillMd(join15(dir, entry.name)));
2099
+ results.push(...findSkillMd(join16(dir, entry.name)));
2037
2100
  }
2038
2101
  }
2039
2102
  return results;
@@ -2077,7 +2140,7 @@ async function runDoctor(input) {
2077
2140
 
2078
2141
  // src/commands/archive.ts
2079
2142
  import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
2080
- import { join as join16, dirname as dirname7 } from "path";
2143
+ import { join as join17, dirname as dirname7 } from "path";
2081
2144
  async function runArchive(input) {
2082
2145
  const scan = await scanVault(input.vault);
2083
2146
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -2089,10 +2152,10 @@ async function runArchive(input) {
2089
2152
  }
2090
2153
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
2091
2154
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
2092
- const archivePath = join16("_archive", relPath);
2093
- await mkdir5(dirname7(join16(input.vault, archivePath)), { recursive: true });
2155
+ const archivePath = join17("_archive", relPath);
2156
+ await mkdir5(dirname7(join17(input.vault, archivePath)), { recursive: true });
2094
2157
  let indexUpdated = false;
2095
- const indexPath = join16(input.vault, "index.md");
2158
+ const indexPath = join17(input.vault, "index.md");
2096
2159
  try {
2097
2160
  const idx = await readFile13(indexPath, "utf8");
2098
2161
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
@@ -2105,7 +2168,7 @@ async function runArchive(input) {
2105
2168
  } catch (e) {
2106
2169
  if (e?.code !== "ENOENT") throw e;
2107
2170
  }
2108
- await rename3(join16(input.vault, relPath), join16(input.vault, archivePath));
2171
+ await rename3(join17(input.vault, relPath), join17(input.vault, archivePath));
2109
2172
  return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
2110
2173
  }
2111
2174
 
@@ -2404,10 +2467,10 @@ ${newBody}`;
2404
2467
 
2405
2468
  // src/commands/update.ts
2406
2469
  import { execSync as execSync2 } from "child_process";
2407
- import { readFileSync as readFileSync3 } from "fs";
2470
+ import { readFileSync as readFileSync4 } from "fs";
2408
2471
  async function runUpdate(input) {
2409
2472
  const pkg2 = JSON.parse(
2410
- readFileSync3(new URL("../../package.json", import.meta.url), "utf8")
2473
+ readFileSync4(new URL("../../package.json", import.meta.url), "utf8")
2411
2474
  );
2412
2475
  const currentVersion = pkg2.version;
2413
2476
  const tag = input.distTag ?? "beta";
@@ -2464,7 +2527,7 @@ async function runUpdate(input) {
2464
2527
  }
2465
2528
 
2466
2529
  // src/cli.ts
2467
- var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
2530
+ var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
2468
2531
  var program = new Command();
2469
2532
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
2470
2533
  program.option("--human", "render terminal-readable output instead of JSON");
@@ -2603,10 +2666,10 @@ program.command("drift [vault]").description("detect content drift in raw source
2603
2666
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2604
2667
  else emit(await runDrift({ vault: v.vault }));
2605
2668
  });
2606
- program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2669
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2607
2670
  const v = await resolveVaultArg(vault, opts.wiki);
2608
2671
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2609
- else emit(await runDedup({ vault: v.vault }));
2672
+ else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
2610
2673
  });
2611
2674
  program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2612
2675
  const v = await resolveVaultArg(vault, opts.wiki);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.26",
3
+ "version": "0.2.0-beta.28",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.26",
3
+ "version": "0.2.0-beta.28",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.0-beta.26",
3
+ "version": "0.2.0-beta.28",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -24,7 +24,8 @@ Standard four reads (SCHEMA, index, log, project context if applicable).
24
24
  1. Identify the target page. Confirm with the user which page to archive (show full relPath).
25
25
  2. Run `skillwiki archive <page> [vault]`. Read the JSON output.
26
26
  3. Verify with `skillwiki index-check [vault]` — confirm no ghost entries remain.
27
- 4. Append a `log.md` entry: `## [{date}] archive | {relPath} _archive/{subdir}/`.
27
+ 4. Run `skillwiki lint [vault]` — check for broken wikilinks from other pages that still reference the archived page. If found, update those pages to point to the replacement or remove the stale link.
28
+ 5. Append a `log.md` entry: `## [{date}] archive | {relPath} → _archive/{subdir}/`.
28
29
 
29
30
  ## Reversibility
30
31
 
@@ -18,7 +18,7 @@ Standard four reads.
18
18
 
19
19
  0. Resolve vault: `skillwiki path` (record source for context).
20
20
  1. Run `skillwiki lint <vault>`. Read the JSON.
21
- 2. Reason over findings; present grouped by severity with concrete suggested actions per kind.
21
+ 2. Reason over findings; present grouped by severity with concrete suggested actions per kind. If the CLI was recently updated with new lint checks, re-running lint on the full vault may flag pre-existing pages that predate the new rule — treat these as legitimate findings, not false positives.
22
22
  3. If `log_rotate_needed` is present and the user consents, run `skillwiki log-rotate <vault> --apply`. Otherwise leave alone.
23
23
  4. Append one `log.md` entry summarizing the lint counts (errors/warnings/info).
24
24