skillwiki 0.9.63 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
package/dist/cli.js CHANGED
@@ -1,53 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ExitCode,
4
- FLEET_REL_PATH,
5
- MetaSchema,
6
- RawSourceSchema,
7
3
  SATELLITE_STALE_MS,
8
4
  acquireLock,
9
5
  acquireOwnedSyncLock,
10
6
  appendLastOp,
11
- assertTargetInsideVault,
12
7
  assessSourceIdentity,
13
- atomicWriteText,
14
8
  buildDegradedReasons,
15
9
  buildRemoteObjectPath,
16
10
  clearLastOp,
17
11
  configPath,
18
- err,
19
12
  evaluateSatelliteRunHealth,
20
13
  extractCitationMarkers,
21
- extractFrontmatter,
22
14
  extractTaxonomy,
23
15
  findPlugin,
24
16
  fixPathTooLong,
25
17
  getCliSessionId,
26
18
  getSessionId,
27
- isBlockedHost,
28
19
  isFailedRunStatus,
29
20
  isValidRemoteDeleteCap,
30
- loadFleetManifest,
31
- loadFleetManifestAndHost,
21
+ mergeTaxonomyConflict,
32
22
  normalizeRemoteRoot,
33
- ok,
34
- parseDotenvFile,
35
- parseDotenvText,
36
23
  planAndMaybePruneRemoteObjects,
37
- prepareTypedPage,
38
24
  probeRemoteHealth,
39
- profileKey,
40
25
  readCliPackageJson,
41
26
  readLastOp,
42
27
  readLock,
43
- readPage,
44
28
  readSatelliteLatestRunFromText,
45
29
  reconcileTaxonomyDocument,
46
- redactSensitiveContent,
47
30
  releaseLock,
48
31
  releaseOwnedSyncLock,
49
32
  renderIndexUpsert,
50
- resolveFleetHostId,
33
+ renderProjectIndex,
51
34
  resolveInitTimePath,
52
35
  resolveRuntimePath,
53
36
  runAudit,
@@ -57,8 +40,6 @@ import {
57
40
  runConfigSet,
58
41
  runDedup,
59
42
  runDoctor,
60
- runFleetContext,
61
- runFleetValidate,
62
43
  runFrontmatterFix,
63
44
  runGraphBuild,
64
45
  runIndexCheck,
@@ -85,14 +66,9 @@ import {
85
66
  runValidate,
86
67
  safeWritePage,
87
68
  satelliteLatestRunPath,
88
- scanSensitiveContent,
89
- scanVault,
90
- snapshotterAliasForLocalHost,
91
- splitFrontmatter,
92
69
  taxonomyCommentForPage,
93
- upsertIndexEntry,
94
- writeDotenv
95
- } from "./chunk-TUFQZ5K4.js";
70
+ upsertIndexEntry
71
+ } from "./chunk-DR7KFHNH.js";
96
72
  import {
97
73
  normalizeDistTag,
98
74
  readCache,
@@ -100,9 +76,52 @@ import {
100
76
  triggerAutoUpdate,
101
77
  writeCache
102
78
  } from "./chunk-7I2TPIV5.js";
79
+ import {
80
+ assertTargetInsideVault,
81
+ atomicWriteText,
82
+ extractFrontmatter,
83
+ prepareTypedPage,
84
+ readPage,
85
+ redactSensitiveContent,
86
+ renderRootIndex,
87
+ scanSensitiveContent,
88
+ scanVault,
89
+ splitFrontmatter,
90
+ writeRootIndexProjection
91
+ } from "./chunk-IZABIE44.js";
92
+ import {
93
+ acquireManagedWriteLock,
94
+ git,
95
+ gitStrict,
96
+ releaseManagedWriteLock,
97
+ runManagedWritePreflight,
98
+ runManagedWriteTransaction,
99
+ runVaultSyncPullHelper
100
+ } from "./chunk-5TBIMLTZ.js";
101
+ import {
102
+ FLEET_REL_PATH,
103
+ loadFleetManifest,
104
+ loadFleetManifestAndHost,
105
+ parseDotenvFile,
106
+ parseDotenvText,
107
+ profileKey,
108
+ resolveFleetHostId,
109
+ runFleetContext,
110
+ runFleetValidate,
111
+ snapshotterAliasForLocalHost,
112
+ writeDotenv
113
+ } from "./chunk-S5ABQCXQ.js";
114
+ import {
115
+ ExitCode,
116
+ MetaSchema,
117
+ RawSourceSchema,
118
+ err,
119
+ isBlockedHost,
120
+ ok
121
+ } from "./chunk-C5OLZRRM.js";
103
122
 
104
123
  // src/cli.ts
105
- import { join as join27 } from "path";
124
+ import { join as join34 } from "path";
106
125
  import { Command } from "commander";
107
126
 
108
127
  // src/utils/output.ts
@@ -710,11 +729,11 @@ async function runInit(input) {
710
729
  ].join("\n");
711
730
  });
712
731
  if (errTemplate) return errTemplate;
713
- const err2 = await writeOrPreserve("log.md", async () => {
732
+ const err22 = await writeOrPreserve("log.md", async () => {
714
733
  const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
715
734
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
716
735
  });
717
- if (err2) return err2;
736
+ if (err22) return err22;
718
737
  const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
719
738
  let envWritten = "";
720
739
  if (!skipEnv) {
@@ -770,10 +789,1009 @@ async function runInit(input) {
770
789
  };
771
790
  }
772
791
 
792
+ // src/commands/index-rebuild.ts
793
+ import { readFileSync as readFileSync3 } from "fs";
794
+ import { join as join7 } from "path";
795
+
796
+ // src/utils/protected-vault-write-guard.ts
797
+ import { readFileSync as readFileSync2 } from "fs";
798
+ import { join as join6, resolve as resolvePath } from "path";
799
+ async function guardProtectedVaultWrite(input) {
800
+ const env = input.env ?? process.env;
801
+ const home = input.home ?? process.env.HOME ?? "";
802
+ const cwd = input.cwd ?? process.cwd();
803
+ const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
804
+ const load = await loadFleetManifestAndHost({
805
+ vault: liveVaultPath ?? input.vault,
806
+ hostId: input.hostId,
807
+ env,
808
+ home,
809
+ cwd,
810
+ osHostname: input.osHostname ?? process.env.HOSTNAME,
811
+ user: input.user ?? process.env.USER
812
+ });
813
+ if (!load?.hostId || load.identityStatus !== "known") {
814
+ return { blocked: false };
815
+ }
816
+ const host = load.manifest.hosts[load.hostId];
817
+ if (!host || host.role !== "snapshotter" || host.protected !== true) {
818
+ return { blocked: false };
819
+ }
820
+ const snapshotWorktree = resolveSnapshotWorktree(home);
821
+ const targetVault = resolvePath(input.vault);
822
+ const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
823
+ const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
824
+ if (canonicalLiveVault && targetVault === canonicalLiveVault) {
825
+ return { blocked: false };
826
+ }
827
+ if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
828
+ return {
829
+ blocked: true,
830
+ exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
831
+ result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
832
+ host_id: load.hostId,
833
+ command: input.command,
834
+ reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
835
+ guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
836
+ })
837
+ };
838
+ }
839
+ if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
840
+ return {
841
+ blocked: true,
842
+ exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
843
+ result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
844
+ host_id: load.hostId,
845
+ command: input.command,
846
+ reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
847
+ guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
848
+ })
849
+ };
850
+ }
851
+ return { blocked: false };
852
+ }
853
+ async function resolveLiveVaultPath(input) {
854
+ const resolved = await resolveRuntimePath({
855
+ flag: void 0,
856
+ envValue: input.env.WIKI_PATH,
857
+ wikiEnv: input.env.WIKI,
858
+ home: input.home,
859
+ cwd: input.cwd
860
+ });
861
+ return resolved.ok ? resolved.data.path : void 0;
862
+ }
863
+ function resolveSnapshotWorktree(home) {
864
+ const skillwikiEnv = join6(home, ".skillwiki", ".env");
865
+ const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
866
+ if (explicitWorktree) return explicitWorktree;
867
+ const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
868
+ if (snapshotProfile) {
869
+ const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
870
+ if (fromProfile) return fromProfile;
871
+ }
872
+ return "/root/wiki-git";
873
+ }
874
+ function readEnvKey(path, keys) {
875
+ try {
876
+ const content = readFileSync2(path, "utf8");
877
+ for (const line of content.split(/\r?\n/)) {
878
+ const trimmed = line.trim();
879
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
880
+ const eq = trimmed.indexOf("=");
881
+ if (eq <= 0) continue;
882
+ const key = trimmed.slice(0, eq).trim();
883
+ if (!keys.includes(key)) continue;
884
+ const value = trimmed.slice(eq + 1).trim();
885
+ if (value.length > 0) return value;
886
+ }
887
+ } catch {
888
+ return void 0;
889
+ }
890
+ return void 0;
891
+ }
892
+
893
+ // src/commands/index-rebuild.ts
894
+ async function runIndexRebuild(input) {
895
+ const projection = await renderRootIndex({ vault: input.vault });
896
+ if (!projection.ok) {
897
+ const code = projection.error === "SCHEME_REJECTED" ? ExitCode.SCHEME_REJECTED : ExitCode.VAULT_PATH_INVALID;
898
+ return { exitCode: code, result: projection };
899
+ }
900
+ let current = "";
901
+ try {
902
+ current = readFileSync3(join7(input.vault, "index.md"), "utf8");
903
+ } catch {
904
+ current = "";
905
+ }
906
+ const changed = current !== projection.data.text;
907
+ if (!input.write) {
908
+ return {
909
+ exitCode: ExitCode.OK,
910
+ result: ok({
911
+ changed,
912
+ dry_run: true,
913
+ entry_count: projection.data.entries.length,
914
+ duplicates_removed: projection.data.duplicates_removed,
915
+ ghosts_removed: projection.data.ghosts_removed,
916
+ humanHint: changed ? `dry run: would rewrite index.md (${projection.data.entries.length} entries)` : `dry run: index.md already canonical (${projection.data.entries.length} entries)`
917
+ })
918
+ };
919
+ }
920
+ const guard = await guardProtectedVaultWrite({
921
+ vault: input.vault,
922
+ command: "index rebuild"
923
+ });
924
+ if (guard.blocked) {
925
+ return { exitCode: guard.exitCode, result: guard.result };
926
+ }
927
+ return runManagedWriteTransaction({
928
+ vault: input.vault,
929
+ command: "index rebuild",
930
+ allowImmutableRecord: false,
931
+ mutate: async () => {
932
+ if (!changed) {
933
+ return {
934
+ exitCode: ExitCode.OK,
935
+ result: ok({
936
+ changed: false,
937
+ dry_run: false,
938
+ entry_count: projection.data.entries.length,
939
+ duplicates_removed: projection.data.duplicates_removed,
940
+ ghosts_removed: projection.data.ghosts_removed,
941
+ humanHint: `index.md already canonical (${projection.data.entries.length} entries)`
942
+ })
943
+ };
944
+ }
945
+ const written = await writeRootIndexProjection(input.vault, projection.data);
946
+ if (!written.ok) {
947
+ return { exitCode: ExitCode.WRITE_FAILED, result: written };
948
+ }
949
+ let installed = "";
950
+ try {
951
+ installed = readFileSync3(join7(input.vault, "index.md"), "utf8");
952
+ } catch (error) {
953
+ return {
954
+ exitCode: ExitCode.WRITE_FAILED,
955
+ result: err("WRITE_FAILED", { message: String(error) })
956
+ };
957
+ }
958
+ if (installed !== projection.data.text) {
959
+ return {
960
+ exitCode: ExitCode.WRITE_FAILED,
961
+ result: err("WRITE_FAILED", { message: "installed index.md differs from projection" })
962
+ };
963
+ }
964
+ return {
965
+ exitCode: ExitCode.OK,
966
+ result: ok({
967
+ changed: true,
968
+ dry_run: false,
969
+ entry_count: projection.data.entries.length,
970
+ duplicates_removed: projection.data.duplicates_removed,
971
+ ghosts_removed: projection.data.ghosts_removed,
972
+ humanHint: `rewrote index.md (${projection.data.entries.length} entries)`
973
+ })
974
+ };
975
+ }
976
+ });
977
+ }
978
+
979
+ // src/commands/derived-conflicts.ts
980
+ import { execFileSync as execFileSync2 } from "child_process";
981
+ import { existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
982
+ import { join as join9 } from "path";
983
+
984
+ // src/utils/log-merge.ts
985
+ import { execFileSync } from "child_process";
986
+ import { mkdtempSync, writeFileSync, rmSync } from "fs";
987
+ import { tmpdir } from "os";
988
+ import { join as join8 } from "path";
989
+ var ENTRY_RE = /^## \[/m;
990
+ var PUBLISH_MARKER_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
991
+ function splitLog(text) {
992
+ const normalized = text.replace(/\r\n/g, "\n");
993
+ const match = ENTRY_RE.exec(normalized);
994
+ if (!match || match.index === void 0) {
995
+ return { preamble: normalized.replace(/\n+$/, ""), entries: [] };
996
+ }
997
+ const preamble = normalized.slice(0, match.index).replace(/\n+$/, "");
998
+ const body = normalized.slice(match.index);
999
+ const parts = body.split(/(?=^## \[)/m).filter((p) => p.length > 0);
1000
+ const entries = parts.map((p) => p.replace(/\n+$/, ""));
1001
+ return { preamble, entries };
1002
+ }
1003
+ function publishId(entry) {
1004
+ return PUBLISH_MARKER_RE.exec(entry)?.[1]?.toLowerCase();
1005
+ }
1006
+ function unionUnstructured(base, ours, theirs) {
1007
+ const dir = mkdtempSync(join8(tmpdir(), "log-union-"));
1008
+ try {
1009
+ const basePath = join8(dir, "base");
1010
+ const oursPath = join8(dir, "ours");
1011
+ const theirsPath = join8(dir, "theirs");
1012
+ writeFileSync(basePath, base.endsWith("\n") || base.length === 0 ? base : `${base}
1013
+ `);
1014
+ writeFileSync(oursPath, ours.endsWith("\n") || ours.length === 0 ? ours : `${ours}
1015
+ `);
1016
+ writeFileSync(theirsPath, theirs.endsWith("\n") || theirs.length === 0 ? theirs : `${theirs}
1017
+ `);
1018
+ const text = execFileSync("git", ["merge-file", "--union", "-p", oursPath, basePath, theirsPath], {
1019
+ encoding: "utf8"
1020
+ });
1021
+ return ok(text.endsWith("\n") ? text : `${text}
1022
+ `);
1023
+ } catch (error) {
1024
+ return err("WRITE_FAILED", { message: `unstructured log union failed: ${String(error)}` });
1025
+ } finally {
1026
+ rmSync(dir, { recursive: true, force: true });
1027
+ }
1028
+ }
1029
+ function mergeLogConflictStages(input) {
1030
+ const ours = splitLog(input.ours);
1031
+ const theirs = splitLog(input.theirs);
1032
+ const baseSplit = splitLog(input.base);
1033
+ if (ours.entries.length === 0 && theirs.entries.length === 0) {
1034
+ const unioned = unionUnstructured(input.base, input.ours, input.theirs);
1035
+ if (!unioned.ok) return unioned;
1036
+ return ok({ text: unioned.data, deduplicated_operation_ids: [] });
1037
+ }
1038
+ const preamble = ours.preamble || theirs.preamble || baseSplit.preamble || "# Log";
1039
+ const seenMarkers = /* @__PURE__ */ new Set();
1040
+ const seenExact = /* @__PURE__ */ new Set();
1041
+ const out = [];
1042
+ const deduped = [];
1043
+ const push = (entry) => {
1044
+ const marker2 = publishId(entry);
1045
+ if (marker2) {
1046
+ if (seenMarkers.has(marker2)) {
1047
+ deduped.push(marker2);
1048
+ return;
1049
+ }
1050
+ seenMarkers.add(marker2);
1051
+ } else if (seenExact.has(entry)) {
1052
+ return;
1053
+ } else {
1054
+ seenExact.add(entry);
1055
+ }
1056
+ out.push(entry);
1057
+ };
1058
+ for (const e of ours.entries) push(e);
1059
+ for (const e of theirs.entries) push(e);
1060
+ let text = preamble;
1061
+ if (out.length > 0) {
1062
+ text = `${preamble.replace(/\n+$/, "")}
1063
+
1064
+ ${out.join("\n\n")}`;
1065
+ }
1066
+ if (!text.endsWith("\n")) text += "\n";
1067
+ return ok({ text, deduplicated_operation_ids: deduped });
1068
+ }
1069
+
1070
+ // src/commands/derived-conflicts.ts
1071
+ function classifyDerivedPath(path) {
1072
+ if (path === "index.md") return "root-index";
1073
+ if (path === "SCHEMA.md") return "taxonomy";
1074
+ if (path === "log.md" || path.endsWith("/log.md")) return "log";
1075
+ if (/^projects\/[^/]+\/knowledge\.md$/.test(path)) return "project-index";
1076
+ return "unknown";
1077
+ }
1078
+ function journalPath(vault, operationId2) {
1079
+ const gitPath = git(vault, ["rev-parse", "--git-path", `vault-sync/operations/${operationId2}.env`]);
1080
+ if (!gitPath) return null;
1081
+ return gitPath.startsWith("/") ? gitPath : join9(vault, gitPath);
1082
+ }
1083
+ function readJournalFields(path) {
1084
+ const text = readFileSync5(path, "utf8");
1085
+ const out = {};
1086
+ for (const line of text.split("\n")) {
1087
+ const i = line.indexOf("=");
1088
+ if (i <= 0) continue;
1089
+ out[line.slice(0, i)] = line.slice(i + 1);
1090
+ }
1091
+ return out;
1092
+ }
1093
+ function showStage(vault, stage, path) {
1094
+ try {
1095
+ return execFileSync2("git", ["show", `:${stage}:${path}`], {
1096
+ cwd: vault,
1097
+ encoding: "utf8",
1098
+ stdio: ["pipe", "pipe", "pipe"]
1099
+ });
1100
+ } catch {
1101
+ return null;
1102
+ }
1103
+ }
1104
+ function stageOid(vault, stage, path) {
1105
+ return git(vault, ["rev-parse", `:${stage}:${path}`]);
1106
+ }
1107
+ function unmergedPaths(vault) {
1108
+ const raw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
1109
+ return raw ? raw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
1110
+ }
1111
+ function verifyOperation(vault, operationId2) {
1112
+ const path = journalPath(vault, operationId2);
1113
+ if (!path || !existsSync(path)) {
1114
+ return err("PREFLIGHT_FAILED", { reason: "missing-operation-journal", operation_id: operationId2 });
1115
+ }
1116
+ const fields = readJournalFields(path);
1117
+ if (fields.operation_id !== operationId2) {
1118
+ return err("PREFLIGHT_FAILED", { reason: "operation-id-mismatch", operation_id: operationId2 });
1119
+ }
1120
+ if (fields.handoff === "1") {
1121
+ return err("PREFLIGHT_FAILED", { reason: "operation-handoff", operation_id: operationId2 });
1122
+ }
1123
+ const phase = fields.phase ?? "";
1124
+ if (phase !== "rebasing" && phase !== "restoring") {
1125
+ return err("PREFLIGHT_FAILED", { reason: "operation-phase", phase, operation_id: operationId2 });
1126
+ }
1127
+ const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
1128
+ const journalGitDir = fields.worktree_git_dir ?? "";
1129
+ if (journalGitDir && currentGitDir && journalGitDir !== currentGitDir) {
1130
+ return err("PREFLIGHT_FAILED", { reason: "worktree-mismatch", operation_id: operationId2 });
1131
+ }
1132
+ if (!journalGitDir && unmergedPaths(vault).length === 0) {
1133
+ return err("PREFLIGHT_FAILED", { reason: "legacy-journal-without-unmerged", operation_id: operationId2 });
1134
+ }
1135
+ return ok({ phase });
1136
+ }
1137
+ async function resolveOne(vault, path, klass) {
1138
+ if (klass === "root-index") {
1139
+ const rendered = await renderRootIndex({ vault });
1140
+ if (!rendered.ok) return rendered;
1141
+ return ok(rendered.data.text);
1142
+ }
1143
+ if (klass === "taxonomy") {
1144
+ const base = showStage(vault, 1, path) ?? "";
1145
+ const ours = showStage(vault, 2, path);
1146
+ const theirs = showStage(vault, 3, path);
1147
+ if (ours === null || theirs === null) {
1148
+ return err("WRITE_FAILED", { path, message: "missing taxonomy stages" });
1149
+ }
1150
+ const merged = mergeTaxonomyConflict(base, ours, theirs);
1151
+ if (!merged.ok) return merged;
1152
+ return ok(merged.data.text);
1153
+ }
1154
+ if (klass === "log") {
1155
+ const base = showStage(vault, 1, path) ?? "";
1156
+ const ours = showStage(vault, 2, path) ?? "";
1157
+ const theirs = showStage(vault, 3, path) ?? "";
1158
+ const merged = mergeLogConflictStages({ base, ours, theirs });
1159
+ if (!merged.ok) return merged;
1160
+ return ok(merged.data.text);
1161
+ }
1162
+ if (klass === "project-index") {
1163
+ const slug = path.replace(/^projects\//, "").replace(/\/knowledge\.md$/, "");
1164
+ const rendered = await renderProjectIndex(vault, slug);
1165
+ if (!rendered.ok) return rendered;
1166
+ return ok(rendered.data.text);
1167
+ }
1168
+ return err("SCHEME_REJECTED", { path, reason: "unknown-derived-class" });
1169
+ }
1170
+ function rollbackPath(vault, path, stage2, stage3) {
1171
+ try {
1172
+ execFileSync2("git", ["checkout", "--conflict=merge", "--", path], {
1173
+ cwd: vault,
1174
+ stdio: ["pipe", "pipe", "pipe"]
1175
+ });
1176
+ } catch {
1177
+ return false;
1178
+ }
1179
+ const s2 = stageOid(vault, 2, path);
1180
+ const s3 = stageOid(vault, 3, path);
1181
+ return s2 === stage2 && s3 === stage3;
1182
+ }
1183
+ function hasMarkers(path, text) {
1184
+ const lines = text.split(/\r?\n/);
1185
+ let open3 = false;
1186
+ let sep2 = false;
1187
+ for (const line of lines) {
1188
+ if (line.startsWith("<<<<<<< ")) {
1189
+ open3 = true;
1190
+ sep2 = false;
1191
+ continue;
1192
+ }
1193
+ if (line === "=======" && open3) {
1194
+ sep2 = true;
1195
+ continue;
1196
+ }
1197
+ if (line.startsWith(">>>>>>> ") && open3 && sep2) return true;
1198
+ if (line.startsWith(">>>>>>> ")) {
1199
+ open3 = false;
1200
+ sep2 = false;
1201
+ }
1202
+ }
1203
+ return false;
1204
+ }
1205
+ async function runDerivedConflictResolution(input) {
1206
+ const vault = input.vault;
1207
+ const verified = verifyOperation(vault, input.operationId);
1208
+ if (!verified.ok) {
1209
+ return {
1210
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1211
+ result: err("PREFLIGHT_FAILED", verified.detail)
1212
+ };
1213
+ }
1214
+ const paths = unmergedPaths(vault);
1215
+ if (paths.length === 0) {
1216
+ return {
1217
+ exitCode: ExitCode.OK,
1218
+ result: ok({
1219
+ resolved: true,
1220
+ resolved_paths: [],
1221
+ unknown_paths: [],
1222
+ rolled_back: false,
1223
+ humanHint: "no unmerged paths"
1224
+ })
1225
+ };
1226
+ }
1227
+ const classes = paths.map((p) => ({ path: p, klass: classifyDerivedPath(p) }));
1228
+ const unknown = classes.filter((c) => c.klass === "unknown").map((c) => c.path);
1229
+ if (unknown.length > 0) {
1230
+ return {
1231
+ exitCode: ExitCode.SYNC_PULL_FAILED,
1232
+ result: ok({
1233
+ resolved: false,
1234
+ resolved_paths: [],
1235
+ unknown_paths: unknown,
1236
+ rolled_back: false,
1237
+ humanHint: `review-required: unknown conflict paths ${unknown.join(", ")}`
1238
+ })
1239
+ };
1240
+ }
1241
+ const snapshots = /* @__PURE__ */ new Map();
1242
+ for (const { path } of classes) {
1243
+ snapshots.set(path, {
1244
+ stage2: stageOid(vault, 2, path),
1245
+ stage3: stageOid(vault, 3, path)
1246
+ });
1247
+ }
1248
+ const staged = [];
1249
+ try {
1250
+ for (const { path, klass } of classes) {
1251
+ const resolved = await resolveOne(vault, path, klass);
1252
+ if (!resolved.ok) throw new Error(`${path}: ${resolved.error}`);
1253
+ if (hasMarkers(path, resolved.data)) {
1254
+ throw new Error(`${path}: conflict markers remain after resolve`);
1255
+ }
1256
+ writeFileSync2(join9(vault, path), resolved.data, "utf8");
1257
+ execFileSync2("git", ["add", "--", path], { cwd: vault, stdio: ["pipe", "pipe", "pipe"] });
1258
+ staged.push(path);
1259
+ }
1260
+ const remaining = unmergedPaths(vault);
1261
+ if (remaining.length > 0) {
1262
+ throw new Error(`still unmerged: ${remaining.join(",")}`);
1263
+ }
1264
+ return {
1265
+ exitCode: ExitCode.OK,
1266
+ result: ok({
1267
+ resolved: true,
1268
+ resolved_paths: staged,
1269
+ unknown_paths: [],
1270
+ rolled_back: false,
1271
+ humanHint: `resolved ${staged.length} derived path(s)`
1272
+ })
1273
+ };
1274
+ } catch (error) {
1275
+ let rolled = true;
1276
+ for (const path of staged) {
1277
+ const snap = snapshots.get(path);
1278
+ if (!snap || !rollbackPath(vault, path, snap.stage2, snap.stage3)) {
1279
+ rolled = false;
1280
+ }
1281
+ }
1282
+ for (const { path } of classes) {
1283
+ if (staged.includes(path)) continue;
1284
+ const snap = snapshots.get(path);
1285
+ if (snap) rollbackPath(vault, path, snap.stage2, snap.stage3);
1286
+ }
1287
+ return {
1288
+ exitCode: ExitCode.SYNC_PULL_FAILED,
1289
+ result: ok({
1290
+ resolved: false,
1291
+ resolved_paths: [],
1292
+ unknown_paths: [],
1293
+ rolled_back: rolled,
1294
+ humanHint: `derived resolve failed: ${String(error)}`
1295
+ })
1296
+ };
1297
+ }
1298
+ }
1299
+
1300
+ // src/commands/log-materialize.ts
1301
+ import { readFileSync as readFileSync6 } from "fs";
1302
+ import { join as join11 } from "path";
1303
+
1304
+ // src/utils/log-events.ts
1305
+ import { mkdir as mkdir4, open, readFile as readFile4, readdir as readdir3 } from "fs/promises";
1306
+ import { join as join10 } from "path";
1307
+ function isPlainObject(value) {
1308
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1309
+ }
1310
+ function canonicalize(value) {
1311
+ if (Array.isArray(value)) return value.map(canonicalize);
1312
+ if (!isPlainObject(value)) return value;
1313
+ const out = {};
1314
+ for (const key of Object.keys(value).sort()) {
1315
+ out[key] = canonicalize(value[key]);
1316
+ }
1317
+ return out;
1318
+ }
1319
+ function eventPathFor(event) {
1320
+ const day2 = event.occurred_at.slice(0, 10);
1321
+ return `meta/log-events/${day2}/${event.operation_id}.json`;
1322
+ }
1323
+ function validateLogEvent(event) {
1324
+ if (event.schema !== "skillwiki-log-event/v1") {
1325
+ return err("SCHEME_REJECTED", { message: "invalid event schema" });
1326
+ }
1327
+ if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
1328
+ return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
1329
+ }
1330
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
1331
+ return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
1332
+ }
1333
+ for (const field of ["host_id", "actor", "kind", "target", "note"]) {
1334
+ const v = event[field];
1335
+ if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
1336
+ return err("SCHEME_REJECTED", { message: `invalid ${field}` });
1337
+ }
1338
+ }
1339
+ if (!isPlainObject(event.metadata)) {
1340
+ return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
1341
+ }
1342
+ const sensitive = scanSensitiveContent(JSON.stringify(event));
1343
+ if (sensitive.length > 0) {
1344
+ return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
1345
+ }
1346
+ return ok({
1347
+ schema: "skillwiki-log-event/v1",
1348
+ operation_id: event.operation_id,
1349
+ occurred_at: event.occurred_at,
1350
+ host_id: event.host_id,
1351
+ actor: event.actor,
1352
+ kind: event.kind,
1353
+ target: event.target,
1354
+ note: event.note,
1355
+ metadata: canonicalize(event.metadata)
1356
+ });
1357
+ }
1358
+ function canonicalEventJson(event) {
1359
+ const ordered = {
1360
+ schema: event.schema,
1361
+ operation_id: event.operation_id,
1362
+ occurred_at: event.occurred_at,
1363
+ host_id: event.host_id,
1364
+ actor: event.actor,
1365
+ kind: event.kind,
1366
+ target: event.target,
1367
+ note: event.note,
1368
+ metadata: event.metadata
1369
+ };
1370
+ return `${JSON.stringify(ordered, null, 2)}
1371
+ `;
1372
+ }
1373
+ async function writeLogEvent(vault, event) {
1374
+ const validated = validateLogEvent(event);
1375
+ if (!validated.ok) return validated;
1376
+ const rel = eventPathFor(validated.data);
1377
+ const abs = join10(vault, rel);
1378
+ await mkdir4(join10(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
1379
+ recursive: true
1380
+ });
1381
+ const body = canonicalEventJson(validated.data);
1382
+ try {
1383
+ const handle = await open(abs, "wx");
1384
+ try {
1385
+ await handle.writeFile(body, "utf8");
1386
+ } finally {
1387
+ await handle.close();
1388
+ }
1389
+ return ok({ path: rel, created: true });
1390
+ } catch (error) {
1391
+ if (error.code === "EEXIST") {
1392
+ let existing;
1393
+ try {
1394
+ existing = await readFile4(abs, "utf8");
1395
+ } catch (readErr) {
1396
+ return err("WRITE_FAILED", { path: rel, message: String(readErr) });
1397
+ }
1398
+ if (existing === body) return ok({ path: rel, created: false });
1399
+ return err("EVENT_IDENTITY_COLLISION", { path: rel });
1400
+ }
1401
+ return err("WRITE_FAILED", { path: rel, message: String(error) });
1402
+ }
1403
+ }
1404
+ async function readLogEvents(vault) {
1405
+ const root = join10(vault, "meta", "log-events");
1406
+ let days;
1407
+ try {
1408
+ days = (await readdir3(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1409
+ } catch {
1410
+ return ok([]);
1411
+ }
1412
+ const events = [];
1413
+ for (const day2 of days) {
1414
+ const files = (await readdir3(join10(root, day2))).filter((f) => f.endsWith(".json")).sort();
1415
+ for (const file of files) {
1416
+ const text = await readFile4(join10(root, day2, file), "utf8");
1417
+ let parsed;
1418
+ try {
1419
+ parsed = JSON.parse(text);
1420
+ } catch {
1421
+ return err("SCHEME_REJECTED", { path: `meta/log-events/${day2}/${file}`, message: "invalid JSON" });
1422
+ }
1423
+ const validated = validateLogEvent(parsed);
1424
+ if (!validated.ok) return validated;
1425
+ if (eventPathFor(validated.data) !== `meta/log-events/${day2}/${file}`) {
1426
+ return err("SCHEME_REJECTED", {
1427
+ path: `meta/log-events/${day2}/${file}`,
1428
+ message: "path/identity mismatch"
1429
+ });
1430
+ }
1431
+ events.push(validated.data);
1432
+ }
1433
+ }
1434
+ events.sort((a, b) => {
1435
+ if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
1436
+ return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
1437
+ });
1438
+ return ok(events);
1439
+ }
1440
+
1441
+ // src/utils/log-projection.ts
1442
+ function day(event) {
1443
+ return event.occurred_at.slice(0, 10);
1444
+ }
1445
+ function marker(event) {
1446
+ return `<!-- skillwiki-log-event:${event.operation_id} -->`;
1447
+ }
1448
+ function bodyFor(event) {
1449
+ if (event.kind === "page-publish") {
1450
+ const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
1451
+ return [
1452
+ `- Published: [[${event.target.replace(/\.md$/, "")}]]`,
1453
+ `- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
1454
+ event.note ? `- Note: ${event.note}` : ""
1455
+ ].filter(Boolean).join("\n");
1456
+ }
1457
+ if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
1458
+ const legacy = event.metadata.legacy_markdown.trimEnd();
1459
+ if (legacy.includes(marker(event))) return legacy;
1460
+ return `${legacy}
1461
+ ${marker(event)}`;
1462
+ }
1463
+ if (event.kind === "session-brief" || event.kind === "archive" || event.kind === "remove" || event.kind === "log-entry") {
1464
+ return [
1465
+ `- Target: ${event.target}`,
1466
+ `- Note: ${event.note}`,
1467
+ `- Actor: ${event.actor}`,
1468
+ `- Host: ${event.host_id}`
1469
+ ].join("\n");
1470
+ }
1471
+ return [
1472
+ `- Event kind: ${event.kind}`,
1473
+ `- Actor: ${event.actor}`,
1474
+ `- Host: ${event.host_id}`,
1475
+ `- Target: ${event.target}`,
1476
+ `- Note: ${event.note}`,
1477
+ `- Metadata: ${JSON.stringify(event.metadata)}`
1478
+ ].join("\n");
1479
+ }
1480
+ function renderLogProjection(events) {
1481
+ const ordered = [...events].sort((a, b) => {
1482
+ if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
1483
+ return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
1484
+ });
1485
+ const lines = [
1486
+ "# Vault Log",
1487
+ "",
1488
+ "Generated by `skillwiki log materialize` from immutable events.",
1489
+ ""
1490
+ ];
1491
+ for (const event of ordered) {
1492
+ if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
1493
+ const block = bodyFor(event);
1494
+ lines.push(block.endsWith("\n") ? block.trimEnd() : block, "");
1495
+ continue;
1496
+ }
1497
+ lines.push(`## [${day(event)}] ${event.kind} | ${event.target}`, "", bodyFor(event), marker(event), "");
1498
+ }
1499
+ return `${lines.join("\n").replace(/\n+$/, "")}
1500
+ `;
1501
+ }
1502
+
1503
+ // src/utils/projection-authority.ts
1504
+ function resolveProjectionAuthority(load) {
1505
+ if (!load) {
1506
+ return ok({ authority_host_id: "standalone", current_host_id: "standalone", can_write: true });
1507
+ }
1508
+ const snapshotters = Object.entries(load.manifest.hosts).filter(([, h]) => h.role === "snapshotter").map(([id]) => id);
1509
+ const configured = load.manifest.projection_authority;
1510
+ let authority = configured;
1511
+ if (!authority) {
1512
+ if (snapshotters.length === 1) authority = snapshotters[0];
1513
+ else {
1514
+ return err("PREFLIGHT_FAILED", {
1515
+ reason: "projection-authority-unresolved",
1516
+ snapshotters
1517
+ });
1518
+ }
1519
+ }
1520
+ if (!load.manifest.hosts[authority]) {
1521
+ return err("PREFLIGHT_FAILED", { reason: "projection-authority-unknown-host", authority });
1522
+ }
1523
+ if (load.identityStatus !== "known" || !load.hostId) {
1524
+ return err("PREFLIGHT_FAILED", {
1525
+ reason: "fleet-identity-unresolved",
1526
+ identity_status: load.identityStatus
1527
+ });
1528
+ }
1529
+ return ok({
1530
+ authority_host_id: authority,
1531
+ current_host_id: load.hostId,
1532
+ can_write: load.hostId === authority
1533
+ });
1534
+ }
1535
+
1536
+ // src/commands/log-materialize.ts
1537
+ async function runLogMaterialize(input) {
1538
+ const events = await readLogEvents(input.vault);
1539
+ if (!events.ok) {
1540
+ return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
1541
+ }
1542
+ const text = renderLogProjection(events.data);
1543
+ let current = "";
1544
+ try {
1545
+ current = readFileSync6(join11(input.vault, "log.md"), "utf8");
1546
+ } catch {
1547
+ current = "";
1548
+ }
1549
+ const changed = current !== text;
1550
+ if (!input.write) {
1551
+ return {
1552
+ exitCode: ExitCode.OK,
1553
+ result: ok({
1554
+ changed,
1555
+ dry_run: true,
1556
+ event_count: events.data.length,
1557
+ humanHint: changed ? `dry run: would rewrite log.md (${events.data.length} events)` : `dry run: log.md already canonical (${events.data.length} events)`
1558
+ })
1559
+ };
1560
+ }
1561
+ if (!input.skipAuthority) {
1562
+ const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
1563
+ const auth = resolveProjectionAuthority(fleet);
1564
+ if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
1565
+ if (!auth.data.can_write) {
1566
+ return {
1567
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1568
+ result: err("PREFLIGHT_FAILED", {
1569
+ reason: "projection-authority",
1570
+ authority_host_id: auth.data.authority_host_id,
1571
+ current_host_id: auth.data.current_host_id
1572
+ })
1573
+ };
1574
+ }
1575
+ const guard = await guardProtectedVaultWrite({ vault: input.vault, command: "log materialize" });
1576
+ if (guard.blocked) return { exitCode: guard.exitCode, result: guard.result };
1577
+ }
1578
+ if (!changed) {
1579
+ return {
1580
+ exitCode: ExitCode.OK,
1581
+ result: ok({
1582
+ changed: false,
1583
+ dry_run: false,
1584
+ event_count: events.data.length,
1585
+ humanHint: `log.md already canonical (${events.data.length} events)`
1586
+ })
1587
+ };
1588
+ }
1589
+ const written = await atomicWriteText(join11(input.vault, "log.md"), text);
1590
+ if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
1591
+ const installed = readFileSync6(join11(input.vault, "log.md"), "utf8");
1592
+ if (installed !== text) {
1593
+ return {
1594
+ exitCode: ExitCode.WRITE_FAILED,
1595
+ result: err("WRITE_FAILED", { message: "installed log.md differs from projection" })
1596
+ };
1597
+ }
1598
+ return {
1599
+ exitCode: ExitCode.OK,
1600
+ result: ok({
1601
+ changed: true,
1602
+ dry_run: false,
1603
+ event_count: events.data.length,
1604
+ humanHint: `rewrote log.md (${events.data.length} events)`
1605
+ })
1606
+ };
1607
+ }
1608
+
1609
+ // src/commands/log-migrate-legacy.ts
1610
+ import { readFileSync as readFileSync7 } from "fs";
1611
+ import { join as join12 } from "path";
1612
+
1613
+ // src/utils/operation-id.ts
1614
+ import { createHash as createHash2 } from "crypto";
1615
+ function operationId(namespace, parts) {
1616
+ const hash = createHash2("sha256").update(namespace).update("\0");
1617
+ for (const part of parts) hash.update(part).update("\0");
1618
+ return hash.digest("hex");
1619
+ }
1620
+
1621
+ // src/commands/log-migrate-legacy.ts
1622
+ var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
1623
+ var PUBLISH_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
1624
+ function splitLegacyBlocks(text) {
1625
+ const matches = [...text.matchAll(BLOCK_RE)];
1626
+ if (matches.length === 0) return [];
1627
+ const out = [];
1628
+ for (let i = 0; i < matches.length; i++) {
1629
+ const m = matches[i];
1630
+ const start = m.index ?? 0;
1631
+ const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
1632
+ const block = text.slice(start, end).replace(/\s+$/, "") + "\n";
1633
+ out.push({ date: m[1], block, ordinal: i });
1634
+ }
1635
+ return out;
1636
+ }
1637
+ async function runLogMigrateLegacy(input) {
1638
+ let text = "";
1639
+ try {
1640
+ text = readFileSync7(join12(input.vault, "log.md"), "utf8");
1641
+ } catch {
1642
+ return {
1643
+ exitCode: ExitCode.OK,
1644
+ result: ok({
1645
+ planned: 0,
1646
+ created: 0,
1647
+ already_covered: 0,
1648
+ dry_run: !input.write,
1649
+ humanHint: "no root log.md to migrate"
1650
+ })
1651
+ };
1652
+ }
1653
+ const blocks = splitLegacyBlocks(text);
1654
+ let created = 0;
1655
+ let already = 0;
1656
+ for (const b of blocks) {
1657
+ const publish = PUBLISH_RE.exec(b.block)?.[1]?.toLowerCase();
1658
+ const op = publish && /^[0-9a-f]{64}$/.test(publish) ? publish : operationId("skillwiki-legacy-log-v1", [b.date, String(b.ordinal), b.block]);
1659
+ const heading = b.block.split("\n")[0] ?? `## [${b.date}] legacy`;
1660
+ const event = {
1661
+ schema: "skillwiki-log-event/v1",
1662
+ operation_id: op,
1663
+ occurred_at: `${b.date}T00:00:00.000Z`,
1664
+ host_id: input.hostId ?? "standalone",
1665
+ actor: "skillwiki-cli",
1666
+ kind: "legacy-log-entry",
1667
+ target: "log.md",
1668
+ note: heading,
1669
+ metadata: { legacy_markdown: b.block }
1670
+ };
1671
+ if (!input.write) continue;
1672
+ const wrote = await writeLogEvent(input.vault, event);
1673
+ if (!wrote.ok) {
1674
+ if (wrote.error === "EVENT_IDENTITY_COLLISION") {
1675
+ already += 1;
1676
+ continue;
1677
+ }
1678
+ return { exitCode: ExitCode.WRITE_FAILED, result: wrote };
1679
+ }
1680
+ if (wrote.data.created) created += 1;
1681
+ else already += 1;
1682
+ }
1683
+ return {
1684
+ exitCode: ExitCode.OK,
1685
+ result: ok({
1686
+ planned: blocks.length,
1687
+ created: input.write ? created : 0,
1688
+ already_covered: input.write ? already : 0,
1689
+ dry_run: !input.write,
1690
+ humanHint: input.write ? `migrated legacy log: created=${created} already=${already} planned=${blocks.length}` : `dry run: would migrate ${blocks.length} legacy log block(s)`
1691
+ })
1692
+ };
1693
+ }
1694
+
1695
+ // src/commands/projections-materialize.ts
1696
+ import { readFileSync as readFileSync8 } from "fs";
1697
+ import { join as join13 } from "path";
1698
+ var defaultDeps = {
1699
+ writeText: (path, text) => atomicWriteText(path, text)
1700
+ };
1701
+ async function runProjectionsMaterialize(input, deps = defaultDeps) {
1702
+ const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
1703
+ const auth = resolveProjectionAuthority(fleet);
1704
+ if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
1705
+ const indexProj = await renderRootIndex({ vault: input.vault });
1706
+ if (!indexProj.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: indexProj };
1707
+ const events = await readLogEvents(input.vault);
1708
+ if (!events.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
1709
+ const logText = renderLogProjection(events.data);
1710
+ let curIndex = "";
1711
+ let curLog = "";
1712
+ try {
1713
+ curIndex = readFileSync8(join13(input.vault, "index.md"), "utf8");
1714
+ } catch {
1715
+ }
1716
+ try {
1717
+ curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
1718
+ } catch {
1719
+ }
1720
+ const indexDrift = curIndex !== indexProj.data.text;
1721
+ const logDrift = curLog !== logText;
1722
+ if (!input.write) {
1723
+ return {
1724
+ exitCode: ExitCode.OK,
1725
+ result: ok({
1726
+ authority_host_id: auth.data.authority_host_id,
1727
+ current_host_id: auth.data.current_host_id,
1728
+ can_write: auth.data.can_write,
1729
+ index_changed: false,
1730
+ log_changed: false,
1731
+ index_drift: indexDrift,
1732
+ log_drift: logDrift,
1733
+ rolled_back: false,
1734
+ dry_run: true,
1735
+ humanHint: `dry run: index_drift=${indexDrift} log_drift=${logDrift} can_write=${auth.data.can_write}`
1736
+ })
1737
+ };
1738
+ }
1739
+ if (!auth.data.can_write) {
1740
+ return {
1741
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1742
+ result: err("PREFLIGHT_FAILED", {
1743
+ reason: "projection-authority",
1744
+ authority_host_id: auth.data.authority_host_id,
1745
+ current_host_id: auth.data.current_host_id,
1746
+ index_drift: indexDrift,
1747
+ log_drift: logDrift
1748
+ })
1749
+ };
1750
+ }
1751
+ const indexPath = join13(input.vault, "index.md");
1752
+ const logPath = join13(input.vault, "log.md");
1753
+ const indexWrite = await deps.writeText(indexPath, indexProj.data.text);
1754
+ if (!indexWrite.ok) return { exitCode: ExitCode.WRITE_FAILED, result: indexWrite };
1755
+ const logWrite = await deps.writeText(logPath, logText);
1756
+ if (!logWrite.ok) {
1757
+ await deps.writeText(indexPath, curIndex);
1758
+ return {
1759
+ exitCode: ExitCode.WRITE_FAILED,
1760
+ result: ok({
1761
+ authority_host_id: auth.data.authority_host_id,
1762
+ current_host_id: auth.data.current_host_id,
1763
+ can_write: true,
1764
+ index_changed: false,
1765
+ log_changed: false,
1766
+ index_drift: indexDrift,
1767
+ log_drift: logDrift,
1768
+ rolled_back: true,
1769
+ dry_run: false,
1770
+ humanHint: "projection materialize rolled back after log write failure"
1771
+ })
1772
+ };
1773
+ }
1774
+ return {
1775
+ exitCode: ExitCode.OK,
1776
+ result: ok({
1777
+ authority_host_id: auth.data.authority_host_id,
1778
+ current_host_id: auth.data.current_host_id,
1779
+ can_write: true,
1780
+ index_changed: indexWrite.data.changed,
1781
+ log_changed: logWrite.data.changed,
1782
+ index_drift: indexDrift,
1783
+ log_drift: logDrift,
1784
+ rolled_back: false,
1785
+ dry_run: false,
1786
+ humanHint: `materialized projections index_changed=${indexWrite.data.changed} log_changed=${logWrite.data.changed}`
1787
+ })
1788
+ };
1789
+ }
1790
+
773
1791
  // src/commands/claim.ts
774
- import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile4 } from "fs/promises";
775
- import { existsSync, statSync } from "fs";
776
- import { join as join6 } from "path";
1792
+ import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
1793
+ import { existsSync as existsSync2, statSync } from "fs";
1794
+ import { join as join14 } from "path";
777
1795
  function extractDate(filename) {
778
1796
  const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
779
1797
  return m?.[1] ?? "";
@@ -785,14 +1803,14 @@ function extractProjectSlug(projectField) {
785
1803
  return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
786
1804
  }
787
1805
  async function runClaim(input) {
788
- if (!existsSync(input.vault) || !statSync(input.vault).isDirectory()) {
1806
+ if (!existsSync2(input.vault) || !statSync(input.vault).isDirectory()) {
789
1807
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { path: input.vault }) };
790
1808
  }
791
- const absTranscript = join6(input.vault, input.transcript);
792
- if (!existsSync(absTranscript)) {
1809
+ const absTranscript = join14(input.vault, input.transcript);
1810
+ if (!existsSync2(absTranscript)) {
793
1811
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.transcript }) };
794
1812
  }
795
- const content = await readFile4(absTranscript, "utf8");
1813
+ const content = await readFile5(absTranscript, "utf8");
796
1814
  const fm = extractFrontmatter(content);
797
1815
  let projectSlug = input.project;
798
1816
  if (!projectSlug && fm.ok && typeof fm.data.project === "string") {
@@ -804,8 +1822,8 @@ async function runClaim(input) {
804
1822
  result: err("SCHEME_REJECTED", { message: "No project specified. Use --project or set project in transcript frontmatter." })
805
1823
  };
806
1824
  }
807
- const projectDir = join6(input.vault, "projects", projectSlug);
808
- if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
1825
+ const projectDir = join14(input.vault, "projects", projectSlug);
1826
+ if (!existsSync2(projectDir) || !statSync(projectDir).isDirectory()) {
809
1827
  return {
810
1828
  exitCode: ExitCode.PROJECT_NOT_FOUND,
811
1829
  result: err("PROJECT_NOT_FOUND", { project: projectSlug })
@@ -821,10 +1839,10 @@ async function runClaim(input) {
821
1839
  }
822
1840
  const workSlug = input.slug || extractSlugFromFilename(filename);
823
1841
  const dirName = `${date}-${workSlug}`;
824
- const workDir = join6(projectDir, "work", dirName);
1842
+ const workDir = join14(projectDir, "work", dirName);
825
1843
  const relWorkDir = `projects/${projectSlug}/work/${dirName}`;
826
1844
  const relSpecPath = `${relWorkDir}/spec.md`;
827
- if (existsSync(workDir)) {
1845
+ if (existsSync2(workDir)) {
828
1846
  return {
829
1847
  exitCode: ExitCode.OK,
830
1848
  result: ok({
@@ -835,7 +1853,7 @@ async function runClaim(input) {
835
1853
  })
836
1854
  };
837
1855
  }
838
- await mkdir4(workDir, { recursive: true });
1856
+ await mkdir5(workDir, { recursive: true });
839
1857
  const kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "task";
840
1858
  const specLines = [
841
1859
  "---",
@@ -850,7 +1868,7 @@ async function runClaim(input) {
850
1868
  `Claimed from ${input.transcript}`,
851
1869
  ""
852
1870
  ];
853
- await writeFile3(join6(workDir, "spec.md"), specLines.join("\n"), "utf8");
1871
+ await writeFile3(join14(workDir, "spec.md"), specLines.join("\n"), "utf8");
854
1872
  appendLastOp(input.vault, {
855
1873
  operation: "claim",
856
1874
  summary: `claimed ${input.transcript} \u2192 ${relWorkDir}`,
@@ -869,8 +1887,8 @@ async function runClaim(input) {
869
1887
  }
870
1888
 
871
1889
  // src/commands/health.ts
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";
1890
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
1891
+ import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
874
1892
  import { platform } from "os";
875
1893
  function statusFromCounts(counts) {
876
1894
  if ((counts.error ?? 0) > 0) return "error";
@@ -981,8 +1999,8 @@ function summarizeChecks(checks) {
981
1999
  };
982
2000
  }
983
2001
  function classifyLog(path, id, label, okPattern) {
984
- if (!existsSync2(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
985
- const lines = readFileSync2(path, "utf8").split(/\r?\n/).filter(Boolean);
2002
+ if (!existsSync3(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
2003
+ const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
986
2004
  if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
987
2005
  const statusLine = [...lines].reverse().find(
988
2006
  (line) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z /.test(line) && (okPattern.test(line) || /\bFAIL\b|\bERROR\b/i.test(line))
@@ -1003,12 +2021,12 @@ function runVaultSyncHealth(home, syncMode) {
1003
2021
  };
1004
2022
  }
1005
2023
  const isMac = platform() === "darwin";
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");
2024
+ const shareDir = isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin");
2025
+ const logDir = isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log");
2026
+ const filterPath = join15(home, ".config", "rclone", "wiki-push-filters.txt");
1009
2027
  const checks = [];
1010
- const pushScript = join7(shareDir, "wiki-push.sh");
1011
- if (syncMode === "optional" && !existsSync2(pushScript)) {
2028
+ const pushScript = join15(shareDir, "wiki-push.sh");
2029
+ if (syncMode === "optional" && !existsSync3(pushScript)) {
1012
2030
  return {
1013
2031
  status: "pass",
1014
2032
  blocking: false,
@@ -1021,26 +2039,26 @@ function runVaultSyncHealth(home, syncMode) {
1021
2039
  }]
1022
2040
  };
1023
2041
  }
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}` });
2042
+ 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}` });
1025
2043
  if (isMac) {
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)" });
2044
+ const pushPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
2045
+ const fetchPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
2046
+ 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)" });
1029
2047
  checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
1030
2048
  } else {
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)) {
2049
+ const pushTimer = join15(home, ".config", "systemd", "user", "wiki-push.timer");
2050
+ const fetchTimer = join15(home, ".config", "systemd", "user", "wiki-fetch.timer");
2051
+ const fuseTimer = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
2052
+ const fuseService = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
2053
+ 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)" });
2054
+ 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)" });
2055
+ }
2056
+ checks.push(classifyLog(join15(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
2057
+ checks.push(classifyLog(join15(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
2058
+ if (!existsSync3(filterPath)) {
1041
2059
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
1042
2060
  } else {
1043
- const content = readFileSync2(filterPath, "utf8");
2061
+ const content = readFileSync9(filterPath, "utf8");
1044
2062
  const missing = [
1045
2063
  "remotely-save/data.json",
1046
2064
  ".skillwiki/sync.lock",
@@ -1135,7 +2153,7 @@ function buildHumanHint(report) {
1135
2153
  function writeReport(out, report) {
1136
2154
  mkdirSync(dirname4(out), { recursive: true });
1137
2155
  const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
1138
- writeFileSync(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
2156
+ writeFileSync3(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
1139
2157
  renameSync(tmp, out);
1140
2158
  }
1141
2159
  function buildIncompleteReport(input, syncMode) {
@@ -1359,12 +2377,12 @@ async function runHealth(input) {
1359
2377
  }
1360
2378
 
1361
2379
  // src/commands/archive.ts
1362
- import { rename as rename2, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
1363
- import { join as join9, dirname as dirname5 } from "path";
2380
+ import { rename as rename2, mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
2381
+ import { join as join17, dirname as dirname5 } from "path";
1364
2382
 
1365
2383
  // src/utils/delete-intent.ts
1366
- import { mkdir as mkdir5, writeFile as writeFile4, readdir as readdir3, readFile as readFile5 } from "fs/promises";
1367
- import { join as join8 } from "path";
2384
+ import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
2385
+ import { join as join16 } from "path";
1368
2386
  var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
1369
2387
  var DELETE_INTENT_DIR = "meta/delete-intents";
1370
2388
  function normalizeVaultRelPath(path) {
@@ -1395,10 +2413,10 @@ function buildDeleteIntent(input) {
1395
2413
  };
1396
2414
  }
1397
2415
  async function writeDeleteIntent(vault, intent) {
1398
- const dir = join8(vault, DELETE_INTENT_DIR);
1399
- await mkdir5(dir, { recursive: true });
2416
+ const dir = join16(vault, DELETE_INTENT_DIR);
2417
+ await mkdir6(dir, { recursive: true });
1400
2418
  const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
1401
- await writeFile4(join8(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
2419
+ await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
1402
2420
  return rel;
1403
2421
  }
1404
2422
 
@@ -1436,7 +2454,7 @@ async function runArchive(input) {
1436
2454
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1437
2455
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1438
2456
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
1439
- const archivePath = join9("_archive", relPath).replace(/\\/g, "/");
2457
+ const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
1440
2458
  const remoteRoot = normalizeRemoteRoot(input.remote);
1441
2459
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1442
2460
  let cascade;
@@ -1462,7 +2480,7 @@ async function runArchive(input) {
1462
2480
  const indexRefs = [];
1463
2481
  if (!isRaw) {
1464
2482
  try {
1465
- const idx = await readFile6(join9(input.vault, "index.md"), "utf8");
2483
+ const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
1466
2484
  idx.split("\n").forEach((line, i) => {
1467
2485
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
1468
2486
  });
@@ -1489,8 +2507,8 @@ async function runArchive(input) {
1489
2507
  }
1490
2508
  if (input.cascade && input.apply && cascade) {
1491
2509
  for (const ref of cascade.source_array_refs) {
1492
- const absPath = join9(input.vault, ref.page);
1493
- const text = await readFile6(absPath, "utf8");
2510
+ const absPath = join17(input.vault, ref.page);
2511
+ const text = await readFile7(absPath, "utf8");
1494
2512
  const split = splitFrontmatter(text);
1495
2513
  if (!split.ok) continue;
1496
2514
  const before = split.data.rawFrontmatter;
@@ -1507,23 +2525,21 @@ ${fmRewritten}
1507
2525
  }
1508
2526
  }
1509
2527
  }
1510
- await mkdir6(dirname5(join9(input.vault, archivePath)), { recursive: true });
2528
+ await mkdir7(dirname5(join17(input.vault, archivePath)), { recursive: true });
2529
+ await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
1511
2530
  let indexUpdated = false;
1512
2531
  if (!isRaw) {
1513
- const indexPath = join9(input.vault, "index.md");
1514
- try {
1515
- const idx = await readFile6(indexPath, "utf8");
1516
- const originalLines = idx.split("\n");
1517
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1518
- if (filtered.length !== originalLines.length) {
1519
- await writeFile5(indexPath, filtered.join("\n"), "utf8");
1520
- indexUpdated = true;
1521
- }
1522
- } catch (e) {
1523
- if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
2532
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2533
+ const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
2534
+ const fullTarget = relPath.replace(/\.md$/, "");
2535
+ const bare = fullTarget.split("/").pop() ?? fullTarget;
2536
+ const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
2537
+ const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
2538
+ if (projection.ok && projection.data.text !== before) {
2539
+ const written = await writeRootIndexProjection2(input.vault, projection.data);
2540
+ if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
1524
2541
  }
1525
2542
  }
1526
- await rename2(join9(input.vault, relPath), join9(input.vault, archivePath));
1527
2543
  const archiveIntent = buildDeleteIntent({
1528
2544
  path: relPath,
1529
2545
  action: "archive",
@@ -1564,8 +2580,8 @@ ${fmRewritten}
1564
2580
  }
1565
2581
 
1566
2582
  // src/commands/remove.ts
1567
- import { unlink as unlink2, readFile as readFile7, writeFile as writeFile6, access } from "fs/promises";
1568
- import { join as join10 } from "path";
2583
+ import { unlink as unlink2, access } from "fs/promises";
2584
+ import { join as join18 } from "path";
1569
2585
  async function pathExists(abs) {
1570
2586
  try {
1571
2587
  await access(abs);
@@ -1591,7 +2607,7 @@ async function runRemove(input) {
1591
2607
  if (!relPath) {
1592
2608
  try {
1593
2609
  const candidate = normalizeVaultRelPath(input.page);
1594
- if (await pathExists(join10(input.vault, candidate))) {
2610
+ if (await pathExists(join18(input.vault, candidate))) {
1595
2611
  relPath = candidate;
1596
2612
  }
1597
2613
  } catch {
@@ -1611,22 +2627,7 @@ async function runRemove(input) {
1611
2627
  }
1612
2628
  const remoteRoot = normalizeRemoteRoot(input.remote);
1613
2629
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1614
- const slug = relPath.replace(/\.md$/, "").split("/").pop() ?? relPath;
1615
2630
  let indexUpdated = false;
1616
- if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
1617
- const indexPath = join10(input.vault, "index.md");
1618
- try {
1619
- const idx = await readFile7(indexPath, "utf8");
1620
- const originalLines = idx.split("\n");
1621
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1622
- if (filtered.length !== originalLines.length) {
1623
- await writeFile6(indexPath, filtered.join("\n"), "utf8");
1624
- indexUpdated = true;
1625
- }
1626
- } catch (e) {
1627
- if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
1628
- }
1629
- }
1630
2631
  const intent = buildDeleteIntent({
1631
2632
  path: relPath,
1632
2633
  action: "remove",
@@ -1635,7 +2636,21 @@ async function runRemove(input) {
1635
2636
  reason: input.reason
1636
2637
  });
1637
2638
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
1638
- await unlink2(join10(input.vault, relPath));
2639
+ await unlink2(join18(input.vault, relPath));
2640
+ if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
2641
+ const { readFile: readFile17 } = await import("fs/promises");
2642
+ const { join: pathJoin } = await import("path");
2643
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2644
+ const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
2645
+ const fullTarget = relPath.replace(/\.md$/, "");
2646
+ const bare = fullTarget.split("/").pop() ?? fullTarget;
2647
+ const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
2648
+ const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
2649
+ if (projection.ok && projection.data.text !== before) {
2650
+ const written = await writeRootIndexProjection2(input.vault, projection.data);
2651
+ if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
2652
+ }
2653
+ }
1639
2654
  appendLastOp(input.vault, {
1640
2655
  operation: "remove",
1641
2656
  summary: `removed ${relPath} (tombstone ${tombstonePath})`,
@@ -1664,7 +2679,7 @@ async function runRemove(input) {
1664
2679
  }
1665
2680
 
1666
2681
  // src/commands/drift.ts
1667
- import { createHash as createHash2 } from "crypto";
2682
+ import { createHash as createHash3 } from "crypto";
1668
2683
 
1669
2684
  // src/utils/fetch.ts
1670
2685
  async function controlledFetch(url, opts) {
@@ -1743,7 +2758,7 @@ async function runDrift(input) {
1743
2758
  });
1744
2759
  continue;
1745
2760
  }
1746
- const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
2761
+ const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1747
2762
  const identity = assessSourceIdentity({
1748
2763
  rawPath: raw.relPath,
1749
2764
  sourceUrl,
@@ -1974,14 +2989,14 @@ ${migratedBody}${newFooter}`;
1974
2989
 
1975
2990
  // src/commands/update.ts
1976
2991
  import { execSync } from "child_process";
1977
- import { join as join11 } from "path";
2992
+ import { join as join19 } from "path";
1978
2993
  function resolveGlobalSkillsRoot() {
1979
2994
  try {
1980
2995
  const globalRoot = execSync("npm root -g", {
1981
2996
  encoding: "utf8",
1982
2997
  timeout: 5e3
1983
2998
  }).trim();
1984
- return join11(globalRoot, "skillwiki", "skills");
2999
+ return join19(globalRoot, "skillwiki", "skills");
1985
3000
  } catch {
1986
3001
  return null;
1987
3002
  }
@@ -2009,7 +3024,7 @@ async function runUpdate(input) {
2009
3024
  const pkg2 = readCliPackageJson();
2010
3025
  const currentVersion = pkg2.version;
2011
3026
  const tag = normalizeDistTag(input.distTag);
2012
- const target = join11(input.home, ".claude", "skills");
3027
+ const target = join19(input.home, ".claude", "skills");
2013
3028
  let latest;
2014
3029
  try {
2015
3030
  latest = execSync(`npm view skillwiki@${tag} version`, {
@@ -2086,22 +3101,22 @@ async function runUpdate(input) {
2086
3101
 
2087
3102
  // src/commands/self-update.ts
2088
3103
  import { execSync as execSync2 } from "child_process";
2089
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2090
- import { join as join12 } from "path";
3104
+ import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
3105
+ import { join as join20 } from "path";
2091
3106
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
2092
3107
  async function runSelfUpdate(input) {
2093
3108
  const currentVersion = readCliPackageJson().version;
2094
3109
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
2095
3110
  const distTag = normalizeDistTag(input.distTag);
2096
- const localPkgPath = join12(sourceRoot, "packages", "cli", "package.json");
2097
- const hasLocalSource = existsSync3(localPkgPath);
3111
+ const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
3112
+ const hasLocalSource = existsSync4(localPkgPath);
2098
3113
  if (input.check) {
2099
3114
  let availableVersion = null;
2100
3115
  let source;
2101
3116
  if (hasLocalSource) {
2102
3117
  source = "local";
2103
3118
  try {
2104
- availableVersion = JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? null;
3119
+ availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
2105
3120
  } catch {
2106
3121
  availableVersion = null;
2107
3122
  }
@@ -2159,7 +3174,7 @@ async function runSelfUpdate(input) {
2159
3174
  }
2160
3175
  const newVersion = (() => {
2161
3176
  try {
2162
- return JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? "unknown";
3177
+ return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
2163
3178
  } catch {
2164
3179
  return "unknown";
2165
3180
  }
@@ -2225,21 +3240,21 @@ async function runSelfUpdate(input) {
2225
3240
  }
2226
3241
 
2227
3242
  // src/commands/transcripts.ts
2228
- import { readdir as readdir4, stat as stat3, readFile as readFile8 } from "fs/promises";
2229
- import { join as join13 } from "path";
3243
+ import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
3244
+ import { join as join21 } from "path";
2230
3245
  async function runTranscripts(input) {
2231
- const dir = join13(input.vault, "raw", "transcripts");
3246
+ const dir = join21(input.vault, "raw", "transcripts");
2232
3247
  let entries;
2233
3248
  try {
2234
- entries = await readdir4(dir, { withFileTypes: true });
3249
+ entries = await readdir5(dir, { withFileTypes: true });
2235
3250
  } catch {
2236
3251
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
2237
3252
  }
2238
3253
  const transcripts = [];
2239
3254
  for (const entry of entries) {
2240
3255
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2241
- const filePath = join13(dir, entry.name);
2242
- const content = await readFile8(filePath, "utf8");
3256
+ const filePath = join21(dir, entry.name);
3257
+ const content = await readFile9(filePath, "utf8");
2243
3258
  const fm = extractFrontmatter(content);
2244
3259
  if (!fm.ok) continue;
2245
3260
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -2256,10 +3271,10 @@ async function runTranscripts(input) {
2256
3271
  }
2257
3272
 
2258
3273
  // src/commands/compound.ts
2259
- import { writeFile as writeFile7, mkdir as mkdir7, readdir as readdir5, unlink as unlink3 } from "fs/promises";
2260
- import { join as join14 } from "path";
2261
- import { existsSync as existsSync4 } from "fs";
2262
- import { readFile as readFile9 } from "fs/promises";
3274
+ import { writeFile as writeFile7, mkdir as mkdir8, readdir as readdir6, unlink as unlink3 } from "fs/promises";
3275
+ import { join as join22 } from "path";
3276
+ import { existsSync as existsSync5 } from "fs";
3277
+ import { readFile as readFile10 } from "fs/promises";
2263
3278
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
2264
3279
  var FIELD_RE = {
2265
3280
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -2357,17 +3372,17 @@ function extractRetroFields(date, cycleName, block) {
2357
3372
  };
2358
3373
  }
2359
3374
  async function runCompound(input) {
2360
- const logPath = join14(input.vault, "log.md");
3375
+ const logPath = join22(input.vault, "log.md");
2361
3376
  let logText;
2362
3377
  try {
2363
- logText = await readFile9(logPath, "utf8");
3378
+ logText = await readFile10(logPath, "utf8");
2364
3379
  } catch {
2365
3380
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2366
3381
  }
2367
3382
  const entries = parseRetroEntries(logText);
2368
3383
  const promoted = [];
2369
3384
  const skipped = [];
2370
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
3385
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
2371
3386
  for (const entry of entries) {
2372
3387
  const generalizeValue = entry.generalize.trim();
2373
3388
  if (!/^yes/i.test(generalizeValue)) {
@@ -2375,8 +3390,8 @@ async function runCompound(input) {
2375
3390
  continue;
2376
3391
  }
2377
3392
  const slug = slugify(entry.cycleName);
2378
- const compoundPath = join14(compoundDir, `${slug}.md`);
2379
- if (existsSync4(compoundPath)) {
3393
+ const compoundPath = join22(compoundDir, `${slug}.md`);
3394
+ if (existsSync5(compoundPath)) {
2380
3395
  skipped.push(entry.date);
2381
3396
  continue;
2382
3397
  }
@@ -2414,8 +3429,8 @@ async function runCompound(input) {
2414
3429
  ].join("\n");
2415
3430
  const content = frontmatter + "\n" + body;
2416
3431
  if (!input.dryRun) {
2417
- if (!existsSync4(compoundDir)) {
2418
- await mkdir7(compoundDir, { recursive: true });
3432
+ if (!existsSync5(compoundDir)) {
3433
+ await mkdir8(compoundDir, { recursive: true });
2419
3434
  }
2420
3435
  await writeFile7(compoundPath, content, "utf8");
2421
3436
  }
@@ -2436,16 +3451,16 @@ async function runCompound(input) {
2436
3451
  };
2437
3452
  }
2438
3453
  async function runCompoundDelete(input) {
2439
- const projectDir = join14(input.vault, "projects", input.project);
2440
- if (!existsSync4(projectDir)) {
3454
+ const projectDir = join22(input.vault, "projects", input.project);
3455
+ if (!existsSync5(projectDir)) {
2441
3456
  return {
2442
3457
  exitCode: ExitCode.PROJECT_NOT_FOUND,
2443
3458
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
2444
3459
  };
2445
3460
  }
2446
3461
  const entryName = input.entry.replace(/\.md$/, "");
2447
- const compoundPath = join14(projectDir, "compound", `${entryName}.md`);
2448
- if (!existsSync4(compoundPath)) {
3462
+ const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
3463
+ if (!existsSync5(compoundPath)) {
2449
3464
  return {
2450
3465
  exitCode: ExitCode.FILE_NOT_FOUND,
2451
3466
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -2478,8 +3493,8 @@ knowledge.md regenerated`
2478
3493
  };
2479
3494
  }
2480
3495
  async function runCompoundList(input) {
2481
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
2482
- if (!existsSync4(compoundDir)) {
3496
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
3497
+ if (!existsSync5(compoundDir)) {
2483
3498
  return {
2484
3499
  exitCode: ExitCode.OK,
2485
3500
  result: ok({
@@ -2493,7 +3508,7 @@ no compound directory found`
2493
3508
  }
2494
3509
  let dirents;
2495
3510
  try {
2496
- dirents = await readdir5(compoundDir, { withFileTypes: true });
3511
+ dirents = await readdir6(compoundDir, { withFileTypes: true });
2497
3512
  } catch {
2498
3513
  return {
2499
3514
  exitCode: ExitCode.OK,
@@ -2509,10 +3524,10 @@ could not read compound directory`
2509
3524
  const entries = [];
2510
3525
  for (const dirent of dirents) {
2511
3526
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
2512
- const filePath = join14(compoundDir, dirent.name);
3527
+ const filePath = join22(compoundDir, dirent.name);
2513
3528
  let text;
2514
3529
  try {
2515
- text = await readFile9(filePath, "utf8");
3530
+ text = await readFile10(filePath, "utf8");
2516
3531
  } catch {
2517
3532
  continue;
2518
3533
  }
@@ -2541,8 +3556,8 @@ no compound entries found`;
2541
3556
  }
2542
3557
 
2543
3558
  // src/commands/session-brief.ts
2544
- import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
2545
- import { join as join15, relative, sep } from "path";
3559
+ import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
3560
+ import { join as join23, relative, sep } from "path";
2546
3561
  var MAX_WORDS = 900;
2547
3562
  async function runSessionBrief(input) {
2548
3563
  const scan = await scanVault(input.vault);
@@ -2642,7 +3657,7 @@ async function resolveProject(input) {
2642
3657
  const envProject = input.env?.SKILLWIKI_PROJECT;
2643
3658
  if (envProject) return envProject;
2644
3659
  const cwd = input.cwd ?? process.cwd();
2645
- const projectDotenv = await readProjectSlug(join15(cwd, ".skillwiki", ".env"));
3660
+ const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
2646
3661
  if (projectDotenv) return projectDotenv;
2647
3662
  const inferred = inferProjectFromPath(input.vault, cwd);
2648
3663
  if (inferred) return inferred;
@@ -2651,7 +3666,7 @@ async function resolveProject(input) {
2651
3666
  async function readProjectSlug(file) {
2652
3667
  let text;
2653
3668
  try {
2654
- text = await readFile10(file, "utf8");
3669
+ text = await readFile11(file, "utf8");
2655
3670
  } catch {
2656
3671
  return void 0;
2657
3672
  }
@@ -2728,7 +3743,7 @@ async function loadTrendDigests(typedPages) {
2728
3743
  return out;
2729
3744
  }
2730
3745
  async function loadSessionPins(vault, project) {
2731
- const text = await readIfExists(join15(vault, "meta", "session-pins.md"));
3746
+ const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
2732
3747
  if (!text) return [];
2733
3748
  const fm = extractFrontmatter(text);
2734
3749
  if (!fm.ok) return [];
@@ -2845,7 +3860,7 @@ function satelliteHealthWarnings(warning) {
2845
3860
  return warning ? [warning] : [];
2846
3861
  }
2847
3862
  async function loadHealthWarnings(vault) {
2848
- const text = await readIfExists(join15(vault, ".skillwiki", "health.json"));
3863
+ const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
2849
3864
  if (!text) return [];
2850
3865
  try {
2851
3866
  const parsed = JSON.parse(text);
@@ -2858,7 +3873,7 @@ async function loadHealthWarnings(vault) {
2858
3873
  }
2859
3874
  }
2860
3875
  async function loadMemoryTopics(vault, project) {
2861
- const text = await readIfExists(join15(vault, ".skillwiki", "memory", project, "topics.json"));
3876
+ const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
2862
3877
  if (!text) return [];
2863
3878
  try {
2864
3879
  const parsed = JSON.parse(text);
@@ -2875,11 +3890,11 @@ async function loadMemoryTopics(vault, project) {
2875
3890
  }
2876
3891
  }
2877
3892
  async function writeBriefArtifacts(vault, input) {
2878
- const metaPath = join15(vault, "meta", "latest-session-brief.md");
2879
- const cacheMdPath = join15(vault, ".skillwiki", "session-brief.md");
2880
- const cacheJsonPath = join15(vault, ".skillwiki", "session-brief.json");
2881
- await mkdir8(join15(vault, "meta"), { recursive: true });
2882
- await mkdir8(join15(vault, ".skillwiki"), { recursive: true });
3893
+ const metaPath = join23(vault, "meta", "latest-session-brief.md");
3894
+ const cacheMdPath = join23(vault, ".skillwiki", "session-brief.md");
3895
+ const cacheJsonPath = join23(vault, ".skillwiki", "session-brief.json");
3896
+ await mkdir9(join23(vault, "meta"), { recursive: true });
3897
+ await mkdir9(join23(vault, ".skillwiki"), { recursive: true });
2883
3898
  const committed = renderCommittedBrief(input);
2884
3899
  const previousComparable = comparableBrief(await readIfExists(metaPath));
2885
3900
  const nextComparable = comparableBrief(committed);
@@ -2894,8 +3909,17 @@ async function writeBriefArtifacts(vault, input) {
2894
3909
  memory_topics: input.memoryTopics
2895
3910
  }, null, 2)}
2896
3911
  `, "utf8");
2897
- const indexUpdated = await ensureIndexEntry(vault);
2898
- const logUpdated = materialChange ? await appendMaterialLog(vault, input.today) : false;
3912
+ const indexUpdated = await rebuildRootIndexProjection(vault);
3913
+ let logUpdated = false;
3914
+ if (materialChange) {
3915
+ const log = await runLogAppend({
3916
+ vault,
3917
+ content: `## [${input.today}] session-brief | refreshed: meta/latest-session-brief.md`,
3918
+ strictLock: true,
3919
+ recordLastOp: false
3920
+ });
3921
+ logUpdated = log.result.ok && log.result.data.appended;
3922
+ }
2899
3923
  if (materialChange) {
2900
3924
  appendLastOp(vault, {
2901
3925
  operation: "session-brief",
@@ -2939,37 +3963,18 @@ function renderCommittedBrief(input) {
2939
3963
  ""
2940
3964
  ].filter((line) => line !== "").join("\n");
2941
3965
  }
2942
- async function ensureIndexEntry(vault) {
2943
- const indexPath = join15(vault, "index.md");
2944
- let text = await readIfExists(indexPath);
2945
- if (!text) return false;
2946
- if (text.includes("[[meta/latest-session-brief]]")) return false;
2947
- const entry = "- [[meta/latest-session-brief]] \u2014 Latest Session Brief";
2948
- const lines = text.split(/\r?\n/);
2949
- const sectionIdx = lines.findIndex((line) => line.trim() === "## Meta");
2950
- if (sectionIdx === -1) {
2951
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
2952
- lines.push("", "## Meta", entry);
2953
- } else {
2954
- let insertAt = sectionIdx + 1;
2955
- while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
2956
- lines.splice(insertAt, 0, entry);
2957
- }
2958
- await writeFile8(indexPath, lines.join("\n"), "utf8");
2959
- return true;
2960
- }
2961
- async function appendMaterialLog(vault, today) {
2962
- const logPath = join15(vault, "log.md");
2963
- const text = await readIfExists(logPath);
2964
- if (!text) return false;
2965
- const entry = `
2966
- ## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
2967
- await writeFile8(logPath, text.trimEnd() + entry + "\n", "utf8");
2968
- return true;
3966
+ async function rebuildRootIndexProjection(vault) {
3967
+ const before = await readIfExists(join23(vault, "index.md"));
3968
+ if (!before) return false;
3969
+ const projection = await renderRootIndex({ vault, currentText: before });
3970
+ if (!projection.ok) return false;
3971
+ if (projection.data.text === before) return false;
3972
+ const written = await writeRootIndexProjection(vault, projection.data);
3973
+ return written.ok && written.data.changed;
2969
3974
  }
2970
3975
  async function readIfExists(path) {
2971
3976
  try {
2972
- return await readFile10(path, "utf8");
3977
+ return await readFile11(path, "utf8");
2973
3978
  } catch {
2974
3979
  return "";
2975
3980
  }
@@ -3011,16 +4016,18 @@ function dateFromPath(path) {
3011
4016
  }
3012
4017
 
3013
4018
  // src/commands/ingest.ts
3014
- import { readFile as readFile12, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
3015
- import { join as join17 } from "path";
4019
+ import { readFile as readFile13, open as open2, unlink as unlink4, mkdir as mkdir10 } from "fs/promises";
4020
+ import { join as join25 } from "path";
3016
4021
  import { createHash as createHash4 } from "crypto";
3017
4022
 
3018
4023
  // src/commands/page-publish.ts
3019
- import { createHash as createHash3 } from "crypto";
3020
4024
  import { realpathSync } from "fs";
3021
- import { readFile as readFile11 } from "fs/promises";
3022
- import { join as join16, resolve as resolve3 } from "path";
3023
- var DEFAULT_DEPS = { afterStage: async () => void 0 };
4025
+ import { readFile as readFile12 } from "fs/promises";
4026
+ import { join as join24, resolve as resolve3 } from "path";
4027
+ var DEFAULT_DEPS = {
4028
+ afterStage: async () => void 0,
4029
+ preflight: (input) => runManagedWritePreflight(input)
4030
+ };
3024
4031
  function errorExitCode(error) {
3025
4032
  switch (error) {
3026
4033
  case "FILE_NOT_FOUND":
@@ -3036,6 +4043,9 @@ function errorExitCode(error) {
3036
4043
  return ExitCode.SENSITIVE_CONTENT_DETECTED;
3037
4044
  case "SYNC_LOCK_HELD":
3038
4045
  return ExitCode.SYNC_LOCK_HELD;
4046
+ case "PREFLIGHT_FAILED":
4047
+ return ExitCode.PREFLIGHT_FAILED;
4048
+ case "EVENT_IDENTITY_COLLISION":
3039
4049
  case "WRITE_FAILED":
3040
4050
  return ExitCode.WRITE_FAILED;
3041
4051
  default:
@@ -3043,7 +4053,11 @@ function errorExitCode(error) {
3043
4053
  }
3044
4054
  }
3045
4055
  function publicationId(target, content, logNote = "") {
3046
- return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
4056
+ return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
4057
+ }
4058
+ function resolveRootAggregateMode(env = process.env) {
4059
+ if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
4060
+ return "dual";
3047
4061
  }
3048
4062
  function prepareFrozenPublication(input, source) {
3049
4063
  const target = assertTargetInsideVault(input.vault, input.target);
@@ -3079,7 +4093,7 @@ function preparePagePublicationFromContent(input) {
3079
4093
  async function preparePagePublication(input) {
3080
4094
  let content;
3081
4095
  try {
3082
- content = await readFile11(input.draftPath, "utf8");
4096
+ content = await readFile12(input.draftPath, "utf8");
3083
4097
  } catch (error) {
3084
4098
  return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
3085
4099
  }
@@ -3150,10 +4164,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
3150
4164
  ExitCode.VAULT_PATH_INVALID
3151
4165
  );
3152
4166
  }
3153
- const schemaPath = join16(vault, "SCHEMA.md");
4167
+ const schemaPath = join24(vault, "SCHEMA.md");
3154
4168
  let schemaText;
3155
4169
  try {
3156
- schemaText = await readFile11(schemaPath, "utf8");
4170
+ schemaText = await readFile12(schemaPath, "utf8");
3157
4171
  } catch (error) {
3158
4172
  return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
3159
4173
  }
@@ -3183,8 +4197,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
3183
4197
  let visibleSchema;
3184
4198
  try {
3185
4199
  [visible, visibleSchema] = await Promise.all([
3186
- readFile11(input.targetPath, "utf8"),
3187
- readFile11(schemaPath, "utf8")
4200
+ readFile12(input.targetPath, "utf8"),
4201
+ readFile12(schemaPath, "utf8")
3188
4202
  ]);
3189
4203
  } catch (error) {
3190
4204
  return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
@@ -3237,7 +4251,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
3237
4251
  })
3238
4252
  };
3239
4253
  }
3240
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
4254
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
3241
4255
  return {
3242
4256
  exitCode: ExitCode.OK,
3243
4257
  result: ok({
@@ -3251,6 +4265,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
3251
4265
  operation_id: input.operationId,
3252
4266
  dry_run: dryRun,
3253
4267
  files_changed: filesChanged,
4268
+ base_oid: receipt?.base_oid ?? null,
4269
+ write_mode: receipt?.mode ?? null,
4270
+ ...receipt?.host_id ? { host_id: receipt.host_id } : {},
3254
4271
  humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
3255
4272
  })
3256
4273
  };
@@ -3266,17 +4283,17 @@ function renderPublicationLog(input, added) {
3266
4283
  }
3267
4284
  async function readPageChanged(targetPath, content) {
3268
4285
  try {
3269
- return ok(await readFile11(targetPath, "utf8") !== content);
4286
+ return ok(await readFile12(targetPath, "utf8") !== content);
3270
4287
  } catch (error) {
3271
4288
  if (error.code === "ENOENT") return ok(true);
3272
4289
  return err("WRITE_FAILED", { path: targetPath, message: String(error) });
3273
4290
  }
3274
4291
  }
3275
4292
  async function previewPreparedPagePublication(input, vault) {
3276
- const schemaPath = join16(vault, "SCHEMA.md");
4293
+ const schemaPath = join24(vault, "SCHEMA.md");
3277
4294
  let schemaText;
3278
4295
  try {
3279
- schemaText = await readFile11(schemaPath, "utf8");
4296
+ schemaText = await readFile12(schemaPath, "utf8");
3280
4297
  } catch (error) {
3281
4298
  const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
3282
4299
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3288,10 +4305,10 @@ async function previewPreparedPagePublication(input, vault) {
3288
4305
  if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
3289
4306
  const pageChanged = await readPageChanged(input.targetPath, input.page.content);
3290
4307
  if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
3291
- const indexPath = join16(vault, "index.md");
4308
+ const indexPath = join24(vault, "index.md");
3292
4309
  let indexText;
3293
4310
  try {
3294
- indexText = await readFile11(indexPath, "utf8");
4311
+ indexText = await readFile12(indexPath, "utf8");
3295
4312
  } catch (error) {
3296
4313
  const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
3297
4314
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3302,10 +4319,10 @@ async function previewPreparedPagePublication(input, vault) {
3302
4319
  type: input.page.type
3303
4320
  });
3304
4321
  if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
3305
- const logPath = join16(vault, "log.md");
4322
+ const logPath = join24(vault, "log.md");
3306
4323
  let logText;
3307
4324
  try {
3308
- logText = await readFile11(logPath, "utf8");
4325
+ logText = await readFile12(logPath, "utf8");
3309
4326
  } catch (error) {
3310
4327
  const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
3311
4328
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3328,93 +4345,159 @@ async function previewPreparedPagePublication(input, vault) {
3328
4345
  );
3329
4346
  }
3330
4347
  async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
3331
- let lock;
4348
+ const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
4349
+ if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
3332
4350
  try {
3333
- lock = acquireOwnedSyncLock(vault, {
3334
- summary: `page publish ${input.page.target}`,
3335
- ttlMinutes: 1
4351
+ const preflight = await deps.preflight({
4352
+ vault,
4353
+ command: `page publish ${input.page.target}`,
4354
+ lockToken: managed.data.ownerToken
3336
4355
  });
3337
- } catch (error) {
3338
- return {
3339
- exitCode: ExitCode.WRITE_FAILED,
3340
- result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
3341
- };
3342
- }
3343
- if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
3344
- let primary;
3345
- let released;
3346
- try {
3347
- primary = await runLockedPrimaryStages(input, vault, deps);
3348
- } catch (error) {
3349
- primary = lockedFailure(
3350
- "schema",
3351
- emptyLockedState(),
3352
- err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
3353
- );
3354
- } finally {
3355
- released = releaseOwnedSyncLock(lock.data);
3356
- }
3357
- const primaryState = primary?.ok ? primary.data : primary?.state;
3358
- if (released === void 0 || !released.ok || !released.data.released) {
3359
- return phaseFailure(
3360
- "unlock",
3361
- input,
3362
- primaryState?.published ?? false,
3363
- released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
3364
- {
3365
- primary_stage: primary && !primary.ok ? primary.stage : "complete",
3366
- primary_error: primary && !primary.ok ? primary.cause.error : void 0
4356
+ if (!preflight.result.ok) {
4357
+ return { exitCode: preflight.exitCode, result: preflight.result };
4358
+ }
4359
+ const writeReceipt = preflight.result.data;
4360
+ const aggregateMode = resolveRootAggregateMode();
4361
+ if (writeReceipt.base_oid) {
4362
+ const head = git(vault, ["rev-parse", "HEAD"]);
4363
+ if (head !== writeReceipt.base_oid) {
4364
+ return {
4365
+ exitCode: ExitCode.PREFLIGHT_FAILED,
4366
+ result: err("PREFLIGHT_FAILED", {
4367
+ reason: "base-oid-drift",
4368
+ expected: writeReceipt.base_oid,
4369
+ actual: head
4370
+ })
4371
+ };
3367
4372
  }
3368
- );
3369
- }
3370
- const unlockHook = await observeStage(deps, "unlock");
3371
- if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
3372
- if (primary === void 0) {
3373
- return phaseFailure(
3374
- "schema",
4373
+ }
4374
+ let lock;
4375
+ try {
4376
+ lock = acquireOwnedSyncLock(vault, {
4377
+ summary: `page publish ${input.page.target}`,
4378
+ ttlMinutes: 1
4379
+ });
4380
+ } catch (error) {
4381
+ return {
4382
+ exitCode: ExitCode.WRITE_FAILED,
4383
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
4384
+ };
4385
+ }
4386
+ if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
4387
+ let primary;
4388
+ let released;
4389
+ try {
4390
+ primary = await runLockedPrimaryStages(input, vault, deps);
4391
+ } catch (error) {
4392
+ primary = lockedFailure(
4393
+ "schema",
4394
+ emptyLockedState(),
4395
+ err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
4396
+ );
4397
+ } finally {
4398
+ released = releaseOwnedSyncLock(lock.data);
4399
+ }
4400
+ const primaryState = primary?.ok ? primary.data : primary?.state;
4401
+ if (released === void 0 || !released.ok || !released.data.released) {
4402
+ return phaseFailure(
4403
+ "unlock",
4404
+ input,
4405
+ primaryState?.published ?? false,
4406
+ released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
4407
+ {
4408
+ primary_stage: primary && !primary.ok ? primary.stage : "complete",
4409
+ primary_error: primary && !primary.ok ? primary.cause.error : void 0
4410
+ }
4411
+ );
4412
+ }
4413
+ const unlockHook = await observeStage(deps, "unlock");
4414
+ if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
4415
+ if (primary === void 0) {
4416
+ return phaseFailure(
4417
+ "schema",
4418
+ input,
4419
+ false,
4420
+ err("WRITE_FAILED", { message: "locked publication produced no result" })
4421
+ );
4422
+ }
4423
+ if (!primary.ok) {
4424
+ return phaseFailure(
4425
+ primary.stage,
4426
+ input,
4427
+ primary.state.published,
4428
+ primary.cause,
4429
+ void 0,
4430
+ primary.exitCode
4431
+ );
4432
+ }
4433
+ const state = primary.data;
4434
+ const event = await writeLogEvent(vault, {
4435
+ schema: "skillwiki-log-event/v1",
4436
+ operation_id: input.operationId,
4437
+ occurred_at: `${input.date}T00:00:00.000Z`,
4438
+ host_id: writeReceipt.host_id ?? "standalone",
4439
+ actor: "skillwiki-cli",
4440
+ kind: "page-publish",
4441
+ target: input.page.target,
4442
+ note: input.logNote ?? `Published ${input.page.title}`,
4443
+ metadata: {
4444
+ page_type: input.page.type,
4445
+ // Stable for replay: do not embed schema-delta taxonomy_added (varies after first write).
4446
+ tags: [...input.page.tags].sort(),
4447
+ base_oid: writeReceipt.base_oid
4448
+ }
4449
+ });
4450
+ if (!event.ok) {
4451
+ return phaseFailure(
4452
+ "event",
4453
+ input,
4454
+ true,
4455
+ event,
4456
+ void 0,
4457
+ event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode(event.error)
4458
+ );
4459
+ }
4460
+ if (event.data.created) state.changed.add(event.data.path);
4461
+ const eventHook = await observeStage(deps, "event");
4462
+ if (eventHook) return phaseFailure("event", input, true, eventHook);
4463
+ let logAppended = false;
4464
+ if (aggregateMode === "dual" || writeReceipt.mode !== "immutable-record") {
4465
+ if (aggregateMode === "dual") {
4466
+ const log = await runLogAppend({
4467
+ vault,
4468
+ content: renderPublicationLog(input, state.taxonomyAdded),
4469
+ operationId: input.operationId,
4470
+ strictLock: true,
4471
+ recordLastOp: false
4472
+ });
4473
+ if (!log.result.ok) return phaseFailure("log", input, true, log.result);
4474
+ if (log.exitCode !== ExitCode.OK) {
4475
+ return phaseFailure(
4476
+ "log",
4477
+ input,
4478
+ true,
4479
+ err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
4480
+ );
4481
+ }
4482
+ logAppended = log.result.data.appended;
4483
+ if (logAppended) state.changed.add("log.md");
4484
+ const logHook = await observeStage(deps, "log");
4485
+ if (logHook) return phaseFailure("log", input, true, logHook);
4486
+ }
4487
+ }
4488
+ return successReceipt(
3375
4489
  input,
4490
+ state.taxonomyAdded,
4491
+ state.pageChanged,
4492
+ state.indexUpdated,
4493
+ logAppended,
4494
+ [...state.changed],
3376
4495
  false,
3377
- err("WRITE_FAILED", { message: "locked publication produced no result" })
3378
- );
3379
- }
3380
- if (!primary.ok) {
3381
- return phaseFailure(
3382
- primary.stage,
3383
- input,
3384
- primary.state.published,
3385
- primary.cause,
3386
- void 0,
3387
- primary.exitCode
3388
- );
3389
- }
3390
- const state = primary.data;
3391
- const log = await runLogAppend({
3392
- vault,
3393
- content: renderPublicationLog(input, state.taxonomyAdded),
3394
- operationId: input.operationId,
3395
- strictLock: true,
3396
- recordLastOp: false
3397
- });
3398
- if (!log.result.ok) return phaseFailure("log", input, true, log.result);
3399
- if (log.exitCode !== ExitCode.OK) {
3400
- return phaseFailure(
3401
- "log",
3402
- input,
3403
- true,
3404
- err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
4496
+ writeReceipt
3405
4497
  );
4498
+ } finally {
4499
+ releaseManagedWriteLock(managed.data);
3406
4500
  }
3407
- if (log.result.data.appended) state.changed.add("log.md");
3408
- const logHook = await observeStage(deps, "log");
3409
- if (logHook) return phaseFailure("log", input, true, logHook);
3410
- return successReceipt(
3411
- input,
3412
- state.taxonomyAdded,
3413
- state.pageChanged,
3414
- state.indexUpdated,
3415
- log.result.data.appended,
3416
- [...state.changed]
3417
- );
3418
4501
  }
3419
4502
  async function runPagePublish(input, deps = DEFAULT_DEPS) {
3420
4503
  const prepared = await preparePagePublication(input);
@@ -3501,7 +4584,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
3501
4584
  }
3502
4585
  async function resolveRawCapture(input) {
3503
4586
  try {
3504
- const existing = await readFile12(input.path, "utf8");
4587
+ const existing = await readFile13(input.path, "utf8");
3505
4588
  const frontmatter = extractFrontmatter(existing);
3506
4589
  if (!frontmatter.ok) {
3507
4590
  return err("INGEST_VALIDATION_FAILED", {
@@ -3559,7 +4642,7 @@ async function acquireRawCaptureLock(path) {
3559
4642
  const lockPath = `${path}.ingest.lock`;
3560
4643
  for (let attempt = 0; attempt < 200; attempt++) {
3561
4644
  try {
3562
- const handle = await open(lockPath, "wx");
4645
+ const handle = await open2(lockPath, "wx");
3563
4646
  await handle.close();
3564
4647
  return ok(lockPath);
3565
4648
  } catch (error) {
@@ -3640,7 +4723,7 @@ async function runIngest(input) {
3640
4723
  sourceContent = fetchResult.data.body;
3641
4724
  } else {
3642
4725
  try {
3643
- sourceContent = await readFile12(input.source, "utf8");
4726
+ sourceContent = await readFile13(input.source, "utf8");
3644
4727
  } catch {
3645
4728
  return {
3646
4729
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -3665,7 +4748,7 @@ async function runIngest(input) {
3665
4748
  const rawRelPath = `raw/articles/${slug}.md`;
3666
4749
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
3667
4750
  const typedRelPath = `${typedDir}/${slug}.md`;
3668
- const rawAbsPath = join17(input.vault, rawRelPath);
4751
+ const rawAbsPath = join25(input.vault, rawRelPath);
3669
4752
  const identity = assessSourceIdentity({
3670
4753
  rawPath: rawRelPath,
3671
4754
  sourceUrl: sourceUrl ?? void 0,
@@ -3709,11 +4792,11 @@ async function runIngest(input) {
3709
4792
  );
3710
4793
  if (!input.dryRun) {
3711
4794
  try {
3712
- await mkdir9(join17(input.vault, typedDir), { recursive: true });
4795
+ await mkdir10(join25(input.vault, typedDir), { recursive: true });
3713
4796
  } catch (error) {
3714
4797
  return {
3715
4798
  exitCode: ExitCode.WRITE_FAILED,
3716
- result: err("WRITE_FAILED", { path: join17(input.vault, typedDir), message: String(error) })
4799
+ result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
3717
4800
  };
3718
4801
  }
3719
4802
  }
@@ -3760,11 +4843,11 @@ async function runIngest(input) {
3760
4843
  };
3761
4844
  }
3762
4845
  try {
3763
- await mkdir9(join17(input.vault, "raw", "articles"), { recursive: true });
4846
+ await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
3764
4847
  } catch (error) {
3765
4848
  return {
3766
4849
  exitCode: ExitCode.WRITE_FAILED,
3767
- result: err("WRITE_FAILED", { path: join17(input.vault, "raw", "articles"), message: String(error) })
4850
+ result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
3768
4851
  };
3769
4852
  }
3770
4853
  const rawWrite = await writeResolvedRaw({
@@ -3992,8 +5075,8 @@ ${body}`;
3992
5075
  }
3993
5076
 
3994
5077
  // src/commands/tag-reconcile.ts
3995
- import { readFile as readFile13 } from "fs/promises";
3996
- import { join as join18, posix } from "path";
5078
+ import { readFile as readFile14 } from "fs/promises";
5079
+ import { join as join26, posix } from "path";
3997
5080
  var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
3998
5081
  function errorExitCode2(error) {
3999
5082
  switch (error) {
@@ -4036,7 +5119,7 @@ function asTagArray(frontmatter, path) {
4036
5119
  async function readTagsFromFile(path) {
4037
5120
  let text;
4038
5121
  try {
4039
- text = await readFile13(path, "utf8");
5122
+ text = await readFile14(path, "utf8");
4040
5123
  } catch (error) {
4041
5124
  if (error.code === "ENOENT") {
4042
5125
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
@@ -4057,7 +5140,7 @@ async function resolveRequestedTags(input, page) {
4057
5140
  result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
4058
5141
  };
4059
5142
  }
4060
- const source = input.from ?? (explicit.length === 0 ? join18(input.vault, page) : void 0);
5143
+ const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
4061
5144
  if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
4062
5145
  const sourced = await readTagsFromFile(source);
4063
5146
  if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
@@ -4068,7 +5151,7 @@ async function resolveRequestedTags(input, page) {
4068
5151
  }
4069
5152
  async function readSchema(schemaPath) {
4070
5153
  try {
4071
- return { exitCode: ExitCode.OK, result: ok(await readFile13(schemaPath, "utf8")) };
5154
+ return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
4072
5155
  } catch (error) {
4073
5156
  if (error.code === "ENOENT") {
4074
5157
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
@@ -4096,7 +5179,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
4096
5179
  };
4097
5180
  }
4098
5181
  async function reconcileTagsWhileLocked(input, page, tags, comment) {
4099
- const schemaPath = join18(input.vault, "SCHEMA.md");
5182
+ const schemaPath = join26(input.vault, "SCHEMA.md");
4100
5183
  const current = await readSchema(schemaPath);
4101
5184
  if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
4102
5185
  const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
@@ -4107,7 +5190,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
4107
5190
  }
4108
5191
  let verifiedText;
4109
5192
  try {
4110
- verifiedText = await readFile13(schemaPath, "utf8");
5193
+ verifiedText = await readFile14(schemaPath, "utf8");
4111
5194
  } catch (error) {
4112
5195
  return {
4113
5196
  exitCode: ExitCode.WRITE_FAILED,
@@ -4133,7 +5216,7 @@ async function runTagReconcile(input) {
4133
5216
  const comment = taxonomyCommentForPage(page.data, date, input.reason);
4134
5217
  if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
4135
5218
  if (!input.write) {
4136
- const schema = await readSchema(join18(input.vault, "SCHEMA.md"));
5219
+ const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
4137
5220
  if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
4138
5221
  const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
4139
5222
  return previewResult(page.data, tags, preview, true, []);
@@ -4185,22 +5268,9 @@ async function runTagReconcile(input) {
4185
5268
  }
4186
5269
 
4187
5270
  // src/commands/sync.ts
4188
- import { existsSync as existsSync5 } from "fs";
4189
- import { join as join19 } from "path";
4190
- import { execFileSync as execFileSync2 } from "child_process";
4191
-
4192
- // src/utils/git.ts
4193
- import { execFileSync } from "child_process";
4194
- function git(cwd, args) {
4195
- try {
4196
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4197
- } catch {
4198
- return "";
4199
- }
4200
- }
4201
- function gitStrict(cwd, args) {
4202
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4203
- }
5271
+ import { existsSync as existsSync6 } from "fs";
5272
+ import { join as join27 } from "path";
5273
+ import { execFileSync as execFileSync3 } from "child_process";
4204
5274
 
4205
5275
  // src/utils/vault-git-pathspec.ts
4206
5276
  var VAULT_GENERATED_COMMIT_PATHS = [
@@ -4241,7 +5311,7 @@ function isTrackedNotePath(path) {
4241
5311
  }
4242
5312
  function refHasPath(vault, ref, path) {
4243
5313
  try {
4244
- execFileSync2("git", ["cat-file", "-e", `${ref}:${path}`], {
5314
+ execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
4245
5315
  cwd: vault,
4246
5316
  stdio: ["pipe", "pipe", "pipe"]
4247
5317
  });
@@ -4253,7 +5323,7 @@ function refHasPath(vault, ref, path) {
4253
5323
  function runSyncStatus(input) {
4254
5324
  const vault = input.vault;
4255
5325
  const includeStashes = input.includeStashes ?? false;
4256
- if (!existsSync5(join19(vault, ".git"))) {
5326
+ if (!existsSync6(join27(vault, ".git"))) {
4257
5327
  return {
4258
5328
  exitCode: ExitCode.VAULT_PATH_INVALID,
4259
5329
  result: ok({
@@ -4360,7 +5430,7 @@ function runSyncStatus(input) {
4360
5430
  }
4361
5431
  async function runSyncPush(input) {
4362
5432
  const vault = input.vault;
4363
- if (!existsSync5(join19(vault, ".git"))) {
5433
+ if (!existsSync6(join27(vault, ".git"))) {
4364
5434
  return {
4365
5435
  exitCode: ExitCode.VAULT_PATH_INVALID,
4366
5436
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -4520,100 +5590,44 @@ function enableGitLongPathsOnWindows(vault) {
4520
5590
  }
4521
5591
  async function runSyncPull(input) {
4522
5592
  const vault = input.vault;
4523
- if (!existsSync5(join19(vault, ".git"))) {
5593
+ if (!existsSync6(join27(vault, ".git"))) {
4524
5594
  return {
4525
5595
  exitCode: ExitCode.VAULT_PATH_INVALID,
4526
5596
  result: err("NOT_A_GIT_REPO", { path: vault })
4527
5597
  };
4528
5598
  }
4529
5599
  enableGitLongPathsOnWindows(vault);
4530
- let fetched = false;
4531
- try {
4532
- gitStrict(vault, ["fetch", "origin"]);
4533
- fetched = true;
4534
- } catch (e) {
5600
+ const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
5601
+ if (!remoteUrl) {
4535
5602
  return {
4536
5603
  exitCode: ExitCode.SYNC_PULL_FAILED,
4537
- result: err("GIT_FETCH_FAILED", { message: String(e) })
5604
+ result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
4538
5605
  };
4539
5606
  }
4540
- let pulled = false;
4541
- let conflicts = 0;
4542
- let filesUpdated = 0;
4543
- let autoResolved = 0;
4544
- try {
4545
- const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
4546
- pulled = true;
4547
- const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
4548
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4549
- } catch (e) {
4550
- const errString = String(e);
4551
- if (errString.includes("conflict")) {
4552
- let inConflict = true;
4553
- while (inConflict) {
4554
- const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
4555
- let commitMsg = "";
4556
- if (stoppedSha) {
4557
- commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
4558
- }
4559
- const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
4560
- const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
4561
- const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
4562
- if (conflictedList.length === 0) {
4563
- try {
4564
- gitStrict(vault, ["rebase", "--continue"]);
4565
- inConflict = true;
4566
- } catch {
4567
- inConflict = false;
4568
- }
4569
- continue;
4570
- }
4571
- if (isArchiveOrSnapshot) {
4572
- for (const f of conflictedList) {
4573
- try {
4574
- gitStrict(vault, ["checkout", "--ours", f]);
4575
- gitStrict(vault, ["add", f]);
4576
- } catch {
4577
- }
4578
- }
4579
- autoResolved += conflictedList.length;
4580
- try {
4581
- gitStrict(vault, ["rebase", "--continue"]);
4582
- } catch (continueErr) {
4583
- continue;
4584
- }
4585
- } else {
4586
- conflicts = conflictedList.length;
4587
- return {
4588
- exitCode: ExitCode.SYNC_PULL_FAILED,
4589
- result: ok({
4590
- fetched,
4591
- pulled: false,
4592
- files_updated: 0,
4593
- conflicts,
4594
- auto_resolved: 0,
4595
- lint_errors: 0,
4596
- lint_warnings: 0,
4597
- humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
4598
- })
4599
- };
4600
- }
4601
- }
4602
- if (autoResolved > 0) {
4603
- const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
4604
- if (diffOutput) {
4605
- const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
4606
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4607
- }
4608
- pulled = true;
4609
- conflicts = 0;
4610
- }
4611
- } else {
5607
+ const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
5608
+ if (!helper.ok) {
5609
+ if (helper.error === "PREFLIGHT_FAILED") {
4612
5610
  return {
4613
- exitCode: ExitCode.SYNC_PULL_FAILED,
4614
- result: err("GIT_PULL_FAILED", { message: errString })
5611
+ exitCode: ExitCode.PREFLIGHT_FAILED,
5612
+ result: err("PREFLIGHT_FAILED", helper.detail)
4615
5613
  };
4616
5614
  }
5615
+ return {
5616
+ exitCode: ExitCode.SYNC_PULL_FAILED,
5617
+ result: err("GIT_PULL_FAILED", helper.detail)
5618
+ };
5619
+ }
5620
+ const fetched = true;
5621
+ const pulled = helper.data.changed;
5622
+ const conflicts = 0;
5623
+ const autoResolved = 0;
5624
+ let filesUpdated = 0;
5625
+ if (helper.data.changed) {
5626
+ const diffOutput = git(vault, ["diff", "--stat", `${helper.data.before_oid}..${helper.data.after_oid}`]);
5627
+ if (diffOutput) {
5628
+ const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
5629
+ if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
5630
+ }
4617
5631
  }
4618
5632
  const pathFix = await fixPathTooLong({ vault });
4619
5633
  const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
@@ -4626,8 +5640,7 @@ async function runSyncPull(input) {
4626
5640
  }
4627
5641
  const hintParts = [];
4628
5642
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
4629
- else hintParts.push("already up to date");
4630
- if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
5643
+ else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
4631
5644
  if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
4632
5645
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
4633
5646
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
@@ -4692,7 +5705,7 @@ function runSyncPeers(input) {
4692
5705
  }
4693
5706
  function runSyncLock(input) {
4694
5707
  const vault = input.vault;
4695
- if (!existsSync5(vault)) {
5708
+ if (!existsSync6(vault)) {
4696
5709
  return {
4697
5710
  exitCode: ExitCode.VAULT_PATH_INVALID,
4698
5711
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4727,7 +5740,7 @@ function runSyncLock(input) {
4727
5740
  }
4728
5741
  function runSyncUnlock(input) {
4729
5742
  const vault = input.vault;
4730
- if (!existsSync5(vault)) {
5743
+ if (!existsSync6(vault)) {
4731
5744
  return {
4732
5745
  exitCode: ExitCode.VAULT_PATH_INVALID,
4733
5746
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4760,8 +5773,8 @@ function runSyncUnlock(input) {
4760
5773
  }
4761
5774
 
4762
5775
  // src/commands/backup.ts
4763
- import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4764
- import { join as join20, relative as relative2, dirname as dirname6 } from "path";
5776
+ import { statSync as statSync2, readdirSync, readFileSync as readFileSync11, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
5777
+ import { join as join28, relative as relative2, dirname as dirname6 } from "path";
4765
5778
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
4766
5779
 
4767
5780
  // src/utils/s3-client.ts
@@ -4785,7 +5798,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
4785
5798
  function* walkMarkdown(dir, base) {
4786
5799
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
4787
5800
  if (SKIP_DIRS.has(entry.name)) continue;
4788
- const full = join20(dir, entry.name);
5801
+ const full = join28(dir, entry.name);
4789
5802
  if (entry.isDirectory()) {
4790
5803
  yield* walkMarkdown(full, base);
4791
5804
  } else if (entry.name.endsWith(".md")) {
@@ -4808,7 +5821,7 @@ async function runBackupSync(input) {
4808
5821
  let failed = 0;
4809
5822
  const files = [...walkMarkdown(input.vault, input.vault)];
4810
5823
  for (const relPath of files) {
4811
- const absPath = join20(input.vault, relPath);
5824
+ const absPath = join28(input.vault, relPath);
4812
5825
  const localStat = statSync2(absPath);
4813
5826
  let needsUpload = true;
4814
5827
  try {
@@ -4827,7 +5840,7 @@ async function runBackupSync(input) {
4827
5840
  continue;
4828
5841
  }
4829
5842
  try {
4830
- const body = readFileSync4(absPath);
5843
+ const body = readFileSync11(absPath);
4831
5844
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
4832
5845
  uploaded++;
4833
5846
  } catch {
@@ -4884,7 +5897,7 @@ async function runBackupRestore(input) {
4884
5897
  const objects = list.Contents ?? [];
4885
5898
  for (const obj of objects) {
4886
5899
  if (!obj.Key) continue;
4887
- const localPath = join20(target, obj.Key);
5900
+ const localPath = join28(target, obj.Key);
4888
5901
  try {
4889
5902
  const localStat = statSync2(localPath);
4890
5903
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -4898,7 +5911,7 @@ async function runBackupRestore(input) {
4898
5911
  const body = await resp.Body?.transformToByteArray();
4899
5912
  if (body) {
4900
5913
  mkdirSync2(dirname6(localPath), { recursive: true });
4901
- writeFileSync2(localPath, Buffer.from(body));
5914
+ writeFileSync4(localPath, Buffer.from(body));
4902
5915
  downloaded++;
4903
5916
  }
4904
5917
  } catch {
@@ -4930,11 +5943,11 @@ async function runBackupRestore(input) {
4930
5943
  }
4931
5944
 
4932
5945
  // src/commands/status.ts
4933
- import { existsSync as existsSync6, statSync as statSync3 } from "fs";
4934
- import { readFile as readFile14 } from "fs/promises";
4935
- import { join as join21 } from "path";
5946
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
5947
+ import { readFile as readFile15 } from "fs/promises";
5948
+ import { join as join29 } from "path";
4936
5949
  async function runStatus(input) {
4937
- if (!existsSync6(input.vault)) {
5950
+ if (!existsSync7(input.vault)) {
4938
5951
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
4939
5952
  }
4940
5953
  const scan = await scanVault(input.vault);
@@ -4959,7 +5972,7 @@ async function runStatus(input) {
4959
5972
  const compound = scan.data.compound.length;
4960
5973
  let schemaVersion = "v1";
4961
5974
  try {
4962
- const schemaContent = await readFile14(join21(input.vault, "SCHEMA.md"), "utf8");
5975
+ const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
4963
5976
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
4964
5977
  if (versionMatch) schemaVersion = versionMatch[1];
4965
5978
  } catch {
@@ -5019,8 +6032,8 @@ async function runStatus(input) {
5019
6032
  }
5020
6033
 
5021
6034
  // src/commands/seed.ts
5022
- import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
5023
- import { join as join22 } from "path";
6035
+ import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
6036
+ import { join as join30 } from "path";
5024
6037
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5025
6038
  var EXAMPLE_PAGES = {
5026
6039
  "entities/example-project.md": `---
@@ -5089,29 +6102,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
5089
6102
  `;
5090
6103
  async function runSeed(input) {
5091
6104
  try {
5092
- await stat4(join22(input.vault, "SCHEMA.md"));
6105
+ await stat4(join30(input.vault, "SCHEMA.md"));
5093
6106
  } catch {
5094
6107
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
5095
6108
  }
5096
6109
  const created = [];
5097
6110
  const skipped = [];
5098
6111
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
5099
- const absPath = join22(input.vault, relPath);
6112
+ const absPath = join30(input.vault, relPath);
5100
6113
  try {
5101
6114
  await stat4(absPath);
5102
6115
  skipped.push(relPath);
5103
6116
  } catch {
5104
- await mkdir10(join22(absPath, ".."), { recursive: true });
6117
+ await mkdir11(join30(absPath, ".."), { recursive: true });
5105
6118
  await writeFile9(absPath, content, "utf8");
5106
6119
  created.push(relPath);
5107
6120
  }
5108
6121
  }
5109
- const rawPath = join22(input.vault, "raw", "articles", "example-source.md");
6122
+ const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
5110
6123
  try {
5111
6124
  await stat4(rawPath);
5112
6125
  skipped.push("raw/articles/example-source.md");
5113
6126
  } catch {
5114
- await mkdir10(join22(rawPath, ".."), { recursive: true });
6127
+ await mkdir11(join30(rawPath, ".."), { recursive: true });
5115
6128
  await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
5116
6129
  created.push("raw/articles/example-source.md");
5117
6130
  }
@@ -5134,9 +6147,9 @@ async function runSeed(input) {
5134
6147
  }
5135
6148
 
5136
6149
  // src/commands/canvas.ts
5137
- import { readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
5138
- import { existsSync as existsSync7 } from "fs";
5139
- import { join as join23 } from "path";
6150
+ import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
6151
+ import { existsSync as existsSync8 } from "fs";
6152
+ import { join as join31 } from "path";
5140
6153
  var NODE_WIDTH = 240;
5141
6154
  var NODE_HEIGHT = 60;
5142
6155
  var COLUMN_SPACING = 400;
@@ -5214,8 +6227,8 @@ function buildCanvasEdges(adjacency) {
5214
6227
  return edges;
5215
6228
  }
5216
6229
  async function runCanvasGenerate(input) {
5217
- const graphPath = input.graphPath ?? join23(input.vault, ".skillwiki", "graph.json");
5218
- if (!existsSync7(graphPath)) {
6230
+ const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
6231
+ if (!existsSync8(graphPath)) {
5219
6232
  return {
5220
6233
  exitCode: ExitCode.FILE_NOT_FOUND,
5221
6234
  result: err("FILE_NOT_FOUND", {
@@ -5226,7 +6239,7 @@ async function runCanvasGenerate(input) {
5226
6239
  }
5227
6240
  let raw;
5228
6241
  try {
5229
- raw = await readFile15(graphPath, "utf8");
6242
+ raw = await readFile16(graphPath, "utf8");
5230
6243
  } catch (e) {
5231
6244
  return {
5232
6245
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5252,7 +6265,7 @@ async function runCanvasGenerate(input) {
5252
6265
  const nodes = buildCanvasNodes(paths);
5253
6266
  const edges = buildCanvasEdges(graph.adjacency);
5254
6267
  const canvas = { nodes, edges };
5255
- const outPath = join23(input.vault, "vault-graph.canvas");
6268
+ const outPath = join31(input.vault, "vault-graph.canvas");
5256
6269
  try {
5257
6270
  await writeFile10(outPath, JSON.stringify(canvas, null, 2));
5258
6271
  } catch (e) {
@@ -5274,15 +6287,15 @@ written: ${outPath}`
5274
6287
  }
5275
6288
 
5276
6289
  // src/commands/fleet-health.ts
5277
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
6290
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
5278
6291
  import { execSync as nodeExecSync } from "child_process";
5279
6292
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
5280
- import { join as join24 } from "path";
6293
+ import { join as join32 } from "path";
5281
6294
  var SSH_TIMEOUT_MS = 15e3;
5282
6295
  var TIMER_UNIT = "agent-memory-trends.timer";
5283
6296
  var SERVICE_UNIT = "agent-memory-trends.service";
5284
6297
  var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
5285
- function defaultDeps() {
6298
+ function defaultDeps2() {
5286
6299
  return {
5287
6300
  platform: () => nodePlatform(),
5288
6301
  execSync: nodeExecSync
@@ -5374,9 +6387,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
5374
6387
  function probeLocal(vaultPath, deps) {
5375
6388
  const latestPath = satelliteLatestRunPath(vaultPath);
5376
6389
  let parsed = null;
5377
- if (existsSync8(latestPath)) {
6390
+ if (existsSync9(latestPath)) {
5378
6391
  try {
5379
- const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
6392
+ const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
5380
6393
  if (wire) {
5381
6394
  parsed = {
5382
6395
  status: wire.status,
@@ -5502,12 +6515,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
5502
6515
  return true;
5503
6516
  }
5504
6517
  async function runFleetHealth(input) {
5505
- const deps = input.deps ?? defaultDeps();
6518
+ const deps = input.deps ?? defaultDeps2();
5506
6519
  const env = input.env ?? process.env;
5507
6520
  const home = input.home ?? env.HOME ?? "";
5508
6521
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
5509
6522
  const vault = input.vault ?? env.WIKI_PATH;
5510
- const file = input.file ?? (vault ? join24(vault, FLEET_REL_PATH) : void 0);
6523
+ const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
5511
6524
  if (!file) {
5512
6525
  return {
5513
6526
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -5591,15 +6604,15 @@ async function runFleetHealth(input) {
5591
6604
  }
5592
6605
 
5593
6606
  // src/utils/auto-commit.ts
5594
- import { existsSync as existsSync9 } from "fs";
5595
- import { join as join25 } from "path";
6607
+ import { existsSync as existsSync10 } from "fs";
6608
+ import { join as join33 } from "path";
5596
6609
  async function postCommit(vault, exitCode) {
5597
6610
  if (exitCode !== 0) return;
5598
6611
  const home = process.env.HOME ?? "";
5599
6612
  const dotenv = await parseDotenvFile(configPath(home));
5600
6613
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
5601
6614
  if (autoCommit === "false") return;
5602
- if (!existsSync9(join25(vault, ".git"))) return;
6615
+ if (!existsSync10(join33(vault, ".git"))) return;
5603
6616
  const lastOps = readLastOp(vault);
5604
6617
  if (lastOps.length === 0) return;
5605
6618
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -5622,103 +6635,6 @@ async function postCommit(vault, exitCode) {
5622
6635
  clearLastOp(vault);
5623
6636
  }
5624
6637
 
5625
- // src/utils/protected-vault-write-guard.ts
5626
- import { readFileSync as readFileSync6 } from "fs";
5627
- import { join as join26, resolve as resolvePath } from "path";
5628
- async function guardProtectedVaultWrite(input) {
5629
- const env = input.env ?? process.env;
5630
- const home = input.home ?? process.env.HOME ?? "";
5631
- const cwd = input.cwd ?? process.cwd();
5632
- const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
5633
- const load = await loadFleetManifestAndHost({
5634
- vault: liveVaultPath ?? input.vault,
5635
- hostId: input.hostId,
5636
- env,
5637
- home,
5638
- cwd,
5639
- osHostname: input.osHostname ?? process.env.HOSTNAME,
5640
- user: input.user ?? process.env.USER
5641
- });
5642
- if (!load?.hostId || load.identityStatus !== "known") {
5643
- return { blocked: false };
5644
- }
5645
- const host = load.manifest.hosts[load.hostId];
5646
- if (!host || host.role !== "snapshotter" || host.protected !== true) {
5647
- return { blocked: false };
5648
- }
5649
- const snapshotWorktree = resolveSnapshotWorktree(home);
5650
- const targetVault = resolvePath(input.vault);
5651
- const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
5652
- const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
5653
- if (canonicalLiveVault && targetVault === canonicalLiveVault) {
5654
- return { blocked: false };
5655
- }
5656
- if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
5657
- return {
5658
- blocked: true,
5659
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5660
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5661
- host_id: load.hostId,
5662
- command: input.command,
5663
- reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
5664
- guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
5665
- })
5666
- };
5667
- }
5668
- if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
5669
- return {
5670
- blocked: true,
5671
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5672
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5673
- host_id: load.hostId,
5674
- command: input.command,
5675
- reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
5676
- guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
5677
- })
5678
- };
5679
- }
5680
- return { blocked: false };
5681
- }
5682
- async function resolveLiveVaultPath(input) {
5683
- const resolved = await resolveRuntimePath({
5684
- flag: void 0,
5685
- envValue: input.env.WIKI_PATH,
5686
- wikiEnv: input.env.WIKI,
5687
- home: input.home,
5688
- cwd: input.cwd
5689
- });
5690
- return resolved.ok ? resolved.data.path : void 0;
5691
- }
5692
- function resolveSnapshotWorktree(home) {
5693
- const skillwikiEnv = join26(home, ".skillwiki", ".env");
5694
- const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
5695
- if (explicitWorktree) return explicitWorktree;
5696
- const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
5697
- if (snapshotProfile) {
5698
- const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
5699
- if (fromProfile) return fromProfile;
5700
- }
5701
- return "/root/wiki-git";
5702
- }
5703
- function readEnvKey(path, keys) {
5704
- try {
5705
- const content = readFileSync6(path, "utf8");
5706
- for (const line of content.split(/\r?\n/)) {
5707
- const trimmed = line.trim();
5708
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
5709
- const eq = trimmed.indexOf("=");
5710
- if (eq <= 0) continue;
5711
- const key = trimmed.slice(0, eq).trim();
5712
- if (!keys.includes(key)) continue;
5713
- const value = trimmed.slice(eq + 1).trim();
5714
- if (value.length > 0) return value;
5715
- }
5716
- } catch {
5717
- return void 0;
5718
- }
5719
- return void 0;
5720
- }
5721
-
5722
6638
  // src/cli.ts
5723
6639
  var pkg = readCliPackageJson();
5724
6640
  var program = new Command();
@@ -5745,6 +6661,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
5745
6661
  }
5746
6662
  return emit(await run(), vault, opts);
5747
6663
  }
6664
+ async function emitManagedVaultWrite(vault, command, mutate, opts) {
6665
+ const guard = await guardProtectedVaultWrite({
6666
+ vault,
6667
+ command,
6668
+ env: process.env,
6669
+ home: process.env.HOME ?? "",
6670
+ cwd: process.cwd(),
6671
+ osHostname: process.env.HOSTNAME,
6672
+ user: process.env.USER
6673
+ });
6674
+ if (guard.blocked) {
6675
+ return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
6676
+ }
6677
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-SILEUQEV.js");
6678
+ const run = await runManagedWriteTransaction2({
6679
+ vault,
6680
+ command,
6681
+ allowImmutableRecord: opts?.allowImmutableRecord === true,
6682
+ mutate: async (receipt) => await mutate(receipt)
6683
+ });
6684
+ return emit(run, vault, opts);
6685
+ }
5748
6686
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
5749
6687
  program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
5750
6688
  program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
@@ -5755,12 +6693,12 @@ program.command("validate <file>").description("validate vault page frontmatter
5755
6693
  else vault = v.vault;
5756
6694
  }
5757
6695
  if (opts.apply && vault) {
5758
- return emitGuardedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
6696
+ return emitManagedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
5759
6697
  }
5760
6698
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
5761
6699
  });
5762
6700
  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) => {
5763
- const out = opts.out ?? join27(vault, ".skillwiki", "graph.json");
6701
+ const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
5764
6702
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
5765
6703
  });
5766
6704
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -5893,6 +6831,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
5893
6831
  { postCommit: false }
5894
6832
  );
5895
6833
  });
6834
+ var indexCmd = program.command("index").description("root index projection and checks");
6835
+ indexCmd.command("rebuild [vault]").description("preview or write deterministic root index.md projection").option("--write", "write projected index.md", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6836
+ const v = await resolveVaultArg(vault, opts.wiki);
6837
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6838
+ else emit(await runIndexRebuild({ vault: v.vault, write: Boolean(opts.write) }), v.vault);
6839
+ });
5896
6840
  program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5897
6841
  const v = await resolveVaultArg(vault, opts.wiki);
5898
6842
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -5945,12 +6889,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
5945
6889
  program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5946
6890
  const v = await resolveVaultArg(vault, opts.wiki);
5947
6891
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5948
- else return emitGuardedVaultWrite(
6892
+ else return emitManagedVaultWrite(
5949
6893
  v.vault,
5950
6894
  "log-append",
5951
6895
  () => runLogAppend({ vault: v.vault, content: opts.content })
5952
6896
  );
5953
6897
  });
6898
+ var logCmd = program.command("log").description("immutable log events and projections");
6899
+ logCmd.command("materialize [vault]").description("preview or write root log.md from immutable events").option("--write", "write projected log.md", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6900
+ const v = await resolveVaultArg(vault, opts.wiki);
6901
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6902
+ else if (opts.write) {
6903
+ return emitManagedVaultWrite(
6904
+ v.vault,
6905
+ "log materialize",
6906
+ () => runLogMaterialize({ vault: v.vault, write: true })
6907
+ );
6908
+ } else emit(await runLogMaterialize({ vault: v.vault, write: false }), v.vault);
6909
+ });
6910
+ logCmd.command("migrate-legacy [vault]").description("backfill immutable events from historical root log.md blocks").option("--write", "write events", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6911
+ const v = await resolveVaultArg(vault, opts.wiki);
6912
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6913
+ else if (opts.write) {
6914
+ return emitManagedVaultWrite(
6915
+ v.vault,
6916
+ "log migrate-legacy",
6917
+ () => runLogMigrateLegacy({ vault: v.vault, write: true })
6918
+ );
6919
+ } else emit(await runLogMigrateLegacy({ vault: v.vault, write: false }), v.vault);
6920
+ });
6921
+ var projectionsCmd = program.command("projections").description("root index/log projection materialization");
6922
+ projectionsCmd.command("materialize [vault]").description("preview or write root index.md and log.md as a pair").option("--write", "write projections", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6923
+ const v = await resolveVaultArg(vault, opts.wiki);
6924
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6925
+ else if (opts.write) {
6926
+ return emitManagedVaultWrite(
6927
+ v.vault,
6928
+ "projections materialize",
6929
+ () => runProjectionsMaterialize({ vault: v.vault, write: true })
6930
+ );
6931
+ } else emit(await runProjectionsMaterialize({ vault: v.vault, write: false }), v.vault);
6932
+ });
5954
6933
  program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5955
6934
  const v = await resolveVaultArg(vault, opts.wiki);
5956
6935
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6045,7 +7024,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
6045
7024
  remoteDelete: !!opts.remoteDelete,
6046
7025
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
6047
7026
  }), v.vault);
6048
- else return emitGuardedVaultWrite(
7027
+ else return emitManagedVaultWrite(
6049
7028
  v.vault,
6050
7029
  "archive",
6051
7030
  () => runArchive({
@@ -6062,7 +7041,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
6062
7041
  program.command("remove <page> [vault]").description("remove a vault path and write a delete-intent tombstone").option("--wiki <name>", "wiki profile name").option("--remote <remote>", "rclone remote root to prune the live path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the live path from the remote after local remove", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").option("--reason <text>", "stored on the delete-intent tombstone").action(async (page, vault, opts) => {
6063
7042
  const v = await resolveVaultArg(vault, opts.wiki);
6064
7043
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6065
- else return emitGuardedVaultWrite(
7044
+ else return emitManagedVaultWrite(
6066
7045
  v.vault,
6067
7046
  "remove",
6068
7047
  () => runRemove({
@@ -6235,6 +7214,17 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
6235
7214
  () => runSyncPull({ vault: v.vault })
6236
7215
  );
6237
7216
  });
7217
+ syncCmd.command("resolve-derived [vault]").description("resolve mixed derived conflicts for an owned vault-sync operation (internal)").requiredOption("--operation-id <id>", "vault-sync operation journal id").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7218
+ const v = await resolveVaultArg(vault, opts.wiki);
7219
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7220
+ else {
7221
+ return emitGuardedVaultWrite(
7222
+ v.vault,
7223
+ "sync resolve-derived",
7224
+ () => runDerivedConflictResolution({ vault: v.vault, operationId: opts.operationId })
7225
+ );
7226
+ }
7227
+ });
6238
7228
  syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6239
7229
  const v = await resolveVaultArg(vault, opts.wiki);
6240
7230
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6334,7 +7324,7 @@ program.command("observe [vault]").description("create a raw transcript observat
6334
7324
  program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6335
7325
  const v = await resolveVaultArg(vault, opts.wiki);
6336
7326
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6337
- else if (opts.write) return emitGuardedVaultWrite(
7327
+ else if (opts.write) return emitManagedVaultWrite(
6338
7328
  v.vault,
6339
7329
  "session-brief --write",
6340
7330
  () => runSessionBrief({