skillwiki 0.9.62 → 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 (40) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-2PENIQ3A.js → chunk-DR7KFHNH.js} +793 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1820 -665
  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 +2 -2
  28. package/skills/.codex-plugin/plugin.json +3 -3
  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 +29 -2
  34. package/skills/skills/wiki-archive/SKILL.md +18 -4
  35. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  36. package/skills/skills/wiki-remove/SKILL.md +96 -0
  37. package/skills/using-skillwiki/SKILL.md +29 -2
  38. package/skills/wiki-archive/SKILL.md +18 -4
  39. package/skills/wiki-crystallize/SKILL.md +3 -0
  40. package/skills/wiki-remove/SKILL.md +96 -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-2PENIQ3A.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 join25 } from "path";
124
+ import { join as join34 } from "path";
106
125
  import { Command } from "commander";
107
126
 
108
127
  // src/utils/output.ts
@@ -625,155 +644,1154 @@ async function runInit(input) {
625
644
  result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
626
645
  };
627
646
  }
628
- const created = [];
647
+ const created = [];
648
+ try {
649
+ await mkdir3(target, { recursive: true });
650
+ for (const d of VAULT_DIRS) {
651
+ await mkdir3(join5(target, d), { recursive: true });
652
+ created.push(d + "/");
653
+ }
654
+ } catch (e) {
655
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
656
+ }
657
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
658
+ let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
659
+ let domain = input.domain;
660
+ let oldTaxonomy = [];
661
+ if (oldSchemaText) {
662
+ if (!domain) {
663
+ const oldDomain = extractDomainFromSchema(oldSchemaText);
664
+ if (oldDomain) domain = oldDomain;
665
+ }
666
+ const oldTax = extractTaxonomy(oldSchemaText);
667
+ if (oldTax.ok) oldTaxonomy = oldTax.data;
668
+ }
669
+ const taxonomySet = new Set(taxonomy);
670
+ for (const t of oldTaxonomy) {
671
+ if (!taxonomySet.has(t)) {
672
+ taxonomy.push(t);
673
+ taxonomySet.add(t);
674
+ }
675
+ }
676
+ const discovered = await discoverTagsFromPages(target, taxonomy);
677
+ const discovered_tags = discovered.length;
678
+ const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
679
+ try {
680
+ const schemaTpl = await readFile3(join5(input.templates, "SCHEMA.md"), "utf8");
681
+ const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
682
+ await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
683
+ created.push("SCHEMA.md");
684
+ } catch (e) {
685
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
686
+ }
687
+ const preserved = [];
688
+ async function writeOrPreserve(fileName, render) {
689
+ try {
690
+ const existing = await readFile3(join5(target, fileName), "utf8");
691
+ if (existing.split("\n").length > 10) {
692
+ preserved.push(fileName);
693
+ return void 0;
694
+ }
695
+ } catch {
696
+ }
697
+ try {
698
+ await writeFile2(join5(target, fileName), await render(), "utf8");
699
+ created.push(fileName);
700
+ return void 0;
701
+ } catch (e) {
702
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
703
+ }
704
+ }
705
+ const err1 = await writeOrPreserve("index.md", async () => {
706
+ const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
707
+ return tpl.replace("{{INIT_DATE}}", today);
708
+ });
709
+ if (err1) return err1;
710
+ const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
711
+ return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
712
+ });
713
+ if (errObsidian) return errObsidian;
714
+ const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
715
+ return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
716
+ });
717
+ if (errTemplatesJson) return errTemplatesJson;
718
+ const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
719
+ return [
720
+ "---",
721
+ "source_url:",
722
+ "created: {{date:YYYY-MM-DD}}",
723
+ "ingested: # filled by ingest pipeline",
724
+ "kind: # idea | bug | task | note | other",
725
+ 'project: # optional: "[[slug]]"',
726
+ "---",
727
+ "",
728
+ ""
729
+ ].join("\n");
730
+ });
731
+ if (errTemplate) return errTemplate;
732
+ const err22 = await writeOrPreserve("log.md", async () => {
733
+ const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
734
+ return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
735
+ });
736
+ if (err22) return err22;
737
+ const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
738
+ let envWritten = "";
739
+ if (!skipEnv) {
740
+ try {
741
+ const envEntries = {};
742
+ if (input.profile) {
743
+ envEntries[profileKey(input.profile, "PATH")] = target;
744
+ envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
745
+ envEntries["WIKI_DEFAULT"] = input.profile;
746
+ } else {
747
+ envEntries["WIKI_PATH"] = target;
748
+ envEntries["WIKI_LANG"] = canonicalLang;
749
+ }
750
+ await writeDotenv(envPath, envEntries, existingEnvRaw);
751
+ envWritten = envPath;
752
+ } catch (e) {
753
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
754
+ }
755
+ }
756
+ const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
757
+ const humanHint = [
758
+ `vault: ${target}`,
759
+ `domain: ${domain}`,
760
+ `lang: ${canonicalLang}`,
761
+ `created: ${created.length}, preserved: ${preserved.length}`,
762
+ `discovered tags: ${discovered_tags}`,
763
+ skipEnv ? "env: skipped" : `env: ${envWritten}`
764
+ ].join("\n");
765
+ if (created.length > 0) {
766
+ appendLastOp(target, {
767
+ operation: "init",
768
+ summary: `initialized vault: ${domain}`,
769
+ files: created,
770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
771
+ });
772
+ }
773
+ return {
774
+ exitCode: ExitCode.OK,
775
+ result: ok({
776
+ vault: target,
777
+ domain,
778
+ taxonomy,
779
+ lang: canonicalLang,
780
+ created,
781
+ preserved,
782
+ env_written: envWritten,
783
+ env_skipped: skipEnv,
784
+ imported_from_hermes: importedFromHermes,
785
+ discovered_tags,
786
+ humanHint,
787
+ templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
788
+ })
789
+ };
790
+ }
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 = "";
629
1639
  try {
630
- await mkdir3(target, { recursive: true });
631
- for (const d of VAULT_DIRS) {
632
- await mkdir3(join5(target, d), { recursive: true });
633
- created.push(d + "/");
634
- }
635
- } catch (e) {
636
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
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
+ };
637
1652
  }
638
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
639
- let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
640
- let domain = input.domain;
641
- let oldTaxonomy = [];
642
- if (oldSchemaText) {
643
- if (!domain) {
644
- const oldDomain = extractDomainFromSchema(oldSchemaText);
645
- if (oldDomain) domain = oldDomain;
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 };
646
1679
  }
647
- const oldTax = extractTaxonomy(oldSchemaText);
648
- if (oldTax.ok) oldTaxonomy = oldTax.data;
1680
+ if (wrote.data.created) created += 1;
1681
+ else already += 1;
649
1682
  }
650
- const taxonomySet = new Set(taxonomy);
651
- for (const t of oldTaxonomy) {
652
- if (!taxonomySet.has(t)) {
653
- taxonomy.push(t);
654
- taxonomySet.add(t);
655
- }
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 {
656
1715
  }
657
- const discovered = await discoverTagsFromPages(target, taxonomy);
658
- const discovered_tags = discovered.length;
659
- const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
660
1716
  try {
661
- const schemaTpl = await readFile3(join5(input.templates, "SCHEMA.md"), "utf8");
662
- const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
663
- await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
664
- created.push("SCHEMA.md");
665
- } catch (e) {
666
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
1717
+ curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
1718
+ } catch {
667
1719
  }
668
- const preserved = [];
669
- async function writeOrPreserve(fileName, render) {
670
- try {
671
- const existing = await readFile3(join5(target, fileName), "utf8");
672
- if (existing.split("\n").length > 10) {
673
- preserved.push(fileName);
674
- return void 0;
675
- }
676
- } catch {
677
- }
678
- try {
679
- await writeFile2(join5(target, fileName), await render(), "utf8");
680
- created.push(fileName);
681
- return void 0;
682
- } catch (e) {
683
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
684
- }
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
+ };
685
1738
  }
686
- const err1 = await writeOrPreserve("index.md", async () => {
687
- const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
688
- return tpl.replace("{{INIT_DATE}}", today);
689
- });
690
- if (err1) return err1;
691
- const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
692
- return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
693
- });
694
- if (errObsidian) return errObsidian;
695
- const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
696
- return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
697
- });
698
- if (errTemplatesJson) return errTemplatesJson;
699
- const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
700
- return [
701
- "---",
702
- "source_url:",
703
- "created: {{date:YYYY-MM-DD}}",
704
- "ingested: # filled by ingest pipeline",
705
- "kind: # idea | bug | task | note | other",
706
- 'project: # optional: "[[slug]]"',
707
- "---",
708
- "",
709
- ""
710
- ].join("\n");
711
- });
712
- if (errTemplate) return errTemplate;
713
- const err2 = await writeOrPreserve("log.md", async () => {
714
- const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
715
- return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
716
- });
717
- if (err2) return err2;
718
- const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
719
- let envWritten = "";
720
- if (!skipEnv) {
721
- try {
722
- const envEntries = {};
723
- if (input.profile) {
724
- envEntries[profileKey(input.profile, "PATH")] = target;
725
- envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
726
- envEntries["WIKI_DEFAULT"] = input.profile;
727
- } else {
728
- envEntries["WIKI_PATH"] = target;
729
- envEntries["WIKI_LANG"] = canonicalLang;
730
- }
731
- await writeDotenv(envPath, envEntries, existingEnvRaw);
732
- envWritten = envPath;
733
- } catch (e) {
734
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
735
- }
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
+ };
736
1750
  }
737
- const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
738
- const humanHint = [
739
- `vault: ${target}`,
740
- `domain: ${domain}`,
741
- `lang: ${canonicalLang}`,
742
- `created: ${created.length}, preserved: ${preserved.length}`,
743
- `discovered tags: ${discovered_tags}`,
744
- skipEnv ? "env: skipped" : `env: ${envWritten}`
745
- ].join("\n");
746
- if (created.length > 0) {
747
- appendLastOp(target, {
748
- operation: "init",
749
- summary: `initialized vault: ${domain}`,
750
- files: created,
751
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
752
- });
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
+ };
753
1773
  }
754
1774
  return {
755
1775
  exitCode: ExitCode.OK,
756
1776
  result: ok({
757
- vault: target,
758
- domain,
759
- taxonomy,
760
- lang: canonicalLang,
761
- created,
762
- preserved,
763
- env_written: envWritten,
764
- env_skipped: skipEnv,
765
- imported_from_hermes: importedFromHermes,
766
- discovered_tags,
767
- humanHint,
768
- templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
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}`
769
1787
  })
770
1788
  };
771
1789
  }
772
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,8 +2377,50 @@ async function runHealth(input) {
1359
2377
  }
1360
2378
 
1361
2379
  // src/commands/archive.ts
1362
- import { rename as rename2, mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1363
- import { join as join8, dirname as dirname5 } from "path";
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";
2382
+
2383
+ // src/utils/delete-intent.ts
2384
+ import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
2385
+ import { join as join16 } from "path";
2386
+ var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
2387
+ var DELETE_INTENT_DIR = "meta/delete-intents";
2388
+ function normalizeVaultRelPath(path) {
2389
+ const p = path.replace(/\\/g, "/").replace(/^\/+/, "");
2390
+ if (!p || p.includes("..") || p.startsWith(".git/")) {
2391
+ throw new Error(`invalid vault-relative path: ${path}`);
2392
+ }
2393
+ return p;
2394
+ }
2395
+ function pathToIntentFilename(path) {
2396
+ const p = normalizeVaultRelPath(path);
2397
+ return `${p.replace(/\//g, "__")}.json`;
2398
+ }
2399
+ function intentHostId() {
2400
+ return process.env.SKILLWIKI_HOST_ID ?? process.env.AGENT_HOST_ID ?? "unknown";
2401
+ }
2402
+ function buildDeleteIntent(input) {
2403
+ return {
2404
+ schema: DELETE_INTENT_SCHEMA,
2405
+ path: normalizeVaultRelPath(input.path),
2406
+ action: input.action,
2407
+ created: input.created ?? (/* @__PURE__ */ new Date()).toISOString(),
2408
+ host: input.host ?? intentHostId(),
2409
+ actor: input.actor,
2410
+ reason: input.reason,
2411
+ source: input.source,
2412
+ expires: input.expires ?? null
2413
+ };
2414
+ }
2415
+ async function writeDeleteIntent(vault, intent) {
2416
+ const dir = join16(vault, DELETE_INTENT_DIR);
2417
+ await mkdir6(dir, { recursive: true });
2418
+ const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
2419
+ await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
2420
+ return rel;
2421
+ }
2422
+
2423
+ // src/commands/archive.ts
1364
2424
  function countWikilinks(body, slug) {
1365
2425
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1366
2426
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -1394,7 +2454,7 @@ async function runArchive(input) {
1394
2454
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1395
2455
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1396
2456
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
1397
- const archivePath = join8("_archive", relPath).replace(/\\/g, "/");
2457
+ const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
1398
2458
  const remoteRoot = normalizeRemoteRoot(input.remote);
1399
2459
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1400
2460
  let cascade;
@@ -1420,7 +2480,7 @@ async function runArchive(input) {
1420
2480
  const indexRefs = [];
1421
2481
  if (!isRaw) {
1422
2482
  try {
1423
- const idx = await readFile5(join8(input.vault, "index.md"), "utf8");
2483
+ const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
1424
2484
  idx.split("\n").forEach((line, i) => {
1425
2485
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
1426
2486
  });
@@ -1447,8 +2507,8 @@ async function runArchive(input) {
1447
2507
  }
1448
2508
  if (input.cascade && input.apply && cascade) {
1449
2509
  for (const ref of cascade.source_array_refs) {
1450
- const absPath = join8(input.vault, ref.page);
1451
- const text = await readFile5(absPath, "utf8");
2510
+ const absPath = join17(input.vault, ref.page);
2511
+ const text = await readFile7(absPath, "utf8");
1452
2512
  const split = splitFrontmatter(text);
1453
2513
  if (!split.ok) continue;
1454
2514
  const before = split.data.rawFrontmatter;
@@ -1459,33 +2519,38 @@ async function runArchive(input) {
1459
2519
  );
1460
2520
  if (fmRewritten === before) continue;
1461
2521
  if (!arraysEqual(ref.sources_after, ref.sources_before)) {
1462
- await writeFile4(absPath, `---
2522
+ await writeFile5(absPath, `---
1463
2523
  ${fmRewritten}
1464
2524
  ---${split.data.body}`, "utf8");
1465
2525
  }
1466
2526
  }
1467
2527
  }
1468
- await mkdir5(dirname5(join8(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));
1469
2530
  let indexUpdated = false;
1470
2531
  if (!isRaw) {
1471
- const indexPath = join8(input.vault, "index.md");
1472
- try {
1473
- const idx = await readFile5(indexPath, "utf8");
1474
- const originalLines = idx.split("\n");
1475
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1476
- if (filtered.length !== originalLines.length) {
1477
- await writeFile4(indexPath, filtered.join("\n"), "utf8");
1478
- indexUpdated = true;
1479
- }
1480
- } catch (e) {
1481
- 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;
1482
2541
  }
1483
2542
  }
1484
- await rename2(join8(input.vault, relPath), join8(input.vault, archivePath));
2543
+ const archiveIntent = buildDeleteIntent({
2544
+ path: relPath,
2545
+ action: "archive",
2546
+ actor: "skillwiki-cli",
2547
+ source: "cli"
2548
+ });
2549
+ const tombstonePath = await writeDeleteIntent(input.vault, archiveIntent);
1485
2550
  appendLastOp(input.vault, {
1486
2551
  operation: input.cascade ? "archive-cascade" : "archive",
1487
- summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
1488
- files: [relPath],
2552
+ summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}; tombstone ${tombstonePath}`,
2553
+ files: [relPath, archivePath, tombstonePath],
1489
2554
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1490
2555
  });
1491
2556
  let remote;
@@ -1514,8 +2579,107 @@ ${fmRewritten}
1514
2579
  };
1515
2580
  }
1516
2581
 
2582
+ // src/commands/remove.ts
2583
+ import { unlink as unlink2, access } from "fs/promises";
2584
+ import { join as join18 } from "path";
2585
+ async function pathExists(abs) {
2586
+ try {
2587
+ await access(abs);
2588
+ return true;
2589
+ } catch {
2590
+ return false;
2591
+ }
2592
+ }
2593
+ async function runRemove(input) {
2594
+ if (input.remoteDelete && !input.remote) {
2595
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --remote" }) };
2596
+ }
2597
+ if (input.remoteDelete && !isValidRemoteDeleteCap(input.maxRemoteDeletes)) {
2598
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--max-remote-deletes must be a positive integer" }) };
2599
+ }
2600
+ const scan = await scanVault(input.vault);
2601
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2602
+ const lookup = (pages) => {
2603
+ if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
2604
+ return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
2605
+ };
2606
+ let relPath = lookup(scan.data.typedKnowledge) ?? lookup(scan.data.raw) ?? null;
2607
+ if (!relPath) {
2608
+ try {
2609
+ const candidate = normalizeVaultRelPath(input.page);
2610
+ if (await pathExists(join18(input.vault, candidate))) {
2611
+ relPath = candidate;
2612
+ }
2613
+ } catch {
2614
+ }
2615
+ }
2616
+ if (!relPath) {
2617
+ return {
2618
+ exitCode: ExitCode.FILE_NOT_FOUND,
2619
+ result: err("FILE_NOT_FOUND", { page: input.page })
2620
+ };
2621
+ }
2622
+ if (relPath.startsWith("_archive/")) {
2623
+ return {
2624
+ exitCode: ExitCode.USAGE,
2625
+ result: err("USAGE", { message: "refusing to remove path already under _archive/; use restore or leave archived" })
2626
+ };
2627
+ }
2628
+ const remoteRoot = normalizeRemoteRoot(input.remote);
2629
+ const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
2630
+ let indexUpdated = false;
2631
+ const intent = buildDeleteIntent({
2632
+ path: relPath,
2633
+ action: "remove",
2634
+ actor: "skillwiki-cli",
2635
+ source: "cli",
2636
+ reason: input.reason
2637
+ });
2638
+ const tombstonePath = await writeDeleteIntent(input.vault, intent);
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
+ }
2654
+ appendLastOp(input.vault, {
2655
+ operation: "remove",
2656
+ summary: `removed ${relPath} (tombstone ${tombstonePath})`,
2657
+ files: [relPath, tombstonePath],
2658
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2659
+ });
2660
+ let remote;
2661
+ if (remoteObjectPath) {
2662
+ const pruned = await planAndMaybePruneRemoteObjects([remoteObjectPath], input);
2663
+ if (!pruned.ok) {
2664
+ return { exitCode: ExitCode.SYNC_PUSH_FAILED, result: pruned };
2665
+ }
2666
+ remote = pruned.data;
2667
+ }
2668
+ const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
2669
+ return {
2670
+ exitCode: ExitCode.OK,
2671
+ result: ok({
2672
+ removed: relPath,
2673
+ tombstone_path: tombstonePath,
2674
+ index_updated: indexUpdated,
2675
+ ...remote ? { remote } : {},
2676
+ humanHint: `removed ${relPath}; tombstone ${tombstonePath}${indexUpdated ? " (index updated)" : ""}${remoteNote}`
2677
+ })
2678
+ };
2679
+ }
2680
+
1517
2681
  // src/commands/drift.ts
1518
- import { createHash as createHash2 } from "crypto";
2682
+ import { createHash as createHash3 } from "crypto";
1519
2683
 
1520
2684
  // src/utils/fetch.ts
1521
2685
  async function controlledFetch(url, opts) {
@@ -1594,7 +2758,7 @@ async function runDrift(input) {
1594
2758
  });
1595
2759
  continue;
1596
2760
  }
1597
- 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");
1598
2762
  const identity = assessSourceIdentity({
1599
2763
  rawPath: raw.relPath,
1600
2764
  sourceUrl,
@@ -1825,14 +2989,14 @@ ${migratedBody}${newFooter}`;
1825
2989
 
1826
2990
  // src/commands/update.ts
1827
2991
  import { execSync } from "child_process";
1828
- import { join as join9 } from "path";
2992
+ import { join as join19 } from "path";
1829
2993
  function resolveGlobalSkillsRoot() {
1830
2994
  try {
1831
2995
  const globalRoot = execSync("npm root -g", {
1832
2996
  encoding: "utf8",
1833
2997
  timeout: 5e3
1834
2998
  }).trim();
1835
- return join9(globalRoot, "skillwiki", "skills");
2999
+ return join19(globalRoot, "skillwiki", "skills");
1836
3000
  } catch {
1837
3001
  return null;
1838
3002
  }
@@ -1860,7 +3024,7 @@ async function runUpdate(input) {
1860
3024
  const pkg2 = readCliPackageJson();
1861
3025
  const currentVersion = pkg2.version;
1862
3026
  const tag = normalizeDistTag(input.distTag);
1863
- const target = join9(input.home, ".claude", "skills");
3027
+ const target = join19(input.home, ".claude", "skills");
1864
3028
  let latest;
1865
3029
  try {
1866
3030
  latest = execSync(`npm view skillwiki@${tag} version`, {
@@ -1937,22 +3101,22 @@ async function runUpdate(input) {
1937
3101
 
1938
3102
  // src/commands/self-update.ts
1939
3103
  import { execSync as execSync2 } from "child_process";
1940
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1941
- import { join as join10 } from "path";
3104
+ import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
3105
+ import { join as join20 } from "path";
1942
3106
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
1943
3107
  async function runSelfUpdate(input) {
1944
3108
  const currentVersion = readCliPackageJson().version;
1945
3109
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
1946
3110
  const distTag = normalizeDistTag(input.distTag);
1947
- const localPkgPath = join10(sourceRoot, "packages", "cli", "package.json");
1948
- const hasLocalSource = existsSync3(localPkgPath);
3111
+ const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
3112
+ const hasLocalSource = existsSync4(localPkgPath);
1949
3113
  if (input.check) {
1950
3114
  let availableVersion = null;
1951
3115
  let source;
1952
3116
  if (hasLocalSource) {
1953
3117
  source = "local";
1954
3118
  try {
1955
- availableVersion = JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? null;
3119
+ availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
1956
3120
  } catch {
1957
3121
  availableVersion = null;
1958
3122
  }
@@ -2010,7 +3174,7 @@ async function runSelfUpdate(input) {
2010
3174
  }
2011
3175
  const newVersion = (() => {
2012
3176
  try {
2013
- return JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? "unknown";
3177
+ return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
2014
3178
  } catch {
2015
3179
  return "unknown";
2016
3180
  }
@@ -2076,21 +3240,21 @@ async function runSelfUpdate(input) {
2076
3240
  }
2077
3241
 
2078
3242
  // src/commands/transcripts.ts
2079
- import { readdir as readdir3, stat as stat3, readFile as readFile6 } from "fs/promises";
2080
- import { join as join11 } from "path";
3243
+ import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
3244
+ import { join as join21 } from "path";
2081
3245
  async function runTranscripts(input) {
2082
- const dir = join11(input.vault, "raw", "transcripts");
3246
+ const dir = join21(input.vault, "raw", "transcripts");
2083
3247
  let entries;
2084
3248
  try {
2085
- entries = await readdir3(dir, { withFileTypes: true });
3249
+ entries = await readdir5(dir, { withFileTypes: true });
2086
3250
  } catch {
2087
3251
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
2088
3252
  }
2089
3253
  const transcripts = [];
2090
3254
  for (const entry of entries) {
2091
3255
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2092
- const filePath = join11(dir, entry.name);
2093
- const content = await readFile6(filePath, "utf8");
3256
+ const filePath = join21(dir, entry.name);
3257
+ const content = await readFile9(filePath, "utf8");
2094
3258
  const fm = extractFrontmatter(content);
2095
3259
  if (!fm.ok) continue;
2096
3260
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -2107,10 +3271,10 @@ async function runTranscripts(input) {
2107
3271
  }
2108
3272
 
2109
3273
  // src/commands/compound.ts
2110
- import { writeFile as writeFile5, mkdir as mkdir6, readdir as readdir4, unlink as unlink2 } from "fs/promises";
2111
- import { join as join12 } from "path";
2112
- import { existsSync as existsSync4 } from "fs";
2113
- import { readFile as readFile7 } from "fs/promises";
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";
2114
3278
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
2115
3279
  var FIELD_RE = {
2116
3280
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -2208,17 +3372,17 @@ function extractRetroFields(date, cycleName, block) {
2208
3372
  };
2209
3373
  }
2210
3374
  async function runCompound(input) {
2211
- const logPath = join12(input.vault, "log.md");
3375
+ const logPath = join22(input.vault, "log.md");
2212
3376
  let logText;
2213
3377
  try {
2214
- logText = await readFile7(logPath, "utf8");
3378
+ logText = await readFile10(logPath, "utf8");
2215
3379
  } catch {
2216
3380
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2217
3381
  }
2218
3382
  const entries = parseRetroEntries(logText);
2219
3383
  const promoted = [];
2220
3384
  const skipped = [];
2221
- const compoundDir = join12(input.vault, "projects", input.project, "compound");
3385
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
2222
3386
  for (const entry of entries) {
2223
3387
  const generalizeValue = entry.generalize.trim();
2224
3388
  if (!/^yes/i.test(generalizeValue)) {
@@ -2226,8 +3390,8 @@ async function runCompound(input) {
2226
3390
  continue;
2227
3391
  }
2228
3392
  const slug = slugify(entry.cycleName);
2229
- const compoundPath = join12(compoundDir, `${slug}.md`);
2230
- if (existsSync4(compoundPath)) {
3393
+ const compoundPath = join22(compoundDir, `${slug}.md`);
3394
+ if (existsSync5(compoundPath)) {
2231
3395
  skipped.push(entry.date);
2232
3396
  continue;
2233
3397
  }
@@ -2265,10 +3429,10 @@ async function runCompound(input) {
2265
3429
  ].join("\n");
2266
3430
  const content = frontmatter + "\n" + body;
2267
3431
  if (!input.dryRun) {
2268
- if (!existsSync4(compoundDir)) {
2269
- await mkdir6(compoundDir, { recursive: true });
3432
+ if (!existsSync5(compoundDir)) {
3433
+ await mkdir8(compoundDir, { recursive: true });
2270
3434
  }
2271
- await writeFile5(compoundPath, content, "utf8");
3435
+ await writeFile7(compoundPath, content, "utf8");
2272
3436
  }
2273
3437
  promoted.push(`${slug}.md`);
2274
3438
  }
@@ -2287,23 +3451,23 @@ async function runCompound(input) {
2287
3451
  };
2288
3452
  }
2289
3453
  async function runCompoundDelete(input) {
2290
- const projectDir = join12(input.vault, "projects", input.project);
2291
- if (!existsSync4(projectDir)) {
3454
+ const projectDir = join22(input.vault, "projects", input.project);
3455
+ if (!existsSync5(projectDir)) {
2292
3456
  return {
2293
3457
  exitCode: ExitCode.PROJECT_NOT_FOUND,
2294
3458
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
2295
3459
  };
2296
3460
  }
2297
3461
  const entryName = input.entry.replace(/\.md$/, "");
2298
- const compoundPath = join12(projectDir, "compound", `${entryName}.md`);
2299
- if (!existsSync4(compoundPath)) {
3462
+ const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
3463
+ if (!existsSync5(compoundPath)) {
2300
3464
  return {
2301
3465
  exitCode: ExitCode.FILE_NOT_FOUND,
2302
3466
  result: err("FILE_NOT_FOUND", { path: compoundPath })
2303
3467
  };
2304
3468
  }
2305
3469
  try {
2306
- await unlink2(compoundPath);
3470
+ await unlink3(compoundPath);
2307
3471
  } catch (e) {
2308
3472
  return {
2309
3473
  exitCode: ExitCode.WRITE_FAILED,
@@ -2329,8 +3493,8 @@ knowledge.md regenerated`
2329
3493
  };
2330
3494
  }
2331
3495
  async function runCompoundList(input) {
2332
- const compoundDir = join12(input.vault, "projects", input.project, "compound");
2333
- if (!existsSync4(compoundDir)) {
3496
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
3497
+ if (!existsSync5(compoundDir)) {
2334
3498
  return {
2335
3499
  exitCode: ExitCode.OK,
2336
3500
  result: ok({
@@ -2344,7 +3508,7 @@ no compound directory found`
2344
3508
  }
2345
3509
  let dirents;
2346
3510
  try {
2347
- dirents = await readdir4(compoundDir, { withFileTypes: true });
3511
+ dirents = await readdir6(compoundDir, { withFileTypes: true });
2348
3512
  } catch {
2349
3513
  return {
2350
3514
  exitCode: ExitCode.OK,
@@ -2360,10 +3524,10 @@ could not read compound directory`
2360
3524
  const entries = [];
2361
3525
  for (const dirent of dirents) {
2362
3526
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
2363
- const filePath = join12(compoundDir, dirent.name);
3527
+ const filePath = join22(compoundDir, dirent.name);
2364
3528
  let text;
2365
3529
  try {
2366
- text = await readFile7(filePath, "utf8");
3530
+ text = await readFile10(filePath, "utf8");
2367
3531
  } catch {
2368
3532
  continue;
2369
3533
  }
@@ -2392,8 +3556,8 @@ no compound entries found`;
2392
3556
  }
2393
3557
 
2394
3558
  // src/commands/session-brief.ts
2395
- import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2396
- import { join as join13, 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";
2397
3561
  var MAX_WORDS = 900;
2398
3562
  async function runSessionBrief(input) {
2399
3563
  const scan = await scanVault(input.vault);
@@ -2493,7 +3657,7 @@ async function resolveProject(input) {
2493
3657
  const envProject = input.env?.SKILLWIKI_PROJECT;
2494
3658
  if (envProject) return envProject;
2495
3659
  const cwd = input.cwd ?? process.cwd();
2496
- const projectDotenv = await readProjectSlug(join13(cwd, ".skillwiki", ".env"));
3660
+ const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
2497
3661
  if (projectDotenv) return projectDotenv;
2498
3662
  const inferred = inferProjectFromPath(input.vault, cwd);
2499
3663
  if (inferred) return inferred;
@@ -2502,7 +3666,7 @@ async function resolveProject(input) {
2502
3666
  async function readProjectSlug(file) {
2503
3667
  let text;
2504
3668
  try {
2505
- text = await readFile8(file, "utf8");
3669
+ text = await readFile11(file, "utf8");
2506
3670
  } catch {
2507
3671
  return void 0;
2508
3672
  }
@@ -2579,7 +3743,7 @@ async function loadTrendDigests(typedPages) {
2579
3743
  return out;
2580
3744
  }
2581
3745
  async function loadSessionPins(vault, project) {
2582
- const text = await readIfExists(join13(vault, "meta", "session-pins.md"));
3746
+ const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
2583
3747
  if (!text) return [];
2584
3748
  const fm = extractFrontmatter(text);
2585
3749
  if (!fm.ok) return [];
@@ -2696,7 +3860,7 @@ function satelliteHealthWarnings(warning) {
2696
3860
  return warning ? [warning] : [];
2697
3861
  }
2698
3862
  async function loadHealthWarnings(vault) {
2699
- const text = await readIfExists(join13(vault, ".skillwiki", "health.json"));
3863
+ const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
2700
3864
  if (!text) return [];
2701
3865
  try {
2702
3866
  const parsed = JSON.parse(text);
@@ -2709,7 +3873,7 @@ async function loadHealthWarnings(vault) {
2709
3873
  }
2710
3874
  }
2711
3875
  async function loadMemoryTopics(vault, project) {
2712
- const text = await readIfExists(join13(vault, ".skillwiki", "memory", project, "topics.json"));
3876
+ const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
2713
3877
  if (!text) return [];
2714
3878
  try {
2715
3879
  const parsed = JSON.parse(text);
@@ -2726,18 +3890,18 @@ async function loadMemoryTopics(vault, project) {
2726
3890
  }
2727
3891
  }
2728
3892
  async function writeBriefArtifacts(vault, input) {
2729
- const metaPath = join13(vault, "meta", "latest-session-brief.md");
2730
- const cacheMdPath = join13(vault, ".skillwiki", "session-brief.md");
2731
- const cacheJsonPath = join13(vault, ".skillwiki", "session-brief.json");
2732
- await mkdir7(join13(vault, "meta"), { recursive: true });
2733
- await mkdir7(join13(vault, ".skillwiki"), { recursive: true });
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 });
2734
3898
  const committed = renderCommittedBrief(input);
2735
3899
  const previousComparable = comparableBrief(await readIfExists(metaPath));
2736
3900
  const nextComparable = comparableBrief(committed);
2737
3901
  const materialChange = previousComparable !== nextComparable;
2738
- await writeFile6(metaPath, committed, "utf8");
2739
- await writeFile6(cacheMdPath, input.brief, "utf8");
2740
- await writeFile6(cacheJsonPath, `${JSON.stringify({
3902
+ await writeFile8(metaPath, committed, "utf8");
3903
+ await writeFile8(cacheMdPath, input.brief, "utf8");
3904
+ await writeFile8(cacheJsonPath, `${JSON.stringify({
2741
3905
  project: input.project,
2742
3906
  brief: input.brief,
2743
3907
  word_count: input.wordCount,
@@ -2745,8 +3909,17 @@ async function writeBriefArtifacts(vault, input) {
2745
3909
  memory_topics: input.memoryTopics
2746
3910
  }, null, 2)}
2747
3911
  `, "utf8");
2748
- const indexUpdated = await ensureIndexEntry(vault);
2749
- 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
+ }
2750
3923
  if (materialChange) {
2751
3924
  appendLastOp(vault, {
2752
3925
  operation: "session-brief",
@@ -2787,40 +3960,21 @@ function renderCommittedBrief(input) {
2787
3960
  "---",
2788
3961
  "",
2789
3962
  input.brief.trimEnd(),
2790
- ""
2791
- ].filter((line) => line !== "").join("\n");
2792
- }
2793
- async function ensureIndexEntry(vault) {
2794
- const indexPath = join13(vault, "index.md");
2795
- let text = await readIfExists(indexPath);
2796
- if (!text) return false;
2797
- if (text.includes("[[meta/latest-session-brief]]")) return false;
2798
- const entry = "- [[meta/latest-session-brief]] \u2014 Latest Session Brief";
2799
- const lines = text.split(/\r?\n/);
2800
- const sectionIdx = lines.findIndex((line) => line.trim() === "## Meta");
2801
- if (sectionIdx === -1) {
2802
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
2803
- lines.push("", "## Meta", entry);
2804
- } else {
2805
- let insertAt = sectionIdx + 1;
2806
- while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
2807
- lines.splice(insertAt, 0, entry);
2808
- }
2809
- await writeFile6(indexPath, lines.join("\n"), "utf8");
2810
- return true;
3963
+ ""
3964
+ ].filter((line) => line !== "").join("\n");
2811
3965
  }
2812
- async function appendMaterialLog(vault, today) {
2813
- const logPath = join13(vault, "log.md");
2814
- const text = await readIfExists(logPath);
2815
- if (!text) return false;
2816
- const entry = `
2817
- ## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
2818
- await writeFile6(logPath, text.trimEnd() + entry + "\n", "utf8");
2819
- 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;
2820
3974
  }
2821
3975
  async function readIfExists(path) {
2822
3976
  try {
2823
- return await readFile8(path, "utf8");
3977
+ return await readFile11(path, "utf8");
2824
3978
  } catch {
2825
3979
  return "";
2826
3980
  }
@@ -2862,16 +4016,18 @@ function dateFromPath(path) {
2862
4016
  }
2863
4017
 
2864
4018
  // src/commands/ingest.ts
2865
- import { readFile as readFile10, open, unlink as unlink3, mkdir as mkdir8 } from "fs/promises";
2866
- import { join as join15 } 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";
2867
4021
  import { createHash as createHash4 } from "crypto";
2868
4022
 
2869
4023
  // src/commands/page-publish.ts
2870
- import { createHash as createHash3 } from "crypto";
2871
4024
  import { realpathSync } from "fs";
2872
- import { readFile as readFile9 } from "fs/promises";
2873
- import { join as join14, resolve as resolve3 } from "path";
2874
- var DEFAULT_DEPS = { afterStage: async () => void 0 };
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
+ };
2875
4031
  function errorExitCode(error) {
2876
4032
  switch (error) {
2877
4033
  case "FILE_NOT_FOUND":
@@ -2887,6 +4043,9 @@ function errorExitCode(error) {
2887
4043
  return ExitCode.SENSITIVE_CONTENT_DETECTED;
2888
4044
  case "SYNC_LOCK_HELD":
2889
4045
  return ExitCode.SYNC_LOCK_HELD;
4046
+ case "PREFLIGHT_FAILED":
4047
+ return ExitCode.PREFLIGHT_FAILED;
4048
+ case "EVENT_IDENTITY_COLLISION":
2890
4049
  case "WRITE_FAILED":
2891
4050
  return ExitCode.WRITE_FAILED;
2892
4051
  default:
@@ -2894,7 +4053,11 @@ function errorExitCode(error) {
2894
4053
  }
2895
4054
  }
2896
4055
  function publicationId(target, content, logNote = "") {
2897
- return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
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";
2898
4061
  }
2899
4062
  function prepareFrozenPublication(input, source) {
2900
4063
  const target = assertTargetInsideVault(input.vault, input.target);
@@ -2930,7 +4093,7 @@ function preparePagePublicationFromContent(input) {
2930
4093
  async function preparePagePublication(input) {
2931
4094
  let content;
2932
4095
  try {
2933
- content = await readFile9(input.draftPath, "utf8");
4096
+ content = await readFile12(input.draftPath, "utf8");
2934
4097
  } catch (error) {
2935
4098
  return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
2936
4099
  }
@@ -3001,10 +4164,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
3001
4164
  ExitCode.VAULT_PATH_INVALID
3002
4165
  );
3003
4166
  }
3004
- const schemaPath = join14(vault, "SCHEMA.md");
4167
+ const schemaPath = join24(vault, "SCHEMA.md");
3005
4168
  let schemaText;
3006
4169
  try {
3007
- schemaText = await readFile9(schemaPath, "utf8");
4170
+ schemaText = await readFile12(schemaPath, "utf8");
3008
4171
  } catch (error) {
3009
4172
  return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
3010
4173
  }
@@ -3034,8 +4197,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
3034
4197
  let visibleSchema;
3035
4198
  try {
3036
4199
  [visible, visibleSchema] = await Promise.all([
3037
- readFile9(input.targetPath, "utf8"),
3038
- readFile9(schemaPath, "utf8")
4200
+ readFile12(input.targetPath, "utf8"),
4201
+ readFile12(schemaPath, "utf8")
3039
4202
  ]);
3040
4203
  } catch (error) {
3041
4204
  return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
@@ -3088,7 +4251,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
3088
4251
  })
3089
4252
  };
3090
4253
  }
3091
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
4254
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
3092
4255
  return {
3093
4256
  exitCode: ExitCode.OK,
3094
4257
  result: ok({
@@ -3102,6 +4265,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
3102
4265
  operation_id: input.operationId,
3103
4266
  dry_run: dryRun,
3104
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 } : {},
3105
4271
  humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
3106
4272
  })
3107
4273
  };
@@ -3117,17 +4283,17 @@ function renderPublicationLog(input, added) {
3117
4283
  }
3118
4284
  async function readPageChanged(targetPath, content) {
3119
4285
  try {
3120
- return ok(await readFile9(targetPath, "utf8") !== content);
4286
+ return ok(await readFile12(targetPath, "utf8") !== content);
3121
4287
  } catch (error) {
3122
4288
  if (error.code === "ENOENT") return ok(true);
3123
4289
  return err("WRITE_FAILED", { path: targetPath, message: String(error) });
3124
4290
  }
3125
4291
  }
3126
4292
  async function previewPreparedPagePublication(input, vault) {
3127
- const schemaPath = join14(vault, "SCHEMA.md");
4293
+ const schemaPath = join24(vault, "SCHEMA.md");
3128
4294
  let schemaText;
3129
4295
  try {
3130
- schemaText = await readFile9(schemaPath, "utf8");
4296
+ schemaText = await readFile12(schemaPath, "utf8");
3131
4297
  } catch (error) {
3132
4298
  const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
3133
4299
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3139,10 +4305,10 @@ async function previewPreparedPagePublication(input, vault) {
3139
4305
  if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
3140
4306
  const pageChanged = await readPageChanged(input.targetPath, input.page.content);
3141
4307
  if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
3142
- const indexPath = join14(vault, "index.md");
4308
+ const indexPath = join24(vault, "index.md");
3143
4309
  let indexText;
3144
4310
  try {
3145
- indexText = await readFile9(indexPath, "utf8");
4311
+ indexText = await readFile12(indexPath, "utf8");
3146
4312
  } catch (error) {
3147
4313
  const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
3148
4314
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3153,10 +4319,10 @@ async function previewPreparedPagePublication(input, vault) {
3153
4319
  type: input.page.type
3154
4320
  });
3155
4321
  if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
3156
- const logPath = join14(vault, "log.md");
4322
+ const logPath = join24(vault, "log.md");
3157
4323
  let logText;
3158
4324
  try {
3159
- logText = await readFile9(logPath, "utf8");
4325
+ logText = await readFile12(logPath, "utf8");
3160
4326
  } catch (error) {
3161
4327
  const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
3162
4328
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3179,93 +4345,159 @@ async function previewPreparedPagePublication(input, vault) {
3179
4345
  );
3180
4346
  }
3181
4347
  async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
3182
- let lock;
4348
+ const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
4349
+ if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
3183
4350
  try {
3184
- lock = acquireOwnedSyncLock(vault, {
3185
- summary: `page publish ${input.page.target}`,
3186
- ttlMinutes: 1
4351
+ const preflight = await deps.preflight({
4352
+ vault,
4353
+ command: `page publish ${input.page.target}`,
4354
+ lockToken: managed.data.ownerToken
3187
4355
  });
3188
- } catch (error) {
3189
- return {
3190
- exitCode: ExitCode.WRITE_FAILED,
3191
- result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
3192
- };
3193
- }
3194
- if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
3195
- let primary;
3196
- let released;
3197
- try {
3198
- primary = await runLockedPrimaryStages(input, vault, deps);
3199
- } catch (error) {
3200
- primary = lockedFailure(
3201
- "schema",
3202
- emptyLockedState(),
3203
- err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
3204
- );
3205
- } finally {
3206
- released = releaseOwnedSyncLock(lock.data);
3207
- }
3208
- const primaryState = primary?.ok ? primary.data : primary?.state;
3209
- if (released === void 0 || !released.ok || !released.data.released) {
3210
- return phaseFailure(
3211
- "unlock",
3212
- input,
3213
- primaryState?.published ?? false,
3214
- released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
3215
- {
3216
- primary_stage: primary && !primary.ok ? primary.stage : "complete",
3217
- primary_error: primary && !primary.ok ? primary.cause.error : void 0
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
+ };
3218
4372
  }
3219
- );
3220
- }
3221
- const unlockHook = await observeStage(deps, "unlock");
3222
- if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
3223
- if (primary === void 0) {
3224
- return phaseFailure(
3225
- "schema",
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(
3226
4489
  input,
4490
+ state.taxonomyAdded,
4491
+ state.pageChanged,
4492
+ state.indexUpdated,
4493
+ logAppended,
4494
+ [...state.changed],
3227
4495
  false,
3228
- err("WRITE_FAILED", { message: "locked publication produced no result" })
3229
- );
3230
- }
3231
- if (!primary.ok) {
3232
- return phaseFailure(
3233
- primary.stage,
3234
- input,
3235
- primary.state.published,
3236
- primary.cause,
3237
- void 0,
3238
- primary.exitCode
3239
- );
3240
- }
3241
- const state = primary.data;
3242
- const log = await runLogAppend({
3243
- vault,
3244
- content: renderPublicationLog(input, state.taxonomyAdded),
3245
- operationId: input.operationId,
3246
- strictLock: true,
3247
- recordLastOp: false
3248
- });
3249
- if (!log.result.ok) return phaseFailure("log", input, true, log.result);
3250
- if (log.exitCode !== ExitCode.OK) {
3251
- return phaseFailure(
3252
- "log",
3253
- input,
3254
- true,
3255
- err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
4496
+ writeReceipt
3256
4497
  );
4498
+ } finally {
4499
+ releaseManagedWriteLock(managed.data);
3257
4500
  }
3258
- if (log.result.data.appended) state.changed.add("log.md");
3259
- const logHook = await observeStage(deps, "log");
3260
- if (logHook) return phaseFailure("log", input, true, logHook);
3261
- return successReceipt(
3262
- input,
3263
- state.taxonomyAdded,
3264
- state.pageChanged,
3265
- state.indexUpdated,
3266
- log.result.data.appended,
3267
- [...state.changed]
3268
- );
3269
4501
  }
3270
4502
  async function runPagePublish(input, deps = DEFAULT_DEPS) {
3271
4503
  const prepared = await preparePagePublication(input);
@@ -3352,7 +4584,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
3352
4584
  }
3353
4585
  async function resolveRawCapture(input) {
3354
4586
  try {
3355
- const existing = await readFile10(input.path, "utf8");
4587
+ const existing = await readFile13(input.path, "utf8");
3356
4588
  const frontmatter = extractFrontmatter(existing);
3357
4589
  if (!frontmatter.ok) {
3358
4590
  return err("INGEST_VALIDATION_FAILED", {
@@ -3401,7 +4633,7 @@ async function writeResolvedRaw(input) {
3401
4633
  return written.ok ? ok({ changed: written.data.changed, capture: resolved.data }) : written;
3402
4634
  } finally {
3403
4635
  try {
3404
- await unlink3(lock.data);
4636
+ await unlink4(lock.data);
3405
4637
  } catch {
3406
4638
  }
3407
4639
  }
@@ -3410,7 +4642,7 @@ async function acquireRawCaptureLock(path) {
3410
4642
  const lockPath = `${path}.ingest.lock`;
3411
4643
  for (let attempt = 0; attempt < 200; attempt++) {
3412
4644
  try {
3413
- const handle = await open(lockPath, "wx");
4645
+ const handle = await open2(lockPath, "wx");
3414
4646
  await handle.close();
3415
4647
  return ok(lockPath);
3416
4648
  } catch (error) {
@@ -3491,7 +4723,7 @@ async function runIngest(input) {
3491
4723
  sourceContent = fetchResult.data.body;
3492
4724
  } else {
3493
4725
  try {
3494
- sourceContent = await readFile10(input.source, "utf8");
4726
+ sourceContent = await readFile13(input.source, "utf8");
3495
4727
  } catch {
3496
4728
  return {
3497
4729
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -3516,7 +4748,7 @@ async function runIngest(input) {
3516
4748
  const rawRelPath = `raw/articles/${slug}.md`;
3517
4749
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
3518
4750
  const typedRelPath = `${typedDir}/${slug}.md`;
3519
- const rawAbsPath = join15(input.vault, rawRelPath);
4751
+ const rawAbsPath = join25(input.vault, rawRelPath);
3520
4752
  const identity = assessSourceIdentity({
3521
4753
  rawPath: rawRelPath,
3522
4754
  sourceUrl: sourceUrl ?? void 0,
@@ -3560,11 +4792,11 @@ async function runIngest(input) {
3560
4792
  );
3561
4793
  if (!input.dryRun) {
3562
4794
  try {
3563
- await mkdir8(join15(input.vault, typedDir), { recursive: true });
4795
+ await mkdir10(join25(input.vault, typedDir), { recursive: true });
3564
4796
  } catch (error) {
3565
4797
  return {
3566
4798
  exitCode: ExitCode.WRITE_FAILED,
3567
- result: err("WRITE_FAILED", { path: join15(input.vault, typedDir), message: String(error) })
4799
+ result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
3568
4800
  };
3569
4801
  }
3570
4802
  }
@@ -3611,11 +4843,11 @@ async function runIngest(input) {
3611
4843
  };
3612
4844
  }
3613
4845
  try {
3614
- await mkdir8(join15(input.vault, "raw", "articles"), { recursive: true });
4846
+ await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
3615
4847
  } catch (error) {
3616
4848
  return {
3617
4849
  exitCode: ExitCode.WRITE_FAILED,
3618
- result: err("WRITE_FAILED", { path: join15(input.vault, "raw", "articles"), message: String(error) })
4850
+ result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
3619
4851
  };
3620
4852
  }
3621
4853
  const rawWrite = await writeResolvedRaw({
@@ -3843,8 +5075,8 @@ ${body}`;
3843
5075
  }
3844
5076
 
3845
5077
  // src/commands/tag-reconcile.ts
3846
- import { readFile as readFile11 } from "fs/promises";
3847
- import { join as join16, posix } from "path";
5078
+ import { readFile as readFile14 } from "fs/promises";
5079
+ import { join as join26, posix } from "path";
3848
5080
  var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
3849
5081
  function errorExitCode2(error) {
3850
5082
  switch (error) {
@@ -3887,7 +5119,7 @@ function asTagArray(frontmatter, path) {
3887
5119
  async function readTagsFromFile(path) {
3888
5120
  let text;
3889
5121
  try {
3890
- text = await readFile11(path, "utf8");
5122
+ text = await readFile14(path, "utf8");
3891
5123
  } catch (error) {
3892
5124
  if (error.code === "ENOENT") {
3893
5125
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
@@ -3908,7 +5140,7 @@ async function resolveRequestedTags(input, page) {
3908
5140
  result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
3909
5141
  };
3910
5142
  }
3911
- const source = input.from ?? (explicit.length === 0 ? join16(input.vault, page) : void 0);
5143
+ const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
3912
5144
  if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
3913
5145
  const sourced = await readTagsFromFile(source);
3914
5146
  if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
@@ -3919,7 +5151,7 @@ async function resolveRequestedTags(input, page) {
3919
5151
  }
3920
5152
  async function readSchema(schemaPath) {
3921
5153
  try {
3922
- return { exitCode: ExitCode.OK, result: ok(await readFile11(schemaPath, "utf8")) };
5154
+ return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
3923
5155
  } catch (error) {
3924
5156
  if (error.code === "ENOENT") {
3925
5157
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
@@ -3947,7 +5179,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
3947
5179
  };
3948
5180
  }
3949
5181
  async function reconcileTagsWhileLocked(input, page, tags, comment) {
3950
- const schemaPath = join16(input.vault, "SCHEMA.md");
5182
+ const schemaPath = join26(input.vault, "SCHEMA.md");
3951
5183
  const current = await readSchema(schemaPath);
3952
5184
  if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
3953
5185
  const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
@@ -3958,7 +5190,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
3958
5190
  }
3959
5191
  let verifiedText;
3960
5192
  try {
3961
- verifiedText = await readFile11(schemaPath, "utf8");
5193
+ verifiedText = await readFile14(schemaPath, "utf8");
3962
5194
  } catch (error) {
3963
5195
  return {
3964
5196
  exitCode: ExitCode.WRITE_FAILED,
@@ -3984,7 +5216,7 @@ async function runTagReconcile(input) {
3984
5216
  const comment = taxonomyCommentForPage(page.data, date, input.reason);
3985
5217
  if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
3986
5218
  if (!input.write) {
3987
- const schema = await readSchema(join16(input.vault, "SCHEMA.md"));
5219
+ const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
3988
5220
  if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
3989
5221
  const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
3990
5222
  return previewResult(page.data, tags, preview, true, []);
@@ -4036,22 +5268,9 @@ async function runTagReconcile(input) {
4036
5268
  }
4037
5269
 
4038
5270
  // src/commands/sync.ts
4039
- import { existsSync as existsSync5 } from "fs";
4040
- import { join as join17 } from "path";
4041
- import { execFileSync as execFileSync2 } from "child_process";
4042
-
4043
- // src/utils/git.ts
4044
- import { execFileSync } from "child_process";
4045
- function git(cwd, args) {
4046
- try {
4047
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4048
- } catch {
4049
- return "";
4050
- }
4051
- }
4052
- function gitStrict(cwd, args) {
4053
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4054
- }
5271
+ import { existsSync as existsSync6 } from "fs";
5272
+ import { join as join27 } from "path";
5273
+ import { execFileSync as execFileSync3 } from "child_process";
4055
5274
 
4056
5275
  // src/utils/vault-git-pathspec.ts
4057
5276
  var VAULT_GENERATED_COMMIT_PATHS = [
@@ -4092,7 +5311,7 @@ function isTrackedNotePath(path) {
4092
5311
  }
4093
5312
  function refHasPath(vault, ref, path) {
4094
5313
  try {
4095
- execFileSync2("git", ["cat-file", "-e", `${ref}:${path}`], {
5314
+ execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
4096
5315
  cwd: vault,
4097
5316
  stdio: ["pipe", "pipe", "pipe"]
4098
5317
  });
@@ -4104,7 +5323,7 @@ function refHasPath(vault, ref, path) {
4104
5323
  function runSyncStatus(input) {
4105
5324
  const vault = input.vault;
4106
5325
  const includeStashes = input.includeStashes ?? false;
4107
- if (!existsSync5(join17(vault, ".git"))) {
5326
+ if (!existsSync6(join27(vault, ".git"))) {
4108
5327
  return {
4109
5328
  exitCode: ExitCode.VAULT_PATH_INVALID,
4110
5329
  result: ok({
@@ -4211,7 +5430,7 @@ function runSyncStatus(input) {
4211
5430
  }
4212
5431
  async function runSyncPush(input) {
4213
5432
  const vault = input.vault;
4214
- if (!existsSync5(join17(vault, ".git"))) {
5433
+ if (!existsSync6(join27(vault, ".git"))) {
4215
5434
  return {
4216
5435
  exitCode: ExitCode.VAULT_PATH_INVALID,
4217
5436
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -4371,100 +5590,44 @@ function enableGitLongPathsOnWindows(vault) {
4371
5590
  }
4372
5591
  async function runSyncPull(input) {
4373
5592
  const vault = input.vault;
4374
- if (!existsSync5(join17(vault, ".git"))) {
5593
+ if (!existsSync6(join27(vault, ".git"))) {
4375
5594
  return {
4376
5595
  exitCode: ExitCode.VAULT_PATH_INVALID,
4377
5596
  result: err("NOT_A_GIT_REPO", { path: vault })
4378
5597
  };
4379
5598
  }
4380
5599
  enableGitLongPathsOnWindows(vault);
4381
- let fetched = false;
4382
- try {
4383
- gitStrict(vault, ["fetch", "origin"]);
4384
- fetched = true;
4385
- } catch (e) {
5600
+ const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
5601
+ if (!remoteUrl) {
4386
5602
  return {
4387
5603
  exitCode: ExitCode.SYNC_PULL_FAILED,
4388
- result: err("GIT_FETCH_FAILED", { message: String(e) })
5604
+ result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
4389
5605
  };
4390
5606
  }
4391
- let pulled = false;
4392
- let conflicts = 0;
4393
- let filesUpdated = 0;
4394
- let autoResolved = 0;
4395
- try {
4396
- const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
4397
- pulled = true;
4398
- const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
4399
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4400
- } catch (e) {
4401
- const errString = String(e);
4402
- if (errString.includes("conflict")) {
4403
- let inConflict = true;
4404
- while (inConflict) {
4405
- const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
4406
- let commitMsg = "";
4407
- if (stoppedSha) {
4408
- commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
4409
- }
4410
- const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
4411
- const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
4412
- const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
4413
- if (conflictedList.length === 0) {
4414
- try {
4415
- gitStrict(vault, ["rebase", "--continue"]);
4416
- inConflict = true;
4417
- } catch {
4418
- inConflict = false;
4419
- }
4420
- continue;
4421
- }
4422
- if (isArchiveOrSnapshot) {
4423
- for (const f of conflictedList) {
4424
- try {
4425
- gitStrict(vault, ["checkout", "--ours", f]);
4426
- gitStrict(vault, ["add", f]);
4427
- } catch {
4428
- }
4429
- }
4430
- autoResolved += conflictedList.length;
4431
- try {
4432
- gitStrict(vault, ["rebase", "--continue"]);
4433
- } catch (continueErr) {
4434
- continue;
4435
- }
4436
- } else {
4437
- conflicts = conflictedList.length;
4438
- return {
4439
- exitCode: ExitCode.SYNC_PULL_FAILED,
4440
- result: ok({
4441
- fetched,
4442
- pulled: false,
4443
- files_updated: 0,
4444
- conflicts,
4445
- auto_resolved: 0,
4446
- lint_errors: 0,
4447
- lint_warnings: 0,
4448
- humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
4449
- })
4450
- };
4451
- }
4452
- }
4453
- if (autoResolved > 0) {
4454
- const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
4455
- if (diffOutput) {
4456
- const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
4457
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4458
- }
4459
- pulled = true;
4460
- conflicts = 0;
4461
- }
4462
- } else {
5607
+ const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
5608
+ if (!helper.ok) {
5609
+ if (helper.error === "PREFLIGHT_FAILED") {
4463
5610
  return {
4464
- exitCode: ExitCode.SYNC_PULL_FAILED,
4465
- result: err("GIT_PULL_FAILED", { message: errString })
5611
+ exitCode: ExitCode.PREFLIGHT_FAILED,
5612
+ result: err("PREFLIGHT_FAILED", helper.detail)
4466
5613
  };
4467
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
+ }
4468
5631
  }
4469
5632
  const pathFix = await fixPathTooLong({ vault });
4470
5633
  const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
@@ -4477,8 +5640,7 @@ async function runSyncPull(input) {
4477
5640
  }
4478
5641
  const hintParts = [];
4479
5642
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
4480
- else hintParts.push("already up to date");
4481
- if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
5643
+ else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
4482
5644
  if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
4483
5645
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
4484
5646
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
@@ -4543,7 +5705,7 @@ function runSyncPeers(input) {
4543
5705
  }
4544
5706
  function runSyncLock(input) {
4545
5707
  const vault = input.vault;
4546
- if (!existsSync5(vault)) {
5708
+ if (!existsSync6(vault)) {
4547
5709
  return {
4548
5710
  exitCode: ExitCode.VAULT_PATH_INVALID,
4549
5711
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4578,7 +5740,7 @@ function runSyncLock(input) {
4578
5740
  }
4579
5741
  function runSyncUnlock(input) {
4580
5742
  const vault = input.vault;
4581
- if (!existsSync5(vault)) {
5743
+ if (!existsSync6(vault)) {
4582
5744
  return {
4583
5745
  exitCode: ExitCode.VAULT_PATH_INVALID,
4584
5746
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4611,8 +5773,8 @@ function runSyncUnlock(input) {
4611
5773
  }
4612
5774
 
4613
5775
  // src/commands/backup.ts
4614
- import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4615
- import { join as join18, relative as relative2, dirname as dirname6 } from "path";
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";
4616
5778
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
4617
5779
 
4618
5780
  // src/utils/s3-client.ts
@@ -4636,7 +5798,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
4636
5798
  function* walkMarkdown(dir, base) {
4637
5799
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
4638
5800
  if (SKIP_DIRS.has(entry.name)) continue;
4639
- const full = join18(dir, entry.name);
5801
+ const full = join28(dir, entry.name);
4640
5802
  if (entry.isDirectory()) {
4641
5803
  yield* walkMarkdown(full, base);
4642
5804
  } else if (entry.name.endsWith(".md")) {
@@ -4659,7 +5821,7 @@ async function runBackupSync(input) {
4659
5821
  let failed = 0;
4660
5822
  const files = [...walkMarkdown(input.vault, input.vault)];
4661
5823
  for (const relPath of files) {
4662
- const absPath = join18(input.vault, relPath);
5824
+ const absPath = join28(input.vault, relPath);
4663
5825
  const localStat = statSync2(absPath);
4664
5826
  let needsUpload = true;
4665
5827
  try {
@@ -4678,7 +5840,7 @@ async function runBackupSync(input) {
4678
5840
  continue;
4679
5841
  }
4680
5842
  try {
4681
- const body = readFileSync4(absPath);
5843
+ const body = readFileSync11(absPath);
4682
5844
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
4683
5845
  uploaded++;
4684
5846
  } catch {
@@ -4735,7 +5897,7 @@ async function runBackupRestore(input) {
4735
5897
  const objects = list.Contents ?? [];
4736
5898
  for (const obj of objects) {
4737
5899
  if (!obj.Key) continue;
4738
- const localPath = join18(target, obj.Key);
5900
+ const localPath = join28(target, obj.Key);
4739
5901
  try {
4740
5902
  const localStat = statSync2(localPath);
4741
5903
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -4749,7 +5911,7 @@ async function runBackupRestore(input) {
4749
5911
  const body = await resp.Body?.transformToByteArray();
4750
5912
  if (body) {
4751
5913
  mkdirSync2(dirname6(localPath), { recursive: true });
4752
- writeFileSync2(localPath, Buffer.from(body));
5914
+ writeFileSync4(localPath, Buffer.from(body));
4753
5915
  downloaded++;
4754
5916
  }
4755
5917
  } catch {
@@ -4781,11 +5943,11 @@ async function runBackupRestore(input) {
4781
5943
  }
4782
5944
 
4783
5945
  // src/commands/status.ts
4784
- import { existsSync as existsSync6, statSync as statSync3 } from "fs";
4785
- import { readFile as readFile12 } from "fs/promises";
4786
- import { join as join19 } from "path";
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";
4787
5949
  async function runStatus(input) {
4788
- if (!existsSync6(input.vault)) {
5950
+ if (!existsSync7(input.vault)) {
4789
5951
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
4790
5952
  }
4791
5953
  const scan = await scanVault(input.vault);
@@ -4810,7 +5972,7 @@ async function runStatus(input) {
4810
5972
  const compound = scan.data.compound.length;
4811
5973
  let schemaVersion = "v1";
4812
5974
  try {
4813
- const schemaContent = await readFile12(join19(input.vault, "SCHEMA.md"), "utf8");
5975
+ const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
4814
5976
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
4815
5977
  if (versionMatch) schemaVersion = versionMatch[1];
4816
5978
  } catch {
@@ -4870,8 +6032,8 @@ async function runStatus(input) {
4870
6032
  }
4871
6033
 
4872
6034
  // src/commands/seed.ts
4873
- import { mkdir as mkdir9, writeFile as writeFile7, stat as stat4 } from "fs/promises";
4874
- import { join as join20 } from "path";
6035
+ import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
6036
+ import { join as join30 } from "path";
4875
6037
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4876
6038
  var EXAMPLE_PAGES = {
4877
6039
  "entities/example-project.md": `---
@@ -4940,30 +6102,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
4940
6102
  `;
4941
6103
  async function runSeed(input) {
4942
6104
  try {
4943
- await stat4(join20(input.vault, "SCHEMA.md"));
6105
+ await stat4(join30(input.vault, "SCHEMA.md"));
4944
6106
  } catch {
4945
6107
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
4946
6108
  }
4947
6109
  const created = [];
4948
6110
  const skipped = [];
4949
6111
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
4950
- const absPath = join20(input.vault, relPath);
6112
+ const absPath = join30(input.vault, relPath);
4951
6113
  try {
4952
6114
  await stat4(absPath);
4953
6115
  skipped.push(relPath);
4954
6116
  } catch {
4955
- await mkdir9(join20(absPath, ".."), { recursive: true });
4956
- await writeFile7(absPath, content, "utf8");
6117
+ await mkdir11(join30(absPath, ".."), { recursive: true });
6118
+ await writeFile9(absPath, content, "utf8");
4957
6119
  created.push(relPath);
4958
6120
  }
4959
6121
  }
4960
- const rawPath = join20(input.vault, "raw", "articles", "example-source.md");
6122
+ const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
4961
6123
  try {
4962
6124
  await stat4(rawPath);
4963
6125
  skipped.push("raw/articles/example-source.md");
4964
6126
  } catch {
4965
- await mkdir9(join20(rawPath, ".."), { recursive: true });
4966
- await writeFile7(rawPath, EXAMPLE_RAW, "utf8");
6127
+ await mkdir11(join30(rawPath, ".."), { recursive: true });
6128
+ await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
4967
6129
  created.push("raw/articles/example-source.md");
4968
6130
  }
4969
6131
  if (created.length > 0) {
@@ -4985,9 +6147,9 @@ async function runSeed(input) {
4985
6147
  }
4986
6148
 
4987
6149
  // src/commands/canvas.ts
4988
- import { readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
4989
- import { existsSync as existsSync7 } from "fs";
4990
- import { join as join21 } from "path";
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";
4991
6153
  var NODE_WIDTH = 240;
4992
6154
  var NODE_HEIGHT = 60;
4993
6155
  var COLUMN_SPACING = 400;
@@ -5065,8 +6227,8 @@ function buildCanvasEdges(adjacency) {
5065
6227
  return edges;
5066
6228
  }
5067
6229
  async function runCanvasGenerate(input) {
5068
- const graphPath = input.graphPath ?? join21(input.vault, ".skillwiki", "graph.json");
5069
- if (!existsSync7(graphPath)) {
6230
+ const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
6231
+ if (!existsSync8(graphPath)) {
5070
6232
  return {
5071
6233
  exitCode: ExitCode.FILE_NOT_FOUND,
5072
6234
  result: err("FILE_NOT_FOUND", {
@@ -5077,7 +6239,7 @@ async function runCanvasGenerate(input) {
5077
6239
  }
5078
6240
  let raw;
5079
6241
  try {
5080
- raw = await readFile13(graphPath, "utf8");
6242
+ raw = await readFile16(graphPath, "utf8");
5081
6243
  } catch (e) {
5082
6244
  return {
5083
6245
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5103,9 +6265,9 @@ async function runCanvasGenerate(input) {
5103
6265
  const nodes = buildCanvasNodes(paths);
5104
6266
  const edges = buildCanvasEdges(graph.adjacency);
5105
6267
  const canvas = { nodes, edges };
5106
- const outPath = join21(input.vault, "vault-graph.canvas");
6268
+ const outPath = join31(input.vault, "vault-graph.canvas");
5107
6269
  try {
5108
- await writeFile8(outPath, JSON.stringify(canvas, null, 2));
6270
+ await writeFile10(outPath, JSON.stringify(canvas, null, 2));
5109
6271
  } catch (e) {
5110
6272
  return {
5111
6273
  exitCode: ExitCode.WRITE_FAILED,
@@ -5125,15 +6287,15 @@ written: ${outPath}`
5125
6287
  }
5126
6288
 
5127
6289
  // src/commands/fleet-health.ts
5128
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
6290
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
5129
6291
  import { execSync as nodeExecSync } from "child_process";
5130
6292
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
5131
- import { join as join22 } from "path";
6293
+ import { join as join32 } from "path";
5132
6294
  var SSH_TIMEOUT_MS = 15e3;
5133
6295
  var TIMER_UNIT = "agent-memory-trends.timer";
5134
6296
  var SERVICE_UNIT = "agent-memory-trends.service";
5135
6297
  var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
5136
- function defaultDeps() {
6298
+ function defaultDeps2() {
5137
6299
  return {
5138
6300
  platform: () => nodePlatform(),
5139
6301
  execSync: nodeExecSync
@@ -5225,9 +6387,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
5225
6387
  function probeLocal(vaultPath, deps) {
5226
6388
  const latestPath = satelliteLatestRunPath(vaultPath);
5227
6389
  let parsed = null;
5228
- if (existsSync8(latestPath)) {
6390
+ if (existsSync9(latestPath)) {
5229
6391
  try {
5230
- const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
6392
+ const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
5231
6393
  if (wire) {
5232
6394
  parsed = {
5233
6395
  status: wire.status,
@@ -5353,12 +6515,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
5353
6515
  return true;
5354
6516
  }
5355
6517
  async function runFleetHealth(input) {
5356
- const deps = input.deps ?? defaultDeps();
6518
+ const deps = input.deps ?? defaultDeps2();
5357
6519
  const env = input.env ?? process.env;
5358
6520
  const home = input.home ?? env.HOME ?? "";
5359
6521
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
5360
6522
  const vault = input.vault ?? env.WIKI_PATH;
5361
- const file = input.file ?? (vault ? join22(vault, FLEET_REL_PATH) : void 0);
6523
+ const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
5362
6524
  if (!file) {
5363
6525
  return {
5364
6526
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -5442,15 +6604,15 @@ async function runFleetHealth(input) {
5442
6604
  }
5443
6605
 
5444
6606
  // src/utils/auto-commit.ts
5445
- import { existsSync as existsSync9 } from "fs";
5446
- import { join as join23 } from "path";
6607
+ import { existsSync as existsSync10 } from "fs";
6608
+ import { join as join33 } from "path";
5447
6609
  async function postCommit(vault, exitCode) {
5448
6610
  if (exitCode !== 0) return;
5449
6611
  const home = process.env.HOME ?? "";
5450
6612
  const dotenv = await parseDotenvFile(configPath(home));
5451
6613
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
5452
6614
  if (autoCommit === "false") return;
5453
- if (!existsSync9(join23(vault, ".git"))) return;
6615
+ if (!existsSync10(join33(vault, ".git"))) return;
5454
6616
  const lastOps = readLastOp(vault);
5455
6617
  if (lastOps.length === 0) return;
5456
6618
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -5473,103 +6635,6 @@ async function postCommit(vault, exitCode) {
5473
6635
  clearLastOp(vault);
5474
6636
  }
5475
6637
 
5476
- // src/utils/protected-vault-write-guard.ts
5477
- import { readFileSync as readFileSync6 } from "fs";
5478
- import { join as join24, resolve as resolvePath } from "path";
5479
- async function guardProtectedVaultWrite(input) {
5480
- const env = input.env ?? process.env;
5481
- const home = input.home ?? process.env.HOME ?? "";
5482
- const cwd = input.cwd ?? process.cwd();
5483
- const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
5484
- const load = await loadFleetManifestAndHost({
5485
- vault: liveVaultPath ?? input.vault,
5486
- hostId: input.hostId,
5487
- env,
5488
- home,
5489
- cwd,
5490
- osHostname: input.osHostname ?? process.env.HOSTNAME,
5491
- user: input.user ?? process.env.USER
5492
- });
5493
- if (!load?.hostId || load.identityStatus !== "known") {
5494
- return { blocked: false };
5495
- }
5496
- const host = load.manifest.hosts[load.hostId];
5497
- if (!host || host.role !== "snapshotter" || host.protected !== true) {
5498
- return { blocked: false };
5499
- }
5500
- const snapshotWorktree = resolveSnapshotWorktree(home);
5501
- const targetVault = resolvePath(input.vault);
5502
- const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
5503
- const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
5504
- if (canonicalLiveVault && targetVault === canonicalLiveVault) {
5505
- return { blocked: false };
5506
- }
5507
- if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
5508
- return {
5509
- blocked: true,
5510
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5511
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5512
- host_id: load.hostId,
5513
- command: input.command,
5514
- reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
5515
- 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."
5516
- })
5517
- };
5518
- }
5519
- if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
5520
- return {
5521
- blocked: true,
5522
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5523
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5524
- host_id: load.hostId,
5525
- command: input.command,
5526
- reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
5527
- guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
5528
- })
5529
- };
5530
- }
5531
- return { blocked: false };
5532
- }
5533
- async function resolveLiveVaultPath(input) {
5534
- const resolved = await resolveRuntimePath({
5535
- flag: void 0,
5536
- envValue: input.env.WIKI_PATH,
5537
- wikiEnv: input.env.WIKI,
5538
- home: input.home,
5539
- cwd: input.cwd
5540
- });
5541
- return resolved.ok ? resolved.data.path : void 0;
5542
- }
5543
- function resolveSnapshotWorktree(home) {
5544
- const skillwikiEnv = join24(home, ".skillwiki", ".env");
5545
- const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
5546
- if (explicitWorktree) return explicitWorktree;
5547
- const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
5548
- if (snapshotProfile) {
5549
- const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
5550
- if (fromProfile) return fromProfile;
5551
- }
5552
- return "/root/wiki-git";
5553
- }
5554
- function readEnvKey(path, keys) {
5555
- try {
5556
- const content = readFileSync6(path, "utf8");
5557
- for (const line of content.split(/\r?\n/)) {
5558
- const trimmed = line.trim();
5559
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
5560
- const eq = trimmed.indexOf("=");
5561
- if (eq <= 0) continue;
5562
- const key = trimmed.slice(0, eq).trim();
5563
- if (!keys.includes(key)) continue;
5564
- const value = trimmed.slice(eq + 1).trim();
5565
- if (value.length > 0) return value;
5566
- }
5567
- } catch {
5568
- return void 0;
5569
- }
5570
- return void 0;
5571
- }
5572
-
5573
6638
  // src/cli.ts
5574
6639
  var pkg = readCliPackageJson();
5575
6640
  var program = new Command();
@@ -5596,6 +6661,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
5596
6661
  }
5597
6662
  return emit(await run(), vault, opts);
5598
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
+ }
5599
6686
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
5600
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 })));
5601
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) => {
@@ -5606,12 +6693,12 @@ program.command("validate <file>").description("validate vault page frontmatter
5606
6693
  else vault = v.vault;
5607
6694
  }
5608
6695
  if (opts.apply && vault) {
5609
- 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);
5610
6697
  }
5611
6698
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
5612
6699
  });
5613
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) => {
5614
- const out = opts.out ?? join25(vault, ".skillwiki", "graph.json");
6701
+ const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
5615
6702
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
5616
6703
  });
5617
6704
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -5744,6 +6831,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
5744
6831
  { postCommit: false }
5745
6832
  );
5746
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
+ });
5747
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) => {
5748
6841
  const v = await resolveVaultArg(vault, opts.wiki);
5749
6842
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -5796,12 +6889,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
5796
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) => {
5797
6890
  const v = await resolveVaultArg(vault, opts.wiki);
5798
6891
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5799
- else return emitGuardedVaultWrite(
6892
+ else return emitManagedVaultWrite(
5800
6893
  v.vault,
5801
6894
  "log-append",
5802
6895
  () => runLogAppend({ vault: v.vault, content: opts.content })
5803
6896
  );
5804
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
+ });
5805
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) => {
5806
6934
  const v = await resolveVaultArg(vault, opts.wiki);
5807
6935
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -5896,7 +7024,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
5896
7024
  remoteDelete: !!opts.remoteDelete,
5897
7025
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
5898
7026
  }), v.vault);
5899
- else return emitGuardedVaultWrite(
7027
+ else return emitManagedVaultWrite(
5900
7028
  v.vault,
5901
7029
  "archive",
5902
7030
  () => runArchive({
@@ -5910,6 +7038,22 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
5910
7038
  })
5911
7039
  );
5912
7040
  });
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) => {
7042
+ const v = await resolveVaultArg(vault, opts.wiki);
7043
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7044
+ else return emitManagedVaultWrite(
7045
+ v.vault,
7046
+ "remove",
7047
+ () => runRemove({
7048
+ vault: v.vault,
7049
+ page,
7050
+ remote: opts.remote,
7051
+ remoteDelete: !!opts.remoteDelete,
7052
+ maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10),
7053
+ reason: opts.reason
7054
+ })
7055
+ );
7056
+ });
5913
7057
  program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5914
7058
  const v = await resolveVaultArg(vault, opts.wiki);
5915
7059
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6070,6 +7214,17 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
6070
7214
  () => runSyncPull({ vault: v.vault })
6071
7215
  );
6072
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
+ });
6073
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) => {
6074
7229
  const v = await resolveVaultArg(vault, opts.wiki);
6075
7230
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6169,7 +7324,7 @@ program.command("observe [vault]").description("create a raw transcript observat
6169
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) => {
6170
7325
  const v = await resolveVaultArg(vault, opts.wiki);
6171
7326
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6172
- else if (opts.write) return emitGuardedVaultWrite(
7327
+ else if (opts.write) return emitManagedVaultWrite(
6173
7328
  v.vault,
6174
7329
  "session-brief --write",
6175
7330
  () => runSessionBrief({