skillwiki 0.10.1 → 0.10.3

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.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ FLEET_REL_PATH,
3
4
  findReviewRequiredOp,
4
5
  git,
5
6
  hasActiveGitSequencer,
@@ -14,6 +15,10 @@ import {
14
15
  ok
15
16
  } from "./chunk-C5OLZRRM.js";
16
17
 
18
+ // src/utils/managed-write-preflight.ts
19
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
20
+ import { join as join2, resolve } from "path";
21
+
17
22
  // src/utils/managed-write-lock.ts
18
23
  import { randomBytes } from "crypto";
19
24
  import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -78,19 +83,52 @@ function preflightBlocker(vault) {
78
83
  if (op) return { reason: "review-required", operation_id: op };
79
84
  return null;
80
85
  }
86
+ function fleetManifestBytes(vault) {
87
+ const path = join2(vault, FLEET_REL_PATH);
88
+ if (!existsSync(path)) return null;
89
+ return readFileSync2(path);
90
+ }
91
+ function isGitVault(vault) {
92
+ return Boolean(git(vault, ["rev-parse", "--absolute-git-dir"]));
93
+ }
81
94
  async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
82
95
  const vault = input.vault;
83
- const blocker = preflightBlocker(vault);
84
- if (blocker) {
96
+ const convergenceVault = input.convergenceVault && resolve(input.convergenceVault) !== resolve(vault) ? resolve(input.convergenceVault) : void 0;
97
+ const gitVault = convergenceVault ?? vault;
98
+ const mutationBlocker = preflightBlocker(vault);
99
+ if (mutationBlocker) {
85
100
  return {
86
101
  exitCode: ExitCode.PREFLIGHT_FAILED,
87
102
  result: err("PREFLIGHT_FAILED", {
88
- reason: blocker.reason,
89
- operation_id: blocker.operation_id,
90
- unmerged_paths: blocker.unmerged_paths
103
+ reason: mutationBlocker.reason,
104
+ operation_id: mutationBlocker.operation_id,
105
+ unmerged_paths: mutationBlocker.unmerged_paths
91
106
  })
92
107
  };
93
108
  }
109
+ if (convergenceVault) {
110
+ if (!isGitVault(convergenceVault)) {
111
+ return {
112
+ exitCode: ExitCode.PREFLIGHT_FAILED,
113
+ result: err("PREFLIGHT_FAILED", {
114
+ reason: "convergence-vault-not-git",
115
+ convergence_vault: convergenceVault
116
+ })
117
+ };
118
+ }
119
+ const gitBlocker = preflightBlocker(convergenceVault);
120
+ if (gitBlocker) {
121
+ return {
122
+ exitCode: ExitCode.PREFLIGHT_FAILED,
123
+ result: err("PREFLIGHT_FAILED", {
124
+ reason: gitBlocker.reason,
125
+ operation_id: gitBlocker.operation_id,
126
+ unmerged_paths: gitBlocker.unmerged_paths,
127
+ convergence_vault: convergenceVault
128
+ })
129
+ };
130
+ }
131
+ }
94
132
  const fleet = await loadFleetManifestAndHost({
95
133
  vault,
96
134
  hostId: input.hostId,
@@ -98,14 +136,16 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
98
136
  home: input.home,
99
137
  osHostname: input.osHostname
100
138
  });
139
+ const dualPathMeta = convergenceVault ? { convergence_vault: convergenceVault } : {};
101
140
  if (!fleet) {
102
- const head = git(vault, ["rev-parse", "HEAD"]) || null;
141
+ const head = git(gitVault, ["rev-parse", "HEAD"]) || null;
103
142
  return {
104
143
  exitCode: ExitCode.OK,
105
144
  result: ok({
106
145
  mode: "standalone",
107
146
  base_oid: head,
108
- converged: false
147
+ converged: false,
148
+ ...dualPathMeta
109
149
  })
110
150
  };
111
151
  }
@@ -119,6 +159,27 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
119
159
  })
120
160
  };
121
161
  }
162
+ if (convergenceVault && fleetManifestBytes(convergenceVault)) {
163
+ const convergeFleetCtx = await loadFleetManifestAndHost({
164
+ vault: convergenceVault,
165
+ hostId: input.hostId ?? fleet.hostId,
166
+ env: input.env,
167
+ home: input.home,
168
+ osHostname: input.osHostname
169
+ });
170
+ if (!convergeFleetCtx || convergeFleetCtx.identityStatus !== "known" || convergeFleetCtx.hostId !== fleet.hostId) {
171
+ return {
172
+ exitCode: ExitCode.PREFLIGHT_FAILED,
173
+ result: err("PREFLIGHT_FAILED", {
174
+ reason: "convergence-vault-identity-mismatch",
175
+ host_id: fleet.hostId,
176
+ convergence_host_id: convergeFleetCtx?.hostId,
177
+ convergence_identity_status: convergeFleetCtx?.identityStatus,
178
+ convergence_vault: convergenceVault
179
+ })
180
+ };
181
+ }
182
+ }
122
183
  const host = fleet.manifest.hosts[fleet.hostId];
123
184
  if (!host) {
124
185
  return {
@@ -134,12 +195,13 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
134
195
  mode: "immutable-record",
135
196
  host_id: fleet.hostId,
136
197
  base_oid: null,
137
- converged: false
198
+ converged: false,
199
+ ...dualPathMeta
138
200
  })
139
201
  };
140
202
  }
141
203
  const converge = await deps.converge({
142
- vault,
204
+ vault: gitVault,
143
205
  lockToken: input.lockToken,
144
206
  env: input.env,
145
207
  home: input.home
@@ -148,11 +210,14 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
148
210
  const exitCode = converge.error === "PREFLIGHT_FAILED" ? ExitCode.PREFLIGHT_FAILED : ExitCode.SYNC_PULL_FAILED;
149
211
  return { exitCode, result: converge };
150
212
  }
151
- const baseOid = git(vault, ["rev-parse", "HEAD"]);
213
+ const baseOid = git(gitVault, ["rev-parse", "HEAD"]);
152
214
  if (!baseOid) {
153
215
  return {
154
216
  exitCode: ExitCode.PREFLIGHT_FAILED,
155
- result: err("PREFLIGHT_FAILED", { reason: "missing-head-after-converge" })
217
+ result: err("PREFLIGHT_FAILED", {
218
+ reason: "missing-head-after-converge",
219
+ ...dualPathMeta
220
+ })
156
221
  };
157
222
  }
158
223
  return {
@@ -162,7 +227,8 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
162
227
  host_id: fleet.hostId,
163
228
  base_oid: baseOid,
164
229
  converged: true,
165
- helper_path: converge.data.helper_path
230
+ helper_path: converge.data.helper_path,
231
+ ...dualPathMeta
166
232
  })
167
233
  };
168
234
  }
@@ -177,6 +243,7 @@ async function runManagedWriteTransaction(input, deps = DEFAULT_DEPS) {
177
243
  {
178
244
  vault: input.vault,
179
245
  command: input.command,
246
+ convergenceVault: input.convergenceVault,
180
247
  hostId: input.hostId,
181
248
  lockToken: handle.ownerToken,
182
249
  env: input.env,
@@ -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,7 +19,7 @@ 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,
@@ -1561,8 +1562,11 @@ function normalizeIndexTarget(raw) {
1561
1562
  return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
1562
1563
  }
1563
1564
  async function runIndexCheck(input) {
1564
- const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
1565
- 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
+ }
1566
1570
  let indexText = "";
1567
1571
  try {
1568
1572
  indexText = await readFile6(join10(input.vault, "index.md"), "utf8");
@@ -1579,25 +1583,22 @@ async function runIndexCheck(input) {
1579
1583
  indexBare.set(bare, list);
1580
1584
  }
1581
1585
  const required = /* @__PURE__ */ new Map();
1582
- const known = /* @__PURE__ */ new Set();
1583
- for (const p of scan.data.typedKnowledge) {
1584
- const target = p.relPath.replace(/\.md$/, "");
1585
- required.set(target, p.relPath);
1586
- known.add(target);
1586
+ for (const entry of universe.data.required) {
1587
+ required.set(entry.target, `${entry.target}.md`);
1587
1588
  }
1588
- for (const p of scan.data.compound) {
1589
- 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);
1590
1593
  }
1594
+ const known = universe.data.knownTargets;
1591
1595
  const missing_from_index = [];
1592
1596
  for (const [target, relPath] of required.entries()) {
1593
1597
  if (indexTargets.has(target)) continue;
1594
1598
  const bare = target.split("/").pop().toLowerCase();
1595
1599
  const bareHits = indexBare.get(bare) ?? [];
1596
1600
  const basenameOnly = bareHits.filter((t) => !t.includes("/"));
1597
- const sameNameRequired = [...required.keys()].filter(
1598
- (t) => t.split("/").pop().toLowerCase() === bare
1599
- );
1600
- if (basenameOnly.length === 1 && sameNameRequired.length === 1) continue;
1601
+ if (basenameOnly.length === 1 && requiredBasenameCounts.get(bare) === 1) continue;
1601
1602
  missing_from_index.push(relPath);
1602
1603
  }
1603
1604
  const ghost_entries = [];
package/dist/cli.js CHANGED
@@ -68,7 +68,7 @@ import {
68
68
  satelliteLatestRunPath,
69
69
  taxonomyCommentForPage,
70
70
  upsertIndexEntry
71
- } from "./chunk-XSTXPA34.js";
71
+ } from "./chunk-Z3WS45PL.js";
72
72
  import {
73
73
  normalizeDistTag,
74
74
  readCache,
@@ -88,13 +88,13 @@ 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
94
  releaseManagedWriteLock,
95
95
  runManagedWritePreflight,
96
96
  runManagedWriteTransaction
97
- } from "./chunk-SCGC7YNM.js";
97
+ } from "./chunk-QH5MAV74.js";
98
98
  import {
99
99
  FLEET_REL_PATH,
100
100
  git,
@@ -2533,7 +2533,7 @@ ${fmRewritten}
2533
2533
  await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
2534
2534
  let indexUpdated = false;
2535
2535
  if (!isRaw) {
2536
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2536
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-VNJ3TLEK.js");
2537
2537
  const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
2538
2538
  const fullTarget = relPath.replace(/\.md$/, "");
2539
2539
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -2644,7 +2644,7 @@ async function runRemove(input) {
2644
2644
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
2645
2645
  const { readFile: readFile17 } = await import("fs/promises");
2646
2646
  const { join: pathJoin } = await import("path");
2647
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
2647
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-VNJ3TLEK.js");
2648
2648
  const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
2649
2649
  const fullTarget = relPath.replace(/\.md$/, "");
2650
2650
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -6749,11 +6749,12 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
6749
6749
  if (guard.blocked) {
6750
6750
  return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
6751
6751
  }
6752
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-PW4OOOMV.js");
6752
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-DTNXZBLI.js");
6753
6753
  const run = await runManagedWriteTransaction2({
6754
6754
  vault,
6755
6755
  command,
6756
6756
  allowImmutableRecord: opts?.allowImmutableRecord === true,
6757
+ convergenceVault: opts?.convergenceVault,
6757
6758
  mutate: async (receipt) => await mutate(receipt)
6758
6759
  });
6759
6760
  return emit(run, vault, opts);
@@ -6982,26 +6983,28 @@ logCmd.command("materialize [vault]").description("preview or write root log.md
6982
6983
  );
6983
6984
  } else emit(await runLogMaterialize({ vault: v.vault, write: false }), v.vault);
6984
6985
  });
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
+ logCmd.command("migrate-legacy [vault]").description("backfill immutable events from historical root log.md blocks").option("--write", "write events", false).option("--converge-vault <dir>", "Git vault used for managed pull and base-OID proof").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6986
6987
  const v = await resolveVaultArg(vault, opts.wiki);
6987
6988
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6988
6989
  else if (opts.write) {
6989
6990
  return emitManagedVaultWrite(
6990
6991
  v.vault,
6991
6992
  "log migrate-legacy",
6992
- () => runLogMigrateLegacy({ vault: v.vault, write: true })
6993
+ () => runLogMigrateLegacy({ vault: v.vault, write: true }),
6994
+ { convergenceVault: opts.convergeVault }
6993
6995
  );
6994
6996
  } else emit(await runLogMigrateLegacy({ vault: v.vault, write: false }), v.vault);
6995
6997
  });
6996
6998
  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) => {
6999
+ projectionsCmd.command("materialize [vault]").description("preview or write root index.md and log.md as a pair").option("--write", "write projections", false).option("--converge-vault <dir>", "Git vault used for managed pull and base-OID proof").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6998
7000
  const v = await resolveVaultArg(vault, opts.wiki);
6999
7001
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7000
7002
  else if (opts.write) {
7001
7003
  return emitManagedVaultWrite(
7002
7004
  v.vault,
7003
7005
  "projections materialize",
7004
- () => runProjectionsMaterialize({ vault: v.vault, write: true })
7006
+ () => runProjectionsMaterialize({ vault: v.vault, write: true }),
7007
+ { convergenceVault: opts.convergeVault }
7005
7008
  );
7006
7009
  } else emit(await runProjectionsMaterialize({ vault: v.vault, write: false }), v.vault);
7007
7010
  });
@@ -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,7 +2,7 @@
2
2
  import {
3
3
  runManagedWritePreflight,
4
4
  runManagedWriteTransaction
5
- } from "./chunk-SCGC7YNM.js";
5
+ } from "./chunk-QH5MAV74.js";
6
6
  import "./chunk-R6BKJWVC.js";
7
7
  import "./chunk-C5OLZRRM.js";
8
8
  export {
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-XSTXPA34.js";
4
+ } from "./chunk-Z3WS45PL.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-IZABIE44.js";
6
+ import "./chunk-U34B2XQJ.js";
7
7
  import "./chunk-R6BKJWVC.js";
8
8
  import "./chunk-C5OLZRRM.js";
9
9
 
@@ -308,9 +308,35 @@ fi
308
308
 
309
309
  refresh_git_baseline
310
310
 
311
+ # Dual-path requires distinct live mutation vs Git convergence roots.
312
+ # Same-path would materialize projections then immediately rclone-overwrite them.
313
+ if [ "$(cd "$WIKI_DIR" 2>/dev/null && pwd -P)" = "$(cd "$SNAPSHOT_WORKTREE" 2>/dev/null && pwd -P)" ]; then
314
+ log "ERROR: WIKI_DIR and WIKI_GIT_WORKTREE must be distinct paths for dual-path projection (got: $WIKI_DIR)"
315
+ exit 1
316
+ fi
317
+
311
318
  # Single-authority root projections before FUSE/S3 pull promotion.
312
- if command -v skillwiki >/dev/null 2>&1; then
313
- if ! skillwiki projections materialize "$WIKI_DIR" --write >>"$LOG_FILE" 2>&1; then
319
+ # Mutation target is the live vault ($WIKI_DIR); Git pull/base-OID use
320
+ # $SNAPSHOT_WORKTREE so FUSE/S3 hosts without a local Git HEAD still work.
321
+ SKILLWIKI_BIN="${WIKI_SNAPSHOT_SKILLWIKI_BIN:-skillwiki}"
322
+ if command -v "$SKILLWIKI_BIN" >/dev/null 2>&1; then
323
+ case "${WIKI_SNAPSHOT_MIGRATE_LEGACY:-0}" in
324
+ 0) ;;
325
+ 1)
326
+ if ! "$SKILLWIKI_BIN" log migrate-legacy "$WIKI_DIR" --write \
327
+ --converge-vault "$SNAPSHOT_WORKTREE" >>"$LOG_FILE" 2>&1; then
328
+ log "FAIL legacy log migration; snapshot promotion refused"
329
+ exit 1
330
+ fi
331
+ log "OK legacy log migration before projection (attended one-run)"
332
+ ;;
333
+ *)
334
+ log "ERROR: WIKI_SNAPSHOT_MIGRATE_LEGACY must be 0 or 1 (got: ${WIKI_SNAPSHOT_MIGRATE_LEGACY})"
335
+ exit 1
336
+ ;;
337
+ esac
338
+ if ! "$SKILLWIKI_BIN" projections materialize "$WIKI_DIR" --write \
339
+ --converge-vault "$SNAPSHOT_WORKTREE" >>"$LOG_FILE" 2>&1; then
314
340
  log "FAIL root projection materialization; snapshot promotion refused"
315
341
  exit 1
316
342
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
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.1",
3
+ "version": "0.10.3",
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.1",
3
+ "version": "0.10.3",
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.1",
3
+ "version": "0.10.3",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",