skillwiki 0.9.61 → 0.9.62

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
@@ -3,15 +3,18 @@ import {
3
3
  ExitCode,
4
4
  FLEET_REL_PATH,
5
5
  MetaSchema,
6
+ RawSourceSchema,
6
7
  SATELLITE_STALE_MS,
7
- TypedKnowledgeSchema,
8
+ acquireLock,
9
+ acquireOwnedSyncLock,
8
10
  appendLastOp,
11
+ assertTargetInsideVault,
9
12
  assessSourceIdentity,
13
+ atomicWriteText,
10
14
  buildDegradedReasons,
11
15
  buildRemoteObjectPath,
12
16
  clearLastOp,
13
17
  configPath,
14
- detectSchema,
15
18
  err,
16
19
  evaluateSatelliteRunHealth,
17
20
  extractCitationMarkers,
@@ -19,6 +22,8 @@ import {
19
22
  extractTaxonomy,
20
23
  findPlugin,
21
24
  fixPathTooLong,
25
+ getCliSessionId,
26
+ getSessionId,
22
27
  isBlockedHost,
23
28
  isFailedRunStatus,
24
29
  isValidRemoteDeleteCap,
@@ -29,12 +34,19 @@ import {
29
34
  parseDotenvFile,
30
35
  parseDotenvText,
31
36
  planAndMaybePruneRemoteObjects,
37
+ prepareTypedPage,
32
38
  probeRemoteHealth,
33
39
  profileKey,
34
40
  readCliPackageJson,
35
41
  readLastOp,
42
+ readLock,
36
43
  readPage,
37
44
  readSatelliteLatestRunFromText,
45
+ reconcileTaxonomyDocument,
46
+ redactSensitiveContent,
47
+ releaseLock,
48
+ releaseOwnedSyncLock,
49
+ renderIndexUpsert,
38
50
  resolveFleetHostId,
39
51
  resolveInitTimePath,
40
52
  resolveRuntimePath,
@@ -53,6 +65,7 @@ import {
53
65
  runIndexLinkFormat,
54
66
  runLinks,
55
67
  runLint,
68
+ runLogAppend,
56
69
  runLogRotate,
57
70
  runMemoryImport,
58
71
  runMemoryIndex,
@@ -76,8 +89,10 @@ import {
76
89
  scanVault,
77
90
  snapshotterAliasForLocalHost,
78
91
  splitFrontmatter,
92
+ taxonomyCommentForPage,
93
+ upsertIndexEntry,
79
94
  writeDotenv
80
- } from "./chunk-FU462DVS.js";
95
+ } from "./chunk-2PENIQ3A.js";
81
96
  import {
82
97
  normalizeDistTag,
83
98
  readCache,
@@ -87,7 +102,7 @@ import {
87
102
  } from "./chunk-7I2TPIV5.js";
88
103
 
89
104
  // src/cli.ts
90
- import { join as join26 } from "path";
105
+ import { join as join25 } from "path";
91
106
  import { Command } from "commander";
92
107
 
93
108
  // src/utils/output.ts
@@ -853,110 +868,9 @@ async function runClaim(input) {
853
868
  };
854
869
  }
855
870
 
856
- // src/commands/log-append.ts
857
- import { readFile as readFile5, rename as rename2, writeFile as writeFile4, stat as stat3 } from "fs/promises";
858
- import { join as join8 } from "path";
859
-
860
- // src/utils/log-lock.ts
861
- import { existsSync as existsSync2, mkdirSync, statSync as statSync2, unlinkSync, writeFileSync } from "fs";
862
- import { join as join7 } from "path";
863
- function logLockPath(vault) {
864
- return join7(vault, ".skillwiki", "log-append.lock");
865
- }
866
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
867
- async function acquireLogLock(vault, opts = {}) {
868
- const retryMs = opts.retryMs ?? 2e3;
869
- const pollMs = opts.pollMs ?? 50;
870
- const staleMs = opts.staleMs ?? 1e4;
871
- const path = logLockPath(vault);
872
- const dir = join7(vault, ".skillwiki");
873
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
874
- const deadline = Date.now() + retryMs;
875
- const content = JSON.stringify({ pid: process.pid, acquired: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
876
- for (; ; ) {
877
- try {
878
- writeFileSync(path, content, { flag: "wx" });
879
- return { ok: true };
880
- } catch (e) {
881
- const err2 = e;
882
- if (err2.code !== "EEXIST") throw err2;
883
- }
884
- try {
885
- const age = Date.now() - statSync2(path).mtimeMs;
886
- if (age > staleMs) {
887
- unlinkSync(path);
888
- continue;
889
- }
890
- } catch {
891
- continue;
892
- }
893
- if (Date.now() >= deadline) return { ok: false };
894
- await sleep(pollMs);
895
- }
896
- }
897
- function releaseLogLock(vault) {
898
- try {
899
- unlinkSync(logLockPath(vault));
900
- } catch {
901
- }
902
- }
903
-
904
- // src/commands/log-append.ts
905
- var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
906
- async function runLogAppend(input) {
907
- try {
908
- await stat3(join8(input.vault, "SCHEMA.md"));
909
- } catch {
910
- return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
911
- }
912
- const content = (input.content ?? "").trim();
913
- if (content.length === 0) {
914
- return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--content must be a non-empty log entry" }) };
915
- }
916
- const acquired = await acquireLogLock(input.vault);
917
- if (!acquired.ok) {
918
- return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
919
- }
920
- const logPath = join8(input.vault, "log.md");
921
- try {
922
- let logText;
923
- try {
924
- logText = await readFile5(logPath, "utf8");
925
- } catch {
926
- return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
927
- }
928
- const entriesBefore = [...logText.matchAll(ENTRY_RE)].length;
929
- const body = logText.replace(/\s+$/, "");
930
- const next = `${body}
931
-
932
- ${content}
933
- `;
934
- try {
935
- const tmp = logPath + ".tmp";
936
- await writeFile4(tmp, next, "utf8");
937
- await rename2(tmp, logPath);
938
- } catch (e) {
939
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
940
- }
941
- appendLastOp(input.vault, {
942
- operation: "log-append",
943
- summary: `appended log entry (${entriesBefore}->${entriesBefore + 1})`,
944
- files: ["log.md"],
945
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
946
- });
947
- const entriesAfter = entriesBefore + 1;
948
- return {
949
- exitCode: ExitCode.OK,
950
- result: ok({ entries_before: entriesBefore, entries_after: entriesAfter, appended: true, humanHint: `appended log entry (${entriesBefore}->${entriesAfter})` })
951
- };
952
- } finally {
953
- releaseLogLock(input.vault);
954
- }
955
- }
956
-
957
871
  // src/commands/health.ts
958
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
959
- import { dirname as dirname4, join as join9, resolve as resolve2 } from "path";
872
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
873
+ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
960
874
  import { platform } from "os";
961
875
  function statusFromCounts(counts) {
962
876
  if ((counts.error ?? 0) > 0) return "error";
@@ -1067,7 +981,7 @@ function summarizeChecks(checks) {
1067
981
  };
1068
982
  }
1069
983
  function classifyLog(path, id, label, okPattern) {
1070
- if (!existsSync3(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
984
+ if (!existsSync2(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
1071
985
  const lines = readFileSync2(path, "utf8").split(/\r?\n/).filter(Boolean);
1072
986
  if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
1073
987
  const statusLine = [...lines].reverse().find(
@@ -1089,12 +1003,12 @@ function runVaultSyncHealth(home, syncMode) {
1089
1003
  };
1090
1004
  }
1091
1005
  const isMac = platform() === "darwin";
1092
- const shareDir = isMac ? join9(home, "Library", "Application Support", "vault-sync", "bin") : join9(home, ".local", "share", "vault-sync", "bin");
1093
- const logDir = isMac ? join9(home, "Library", "Logs") : join9(home, ".local", "state", "vault-sync", "log");
1094
- const filterPath = join9(home, ".config", "rclone", "wiki-push-filters.txt");
1006
+ const shareDir = isMac ? join7(home, "Library", "Application Support", "vault-sync", "bin") : join7(home, ".local", "share", "vault-sync", "bin");
1007
+ const logDir = isMac ? join7(home, "Library", "Logs") : join7(home, ".local", "state", "vault-sync", "log");
1008
+ const filterPath = join7(home, ".config", "rclone", "wiki-push-filters.txt");
1095
1009
  const checks = [];
1096
- const pushScript = join9(shareDir, "wiki-push.sh");
1097
- if (syncMode === "optional" && !existsSync3(pushScript)) {
1010
+ const pushScript = join7(shareDir, "wiki-push.sh");
1011
+ if (syncMode === "optional" && !existsSync2(pushScript)) {
1098
1012
  return {
1099
1013
  status: "pass",
1100
1014
  blocking: false,
@@ -1107,23 +1021,23 @@ function runVaultSyncHealth(home, syncMode) {
1107
1021
  }]
1108
1022
  };
1109
1023
  }
1110
- checks.push(existsSync3(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
1024
+ checks.push(existsSync2(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
1111
1025
  if (isMac) {
1112
- const pushPlist = join9(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
1113
- const fetchPlist = join9(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
1114
- checks.push(existsSync3(pushPlist) && existsSync3(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
1026
+ const pushPlist = join7(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
1027
+ const fetchPlist = join7(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
1028
+ checks.push(existsSync2(pushPlist) && existsSync2(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
1115
1029
  checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
1116
1030
  } else {
1117
- const pushTimer = join9(home, ".config", "systemd", "user", "wiki-push.timer");
1118
- const fetchTimer = join9(home, ".config", "systemd", "user", "wiki-fetch.timer");
1119
- const fuseTimer = join9(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
1120
- const fuseService = join9(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
1121
- checks.push(existsSync3(pushTimer) && existsSync3(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
1122
- checks.push(existsSync3(fuseTimer) && existsSync3(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
1123
- }
1124
- checks.push(classifyLog(join9(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
1125
- checks.push(classifyLog(join9(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
1126
- if (!existsSync3(filterPath)) {
1031
+ const pushTimer = join7(home, ".config", "systemd", "user", "wiki-push.timer");
1032
+ const fetchTimer = join7(home, ".config", "systemd", "user", "wiki-fetch.timer");
1033
+ const fuseTimer = join7(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
1034
+ const fuseService = join7(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
1035
+ checks.push(existsSync2(pushTimer) && existsSync2(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
1036
+ checks.push(existsSync2(fuseTimer) && existsSync2(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
1037
+ }
1038
+ checks.push(classifyLog(join7(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
1039
+ checks.push(classifyLog(join7(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
1040
+ if (!existsSync2(filterPath)) {
1127
1041
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
1128
1042
  } else {
1129
1043
  const content = readFileSync2(filterPath, "utf8");
@@ -1219,9 +1133,9 @@ function buildHumanHint(report) {
1219
1133
  return lines.join("\n");
1220
1134
  }
1221
1135
  function writeReport(out, report) {
1222
- mkdirSync2(dirname4(out), { recursive: true });
1136
+ mkdirSync(dirname4(out), { recursive: true });
1223
1137
  const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
1224
- writeFileSync2(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
1138
+ writeFileSync(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
1225
1139
  renameSync(tmp, out);
1226
1140
  }
1227
1141
  function buildIncompleteReport(input, syncMode) {
@@ -1445,8 +1359,8 @@ async function runHealth(input) {
1445
1359
  }
1446
1360
 
1447
1361
  // src/commands/archive.ts
1448
- import { rename as rename3, mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
1449
- import { join as join10, dirname as dirname5 } from "path";
1362
+ import { rename as rename2, mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1363
+ import { join as join8, dirname as dirname5 } from "path";
1450
1364
  function countWikilinks(body, slug) {
1451
1365
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1452
1366
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -1480,7 +1394,7 @@ async function runArchive(input) {
1480
1394
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1481
1395
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1482
1396
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
1483
- const archivePath = join10("_archive", relPath).replace(/\\/g, "/");
1397
+ const archivePath = join8("_archive", relPath).replace(/\\/g, "/");
1484
1398
  const remoteRoot = normalizeRemoteRoot(input.remote);
1485
1399
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1486
1400
  let cascade;
@@ -1506,7 +1420,7 @@ async function runArchive(input) {
1506
1420
  const indexRefs = [];
1507
1421
  if (!isRaw) {
1508
1422
  try {
1509
- const idx = await readFile6(join10(input.vault, "index.md"), "utf8");
1423
+ const idx = await readFile5(join8(input.vault, "index.md"), "utf8");
1510
1424
  idx.split("\n").forEach((line, i) => {
1511
1425
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
1512
1426
  });
@@ -1533,8 +1447,8 @@ async function runArchive(input) {
1533
1447
  }
1534
1448
  if (input.cascade && input.apply && cascade) {
1535
1449
  for (const ref of cascade.source_array_refs) {
1536
- const absPath = join10(input.vault, ref.page);
1537
- const text = await readFile6(absPath, "utf8");
1450
+ const absPath = join8(input.vault, ref.page);
1451
+ const text = await readFile5(absPath, "utf8");
1538
1452
  const split = splitFrontmatter(text);
1539
1453
  if (!split.ok) continue;
1540
1454
  const before = split.data.rawFrontmatter;
@@ -1545,29 +1459,29 @@ async function runArchive(input) {
1545
1459
  );
1546
1460
  if (fmRewritten === before) continue;
1547
1461
  if (!arraysEqual(ref.sources_after, ref.sources_before)) {
1548
- await writeFile5(absPath, `---
1462
+ await writeFile4(absPath, `---
1549
1463
  ${fmRewritten}
1550
1464
  ---${split.data.body}`, "utf8");
1551
1465
  }
1552
1466
  }
1553
1467
  }
1554
- await mkdir5(dirname5(join10(input.vault, archivePath)), { recursive: true });
1468
+ await mkdir5(dirname5(join8(input.vault, archivePath)), { recursive: true });
1555
1469
  let indexUpdated = false;
1556
1470
  if (!isRaw) {
1557
- const indexPath = join10(input.vault, "index.md");
1471
+ const indexPath = join8(input.vault, "index.md");
1558
1472
  try {
1559
- const idx = await readFile6(indexPath, "utf8");
1473
+ const idx = await readFile5(indexPath, "utf8");
1560
1474
  const originalLines = idx.split("\n");
1561
1475
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1562
1476
  if (filtered.length !== originalLines.length) {
1563
- await writeFile5(indexPath, filtered.join("\n"), "utf8");
1477
+ await writeFile4(indexPath, filtered.join("\n"), "utf8");
1564
1478
  indexUpdated = true;
1565
1479
  }
1566
1480
  } catch (e) {
1567
1481
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
1568
1482
  }
1569
1483
  }
1570
- await rename3(join10(input.vault, relPath), join10(input.vault, archivePath));
1484
+ await rename2(join8(input.vault, relPath), join8(input.vault, archivePath));
1571
1485
  appendLastOp(input.vault, {
1572
1486
  operation: input.cascade ? "archive-cascade" : "archive",
1573
1487
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -1911,14 +1825,14 @@ ${migratedBody}${newFooter}`;
1911
1825
 
1912
1826
  // src/commands/update.ts
1913
1827
  import { execSync } from "child_process";
1914
- import { join as join11 } from "path";
1828
+ import { join as join9 } from "path";
1915
1829
  function resolveGlobalSkillsRoot() {
1916
1830
  try {
1917
1831
  const globalRoot = execSync("npm root -g", {
1918
1832
  encoding: "utf8",
1919
1833
  timeout: 5e3
1920
1834
  }).trim();
1921
- return join11(globalRoot, "skillwiki", "skills");
1835
+ return join9(globalRoot, "skillwiki", "skills");
1922
1836
  } catch {
1923
1837
  return null;
1924
1838
  }
@@ -1946,7 +1860,7 @@ async function runUpdate(input) {
1946
1860
  const pkg2 = readCliPackageJson();
1947
1861
  const currentVersion = pkg2.version;
1948
1862
  const tag = normalizeDistTag(input.distTag);
1949
- const target = join11(input.home, ".claude", "skills");
1863
+ const target = join9(input.home, ".claude", "skills");
1950
1864
  let latest;
1951
1865
  try {
1952
1866
  latest = execSync(`npm view skillwiki@${tag} version`, {
@@ -2023,15 +1937,15 @@ async function runUpdate(input) {
2023
1937
 
2024
1938
  // src/commands/self-update.ts
2025
1939
  import { execSync as execSync2 } from "child_process";
2026
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2027
- import { join as join12 } from "path";
1940
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1941
+ import { join as join10 } from "path";
2028
1942
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
2029
1943
  async function runSelfUpdate(input) {
2030
1944
  const currentVersion = readCliPackageJson().version;
2031
1945
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
2032
1946
  const distTag = normalizeDistTag(input.distTag);
2033
- const localPkgPath = join12(sourceRoot, "packages", "cli", "package.json");
2034
- const hasLocalSource = existsSync4(localPkgPath);
1947
+ const localPkgPath = join10(sourceRoot, "packages", "cli", "package.json");
1948
+ const hasLocalSource = existsSync3(localPkgPath);
2035
1949
  if (input.check) {
2036
1950
  let availableVersion = null;
2037
1951
  let source;
@@ -2162,10 +2076,10 @@ async function runSelfUpdate(input) {
2162
2076
  }
2163
2077
 
2164
2078
  // src/commands/transcripts.ts
2165
- import { readdir as readdir3, stat as stat4, readFile as readFile7 } from "fs/promises";
2166
- import { join as join13 } from "path";
2079
+ import { readdir as readdir3, stat as stat3, readFile as readFile6 } from "fs/promises";
2080
+ import { join as join11 } from "path";
2167
2081
  async function runTranscripts(input) {
2168
- const dir = join13(input.vault, "raw", "transcripts");
2082
+ const dir = join11(input.vault, "raw", "transcripts");
2169
2083
  let entries;
2170
2084
  try {
2171
2085
  entries = await readdir3(dir, { withFileTypes: true });
@@ -2175,13 +2089,13 @@ async function runTranscripts(input) {
2175
2089
  const transcripts = [];
2176
2090
  for (const entry of entries) {
2177
2091
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2178
- const filePath = join13(dir, entry.name);
2179
- const content = await readFile7(filePath, "utf8");
2092
+ const filePath = join11(dir, entry.name);
2093
+ const content = await readFile6(filePath, "utf8");
2180
2094
  const fm = extractFrontmatter(content);
2181
2095
  if (!fm.ok) continue;
2182
2096
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
2183
2097
  if (input.since && ingested && ingested < input.since) continue;
2184
- const s = await stat4(filePath);
2098
+ const s = await stat3(filePath);
2185
2099
  transcripts.push({
2186
2100
  file: `raw/transcripts/${entry.name}`,
2187
2101
  ingested,
@@ -2193,10 +2107,10 @@ async function runTranscripts(input) {
2193
2107
  }
2194
2108
 
2195
2109
  // src/commands/compound.ts
2196
- import { writeFile as writeFile6, mkdir as mkdir6, readdir as readdir4, unlink as unlink2 } from "fs/promises";
2197
- import { join as join14 } from "path";
2198
- import { existsSync as existsSync5 } from "fs";
2199
- import { readFile as readFile8 } from "fs/promises";
2110
+ import { writeFile as writeFile5, mkdir as mkdir6, readdir as readdir4, unlink as unlink2 } from "fs/promises";
2111
+ import { join as join12 } from "path";
2112
+ import { existsSync as existsSync4 } from "fs";
2113
+ import { readFile as readFile7 } from "fs/promises";
2200
2114
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
2201
2115
  var FIELD_RE = {
2202
2116
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -2294,17 +2208,17 @@ function extractRetroFields(date, cycleName, block) {
2294
2208
  };
2295
2209
  }
2296
2210
  async function runCompound(input) {
2297
- const logPath = join14(input.vault, "log.md");
2211
+ const logPath = join12(input.vault, "log.md");
2298
2212
  let logText;
2299
2213
  try {
2300
- logText = await readFile8(logPath, "utf8");
2214
+ logText = await readFile7(logPath, "utf8");
2301
2215
  } catch {
2302
2216
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2303
2217
  }
2304
2218
  const entries = parseRetroEntries(logText);
2305
2219
  const promoted = [];
2306
2220
  const skipped = [];
2307
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
2221
+ const compoundDir = join12(input.vault, "projects", input.project, "compound");
2308
2222
  for (const entry of entries) {
2309
2223
  const generalizeValue = entry.generalize.trim();
2310
2224
  if (!/^yes/i.test(generalizeValue)) {
@@ -2312,8 +2226,8 @@ async function runCompound(input) {
2312
2226
  continue;
2313
2227
  }
2314
2228
  const slug = slugify(entry.cycleName);
2315
- const compoundPath = join14(compoundDir, `${slug}.md`);
2316
- if (existsSync5(compoundPath)) {
2229
+ const compoundPath = join12(compoundDir, `${slug}.md`);
2230
+ if (existsSync4(compoundPath)) {
2317
2231
  skipped.push(entry.date);
2318
2232
  continue;
2319
2233
  }
@@ -2351,10 +2265,10 @@ async function runCompound(input) {
2351
2265
  ].join("\n");
2352
2266
  const content = frontmatter + "\n" + body;
2353
2267
  if (!input.dryRun) {
2354
- if (!existsSync5(compoundDir)) {
2268
+ if (!existsSync4(compoundDir)) {
2355
2269
  await mkdir6(compoundDir, { recursive: true });
2356
2270
  }
2357
- await writeFile6(compoundPath, content, "utf8");
2271
+ await writeFile5(compoundPath, content, "utf8");
2358
2272
  }
2359
2273
  promoted.push(`${slug}.md`);
2360
2274
  }
@@ -2373,16 +2287,16 @@ async function runCompound(input) {
2373
2287
  };
2374
2288
  }
2375
2289
  async function runCompoundDelete(input) {
2376
- const projectDir = join14(input.vault, "projects", input.project);
2377
- if (!existsSync5(projectDir)) {
2290
+ const projectDir = join12(input.vault, "projects", input.project);
2291
+ if (!existsSync4(projectDir)) {
2378
2292
  return {
2379
2293
  exitCode: ExitCode.PROJECT_NOT_FOUND,
2380
2294
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
2381
2295
  };
2382
2296
  }
2383
2297
  const entryName = input.entry.replace(/\.md$/, "");
2384
- const compoundPath = join14(projectDir, "compound", `${entryName}.md`);
2385
- if (!existsSync5(compoundPath)) {
2298
+ const compoundPath = join12(projectDir, "compound", `${entryName}.md`);
2299
+ if (!existsSync4(compoundPath)) {
2386
2300
  return {
2387
2301
  exitCode: ExitCode.FILE_NOT_FOUND,
2388
2302
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -2415,8 +2329,8 @@ knowledge.md regenerated`
2415
2329
  };
2416
2330
  }
2417
2331
  async function runCompoundList(input) {
2418
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
2419
- if (!existsSync5(compoundDir)) {
2332
+ const compoundDir = join12(input.vault, "projects", input.project, "compound");
2333
+ if (!existsSync4(compoundDir)) {
2420
2334
  return {
2421
2335
  exitCode: ExitCode.OK,
2422
2336
  result: ok({
@@ -2446,10 +2360,10 @@ could not read compound directory`
2446
2360
  const entries = [];
2447
2361
  for (const dirent of dirents) {
2448
2362
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
2449
- const filePath = join14(compoundDir, dirent.name);
2363
+ const filePath = join12(compoundDir, dirent.name);
2450
2364
  let text;
2451
2365
  try {
2452
- text = await readFile8(filePath, "utf8");
2366
+ text = await readFile7(filePath, "utf8");
2453
2367
  } catch {
2454
2368
  continue;
2455
2369
  }
@@ -2478,8 +2392,8 @@ no compound entries found`;
2478
2392
  }
2479
2393
 
2480
2394
  // src/commands/session-brief.ts
2481
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
2482
- import { join as join15, relative, sep } from "path";
2395
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2396
+ import { join as join13, relative, sep } from "path";
2483
2397
  var MAX_WORDS = 900;
2484
2398
  async function runSessionBrief(input) {
2485
2399
  const scan = await scanVault(input.vault);
@@ -2579,7 +2493,7 @@ async function resolveProject(input) {
2579
2493
  const envProject = input.env?.SKILLWIKI_PROJECT;
2580
2494
  if (envProject) return envProject;
2581
2495
  const cwd = input.cwd ?? process.cwd();
2582
- const projectDotenv = await readProjectSlug(join15(cwd, ".skillwiki", ".env"));
2496
+ const projectDotenv = await readProjectSlug(join13(cwd, ".skillwiki", ".env"));
2583
2497
  if (projectDotenv) return projectDotenv;
2584
2498
  const inferred = inferProjectFromPath(input.vault, cwd);
2585
2499
  if (inferred) return inferred;
@@ -2588,7 +2502,7 @@ async function resolveProject(input) {
2588
2502
  async function readProjectSlug(file) {
2589
2503
  let text;
2590
2504
  try {
2591
- text = await readFile9(file, "utf8");
2505
+ text = await readFile8(file, "utf8");
2592
2506
  } catch {
2593
2507
  return void 0;
2594
2508
  }
@@ -2665,7 +2579,7 @@ async function loadTrendDigests(typedPages) {
2665
2579
  return out;
2666
2580
  }
2667
2581
  async function loadSessionPins(vault, project) {
2668
- const text = await readIfExists(join15(vault, "meta", "session-pins.md"));
2582
+ const text = await readIfExists(join13(vault, "meta", "session-pins.md"));
2669
2583
  if (!text) return [];
2670
2584
  const fm = extractFrontmatter(text);
2671
2585
  if (!fm.ok) return [];
@@ -2782,7 +2696,7 @@ function satelliteHealthWarnings(warning) {
2782
2696
  return warning ? [warning] : [];
2783
2697
  }
2784
2698
  async function loadHealthWarnings(vault) {
2785
- const text = await readIfExists(join15(vault, ".skillwiki", "health.json"));
2699
+ const text = await readIfExists(join13(vault, ".skillwiki", "health.json"));
2786
2700
  if (!text) return [];
2787
2701
  try {
2788
2702
  const parsed = JSON.parse(text);
@@ -2795,7 +2709,7 @@ async function loadHealthWarnings(vault) {
2795
2709
  }
2796
2710
  }
2797
2711
  async function loadMemoryTopics(vault, project) {
2798
- const text = await readIfExists(join15(vault, ".skillwiki", "memory", project, "topics.json"));
2712
+ const text = await readIfExists(join13(vault, ".skillwiki", "memory", project, "topics.json"));
2799
2713
  if (!text) return [];
2800
2714
  try {
2801
2715
  const parsed = JSON.parse(text);
@@ -2812,18 +2726,18 @@ async function loadMemoryTopics(vault, project) {
2812
2726
  }
2813
2727
  }
2814
2728
  async function writeBriefArtifacts(vault, input) {
2815
- const metaPath = join15(vault, "meta", "latest-session-brief.md");
2816
- const cacheMdPath = join15(vault, ".skillwiki", "session-brief.md");
2817
- const cacheJsonPath = join15(vault, ".skillwiki", "session-brief.json");
2818
- await mkdir7(join15(vault, "meta"), { recursive: true });
2819
- await mkdir7(join15(vault, ".skillwiki"), { recursive: true });
2729
+ const metaPath = join13(vault, "meta", "latest-session-brief.md");
2730
+ const cacheMdPath = join13(vault, ".skillwiki", "session-brief.md");
2731
+ const cacheJsonPath = join13(vault, ".skillwiki", "session-brief.json");
2732
+ await mkdir7(join13(vault, "meta"), { recursive: true });
2733
+ await mkdir7(join13(vault, ".skillwiki"), { recursive: true });
2820
2734
  const committed = renderCommittedBrief(input);
2821
2735
  const previousComparable = comparableBrief(await readIfExists(metaPath));
2822
2736
  const nextComparable = comparableBrief(committed);
2823
2737
  const materialChange = previousComparable !== nextComparable;
2824
- await writeFile7(metaPath, committed, "utf8");
2825
- await writeFile7(cacheMdPath, input.brief, "utf8");
2826
- await writeFile7(cacheJsonPath, `${JSON.stringify({
2738
+ await writeFile6(metaPath, committed, "utf8");
2739
+ await writeFile6(cacheMdPath, input.brief, "utf8");
2740
+ await writeFile6(cacheJsonPath, `${JSON.stringify({
2827
2741
  project: input.project,
2828
2742
  brief: input.brief,
2829
2743
  word_count: input.wordCount,
@@ -2877,7 +2791,7 @@ function renderCommittedBrief(input) {
2877
2791
  ].filter((line) => line !== "").join("\n");
2878
2792
  }
2879
2793
  async function ensureIndexEntry(vault) {
2880
- const indexPath = join15(vault, "index.md");
2794
+ const indexPath = join13(vault, "index.md");
2881
2795
  let text = await readIfExists(indexPath);
2882
2796
  if (!text) return false;
2883
2797
  if (text.includes("[[meta/latest-session-brief]]")) return false;
@@ -2892,21 +2806,21 @@ async function ensureIndexEntry(vault) {
2892
2806
  while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
2893
2807
  lines.splice(insertAt, 0, entry);
2894
2808
  }
2895
- await writeFile7(indexPath, lines.join("\n"), "utf8");
2809
+ await writeFile6(indexPath, lines.join("\n"), "utf8");
2896
2810
  return true;
2897
2811
  }
2898
2812
  async function appendMaterialLog(vault, today) {
2899
- const logPath = join15(vault, "log.md");
2813
+ const logPath = join13(vault, "log.md");
2900
2814
  const text = await readIfExists(logPath);
2901
2815
  if (!text) return false;
2902
2816
  const entry = `
2903
2817
  ## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
2904
- await writeFile7(logPath, text.trimEnd() + entry + "\n", "utf8");
2818
+ await writeFile6(logPath, text.trimEnd() + entry + "\n", "utf8");
2905
2819
  return true;
2906
2820
  }
2907
2821
  async function readIfExists(path) {
2908
2822
  try {
2909
- return await readFile9(path, "utf8");
2823
+ return await readFile8(path, "utf8");
2910
2824
  } catch {
2911
2825
  return "";
2912
2826
  }
@@ -2948,9 +2862,419 @@ function dateFromPath(path) {
2948
2862
  }
2949
2863
 
2950
2864
  // src/commands/ingest.ts
2951
- import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir8 } from "fs/promises";
2952
- import { join as join16 } from "path";
2865
+ import { readFile as readFile10, open, unlink as unlink3, mkdir as mkdir8 } from "fs/promises";
2866
+ import { join as join15 } from "path";
2867
+ import { createHash as createHash4 } from "crypto";
2868
+
2869
+ // src/commands/page-publish.ts
2953
2870
  import { createHash as createHash3 } from "crypto";
2871
+ import { realpathSync } from "fs";
2872
+ import { readFile as readFile9 } from "fs/promises";
2873
+ import { join as join14, resolve as resolve3 } from "path";
2874
+ var DEFAULT_DEPS = { afterStage: async () => void 0 };
2875
+ function errorExitCode(error) {
2876
+ switch (error) {
2877
+ case "FILE_NOT_FOUND":
2878
+ return ExitCode.FILE_NOT_FOUND;
2879
+ case "MISSING_CLOSING_DELIMITER":
2880
+ return ExitCode.MISSING_CLOSING_DELIMITER;
2881
+ case "SCHEME_REJECTED":
2882
+ case "NO_TAXONOMY_BLOCK":
2883
+ return ExitCode.SCHEME_REJECTED;
2884
+ case "VAULT_PATH_INVALID":
2885
+ return ExitCode.VAULT_PATH_INVALID;
2886
+ case "SENSITIVE_CONTENT_DETECTED":
2887
+ return ExitCode.SENSITIVE_CONTENT_DETECTED;
2888
+ case "SYNC_LOCK_HELD":
2889
+ return ExitCode.SYNC_LOCK_HELD;
2890
+ case "WRITE_FAILED":
2891
+ return ExitCode.WRITE_FAILED;
2892
+ default:
2893
+ return ExitCode.INVALID_FRONTMATTER;
2894
+ }
2895
+ }
2896
+ function publicationId(target, content, logNote = "") {
2897
+ return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
2898
+ }
2899
+ function prepareFrozenPublication(input, source) {
2900
+ const target = assertTargetInsideVault(input.vault, input.target);
2901
+ if (!target.ok) return target;
2902
+ const page = prepareTypedPage(input.content, input.target);
2903
+ if (!page.ok) return page;
2904
+ if (input.logNote !== void 0 && /[\r\n]/.test(input.logNote)) {
2905
+ return err("SCHEME_REJECTED", { message: "log note must be one line" });
2906
+ }
2907
+ const logNote = input.logNote?.trim() || void 0;
2908
+ if (logNote && Buffer.byteLength(logNote, "utf8") > 500) {
2909
+ return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
2910
+ }
2911
+ if (logNote && scanSensitiveContent(logNote, { file: "page-publish log note" }).length > 0) {
2912
+ return err("SENSITIVE_CONTENT_DETECTED", { message: "log note contains sensitive authentication material" });
2913
+ }
2914
+ const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2915
+ const taxonomyComment = taxonomyCommentForPage(input.target, date);
2916
+ if (!taxonomyComment.ok) return taxonomyComment;
2917
+ return ok({
2918
+ page: page.data,
2919
+ source,
2920
+ targetPath: target.data.absolutePath,
2921
+ logNote,
2922
+ operationId: publicationId(input.target, input.content, logNote),
2923
+ date,
2924
+ taxonomyComment: taxonomyComment.data
2925
+ });
2926
+ }
2927
+ function preparePagePublicationFromContent(input) {
2928
+ return prepareFrozenPublication(input, { kind: "content" });
2929
+ }
2930
+ async function preparePagePublication(input) {
2931
+ let content;
2932
+ try {
2933
+ content = await readFile9(input.draftPath, "utf8");
2934
+ } catch (error) {
2935
+ return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
2936
+ }
2937
+ let draftRealPath;
2938
+ try {
2939
+ draftRealPath = realpathSync(input.draftPath);
2940
+ } catch (error) {
2941
+ return err("VAULT_PATH_INVALID", {
2942
+ path: input.draftPath,
2943
+ message: `draft realpath failed: ${String(error)}`
2944
+ });
2945
+ }
2946
+ const target = assertTargetInsideVault(input.vault, input.target);
2947
+ if (!target.ok) return target;
2948
+ if (resolve3(input.draftPath) === target.data.absolutePath || target.data.existingRealPath !== void 0 && draftRealPath === target.data.existingRealPath) {
2949
+ return err("VAULT_PATH_INVALID", { message: "draft must not alias the final target" });
2950
+ }
2951
+ return prepareFrozenPublication(
2952
+ {
2953
+ vault: input.vault,
2954
+ content,
2955
+ target: input.target,
2956
+ logNote: input.logNote,
2957
+ now: input.now
2958
+ },
2959
+ { kind: "file", realPath: draftRealPath }
2960
+ );
2961
+ }
2962
+ function emptyLockedState() {
2963
+ return {
2964
+ taxonomyAdded: [],
2965
+ pageChanged: false,
2966
+ indexUpdated: false,
2967
+ published: false,
2968
+ changed: /* @__PURE__ */ new Set()
2969
+ };
2970
+ }
2971
+ function lockedFailure(stage, state, cause, exitCode = ExitCode.WRITE_FAILED) {
2972
+ return { ok: false, exitCode, stage, state, cause };
2973
+ }
2974
+ async function observeStage(deps, stage) {
2975
+ try {
2976
+ await deps.afterStage(stage);
2977
+ return void 0;
2978
+ } catch (error) {
2979
+ return err("WRITE_FAILED", { message: `stage hook failed at ${stage}: ${String(error)}` });
2980
+ }
2981
+ }
2982
+ async function runLockedPrimaryStages(input, vault, deps) {
2983
+ const state = emptyLockedState();
2984
+ const freshTarget = assertTargetInsideVault(vault, input.page.target);
2985
+ if (!freshTarget.ok) {
2986
+ return lockedFailure("target", state, freshTarget, ExitCode.VAULT_PATH_INVALID);
2987
+ }
2988
+ if (freshTarget.data.absolutePath !== input.targetPath) {
2989
+ return lockedFailure(
2990
+ "target",
2991
+ state,
2992
+ err("VAULT_PATH_INVALID", { message: "target canonical path changed after preparation" }),
2993
+ ExitCode.VAULT_PATH_INVALID
2994
+ );
2995
+ }
2996
+ if (input.source.kind === "file" && freshTarget.data.existingRealPath !== void 0 && freshTarget.data.existingRealPath === input.source.realPath) {
2997
+ return lockedFailure(
2998
+ "target",
2999
+ state,
3000
+ err("VAULT_PATH_INVALID", { message: "draft now aliases the final target" }),
3001
+ ExitCode.VAULT_PATH_INVALID
3002
+ );
3003
+ }
3004
+ const schemaPath = join14(vault, "SCHEMA.md");
3005
+ let schemaText;
3006
+ try {
3007
+ schemaText = await readFile9(schemaPath, "utf8");
3008
+ } catch (error) {
3009
+ return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
3010
+ }
3011
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
3012
+ tags: input.page.tags,
3013
+ comment: input.taxonomyComment
3014
+ });
3015
+ if (!reconciled.ok) {
3016
+ return lockedFailure("schema", state, reconciled, errorExitCode(reconciled.error));
3017
+ }
3018
+ state.taxonomyAdded = reconciled.data.added;
3019
+ if (reconciled.data.changed) {
3020
+ const schemaWrite = await atomicWriteText(schemaPath, reconciled.data.text);
3021
+ if (!schemaWrite.ok) return lockedFailure("schema", state, schemaWrite);
3022
+ if (schemaWrite.data.changed) state.changed.add("SCHEMA.md");
3023
+ }
3024
+ const schemaHook = await observeStage(deps, "schema");
3025
+ if (schemaHook) return lockedFailure("schema", state, schemaHook);
3026
+ const pageWrite = await safeWritePage(input.targetPath, input.page.content);
3027
+ if (!pageWrite.ok) return lockedFailure("page", state, pageWrite);
3028
+ state.pageChanged = pageWrite.data.changed;
3029
+ if (state.pageChanged) state.changed.add(input.page.target);
3030
+ state.published = true;
3031
+ const pageHook = await observeStage(deps, "page");
3032
+ if (pageHook) return lockedFailure("page", state, pageHook);
3033
+ let visible;
3034
+ let visibleSchema;
3035
+ try {
3036
+ [visible, visibleSchema] = await Promise.all([
3037
+ readFile9(input.targetPath, "utf8"),
3038
+ readFile9(schemaPath, "utf8")
3039
+ ]);
3040
+ } catch (error) {
3041
+ return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
3042
+ }
3043
+ const visiblePage = prepareTypedPage(visible, input.page.target);
3044
+ const visibleTaxonomy = extractTaxonomy(visibleSchema);
3045
+ if (!visiblePage.ok || visible !== input.page.content || !visibleTaxonomy.ok || input.page.tags.some((tag) => !visibleTaxonomy.data.includes(tag))) {
3046
+ return lockedFailure(
3047
+ "verify",
3048
+ state,
3049
+ err("WRITE_FAILED", { message: "published bytes or taxonomy verification failed" })
3050
+ );
3051
+ }
3052
+ const verifyHook = await observeStage(deps, "verify");
3053
+ if (verifyHook) return lockedFailure("verify", state, verifyHook);
3054
+ const index = await upsertIndexEntry({
3055
+ vault,
3056
+ target: input.page.target,
3057
+ title: input.page.title,
3058
+ type: input.page.type
3059
+ });
3060
+ if (!index.ok) return lockedFailure("index", state, index);
3061
+ state.indexUpdated = index.data.changed;
3062
+ if (state.indexUpdated) state.changed.add("index.md");
3063
+ const indexHook = await observeStage(deps, "index");
3064
+ if (indexHook) return lockedFailure("index", state, indexHook);
3065
+ return { ok: true, data: state };
3066
+ }
3067
+ function redactDetail(detail) {
3068
+ if (detail === void 0) return void 0;
3069
+ try {
3070
+ const encoded = JSON.stringify(detail);
3071
+ return JSON.parse(redactSensitiveContent(encoded).text);
3072
+ } catch {
3073
+ return { message: "unserializable error detail omitted" };
3074
+ }
3075
+ }
3076
+ function phaseFailure(stage, input, published, cause, context = {}, exitCode = ExitCode.WRITE_FAILED) {
3077
+ return {
3078
+ exitCode,
3079
+ result: err("WRITE_FAILED", {
3080
+ ...context,
3081
+ stage,
3082
+ published,
3083
+ target: input.page.target,
3084
+ operation_id: input.operationId,
3085
+ retry_safe: stage !== "target",
3086
+ cause_error: cause.error,
3087
+ cause_detail: redactDetail(cause.detail)
3088
+ })
3089
+ };
3090
+ }
3091
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
3092
+ return {
3093
+ exitCode: ExitCode.OK,
3094
+ result: ok({
3095
+ target: input.page.target,
3096
+ page_type: input.page.type,
3097
+ tags: [...input.page.tags],
3098
+ taxonomy_added: [...taxonomyAdded],
3099
+ page_changed: pageChanged,
3100
+ index_updated: indexUpdated,
3101
+ log_appended: logAppended,
3102
+ operation_id: input.operationId,
3103
+ dry_run: dryRun,
3104
+ files_changed: filesChanged,
3105
+ humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
3106
+ })
3107
+ };
3108
+ }
3109
+ function renderPublicationLog(input, added) {
3110
+ return [
3111
+ `## [${input.date}] page-publish | ${input.page.target}`,
3112
+ "",
3113
+ `- Published: [[${input.page.target.replace(/\.md$/, "")}]]`,
3114
+ `- Taxonomy: ${added.length > 0 ? `added ${added.join(", ")}` : "no additions"}`,
3115
+ ...input.logNote ? [`- Note: ${input.logNote}`] : []
3116
+ ].join("\n");
3117
+ }
3118
+ async function readPageChanged(targetPath, content) {
3119
+ try {
3120
+ return ok(await readFile9(targetPath, "utf8") !== content);
3121
+ } catch (error) {
3122
+ if (error.code === "ENOENT") return ok(true);
3123
+ return err("WRITE_FAILED", { path: targetPath, message: String(error) });
3124
+ }
3125
+ }
3126
+ async function previewPreparedPagePublication(input, vault) {
3127
+ const schemaPath = join14(vault, "SCHEMA.md");
3128
+ let schemaText;
3129
+ try {
3130
+ schemaText = await readFile9(schemaPath, "utf8");
3131
+ } catch (error) {
3132
+ const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
3133
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result };
3134
+ }
3135
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
3136
+ tags: input.page.tags,
3137
+ comment: input.taxonomyComment
3138
+ });
3139
+ if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
3140
+ const pageChanged = await readPageChanged(input.targetPath, input.page.content);
3141
+ if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
3142
+ const indexPath = join14(vault, "index.md");
3143
+ let indexText;
3144
+ try {
3145
+ indexText = await readFile9(indexPath, "utf8");
3146
+ } catch (error) {
3147
+ const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
3148
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result };
3149
+ }
3150
+ const index = renderIndexUpsert(indexText, {
3151
+ target: input.page.target,
3152
+ title: input.page.title,
3153
+ type: input.page.type
3154
+ });
3155
+ if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
3156
+ const logPath = join14(vault, "log.md");
3157
+ let logText;
3158
+ try {
3159
+ logText = await readFile9(logPath, "utf8");
3160
+ } catch (error) {
3161
+ const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
3162
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result };
3163
+ }
3164
+ const logAppended = !logText.includes(`<!-- skillwiki-page-publish:${input.operationId} -->`);
3165
+ const filesChanged = [
3166
+ ...reconciled.data.changed ? ["SCHEMA.md"] : [],
3167
+ ...pageChanged.data ? [input.page.target] : [],
3168
+ ...index.data.changed ? ["index.md"] : [],
3169
+ ...logAppended ? ["log.md"] : []
3170
+ ];
3171
+ return successReceipt(
3172
+ input,
3173
+ reconciled.data.added,
3174
+ pageChanged.data,
3175
+ index.data.changed,
3176
+ logAppended,
3177
+ filesChanged,
3178
+ true
3179
+ );
3180
+ }
3181
+ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
3182
+ let lock;
3183
+ try {
3184
+ lock = acquireOwnedSyncLock(vault, {
3185
+ summary: `page publish ${input.page.target}`,
3186
+ ttlMinutes: 1
3187
+ });
3188
+ } catch (error) {
3189
+ return {
3190
+ exitCode: ExitCode.WRITE_FAILED,
3191
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
3192
+ };
3193
+ }
3194
+ if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
3195
+ let primary;
3196
+ let released;
3197
+ try {
3198
+ primary = await runLockedPrimaryStages(input, vault, deps);
3199
+ } catch (error) {
3200
+ primary = lockedFailure(
3201
+ "schema",
3202
+ emptyLockedState(),
3203
+ err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
3204
+ );
3205
+ } finally {
3206
+ released = releaseOwnedSyncLock(lock.data);
3207
+ }
3208
+ const primaryState = primary?.ok ? primary.data : primary?.state;
3209
+ if (released === void 0 || !released.ok || !released.data.released) {
3210
+ return phaseFailure(
3211
+ "unlock",
3212
+ input,
3213
+ primaryState?.published ?? false,
3214
+ released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
3215
+ {
3216
+ primary_stage: primary && !primary.ok ? primary.stage : "complete",
3217
+ primary_error: primary && !primary.ok ? primary.cause.error : void 0
3218
+ }
3219
+ );
3220
+ }
3221
+ const unlockHook = await observeStage(deps, "unlock");
3222
+ if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
3223
+ if (primary === void 0) {
3224
+ return phaseFailure(
3225
+ "schema",
3226
+ input,
3227
+ false,
3228
+ err("WRITE_FAILED", { message: "locked publication produced no result" })
3229
+ );
3230
+ }
3231
+ if (!primary.ok) {
3232
+ return phaseFailure(
3233
+ primary.stage,
3234
+ input,
3235
+ primary.state.published,
3236
+ primary.cause,
3237
+ void 0,
3238
+ primary.exitCode
3239
+ );
3240
+ }
3241
+ const state = primary.data;
3242
+ const log = await runLogAppend({
3243
+ vault,
3244
+ content: renderPublicationLog(input, state.taxonomyAdded),
3245
+ operationId: input.operationId,
3246
+ strictLock: true,
3247
+ recordLastOp: false
3248
+ });
3249
+ if (!log.result.ok) return phaseFailure("log", input, true, log.result);
3250
+ if (log.exitCode !== ExitCode.OK) {
3251
+ return phaseFailure(
3252
+ "log",
3253
+ input,
3254
+ true,
3255
+ err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
3256
+ );
3257
+ }
3258
+ if (log.result.data.appended) state.changed.add("log.md");
3259
+ const logHook = await observeStage(deps, "log");
3260
+ if (logHook) return phaseFailure("log", input, true, logHook);
3261
+ return successReceipt(
3262
+ input,
3263
+ state.taxonomyAdded,
3264
+ state.pageChanged,
3265
+ state.indexUpdated,
3266
+ log.result.data.appended,
3267
+ [...state.changed]
3268
+ );
3269
+ }
3270
+ async function runPagePublish(input, deps = DEFAULT_DEPS) {
3271
+ const prepared = await preparePagePublication(input);
3272
+ if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
3273
+ if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
3274
+ return publishPreparedPage(prepared.data, input.vault, deps);
3275
+ }
3276
+
3277
+ // src/commands/ingest.ts
2954
3278
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
2955
3279
  var TYPE_DIR = {
2956
3280
  entity: "entities",
@@ -2991,19 +3315,6 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
2991
3315
  const aliases = [];
2992
3316
  const sourcesYaml = ` - ${rawRelPath}`;
2993
3317
  const tagsYaml = tags.length > 0 ? tags.map((t) => ` - ${t}`).join("\n") : " []";
2994
- const fm = {
2995
- title,
2996
- aliases,
2997
- created: ingested,
2998
- updated: ingested,
2999
- type,
3000
- tags,
3001
- sources: [rawRelPath],
3002
- confidence: "medium"
3003
- };
3004
- if (provenance) {
3005
- fm.provenance = provenance;
3006
- }
3007
3318
  const fmLines = ["---"];
3008
3319
  fmLines.push(`title: "${title}"`);
3009
3320
  if (aliases.length > 0) {
@@ -3039,6 +3350,78 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
3039
3350
  ].join("\n");
3040
3351
  return fmLines.join("\n") + body;
3041
3352
  }
3353
+ async function resolveRawCapture(input) {
3354
+ try {
3355
+ const existing = await readFile10(input.path, "utf8");
3356
+ const frontmatter = extractFrontmatter(existing);
3357
+ if (!frontmatter.ok) {
3358
+ return err("INGEST_VALIDATION_FAILED", {
3359
+ path: input.path,
3360
+ message: "existing immutable raw source has invalid frontmatter",
3361
+ source_error: frontmatter.error
3362
+ });
3363
+ }
3364
+ const parsed = RawSourceSchema.safeParse(frontmatter.data);
3365
+ if (!parsed.success || parsed.data.sha256 !== input.sha256 || (parsed.data.source_url ?? null) !== input.sourceUrl || existing !== buildRawContent(
3366
+ input.sourceUrl,
3367
+ String(parsed.data.ingested),
3368
+ input.sha256,
3369
+ input.sourceContent
3370
+ )) {
3371
+ return err("INGEST_VALIDATION_FAILED", {
3372
+ path: input.path,
3373
+ message: "existing immutable raw source differs from the fetched source"
3374
+ });
3375
+ }
3376
+ return ok({
3377
+ content: existing,
3378
+ ingested: String(parsed.data.ingested),
3379
+ shouldWrite: false
3380
+ });
3381
+ } catch (error) {
3382
+ if (error.code !== "ENOENT") {
3383
+ return err("WRITE_FAILED", { path: input.path, message: String(error) });
3384
+ }
3385
+ }
3386
+ return ok({
3387
+ content: buildRawContent(input.sourceUrl, input.today, input.sha256, input.sourceContent),
3388
+ ingested: input.today,
3389
+ shouldWrite: true
3390
+ });
3391
+ }
3392
+ async function writeResolvedRaw(input) {
3393
+ if (!input.capture.shouldWrite) return ok({ changed: false, capture: input.capture });
3394
+ const lock = await acquireRawCaptureLock(input.path);
3395
+ if (!lock.ok) return lock;
3396
+ try {
3397
+ const resolved = await resolveRawCapture(input);
3398
+ if (!resolved.ok) return resolved;
3399
+ if (!resolved.data.shouldWrite) return ok({ changed: false, capture: resolved.data });
3400
+ const written = await atomicWriteText(input.path, resolved.data.content);
3401
+ return written.ok ? ok({ changed: written.data.changed, capture: resolved.data }) : written;
3402
+ } finally {
3403
+ try {
3404
+ await unlink3(lock.data);
3405
+ } catch {
3406
+ }
3407
+ }
3408
+ }
3409
+ async function acquireRawCaptureLock(path) {
3410
+ const lockPath = `${path}.ingest.lock`;
3411
+ for (let attempt = 0; attempt < 200; attempt++) {
3412
+ try {
3413
+ const handle = await open(lockPath, "wx");
3414
+ await handle.close();
3415
+ return ok(lockPath);
3416
+ } catch (error) {
3417
+ if (error.code !== "EEXIST") {
3418
+ return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
3419
+ }
3420
+ await new Promise((resolve4) => setTimeout(resolve4, 10));
3421
+ }
3422
+ }
3423
+ return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
3424
+ }
3042
3425
  async function runIngest(input) {
3043
3426
  if (!input.source || input.source.trim().length === 0) {
3044
3427
  return {
@@ -3126,15 +3509,14 @@ async function runIngest(input) {
3126
3509
  })
3127
3510
  };
3128
3511
  }
3129
- const sha256 = createHash3("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
3512
+ const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
3130
3513
  const today = todayIso();
3131
3514
  const slug = slugify2(input.title);
3132
3515
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
3133
3516
  const rawRelPath = `raw/articles/${slug}.md`;
3134
3517
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
3135
3518
  const typedRelPath = `${typedDir}/${slug}.md`;
3136
- const rawAbsPath = join16(input.vault, rawRelPath);
3137
- const typedAbsPath = join16(input.vault, typedRelPath);
3519
+ const rawAbsPath = join15(input.vault, rawRelPath);
3138
3520
  const identity = assessSourceIdentity({
3139
3521
  rawPath: rawRelPath,
3140
3522
  sourceUrl: sourceUrl ?? void 0,
@@ -3154,16 +3536,62 @@ async function runIngest(input) {
3154
3536
  })
3155
3537
  };
3156
3538
  }
3157
- const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
3158
- const typedContent = buildTypedContent(
3539
+ const resolvedRaw = await resolveRawCapture({
3540
+ path: rawAbsPath,
3541
+ sourceUrl,
3542
+ sourceContent,
3543
+ sha256,
3544
+ today
3545
+ });
3546
+ if (!resolvedRaw.ok) {
3547
+ return {
3548
+ exitCode: resolvedRaw.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
3549
+ result: resolvedRaw
3550
+ };
3551
+ }
3552
+ let publicationDate = resolvedRaw.data.ingested;
3553
+ let typedContent = buildTypedContent(
3159
3554
  input.title,
3160
- today,
3555
+ publicationDate,
3161
3556
  input.type,
3162
3557
  tags,
3163
3558
  rawRelPath,
3164
3559
  input.provenance
3165
3560
  );
3561
+ if (!input.dryRun) {
3562
+ try {
3563
+ await mkdir8(join15(input.vault, typedDir), { recursive: true });
3564
+ } catch (error) {
3565
+ return {
3566
+ exitCode: ExitCode.WRITE_FAILED,
3567
+ result: err("WRITE_FAILED", { path: join15(input.vault, typedDir), message: String(error) })
3568
+ };
3569
+ }
3570
+ }
3571
+ let publication = preparePagePublicationFromContent({
3572
+ vault: input.vault,
3573
+ content: typedContent,
3574
+ target: typedRelPath,
3575
+ logNote: `ingested from ${rawRelPath}`,
3576
+ now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
3577
+ });
3578
+ if (!publication.ok) {
3579
+ return {
3580
+ exitCode: ExitCode.INGEST_VALIDATION_FAILED,
3581
+ result: publication
3582
+ };
3583
+ }
3166
3584
  if (input.dryRun) {
3585
+ const preview = await previewPreparedPagePublication(publication.data, input.vault);
3586
+ if (!preview.result.ok) {
3587
+ return { exitCode: preview.exitCode, result: preview.result };
3588
+ }
3589
+ if (preview.exitCode !== ExitCode.OK) {
3590
+ return {
3591
+ exitCode: preview.exitCode,
3592
+ result: err("WRITE_FAILED", { message: "publication preview returned inconsistent success state" })
3593
+ };
3594
+ }
3167
3595
  return {
3168
3596
  exitCode: ExitCode.OK,
3169
3597
  result: ok({
@@ -3172,78 +3600,84 @@ async function runIngest(input) {
3172
3600
  sha256,
3173
3601
  dry_run: true,
3174
3602
  humanHint: [
3175
- `DRY RUN \u2014 would create:`,
3603
+ "DRY RUN \u2014 would create:",
3176
3604
  ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
3177
3605
  ` ${typedRelPath}`,
3178
3606
  ` type: ${input.type}, tags: [${tags.join(", ")}]`,
3179
3607
  input.provenance ? ` provenance: ${input.provenance}` : ""
3180
- ].filter(Boolean).join("\n")
3608
+ ].filter(Boolean).join("\n"),
3609
+ publication: preview.result.data
3181
3610
  })
3182
3611
  };
3183
3612
  }
3184
- const typedFm = {
3185
- title: input.title,
3186
- aliases: [],
3187
- created: today,
3188
- updated: today,
3189
- type: input.type,
3190
- tags,
3191
- sources: [rawRelPath],
3192
- confidence: "medium",
3193
- ...input.provenance ? { provenance: input.provenance } : {}
3194
- };
3195
- const det = detectSchema(typedFm);
3196
- if (!det.schema) {
3613
+ try {
3614
+ await mkdir8(join15(input.vault, "raw", "articles"), { recursive: true });
3615
+ } catch (error) {
3197
3616
  return {
3198
- exitCode: ExitCode.INGEST_VALIDATION_FAILED,
3199
- result: err("INGEST_VALIDATION_FAILED", {
3200
- message: "generated typed-knowledge page could not be detected as a valid schema"
3201
- })
3617
+ exitCode: ExitCode.WRITE_FAILED,
3618
+ result: err("WRITE_FAILED", { path: join15(input.vault, "raw", "articles"), message: String(error) })
3202
3619
  };
3203
3620
  }
3204
- const parsed = TypedKnowledgeSchema.safeParse(typedFm);
3205
- if (!parsed.success) {
3206
- const errors = parsed.error.issues.map((i) => ({
3207
- path: i.path.join("."),
3208
- message: i.message
3209
- }));
3621
+ const rawWrite = await writeResolvedRaw({
3622
+ path: rawAbsPath,
3623
+ sourceUrl,
3624
+ sourceContent,
3625
+ sha256,
3626
+ today,
3627
+ capture: resolvedRaw.data
3628
+ });
3629
+ if (!rawWrite.ok) {
3210
3630
  return {
3211
- exitCode: ExitCode.INGEST_VALIDATION_FAILED,
3212
- result: err("INGEST_VALIDATION_FAILED", {
3213
- message: "generated typed-knowledge page failed schema validation",
3214
- errors
3215
- })
3631
+ exitCode: rawWrite.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
3632
+ result: rawWrite
3216
3633
  };
3217
3634
  }
3218
- try {
3219
- await mkdir8(join16(input.vault, "raw", "articles"), { recursive: true });
3220
- await writeFile8(rawAbsPath, rawContent, "utf8");
3221
- } catch (e) {
3222
- return {
3223
- exitCode: ExitCode.WRITE_FAILED,
3224
- result: err("WRITE_FAILED", { path: rawAbsPath, message: String(e) })
3225
- };
3635
+ if (rawWrite.data.capture.ingested !== publicationDate) {
3636
+ publicationDate = rawWrite.data.capture.ingested;
3637
+ typedContent = buildTypedContent(
3638
+ input.title,
3639
+ publicationDate,
3640
+ input.type,
3641
+ tags,
3642
+ rawRelPath,
3643
+ input.provenance
3644
+ );
3645
+ publication = preparePagePublicationFromContent({
3646
+ vault: input.vault,
3647
+ content: typedContent,
3648
+ target: typedRelPath,
3649
+ logNote: `ingested from ${rawRelPath}`,
3650
+ now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
3651
+ });
3652
+ if (!publication.ok) {
3653
+ return {
3654
+ exitCode: ExitCode.INGEST_VALIDATION_FAILED,
3655
+ result: publication
3656
+ };
3657
+ }
3226
3658
  }
3227
- try {
3228
- await mkdir8(join16(input.vault, typedDir), { recursive: true });
3229
- await writeFile8(typedAbsPath, typedContent, "utf8");
3230
- } catch (e) {
3659
+ const published = await publishPreparedPage(publication.data, input.vault);
3660
+ if (!published.result.ok) {
3661
+ return { exitCode: published.exitCode, result: published.result };
3662
+ }
3663
+ if (published.exitCode !== ExitCode.OK) {
3231
3664
  return {
3232
- exitCode: ExitCode.WRITE_FAILED,
3233
- result: err("WRITE_FAILED", { path: typedAbsPath, message: String(e) })
3665
+ exitCode: published.exitCode,
3666
+ result: err("WRITE_FAILED", { message: "publisher returned inconsistent success state" })
3234
3667
  };
3235
3668
  }
3236
- const humanHint = [
3237
- `created:`,
3238
- ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
3239
- ` ${typedRelPath}`
3240
- ].join("\n");
3241
- appendLastOp(input.vault, {
3242
- operation: "ingest",
3243
- summary: `added ${slug}`,
3244
- files: [rawRelPath, typedRelPath],
3245
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3246
- });
3669
+ const changedFiles = [
3670
+ ...rawWrite.data.changed ? [rawRelPath] : [],
3671
+ ...published.result.data.files_changed
3672
+ ];
3673
+ if (changedFiles.length > 0) {
3674
+ appendLastOp(input.vault, {
3675
+ operation: "ingest",
3676
+ summary: `added ${slug}`,
3677
+ files: [...new Set(changedFiles)],
3678
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3679
+ });
3680
+ }
3247
3681
  return {
3248
3682
  exitCode: ExitCode.OK,
3249
3683
  result: ok({
@@ -3251,7 +3685,12 @@ async function runIngest(input) {
3251
3685
  typed_path: typedRelPath,
3252
3686
  sha256,
3253
3687
  dry_run: false,
3254
- humanHint
3688
+ humanHint: [
3689
+ "created:",
3690
+ ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
3691
+ ` ${typedRelPath}`
3692
+ ].join("\n"),
3693
+ publication: published.result.data
3255
3694
  })
3256
3695
  };
3257
3696
  }
@@ -3403,138 +3842,216 @@ ${body}`;
3403
3842
  };
3404
3843
  }
3405
3844
 
3406
- // src/commands/sync.ts
3407
- import { existsSync as existsSync7 } from "fs";
3408
- import { join as join18 } from "path";
3409
- import { execFileSync as execFileSync2 } from "child_process";
3410
-
3411
- // src/utils/git.ts
3412
- import { execFileSync } from "child_process";
3413
- function git(cwd, args) {
3414
- try {
3415
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3416
- } catch {
3417
- return "";
3845
+ // src/commands/tag-reconcile.ts
3846
+ import { readFile as readFile11 } from "fs/promises";
3847
+ import { join as join16, posix } from "path";
3848
+ var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
3849
+ function errorExitCode2(error) {
3850
+ switch (error) {
3851
+ case "FILE_NOT_FOUND":
3852
+ return ExitCode.FILE_NOT_FOUND;
3853
+ case "MISSING_CLOSING_DELIMITER":
3854
+ return ExitCode.MISSING_CLOSING_DELIMITER;
3855
+ case "SCHEME_REJECTED":
3856
+ return ExitCode.SCHEME_REJECTED;
3857
+ case "VAULT_PATH_INVALID":
3858
+ return ExitCode.VAULT_PATH_INVALID;
3859
+ case "WRITE_FAILED":
3860
+ return ExitCode.WRITE_FAILED;
3861
+ case "SYNC_LOCK_HELD":
3862
+ return ExitCode.SYNC_LOCK_HELD;
3863
+ default:
3864
+ return ExitCode.INVALID_FRONTMATTER;
3865
+ }
3866
+ }
3867
+ function validatePageIdentity(page) {
3868
+ const segments = page.split("/");
3869
+ if (page.length === 0 || posix.isAbsolute(page) || page.includes("\\") || posix.normalize(page) !== page || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !TYPED_TARGET_RE.test(page)) {
3870
+ return err("VAULT_PATH_INVALID", {
3871
+ page,
3872
+ message: "page must be a normalized vault-relative typed Markdown path"
3873
+ });
3418
3874
  }
3875
+ return ok(page);
3419
3876
  }
3420
- function gitStrict(cwd, args) {
3421
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3422
- }
3423
-
3424
- // src/utils/sync-lock.ts
3425
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
3426
- import { join as join17 } from "path";
3427
- import { createHash as createHash4 } from "crypto";
3428
- function getEnvSessionId() {
3429
- if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
3430
- if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
3431
- return void 0;
3432
- }
3433
- function getSessionId() {
3434
- const envSessionId = getEnvSessionId();
3435
- if (envSessionId) return envSessionId;
3436
- return process.pid.toString();
3437
- }
3438
- function getCwdHash(cwd) {
3439
- const path = cwd || process.cwd();
3440
- const hash = createHash4("sha256").update(path).digest("hex");
3441
- return hash.slice(0, 8);
3877
+ function asTagArray(frontmatter, path) {
3878
+ const tags = frontmatter.tags;
3879
+ if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
3880
+ return err("INVALID_FRONTMATTER", {
3881
+ path,
3882
+ message: "frontmatter tags must be an array of strings"
3883
+ });
3884
+ }
3885
+ return ok(tags);
3442
3886
  }
3443
- function getCliSessionId(cwd) {
3444
- const envSessionId = getEnvSessionId();
3445
- if (envSessionId) return envSessionId;
3446
- return `cli-${getCwdHash(cwd)}`;
3887
+ async function readTagsFromFile(path) {
3888
+ let text;
3889
+ try {
3890
+ text = await readFile11(path, "utf8");
3891
+ } catch (error) {
3892
+ if (error.code === "ENOENT") {
3893
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
3894
+ }
3895
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path, message: String(error) }) };
3896
+ }
3897
+ const frontmatter = extractFrontmatter(text);
3898
+ if (!frontmatter.ok) return { exitCode: errorExitCode2(frontmatter.error), result: frontmatter };
3899
+ const tags = asTagArray(frontmatter.data, path);
3900
+ if (!tags.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tags };
3901
+ return { exitCode: ExitCode.OK, result: tags };
3447
3902
  }
3448
- function lockPath(vault) {
3449
- return join17(vault, ".skillwiki", "sync.lock");
3903
+ async function resolveRequestedTags(input, page) {
3904
+ const explicit = input.tags ?? [];
3905
+ if (!Array.isArray(explicit) || !explicit.every((tag) => typeof tag === "string")) {
3906
+ return {
3907
+ exitCode: ExitCode.INVALID_FRONTMATTER,
3908
+ result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
3909
+ };
3910
+ }
3911
+ const source = input.from ?? (explicit.length === 0 ? join16(input.vault, page) : void 0);
3912
+ if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
3913
+ const sourced = await readTagsFromFile(source);
3914
+ if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
3915
+ return {
3916
+ exitCode: ExitCode.OK,
3917
+ result: ok({ tags: [.../* @__PURE__ */ new Set([...explicit, ...sourced.result.data])].sort() })
3918
+ };
3450
3919
  }
3451
- function readLock(vault) {
3452
- const path = lockPath(vault);
3453
- if (!existsSync6(path)) return null;
3920
+ async function readSchema(schemaPath) {
3454
3921
  try {
3455
- const raw = readFileSync4(path, "utf8");
3456
- return JSON.parse(raw);
3457
- } catch {
3458
- return null;
3922
+ return { exitCode: ExitCode.OK, result: ok(await readFile11(schemaPath, "utf8")) };
3923
+ } catch (error) {
3924
+ if (error.code === "ENOENT") {
3925
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
3926
+ }
3927
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: schemaPath, message: String(error) }) };
3459
3928
  }
3460
3929
  }
3461
- function isStale(lock, now) {
3462
- const nowTime = (now ?? /* @__PURE__ */ new Date()).getTime();
3463
- const expiresTime = new Date(lock.expires).getTime();
3464
- return expiresTime < nowTime;
3930
+ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
3931
+ if (!reconciled.ok) return { exitCode: errorExitCode2(reconciled.error), result: reconciled };
3932
+ const missingTags = reconciled.data.missing;
3933
+ const addedTags = dryRun ? [] : reconciled.data.added;
3934
+ const humanHint = dryRun ? missingTags.length > 0 ? `dry run: would add ${missingTags.join(", ")} to taxonomy for ${page}` : `dry run: taxonomy already includes requested tags for ${page}` : addedTags.length > 0 ? `added ${addedTags.join(", ")} to taxonomy for ${page}` : `taxonomy already includes requested tags for ${page}`;
3935
+ return {
3936
+ exitCode: ExitCode.OK,
3937
+ result: ok({
3938
+ page,
3939
+ requested_tags: tags,
3940
+ missing_tags: missingTags,
3941
+ added_tags: addedTags,
3942
+ changed: reconciled.data.changed,
3943
+ dry_run: dryRun,
3944
+ files_changed: filesChanged,
3945
+ humanHint
3946
+ })
3947
+ };
3465
3948
  }
3466
- function acquireLock(vault, opts = {}) {
3467
- const path = lockPath(vault);
3468
- const dir = join17(vault, ".skillwiki");
3469
- if (!existsSync6(dir)) {
3470
- mkdirSync3(dir, { recursive: true });
3949
+ async function reconcileTagsWhileLocked(input, page, tags, comment) {
3950
+ const schemaPath = join16(input.vault, "SCHEMA.md");
3951
+ const current = await readSchema(schemaPath);
3952
+ if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
3953
+ const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
3954
+ if (!next.ok) return { exitCode: errorExitCode2(next.error), result: next };
3955
+ if (next.data.changed) {
3956
+ const written = await atomicWriteText(schemaPath, next.data.text);
3957
+ if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
3958
+ }
3959
+ let verifiedText;
3960
+ try {
3961
+ verifiedText = await readFile11(schemaPath, "utf8");
3962
+ } catch (error) {
3963
+ return {
3964
+ exitCode: ExitCode.WRITE_FAILED,
3965
+ result: err("WRITE_FAILED", { stage: "verify-taxonomy", page, message: String(error) })
3966
+ };
3471
3967
  }
3472
- const sessionId = opts.sessionId ?? getSessionId();
3473
- const summary = opts.summary ?? "skillwiki sync";
3474
- const ttlMinutes = opts.ttlMinutes ?? 30;
3475
- const force = opts.force ?? false;
3476
- const now = /* @__PURE__ */ new Date();
3477
- const acquired = now.toISOString();
3478
- const expires = new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString();
3479
- const lock = {
3480
- session_id: sessionId,
3481
- pid: process.pid,
3482
- cwd: process.cwd(),
3483
- summary,
3484
- acquired,
3485
- expires
3486
- };
3968
+ const verified = extractTaxonomy(verifiedText);
3969
+ if (!verified.ok || tags.some((tag) => !verified.data.includes(tag))) {
3970
+ return {
3971
+ exitCode: ExitCode.WRITE_FAILED,
3972
+ result: err("WRITE_FAILED", { stage: "verify-taxonomy", page })
3973
+ };
3974
+ }
3975
+ return previewResult(page, tags, next, false, next.data.changed ? ["SCHEMA.md"] : []);
3976
+ }
3977
+ async function runTagReconcile(input) {
3978
+ const page = validatePageIdentity(input.page);
3979
+ if (!page.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: page };
3980
+ const resolvedTags = await resolveRequestedTags(input, page.data);
3981
+ if (!resolvedTags.result.ok) return { exitCode: resolvedTags.exitCode, result: resolvedTags.result };
3982
+ const tags = resolvedTags.result.data.tags;
3983
+ const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3984
+ const comment = taxonomyCommentForPage(page.data, date, input.reason);
3985
+ if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
3986
+ if (!input.write) {
3987
+ const schema = await readSchema(join16(input.vault, "SCHEMA.md"));
3988
+ if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
3989
+ const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
3990
+ return previewResult(page.data, tags, preview, true, []);
3991
+ }
3992
+ let lock;
3487
3993
  try {
3488
- const content = JSON.stringify(lock, null, 2) + "\n";
3489
- writeFileSync3(path, content, { flag: "wx" });
3490
- return { ok: true, lock };
3491
- } catch (e) {
3492
- const err2 = e;
3493
- if (err2.code !== "EEXIST") throw err2;
3494
- }
3495
- const existing = readLock(vault);
3496
- if (!existing) {
3497
- writeLockedFile(path, lock);
3498
- return { ok: true, lock };
3499
- }
3500
- if (force || isStale(existing)) {
3501
- writeLockedFile(path, lock);
3502
- return { ok: true, lock };
3503
- }
3504
- return { ok: false, held: existing };
3505
- }
3506
- function writeLockedFile(path, lock) {
3507
- const tmp = path + ".tmp";
3508
- const content = JSON.stringify(lock, null, 2) + "\n";
3509
- writeFileSync3(tmp, content);
3510
- renameSync2(tmp, path);
3511
- }
3512
- function releaseLock(vault, opts = {}) {
3513
- const path = lockPath(vault);
3514
- if (!existsSync6(path)) {
3515
- return { released: false };
3516
- }
3517
- const sessionId = opts.sessionId ?? getSessionId();
3518
- const existing = readLock(vault);
3519
- if (opts.force) {
3520
- try {
3521
- unlinkSync2(path);
3522
- const prior = existing && existing.session_id !== sessionId ? existing : void 0;
3523
- return { released: true, prior };
3524
- } catch {
3525
- return { released: false };
3526
- }
3994
+ lock = acquireOwnedSyncLock(input.vault, {
3995
+ summary: `tag reconcile ${page.data}`,
3996
+ ttlMinutes: 1
3997
+ });
3998
+ } catch (error) {
3999
+ return {
4000
+ exitCode: ExitCode.WRITE_FAILED,
4001
+ result: err("WRITE_FAILED", { stage: "lock", page: page.data, message: String(error) })
4002
+ };
3527
4003
  }
3528
- if (!existing || existing.session_id !== sessionId) {
3529
- return { released: false };
4004
+ if (!lock.ok) return { exitCode: errorExitCode2(lock.error), result: lock };
4005
+ let outcome;
4006
+ let released;
4007
+ try {
4008
+ outcome = await reconcileTagsWhileLocked(input, page.data, tags, comment.data);
4009
+ } catch (error) {
4010
+ outcome = {
4011
+ exitCode: ExitCode.WRITE_FAILED,
4012
+ result: err("WRITE_FAILED", { stage: "reconcile", page: page.data, message: String(error) })
4013
+ };
4014
+ } finally {
4015
+ released = releaseOwnedSyncLock(lock.data);
3530
4016
  }
4017
+ if (released === void 0 || !released.ok) {
4018
+ return {
4019
+ exitCode: ExitCode.WRITE_FAILED,
4020
+ result: err("WRITE_FAILED", {
4021
+ stage: "unlock",
4022
+ page: page.data,
4023
+ primary_error: outcome && !outcome.result.ok ? outcome.result.error : void 0,
4024
+ release_error: released && !released.ok ? released.detail : "release did not run"
4025
+ })
4026
+ };
4027
+ }
4028
+ return outcome ?? {
4029
+ exitCode: ExitCode.WRITE_FAILED,
4030
+ result: err("WRITE_FAILED", {
4031
+ stage: "reconcile",
4032
+ page: page.data,
4033
+ message: "locked reconciliation produced no result"
4034
+ })
4035
+ };
4036
+ }
4037
+
4038
+ // src/commands/sync.ts
4039
+ import { existsSync as existsSync5 } from "fs";
4040
+ import { join as join17 } from "path";
4041
+ import { execFileSync as execFileSync2 } from "child_process";
4042
+
4043
+ // src/utils/git.ts
4044
+ import { execFileSync } from "child_process";
4045
+ function git(cwd, args) {
3531
4046
  try {
3532
- unlinkSync2(path);
3533
- return { released: true };
4047
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3534
4048
  } catch {
3535
- return { released: false };
4049
+ return "";
3536
4050
  }
3537
4051
  }
4052
+ function gitStrict(cwd, args) {
4053
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4054
+ }
3538
4055
 
3539
4056
  // src/utils/vault-git-pathspec.ts
3540
4057
  var VAULT_GENERATED_COMMIT_PATHS = [
@@ -3587,7 +4104,7 @@ function refHasPath(vault, ref, path) {
3587
4104
  function runSyncStatus(input) {
3588
4105
  const vault = input.vault;
3589
4106
  const includeStashes = input.includeStashes ?? false;
3590
- if (!existsSync7(join18(vault, ".git"))) {
4107
+ if (!existsSync5(join17(vault, ".git"))) {
3591
4108
  return {
3592
4109
  exitCode: ExitCode.VAULT_PATH_INVALID,
3593
4110
  result: ok({
@@ -3694,7 +4211,7 @@ function runSyncStatus(input) {
3694
4211
  }
3695
4212
  async function runSyncPush(input) {
3696
4213
  const vault = input.vault;
3697
- if (!existsSync7(join18(vault, ".git"))) {
4214
+ if (!existsSync5(join17(vault, ".git"))) {
3698
4215
  return {
3699
4216
  exitCode: ExitCode.VAULT_PATH_INVALID,
3700
4217
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -3854,7 +4371,7 @@ function enableGitLongPathsOnWindows(vault) {
3854
4371
  }
3855
4372
  async function runSyncPull(input) {
3856
4373
  const vault = input.vault;
3857
- if (!existsSync7(join18(vault, ".git"))) {
4374
+ if (!existsSync5(join17(vault, ".git"))) {
3858
4375
  return {
3859
4376
  exitCode: ExitCode.VAULT_PATH_INVALID,
3860
4377
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -4026,7 +4543,7 @@ function runSyncPeers(input) {
4026
4543
  }
4027
4544
  function runSyncLock(input) {
4028
4545
  const vault = input.vault;
4029
- if (!existsSync7(vault)) {
4546
+ if (!existsSync5(vault)) {
4030
4547
  return {
4031
4548
  exitCode: ExitCode.VAULT_PATH_INVALID,
4032
4549
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4061,7 +4578,7 @@ function runSyncLock(input) {
4061
4578
  }
4062
4579
  function runSyncUnlock(input) {
4063
4580
  const vault = input.vault;
4064
- if (!existsSync7(vault)) {
4581
+ if (!existsSync5(vault)) {
4065
4582
  return {
4066
4583
  exitCode: ExitCode.VAULT_PATH_INVALID,
4067
4584
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4094,8 +4611,8 @@ function runSyncUnlock(input) {
4094
4611
  }
4095
4612
 
4096
4613
  // src/commands/backup.ts
4097
- import { statSync as statSync3, readdirSync, readFileSync as readFileSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
4098
- import { join as join19, relative as relative2, dirname as dirname6 } from "path";
4614
+ import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4615
+ import { join as join18, relative as relative2, dirname as dirname6 } from "path";
4099
4616
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
4100
4617
 
4101
4618
  // src/utils/s3-client.ts
@@ -4119,7 +4636,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
4119
4636
  function* walkMarkdown(dir, base) {
4120
4637
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
4121
4638
  if (SKIP_DIRS.has(entry.name)) continue;
4122
- const full = join19(dir, entry.name);
4639
+ const full = join18(dir, entry.name);
4123
4640
  if (entry.isDirectory()) {
4124
4641
  yield* walkMarkdown(full, base);
4125
4642
  } else if (entry.name.endsWith(".md")) {
@@ -4142,8 +4659,8 @@ async function runBackupSync(input) {
4142
4659
  let failed = 0;
4143
4660
  const files = [...walkMarkdown(input.vault, input.vault)];
4144
4661
  for (const relPath of files) {
4145
- const absPath = join19(input.vault, relPath);
4146
- const localStat = statSync3(absPath);
4662
+ const absPath = join18(input.vault, relPath);
4663
+ const localStat = statSync2(absPath);
4147
4664
  let needsUpload = true;
4148
4665
  try {
4149
4666
  const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
@@ -4161,7 +4678,7 @@ async function runBackupSync(input) {
4161
4678
  continue;
4162
4679
  }
4163
4680
  try {
4164
- const body = readFileSync5(absPath);
4681
+ const body = readFileSync4(absPath);
4165
4682
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
4166
4683
  uploaded++;
4167
4684
  } catch {
@@ -4218,9 +4735,9 @@ async function runBackupRestore(input) {
4218
4735
  const objects = list.Contents ?? [];
4219
4736
  for (const obj of objects) {
4220
4737
  if (!obj.Key) continue;
4221
- const localPath = join19(target, obj.Key);
4738
+ const localPath = join18(target, obj.Key);
4222
4739
  try {
4223
- const localStat = statSync3(localPath);
4740
+ const localStat = statSync2(localPath);
4224
4741
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
4225
4742
  conflicts++;
4226
4743
  continue;
@@ -4231,8 +4748,8 @@ async function runBackupRestore(input) {
4231
4748
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
4232
4749
  const body = await resp.Body?.transformToByteArray();
4233
4750
  if (body) {
4234
- mkdirSync4(dirname6(localPath), { recursive: true });
4235
- writeFileSync4(localPath, Buffer.from(body));
4751
+ mkdirSync2(dirname6(localPath), { recursive: true });
4752
+ writeFileSync2(localPath, Buffer.from(body));
4236
4753
  downloaded++;
4237
4754
  }
4238
4755
  } catch {
@@ -4264,11 +4781,11 @@ async function runBackupRestore(input) {
4264
4781
  }
4265
4782
 
4266
4783
  // src/commands/status.ts
4267
- import { existsSync as existsSync8, statSync as statSync4 } from "fs";
4268
- import { readFile as readFile11 } from "fs/promises";
4269
- import { join as join20 } from "path";
4784
+ import { existsSync as existsSync6, statSync as statSync3 } from "fs";
4785
+ import { readFile as readFile12 } from "fs/promises";
4786
+ import { join as join19 } from "path";
4270
4787
  async function runStatus(input) {
4271
- if (!existsSync8(input.vault)) {
4788
+ if (!existsSync6(input.vault)) {
4272
4789
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
4273
4790
  }
4274
4791
  const scan = await scanVault(input.vault);
@@ -4293,7 +4810,7 @@ async function runStatus(input) {
4293
4810
  const compound = scan.data.compound.length;
4294
4811
  let schemaVersion = "v1";
4295
4812
  try {
4296
- const schemaContent = await readFile11(join20(input.vault, "SCHEMA.md"), "utf8");
4813
+ const schemaContent = await readFile12(join19(input.vault, "SCHEMA.md"), "utf8");
4297
4814
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
4298
4815
  if (versionMatch) schemaVersion = versionMatch[1];
4299
4816
  } catch {
@@ -4309,7 +4826,7 @@ async function runStatus(input) {
4309
4826
  let maxTime = 0;
4310
4827
  for (const page of allPages) {
4311
4828
  try {
4312
- const st = statSync4(page.absPath);
4829
+ const st = statSync3(page.absPath);
4313
4830
  if (st.mtimeMs > maxTime) {
4314
4831
  maxTime = st.mtimeMs;
4315
4832
  lastModified = st.mtime.toISOString();
@@ -4353,8 +4870,8 @@ async function runStatus(input) {
4353
4870
  }
4354
4871
 
4355
4872
  // src/commands/seed.ts
4356
- import { mkdir as mkdir9, writeFile as writeFile9, stat as stat5 } from "fs/promises";
4357
- import { join as join21 } from "path";
4873
+ import { mkdir as mkdir9, writeFile as writeFile7, stat as stat4 } from "fs/promises";
4874
+ import { join as join20 } from "path";
4358
4875
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4359
4876
  var EXAMPLE_PAGES = {
4360
4877
  "entities/example-project.md": `---
@@ -4423,30 +4940,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
4423
4940
  `;
4424
4941
  async function runSeed(input) {
4425
4942
  try {
4426
- await stat5(join21(input.vault, "SCHEMA.md"));
4943
+ await stat4(join20(input.vault, "SCHEMA.md"));
4427
4944
  } catch {
4428
4945
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
4429
4946
  }
4430
4947
  const created = [];
4431
4948
  const skipped = [];
4432
4949
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
4433
- const absPath = join21(input.vault, relPath);
4950
+ const absPath = join20(input.vault, relPath);
4434
4951
  try {
4435
- await stat5(absPath);
4952
+ await stat4(absPath);
4436
4953
  skipped.push(relPath);
4437
4954
  } catch {
4438
- await mkdir9(join21(absPath, ".."), { recursive: true });
4439
- await writeFile9(absPath, content, "utf8");
4955
+ await mkdir9(join20(absPath, ".."), { recursive: true });
4956
+ await writeFile7(absPath, content, "utf8");
4440
4957
  created.push(relPath);
4441
4958
  }
4442
4959
  }
4443
- const rawPath = join21(input.vault, "raw", "articles", "example-source.md");
4960
+ const rawPath = join20(input.vault, "raw", "articles", "example-source.md");
4444
4961
  try {
4445
- await stat5(rawPath);
4962
+ await stat4(rawPath);
4446
4963
  skipped.push("raw/articles/example-source.md");
4447
4964
  } catch {
4448
- await mkdir9(join21(rawPath, ".."), { recursive: true });
4449
- await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
4965
+ await mkdir9(join20(rawPath, ".."), { recursive: true });
4966
+ await writeFile7(rawPath, EXAMPLE_RAW, "utf8");
4450
4967
  created.push("raw/articles/example-source.md");
4451
4968
  }
4452
4969
  if (created.length > 0) {
@@ -4468,9 +4985,9 @@ async function runSeed(input) {
4468
4985
  }
4469
4986
 
4470
4987
  // src/commands/canvas.ts
4471
- import { readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
4472
- import { existsSync as existsSync9 } from "fs";
4473
- import { join as join22 } from "path";
4988
+ import { readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
4989
+ import { existsSync as existsSync7 } from "fs";
4990
+ import { join as join21 } from "path";
4474
4991
  var NODE_WIDTH = 240;
4475
4992
  var NODE_HEIGHT = 60;
4476
4993
  var COLUMN_SPACING = 400;
@@ -4548,8 +5065,8 @@ function buildCanvasEdges(adjacency) {
4548
5065
  return edges;
4549
5066
  }
4550
5067
  async function runCanvasGenerate(input) {
4551
- const graphPath = input.graphPath ?? join22(input.vault, ".skillwiki", "graph.json");
4552
- if (!existsSync9(graphPath)) {
5068
+ const graphPath = input.graphPath ?? join21(input.vault, ".skillwiki", "graph.json");
5069
+ if (!existsSync7(graphPath)) {
4553
5070
  return {
4554
5071
  exitCode: ExitCode.FILE_NOT_FOUND,
4555
5072
  result: err("FILE_NOT_FOUND", {
@@ -4560,7 +5077,7 @@ async function runCanvasGenerate(input) {
4560
5077
  }
4561
5078
  let raw;
4562
5079
  try {
4563
- raw = await readFile12(graphPath, "utf8");
5080
+ raw = await readFile13(graphPath, "utf8");
4564
5081
  } catch (e) {
4565
5082
  return {
4566
5083
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -4586,9 +5103,9 @@ async function runCanvasGenerate(input) {
4586
5103
  const nodes = buildCanvasNodes(paths);
4587
5104
  const edges = buildCanvasEdges(graph.adjacency);
4588
5105
  const canvas = { nodes, edges };
4589
- const outPath = join22(input.vault, "vault-graph.canvas");
5106
+ const outPath = join21(input.vault, "vault-graph.canvas");
4590
5107
  try {
4591
- await writeFile10(outPath, JSON.stringify(canvas, null, 2));
5108
+ await writeFile8(outPath, JSON.stringify(canvas, null, 2));
4592
5109
  } catch (e) {
4593
5110
  return {
4594
5111
  exitCode: ExitCode.WRITE_FAILED,
@@ -4608,10 +5125,10 @@ written: ${outPath}`
4608
5125
  }
4609
5126
 
4610
5127
  // src/commands/fleet-health.ts
4611
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
5128
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
4612
5129
  import { execSync as nodeExecSync } from "child_process";
4613
5130
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
4614
- import { join as join23 } from "path";
5131
+ import { join as join22 } from "path";
4615
5132
  var SSH_TIMEOUT_MS = 15e3;
4616
5133
  var TIMER_UNIT = "agent-memory-trends.timer";
4617
5134
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -4708,9 +5225,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
4708
5225
  function probeLocal(vaultPath, deps) {
4709
5226
  const latestPath = satelliteLatestRunPath(vaultPath);
4710
5227
  let parsed = null;
4711
- if (existsSync10(latestPath)) {
5228
+ if (existsSync8(latestPath)) {
4712
5229
  try {
4713
- const wire = readSatelliteLatestRunFromText(readFileSync6(latestPath, "utf8"));
5230
+ const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
4714
5231
  if (wire) {
4715
5232
  parsed = {
4716
5233
  status: wire.status,
@@ -4841,7 +5358,7 @@ async function runFleetHealth(input) {
4841
5358
  const home = input.home ?? env.HOME ?? "";
4842
5359
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
4843
5360
  const vault = input.vault ?? env.WIKI_PATH;
4844
- const file = input.file ?? (vault ? join23(vault, FLEET_REL_PATH) : void 0);
5361
+ const file = input.file ?? (vault ? join22(vault, FLEET_REL_PATH) : void 0);
4845
5362
  if (!file) {
4846
5363
  return {
4847
5364
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -4925,15 +5442,15 @@ async function runFleetHealth(input) {
4925
5442
  }
4926
5443
 
4927
5444
  // src/utils/auto-commit.ts
4928
- import { existsSync as existsSync11 } from "fs";
4929
- import { join as join24 } from "path";
5445
+ import { existsSync as existsSync9 } from "fs";
5446
+ import { join as join23 } from "path";
4930
5447
  async function postCommit(vault, exitCode) {
4931
5448
  if (exitCode !== 0) return;
4932
5449
  const home = process.env.HOME ?? "";
4933
5450
  const dotenv = await parseDotenvFile(configPath(home));
4934
5451
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
4935
5452
  if (autoCommit === "false") return;
4936
- if (!existsSync11(join24(vault, ".git"))) return;
5453
+ if (!existsSync9(join23(vault, ".git"))) return;
4937
5454
  const lastOps = readLastOp(vault);
4938
5455
  if (lastOps.length === 0) return;
4939
5456
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -4957,8 +5474,8 @@ async function postCommit(vault, exitCode) {
4957
5474
  }
4958
5475
 
4959
5476
  // src/utils/protected-vault-write-guard.ts
4960
- import { readFileSync as readFileSync7 } from "fs";
4961
- import { join as join25, resolve as resolvePath } from "path";
5477
+ import { readFileSync as readFileSync6 } from "fs";
5478
+ import { join as join24, resolve as resolvePath } from "path";
4962
5479
  async function guardProtectedVaultWrite(input) {
4963
5480
  const env = input.env ?? process.env;
4964
5481
  const home = input.home ?? process.env.HOME ?? "";
@@ -5024,7 +5541,7 @@ async function resolveLiveVaultPath(input) {
5024
5541
  return resolved.ok ? resolved.data.path : void 0;
5025
5542
  }
5026
5543
  function resolveSnapshotWorktree(home) {
5027
- const skillwikiEnv = join25(home, ".skillwiki", ".env");
5544
+ const skillwikiEnv = join24(home, ".skillwiki", ".env");
5028
5545
  const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
5029
5546
  if (explicitWorktree) return explicitWorktree;
5030
5547
  const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
@@ -5036,7 +5553,7 @@ function resolveSnapshotWorktree(home) {
5036
5553
  }
5037
5554
  function readEnvKey(path, keys) {
5038
5555
  try {
5039
- const content = readFileSync7(path, "utf8");
5556
+ const content = readFileSync6(path, "utf8");
5040
5557
  for (const line of content.split(/\r?\n/)) {
5041
5558
  const trimmed = line.trim();
5042
5559
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
@@ -5094,7 +5611,7 @@ program.command("validate <file>").description("validate vault page frontmatter
5094
5611
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
5095
5612
  });
5096
5613
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5097
- const out = opts.out ?? join26(vault, ".skillwiki", "graph.json");
5614
+ const out = opts.out ?? join25(vault, ".skillwiki", "graph.json");
5098
5615
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
5099
5616
  });
5100
5617
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -5188,6 +5705,45 @@ program.command("tag-audit [vault]").description("audit tag taxonomy consistency
5188
5705
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5189
5706
  else emit(await runTagAudit({ vault: v.vault }), v.vault);
5190
5707
  });
5708
+ var tagCmd = program.command("tag").description("manage the vault tag taxonomy");
5709
+ tagCmd.command("reconcile [vault]").description("preview or add prospective page tags to SCHEMA taxonomy").requiredOption("--page <path>", "vault-relative typed page target").option("--from <path>", "unpublished draft or existing page to read tags from").option("--tags <csv>", "comma-separated prospective tags").option("--reason <text>", "reconciliation comment reason").option("--write", "write SCHEMA.md after successful preview", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5710
+ const resolved = await resolveVaultArg(vault, opts.wiki);
5711
+ if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
5712
+ const input = {
5713
+ vault: resolved.vault,
5714
+ page: opts.page,
5715
+ from: opts.from,
5716
+ tags: opts.tags?.split(",").map((tag) => tag.trim()).filter(Boolean),
5717
+ reason: opts.reason,
5718
+ write: !!opts.write
5719
+ };
5720
+ if (!opts.write) return emit(await runTagReconcile(input), resolved.vault, { postCommit: false });
5721
+ return emitGuardedVaultWrite(
5722
+ resolved.vault,
5723
+ "tag reconcile",
5724
+ () => runTagReconcile(input),
5725
+ { postCommit: false }
5726
+ );
5727
+ });
5728
+ var pageCmd = program.command("page").description("validate and publish typed vault pages");
5729
+ pageCmd.command("publish <draft> [vault]").description("preview or publish an unpublished typed-page draft").requiredOption("--target <path>", "vault-relative typed page target").option("--log-note <text>", "single-line publication log note").option("--write", "publish SCHEMA, page, index, and log", false).option("--wiki <name>", "wiki profile name").action(async (draft, vault, opts) => {
5730
+ const resolved = await resolveVaultArg(vault, opts.wiki);
5731
+ if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
5732
+ const input = {
5733
+ vault: resolved.vault,
5734
+ draftPath: draft,
5735
+ target: opts.target,
5736
+ logNote: opts.logNote,
5737
+ write: !!opts.write
5738
+ };
5739
+ if (!opts.write) return emit(await runPagePublish(input), resolved.vault, { postCommit: false });
5740
+ return emitGuardedVaultWrite(
5741
+ resolved.vault,
5742
+ "page publish",
5743
+ () => runPagePublish(input),
5744
+ { postCommit: false }
5745
+ );
5746
+ });
5191
5747
  program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5192
5748
  const v = await resolveVaultArg(vault, opts.wiki);
5193
5749
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });