skillwiki 0.9.63 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/chunk-C5OLZRRM.js +357 -0
  2. package/dist/chunk-IZABIE44.js +647 -0
  3. package/dist/chunk-R6BKJWVC.js +890 -0
  4. package/dist/chunk-SCGC7YNM.js +213 -0
  5. package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
  6. package/dist/cli.js +1780 -697
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-PW4OOOMV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +24 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +24 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
package/dist/cli.js CHANGED
@@ -1,53 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ExitCode,
4
- FLEET_REL_PATH,
5
- MetaSchema,
6
- RawSourceSchema,
7
3
  SATELLITE_STALE_MS,
8
4
  acquireLock,
9
5
  acquireOwnedSyncLock,
10
6
  appendLastOp,
11
- assertTargetInsideVault,
12
7
  assessSourceIdentity,
13
- atomicWriteText,
14
8
  buildDegradedReasons,
15
9
  buildRemoteObjectPath,
16
10
  clearLastOp,
17
11
  configPath,
18
- err,
19
12
  evaluateSatelliteRunHealth,
20
13
  extractCitationMarkers,
21
- extractFrontmatter,
22
14
  extractTaxonomy,
23
15
  findPlugin,
24
16
  fixPathTooLong,
25
17
  getCliSessionId,
26
18
  getSessionId,
27
- isBlockedHost,
28
19
  isFailedRunStatus,
29
20
  isValidRemoteDeleteCap,
30
- loadFleetManifest,
31
- loadFleetManifestAndHost,
21
+ mergeTaxonomyConflict,
32
22
  normalizeRemoteRoot,
33
- ok,
34
- parseDotenvFile,
35
- parseDotenvText,
36
23
  planAndMaybePruneRemoteObjects,
37
- prepareTypedPage,
38
24
  probeRemoteHealth,
39
- profileKey,
40
25
  readCliPackageJson,
41
26
  readLastOp,
42
27
  readLock,
43
- readPage,
44
28
  readSatelliteLatestRunFromText,
45
29
  reconcileTaxonomyDocument,
46
- redactSensitiveContent,
47
30
  releaseLock,
48
31
  releaseOwnedSyncLock,
49
32
  renderIndexUpsert,
50
- resolveFleetHostId,
33
+ renderProjectIndex,
51
34
  resolveInitTimePath,
52
35
  resolveRuntimePath,
53
36
  runAudit,
@@ -57,8 +40,6 @@ import {
57
40
  runConfigSet,
58
41
  runDedup,
59
42
  runDoctor,
60
- runFleetContext,
61
- runFleetValidate,
62
43
  runFrontmatterFix,
63
44
  runGraphBuild,
64
45
  runIndexCheck,
@@ -85,14 +66,9 @@ import {
85
66
  runValidate,
86
67
  safeWritePage,
87
68
  satelliteLatestRunPath,
88
- scanSensitiveContent,
89
- scanVault,
90
- snapshotterAliasForLocalHost,
91
- splitFrontmatter,
92
69
  taxonomyCommentForPage,
93
- upsertIndexEntry,
94
- writeDotenv
95
- } from "./chunk-TUFQZ5K4.js";
70
+ upsertIndexEntry
71
+ } from "./chunk-XSTXPA34.js";
96
72
  import {
97
73
  normalizeDistTag,
98
74
  readCache,
@@ -100,9 +76,56 @@ 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
+ releaseManagedWriteLock,
95
+ runManagedWritePreflight,
96
+ runManagedWriteTransaction
97
+ } from "./chunk-SCGC7YNM.js";
98
+ import {
99
+ FLEET_REL_PATH,
100
+ git,
101
+ gitStrict,
102
+ listJournalOpIds,
103
+ listReviewRequiredOps,
104
+ loadFleetManifest,
105
+ loadFleetManifestAndHost,
106
+ parseDotenvFile,
107
+ parseDotenvText,
108
+ profileKey,
109
+ readJournal,
110
+ resolveFleetHostId,
111
+ runFleetContext,
112
+ runFleetValidate,
113
+ runVaultSyncPullHelper,
114
+ snapshotterAliasForLocalHost,
115
+ supersedeStaleReviewRequiredJournals,
116
+ writeDotenv
117
+ } from "./chunk-R6BKJWVC.js";
118
+ import {
119
+ ExitCode,
120
+ MetaSchema,
121
+ RawSourceSchema,
122
+ err,
123
+ isBlockedHost,
124
+ ok
125
+ } from "./chunk-C5OLZRRM.js";
103
126
 
104
127
  // src/cli.ts
105
- import { join as join27 } from "path";
128
+ import { join as join34 } from "path";
106
129
  import { Command } from "commander";
107
130
 
108
131
  // src/utils/output.ts
@@ -597,183 +620,1182 @@ async function runInit(input) {
597
620
  oldSchemaText = await readFile3(join5(target, "SCHEMA.md"), "utf8");
598
621
  } catch {
599
622
  }
600
- if (oldSchemaText && !input.force) {
623
+ if (oldSchemaText && !input.force) {
624
+ return {
625
+ exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
626
+ result: err("INIT_TARGET_NOT_EMPTY", { target })
627
+ };
628
+ }
629
+ const envPath = join5(input.home, ".skillwiki", ".env");
630
+ let existingEnvRaw = "";
631
+ try {
632
+ existingEnvRaw = await readFile3(envPath, "utf8");
633
+ } catch {
634
+ }
635
+ const existingEnv = parseDotenvText(existingEnvRaw);
636
+ const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
637
+ const explicitTarget = !!input.flag;
638
+ const skipConflictCheck = explicitTarget || !!input.noEnv;
639
+ if (!input.profile && !skipConflictCheck && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
640
+ return {
641
+ exitCode: ExitCode.ENV_WRITE_CONFLICT,
642
+ result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
643
+ };
644
+ }
645
+ if (!input.profile && !skipConflictCheck && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
646
+ return {
647
+ exitCode: ExitCode.ENV_WRITE_CONFLICT,
648
+ result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
649
+ };
650
+ }
651
+ const created = [];
652
+ try {
653
+ await mkdir3(target, { recursive: true });
654
+ for (const d of VAULT_DIRS) {
655
+ await mkdir3(join5(target, d), { recursive: true });
656
+ created.push(d + "/");
657
+ }
658
+ } catch (e) {
659
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
660
+ }
661
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
662
+ let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
663
+ let domain = input.domain;
664
+ let oldTaxonomy = [];
665
+ if (oldSchemaText) {
666
+ if (!domain) {
667
+ const oldDomain = extractDomainFromSchema(oldSchemaText);
668
+ if (oldDomain) domain = oldDomain;
669
+ }
670
+ const oldTax = extractTaxonomy(oldSchemaText);
671
+ if (oldTax.ok) oldTaxonomy = oldTax.data;
672
+ }
673
+ const taxonomySet = new Set(taxonomy);
674
+ for (const t of oldTaxonomy) {
675
+ if (!taxonomySet.has(t)) {
676
+ taxonomy.push(t);
677
+ taxonomySet.add(t);
678
+ }
679
+ }
680
+ const discovered = await discoverTagsFromPages(target, taxonomy);
681
+ const discovered_tags = discovered.length;
682
+ 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");
683
+ try {
684
+ const schemaTpl = await readFile3(join5(input.templates, "SCHEMA.md"), "utf8");
685
+ const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
686
+ await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
687
+ created.push("SCHEMA.md");
688
+ } catch (e) {
689
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
690
+ }
691
+ const preserved = [];
692
+ async function writeOrPreserve(fileName, render) {
693
+ try {
694
+ const existing = await readFile3(join5(target, fileName), "utf8");
695
+ if (existing.split("\n").length > 10) {
696
+ preserved.push(fileName);
697
+ return void 0;
698
+ }
699
+ } catch {
700
+ }
701
+ try {
702
+ await writeFile2(join5(target, fileName), await render(), "utf8");
703
+ created.push(fileName);
704
+ return void 0;
705
+ } catch (e) {
706
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
707
+ }
708
+ }
709
+ const err1 = await writeOrPreserve("index.md", async () => {
710
+ const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
711
+ return tpl.replace("{{INIT_DATE}}", today);
712
+ });
713
+ if (err1) return err1;
714
+ const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
715
+ return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
716
+ });
717
+ if (errObsidian) return errObsidian;
718
+ const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
719
+ return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
720
+ });
721
+ if (errTemplatesJson) return errTemplatesJson;
722
+ const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
723
+ return [
724
+ "---",
725
+ "source_url:",
726
+ "created: {{date:YYYY-MM-DD}}",
727
+ "ingested: # filled by ingest pipeline",
728
+ "kind: # idea | bug | task | note | other",
729
+ 'project: # optional: "[[slug]]"',
730
+ "---",
731
+ "",
732
+ ""
733
+ ].join("\n");
734
+ });
735
+ if (errTemplate) return errTemplate;
736
+ const err22 = await writeOrPreserve("log.md", async () => {
737
+ const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
738
+ return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
739
+ });
740
+ if (err22) return err22;
741
+ const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
742
+ let envWritten = "";
743
+ if (!skipEnv) {
744
+ try {
745
+ const envEntries = {};
746
+ if (input.profile) {
747
+ envEntries[profileKey(input.profile, "PATH")] = target;
748
+ envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
749
+ envEntries["WIKI_DEFAULT"] = input.profile;
750
+ } else {
751
+ envEntries["WIKI_PATH"] = target;
752
+ envEntries["WIKI_LANG"] = canonicalLang;
753
+ }
754
+ await writeDotenv(envPath, envEntries, existingEnvRaw);
755
+ envWritten = envPath;
756
+ } catch (e) {
757
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
758
+ }
759
+ }
760
+ const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
761
+ const humanHint = [
762
+ `vault: ${target}`,
763
+ `domain: ${domain}`,
764
+ `lang: ${canonicalLang}`,
765
+ `created: ${created.length}, preserved: ${preserved.length}`,
766
+ `discovered tags: ${discovered_tags}`,
767
+ skipEnv ? "env: skipped" : `env: ${envWritten}`
768
+ ].join("\n");
769
+ if (created.length > 0) {
770
+ appendLastOp(target, {
771
+ operation: "init",
772
+ summary: `initialized vault: ${domain}`,
773
+ files: created,
774
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
775
+ });
776
+ }
777
+ return {
778
+ exitCode: ExitCode.OK,
779
+ result: ok({
780
+ vault: target,
781
+ domain,
782
+ taxonomy,
783
+ lang: canonicalLang,
784
+ created,
785
+ preserved,
786
+ env_written: envWritten,
787
+ env_skipped: skipEnv,
788
+ imported_from_hermes: importedFromHermes,
789
+ discovered_tags,
790
+ humanHint,
791
+ templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
792
+ })
793
+ };
794
+ }
795
+
796
+ // src/commands/index-rebuild.ts
797
+ import { readFileSync as readFileSync3 } from "fs";
798
+ import { join as join7 } from "path";
799
+
800
+ // src/utils/protected-vault-write-guard.ts
801
+ import { readFileSync as readFileSync2 } from "fs";
802
+ import { join as join6, resolve as resolvePath } from "path";
803
+ async function guardProtectedVaultWrite(input) {
804
+ const env = input.env ?? process.env;
805
+ const home = input.home ?? process.env.HOME ?? "";
806
+ const cwd = input.cwd ?? process.cwd();
807
+ const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
808
+ const load = await loadFleetManifestAndHost({
809
+ vault: liveVaultPath ?? input.vault,
810
+ hostId: input.hostId,
811
+ env,
812
+ home,
813
+ cwd,
814
+ osHostname: input.osHostname ?? process.env.HOSTNAME,
815
+ user: input.user ?? process.env.USER
816
+ });
817
+ if (!load?.hostId || load.identityStatus !== "known") {
818
+ return { blocked: false };
819
+ }
820
+ const host = load.manifest.hosts[load.hostId];
821
+ if (!host || host.role !== "snapshotter" || host.protected !== true) {
822
+ return { blocked: false };
823
+ }
824
+ const snapshotWorktree = resolveSnapshotWorktree(home);
825
+ const targetVault = resolvePath(input.vault);
826
+ const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
827
+ const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
828
+ if (canonicalLiveVault && targetVault === canonicalLiveVault) {
829
+ return { blocked: false };
830
+ }
831
+ if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
832
+ return {
833
+ blocked: true,
834
+ exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
835
+ result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
836
+ host_id: load.hostId,
837
+ command: input.command,
838
+ reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
839
+ 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."
840
+ })
841
+ };
842
+ }
843
+ if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
844
+ return {
845
+ blocked: true,
846
+ exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
847
+ result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
848
+ host_id: load.hostId,
849
+ command: input.command,
850
+ reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
851
+ guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
852
+ })
853
+ };
854
+ }
855
+ return { blocked: false };
856
+ }
857
+ async function resolveLiveVaultPath(input) {
858
+ const resolved = await resolveRuntimePath({
859
+ flag: void 0,
860
+ envValue: input.env.WIKI_PATH,
861
+ wikiEnv: input.env.WIKI,
862
+ home: input.home,
863
+ cwd: input.cwd
864
+ });
865
+ return resolved.ok ? resolved.data.path : void 0;
866
+ }
867
+ function resolveSnapshotWorktree(home) {
868
+ const skillwikiEnv = join6(home, ".skillwiki", ".env");
869
+ const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
870
+ if (explicitWorktree) return explicitWorktree;
871
+ const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
872
+ if (snapshotProfile) {
873
+ const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
874
+ if (fromProfile) return fromProfile;
875
+ }
876
+ return "/root/wiki-git";
877
+ }
878
+ function readEnvKey(path, keys) {
879
+ try {
880
+ const content = readFileSync2(path, "utf8");
881
+ for (const line of content.split(/\r?\n/)) {
882
+ const trimmed = line.trim();
883
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
884
+ const eq = trimmed.indexOf("=");
885
+ if (eq <= 0) continue;
886
+ const key = trimmed.slice(0, eq).trim();
887
+ if (!keys.includes(key)) continue;
888
+ const value = trimmed.slice(eq + 1).trim();
889
+ if (value.length > 0) return value;
890
+ }
891
+ } catch {
892
+ return void 0;
893
+ }
894
+ return void 0;
895
+ }
896
+
897
+ // src/commands/index-rebuild.ts
898
+ async function runIndexRebuild(input) {
899
+ const projection = await renderRootIndex({ vault: input.vault });
900
+ if (!projection.ok) {
901
+ const code = projection.error === "SCHEME_REJECTED" ? ExitCode.SCHEME_REJECTED : ExitCode.VAULT_PATH_INVALID;
902
+ return { exitCode: code, result: projection };
903
+ }
904
+ let current = "";
905
+ try {
906
+ current = readFileSync3(join7(input.vault, "index.md"), "utf8");
907
+ } catch {
908
+ current = "";
909
+ }
910
+ const changed = current !== projection.data.text;
911
+ if (!input.write) {
912
+ return {
913
+ exitCode: ExitCode.OK,
914
+ result: ok({
915
+ changed,
916
+ dry_run: true,
917
+ entry_count: projection.data.entries.length,
918
+ duplicates_removed: projection.data.duplicates_removed,
919
+ ghosts_removed: projection.data.ghosts_removed,
920
+ humanHint: changed ? `dry run: would rewrite index.md (${projection.data.entries.length} entries)` : `dry run: index.md already canonical (${projection.data.entries.length} entries)`
921
+ })
922
+ };
923
+ }
924
+ const guard = await guardProtectedVaultWrite({
925
+ vault: input.vault,
926
+ command: "index rebuild"
927
+ });
928
+ if (guard.blocked) {
929
+ return { exitCode: guard.exitCode, result: guard.result };
930
+ }
931
+ return runManagedWriteTransaction({
932
+ vault: input.vault,
933
+ command: "index rebuild",
934
+ allowImmutableRecord: false,
935
+ mutate: async () => {
936
+ if (!changed) {
937
+ return {
938
+ exitCode: ExitCode.OK,
939
+ result: ok({
940
+ changed: false,
941
+ dry_run: false,
942
+ entry_count: projection.data.entries.length,
943
+ duplicates_removed: projection.data.duplicates_removed,
944
+ ghosts_removed: projection.data.ghosts_removed,
945
+ humanHint: `index.md already canonical (${projection.data.entries.length} entries)`
946
+ })
947
+ };
948
+ }
949
+ const written = await writeRootIndexProjection(input.vault, projection.data);
950
+ if (!written.ok) {
951
+ return { exitCode: ExitCode.WRITE_FAILED, result: written };
952
+ }
953
+ let installed = "";
954
+ try {
955
+ installed = readFileSync3(join7(input.vault, "index.md"), "utf8");
956
+ } catch (error) {
957
+ return {
958
+ exitCode: ExitCode.WRITE_FAILED,
959
+ result: err("WRITE_FAILED", { message: String(error) })
960
+ };
961
+ }
962
+ if (installed !== projection.data.text) {
963
+ return {
964
+ exitCode: ExitCode.WRITE_FAILED,
965
+ result: err("WRITE_FAILED", { message: "installed index.md differs from projection" })
966
+ };
967
+ }
968
+ return {
969
+ exitCode: ExitCode.OK,
970
+ result: ok({
971
+ changed: true,
972
+ dry_run: false,
973
+ entry_count: projection.data.entries.length,
974
+ duplicates_removed: projection.data.duplicates_removed,
975
+ ghosts_removed: projection.data.ghosts_removed,
976
+ humanHint: `rewrote index.md (${projection.data.entries.length} entries)`
977
+ })
978
+ };
979
+ }
980
+ });
981
+ }
982
+
983
+ // src/commands/derived-conflicts.ts
984
+ import { execFileSync as execFileSync2 } from "child_process";
985
+ import { existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
986
+ import { join as join9 } from "path";
987
+
988
+ // src/utils/log-merge.ts
989
+ import { execFileSync } from "child_process";
990
+ import { mkdtempSync, writeFileSync, rmSync } from "fs";
991
+ import { tmpdir } from "os";
992
+ import { join as join8 } from "path";
993
+ var ENTRY_RE = /^## \[/m;
994
+ var PUBLISH_MARKER_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
995
+ function splitLog(text) {
996
+ const normalized = text.replace(/\r\n/g, "\n");
997
+ const match = ENTRY_RE.exec(normalized);
998
+ if (!match || match.index === void 0) {
999
+ return { preamble: normalized.replace(/\n+$/, ""), entries: [] };
1000
+ }
1001
+ const preamble = normalized.slice(0, match.index).replace(/\n+$/, "");
1002
+ const body = normalized.slice(match.index);
1003
+ const parts = body.split(/(?=^## \[)/m).filter((p) => p.length > 0);
1004
+ const entries = parts.map((p) => p.replace(/\n+$/, ""));
1005
+ return { preamble, entries };
1006
+ }
1007
+ function publishId(entry) {
1008
+ return PUBLISH_MARKER_RE.exec(entry)?.[1]?.toLowerCase();
1009
+ }
1010
+ function unionUnstructured(base, ours, theirs) {
1011
+ const dir = mkdtempSync(join8(tmpdir(), "log-union-"));
1012
+ try {
1013
+ const basePath = join8(dir, "base");
1014
+ const oursPath = join8(dir, "ours");
1015
+ const theirsPath = join8(dir, "theirs");
1016
+ writeFileSync(basePath, base.endsWith("\n") || base.length === 0 ? base : `${base}
1017
+ `);
1018
+ writeFileSync(oursPath, ours.endsWith("\n") || ours.length === 0 ? ours : `${ours}
1019
+ `);
1020
+ writeFileSync(theirsPath, theirs.endsWith("\n") || theirs.length === 0 ? theirs : `${theirs}
1021
+ `);
1022
+ const text = execFileSync("git", ["merge-file", "--union", "-p", oursPath, basePath, theirsPath], {
1023
+ encoding: "utf8"
1024
+ });
1025
+ return ok(text.endsWith("\n") ? text : `${text}
1026
+ `);
1027
+ } catch (error) {
1028
+ return err("WRITE_FAILED", { message: `unstructured log union failed: ${String(error)}` });
1029
+ } finally {
1030
+ rmSync(dir, { recursive: true, force: true });
1031
+ }
1032
+ }
1033
+ function mergeLogConflictStages(input) {
1034
+ const ours = splitLog(input.ours);
1035
+ const theirs = splitLog(input.theirs);
1036
+ const baseSplit = splitLog(input.base);
1037
+ if (ours.entries.length === 0 && theirs.entries.length === 0) {
1038
+ const unioned = unionUnstructured(input.base, input.ours, input.theirs);
1039
+ if (!unioned.ok) return unioned;
1040
+ return ok({ text: unioned.data, deduplicated_operation_ids: [] });
1041
+ }
1042
+ const preamble = ours.preamble || theirs.preamble || baseSplit.preamble || "# Log";
1043
+ const seenMarkers = /* @__PURE__ */ new Set();
1044
+ const seenExact = /* @__PURE__ */ new Set();
1045
+ const out = [];
1046
+ const deduped = [];
1047
+ const push = (entry) => {
1048
+ const marker2 = publishId(entry);
1049
+ if (marker2) {
1050
+ if (seenMarkers.has(marker2)) {
1051
+ deduped.push(marker2);
1052
+ return;
1053
+ }
1054
+ seenMarkers.add(marker2);
1055
+ } else if (seenExact.has(entry)) {
1056
+ return;
1057
+ } else {
1058
+ seenExact.add(entry);
1059
+ }
1060
+ out.push(entry);
1061
+ };
1062
+ for (const e of ours.entries) push(e);
1063
+ for (const e of theirs.entries) push(e);
1064
+ let text = preamble;
1065
+ if (out.length > 0) {
1066
+ text = `${preamble.replace(/\n+$/, "")}
1067
+
1068
+ ${out.join("\n\n")}`;
1069
+ }
1070
+ if (!text.endsWith("\n")) text += "\n";
1071
+ return ok({ text, deduplicated_operation_ids: deduped });
1072
+ }
1073
+
1074
+ // src/commands/derived-conflicts.ts
1075
+ function classifyDerivedPath(path) {
1076
+ if (path === "index.md") return "root-index";
1077
+ if (path === "SCHEMA.md") return "taxonomy";
1078
+ if (path === "log.md" || path.endsWith("/log.md")) return "log";
1079
+ if (/^projects\/[^/]+\/knowledge\.md$/.test(path)) return "project-index";
1080
+ return "unknown";
1081
+ }
1082
+ function journalPath(vault, operationId2) {
1083
+ const gitPath = git(vault, ["rev-parse", "--git-path", `vault-sync/operations/${operationId2}.env`]);
1084
+ if (!gitPath) return null;
1085
+ return gitPath.startsWith("/") ? gitPath : join9(vault, gitPath);
1086
+ }
1087
+ function readJournalFields(path) {
1088
+ const text = readFileSync5(path, "utf8");
1089
+ const out = {};
1090
+ for (const line of text.split("\n")) {
1091
+ const i = line.indexOf("=");
1092
+ if (i <= 0) continue;
1093
+ out[line.slice(0, i)] = line.slice(i + 1);
1094
+ }
1095
+ return out;
1096
+ }
1097
+ function showStage(vault, stage, path) {
1098
+ try {
1099
+ return execFileSync2("git", ["show", `:${stage}:${path}`], {
1100
+ cwd: vault,
1101
+ encoding: "utf8",
1102
+ stdio: ["pipe", "pipe", "pipe"]
1103
+ });
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function stageOid(vault, stage, path) {
1109
+ return git(vault, ["rev-parse", `:${stage}:${path}`]);
1110
+ }
1111
+ function unmergedPaths(vault) {
1112
+ const raw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
1113
+ return raw ? raw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
1114
+ }
1115
+ function verifyOperation(vault, operationId2) {
1116
+ const path = journalPath(vault, operationId2);
1117
+ if (!path || !existsSync(path)) {
1118
+ return err("PREFLIGHT_FAILED", { reason: "missing-operation-journal", operation_id: operationId2 });
1119
+ }
1120
+ const fields = readJournalFields(path);
1121
+ if (fields.operation_id !== operationId2) {
1122
+ return err("PREFLIGHT_FAILED", { reason: "operation-id-mismatch", operation_id: operationId2 });
1123
+ }
1124
+ if (fields.handoff === "1") {
1125
+ return err("PREFLIGHT_FAILED", { reason: "operation-handoff", operation_id: operationId2 });
1126
+ }
1127
+ const phase = fields.phase ?? "";
1128
+ if (phase !== "rebasing" && phase !== "restoring") {
1129
+ return err("PREFLIGHT_FAILED", { reason: "operation-phase", phase, operation_id: operationId2 });
1130
+ }
1131
+ const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
1132
+ const journalGitDir = fields.worktree_git_dir ?? "";
1133
+ if (journalGitDir && currentGitDir && journalGitDir !== currentGitDir) {
1134
+ return err("PREFLIGHT_FAILED", { reason: "worktree-mismatch", operation_id: operationId2 });
1135
+ }
1136
+ if (!journalGitDir && unmergedPaths(vault).length === 0) {
1137
+ return err("PREFLIGHT_FAILED", { reason: "legacy-journal-without-unmerged", operation_id: operationId2 });
1138
+ }
1139
+ return ok({ phase });
1140
+ }
1141
+ async function resolveOne(vault, path, klass) {
1142
+ if (klass === "root-index") {
1143
+ const rendered = await renderRootIndex({ vault });
1144
+ if (!rendered.ok) return rendered;
1145
+ return ok(rendered.data.text);
1146
+ }
1147
+ if (klass === "taxonomy") {
1148
+ const base = showStage(vault, 1, path) ?? "";
1149
+ const ours = showStage(vault, 2, path);
1150
+ const theirs = showStage(vault, 3, path);
1151
+ if (ours === null || theirs === null) {
1152
+ return err("WRITE_FAILED", { path, message: "missing taxonomy stages" });
1153
+ }
1154
+ const merged = mergeTaxonomyConflict(base, ours, theirs);
1155
+ if (!merged.ok) return merged;
1156
+ return ok(merged.data.text);
1157
+ }
1158
+ if (klass === "log") {
1159
+ const base = showStage(vault, 1, path) ?? "";
1160
+ const ours = showStage(vault, 2, path) ?? "";
1161
+ const theirs = showStage(vault, 3, path) ?? "";
1162
+ const merged = mergeLogConflictStages({ base, ours, theirs });
1163
+ if (!merged.ok) return merged;
1164
+ return ok(merged.data.text);
1165
+ }
1166
+ if (klass === "project-index") {
1167
+ const slug = path.replace(/^projects\//, "").replace(/\/knowledge\.md$/, "");
1168
+ const rendered = await renderProjectIndex(vault, slug);
1169
+ if (!rendered.ok) return rendered;
1170
+ return ok(rendered.data.text);
1171
+ }
1172
+ return err("SCHEME_REJECTED", { path, reason: "unknown-derived-class" });
1173
+ }
1174
+ function rollbackPath(vault, path, stage2, stage3) {
1175
+ try {
1176
+ execFileSync2("git", ["checkout", "--conflict=merge", "--", path], {
1177
+ cwd: vault,
1178
+ stdio: ["pipe", "pipe", "pipe"]
1179
+ });
1180
+ } catch {
1181
+ return false;
1182
+ }
1183
+ const s2 = stageOid(vault, 2, path);
1184
+ const s3 = stageOid(vault, 3, path);
1185
+ return s2 === stage2 && s3 === stage3;
1186
+ }
1187
+ function hasMarkers(path, text) {
1188
+ const lines = text.split(/\r?\n/);
1189
+ let open3 = false;
1190
+ let sep2 = false;
1191
+ for (const line of lines) {
1192
+ if (line.startsWith("<<<<<<< ")) {
1193
+ open3 = true;
1194
+ sep2 = false;
1195
+ continue;
1196
+ }
1197
+ if (line === "=======" && open3) {
1198
+ sep2 = true;
1199
+ continue;
1200
+ }
1201
+ if (line.startsWith(">>>>>>> ") && open3 && sep2) return true;
1202
+ if (line.startsWith(">>>>>>> ")) {
1203
+ open3 = false;
1204
+ sep2 = false;
1205
+ }
1206
+ }
1207
+ return false;
1208
+ }
1209
+ async function runDerivedConflictResolution(input) {
1210
+ const vault = input.vault;
1211
+ const verified = verifyOperation(vault, input.operationId);
1212
+ if (!verified.ok) {
1213
+ return {
1214
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1215
+ result: err("PREFLIGHT_FAILED", verified.detail)
1216
+ };
1217
+ }
1218
+ const paths = unmergedPaths(vault);
1219
+ if (paths.length === 0) {
1220
+ return {
1221
+ exitCode: ExitCode.OK,
1222
+ result: ok({
1223
+ resolved: true,
1224
+ resolved_paths: [],
1225
+ unknown_paths: [],
1226
+ rolled_back: false,
1227
+ humanHint: "no unmerged paths"
1228
+ })
1229
+ };
1230
+ }
1231
+ const classes = paths.map((p) => ({ path: p, klass: classifyDerivedPath(p) }));
1232
+ const unknown = classes.filter((c) => c.klass === "unknown").map((c) => c.path);
1233
+ if (unknown.length > 0) {
1234
+ return {
1235
+ exitCode: ExitCode.SYNC_PULL_FAILED,
1236
+ result: ok({
1237
+ resolved: false,
1238
+ resolved_paths: [],
1239
+ unknown_paths: unknown,
1240
+ rolled_back: false,
1241
+ humanHint: `review-required: unknown conflict paths ${unknown.join(", ")}`
1242
+ })
1243
+ };
1244
+ }
1245
+ const snapshots = /* @__PURE__ */ new Map();
1246
+ for (const { path } of classes) {
1247
+ snapshots.set(path, {
1248
+ stage2: stageOid(vault, 2, path),
1249
+ stage3: stageOid(vault, 3, path)
1250
+ });
1251
+ }
1252
+ const staged = [];
1253
+ try {
1254
+ for (const { path, klass } of classes) {
1255
+ const resolved = await resolveOne(vault, path, klass);
1256
+ if (!resolved.ok) throw new Error(`${path}: ${resolved.error}`);
1257
+ if (hasMarkers(path, resolved.data)) {
1258
+ throw new Error(`${path}: conflict markers remain after resolve`);
1259
+ }
1260
+ writeFileSync2(join9(vault, path), resolved.data, "utf8");
1261
+ execFileSync2("git", ["add", "--", path], { cwd: vault, stdio: ["pipe", "pipe", "pipe"] });
1262
+ staged.push(path);
1263
+ }
1264
+ const remaining = unmergedPaths(vault);
1265
+ if (remaining.length > 0) {
1266
+ throw new Error(`still unmerged: ${remaining.join(",")}`);
1267
+ }
1268
+ return {
1269
+ exitCode: ExitCode.OK,
1270
+ result: ok({
1271
+ resolved: true,
1272
+ resolved_paths: staged,
1273
+ unknown_paths: [],
1274
+ rolled_back: false,
1275
+ humanHint: `resolved ${staged.length} derived path(s)`
1276
+ })
1277
+ };
1278
+ } catch (error) {
1279
+ let rolled = true;
1280
+ for (const path of staged) {
1281
+ const snap = snapshots.get(path);
1282
+ if (!snap || !rollbackPath(vault, path, snap.stage2, snap.stage3)) {
1283
+ rolled = false;
1284
+ }
1285
+ }
1286
+ for (const { path } of classes) {
1287
+ if (staged.includes(path)) continue;
1288
+ const snap = snapshots.get(path);
1289
+ if (snap) rollbackPath(vault, path, snap.stage2, snap.stage3);
1290
+ }
1291
+ return {
1292
+ exitCode: ExitCode.SYNC_PULL_FAILED,
1293
+ result: ok({
1294
+ resolved: false,
1295
+ resolved_paths: [],
1296
+ unknown_paths: [],
1297
+ rolled_back: rolled,
1298
+ humanHint: `derived resolve failed: ${String(error)}`
1299
+ })
1300
+ };
1301
+ }
1302
+ }
1303
+
1304
+ // src/commands/log-materialize.ts
1305
+ import { readFileSync as readFileSync6 } from "fs";
1306
+ import { join as join11 } from "path";
1307
+
1308
+ // src/utils/log-events.ts
1309
+ import { mkdir as mkdir4, open, readFile as readFile4, readdir as readdir3 } from "fs/promises";
1310
+ import { join as join10 } from "path";
1311
+ function isPlainObject(value) {
1312
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1313
+ }
1314
+ function canonicalize(value) {
1315
+ if (Array.isArray(value)) return value.map(canonicalize);
1316
+ if (!isPlainObject(value)) return value;
1317
+ const out = {};
1318
+ for (const key of Object.keys(value).sort()) {
1319
+ out[key] = canonicalize(value[key]);
1320
+ }
1321
+ return out;
1322
+ }
1323
+ function eventPathFor(event) {
1324
+ const day2 = event.occurred_at.slice(0, 10);
1325
+ return `meta/log-events/${day2}/${event.operation_id}.json`;
1326
+ }
1327
+ function validateLogEvent(event) {
1328
+ if (event.schema !== "skillwiki-log-event/v1") {
1329
+ return err("SCHEME_REJECTED", { message: "invalid event schema" });
1330
+ }
1331
+ if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
1332
+ return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
1333
+ }
1334
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
1335
+ return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
1336
+ }
1337
+ for (const field of ["host_id", "actor", "kind", "target", "note"]) {
1338
+ const v = event[field];
1339
+ if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
1340
+ return err("SCHEME_REJECTED", { message: `invalid ${field}` });
1341
+ }
1342
+ }
1343
+ if (!isPlainObject(event.metadata)) {
1344
+ return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
1345
+ }
1346
+ const sensitive = scanSensitiveContent(JSON.stringify(event));
1347
+ if (sensitive.length > 0) {
1348
+ return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
1349
+ }
1350
+ return ok({
1351
+ schema: "skillwiki-log-event/v1",
1352
+ operation_id: event.operation_id,
1353
+ occurred_at: event.occurred_at,
1354
+ host_id: event.host_id,
1355
+ actor: event.actor,
1356
+ kind: event.kind,
1357
+ target: event.target,
1358
+ note: event.note,
1359
+ metadata: canonicalize(event.metadata)
1360
+ });
1361
+ }
1362
+ function canonicalEventJson(event) {
1363
+ const ordered = {
1364
+ schema: event.schema,
1365
+ operation_id: event.operation_id,
1366
+ occurred_at: event.occurred_at,
1367
+ host_id: event.host_id,
1368
+ actor: event.actor,
1369
+ kind: event.kind,
1370
+ target: event.target,
1371
+ note: event.note,
1372
+ metadata: event.metadata
1373
+ };
1374
+ return `${JSON.stringify(ordered, null, 2)}
1375
+ `;
1376
+ }
1377
+ async function writeLogEvent(vault, event) {
1378
+ const validated = validateLogEvent(event);
1379
+ if (!validated.ok) return validated;
1380
+ const rel = eventPathFor(validated.data);
1381
+ const abs = join10(vault, rel);
1382
+ await mkdir4(join10(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
1383
+ recursive: true
1384
+ });
1385
+ const body = canonicalEventJson(validated.data);
1386
+ try {
1387
+ const handle = await open(abs, "wx");
1388
+ try {
1389
+ await handle.writeFile(body, "utf8");
1390
+ } finally {
1391
+ await handle.close();
1392
+ }
1393
+ return ok({ path: rel, created: true });
1394
+ } catch (error) {
1395
+ if (error.code === "EEXIST") {
1396
+ let existing;
1397
+ try {
1398
+ existing = await readFile4(abs, "utf8");
1399
+ } catch (readErr) {
1400
+ return err("WRITE_FAILED", { path: rel, message: String(readErr) });
1401
+ }
1402
+ if (existing === body) return ok({ path: rel, created: false });
1403
+ return err("EVENT_IDENTITY_COLLISION", { path: rel });
1404
+ }
1405
+ return err("WRITE_FAILED", { path: rel, message: String(error) });
1406
+ }
1407
+ }
1408
+ async function readLogEvents(vault) {
1409
+ const root = join10(vault, "meta", "log-events");
1410
+ let days;
1411
+ try {
1412
+ days = (await readdir3(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1413
+ } catch {
1414
+ return ok([]);
1415
+ }
1416
+ const events = [];
1417
+ for (const day2 of days) {
1418
+ const files = (await readdir3(join10(root, day2))).filter((f) => f.endsWith(".json")).sort();
1419
+ for (const file of files) {
1420
+ const text = await readFile4(join10(root, day2, file), "utf8");
1421
+ let parsed;
1422
+ try {
1423
+ parsed = JSON.parse(text);
1424
+ } catch {
1425
+ return err("SCHEME_REJECTED", { path: `meta/log-events/${day2}/${file}`, message: "invalid JSON" });
1426
+ }
1427
+ const validated = validateLogEvent(parsed);
1428
+ if (!validated.ok) return validated;
1429
+ if (eventPathFor(validated.data) !== `meta/log-events/${day2}/${file}`) {
1430
+ return err("SCHEME_REJECTED", {
1431
+ path: `meta/log-events/${day2}/${file}`,
1432
+ message: "path/identity mismatch"
1433
+ });
1434
+ }
1435
+ events.push(validated.data);
1436
+ }
1437
+ }
1438
+ events.sort((a, b) => {
1439
+ if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
1440
+ return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
1441
+ });
1442
+ return ok(events);
1443
+ }
1444
+
1445
+ // src/utils/log-projection.ts
1446
+ function day(event) {
1447
+ return event.occurred_at.slice(0, 10);
1448
+ }
1449
+ function marker(event) {
1450
+ return `<!-- skillwiki-log-event:${event.operation_id} -->`;
1451
+ }
1452
+ function bodyFor(event) {
1453
+ if (event.kind === "page-publish") {
1454
+ const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
1455
+ return [
1456
+ `- Published: [[${event.target.replace(/\.md$/, "")}]]`,
1457
+ `- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
1458
+ event.note ? `- Note: ${event.note}` : ""
1459
+ ].filter(Boolean).join("\n");
1460
+ }
1461
+ if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
1462
+ const legacy = event.metadata.legacy_markdown.trimEnd();
1463
+ if (legacy.includes(marker(event))) return legacy;
1464
+ return `${legacy}
1465
+ ${marker(event)}`;
1466
+ }
1467
+ if (event.kind === "session-brief" || event.kind === "archive" || event.kind === "remove" || event.kind === "log-entry") {
1468
+ return [
1469
+ `- Target: ${event.target}`,
1470
+ `- Note: ${event.note}`,
1471
+ `- Actor: ${event.actor}`,
1472
+ `- Host: ${event.host_id}`
1473
+ ].join("\n");
1474
+ }
1475
+ return [
1476
+ `- Event kind: ${event.kind}`,
1477
+ `- Actor: ${event.actor}`,
1478
+ `- Host: ${event.host_id}`,
1479
+ `- Target: ${event.target}`,
1480
+ `- Note: ${event.note}`,
1481
+ `- Metadata: ${JSON.stringify(event.metadata)}`
1482
+ ].join("\n");
1483
+ }
1484
+ function renderLogProjection(events) {
1485
+ const ordered = [...events].sort((a, b) => {
1486
+ if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
1487
+ return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
1488
+ });
1489
+ const lines = [
1490
+ "# Vault Log",
1491
+ "",
1492
+ "Generated by `skillwiki log materialize` from immutable events.",
1493
+ ""
1494
+ ];
1495
+ for (const event of ordered) {
1496
+ if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
1497
+ const block = bodyFor(event);
1498
+ lines.push(block.endsWith("\n") ? block.trimEnd() : block, "");
1499
+ continue;
1500
+ }
1501
+ lines.push(`## [${day(event)}] ${event.kind} | ${event.target}`, "", bodyFor(event), marker(event), "");
1502
+ }
1503
+ return `${lines.join("\n").replace(/\n+$/, "")}
1504
+ `;
1505
+ }
1506
+
1507
+ // src/utils/projection-authority.ts
1508
+ function resolveProjectionAuthority(load) {
1509
+ if (!load) {
1510
+ return ok({ authority_host_id: "standalone", current_host_id: "standalone", can_write: true });
1511
+ }
1512
+ const snapshotters = Object.entries(load.manifest.hosts).filter(([, h]) => h.role === "snapshotter").map(([id]) => id);
1513
+ const configured = load.manifest.projection_authority;
1514
+ let authority = configured;
1515
+ if (!authority) {
1516
+ if (snapshotters.length === 1) authority = snapshotters[0];
1517
+ else {
1518
+ return err("PREFLIGHT_FAILED", {
1519
+ reason: "projection-authority-unresolved",
1520
+ snapshotters
1521
+ });
1522
+ }
1523
+ }
1524
+ if (!load.manifest.hosts[authority]) {
1525
+ return err("PREFLIGHT_FAILED", { reason: "projection-authority-unknown-host", authority });
1526
+ }
1527
+ if (load.identityStatus !== "known" || !load.hostId) {
1528
+ return err("PREFLIGHT_FAILED", {
1529
+ reason: "fleet-identity-unresolved",
1530
+ identity_status: load.identityStatus
1531
+ });
1532
+ }
1533
+ return ok({
1534
+ authority_host_id: authority,
1535
+ current_host_id: load.hostId,
1536
+ can_write: load.hostId === authority
1537
+ });
1538
+ }
1539
+
1540
+ // src/commands/log-materialize.ts
1541
+ async function runLogMaterialize(input) {
1542
+ const events = await readLogEvents(input.vault);
1543
+ if (!events.ok) {
1544
+ return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
1545
+ }
1546
+ const text = renderLogProjection(events.data);
1547
+ let current = "";
1548
+ try {
1549
+ current = readFileSync6(join11(input.vault, "log.md"), "utf8");
1550
+ } catch {
1551
+ current = "";
1552
+ }
1553
+ const changed = current !== text;
1554
+ if (!input.write) {
1555
+ return {
1556
+ exitCode: ExitCode.OK,
1557
+ result: ok({
1558
+ changed,
1559
+ dry_run: true,
1560
+ event_count: events.data.length,
1561
+ humanHint: changed ? `dry run: would rewrite log.md (${events.data.length} events)` : `dry run: log.md already canonical (${events.data.length} events)`
1562
+ })
1563
+ };
1564
+ }
1565
+ if (!input.skipAuthority) {
1566
+ const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
1567
+ const auth = resolveProjectionAuthority(fleet);
1568
+ if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
1569
+ if (!auth.data.can_write) {
1570
+ return {
1571
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1572
+ result: err("PREFLIGHT_FAILED", {
1573
+ reason: "projection-authority",
1574
+ authority_host_id: auth.data.authority_host_id,
1575
+ current_host_id: auth.data.current_host_id
1576
+ })
1577
+ };
1578
+ }
1579
+ const guard = await guardProtectedVaultWrite({ vault: input.vault, command: "log materialize" });
1580
+ if (guard.blocked) return { exitCode: guard.exitCode, result: guard.result };
1581
+ }
1582
+ if (!changed) {
1583
+ return {
1584
+ exitCode: ExitCode.OK,
1585
+ result: ok({
1586
+ changed: false,
1587
+ dry_run: false,
1588
+ event_count: events.data.length,
1589
+ humanHint: `log.md already canonical (${events.data.length} events)`
1590
+ })
1591
+ };
1592
+ }
1593
+ const written = await atomicWriteText(join11(input.vault, "log.md"), text);
1594
+ if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
1595
+ const installed = readFileSync6(join11(input.vault, "log.md"), "utf8");
1596
+ if (installed !== text) {
601
1597
  return {
602
- exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
603
- result: err("INIT_TARGET_NOT_EMPTY", { target })
1598
+ exitCode: ExitCode.WRITE_FAILED,
1599
+ result: err("WRITE_FAILED", { message: "installed log.md differs from projection" })
604
1600
  };
605
1601
  }
606
- const envPath = join5(input.home, ".skillwiki", ".env");
607
- let existingEnvRaw = "";
1602
+ return {
1603
+ exitCode: ExitCode.OK,
1604
+ result: ok({
1605
+ changed: true,
1606
+ dry_run: false,
1607
+ event_count: events.data.length,
1608
+ humanHint: `rewrote log.md (${events.data.length} events)`
1609
+ })
1610
+ };
1611
+ }
1612
+
1613
+ // src/commands/log-migrate-legacy.ts
1614
+ import { readFileSync as readFileSync7 } from "fs";
1615
+ import { join as join12 } from "path";
1616
+
1617
+ // src/utils/operation-id.ts
1618
+ import { createHash as createHash2 } from "crypto";
1619
+ function operationId(namespace, parts) {
1620
+ const hash = createHash2("sha256").update(namespace).update("\0");
1621
+ for (const part of parts) hash.update(part).update("\0");
1622
+ return hash.digest("hex");
1623
+ }
1624
+
1625
+ // src/commands/log-migrate-legacy.ts
1626
+ var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
1627
+ var PUBLISH_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
1628
+ function splitLegacyBlocks(text) {
1629
+ const matches = [...text.matchAll(BLOCK_RE)];
1630
+ if (matches.length === 0) return [];
1631
+ const out = [];
1632
+ for (let i = 0; i < matches.length; i++) {
1633
+ const m = matches[i];
1634
+ const start = m.index ?? 0;
1635
+ const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
1636
+ const block = text.slice(start, end).replace(/\s+$/, "") + "\n";
1637
+ out.push({ date: m[1], block, ordinal: i });
1638
+ }
1639
+ return out;
1640
+ }
1641
+ async function runLogMigrateLegacy(input) {
1642
+ let text = "";
608
1643
  try {
609
- existingEnvRaw = await readFile3(envPath, "utf8");
1644
+ text = readFileSync7(join12(input.vault, "log.md"), "utf8");
610
1645
  } catch {
611
- }
612
- const existingEnv = parseDotenvText(existingEnvRaw);
613
- const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
614
- const explicitTarget = !!input.flag;
615
- const skipConflictCheck = explicitTarget || !!input.noEnv;
616
- if (!input.profile && !skipConflictCheck && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
617
1646
  return {
618
- exitCode: ExitCode.ENV_WRITE_CONFLICT,
619
- result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
1647
+ exitCode: ExitCode.OK,
1648
+ result: ok({
1649
+ planned: 0,
1650
+ created: 0,
1651
+ already_covered: 0,
1652
+ dry_run: !input.write,
1653
+ humanHint: "no root log.md to migrate"
1654
+ })
620
1655
  };
621
1656
  }
622
- if (!input.profile && !skipConflictCheck && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
623
- return {
624
- exitCode: ExitCode.ENV_WRITE_CONFLICT,
625
- result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
1657
+ const blocks = splitLegacyBlocks(text);
1658
+ let created = 0;
1659
+ let already = 0;
1660
+ for (const b of blocks) {
1661
+ const publish = PUBLISH_RE.exec(b.block)?.[1]?.toLowerCase();
1662
+ const op = publish && /^[0-9a-f]{64}$/.test(publish) ? publish : operationId("skillwiki-legacy-log-v1", [b.date, String(b.ordinal), b.block]);
1663
+ const heading = b.block.split("\n")[0] ?? `## [${b.date}] legacy`;
1664
+ const event = {
1665
+ schema: "skillwiki-log-event/v1",
1666
+ operation_id: op,
1667
+ occurred_at: `${b.date}T00:00:00.000Z`,
1668
+ host_id: input.hostId ?? "standalone",
1669
+ actor: "skillwiki-cli",
1670
+ kind: "legacy-log-entry",
1671
+ target: "log.md",
1672
+ note: heading,
1673
+ metadata: { legacy_markdown: b.block }
626
1674
  };
627
- }
628
- const created = [];
629
- 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) }) };
637
- }
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;
1675
+ if (!input.write) continue;
1676
+ const wrote = await writeLogEvent(input.vault, event);
1677
+ if (!wrote.ok) {
1678
+ if (wrote.error === "EVENT_IDENTITY_COLLISION") {
1679
+ already += 1;
1680
+ continue;
1681
+ }
1682
+ return { exitCode: ExitCode.WRITE_FAILED, result: wrote };
646
1683
  }
647
- const oldTax = extractTaxonomy(oldSchemaText);
648
- if (oldTax.ok) oldTaxonomy = oldTax.data;
1684
+ if (wrote.data.created) created += 1;
1685
+ else already += 1;
649
1686
  }
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
- }
1687
+ return {
1688
+ exitCode: ExitCode.OK,
1689
+ result: ok({
1690
+ planned: blocks.length,
1691
+ created: input.write ? created : 0,
1692
+ already_covered: input.write ? already : 0,
1693
+ dry_run: !input.write,
1694
+ humanHint: input.write ? `migrated legacy log: created=${created} already=${already} planned=${blocks.length}` : `dry run: would migrate ${blocks.length} legacy log block(s)`
1695
+ })
1696
+ };
1697
+ }
1698
+
1699
+ // src/commands/projections-materialize.ts
1700
+ import { readFileSync as readFileSync8 } from "fs";
1701
+ import { join as join13 } from "path";
1702
+ var defaultDeps = {
1703
+ writeText: (path, text) => atomicWriteText(path, text)
1704
+ };
1705
+ async function runProjectionsMaterialize(input, deps = defaultDeps) {
1706
+ const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
1707
+ const auth = resolveProjectionAuthority(fleet);
1708
+ if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
1709
+ const indexProj = await renderRootIndex({ vault: input.vault });
1710
+ if (!indexProj.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: indexProj };
1711
+ const events = await readLogEvents(input.vault);
1712
+ if (!events.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
1713
+ const logText = renderLogProjection(events.data);
1714
+ let curIndex = "";
1715
+ let curLog = "";
1716
+ try {
1717
+ curIndex = readFileSync8(join13(input.vault, "index.md"), "utf8");
1718
+ } catch {
656
1719
  }
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
1720
  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) }) };
1721
+ curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
1722
+ } catch {
667
1723
  }
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
- }
1724
+ const indexDrift = curIndex !== indexProj.data.text;
1725
+ const logDrift = curLog !== logText;
1726
+ if (!input.write) {
1727
+ return {
1728
+ exitCode: ExitCode.OK,
1729
+ result: ok({
1730
+ authority_host_id: auth.data.authority_host_id,
1731
+ current_host_id: auth.data.current_host_id,
1732
+ can_write: auth.data.can_write,
1733
+ index_changed: false,
1734
+ log_changed: false,
1735
+ index_drift: indexDrift,
1736
+ log_drift: logDrift,
1737
+ rolled_back: false,
1738
+ dry_run: true,
1739
+ humanHint: `dry run: index_drift=${indexDrift} log_drift=${logDrift} can_write=${auth.data.can_write}`
1740
+ })
1741
+ };
685
1742
  }
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
- }
1743
+ if (!auth.data.can_write) {
1744
+ return {
1745
+ exitCode: ExitCode.PREFLIGHT_FAILED,
1746
+ result: err("PREFLIGHT_FAILED", {
1747
+ reason: "projection-authority",
1748
+ authority_host_id: auth.data.authority_host_id,
1749
+ current_host_id: auth.data.current_host_id,
1750
+ index_drift: indexDrift,
1751
+ log_drift: logDrift
1752
+ })
1753
+ };
736
1754
  }
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
- });
1755
+ const indexPath = join13(input.vault, "index.md");
1756
+ const logPath = join13(input.vault, "log.md");
1757
+ const indexWrite = await deps.writeText(indexPath, indexProj.data.text);
1758
+ if (!indexWrite.ok) return { exitCode: ExitCode.WRITE_FAILED, result: indexWrite };
1759
+ const logWrite = await deps.writeText(logPath, logText);
1760
+ if (!logWrite.ok) {
1761
+ await deps.writeText(indexPath, curIndex);
1762
+ return {
1763
+ exitCode: ExitCode.WRITE_FAILED,
1764
+ result: ok({
1765
+ authority_host_id: auth.data.authority_host_id,
1766
+ current_host_id: auth.data.current_host_id,
1767
+ can_write: true,
1768
+ index_changed: false,
1769
+ log_changed: false,
1770
+ index_drift: indexDrift,
1771
+ log_drift: logDrift,
1772
+ rolled_back: true,
1773
+ dry_run: false,
1774
+ humanHint: "projection materialize rolled back after log write failure"
1775
+ })
1776
+ };
753
1777
  }
754
1778
  return {
755
1779
  exitCode: ExitCode.OK,
756
1780
  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`)
1781
+ authority_host_id: auth.data.authority_host_id,
1782
+ current_host_id: auth.data.current_host_id,
1783
+ can_write: true,
1784
+ index_changed: indexWrite.data.changed,
1785
+ log_changed: logWrite.data.changed,
1786
+ index_drift: indexDrift,
1787
+ log_drift: logDrift,
1788
+ rolled_back: false,
1789
+ dry_run: false,
1790
+ humanHint: `materialized projections index_changed=${indexWrite.data.changed} log_changed=${logWrite.data.changed}`
769
1791
  })
770
1792
  };
771
1793
  }
772
1794
 
773
1795
  // 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";
1796
+ import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
1797
+ import { existsSync as existsSync2, statSync } from "fs";
1798
+ import { join as join14 } from "path";
777
1799
  function extractDate(filename) {
778
1800
  const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
779
1801
  return m?.[1] ?? "";
@@ -785,14 +1807,14 @@ function extractProjectSlug(projectField) {
785
1807
  return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
786
1808
  }
787
1809
  async function runClaim(input) {
788
- if (!existsSync(input.vault) || !statSync(input.vault).isDirectory()) {
1810
+ if (!existsSync2(input.vault) || !statSync(input.vault).isDirectory()) {
789
1811
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { path: input.vault }) };
790
1812
  }
791
- const absTranscript = join6(input.vault, input.transcript);
792
- if (!existsSync(absTranscript)) {
1813
+ const absTranscript = join14(input.vault, input.transcript);
1814
+ if (!existsSync2(absTranscript)) {
793
1815
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.transcript }) };
794
1816
  }
795
- const content = await readFile4(absTranscript, "utf8");
1817
+ const content = await readFile5(absTranscript, "utf8");
796
1818
  const fm = extractFrontmatter(content);
797
1819
  let projectSlug = input.project;
798
1820
  if (!projectSlug && fm.ok && typeof fm.data.project === "string") {
@@ -804,8 +1826,8 @@ async function runClaim(input) {
804
1826
  result: err("SCHEME_REJECTED", { message: "No project specified. Use --project or set project in transcript frontmatter." })
805
1827
  };
806
1828
  }
807
- const projectDir = join6(input.vault, "projects", projectSlug);
808
- if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
1829
+ const projectDir = join14(input.vault, "projects", projectSlug);
1830
+ if (!existsSync2(projectDir) || !statSync(projectDir).isDirectory()) {
809
1831
  return {
810
1832
  exitCode: ExitCode.PROJECT_NOT_FOUND,
811
1833
  result: err("PROJECT_NOT_FOUND", { project: projectSlug })
@@ -821,10 +1843,10 @@ async function runClaim(input) {
821
1843
  }
822
1844
  const workSlug = input.slug || extractSlugFromFilename(filename);
823
1845
  const dirName = `${date}-${workSlug}`;
824
- const workDir = join6(projectDir, "work", dirName);
1846
+ const workDir = join14(projectDir, "work", dirName);
825
1847
  const relWorkDir = `projects/${projectSlug}/work/${dirName}`;
826
1848
  const relSpecPath = `${relWorkDir}/spec.md`;
827
- if (existsSync(workDir)) {
1849
+ if (existsSync2(workDir)) {
828
1850
  return {
829
1851
  exitCode: ExitCode.OK,
830
1852
  result: ok({
@@ -835,7 +1857,7 @@ async function runClaim(input) {
835
1857
  })
836
1858
  };
837
1859
  }
838
- await mkdir4(workDir, { recursive: true });
1860
+ await mkdir5(workDir, { recursive: true });
839
1861
  const kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "task";
840
1862
  const specLines = [
841
1863
  "---",
@@ -850,7 +1872,7 @@ async function runClaim(input) {
850
1872
  `Claimed from ${input.transcript}`,
851
1873
  ""
852
1874
  ];
853
- await writeFile3(join6(workDir, "spec.md"), specLines.join("\n"), "utf8");
1875
+ await writeFile3(join14(workDir, "spec.md"), specLines.join("\n"), "utf8");
854
1876
  appendLastOp(input.vault, {
855
1877
  operation: "claim",
856
1878
  summary: `claimed ${input.transcript} \u2192 ${relWorkDir}`,
@@ -869,8 +1891,8 @@ async function runClaim(input) {
869
1891
  }
870
1892
 
871
1893
  // 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";
1894
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
1895
+ import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
874
1896
  import { platform } from "os";
875
1897
  function statusFromCounts(counts) {
876
1898
  if ((counts.error ?? 0) > 0) return "error";
@@ -981,8 +2003,8 @@ function summarizeChecks(checks) {
981
2003
  };
982
2004
  }
983
2005
  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);
2006
+ if (!existsSync3(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
2007
+ const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
986
2008
  if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
987
2009
  const statusLine = [...lines].reverse().find(
988
2010
  (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 +2025,12 @@ function runVaultSyncHealth(home, syncMode) {
1003
2025
  };
1004
2026
  }
1005
2027
  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");
2028
+ const shareDir = isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin");
2029
+ const logDir = isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log");
2030
+ const filterPath = join15(home, ".config", "rclone", "wiki-push-filters.txt");
1009
2031
  const checks = [];
1010
- const pushScript = join7(shareDir, "wiki-push.sh");
1011
- if (syncMode === "optional" && !existsSync2(pushScript)) {
2032
+ const pushScript = join15(shareDir, "wiki-push.sh");
2033
+ if (syncMode === "optional" && !existsSync3(pushScript)) {
1012
2034
  return {
1013
2035
  status: "pass",
1014
2036
  blocking: false,
@@ -1021,26 +2043,26 @@ function runVaultSyncHealth(home, syncMode) {
1021
2043
  }]
1022
2044
  };
1023
2045
  }
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}` });
2046
+ 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
2047
  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)" });
2048
+ const pushPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
2049
+ const fetchPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
2050
+ 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
2051
  checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
1030
2052
  } 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)) {
2053
+ const pushTimer = join15(home, ".config", "systemd", "user", "wiki-push.timer");
2054
+ const fetchTimer = join15(home, ".config", "systemd", "user", "wiki-fetch.timer");
2055
+ const fuseTimer = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
2056
+ const fuseService = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
2057
+ 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)" });
2058
+ 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)" });
2059
+ }
2060
+ checks.push(classifyLog(join15(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
2061
+ checks.push(classifyLog(join15(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
2062
+ if (!existsSync3(filterPath)) {
1041
2063
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
1042
2064
  } else {
1043
- const content = readFileSync2(filterPath, "utf8");
2065
+ const content = readFileSync9(filterPath, "utf8");
1044
2066
  const missing = [
1045
2067
  "remotely-save/data.json",
1046
2068
  ".skillwiki/sync.lock",
@@ -1135,7 +2157,7 @@ function buildHumanHint(report) {
1135
2157
  function writeReport(out, report) {
1136
2158
  mkdirSync(dirname4(out), { recursive: true });
1137
2159
  const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
1138
- writeFileSync(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
2160
+ writeFileSync3(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
1139
2161
  renameSync(tmp, out);
1140
2162
  }
1141
2163
  function buildIncompleteReport(input, syncMode) {
@@ -1359,12 +2381,12 @@ async function runHealth(input) {
1359
2381
  }
1360
2382
 
1361
2383
  // src/commands/archive.ts
1362
- import { rename as rename2, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
1363
- import { join as join9, dirname as dirname5 } from "path";
2384
+ import { rename as rename2, mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
2385
+ import { join as join17, dirname as dirname5 } from "path";
1364
2386
 
1365
2387
  // src/utils/delete-intent.ts
1366
- import { mkdir as mkdir5, writeFile as writeFile4, readdir as readdir3, readFile as readFile5 } from "fs/promises";
1367
- import { join as join8 } from "path";
2388
+ import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
2389
+ import { join as join16 } from "path";
1368
2390
  var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
1369
2391
  var DELETE_INTENT_DIR = "meta/delete-intents";
1370
2392
  function normalizeVaultRelPath(path) {
@@ -1395,10 +2417,10 @@ function buildDeleteIntent(input) {
1395
2417
  };
1396
2418
  }
1397
2419
  async function writeDeleteIntent(vault, intent) {
1398
- const dir = join8(vault, DELETE_INTENT_DIR);
1399
- await mkdir5(dir, { recursive: true });
2420
+ const dir = join16(vault, DELETE_INTENT_DIR);
2421
+ await mkdir6(dir, { recursive: true });
1400
2422
  const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
1401
- await writeFile4(join8(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
2423
+ await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
1402
2424
  return rel;
1403
2425
  }
1404
2426
 
@@ -1436,7 +2458,7 @@ async function runArchive(input) {
1436
2458
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1437
2459
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1438
2460
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
1439
- const archivePath = join9("_archive", relPath).replace(/\\/g, "/");
2461
+ const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
1440
2462
  const remoteRoot = normalizeRemoteRoot(input.remote);
1441
2463
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1442
2464
  let cascade;
@@ -1462,7 +2484,7 @@ async function runArchive(input) {
1462
2484
  const indexRefs = [];
1463
2485
  if (!isRaw) {
1464
2486
  try {
1465
- const idx = await readFile6(join9(input.vault, "index.md"), "utf8");
2487
+ const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
1466
2488
  idx.split("\n").forEach((line, i) => {
1467
2489
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
1468
2490
  });
@@ -1489,8 +2511,8 @@ async function runArchive(input) {
1489
2511
  }
1490
2512
  if (input.cascade && input.apply && cascade) {
1491
2513
  for (const ref of cascade.source_array_refs) {
1492
- const absPath = join9(input.vault, ref.page);
1493
- const text = await readFile6(absPath, "utf8");
2514
+ const absPath = join17(input.vault, ref.page);
2515
+ const text = await readFile7(absPath, "utf8");
1494
2516
  const split = splitFrontmatter(text);
1495
2517
  if (!split.ok) continue;
1496
2518
  const before = split.data.rawFrontmatter;
@@ -1507,23 +2529,21 @@ ${fmRewritten}
1507
2529
  }
1508
2530
  }
1509
2531
  }
1510
- await mkdir6(dirname5(join9(input.vault, archivePath)), { recursive: true });
2532
+ await mkdir7(dirname5(join17(input.vault, archivePath)), { recursive: true });
2533
+ await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
1511
2534
  let indexUpdated = false;
1512
2535
  if (!isRaw) {
1513
- const indexPath = join9(input.vault, "index.md");
1514
- try {
1515
- const idx = await readFile6(indexPath, "utf8");
1516
- const originalLines = idx.split("\n");
1517
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1518
- if (filtered.length !== originalLines.length) {
1519
- await writeFile5(indexPath, filtered.join("\n"), "utf8");
1520
- indexUpdated = true;
1521
- }
1522
- } catch (e) {
1523
- if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
2536
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2537
+ const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
2538
+ const fullTarget = relPath.replace(/\.md$/, "");
2539
+ const bare = fullTarget.split("/").pop() ?? fullTarget;
2540
+ const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
2541
+ const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
2542
+ if (projection.ok && projection.data.text !== before) {
2543
+ const written = await writeRootIndexProjection2(input.vault, projection.data);
2544
+ if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
1524
2545
  }
1525
2546
  }
1526
- await rename2(join9(input.vault, relPath), join9(input.vault, archivePath));
1527
2547
  const archiveIntent = buildDeleteIntent({
1528
2548
  path: relPath,
1529
2549
  action: "archive",
@@ -1564,8 +2584,8 @@ ${fmRewritten}
1564
2584
  }
1565
2585
 
1566
2586
  // src/commands/remove.ts
1567
- import { unlink as unlink2, readFile as readFile7, writeFile as writeFile6, access } from "fs/promises";
1568
- import { join as join10 } from "path";
2587
+ import { unlink as unlink2, access } from "fs/promises";
2588
+ import { join as join18 } from "path";
1569
2589
  async function pathExists(abs) {
1570
2590
  try {
1571
2591
  await access(abs);
@@ -1591,7 +2611,7 @@ async function runRemove(input) {
1591
2611
  if (!relPath) {
1592
2612
  try {
1593
2613
  const candidate = normalizeVaultRelPath(input.page);
1594
- if (await pathExists(join10(input.vault, candidate))) {
2614
+ if (await pathExists(join18(input.vault, candidate))) {
1595
2615
  relPath = candidate;
1596
2616
  }
1597
2617
  } catch {
@@ -1611,22 +2631,7 @@ async function runRemove(input) {
1611
2631
  }
1612
2632
  const remoteRoot = normalizeRemoteRoot(input.remote);
1613
2633
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1614
- const slug = relPath.replace(/\.md$/, "").split("/").pop() ?? relPath;
1615
2634
  let indexUpdated = false;
1616
- if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
1617
- const indexPath = join10(input.vault, "index.md");
1618
- try {
1619
- const idx = await readFile7(indexPath, "utf8");
1620
- const originalLines = idx.split("\n");
1621
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1622
- if (filtered.length !== originalLines.length) {
1623
- await writeFile6(indexPath, filtered.join("\n"), "utf8");
1624
- indexUpdated = true;
1625
- }
1626
- } catch (e) {
1627
- if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
1628
- }
1629
- }
1630
2635
  const intent = buildDeleteIntent({
1631
2636
  path: relPath,
1632
2637
  action: "remove",
@@ -1635,7 +2640,21 @@ async function runRemove(input) {
1635
2640
  reason: input.reason
1636
2641
  });
1637
2642
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
1638
- await unlink2(join10(input.vault, relPath));
2643
+ await unlink2(join18(input.vault, relPath));
2644
+ if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
2645
+ const { readFile: readFile17 } = await import("fs/promises");
2646
+ const { join: pathJoin } = await import("path");
2647
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2648
+ const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
2649
+ const fullTarget = relPath.replace(/\.md$/, "");
2650
+ const bare = fullTarget.split("/").pop() ?? fullTarget;
2651
+ const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
2652
+ const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
2653
+ if (projection.ok && projection.data.text !== before) {
2654
+ const written = await writeRootIndexProjection2(input.vault, projection.data);
2655
+ if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
2656
+ }
2657
+ }
1639
2658
  appendLastOp(input.vault, {
1640
2659
  operation: "remove",
1641
2660
  summary: `removed ${relPath} (tombstone ${tombstonePath})`,
@@ -1664,7 +2683,7 @@ async function runRemove(input) {
1664
2683
  }
1665
2684
 
1666
2685
  // src/commands/drift.ts
1667
- import { createHash as createHash2 } from "crypto";
2686
+ import { createHash as createHash3 } from "crypto";
1668
2687
 
1669
2688
  // src/utils/fetch.ts
1670
2689
  async function controlledFetch(url, opts) {
@@ -1743,7 +2762,7 @@ async function runDrift(input) {
1743
2762
  });
1744
2763
  continue;
1745
2764
  }
1746
- const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
2765
+ const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1747
2766
  const identity = assessSourceIdentity({
1748
2767
  rawPath: raw.relPath,
1749
2768
  sourceUrl,
@@ -1974,14 +2993,38 @@ ${migratedBody}${newFooter}`;
1974
2993
 
1975
2994
  // src/commands/update.ts
1976
2995
  import { execSync } from "child_process";
1977
- import { join as join11 } from "path";
2996
+ import { join as join19 } from "path";
2997
+ function parseCoreSemver(version) {
2998
+ const m = version.match(/^(\d+)\.(\d+)\.(\d+)/);
2999
+ if (!m) return null;
3000
+ return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10) };
3001
+ }
3002
+ function needs0101Migration(previousVersion, newVersion) {
3003
+ const p = parseCoreSemver(previousVersion);
3004
+ const n = parseCoreSemver(newVersion);
3005
+ if (!p || !n) return false;
3006
+ const prevLt = p.major < 0 || p.major === 0 && p.minor < 10 || p.major === 0 && p.minor === 10 && p.patch < 1;
3007
+ const newGe = n.major > 0 || n.major === 0 && n.minor > 10 || n.major === 0 && n.minor === 10 && n.patch >= 1;
3008
+ return prevLt && newGe;
3009
+ }
3010
+ function migrationNotesForUpgrade(previousVersion, newVersion) {
3011
+ if (!needs0101Migration(previousVersion, newVersion)) return [];
3012
+ return [
3013
+ "Migration 0.10.1:",
3014
+ "- pull helper resolves from dist/ + host vault-sync install",
3015
+ "- run: skillwiki doctor",
3016
+ "- if managed writes blocked: skillwiki sync journal list",
3017
+ "- then: skillwiki sync journal clear-stale --dry-run",
3018
+ "- legacy override: SKILLWIKI_VAULT_SYNC_PULL_HELPER=<path-to-wiki-pull-with-auto-resolve.sh>"
3019
+ ];
3020
+ }
1978
3021
  function resolveGlobalSkillsRoot() {
1979
3022
  try {
1980
3023
  const globalRoot = execSync("npm root -g", {
1981
3024
  encoding: "utf8",
1982
3025
  timeout: 5e3
1983
3026
  }).trim();
1984
- return join11(globalRoot, "skillwiki", "skills");
3027
+ return join19(globalRoot, "skillwiki", "skills");
1985
3028
  } catch {
1986
3029
  return null;
1987
3030
  }
@@ -2009,7 +3052,7 @@ async function runUpdate(input) {
2009
3052
  const pkg2 = readCliPackageJson();
2010
3053
  const currentVersion = pkg2.version;
2011
3054
  const tag = normalizeDistTag(input.distTag);
2012
- const target = join11(input.home, ".claude", "skills");
3055
+ const target = join19(input.home, ".claude", "skills");
2013
3056
  let latest;
2014
3057
  try {
2015
3058
  latest = execSync(`npm view skillwiki@${tag} version`, {
@@ -2070,6 +3113,9 @@ async function runUpdate(input) {
2070
3113
  hintLines.push(`version warnings: ${version_warnings.length}`);
2071
3114
  for (const w of version_warnings) hintLines.push(` ${w}`);
2072
3115
  }
3116
+ for (const line of migrationNotesForUpgrade(currentVersion, latest)) {
3117
+ hintLines.push(line);
3118
+ }
2073
3119
  return {
2074
3120
  exitCode: ExitCode.OK,
2075
3121
  result: ok({
@@ -2086,22 +3132,22 @@ async function runUpdate(input) {
2086
3132
 
2087
3133
  // src/commands/self-update.ts
2088
3134
  import { execSync as execSync2 } from "child_process";
2089
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2090
- import { join as join12 } from "path";
3135
+ import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
3136
+ import { join as join20 } from "path";
2091
3137
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
2092
3138
  async function runSelfUpdate(input) {
2093
3139
  const currentVersion = readCliPackageJson().version;
2094
3140
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
2095
3141
  const distTag = normalizeDistTag(input.distTag);
2096
- const localPkgPath = join12(sourceRoot, "packages", "cli", "package.json");
2097
- const hasLocalSource = existsSync3(localPkgPath);
3142
+ const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
3143
+ const hasLocalSource = existsSync4(localPkgPath);
2098
3144
  if (input.check) {
2099
3145
  let availableVersion = null;
2100
3146
  let source;
2101
3147
  if (hasLocalSource) {
2102
3148
  source = "local";
2103
3149
  try {
2104
- availableVersion = JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? null;
3150
+ availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
2105
3151
  } catch {
2106
3152
  availableVersion = null;
2107
3153
  }
@@ -2159,7 +3205,7 @@ async function runSelfUpdate(input) {
2159
3205
  }
2160
3206
  const newVersion = (() => {
2161
3207
  try {
2162
- return JSON.parse(readFileSync3(localPkgPath, "utf8")).version ?? "unknown";
3208
+ return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
2163
3209
  } catch {
2164
3210
  return "unknown";
2165
3211
  }
@@ -2225,21 +3271,21 @@ async function runSelfUpdate(input) {
2225
3271
  }
2226
3272
 
2227
3273
  // src/commands/transcripts.ts
2228
- import { readdir as readdir4, stat as stat3, readFile as readFile8 } from "fs/promises";
2229
- import { join as join13 } from "path";
3274
+ import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
3275
+ import { join as join21 } from "path";
2230
3276
  async function runTranscripts(input) {
2231
- const dir = join13(input.vault, "raw", "transcripts");
3277
+ const dir = join21(input.vault, "raw", "transcripts");
2232
3278
  let entries;
2233
3279
  try {
2234
- entries = await readdir4(dir, { withFileTypes: true });
3280
+ entries = await readdir5(dir, { withFileTypes: true });
2235
3281
  } catch {
2236
3282
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
2237
3283
  }
2238
3284
  const transcripts = [];
2239
3285
  for (const entry of entries) {
2240
3286
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2241
- const filePath = join13(dir, entry.name);
2242
- const content = await readFile8(filePath, "utf8");
3287
+ const filePath = join21(dir, entry.name);
3288
+ const content = await readFile9(filePath, "utf8");
2243
3289
  const fm = extractFrontmatter(content);
2244
3290
  if (!fm.ok) continue;
2245
3291
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -2256,10 +3302,10 @@ async function runTranscripts(input) {
2256
3302
  }
2257
3303
 
2258
3304
  // src/commands/compound.ts
2259
- import { writeFile as writeFile7, mkdir as mkdir7, readdir as readdir5, unlink as unlink3 } from "fs/promises";
2260
- import { join as join14 } from "path";
2261
- import { existsSync as existsSync4 } from "fs";
2262
- import { readFile as readFile9 } from "fs/promises";
3305
+ import { writeFile as writeFile7, mkdir as mkdir8, readdir as readdir6, unlink as unlink3 } from "fs/promises";
3306
+ import { join as join22 } from "path";
3307
+ import { existsSync as existsSync5 } from "fs";
3308
+ import { readFile as readFile10 } from "fs/promises";
2263
3309
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
2264
3310
  var FIELD_RE = {
2265
3311
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -2357,17 +3403,17 @@ function extractRetroFields(date, cycleName, block) {
2357
3403
  };
2358
3404
  }
2359
3405
  async function runCompound(input) {
2360
- const logPath = join14(input.vault, "log.md");
3406
+ const logPath = join22(input.vault, "log.md");
2361
3407
  let logText;
2362
3408
  try {
2363
- logText = await readFile9(logPath, "utf8");
3409
+ logText = await readFile10(logPath, "utf8");
2364
3410
  } catch {
2365
3411
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2366
3412
  }
2367
3413
  const entries = parseRetroEntries(logText);
2368
3414
  const promoted = [];
2369
3415
  const skipped = [];
2370
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
3416
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
2371
3417
  for (const entry of entries) {
2372
3418
  const generalizeValue = entry.generalize.trim();
2373
3419
  if (!/^yes/i.test(generalizeValue)) {
@@ -2375,8 +3421,8 @@ async function runCompound(input) {
2375
3421
  continue;
2376
3422
  }
2377
3423
  const slug = slugify(entry.cycleName);
2378
- const compoundPath = join14(compoundDir, `${slug}.md`);
2379
- if (existsSync4(compoundPath)) {
3424
+ const compoundPath = join22(compoundDir, `${slug}.md`);
3425
+ if (existsSync5(compoundPath)) {
2380
3426
  skipped.push(entry.date);
2381
3427
  continue;
2382
3428
  }
@@ -2414,8 +3460,8 @@ async function runCompound(input) {
2414
3460
  ].join("\n");
2415
3461
  const content = frontmatter + "\n" + body;
2416
3462
  if (!input.dryRun) {
2417
- if (!existsSync4(compoundDir)) {
2418
- await mkdir7(compoundDir, { recursive: true });
3463
+ if (!existsSync5(compoundDir)) {
3464
+ await mkdir8(compoundDir, { recursive: true });
2419
3465
  }
2420
3466
  await writeFile7(compoundPath, content, "utf8");
2421
3467
  }
@@ -2436,16 +3482,16 @@ async function runCompound(input) {
2436
3482
  };
2437
3483
  }
2438
3484
  async function runCompoundDelete(input) {
2439
- const projectDir = join14(input.vault, "projects", input.project);
2440
- if (!existsSync4(projectDir)) {
3485
+ const projectDir = join22(input.vault, "projects", input.project);
3486
+ if (!existsSync5(projectDir)) {
2441
3487
  return {
2442
3488
  exitCode: ExitCode.PROJECT_NOT_FOUND,
2443
3489
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
2444
3490
  };
2445
3491
  }
2446
3492
  const entryName = input.entry.replace(/\.md$/, "");
2447
- const compoundPath = join14(projectDir, "compound", `${entryName}.md`);
2448
- if (!existsSync4(compoundPath)) {
3493
+ const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
3494
+ if (!existsSync5(compoundPath)) {
2449
3495
  return {
2450
3496
  exitCode: ExitCode.FILE_NOT_FOUND,
2451
3497
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -2478,8 +3524,8 @@ knowledge.md regenerated`
2478
3524
  };
2479
3525
  }
2480
3526
  async function runCompoundList(input) {
2481
- const compoundDir = join14(input.vault, "projects", input.project, "compound");
2482
- if (!existsSync4(compoundDir)) {
3527
+ const compoundDir = join22(input.vault, "projects", input.project, "compound");
3528
+ if (!existsSync5(compoundDir)) {
2483
3529
  return {
2484
3530
  exitCode: ExitCode.OK,
2485
3531
  result: ok({
@@ -2493,7 +3539,7 @@ no compound directory found`
2493
3539
  }
2494
3540
  let dirents;
2495
3541
  try {
2496
- dirents = await readdir5(compoundDir, { withFileTypes: true });
3542
+ dirents = await readdir6(compoundDir, { withFileTypes: true });
2497
3543
  } catch {
2498
3544
  return {
2499
3545
  exitCode: ExitCode.OK,
@@ -2509,10 +3555,10 @@ could not read compound directory`
2509
3555
  const entries = [];
2510
3556
  for (const dirent of dirents) {
2511
3557
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
2512
- const filePath = join14(compoundDir, dirent.name);
3558
+ const filePath = join22(compoundDir, dirent.name);
2513
3559
  let text;
2514
3560
  try {
2515
- text = await readFile9(filePath, "utf8");
3561
+ text = await readFile10(filePath, "utf8");
2516
3562
  } catch {
2517
3563
  continue;
2518
3564
  }
@@ -2541,8 +3587,8 @@ no compound entries found`;
2541
3587
  }
2542
3588
 
2543
3589
  // src/commands/session-brief.ts
2544
- import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
2545
- import { join as join15, relative, sep } from "path";
3590
+ import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
3591
+ import { join as join23, relative, sep } from "path";
2546
3592
  var MAX_WORDS = 900;
2547
3593
  async function runSessionBrief(input) {
2548
3594
  const scan = await scanVault(input.vault);
@@ -2642,7 +3688,7 @@ async function resolveProject(input) {
2642
3688
  const envProject = input.env?.SKILLWIKI_PROJECT;
2643
3689
  if (envProject) return envProject;
2644
3690
  const cwd = input.cwd ?? process.cwd();
2645
- const projectDotenv = await readProjectSlug(join15(cwd, ".skillwiki", ".env"));
3691
+ const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
2646
3692
  if (projectDotenv) return projectDotenv;
2647
3693
  const inferred = inferProjectFromPath(input.vault, cwd);
2648
3694
  if (inferred) return inferred;
@@ -2651,7 +3697,7 @@ async function resolveProject(input) {
2651
3697
  async function readProjectSlug(file) {
2652
3698
  let text;
2653
3699
  try {
2654
- text = await readFile10(file, "utf8");
3700
+ text = await readFile11(file, "utf8");
2655
3701
  } catch {
2656
3702
  return void 0;
2657
3703
  }
@@ -2728,7 +3774,7 @@ async function loadTrendDigests(typedPages) {
2728
3774
  return out;
2729
3775
  }
2730
3776
  async function loadSessionPins(vault, project) {
2731
- const text = await readIfExists(join15(vault, "meta", "session-pins.md"));
3777
+ const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
2732
3778
  if (!text) return [];
2733
3779
  const fm = extractFrontmatter(text);
2734
3780
  if (!fm.ok) return [];
@@ -2845,7 +3891,7 @@ function satelliteHealthWarnings(warning) {
2845
3891
  return warning ? [warning] : [];
2846
3892
  }
2847
3893
  async function loadHealthWarnings(vault) {
2848
- const text = await readIfExists(join15(vault, ".skillwiki", "health.json"));
3894
+ const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
2849
3895
  if (!text) return [];
2850
3896
  try {
2851
3897
  const parsed = JSON.parse(text);
@@ -2858,7 +3904,7 @@ async function loadHealthWarnings(vault) {
2858
3904
  }
2859
3905
  }
2860
3906
  async function loadMemoryTopics(vault, project) {
2861
- const text = await readIfExists(join15(vault, ".skillwiki", "memory", project, "topics.json"));
3907
+ const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
2862
3908
  if (!text) return [];
2863
3909
  try {
2864
3910
  const parsed = JSON.parse(text);
@@ -2875,11 +3921,11 @@ async function loadMemoryTopics(vault, project) {
2875
3921
  }
2876
3922
  }
2877
3923
  async function writeBriefArtifacts(vault, input) {
2878
- const metaPath = join15(vault, "meta", "latest-session-brief.md");
2879
- const cacheMdPath = join15(vault, ".skillwiki", "session-brief.md");
2880
- const cacheJsonPath = join15(vault, ".skillwiki", "session-brief.json");
2881
- await mkdir8(join15(vault, "meta"), { recursive: true });
2882
- await mkdir8(join15(vault, ".skillwiki"), { recursive: true });
3924
+ const metaPath = join23(vault, "meta", "latest-session-brief.md");
3925
+ const cacheMdPath = join23(vault, ".skillwiki", "session-brief.md");
3926
+ const cacheJsonPath = join23(vault, ".skillwiki", "session-brief.json");
3927
+ await mkdir9(join23(vault, "meta"), { recursive: true });
3928
+ await mkdir9(join23(vault, ".skillwiki"), { recursive: true });
2883
3929
  const committed = renderCommittedBrief(input);
2884
3930
  const previousComparable = comparableBrief(await readIfExists(metaPath));
2885
3931
  const nextComparable = comparableBrief(committed);
@@ -2894,8 +3940,17 @@ async function writeBriefArtifacts(vault, input) {
2894
3940
  memory_topics: input.memoryTopics
2895
3941
  }, null, 2)}
2896
3942
  `, "utf8");
2897
- const indexUpdated = await ensureIndexEntry(vault);
2898
- const logUpdated = materialChange ? await appendMaterialLog(vault, input.today) : false;
3943
+ const indexUpdated = await rebuildRootIndexProjection(vault);
3944
+ let logUpdated = false;
3945
+ if (materialChange) {
3946
+ const log = await runLogAppend({
3947
+ vault,
3948
+ content: `## [${input.today}] session-brief | refreshed: meta/latest-session-brief.md`,
3949
+ strictLock: true,
3950
+ recordLastOp: false
3951
+ });
3952
+ logUpdated = log.result.ok && log.result.data.appended;
3953
+ }
2899
3954
  if (materialChange) {
2900
3955
  appendLastOp(vault, {
2901
3956
  operation: "session-brief",
@@ -2939,37 +3994,18 @@ function renderCommittedBrief(input) {
2939
3994
  ""
2940
3995
  ].filter((line) => line !== "").join("\n");
2941
3996
  }
2942
- async function ensureIndexEntry(vault) {
2943
- const indexPath = join15(vault, "index.md");
2944
- let text = await readIfExists(indexPath);
2945
- if (!text) return false;
2946
- if (text.includes("[[meta/latest-session-brief]]")) return false;
2947
- const entry = "- [[meta/latest-session-brief]] \u2014 Latest Session Brief";
2948
- const lines = text.split(/\r?\n/);
2949
- const sectionIdx = lines.findIndex((line) => line.trim() === "## Meta");
2950
- if (sectionIdx === -1) {
2951
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
2952
- lines.push("", "## Meta", entry);
2953
- } else {
2954
- let insertAt = sectionIdx + 1;
2955
- while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
2956
- lines.splice(insertAt, 0, entry);
2957
- }
2958
- await writeFile8(indexPath, lines.join("\n"), "utf8");
2959
- return true;
2960
- }
2961
- async function appendMaterialLog(vault, today) {
2962
- const logPath = join15(vault, "log.md");
2963
- const text = await readIfExists(logPath);
2964
- if (!text) return false;
2965
- const entry = `
2966
- ## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
2967
- await writeFile8(logPath, text.trimEnd() + entry + "\n", "utf8");
2968
- return true;
3997
+ async function rebuildRootIndexProjection(vault) {
3998
+ const before = await readIfExists(join23(vault, "index.md"));
3999
+ if (!before) return false;
4000
+ const projection = await renderRootIndex({ vault, currentText: before });
4001
+ if (!projection.ok) return false;
4002
+ if (projection.data.text === before) return false;
4003
+ const written = await writeRootIndexProjection(vault, projection.data);
4004
+ return written.ok && written.data.changed;
2969
4005
  }
2970
4006
  async function readIfExists(path) {
2971
4007
  try {
2972
- return await readFile10(path, "utf8");
4008
+ return await readFile11(path, "utf8");
2973
4009
  } catch {
2974
4010
  return "";
2975
4011
  }
@@ -3011,16 +4047,18 @@ function dateFromPath(path) {
3011
4047
  }
3012
4048
 
3013
4049
  // src/commands/ingest.ts
3014
- import { readFile as readFile12, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
3015
- import { join as join17 } from "path";
4050
+ import { readFile as readFile13, open as open2, unlink as unlink4, mkdir as mkdir10 } from "fs/promises";
4051
+ import { join as join25 } from "path";
3016
4052
  import { createHash as createHash4 } from "crypto";
3017
4053
 
3018
4054
  // src/commands/page-publish.ts
3019
- import { createHash as createHash3 } from "crypto";
3020
4055
  import { realpathSync } from "fs";
3021
- import { readFile as readFile11 } from "fs/promises";
3022
- import { join as join16, resolve as resolve3 } from "path";
3023
- var DEFAULT_DEPS = { afterStage: async () => void 0 };
4056
+ import { readFile as readFile12 } from "fs/promises";
4057
+ import { join as join24, resolve as resolve3 } from "path";
4058
+ var DEFAULT_DEPS = {
4059
+ afterStage: async () => void 0,
4060
+ preflight: (input) => runManagedWritePreflight(input)
4061
+ };
3024
4062
  function errorExitCode(error) {
3025
4063
  switch (error) {
3026
4064
  case "FILE_NOT_FOUND":
@@ -3036,6 +4074,9 @@ function errorExitCode(error) {
3036
4074
  return ExitCode.SENSITIVE_CONTENT_DETECTED;
3037
4075
  case "SYNC_LOCK_HELD":
3038
4076
  return ExitCode.SYNC_LOCK_HELD;
4077
+ case "PREFLIGHT_FAILED":
4078
+ return ExitCode.PREFLIGHT_FAILED;
4079
+ case "EVENT_IDENTITY_COLLISION":
3039
4080
  case "WRITE_FAILED":
3040
4081
  return ExitCode.WRITE_FAILED;
3041
4082
  default:
@@ -3043,7 +4084,11 @@ function errorExitCode(error) {
3043
4084
  }
3044
4085
  }
3045
4086
  function publicationId(target, content, logNote = "") {
3046
- return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
4087
+ return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
4088
+ }
4089
+ function resolveRootAggregateMode(env = process.env) {
4090
+ if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
4091
+ return "dual";
3047
4092
  }
3048
4093
  function prepareFrozenPublication(input, source) {
3049
4094
  const target = assertTargetInsideVault(input.vault, input.target);
@@ -3079,7 +4124,7 @@ function preparePagePublicationFromContent(input) {
3079
4124
  async function preparePagePublication(input) {
3080
4125
  let content;
3081
4126
  try {
3082
- content = await readFile11(input.draftPath, "utf8");
4127
+ content = await readFile12(input.draftPath, "utf8");
3083
4128
  } catch (error) {
3084
4129
  return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
3085
4130
  }
@@ -3150,10 +4195,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
3150
4195
  ExitCode.VAULT_PATH_INVALID
3151
4196
  );
3152
4197
  }
3153
- const schemaPath = join16(vault, "SCHEMA.md");
4198
+ const schemaPath = join24(vault, "SCHEMA.md");
3154
4199
  let schemaText;
3155
4200
  try {
3156
- schemaText = await readFile11(schemaPath, "utf8");
4201
+ schemaText = await readFile12(schemaPath, "utf8");
3157
4202
  } catch (error) {
3158
4203
  return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
3159
4204
  }
@@ -3183,8 +4228,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
3183
4228
  let visibleSchema;
3184
4229
  try {
3185
4230
  [visible, visibleSchema] = await Promise.all([
3186
- readFile11(input.targetPath, "utf8"),
3187
- readFile11(schemaPath, "utf8")
4231
+ readFile12(input.targetPath, "utf8"),
4232
+ readFile12(schemaPath, "utf8")
3188
4233
  ]);
3189
4234
  } catch (error) {
3190
4235
  return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
@@ -3237,7 +4282,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
3237
4282
  })
3238
4283
  };
3239
4284
  }
3240
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
4285
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
3241
4286
  return {
3242
4287
  exitCode: ExitCode.OK,
3243
4288
  result: ok({
@@ -3251,6 +4296,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
3251
4296
  operation_id: input.operationId,
3252
4297
  dry_run: dryRun,
3253
4298
  files_changed: filesChanged,
4299
+ base_oid: receipt?.base_oid ?? null,
4300
+ write_mode: receipt?.mode ?? null,
4301
+ ...receipt?.host_id ? { host_id: receipt.host_id } : {},
3254
4302
  humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
3255
4303
  })
3256
4304
  };
@@ -3266,17 +4314,17 @@ function renderPublicationLog(input, added) {
3266
4314
  }
3267
4315
  async function readPageChanged(targetPath, content) {
3268
4316
  try {
3269
- return ok(await readFile11(targetPath, "utf8") !== content);
4317
+ return ok(await readFile12(targetPath, "utf8") !== content);
3270
4318
  } catch (error) {
3271
4319
  if (error.code === "ENOENT") return ok(true);
3272
4320
  return err("WRITE_FAILED", { path: targetPath, message: String(error) });
3273
4321
  }
3274
4322
  }
3275
4323
  async function previewPreparedPagePublication(input, vault) {
3276
- const schemaPath = join16(vault, "SCHEMA.md");
4324
+ const schemaPath = join24(vault, "SCHEMA.md");
3277
4325
  let schemaText;
3278
4326
  try {
3279
- schemaText = await readFile11(schemaPath, "utf8");
4327
+ schemaText = await readFile12(schemaPath, "utf8");
3280
4328
  } catch (error) {
3281
4329
  const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
3282
4330
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3288,10 +4336,10 @@ async function previewPreparedPagePublication(input, vault) {
3288
4336
  if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
3289
4337
  const pageChanged = await readPageChanged(input.targetPath, input.page.content);
3290
4338
  if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
3291
- const indexPath = join16(vault, "index.md");
4339
+ const indexPath = join24(vault, "index.md");
3292
4340
  let indexText;
3293
4341
  try {
3294
- indexText = await readFile11(indexPath, "utf8");
4342
+ indexText = await readFile12(indexPath, "utf8");
3295
4343
  } catch (error) {
3296
4344
  const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
3297
4345
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3302,10 +4350,10 @@ async function previewPreparedPagePublication(input, vault) {
3302
4350
  type: input.page.type
3303
4351
  });
3304
4352
  if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
3305
- const logPath = join16(vault, "log.md");
4353
+ const logPath = join24(vault, "log.md");
3306
4354
  let logText;
3307
4355
  try {
3308
- logText = await readFile11(logPath, "utf8");
4356
+ logText = await readFile12(logPath, "utf8");
3309
4357
  } catch (error) {
3310
4358
  const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
3311
4359
  return { exitCode: ExitCode.FILE_NOT_FOUND, result };
@@ -3328,93 +4376,159 @@ async function previewPreparedPagePublication(input, vault) {
3328
4376
  );
3329
4377
  }
3330
4378
  async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
3331
- let lock;
4379
+ const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
4380
+ if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
3332
4381
  try {
3333
- lock = acquireOwnedSyncLock(vault, {
3334
- summary: `page publish ${input.page.target}`,
3335
- ttlMinutes: 1
4382
+ const preflight = await deps.preflight({
4383
+ vault,
4384
+ command: `page publish ${input.page.target}`,
4385
+ lockToken: managed.data.ownerToken
4386
+ });
4387
+ if (!preflight.result.ok) {
4388
+ return { exitCode: preflight.exitCode, result: preflight.result };
4389
+ }
4390
+ const writeReceipt = preflight.result.data;
4391
+ const aggregateMode = resolveRootAggregateMode();
4392
+ if (writeReceipt.base_oid) {
4393
+ const head = git(vault, ["rev-parse", "HEAD"]);
4394
+ if (head !== writeReceipt.base_oid) {
4395
+ return {
4396
+ exitCode: ExitCode.PREFLIGHT_FAILED,
4397
+ result: err("PREFLIGHT_FAILED", {
4398
+ reason: "base-oid-drift",
4399
+ expected: writeReceipt.base_oid,
4400
+ actual: head
4401
+ })
4402
+ };
4403
+ }
4404
+ }
4405
+ let lock;
4406
+ try {
4407
+ lock = acquireOwnedSyncLock(vault, {
4408
+ summary: `page publish ${input.page.target}`,
4409
+ ttlMinutes: 1
4410
+ });
4411
+ } catch (error) {
4412
+ return {
4413
+ exitCode: ExitCode.WRITE_FAILED,
4414
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
4415
+ };
4416
+ }
4417
+ if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
4418
+ let primary;
4419
+ let released;
4420
+ try {
4421
+ primary = await runLockedPrimaryStages(input, vault, deps);
4422
+ } catch (error) {
4423
+ primary = lockedFailure(
4424
+ "schema",
4425
+ emptyLockedState(),
4426
+ err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
4427
+ );
4428
+ } finally {
4429
+ released = releaseOwnedSyncLock(lock.data);
4430
+ }
4431
+ const primaryState = primary?.ok ? primary.data : primary?.state;
4432
+ if (released === void 0 || !released.ok || !released.data.released) {
4433
+ return phaseFailure(
4434
+ "unlock",
4435
+ input,
4436
+ primaryState?.published ?? false,
4437
+ released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
4438
+ {
4439
+ primary_stage: primary && !primary.ok ? primary.stage : "complete",
4440
+ primary_error: primary && !primary.ok ? primary.cause.error : void 0
4441
+ }
4442
+ );
4443
+ }
4444
+ const unlockHook = await observeStage(deps, "unlock");
4445
+ if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
4446
+ if (primary === void 0) {
4447
+ return phaseFailure(
4448
+ "schema",
4449
+ input,
4450
+ false,
4451
+ err("WRITE_FAILED", { message: "locked publication produced no result" })
4452
+ );
4453
+ }
4454
+ if (!primary.ok) {
4455
+ return phaseFailure(
4456
+ primary.stage,
4457
+ input,
4458
+ primary.state.published,
4459
+ primary.cause,
4460
+ void 0,
4461
+ primary.exitCode
4462
+ );
4463
+ }
4464
+ const state = primary.data;
4465
+ const event = await writeLogEvent(vault, {
4466
+ schema: "skillwiki-log-event/v1",
4467
+ operation_id: input.operationId,
4468
+ occurred_at: `${input.date}T00:00:00.000Z`,
4469
+ host_id: writeReceipt.host_id ?? "standalone",
4470
+ actor: "skillwiki-cli",
4471
+ kind: "page-publish",
4472
+ target: input.page.target,
4473
+ note: input.logNote ?? `Published ${input.page.title}`,
4474
+ metadata: {
4475
+ page_type: input.page.type,
4476
+ // Stable for replay: do not embed schema-delta taxonomy_added (varies after first write).
4477
+ tags: [...input.page.tags].sort(),
4478
+ base_oid: writeReceipt.base_oid
4479
+ }
3336
4480
  });
3337
- } catch (error) {
3338
- return {
3339
- exitCode: ExitCode.WRITE_FAILED,
3340
- result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
3341
- };
3342
- }
3343
- if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
3344
- let primary;
3345
- let released;
3346
- try {
3347
- primary = await runLockedPrimaryStages(input, vault, deps);
3348
- } catch (error) {
3349
- primary = lockedFailure(
3350
- "schema",
3351
- emptyLockedState(),
3352
- err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
3353
- );
3354
- } finally {
3355
- released = releaseOwnedSyncLock(lock.data);
3356
- }
3357
- const primaryState = primary?.ok ? primary.data : primary?.state;
3358
- if (released === void 0 || !released.ok || !released.data.released) {
3359
- return phaseFailure(
3360
- "unlock",
3361
- input,
3362
- primaryState?.published ?? false,
3363
- released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
3364
- {
3365
- primary_stage: primary && !primary.ok ? primary.stage : "complete",
3366
- primary_error: primary && !primary.ok ? primary.cause.error : void 0
4481
+ if (!event.ok) {
4482
+ return phaseFailure(
4483
+ "event",
4484
+ input,
4485
+ true,
4486
+ event,
4487
+ void 0,
4488
+ event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode(event.error)
4489
+ );
4490
+ }
4491
+ if (event.data.created) state.changed.add(event.data.path);
4492
+ const eventHook = await observeStage(deps, "event");
4493
+ if (eventHook) return phaseFailure("event", input, true, eventHook);
4494
+ let logAppended = false;
4495
+ if (aggregateMode === "dual" || writeReceipt.mode !== "immutable-record") {
4496
+ if (aggregateMode === "dual") {
4497
+ const log = await runLogAppend({
4498
+ vault,
4499
+ content: renderPublicationLog(input, state.taxonomyAdded),
4500
+ operationId: input.operationId,
4501
+ strictLock: true,
4502
+ recordLastOp: false
4503
+ });
4504
+ if (!log.result.ok) return phaseFailure("log", input, true, log.result);
4505
+ if (log.exitCode !== ExitCode.OK) {
4506
+ return phaseFailure(
4507
+ "log",
4508
+ input,
4509
+ true,
4510
+ err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
4511
+ );
4512
+ }
4513
+ logAppended = log.result.data.appended;
4514
+ if (logAppended) state.changed.add("log.md");
4515
+ const logHook = await observeStage(deps, "log");
4516
+ if (logHook) return phaseFailure("log", input, true, logHook);
3367
4517
  }
3368
- );
3369
- }
3370
- const unlockHook = await observeStage(deps, "unlock");
3371
- if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
3372
- if (primary === void 0) {
3373
- return phaseFailure(
3374
- "schema",
4518
+ }
4519
+ return successReceipt(
3375
4520
  input,
4521
+ state.taxonomyAdded,
4522
+ state.pageChanged,
4523
+ state.indexUpdated,
4524
+ logAppended,
4525
+ [...state.changed],
3376
4526
  false,
3377
- err("WRITE_FAILED", { message: "locked publication produced no result" })
3378
- );
3379
- }
3380
- if (!primary.ok) {
3381
- return phaseFailure(
3382
- primary.stage,
3383
- input,
3384
- primary.state.published,
3385
- primary.cause,
3386
- void 0,
3387
- primary.exitCode
3388
- );
3389
- }
3390
- const state = primary.data;
3391
- const log = await runLogAppend({
3392
- vault,
3393
- content: renderPublicationLog(input, state.taxonomyAdded),
3394
- operationId: input.operationId,
3395
- strictLock: true,
3396
- recordLastOp: false
3397
- });
3398
- if (!log.result.ok) return phaseFailure("log", input, true, log.result);
3399
- if (log.exitCode !== ExitCode.OK) {
3400
- return phaseFailure(
3401
- "log",
3402
- input,
3403
- true,
3404
- err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
4527
+ writeReceipt
3405
4528
  );
4529
+ } finally {
4530
+ releaseManagedWriteLock(managed.data);
3406
4531
  }
3407
- if (log.result.data.appended) state.changed.add("log.md");
3408
- const logHook = await observeStage(deps, "log");
3409
- if (logHook) return phaseFailure("log", input, true, logHook);
3410
- return successReceipt(
3411
- input,
3412
- state.taxonomyAdded,
3413
- state.pageChanged,
3414
- state.indexUpdated,
3415
- log.result.data.appended,
3416
- [...state.changed]
3417
- );
3418
4532
  }
3419
4533
  async function runPagePublish(input, deps = DEFAULT_DEPS) {
3420
4534
  const prepared = await preparePagePublication(input);
@@ -3501,7 +4615,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
3501
4615
  }
3502
4616
  async function resolveRawCapture(input) {
3503
4617
  try {
3504
- const existing = await readFile12(input.path, "utf8");
4618
+ const existing = await readFile13(input.path, "utf8");
3505
4619
  const frontmatter = extractFrontmatter(existing);
3506
4620
  if (!frontmatter.ok) {
3507
4621
  return err("INGEST_VALIDATION_FAILED", {
@@ -3559,7 +4673,7 @@ async function acquireRawCaptureLock(path) {
3559
4673
  const lockPath = `${path}.ingest.lock`;
3560
4674
  for (let attempt = 0; attempt < 200; attempt++) {
3561
4675
  try {
3562
- const handle = await open(lockPath, "wx");
4676
+ const handle = await open2(lockPath, "wx");
3563
4677
  await handle.close();
3564
4678
  return ok(lockPath);
3565
4679
  } catch (error) {
@@ -3640,7 +4754,7 @@ async function runIngest(input) {
3640
4754
  sourceContent = fetchResult.data.body;
3641
4755
  } else {
3642
4756
  try {
3643
- sourceContent = await readFile12(input.source, "utf8");
4757
+ sourceContent = await readFile13(input.source, "utf8");
3644
4758
  } catch {
3645
4759
  return {
3646
4760
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -3665,7 +4779,7 @@ async function runIngest(input) {
3665
4779
  const rawRelPath = `raw/articles/${slug}.md`;
3666
4780
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
3667
4781
  const typedRelPath = `${typedDir}/${slug}.md`;
3668
- const rawAbsPath = join17(input.vault, rawRelPath);
4782
+ const rawAbsPath = join25(input.vault, rawRelPath);
3669
4783
  const identity = assessSourceIdentity({
3670
4784
  rawPath: rawRelPath,
3671
4785
  sourceUrl: sourceUrl ?? void 0,
@@ -3709,11 +4823,11 @@ async function runIngest(input) {
3709
4823
  );
3710
4824
  if (!input.dryRun) {
3711
4825
  try {
3712
- await mkdir9(join17(input.vault, typedDir), { recursive: true });
4826
+ await mkdir10(join25(input.vault, typedDir), { recursive: true });
3713
4827
  } catch (error) {
3714
4828
  return {
3715
4829
  exitCode: ExitCode.WRITE_FAILED,
3716
- result: err("WRITE_FAILED", { path: join17(input.vault, typedDir), message: String(error) })
4830
+ result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
3717
4831
  };
3718
4832
  }
3719
4833
  }
@@ -3760,11 +4874,11 @@ async function runIngest(input) {
3760
4874
  };
3761
4875
  }
3762
4876
  try {
3763
- await mkdir9(join17(input.vault, "raw", "articles"), { recursive: true });
4877
+ await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
3764
4878
  } catch (error) {
3765
4879
  return {
3766
4880
  exitCode: ExitCode.WRITE_FAILED,
3767
- result: err("WRITE_FAILED", { path: join17(input.vault, "raw", "articles"), message: String(error) })
4881
+ result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
3768
4882
  };
3769
4883
  }
3770
4884
  const rawWrite = await writeResolvedRaw({
@@ -3992,8 +5106,8 @@ ${body}`;
3992
5106
  }
3993
5107
 
3994
5108
  // src/commands/tag-reconcile.ts
3995
- import { readFile as readFile13 } from "fs/promises";
3996
- import { join as join18, posix } from "path";
5109
+ import { readFile as readFile14 } from "fs/promises";
5110
+ import { join as join26, posix } from "path";
3997
5111
  var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
3998
5112
  function errorExitCode2(error) {
3999
5113
  switch (error) {
@@ -4036,7 +5150,7 @@ function asTagArray(frontmatter, path) {
4036
5150
  async function readTagsFromFile(path) {
4037
5151
  let text;
4038
5152
  try {
4039
- text = await readFile13(path, "utf8");
5153
+ text = await readFile14(path, "utf8");
4040
5154
  } catch (error) {
4041
5155
  if (error.code === "ENOENT") {
4042
5156
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
@@ -4057,7 +5171,7 @@ async function resolveRequestedTags(input, page) {
4057
5171
  result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
4058
5172
  };
4059
5173
  }
4060
- const source = input.from ?? (explicit.length === 0 ? join18(input.vault, page) : void 0);
5174
+ const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
4061
5175
  if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
4062
5176
  const sourced = await readTagsFromFile(source);
4063
5177
  if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
@@ -4068,7 +5182,7 @@ async function resolveRequestedTags(input, page) {
4068
5182
  }
4069
5183
  async function readSchema(schemaPath) {
4070
5184
  try {
4071
- return { exitCode: ExitCode.OK, result: ok(await readFile13(schemaPath, "utf8")) };
5185
+ return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
4072
5186
  } catch (error) {
4073
5187
  if (error.code === "ENOENT") {
4074
5188
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
@@ -4096,7 +5210,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
4096
5210
  };
4097
5211
  }
4098
5212
  async function reconcileTagsWhileLocked(input, page, tags, comment) {
4099
- const schemaPath = join18(input.vault, "SCHEMA.md");
5213
+ const schemaPath = join26(input.vault, "SCHEMA.md");
4100
5214
  const current = await readSchema(schemaPath);
4101
5215
  if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
4102
5216
  const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
@@ -4107,7 +5221,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
4107
5221
  }
4108
5222
  let verifiedText;
4109
5223
  try {
4110
- verifiedText = await readFile13(schemaPath, "utf8");
5224
+ verifiedText = await readFile14(schemaPath, "utf8");
4111
5225
  } catch (error) {
4112
5226
  return {
4113
5227
  exitCode: ExitCode.WRITE_FAILED,
@@ -4133,7 +5247,7 @@ async function runTagReconcile(input) {
4133
5247
  const comment = taxonomyCommentForPage(page.data, date, input.reason);
4134
5248
  if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
4135
5249
  if (!input.write) {
4136
- const schema = await readSchema(join18(input.vault, "SCHEMA.md"));
5250
+ const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
4137
5251
  if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
4138
5252
  const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
4139
5253
  return previewResult(page.data, tags, preview, true, []);
@@ -4185,22 +5299,9 @@ async function runTagReconcile(input) {
4185
5299
  }
4186
5300
 
4187
5301
  // src/commands/sync.ts
4188
- import { existsSync as existsSync5 } from "fs";
4189
- import { join as join19 } from "path";
4190
- import { execFileSync as execFileSync2 } from "child_process";
4191
-
4192
- // src/utils/git.ts
4193
- import { execFileSync } from "child_process";
4194
- function git(cwd, args) {
4195
- try {
4196
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4197
- } catch {
4198
- return "";
4199
- }
4200
- }
4201
- function gitStrict(cwd, args) {
4202
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4203
- }
5302
+ import { existsSync as existsSync6 } from "fs";
5303
+ import { join as join27 } from "path";
5304
+ import { execFileSync as execFileSync3 } from "child_process";
4204
5305
 
4205
5306
  // src/utils/vault-git-pathspec.ts
4206
5307
  var VAULT_GENERATED_COMMIT_PATHS = [
@@ -4241,7 +5342,7 @@ function isTrackedNotePath(path) {
4241
5342
  }
4242
5343
  function refHasPath(vault, ref, path) {
4243
5344
  try {
4244
- execFileSync2("git", ["cat-file", "-e", `${ref}:${path}`], {
5345
+ execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
4245
5346
  cwd: vault,
4246
5347
  stdio: ["pipe", "pipe", "pipe"]
4247
5348
  });
@@ -4253,7 +5354,7 @@ function refHasPath(vault, ref, path) {
4253
5354
  function runSyncStatus(input) {
4254
5355
  const vault = input.vault;
4255
5356
  const includeStashes = input.includeStashes ?? false;
4256
- if (!existsSync5(join19(vault, ".git"))) {
5357
+ if (!existsSync6(join27(vault, ".git"))) {
4257
5358
  return {
4258
5359
  exitCode: ExitCode.VAULT_PATH_INVALID,
4259
5360
  result: ok({
@@ -4360,7 +5461,7 @@ function runSyncStatus(input) {
4360
5461
  }
4361
5462
  async function runSyncPush(input) {
4362
5463
  const vault = input.vault;
4363
- if (!existsSync5(join19(vault, ".git"))) {
5464
+ if (!existsSync6(join27(vault, ".git"))) {
4364
5465
  return {
4365
5466
  exitCode: ExitCode.VAULT_PATH_INVALID,
4366
5467
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -4520,100 +5621,44 @@ function enableGitLongPathsOnWindows(vault) {
4520
5621
  }
4521
5622
  async function runSyncPull(input) {
4522
5623
  const vault = input.vault;
4523
- if (!existsSync5(join19(vault, ".git"))) {
5624
+ if (!existsSync6(join27(vault, ".git"))) {
4524
5625
  return {
4525
5626
  exitCode: ExitCode.VAULT_PATH_INVALID,
4526
5627
  result: err("NOT_A_GIT_REPO", { path: vault })
4527
5628
  };
4528
5629
  }
4529
5630
  enableGitLongPathsOnWindows(vault);
4530
- let fetched = false;
4531
- try {
4532
- gitStrict(vault, ["fetch", "origin"]);
4533
- fetched = true;
4534
- } catch (e) {
5631
+ const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
5632
+ if (!remoteUrl) {
4535
5633
  return {
4536
5634
  exitCode: ExitCode.SYNC_PULL_FAILED,
4537
- result: err("GIT_FETCH_FAILED", { message: String(e) })
5635
+ result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
4538
5636
  };
4539
5637
  }
4540
- let pulled = false;
4541
- let conflicts = 0;
4542
- let filesUpdated = 0;
4543
- let autoResolved = 0;
4544
- try {
4545
- const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
4546
- pulled = true;
4547
- const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
4548
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4549
- } catch (e) {
4550
- const errString = String(e);
4551
- if (errString.includes("conflict")) {
4552
- let inConflict = true;
4553
- while (inConflict) {
4554
- const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
4555
- let commitMsg = "";
4556
- if (stoppedSha) {
4557
- commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
4558
- }
4559
- const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
4560
- const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
4561
- const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
4562
- if (conflictedList.length === 0) {
4563
- try {
4564
- gitStrict(vault, ["rebase", "--continue"]);
4565
- inConflict = true;
4566
- } catch {
4567
- inConflict = false;
4568
- }
4569
- continue;
4570
- }
4571
- if (isArchiveOrSnapshot) {
4572
- for (const f of conflictedList) {
4573
- try {
4574
- gitStrict(vault, ["checkout", "--ours", f]);
4575
- gitStrict(vault, ["add", f]);
4576
- } catch {
4577
- }
4578
- }
4579
- autoResolved += conflictedList.length;
4580
- try {
4581
- gitStrict(vault, ["rebase", "--continue"]);
4582
- } catch (continueErr) {
4583
- continue;
4584
- }
4585
- } else {
4586
- conflicts = conflictedList.length;
4587
- return {
4588
- exitCode: ExitCode.SYNC_PULL_FAILED,
4589
- result: ok({
4590
- fetched,
4591
- pulled: false,
4592
- files_updated: 0,
4593
- conflicts,
4594
- auto_resolved: 0,
4595
- lint_errors: 0,
4596
- lint_warnings: 0,
4597
- humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
4598
- })
4599
- };
4600
- }
4601
- }
4602
- if (autoResolved > 0) {
4603
- const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
4604
- if (diffOutput) {
4605
- const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
4606
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
4607
- }
4608
- pulled = true;
4609
- conflicts = 0;
4610
- }
4611
- } else {
5638
+ const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
5639
+ if (!helper.ok) {
5640
+ if (helper.error === "PREFLIGHT_FAILED") {
4612
5641
  return {
4613
- exitCode: ExitCode.SYNC_PULL_FAILED,
4614
- result: err("GIT_PULL_FAILED", { message: errString })
5642
+ exitCode: ExitCode.PREFLIGHT_FAILED,
5643
+ result: err("PREFLIGHT_FAILED", helper.detail)
4615
5644
  };
4616
5645
  }
5646
+ return {
5647
+ exitCode: ExitCode.SYNC_PULL_FAILED,
5648
+ result: err("GIT_PULL_FAILED", helper.detail)
5649
+ };
5650
+ }
5651
+ const fetched = true;
5652
+ const pulled = helper.data.changed;
5653
+ const conflicts = 0;
5654
+ const autoResolved = 0;
5655
+ let filesUpdated = 0;
5656
+ if (helper.data.changed) {
5657
+ const diffOutput = git(vault, ["diff", "--stat", `${helper.data.before_oid}..${helper.data.after_oid}`]);
5658
+ if (diffOutput) {
5659
+ const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
5660
+ if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
5661
+ }
4617
5662
  }
4618
5663
  const pathFix = await fixPathTooLong({ vault });
4619
5664
  const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
@@ -4626,8 +5671,7 @@ async function runSyncPull(input) {
4626
5671
  }
4627
5672
  const hintParts = [];
4628
5673
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
4629
- else hintParts.push("already up to date");
4630
- if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
5674
+ else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
4631
5675
  if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
4632
5676
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
4633
5677
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
@@ -4692,7 +5736,7 @@ function runSyncPeers(input) {
4692
5736
  }
4693
5737
  function runSyncLock(input) {
4694
5738
  const vault = input.vault;
4695
- if (!existsSync5(vault)) {
5739
+ if (!existsSync6(vault)) {
4696
5740
  return {
4697
5741
  exitCode: ExitCode.VAULT_PATH_INVALID,
4698
5742
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4727,7 +5771,7 @@ function runSyncLock(input) {
4727
5771
  }
4728
5772
  function runSyncUnlock(input) {
4729
5773
  const vault = input.vault;
4730
- if (!existsSync5(vault)) {
5774
+ if (!existsSync6(vault)) {
4731
5775
  return {
4732
5776
  exitCode: ExitCode.VAULT_PATH_INVALID,
4733
5777
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -4759,9 +5803,53 @@ function runSyncUnlock(input) {
4759
5803
  };
4760
5804
  }
4761
5805
 
5806
+ // src/commands/sync-journal.ts
5807
+ function runSyncJournalList(input) {
5808
+ const byPhase = {};
5809
+ for (const opId of listJournalOpIds(input.vault)) {
5810
+ const fields = readJournal(input.vault, opId);
5811
+ const phase = fields?.phase ?? "unknown";
5812
+ byPhase[phase] = (byPhase[phase] ?? 0) + 1;
5813
+ }
5814
+ const review = listReviewRequiredOps(input.vault).map(({ opId, fields }) => ({
5815
+ operation_id: opId,
5816
+ reason: fields.reason,
5817
+ target_oid: fields.target_oid,
5818
+ original_head: fields.original_head
5819
+ }));
5820
+ const total = Object.values(byPhase).reduce((a, b) => a + b, 0);
5821
+ const hint = review.length === 0 ? `journals: ${total} total; no review-required handoffs` : `journals: ${total} total; ${review.length} review-required \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`;
5822
+ return {
5823
+ exitCode: ExitCode.OK,
5824
+ result: ok({
5825
+ total,
5826
+ by_phase: byPhase,
5827
+ review_required: review,
5828
+ humanHint: hint
5829
+ })
5830
+ };
5831
+ }
5832
+ function runSyncJournalClearStale(input) {
5833
+ const { superseded, skipped } = supersedeStaleReviewRequiredJournals(input.vault, {
5834
+ dryRun: !!input.dryRun,
5835
+ by: input.dryRun ? "skillwiki-sync-journal-clear-stale-dry-run" : "skillwiki-sync-journal-clear-stale"
5836
+ });
5837
+ const mode = input.dryRun ? "dry-run" : "write";
5838
+ const hint = superseded.length === 0 ? `clear-stale (${mode}): nothing to supersede; skipped=${skipped.length}` : `clear-stale (${mode}): ${superseded.length} journal(s); skipped=${skipped.length}`;
5839
+ return {
5840
+ exitCode: ExitCode.OK,
5841
+ result: ok({
5842
+ dry_run: !!input.dryRun,
5843
+ superseded,
5844
+ skipped,
5845
+ humanHint: hint
5846
+ })
5847
+ };
5848
+ }
5849
+
4762
5850
  // src/commands/backup.ts
4763
- import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4764
- import { join as join20, relative as relative2, dirname as dirname6 } from "path";
5851
+ import { statSync as statSync2, readdirSync, readFileSync as readFileSync11, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
5852
+ import { join as join28, relative as relative2, dirname as dirname6 } from "path";
4765
5853
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
4766
5854
 
4767
5855
  // src/utils/s3-client.ts
@@ -4785,7 +5873,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
4785
5873
  function* walkMarkdown(dir, base) {
4786
5874
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
4787
5875
  if (SKIP_DIRS.has(entry.name)) continue;
4788
- const full = join20(dir, entry.name);
5876
+ const full = join28(dir, entry.name);
4789
5877
  if (entry.isDirectory()) {
4790
5878
  yield* walkMarkdown(full, base);
4791
5879
  } else if (entry.name.endsWith(".md")) {
@@ -4808,7 +5896,7 @@ async function runBackupSync(input) {
4808
5896
  let failed = 0;
4809
5897
  const files = [...walkMarkdown(input.vault, input.vault)];
4810
5898
  for (const relPath of files) {
4811
- const absPath = join20(input.vault, relPath);
5899
+ const absPath = join28(input.vault, relPath);
4812
5900
  const localStat = statSync2(absPath);
4813
5901
  let needsUpload = true;
4814
5902
  try {
@@ -4827,7 +5915,7 @@ async function runBackupSync(input) {
4827
5915
  continue;
4828
5916
  }
4829
5917
  try {
4830
- const body = readFileSync4(absPath);
5918
+ const body = readFileSync11(absPath);
4831
5919
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
4832
5920
  uploaded++;
4833
5921
  } catch {
@@ -4884,7 +5972,7 @@ async function runBackupRestore(input) {
4884
5972
  const objects = list.Contents ?? [];
4885
5973
  for (const obj of objects) {
4886
5974
  if (!obj.Key) continue;
4887
- const localPath = join20(target, obj.Key);
5975
+ const localPath = join28(target, obj.Key);
4888
5976
  try {
4889
5977
  const localStat = statSync2(localPath);
4890
5978
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -4898,7 +5986,7 @@ async function runBackupRestore(input) {
4898
5986
  const body = await resp.Body?.transformToByteArray();
4899
5987
  if (body) {
4900
5988
  mkdirSync2(dirname6(localPath), { recursive: true });
4901
- writeFileSync2(localPath, Buffer.from(body));
5989
+ writeFileSync4(localPath, Buffer.from(body));
4902
5990
  downloaded++;
4903
5991
  }
4904
5992
  } catch {
@@ -4930,11 +6018,11 @@ async function runBackupRestore(input) {
4930
6018
  }
4931
6019
 
4932
6020
  // src/commands/status.ts
4933
- import { existsSync as existsSync6, statSync as statSync3 } from "fs";
4934
- import { readFile as readFile14 } from "fs/promises";
4935
- import { join as join21 } from "path";
6021
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
6022
+ import { readFile as readFile15 } from "fs/promises";
6023
+ import { join as join29 } from "path";
4936
6024
  async function runStatus(input) {
4937
- if (!existsSync6(input.vault)) {
6025
+ if (!existsSync7(input.vault)) {
4938
6026
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
4939
6027
  }
4940
6028
  const scan = await scanVault(input.vault);
@@ -4959,7 +6047,7 @@ async function runStatus(input) {
4959
6047
  const compound = scan.data.compound.length;
4960
6048
  let schemaVersion = "v1";
4961
6049
  try {
4962
- const schemaContent = await readFile14(join21(input.vault, "SCHEMA.md"), "utf8");
6050
+ const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
4963
6051
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
4964
6052
  if (versionMatch) schemaVersion = versionMatch[1];
4965
6053
  } catch {
@@ -5019,8 +6107,8 @@ async function runStatus(input) {
5019
6107
  }
5020
6108
 
5021
6109
  // src/commands/seed.ts
5022
- import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
5023
- import { join as join22 } from "path";
6110
+ import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
6111
+ import { join as join30 } from "path";
5024
6112
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5025
6113
  var EXAMPLE_PAGES = {
5026
6114
  "entities/example-project.md": `---
@@ -5089,29 +6177,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
5089
6177
  `;
5090
6178
  async function runSeed(input) {
5091
6179
  try {
5092
- await stat4(join22(input.vault, "SCHEMA.md"));
6180
+ await stat4(join30(input.vault, "SCHEMA.md"));
5093
6181
  } catch {
5094
6182
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
5095
6183
  }
5096
6184
  const created = [];
5097
6185
  const skipped = [];
5098
6186
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
5099
- const absPath = join22(input.vault, relPath);
6187
+ const absPath = join30(input.vault, relPath);
5100
6188
  try {
5101
6189
  await stat4(absPath);
5102
6190
  skipped.push(relPath);
5103
6191
  } catch {
5104
- await mkdir10(join22(absPath, ".."), { recursive: true });
6192
+ await mkdir11(join30(absPath, ".."), { recursive: true });
5105
6193
  await writeFile9(absPath, content, "utf8");
5106
6194
  created.push(relPath);
5107
6195
  }
5108
6196
  }
5109
- const rawPath = join22(input.vault, "raw", "articles", "example-source.md");
6197
+ const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
5110
6198
  try {
5111
6199
  await stat4(rawPath);
5112
6200
  skipped.push("raw/articles/example-source.md");
5113
6201
  } catch {
5114
- await mkdir10(join22(rawPath, ".."), { recursive: true });
6202
+ await mkdir11(join30(rawPath, ".."), { recursive: true });
5115
6203
  await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
5116
6204
  created.push("raw/articles/example-source.md");
5117
6205
  }
@@ -5134,9 +6222,9 @@ async function runSeed(input) {
5134
6222
  }
5135
6223
 
5136
6224
  // src/commands/canvas.ts
5137
- import { readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
5138
- import { existsSync as existsSync7 } from "fs";
5139
- import { join as join23 } from "path";
6225
+ import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
6226
+ import { existsSync as existsSync8 } from "fs";
6227
+ import { join as join31 } from "path";
5140
6228
  var NODE_WIDTH = 240;
5141
6229
  var NODE_HEIGHT = 60;
5142
6230
  var COLUMN_SPACING = 400;
@@ -5214,8 +6302,8 @@ function buildCanvasEdges(adjacency) {
5214
6302
  return edges;
5215
6303
  }
5216
6304
  async function runCanvasGenerate(input) {
5217
- const graphPath = input.graphPath ?? join23(input.vault, ".skillwiki", "graph.json");
5218
- if (!existsSync7(graphPath)) {
6305
+ const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
6306
+ if (!existsSync8(graphPath)) {
5219
6307
  return {
5220
6308
  exitCode: ExitCode.FILE_NOT_FOUND,
5221
6309
  result: err("FILE_NOT_FOUND", {
@@ -5226,7 +6314,7 @@ async function runCanvasGenerate(input) {
5226
6314
  }
5227
6315
  let raw;
5228
6316
  try {
5229
- raw = await readFile15(graphPath, "utf8");
6317
+ raw = await readFile16(graphPath, "utf8");
5230
6318
  } catch (e) {
5231
6319
  return {
5232
6320
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5252,7 +6340,7 @@ async function runCanvasGenerate(input) {
5252
6340
  const nodes = buildCanvasNodes(paths);
5253
6341
  const edges = buildCanvasEdges(graph.adjacency);
5254
6342
  const canvas = { nodes, edges };
5255
- const outPath = join23(input.vault, "vault-graph.canvas");
6343
+ const outPath = join31(input.vault, "vault-graph.canvas");
5256
6344
  try {
5257
6345
  await writeFile10(outPath, JSON.stringify(canvas, null, 2));
5258
6346
  } catch (e) {
@@ -5274,15 +6362,15 @@ written: ${outPath}`
5274
6362
  }
5275
6363
 
5276
6364
  // src/commands/fleet-health.ts
5277
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
6365
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
5278
6366
  import { execSync as nodeExecSync } from "child_process";
5279
6367
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
5280
- import { join as join24 } from "path";
6368
+ import { join as join32 } from "path";
5281
6369
  var SSH_TIMEOUT_MS = 15e3;
5282
6370
  var TIMER_UNIT = "agent-memory-trends.timer";
5283
6371
  var SERVICE_UNIT = "agent-memory-trends.service";
5284
6372
  var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
5285
- function defaultDeps() {
6373
+ function defaultDeps2() {
5286
6374
  return {
5287
6375
  platform: () => nodePlatform(),
5288
6376
  execSync: nodeExecSync
@@ -5374,9 +6462,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
5374
6462
  function probeLocal(vaultPath, deps) {
5375
6463
  const latestPath = satelliteLatestRunPath(vaultPath);
5376
6464
  let parsed = null;
5377
- if (existsSync8(latestPath)) {
6465
+ if (existsSync9(latestPath)) {
5378
6466
  try {
5379
- const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
6467
+ const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
5380
6468
  if (wire) {
5381
6469
  parsed = {
5382
6470
  status: wire.status,
@@ -5502,12 +6590,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
5502
6590
  return true;
5503
6591
  }
5504
6592
  async function runFleetHealth(input) {
5505
- const deps = input.deps ?? defaultDeps();
6593
+ const deps = input.deps ?? defaultDeps2();
5506
6594
  const env = input.env ?? process.env;
5507
6595
  const home = input.home ?? env.HOME ?? "";
5508
6596
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
5509
6597
  const vault = input.vault ?? env.WIKI_PATH;
5510
- const file = input.file ?? (vault ? join24(vault, FLEET_REL_PATH) : void 0);
6598
+ const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
5511
6599
  if (!file) {
5512
6600
  return {
5513
6601
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -5591,15 +6679,15 @@ async function runFleetHealth(input) {
5591
6679
  }
5592
6680
 
5593
6681
  // src/utils/auto-commit.ts
5594
- import { existsSync as existsSync9 } from "fs";
5595
- import { join as join25 } from "path";
6682
+ import { existsSync as existsSync10 } from "fs";
6683
+ import { join as join33 } from "path";
5596
6684
  async function postCommit(vault, exitCode) {
5597
6685
  if (exitCode !== 0) return;
5598
6686
  const home = process.env.HOME ?? "";
5599
6687
  const dotenv = await parseDotenvFile(configPath(home));
5600
6688
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
5601
6689
  if (autoCommit === "false") return;
5602
- if (!existsSync9(join25(vault, ".git"))) return;
6690
+ if (!existsSync10(join33(vault, ".git"))) return;
5603
6691
  const lastOps = readLastOp(vault);
5604
6692
  if (lastOps.length === 0) return;
5605
6693
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -5622,103 +6710,6 @@ async function postCommit(vault, exitCode) {
5622
6710
  clearLastOp(vault);
5623
6711
  }
5624
6712
 
5625
- // src/utils/protected-vault-write-guard.ts
5626
- import { readFileSync as readFileSync6 } from "fs";
5627
- import { join as join26, resolve as resolvePath } from "path";
5628
- async function guardProtectedVaultWrite(input) {
5629
- const env = input.env ?? process.env;
5630
- const home = input.home ?? process.env.HOME ?? "";
5631
- const cwd = input.cwd ?? process.cwd();
5632
- const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
5633
- const load = await loadFleetManifestAndHost({
5634
- vault: liveVaultPath ?? input.vault,
5635
- hostId: input.hostId,
5636
- env,
5637
- home,
5638
- cwd,
5639
- osHostname: input.osHostname ?? process.env.HOSTNAME,
5640
- user: input.user ?? process.env.USER
5641
- });
5642
- if (!load?.hostId || load.identityStatus !== "known") {
5643
- return { blocked: false };
5644
- }
5645
- const host = load.manifest.hosts[load.hostId];
5646
- if (!host || host.role !== "snapshotter" || host.protected !== true) {
5647
- return { blocked: false };
5648
- }
5649
- const snapshotWorktree = resolveSnapshotWorktree(home);
5650
- const targetVault = resolvePath(input.vault);
5651
- const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
5652
- const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
5653
- if (canonicalLiveVault && targetVault === canonicalLiveVault) {
5654
- return { blocked: false };
5655
- }
5656
- if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
5657
- return {
5658
- blocked: true,
5659
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5660
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5661
- host_id: load.hostId,
5662
- command: input.command,
5663
- reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
5664
- guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
5665
- })
5666
- };
5667
- }
5668
- if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
5669
- return {
5670
- blocked: true,
5671
- exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
5672
- result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
5673
- host_id: load.hostId,
5674
- command: input.command,
5675
- reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
5676
- guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
5677
- })
5678
- };
5679
- }
5680
- return { blocked: false };
5681
- }
5682
- async function resolveLiveVaultPath(input) {
5683
- const resolved = await resolveRuntimePath({
5684
- flag: void 0,
5685
- envValue: input.env.WIKI_PATH,
5686
- wikiEnv: input.env.WIKI,
5687
- home: input.home,
5688
- cwd: input.cwd
5689
- });
5690
- return resolved.ok ? resolved.data.path : void 0;
5691
- }
5692
- function resolveSnapshotWorktree(home) {
5693
- const skillwikiEnv = join26(home, ".skillwiki", ".env");
5694
- const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
5695
- if (explicitWorktree) return explicitWorktree;
5696
- const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
5697
- if (snapshotProfile) {
5698
- const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
5699
- if (fromProfile) return fromProfile;
5700
- }
5701
- return "/root/wiki-git";
5702
- }
5703
- function readEnvKey(path, keys) {
5704
- try {
5705
- const content = readFileSync6(path, "utf8");
5706
- for (const line of content.split(/\r?\n/)) {
5707
- const trimmed = line.trim();
5708
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
5709
- const eq = trimmed.indexOf("=");
5710
- if (eq <= 0) continue;
5711
- const key = trimmed.slice(0, eq).trim();
5712
- if (!keys.includes(key)) continue;
5713
- const value = trimmed.slice(eq + 1).trim();
5714
- if (value.length > 0) return value;
5715
- }
5716
- } catch {
5717
- return void 0;
5718
- }
5719
- return void 0;
5720
- }
5721
-
5722
6713
  // src/cli.ts
5723
6714
  var pkg = readCliPackageJson();
5724
6715
  var program = new Command();
@@ -5745,6 +6736,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
5745
6736
  }
5746
6737
  return emit(await run(), vault, opts);
5747
6738
  }
6739
+ async function emitManagedVaultWrite(vault, command, mutate, opts) {
6740
+ const guard = await guardProtectedVaultWrite({
6741
+ vault,
6742
+ command,
6743
+ env: process.env,
6744
+ home: process.env.HOME ?? "",
6745
+ cwd: process.cwd(),
6746
+ osHostname: process.env.HOSTNAME,
6747
+ user: process.env.USER
6748
+ });
6749
+ if (guard.blocked) {
6750
+ return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
6751
+ }
6752
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-PW4OOOMV.js");
6753
+ const run = await runManagedWriteTransaction2({
6754
+ vault,
6755
+ command,
6756
+ allowImmutableRecord: opts?.allowImmutableRecord === true,
6757
+ mutate: async (receipt) => await mutate(receipt)
6758
+ });
6759
+ return emit(run, vault, opts);
6760
+ }
5748
6761
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
5749
6762
  program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
5750
6763
  program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
@@ -5755,12 +6768,12 @@ program.command("validate <file>").description("validate vault page frontmatter
5755
6768
  else vault = v.vault;
5756
6769
  }
5757
6770
  if (opts.apply && vault) {
5758
- return emitGuardedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
6771
+ return emitManagedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
5759
6772
  }
5760
6773
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
5761
6774
  });
5762
6775
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5763
- const out = opts.out ?? join27(vault, ".skillwiki", "graph.json");
6776
+ const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
5764
6777
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
5765
6778
  });
5766
6779
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -5893,6 +6906,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
5893
6906
  { postCommit: false }
5894
6907
  );
5895
6908
  });
6909
+ var indexCmd = program.command("index").description("root index projection and checks");
6910
+ 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) => {
6911
+ const v = await resolveVaultArg(vault, opts.wiki);
6912
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6913
+ else emit(await runIndexRebuild({ vault: v.vault, write: Boolean(opts.write) }), v.vault);
6914
+ });
5896
6915
  program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5897
6916
  const v = await resolveVaultArg(vault, opts.wiki);
5898
6917
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -5945,12 +6964,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
5945
6964
  program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5946
6965
  const v = await resolveVaultArg(vault, opts.wiki);
5947
6966
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5948
- else return emitGuardedVaultWrite(
6967
+ else return emitManagedVaultWrite(
5949
6968
  v.vault,
5950
6969
  "log-append",
5951
6970
  () => runLogAppend({ vault: v.vault, content: opts.content })
5952
6971
  );
5953
6972
  });
6973
+ var logCmd = program.command("log").description("immutable log events and projections");
6974
+ 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) => {
6975
+ const v = await resolveVaultArg(vault, opts.wiki);
6976
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6977
+ else if (opts.write) {
6978
+ return emitManagedVaultWrite(
6979
+ v.vault,
6980
+ "log materialize",
6981
+ () => runLogMaterialize({ vault: v.vault, write: true })
6982
+ );
6983
+ } else emit(await runLogMaterialize({ vault: v.vault, write: false }), v.vault);
6984
+ });
6985
+ 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) => {
6986
+ const v = await resolveVaultArg(vault, opts.wiki);
6987
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6988
+ else if (opts.write) {
6989
+ return emitManagedVaultWrite(
6990
+ v.vault,
6991
+ "log migrate-legacy",
6992
+ () => runLogMigrateLegacy({ vault: v.vault, write: true })
6993
+ );
6994
+ } else emit(await runLogMigrateLegacy({ vault: v.vault, write: false }), v.vault);
6995
+ });
6996
+ var projectionsCmd = program.command("projections").description("root index/log projection materialization");
6997
+ 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) => {
6998
+ const v = await resolveVaultArg(vault, opts.wiki);
6999
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7000
+ else if (opts.write) {
7001
+ return emitManagedVaultWrite(
7002
+ v.vault,
7003
+ "projections materialize",
7004
+ () => runProjectionsMaterialize({ vault: v.vault, write: true })
7005
+ );
7006
+ } else emit(await runProjectionsMaterialize({ vault: v.vault, write: false }), v.vault);
7007
+ });
5954
7008
  program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5955
7009
  const v = await resolveVaultArg(vault, opts.wiki);
5956
7010
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6045,7 +7099,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
6045
7099
  remoteDelete: !!opts.remoteDelete,
6046
7100
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
6047
7101
  }), v.vault);
6048
- else return emitGuardedVaultWrite(
7102
+ else return emitManagedVaultWrite(
6049
7103
  v.vault,
6050
7104
  "archive",
6051
7105
  () => runArchive({
@@ -6062,7 +7116,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
6062
7116
  program.command("remove <page> [vault]").description("remove a vault path and write a delete-intent tombstone").option("--wiki <name>", "wiki profile name").option("--remote <remote>", "rclone remote root to prune the live path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the live path from the remote after local remove", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").option("--reason <text>", "stored on the delete-intent tombstone").action(async (page, vault, opts) => {
6063
7117
  const v = await resolveVaultArg(vault, opts.wiki);
6064
7118
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6065
- else return emitGuardedVaultWrite(
7119
+ else return emitManagedVaultWrite(
6066
7120
  v.vault,
6067
7121
  "remove",
6068
7122
  () => runRemove({
@@ -6235,6 +7289,35 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
6235
7289
  () => runSyncPull({ vault: v.vault })
6236
7290
  );
6237
7291
  });
7292
+ 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) => {
7293
+ const v = await resolveVaultArg(vault, opts.wiki);
7294
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7295
+ else {
7296
+ return emitGuardedVaultWrite(
7297
+ v.vault,
7298
+ "sync resolve-derived",
7299
+ () => runDerivedConflictResolution({ vault: v.vault, operationId: opts.operationId })
7300
+ );
7301
+ }
7302
+ });
7303
+ var syncJournalCmd = syncCmd.command("journal").description("inspect or clear vault-sync operation journals");
7304
+ syncJournalCmd.command("list [vault]").description("list vault-sync operation journals and review-required handoffs").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7305
+ const v = await resolveVaultArg(vault, opts.wiki);
7306
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7307
+ else emit(runSyncJournalList({ vault: v.vault }));
7308
+ });
7309
+ syncJournalCmd.command("clear-stale [vault]").description("supersede stale review-required journals when worktree is clean").option("--dry-run", "report what would be cleared without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7310
+ const v = await resolveVaultArg(vault, opts.wiki);
7311
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7312
+ else if (opts.dryRun) emit(runSyncJournalClearStale({ vault: v.vault, dryRun: true }));
7313
+ else {
7314
+ return emitGuardedVaultWrite(
7315
+ v.vault,
7316
+ "sync journal clear-stale",
7317
+ async () => runSyncJournalClearStale({ vault: v.vault, dryRun: false })
7318
+ );
7319
+ }
7320
+ });
6238
7321
  syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6239
7322
  const v = await resolveVaultArg(vault, opts.wiki);
6240
7323
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6334,7 +7417,7 @@ program.command("observe [vault]").description("create a raw transcript observat
6334
7417
  program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6335
7418
  const v = await resolveVaultArg(vault, opts.wiki);
6336
7419
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6337
- else if (opts.write) return emitGuardedVaultWrite(
7420
+ else if (opts.write) return emitManagedVaultWrite(
6338
7421
  v.vault,
6339
7422
  "session-brief --write",
6340
7423
  () => runSessionBrief({