skillwiki 0.10.0 → 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.
@@ -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
  };
@@ -22,14 +22,16 @@ import {
22
22
  import {
23
23
  CONFIG_KEYS,
24
24
  isValidWikiProfileKey,
25
+ listReviewRequiredOps,
25
26
  loadFleetManifestAndHost,
26
27
  parseDotenvFile,
27
28
  parseDotenvText,
28
29
  profileKey,
30
+ resolveVaultSyncPullHelper,
29
31
  satelliteGateFromFleetLoad,
30
32
  snapshotterAliasForLocalHost,
31
33
  writeDotenv
32
- } from "./chunk-S5ABQCXQ.js";
34
+ } from "./chunk-R6BKJWVC.js";
33
35
  import {
34
36
  CompoundSchema,
35
37
  ExitCode,
@@ -3032,6 +3034,10 @@ function buildCliSurface() {
3032
3034
  syncCmd.command("status").option("--wiki <name>").option("--include-stashes").option("--include-remote-health").option("--check-snapshotter");
3033
3035
  syncCmd.command("push").option("--wiki <name>");
3034
3036
  syncCmd.command("pull").option("--wiki <name>");
3037
+ syncCmd.command("resolve-derived").option("--operation-id <id>").option("--wiki <name>");
3038
+ const syncJournalCmd = syncCmd.command("journal");
3039
+ syncJournalCmd.command("list").option("--wiki <name>");
3040
+ syncJournalCmd.command("clear-stale").option("--dry-run").option("--wiki <name>");
3035
3041
  syncCmd.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
3036
3042
  syncCmd.command("unlock").option("--force").option("--wiki <name>");
3037
3043
  syncCmd.command("peers").option("--wiki <name>");
@@ -6210,6 +6216,42 @@ function checkVfsCacheHealth(resolvedPath) {
6210
6216
  `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
6211
6217
  );
6212
6218
  }
6219
+ function checkVaultSyncPullHelper(home, env) {
6220
+ const path = resolveVaultSyncPullHelper({
6221
+ vault: "",
6222
+ home,
6223
+ env
6224
+ });
6225
+ if (path) {
6226
+ return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
6227
+ }
6228
+ return check(
6229
+ "error",
6230
+ "vault_sync_pull_helper",
6231
+ "Vault-sync pull helper",
6232
+ "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
6233
+ );
6234
+ }
6235
+ function checkVaultSyncReviewRequiredJournals(vaultPath) {
6236
+ if (!vaultPath || !existsSync13(join24(vaultPath, ".git"))) {
6237
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
6238
+ }
6239
+ try {
6240
+ const ops = listReviewRequiredOps(vaultPath);
6241
+ if (ops.length === 0) {
6242
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
6243
+ }
6244
+ const sample = ops[0]?.opId ?? "?";
6245
+ return check(
6246
+ "warn",
6247
+ "vault_sync_review_required_journals",
6248
+ "Review-required journals",
6249
+ `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
6250
+ );
6251
+ } catch {
6252
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
6253
+ }
6254
+ }
6213
6255
  function readVaultSyncConfig(home) {
6214
6256
  try {
6215
6257
  const content = readFileSync10(join24(home, ".skillwiki", ".env"), "utf8");
@@ -6820,6 +6862,8 @@ async function runDoctor(input) {
6820
6862
  vaultSyncServiceScope: vsConfig.serviceScope,
6821
6863
  snapshotScriptPath: vsConfig.snapshotScript
6822
6864
  }));
6865
+ checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
6866
+ checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
6823
6867
  const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
6824
6868
  checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
6825
6869
  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-XSTXPA34.js";
72
72
  import {
73
73
  normalizeDistTag,
74
74
  readCache,
@@ -91,26 +91,30 @@ import {
91
91
  } from "./chunk-IZABIE44.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,
@@ -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,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-XSTXPA34.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
  import "./chunk-IZABIE44.js";
7
- import "./chunk-S5ABQCXQ.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.1",
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.1",
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.1",
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.1",
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)