skillwiki 0.10.0 → 0.10.2

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.
@@ -562,6 +562,305 @@ function safeUserName() {
562
562
  }
563
563
  }
564
564
 
565
+ // src/utils/git.ts
566
+ import { execFileSync } from "child_process";
567
+ function git(cwd, args) {
568
+ try {
569
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
570
+ } catch {
571
+ return "";
572
+ }
573
+ }
574
+ function gitStrict(cwd, args) {
575
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
576
+ }
577
+
578
+ // src/utils/operation-journal.ts
579
+ import {
580
+ existsSync,
581
+ mkdirSync,
582
+ readdirSync,
583
+ readFileSync,
584
+ renameSync,
585
+ writeFileSync
586
+ } from "fs";
587
+ import { join as join2 } from "path";
588
+ function journalDir(vault) {
589
+ const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/operations"]);
590
+ if (!gitPath) return null;
591
+ return gitPath.startsWith("/") ? gitPath : join2(vault, gitPath);
592
+ }
593
+ function parseJournalEnv(text) {
594
+ return Object.fromEntries(
595
+ text.split("\n").filter((line) => line.includes("=")).map((line) => {
596
+ const i = line.indexOf("=");
597
+ return [line.slice(0, i), line.slice(i + 1)];
598
+ })
599
+ );
600
+ }
601
+ function serializeJournalEnv(fields, preferredOrder = []) {
602
+ const keys = [...preferredOrder.filter((k) => k in fields), ...Object.keys(fields).filter((k) => !preferredOrder.includes(k))];
603
+ const seen = /* @__PURE__ */ new Set();
604
+ const lines = [];
605
+ for (const k of keys) {
606
+ if (seen.has(k)) continue;
607
+ seen.add(k);
608
+ lines.push(`${k}=${fields[k]}`);
609
+ }
610
+ return lines.join("\n") + "\n";
611
+ }
612
+ function readJournal(vault, opId) {
613
+ const dir = journalDir(vault);
614
+ if (!dir) return null;
615
+ const path = join2(dir, `${opId}.env`);
616
+ if (!existsSync(path)) return null;
617
+ try {
618
+ return parseJournalEnv(readFileSync(path, "utf8"));
619
+ } catch {
620
+ return null;
621
+ }
622
+ }
623
+ function writeJournal(vault, opId, fields) {
624
+ const dir = journalDir(vault);
625
+ if (!dir) return false;
626
+ try {
627
+ mkdirSync(dir, { recursive: true });
628
+ const path = join2(dir, `${opId}.env`);
629
+ const tmp = `${path}.tmp.${process.pid}`;
630
+ const order = [
631
+ "operation_id",
632
+ "phase",
633
+ "retry_count",
634
+ "original_branch",
635
+ "original_head",
636
+ "target_oid",
637
+ "owned_stash_oid",
638
+ "preservation_scope",
639
+ "lock_identity",
640
+ "helper_version",
641
+ "deployed_runtime_hash",
642
+ "conflict_identity",
643
+ "handoff",
644
+ "reason",
645
+ "prior_reason",
646
+ "superseded_at",
647
+ "cleared_reason",
648
+ "cleared_by",
649
+ "worktree_path",
650
+ "worktree_git_dir",
651
+ "inventory_path"
652
+ ];
653
+ writeFileSync(tmp, serializeJournalEnv(fields, order), "utf8");
654
+ renameSync(tmp, path);
655
+ return true;
656
+ } catch {
657
+ return false;
658
+ }
659
+ }
660
+ function listJournalOpIds(vault) {
661
+ const dir = journalDir(vault);
662
+ if (!dir || !existsSync(dir)) return [];
663
+ try {
664
+ return readdirSync(dir).filter((f) => f.endsWith(".env")).map((f) => f.replace(/\.env$/, "")).sort();
665
+ } catch {
666
+ return [];
667
+ }
668
+ }
669
+ function listReviewRequiredOps(vault) {
670
+ const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
671
+ const out = [];
672
+ for (const opId of listJournalOpIds(vault)) {
673
+ const fields = readJournal(vault, opId);
674
+ if (!fields) continue;
675
+ if (fields.phase !== "review-required" || fields.handoff !== "1") continue;
676
+ const journalGitDir = fields.worktree_git_dir ?? "";
677
+ if (!journalGitDir || !currentGitDir || journalGitDir === currentGitDir) {
678
+ out.push({ opId, fields });
679
+ }
680
+ }
681
+ return out;
682
+ }
683
+ function findReviewRequiredOp(vault) {
684
+ return listReviewRequiredOps(vault)[0]?.opId;
685
+ }
686
+ function hasUnmergedPaths(vault) {
687
+ const unmergedRaw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
688
+ return unmergedRaw ? unmergedRaw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
689
+ }
690
+ function hasActiveGitSequencer(vault) {
691
+ const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
692
+ if (!gitDir) return false;
693
+ for (const m of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
694
+ if (existsSync(join2(gitDir, m))) return true;
695
+ }
696
+ if (existsSync(join2(gitDir, "rebase-merge")) || existsSync(join2(gitDir, "rebase-apply"))) {
697
+ return true;
698
+ }
699
+ return false;
700
+ }
701
+ function isWorktreeClean(vault) {
702
+ const porcelain = git(vault, ["status", "--porcelain"]);
703
+ return !porcelain || porcelain.trim() === "";
704
+ }
705
+ function canSupersedeJournal(vault, fields) {
706
+ if (hasUnmergedPaths(vault).length > 0) return false;
707
+ if (hasActiveGitSequencer(vault)) return false;
708
+ if (!isWorktreeClean(vault)) return false;
709
+ const target = fields.target_oid?.trim();
710
+ if (!target) return false;
711
+ const head = git(vault, ["rev-parse", "HEAD"]);
712
+ if (!head) return false;
713
+ return gitMergeBaseIsAncestor(vault, target, head);
714
+ }
715
+ function gitMergeBaseIsAncestor(vault, ancestor, tip) {
716
+ if (ancestor === tip) return true;
717
+ const mb = git(vault, ["merge-base", ancestor, tip]);
718
+ return mb !== "" && mb === ancestor;
719
+ }
720
+ function markJournalSuperseded(vault, opId, fields, by) {
721
+ const next = { ...fields };
722
+ if (next.reason && next.reason !== "superseded-stale-review-required") {
723
+ next.prior_reason = next.prior_reason || next.reason;
724
+ }
725
+ next.phase = "complete";
726
+ next.reason = "superseded-stale-review-required";
727
+ next.superseded_at = (/* @__PURE__ */ new Date()).toISOString();
728
+ next.cleared_by = by;
729
+ next.cleared_reason = `operator-or-preflight ${next.superseded_at}`;
730
+ if (!next.handoff) next.handoff = "1";
731
+ if (!next.operation_id) next.operation_id = opId;
732
+ return writeJournal(vault, opId, next);
733
+ }
734
+ function supersedeStaleReviewRequiredJournals(vault, opts = {}) {
735
+ const by = opts.by ?? "skillwiki-preflight";
736
+ const superseded = [];
737
+ const skipped = [];
738
+ if (hasUnmergedPaths(vault).length > 0 || hasActiveGitSequencer(vault) || !isWorktreeClean(vault)) {
739
+ for (const { opId } of listReviewRequiredOps(vault)) skipped.push(opId);
740
+ return { superseded, skipped };
741
+ }
742
+ for (const { opId, fields } of listReviewRequiredOps(vault)) {
743
+ if (!canSupersedeJournal(vault, fields)) {
744
+ skipped.push(opId);
745
+ continue;
746
+ }
747
+ if (opts.dryRun) {
748
+ superseded.push(opId);
749
+ continue;
750
+ }
751
+ if (markJournalSuperseded(vault, opId, fields, by)) {
752
+ superseded.push(opId);
753
+ } else {
754
+ skipped.push(opId);
755
+ }
756
+ }
757
+ return { superseded, skipped };
758
+ }
759
+
760
+ // src/utils/vault-sync-helper.ts
761
+ import { spawnSync } from "child_process";
762
+ import { existsSync as existsSync2 } from "fs";
763
+ import { homedir, platform } from "os";
764
+ import { dirname as dirname2, join as join3 } from "path";
765
+ import { fileURLToPath } from "url";
766
+ var HELPER_NAME = "wiki-pull-with-auto-resolve.sh";
767
+ function candidateHelperPaths(input = { vault: "" }) {
768
+ const env = input.env ?? process.env;
769
+ const paths = [];
770
+ if (input.helperPath) paths.push(input.helperPath);
771
+ if (env.SKILLWIKI_VAULT_SYNC_PULL_HELPER) paths.push(env.SKILLWIKI_VAULT_SYNC_PULL_HELPER);
772
+ let here = input.moduleDir;
773
+ if (!here) {
774
+ try {
775
+ here = dirname2(fileURLToPath(import.meta.url));
776
+ } catch {
777
+ here = void 0;
778
+ }
779
+ }
780
+ if (here) {
781
+ paths.push(join3(here, "vault-sync", "scripts", HELPER_NAME));
782
+ paths.push(join3(here, "..", "vault-sync", "scripts", HELPER_NAME));
783
+ paths.push(join3(here, "..", "..", "vault-sync", "scripts", HELPER_NAME));
784
+ paths.push(join3(here, "..", "..", "..", "vault-sync", "scripts", HELPER_NAME));
785
+ }
786
+ const home = input.home ?? env.HOME ?? env.USERPROFILE ?? (() => {
787
+ try {
788
+ return homedir();
789
+ } catch {
790
+ return void 0;
791
+ }
792
+ })();
793
+ if (home) {
794
+ const xdg = env.XDG_DATA_HOME;
795
+ const isDarwin = platform() === "darwin";
796
+ if (isDarwin) {
797
+ paths.push(join3(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
798
+ }
799
+ paths.push(join3(xdg || join3(home, ".local", "share"), "vault-sync", "bin", HELPER_NAME));
800
+ if (!isDarwin) {
801
+ paths.push(join3(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
802
+ }
803
+ }
804
+ return paths;
805
+ }
806
+ function resolveVaultSyncPullHelper(input) {
807
+ for (const p of candidateHelperPaths(input)) {
808
+ if (p && existsSync2(p)) return p;
809
+ }
810
+ return null;
811
+ }
812
+ async function runVaultSyncPullHelper(input) {
813
+ const helperPath = resolveVaultSyncPullHelper(input);
814
+ if (!helperPath) {
815
+ const tried = candidateHelperPaths(input).filter(Boolean);
816
+ return err("GIT_PULL_FAILED", {
817
+ message: "canonical vault-sync pull helper not found; run skillwiki doctor; install skillwiki@0.10.1+ or set SKILLWIKI_VAULT_SYNC_PULL_HELPER; host install: ~/Library/Application Support/vault-sync/bin or ~/.local/share/vault-sync/bin",
818
+ tried_paths: tried.slice(0, 12)
819
+ });
820
+ }
821
+ const remote = input.remote ?? "origin";
822
+ const branch = input.branch ?? "main";
823
+ const beforeOid = git(input.vault, ["rev-parse", "HEAD"]);
824
+ if (!beforeOid) {
825
+ return err("GIT_PULL_FAILED", { message: "could not read HEAD before pull" });
826
+ }
827
+ const env = {
828
+ ...process.env,
829
+ ...input.env ?? {},
830
+ WIKI_DIR: input.vault
831
+ };
832
+ if (input.lockToken) {
833
+ env.VAULT_SYNC_MANAGED_LOCK_TOKEN = input.lockToken;
834
+ }
835
+ const result = spawnSync("bash", [helperPath, remote, branch], {
836
+ env,
837
+ encoding: "utf8",
838
+ cwd: input.vault
839
+ });
840
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
841
+ const status = result.status ?? 1;
842
+ if (status === 2) {
843
+ return err("PREFLIGHT_FAILED", { reason: "existing-handoff", output, helper_path: helperPath });
844
+ }
845
+ if (status !== 0) {
846
+ return err("GIT_PULL_FAILED", {
847
+ message: result.error ? String(result.error) : `helper exited ${status}`,
848
+ output,
849
+ helper_path: helperPath
850
+ });
851
+ }
852
+ const afterOid = git(input.vault, ["rev-parse", "HEAD"]);
853
+ if (!afterOid) {
854
+ return err("GIT_PULL_FAILED", { message: "could not read HEAD after pull", helper_path: helperPath });
855
+ }
856
+ return ok({
857
+ before_oid: beforeOid,
858
+ after_oid: afterOid,
859
+ changed: beforeOid !== afterOid,
860
+ helper_path: helperPath
861
+ });
862
+ }
863
+
565
864
  export {
566
865
  CONFIG_KEYS,
567
866
  isValidWikiProfileKey,
@@ -576,5 +875,16 @@ export {
576
875
  snapshotterAliasForLocalHost,
577
876
  satelliteGateFromFleetLoad,
578
877
  loadFleetManifest,
579
- resolveFleetHostId
878
+ resolveFleetHostId,
879
+ git,
880
+ gitStrict,
881
+ readJournal,
882
+ listJournalOpIds,
883
+ listReviewRequiredOps,
884
+ findReviewRequiredOp,
885
+ hasUnmergedPaths,
886
+ hasActiveGitSequencer,
887
+ supersedeStaleReviewRequiredJournals,
888
+ resolveVaultSyncPullHelper,
889
+ runVaultSyncPullHelper
580
890
  };
@@ -1,30 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadFleetManifestAndHost
4
- } from "./chunk-S5ABQCXQ.js";
3
+ findReviewRequiredOp,
4
+ git,
5
+ hasActiveGitSequencer,
6
+ hasUnmergedPaths,
7
+ loadFleetManifestAndHost,
8
+ runVaultSyncPullHelper,
9
+ supersedeStaleReviewRequiredJournals
10
+ } from "./chunk-R6BKJWVC.js";
5
11
  import {
6
12
  ExitCode,
7
13
  err,
8
14
  ok
9
15
  } from "./chunk-C5OLZRRM.js";
10
16
 
11
- // src/utils/managed-write-preflight.ts
12
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
13
- import { join as join3 } from "path";
14
-
15
- // src/utils/git.ts
16
- import { execFileSync } from "child_process";
17
- function git(cwd, args) {
18
- try {
19
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
20
- } catch {
21
- return "";
22
- }
23
- }
24
- function gitStrict(cwd, args) {
25
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
26
- }
27
-
28
17
  // src/utils/managed-write-lock.ts
29
18
  import { randomBytes } from "crypto";
30
19
  import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -68,122 +57,12 @@ function releaseManagedWriteLock(handle) {
68
57
  }
69
58
  }
70
59
 
71
- // src/utils/vault-sync-helper.ts
72
- import { spawnSync } from "child_process";
73
- import { existsSync } from "fs";
74
- import { dirname as dirname2, join as join2 } from "path";
75
- import { fileURLToPath } from "url";
76
- function candidateHelperPaths(input) {
77
- const env = input.env ?? process.env;
78
- const paths = [];
79
- if (input.helperPath) paths.push(input.helperPath);
80
- if (env.SKILLWIKI_VAULT_SYNC_PULL_HELPER) paths.push(env.SKILLWIKI_VAULT_SYNC_PULL_HELPER);
81
- try {
82
- const here = dirname2(fileURLToPath(import.meta.url));
83
- paths.push(join2(here, "..", "vault-sync", "scripts", "wiki-pull-with-auto-resolve.sh"));
84
- paths.push(join2(here, "..", "..", "vault-sync", "scripts", "wiki-pull-with-auto-resolve.sh"));
85
- paths.push(join2(here, "..", "..", "..", "vault-sync", "scripts", "wiki-pull-with-auto-resolve.sh"));
86
- } catch {
87
- }
88
- return paths;
89
- }
90
- function resolveVaultSyncPullHelper(input) {
91
- for (const p of candidateHelperPaths(input)) {
92
- if (p && existsSync(p)) return p;
93
- }
94
- return null;
95
- }
96
- async function runVaultSyncPullHelper(input) {
97
- const helperPath = resolveVaultSyncPullHelper(input);
98
- if (!helperPath) {
99
- return err("GIT_PULL_FAILED", { message: "canonical vault-sync pull helper not found" });
100
- }
101
- const remote = input.remote ?? "origin";
102
- const branch = input.branch ?? "main";
103
- const beforeOid = git(input.vault, ["rev-parse", "HEAD"]);
104
- if (!beforeOid) {
105
- return err("GIT_PULL_FAILED", { message: "could not read HEAD before pull" });
106
- }
107
- const env = {
108
- ...process.env,
109
- ...input.env ?? {},
110
- WIKI_DIR: input.vault
111
- };
112
- if (input.lockToken) {
113
- env.VAULT_SYNC_MANAGED_LOCK_TOKEN = input.lockToken;
114
- }
115
- const result = spawnSync("bash", [helperPath, remote, branch], {
116
- env,
117
- encoding: "utf8",
118
- cwd: input.vault
119
- });
120
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
121
- const status = result.status ?? 1;
122
- if (status === 2) {
123
- return err("PREFLIGHT_FAILED", { reason: "existing-handoff", output, helper_path: helperPath });
124
- }
125
- if (status !== 0) {
126
- return err("GIT_PULL_FAILED", {
127
- message: result.error ? String(result.error) : `helper exited ${status}`,
128
- output,
129
- helper_path: helperPath
130
- });
131
- }
132
- const afterOid = git(input.vault, ["rev-parse", "HEAD"]);
133
- if (!afterOid) {
134
- return err("GIT_PULL_FAILED", { message: "could not read HEAD after pull", helper_path: helperPath });
135
- }
136
- return ok({
137
- before_oid: beforeOid,
138
- after_oid: afterOid,
139
- changed: beforeOid !== afterOid,
140
- helper_path: helperPath
141
- });
142
- }
143
-
144
60
  // src/utils/managed-write-preflight.ts
145
61
  var DEFAULT_DEPS = {
146
62
  converge: (input) => runVaultSyncPullHelper(input)
147
63
  };
148
- function journalDir(vault) {
149
- const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/operations"]);
150
- if (!gitPath) return null;
151
- return gitPath.startsWith("/") ? gitPath : join3(vault, gitPath);
152
- }
153
- function findReviewRequiredOp(vault) {
154
- const dir = journalDir(vault);
155
- if (!dir) return void 0;
156
- let files;
157
- try {
158
- files = readdirSync(dir).filter((f) => f.endsWith(".env"));
159
- } catch {
160
- return void 0;
161
- }
162
- const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
163
- for (const file of files) {
164
- let text;
165
- try {
166
- text = readFileSync2(join3(dir, file), "utf8");
167
- } catch {
168
- continue;
169
- }
170
- const fields = Object.fromEntries(
171
- text.split("\n").filter((line) => line.includes("=")).map((line) => {
172
- const i = line.indexOf("=");
173
- return [line.slice(0, i), line.slice(i + 1)];
174
- })
175
- );
176
- if (fields.phase !== "review-required" || fields.handoff !== "1") continue;
177
- const journalGitDir = fields.worktree_git_dir ?? "";
178
- if (!journalGitDir || !currentGitDir || journalGitDir === currentGitDir) {
179
- return file.replace(/\.env$/, "");
180
- }
181
- }
182
- return void 0;
183
- }
184
64
  function preflightBlocker(vault) {
185
- const unmergedRaw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
186
- const unmerged = unmergedRaw ? unmergedRaw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
65
+ const unmerged = hasUnmergedPaths(vault);
187
66
  if (unmerged.length > 0) {
188
67
  return {
189
68
  reason: "unmerged-paths",
@@ -191,17 +70,10 @@ function preflightBlocker(vault) {
191
70
  unmerged_paths: unmerged
192
71
  };
193
72
  }
194
- const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
195
- if (gitDir) {
196
- const markers = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
197
- for (const m of markers) {
198
- try {
199
- readFileSync2(join3(gitDir, m));
200
- return { reason: "git-operation-in-progress" };
201
- } catch {
202
- }
203
- }
73
+ if (hasActiveGitSequencer(vault)) {
74
+ return { reason: "git-operation-in-progress" };
204
75
  }
76
+ supersedeStaleReviewRequiredJournals(vault, { by: "skillwiki-managed-write-preflight" });
205
77
  const op = findReviewRequiredOp(vault);
206
78
  if (op) return { reason: "review-required", operation_id: op };
207
79
  return null;
@@ -269,7 +141,8 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
269
141
  const converge = await deps.converge({
270
142
  vault,
271
143
  lockToken: input.lockToken,
272
- env: input.env
144
+ env: input.env,
145
+ home: input.home
273
146
  });
274
147
  if (!converge.ok) {
275
148
  const exitCode = converge.error === "PREFLIGHT_FAILED" ? ExitCode.PREFLIGHT_FAILED : ExitCode.SYNC_PULL_FAILED;
@@ -333,11 +206,8 @@ async function runManagedWriteTransaction(input, deps = DEFAULT_DEPS) {
333
206
  }
334
207
 
335
208
  export {
336
- git,
337
- gitStrict,
338
209
  acquireManagedWriteLock,
339
210
  releaseManagedWriteLock,
340
- runVaultSyncPullHelper,
341
211
  runManagedWritePreflight,
342
212
  runManagedWriteTransaction
343
213
  };
@@ -9,8 +9,7 @@ import {
9
9
  } from "./chunk-C5OLZRRM.js";
10
10
 
11
11
  // src/utils/index-projection.ts
12
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
13
- import { readFile as readFile3 } from "fs/promises";
12
+ import { readFile as readFile4 } from "fs/promises";
14
13
  import { join as join3 } from "path";
15
14
 
16
15
  // src/utils/atomic-write.ts
@@ -59,6 +58,9 @@ async function atomicWriteText(path, text) {
59
58
  }
60
59
  }
61
60
 
61
+ // src/utils/index-universe.ts
62
+ import { readFile as readFile3 } from "fs/promises";
63
+
62
64
  // src/utils/typed-page.ts
63
65
  import { lstatSync, realpathSync } from "fs";
64
66
  import { dirname as dirname2, posix, relative, resolve, sep } from "path";
@@ -440,8 +442,15 @@ async function readPageCached(p, cache) {
440
442
  return pending;
441
443
  }
442
444
 
443
- // src/utils/index-projection.ts
444
- var SECTION_ORDER = ["Entities", "Concepts", "Comparisons", "Queries", "Meta", "Projects"];
445
+ // src/utils/index-universe.ts
446
+ var ROOT_INDEX_SECTION_ORDER = [
447
+ "Entities",
448
+ "Concepts",
449
+ "Comparisons",
450
+ "Queries",
451
+ "Meta",
452
+ "Projects"
453
+ ];
445
454
  var TYPE_SECTION = {
446
455
  entity: "Entities",
447
456
  concept: "Concepts",
@@ -450,6 +459,106 @@ var TYPE_SECTION = {
450
459
  meta: "Meta"
451
460
  };
452
461
  var compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
462
+ function projectTitleFromReadme(text, slug) {
463
+ const match = text.match(/^#\s+Project:\s+(.+)$/m);
464
+ return match?.[1]?.trim() || slug;
465
+ }
466
+ async function buildRootIndexUniverse(input) {
467
+ const scanned = input.scan ? ok(input.scan) : await scanVault(input.vault);
468
+ if (!scanned.ok) return scanned;
469
+ const required = [];
470
+ const rejectedTyped = [];
471
+ const seen = /* @__PURE__ */ new Map();
472
+ let duplicatesRemoved = 0;
473
+ const typedPages = [...scanned.data.typedKnowledge].sort((a, b) => compareText(a.relPath, b.relPath));
474
+ for (const page of typedPages) {
475
+ let text;
476
+ try {
477
+ text = await readFile3(page.absPath, "utf8");
478
+ } catch {
479
+ continue;
480
+ }
481
+ const prepared = prepareTypedPage(text, page.relPath);
482
+ if (!prepared.ok) {
483
+ rejectedTyped.push({
484
+ relPath: page.relPath,
485
+ error: prepared.error,
486
+ detail: prepared.detail
487
+ });
488
+ continue;
489
+ }
490
+ const section = TYPE_SECTION[prepared.data.type];
491
+ if (!section) {
492
+ rejectedTyped.push({
493
+ relPath: page.relPath,
494
+ error: "SCHEME_REJECTED",
495
+ detail: { type: prepared.data.type }
496
+ });
497
+ continue;
498
+ }
499
+ const target = prepared.data.target.replace(/\.md$/, "");
500
+ const previousTitle = seen.get(target);
501
+ if (previousTitle !== void 0) {
502
+ if (previousTitle !== prepared.data.title) {
503
+ return err("SCHEME_REJECTED", {
504
+ message: `duplicate index target ${target} with differing titles`,
505
+ titles: [previousTitle, prepared.data.title]
506
+ });
507
+ }
508
+ duplicatesRemoved += 1;
509
+ continue;
510
+ }
511
+ seen.set(target, prepared.data.title);
512
+ required.push({
513
+ section,
514
+ target,
515
+ title: prepared.data.title,
516
+ source: "typed"
517
+ });
518
+ }
519
+ const projectReadmes = scanned.data.allMarkdown.filter((page) => /^projects\/[^/]+\/README\.md$/.test(page.relPath)).sort((a, b) => compareText(a.relPath, b.relPath));
520
+ for (const readme of projectReadmes) {
521
+ let text;
522
+ try {
523
+ text = await readFile3(readme.absPath, "utf8");
524
+ } catch {
525
+ continue;
526
+ }
527
+ const projectSlug = readme.relPath.split("/")[1];
528
+ const target = `projects/${projectSlug}/README`;
529
+ const title = projectTitleFromReadme(text, projectSlug);
530
+ const previousTitle = seen.get(target);
531
+ if (previousTitle !== void 0) {
532
+ if (previousTitle !== title) {
533
+ return err("SCHEME_REJECTED", {
534
+ message: `duplicate index target ${target} with differing titles`
535
+ });
536
+ }
537
+ duplicatesRemoved += 1;
538
+ continue;
539
+ }
540
+ seen.set(target, title);
541
+ required.push({ section: "Projects", target, title, source: "project" });
542
+ }
543
+ required.sort((a, b) => {
544
+ const sectionA = ROOT_INDEX_SECTION_ORDER.indexOf(a.section);
545
+ const sectionB = ROOT_INDEX_SECTION_ORDER.indexOf(b.section);
546
+ if (sectionA !== sectionB) return sectionA - sectionB;
547
+ return compareText(a.target, b.target);
548
+ });
549
+ const knownTargets = new Set(required.map((entry) => entry.target));
550
+ for (const page of [...scanned.data.compound].sort((a, b) => compareText(a.relPath, b.relPath))) {
551
+ knownTargets.add(page.relPath.replace(/\.md$/, ""));
552
+ }
553
+ return ok({
554
+ required,
555
+ knownTargets,
556
+ rejectedTyped,
557
+ duplicatesRemoved
558
+ });
559
+ }
560
+
561
+ // src/utils/index-projection.ts
453
562
  var UNMANAGED_START = "<!-- skillwiki:index-unmanaged:start -->";
454
563
  var UNMANAGED_END = "<!-- skillwiki:index-unmanaged:end -->";
455
564
  function extractUnmanaged(currentText) {
@@ -480,94 +589,27 @@ function priorWikilinkTargets(text) {
480
589
  }
481
590
  return out;
482
591
  }
483
- function projectTitleFromReadme(text, slug) {
484
- const m = text.match(/^#\s+Project:\s+(.+)$/m);
485
- return m?.[1]?.trim() || slug;
486
- }
487
592
  async function renderRootIndex(input) {
488
593
  const vault = input.vault;
489
594
  let currentText = input.currentText;
490
595
  if (currentText === void 0) {
491
596
  try {
492
- currentText = await readFile3(join3(vault, "index.md"), "utf8");
597
+ currentText = await readFile4(join3(vault, "index.md"), "utf8");
493
598
  } catch {
494
599
  currentText = "";
495
600
  }
496
601
  }
497
602
  const unmanaged = extractUnmanaged(currentText);
498
603
  if (!unmanaged.ok) return unmanaged;
499
- const scan = await scanVault(vault);
500
- if (!scan.ok) return scan;
501
- const entries = [];
502
- const seen = /* @__PURE__ */ new Map();
503
- let duplicatesRemoved = 0;
504
- for (const page of scan.data.typedKnowledge) {
505
- let text;
506
- try {
507
- text = await readFile3(join3(vault, page.relPath), "utf8");
508
- } catch {
509
- continue;
510
- }
511
- const prepared = prepareTypedPage(text, page.relPath);
512
- if (!prepared.ok) continue;
513
- const section = TYPE_SECTION[prepared.data.type];
514
- if (!section) continue;
515
- const target = prepared.data.target.replace(/\.md$/, "");
516
- const prev = seen.get(target);
517
- if (prev !== void 0) {
518
- if (prev !== prepared.data.title) {
519
- return err("SCHEME_REJECTED", {
520
- message: `duplicate index target ${target} with differing titles`,
521
- titles: [prev, prepared.data.title]
522
- });
523
- }
524
- duplicatesRemoved += 1;
525
- continue;
526
- }
527
- seen.set(target, prepared.data.title);
528
- entries.push({
529
- section,
530
- target,
531
- title: prepared.data.title,
532
- source: "typed"
533
- });
534
- }
535
- try {
536
- const projectsRoot = join3(vault, "projects");
537
- for (const slug of readdirSync(projectsRoot, { withFileTypes: true })) {
538
- if (!slug.isDirectory()) continue;
539
- const readmePath = join3(projectsRoot, slug.name, "README.md");
540
- let text;
541
- try {
542
- text = readFileSync2(readmePath, "utf8");
543
- } catch {
544
- continue;
545
- }
546
- const target = `projects/${slug.name}/README`;
547
- const title = projectTitleFromReadme(text, slug.name);
548
- const prev = seen.get(target);
549
- if (prev !== void 0) {
550
- if (prev !== title) {
551
- return err("SCHEME_REJECTED", {
552
- message: `duplicate index target ${target} with differing titles`
553
- });
554
- }
555
- duplicatesRemoved += 1;
556
- continue;
557
- }
558
- seen.set(target, title);
559
- entries.push({ section: "Projects", target, title, source: "project" });
560
- }
561
- } catch {
562
- }
563
- entries.sort((a, b) => {
564
- const sa = SECTION_ORDER.indexOf(a.section);
565
- const sb = SECTION_ORDER.indexOf(b.section);
566
- if (sa !== sb) return sa - sb;
567
- return compareText(a.target, b.target);
568
- });
604
+ const universe = await buildRootIndexUniverse({ vault });
605
+ if (!universe.ok) return universe;
606
+ const entries = universe.data.required;
607
+ let duplicatesRemoved = universe.data.duplicatesRemoved;
569
608
  const prior = priorWikilinkTargets(currentText);
570
- const generatedTargets = new Set(entries.map((e) => e.target));
609
+ const knownTargets = universe.data.knownTargets;
610
+ const knownBasenames = new Set(
611
+ [...knownTargets].map((target) => target.split("/").pop().toLowerCase())
612
+ );
571
613
  const priorLower = /* @__PURE__ */ new Map();
572
614
  for (const t of prior) {
573
615
  const k = t.toLowerCase();
@@ -585,15 +627,10 @@ async function renderRootIndex(input) {
585
627
  const ghostsRemoved = [
586
628
  ...new Set(
587
629
  prior.filter((t) => {
588
- const bare = t.includes("/") ? t : null;
589
- if (bare && !generatedTargets.has(bare) && !generatedTargets.has(t)) {
590
- if (!seen.has(t)) return true;
591
- }
592
630
  if (!t.includes("/")) {
593
- const matches = [...generatedTargets].filter((g) => g.split("/").pop() === t);
594
- return matches.length === 0;
631
+ return !knownBasenames.has(t.toLowerCase());
595
632
  }
596
- return !generatedTargets.has(t);
633
+ return !knownTargets.has(t);
597
634
  })
598
635
  )
599
636
  ];
@@ -603,7 +640,7 @@ async function renderRootIndex(input) {
603
640
  "Generated by `skillwiki index rebuild`. Generated sections are derived from typed-page frontmatter and project manifests.",
604
641
  ""
605
642
  ];
606
- for (const section of SECTION_ORDER) {
643
+ for (const section of ROOT_INDEX_SECTION_ORDER) {
607
644
  lines.push(`## ${section}`, "");
608
645
  const sectionEntries = entries.filter((e) => e.section === section);
609
646
  for (const e of sectionEntries) {
@@ -642,6 +679,7 @@ export {
642
679
  scanVault,
643
680
  readPage,
644
681
  readPageCached,
682
+ buildRootIndexUniverse,
645
683
  renderRootIndex,
646
684
  writeRootIndexProjection
647
685
  };
@@ -5,6 +5,7 @@ import {
5
5
  } from "./chunk-7I2TPIV5.js";
6
6
  import {
7
7
  atomicWriteText,
8
+ buildRootIndexUniverse,
8
9
  extractFrontmatter,
9
10
  mapWithConcurrency,
10
11
  prepareTypedPage,
@@ -18,18 +19,20 @@ import {
18
19
  splitFrontmatter,
19
20
  vaultIoConcurrency,
20
21
  writeRootIndexProjection
21
- } from "./chunk-IZABIE44.js";
22
+ } from "./chunk-U34B2XQJ.js";
22
23
  import {
23
24
  CONFIG_KEYS,
24
25
  isValidWikiProfileKey,
26
+ listReviewRequiredOps,
25
27
  loadFleetManifestAndHost,
26
28
  parseDotenvFile,
27
29
  parseDotenvText,
28
30
  profileKey,
31
+ resolveVaultSyncPullHelper,
29
32
  satelliteGateFromFleetLoad,
30
33
  snapshotterAliasForLocalHost,
31
34
  writeDotenv
32
- } from "./chunk-S5ABQCXQ.js";
35
+ } from "./chunk-R6BKJWVC.js";
33
36
  import {
34
37
  CompoundSchema,
35
38
  ExitCode,
@@ -1559,8 +1562,11 @@ function normalizeIndexTarget(raw) {
1559
1562
  return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
1560
1563
  }
1561
1564
  async function runIndexCheck(input) {
1562
- const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
1563
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1565
+ const universe = await buildRootIndexUniverse({ vault: input.vault, scan: input.scan });
1566
+ if (!universe.ok) {
1567
+ const exitCode = universe.error === "VAULT_PATH_INVALID" ? ExitCode.VAULT_PATH_INVALID : ExitCode.SCHEME_REJECTED;
1568
+ return { exitCode, result: universe };
1569
+ }
1564
1570
  let indexText = "";
1565
1571
  try {
1566
1572
  indexText = await readFile6(join10(input.vault, "index.md"), "utf8");
@@ -1577,25 +1583,22 @@ async function runIndexCheck(input) {
1577
1583
  indexBare.set(bare, list);
1578
1584
  }
1579
1585
  const required = /* @__PURE__ */ new Map();
1580
- const known = /* @__PURE__ */ new Set();
1581
- for (const p of scan.data.typedKnowledge) {
1582
- const target = p.relPath.replace(/\.md$/, "");
1583
- required.set(target, p.relPath);
1584
- known.add(target);
1586
+ for (const entry of universe.data.required) {
1587
+ required.set(entry.target, `${entry.target}.md`);
1585
1588
  }
1586
- for (const p of scan.data.compound) {
1587
- known.add(p.relPath.replace(/\.md$/, ""));
1589
+ const requiredBasenameCounts = /* @__PURE__ */ new Map();
1590
+ for (const target of required.keys()) {
1591
+ const basename3 = target.split("/").pop().toLowerCase();
1592
+ requiredBasenameCounts.set(basename3, (requiredBasenameCounts.get(basename3) ?? 0) + 1);
1588
1593
  }
1594
+ const known = universe.data.knownTargets;
1589
1595
  const missing_from_index = [];
1590
1596
  for (const [target, relPath] of required.entries()) {
1591
1597
  if (indexTargets.has(target)) continue;
1592
1598
  const bare = target.split("/").pop().toLowerCase();
1593
1599
  const bareHits = indexBare.get(bare) ?? [];
1594
1600
  const basenameOnly = bareHits.filter((t) => !t.includes("/"));
1595
- const sameNameRequired = [...required.keys()].filter(
1596
- (t) => t.split("/").pop().toLowerCase() === bare
1597
- );
1598
- if (basenameOnly.length === 1 && sameNameRequired.length === 1) continue;
1601
+ if (basenameOnly.length === 1 && requiredBasenameCounts.get(bare) === 1) continue;
1599
1602
  missing_from_index.push(relPath);
1600
1603
  }
1601
1604
  const ghost_entries = [];
@@ -3032,6 +3035,10 @@ function buildCliSurface() {
3032
3035
  syncCmd.command("status").option("--wiki <name>").option("--include-stashes").option("--include-remote-health").option("--check-snapshotter");
3033
3036
  syncCmd.command("push").option("--wiki <name>");
3034
3037
  syncCmd.command("pull").option("--wiki <name>");
3038
+ syncCmd.command("resolve-derived").option("--operation-id <id>").option("--wiki <name>");
3039
+ const syncJournalCmd = syncCmd.command("journal");
3040
+ syncJournalCmd.command("list").option("--wiki <name>");
3041
+ syncJournalCmd.command("clear-stale").option("--dry-run").option("--wiki <name>");
3035
3042
  syncCmd.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
3036
3043
  syncCmd.command("unlock").option("--force").option("--wiki <name>");
3037
3044
  syncCmd.command("peers").option("--wiki <name>");
@@ -6210,6 +6217,42 @@ function checkVfsCacheHealth(resolvedPath) {
6210
6217
  `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
6211
6218
  );
6212
6219
  }
6220
+ function checkVaultSyncPullHelper(home, env) {
6221
+ const path = resolveVaultSyncPullHelper({
6222
+ vault: "",
6223
+ home,
6224
+ env
6225
+ });
6226
+ if (path) {
6227
+ return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
6228
+ }
6229
+ return check(
6230
+ "error",
6231
+ "vault_sync_pull_helper",
6232
+ "Vault-sync pull helper",
6233
+ "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
6234
+ );
6235
+ }
6236
+ function checkVaultSyncReviewRequiredJournals(vaultPath) {
6237
+ if (!vaultPath || !existsSync13(join24(vaultPath, ".git"))) {
6238
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
6239
+ }
6240
+ try {
6241
+ const ops = listReviewRequiredOps(vaultPath);
6242
+ if (ops.length === 0) {
6243
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
6244
+ }
6245
+ const sample = ops[0]?.opId ?? "?";
6246
+ return check(
6247
+ "warn",
6248
+ "vault_sync_review_required_journals",
6249
+ "Review-required journals",
6250
+ `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
6251
+ );
6252
+ } catch {
6253
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
6254
+ }
6255
+ }
6213
6256
  function readVaultSyncConfig(home) {
6214
6257
  try {
6215
6258
  const content = readFileSync10(join24(home, ".skillwiki", ".env"), "utf8");
@@ -6820,6 +6863,8 @@ async function runDoctor(input) {
6820
6863
  vaultSyncServiceScope: vsConfig.serviceScope,
6821
6864
  snapshotScriptPath: vsConfig.snapshotScript
6822
6865
  }));
6866
+ checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
6867
+ checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
6823
6868
  const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
6824
6869
  checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
6825
6870
  checks.push(checkSatelliteTimer(satelliteGate.satelliteExpected));
package/dist/cli.js CHANGED
@@ -68,7 +68,7 @@ import {
68
68
  satelliteLatestRunPath,
69
69
  taxonomyCommentForPage,
70
70
  upsertIndexEntry
71
- } from "./chunk-DR7KFHNH.js";
71
+ } from "./chunk-Z3WS45PL.js";
72
72
  import {
73
73
  normalizeDistTag,
74
74
  readCache,
@@ -88,29 +88,33 @@ import {
88
88
  scanVault,
89
89
  splitFrontmatter,
90
90
  writeRootIndexProjection
91
- } from "./chunk-IZABIE44.js";
91
+ } from "./chunk-U34B2XQJ.js";
92
92
  import {
93
93
  acquireManagedWriteLock,
94
- git,
95
- gitStrict,
96
94
  releaseManagedWriteLock,
97
95
  runManagedWritePreflight,
98
- runManagedWriteTransaction,
99
- runVaultSyncPullHelper
100
- } from "./chunk-5TBIMLTZ.js";
96
+ runManagedWriteTransaction
97
+ } from "./chunk-SCGC7YNM.js";
101
98
  import {
102
99
  FLEET_REL_PATH,
100
+ git,
101
+ gitStrict,
102
+ listJournalOpIds,
103
+ listReviewRequiredOps,
103
104
  loadFleetManifest,
104
105
  loadFleetManifestAndHost,
105
106
  parseDotenvFile,
106
107
  parseDotenvText,
107
108
  profileKey,
109
+ readJournal,
108
110
  resolveFleetHostId,
109
111
  runFleetContext,
110
112
  runFleetValidate,
113
+ runVaultSyncPullHelper,
111
114
  snapshotterAliasForLocalHost,
115
+ supersedeStaleReviewRequiredJournals,
112
116
  writeDotenv
113
- } from "./chunk-S5ABQCXQ.js";
117
+ } from "./chunk-R6BKJWVC.js";
114
118
  import {
115
119
  ExitCode,
116
120
  MetaSchema,
@@ -2529,7 +2533,7 @@ ${fmRewritten}
2529
2533
  await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
2530
2534
  let indexUpdated = false;
2531
2535
  if (!isRaw) {
2532
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2536
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-VNJ3TLEK.js");
2533
2537
  const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
2534
2538
  const fullTarget = relPath.replace(/\.md$/, "");
2535
2539
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -2640,7 +2644,7 @@ async function runRemove(input) {
2640
2644
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
2641
2645
  const { readFile: readFile17 } = await import("fs/promises");
2642
2646
  const { join: pathJoin } = await import("path");
2643
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2647
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-VNJ3TLEK.js");
2644
2648
  const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
2645
2649
  const fullTarget = relPath.replace(/\.md$/, "");
2646
2650
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -2990,6 +2994,30 @@ ${migratedBody}${newFooter}`;
2990
2994
  // src/commands/update.ts
2991
2995
  import { execSync } from "child_process";
2992
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
+ }
2993
3021
  function resolveGlobalSkillsRoot() {
2994
3022
  try {
2995
3023
  const globalRoot = execSync("npm root -g", {
@@ -3085,6 +3113,9 @@ async function runUpdate(input) {
3085
3113
  hintLines.push(`version warnings: ${version_warnings.length}`);
3086
3114
  for (const w of version_warnings) hintLines.push(` ${w}`);
3087
3115
  }
3116
+ for (const line of migrationNotesForUpgrade(currentVersion, latest)) {
3117
+ hintLines.push(line);
3118
+ }
3088
3119
  return {
3089
3120
  exitCode: ExitCode.OK,
3090
3121
  result: ok({
@@ -5772,6 +5803,50 @@ function runSyncUnlock(input) {
5772
5803
  };
5773
5804
  }
5774
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
+
5775
5850
  // src/commands/backup.ts
5776
5851
  import { statSync as statSync2, readdirSync, readFileSync as readFileSync11, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
5777
5852
  import { join as join28, relative as relative2, dirname as dirname6 } from "path";
@@ -6674,7 +6749,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
6674
6749
  if (guard.blocked) {
6675
6750
  return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
6676
6751
  }
6677
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-SILEUQEV.js");
6752
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-PW4OOOMV.js");
6678
6753
  const run = await runManagedWriteTransaction2({
6679
6754
  vault,
6680
6755
  command,
@@ -7225,6 +7300,24 @@ syncCmd.command("resolve-derived [vault]").description("resolve mixed derived co
7225
7300
  );
7226
7301
  }
7227
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
+ });
7228
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) => {
7229
7322
  const v = await resolveVaultArg(vault, opts.wiki);
7230
7323
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  renderRootIndex,
4
4
  writeRootIndexProjection
5
- } from "./chunk-IZABIE44.js";
5
+ } from "./chunk-U34B2XQJ.js";
6
6
  import "./chunk-C5OLZRRM.js";
7
7
  export {
8
8
  renderRootIndex,
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  runManagedWritePreflight,
4
4
  runManagedWriteTransaction
5
- } from "./chunk-5TBIMLTZ.js";
6
- import "./chunk-S5ABQCXQ.js";
5
+ } from "./chunk-SCGC7YNM.js";
6
+ import "./chunk-R6BKJWVC.js";
7
7
  import "./chunk-C5OLZRRM.js";
8
8
  export {
9
9
  runManagedWritePreflight,
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-DR7KFHNH.js";
4
+ } from "./chunk-Z3WS45PL.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-IZABIE44.js";
7
- import "./chunk-S5ABQCXQ.js";
6
+ import "./chunk-U34B2XQJ.js";
7
+ import "./chunk-R6BKJWVC.js";
8
8
  import "./chunk-C5OLZRRM.js";
9
9
 
10
10
  // src/mcp-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -80,6 +80,17 @@ path, run publisher dry-run, then run the same command with `--write`.
80
80
 
81
81
  Before a managed vault mutation, invoke the managed SkillWiki command while the draft remains outside the authoritative target path. The command resolves fleet authority, refuses existing unmerged/review-required state, converges an authorized Git writer, freezes the base OID, and only then applies the write. Do not run `git pull --rebase --autostash` after placing the authoritative change in the live worktree. Do not edit root `index.md` or root `log.md` directly; projection and log commands own those compatibility files.
82
82
 
83
+ ### Managed write / 0.10.1 migration troubleshooting
84
+
85
+ After upgrading to skillwiki **≥0.10.1** (or when managed write fails):
86
+
87
+ 1. Run `skillwiki doctor` — checks `vault_sync_pull_helper` and `vault_sync_review_required_journals`.
88
+ 2. If pull helper is missing: install `skillwiki@0.10.1+` (helper must resolve from `dist/vault-sync/scripts/`) and/or redeploy vault-sync host install. Last-resort override: `SKILLWIKI_VAULT_SYNC_PULL_HELPER` pointing at `wiki-pull-with-auto-resolve.sh` under host vault-sync `bin/` (macOS Application Support or Linux `~/.local/share/vault-sync/bin`).
89
+ 3. If preflight reports `review-required` on a **clean** worktree: `skillwiki sync journal list`, then `skillwiki sync journal clear-stale --dry-run`, then `clear-stale` without dry-run. Preflight also auto-supersedes stale handoffs when criteria match; do not force-clear during active rebase/unmerged state.
90
+ 4. After `skillwiki update` across 0.10.1, read the printed Migration 0.10.1 notes.
91
+
92
+ Also mirror these pointers in vault-presync / vault-sync-status skills when operating pull/push.
93
+
83
94
  - typed pages: `skillwiki page publish <draft> <vault> --target <path>` then the same command with `--write`
84
95
  - archive: `skillwiki archive <path> <vault>`
85
96
  - ad-hoc structural log: `skillwiki log-append <vault> --content '<entry>'` (Release A dual-write) or event materialization (Release B)
@@ -80,6 +80,17 @@ path, run publisher dry-run, then run the same command with `--write`.
80
80
 
81
81
  Before a managed vault mutation, invoke the managed SkillWiki command while the draft remains outside the authoritative target path. The command resolves fleet authority, refuses existing unmerged/review-required state, converges an authorized Git writer, freezes the base OID, and only then applies the write. Do not run `git pull --rebase --autostash` after placing the authoritative change in the live worktree. Do not edit root `index.md` or root `log.md` directly; projection and log commands own those compatibility files.
82
82
 
83
+ ### Managed write / 0.10.1 migration troubleshooting
84
+
85
+ After upgrading to skillwiki **≥0.10.1** (or when managed write fails):
86
+
87
+ 1. Run `skillwiki doctor` — checks `vault_sync_pull_helper` and `vault_sync_review_required_journals`.
88
+ 2. If pull helper is missing: install `skillwiki@0.10.1+` (helper must resolve from `dist/vault-sync/scripts/`) and/or redeploy vault-sync host install. Last-resort override: `SKILLWIKI_VAULT_SYNC_PULL_HELPER` pointing at `wiki-pull-with-auto-resolve.sh` under host vault-sync `bin/` (macOS Application Support or Linux `~/.local/share/vault-sync/bin`).
89
+ 3. If preflight reports `review-required` on a **clean** worktree: `skillwiki sync journal list`, then `skillwiki sync journal clear-stale --dry-run`, then `clear-stale` without dry-run. Preflight also auto-supersedes stale handoffs when criteria match; do not force-clear during active rebase/unmerged state.
90
+ 4. After `skillwiki update` across 0.10.1, read the printed Migration 0.10.1 notes.
91
+
92
+ Also mirror these pointers in vault-presync / vault-sync-status skills when operating pull/push.
93
+
83
94
  - typed pages: `skillwiki page publish <draft> <vault> --target <path>` then the same command with `--write`
84
95
  - archive: `skillwiki archive <path> <vault>`
85
96
  - ad-hoc structural log: `skillwiki log-append <vault> --content '<entry>'` (Release A dual-write) or event materialization (Release B)