skillwiki 0.10.91 → 0.10.93

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.
@@ -3,11 +3,11 @@ import {
3
3
  ROOT_INDEX_SECTION_ORDER,
4
4
  atomicWriteText,
5
5
  buildRootIndexUniverse
6
- } from "./chunk-JHDN3FSF.js";
6
+ } from "./chunk-C5WOEVX3.js";
7
7
  import {
8
8
  err,
9
9
  ok
10
- } from "./chunk-XJR3XXP5.js";
10
+ } from "./chunk-SXNKZGNM.js";
11
11
 
12
12
  // src/utils/index-projection.ts
13
13
  import { readFile } from "fs/promises";
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  renderRootIndex,
8
8
  writeRootIndexProjection
9
- } from "./chunk-ZUHA2EXE.js";
9
+ } from "./chunk-3KEMRVWR.js";
10
10
  import {
11
11
  CONFIG_KEYS,
12
12
  VAULT_HYGIENE_GENERATED_COMMIT_PATHS,
@@ -40,11 +40,11 @@ import {
40
40
  snapshotterAliasForLocalHost,
41
41
  toUndirectedWeighted,
42
42
  writeDotenv
43
- } from "./chunk-6WE7E6VB.js";
43
+ } from "./chunk-OTESAW36.js";
44
44
  import {
45
45
  atomicWriteText,
46
46
  prepareTypedPage
47
- } from "./chunk-JHDN3FSF.js";
47
+ } from "./chunk-C5WOEVX3.js";
48
48
  import {
49
49
  applySourceCompileClaim,
50
50
  applySourceCompilePublished,
@@ -57,11 +57,11 @@ import {
57
57
  planSourceCompileRelease,
58
58
  planSourceReview,
59
59
  runSourcesPending
60
- } from "./chunk-ECOEJABP.js";
60
+ } from "./chunk-UDOZKMCN.js";
61
61
  import {
62
62
  eventPathFor,
63
63
  writeLogEvent
64
- } from "./chunk-QFRZDTJY.js";
64
+ } from "./chunk-LTSIHHKD.js";
65
65
  import {
66
66
  CompoundSchema,
67
67
  ExitCode,
@@ -82,7 +82,7 @@ import {
82
82
  scanVault,
83
83
  splitFrontmatter,
84
84
  systemdPropertyFor
85
- } from "./chunk-XJR3XXP5.js";
85
+ } from "./chunk-SXNKZGNM.js";
86
86
 
87
87
  // src/commands/log-append.ts
88
88
  import { readFile, stat } from "fs/promises";
@@ -4516,6 +4516,9 @@ function isEventLedgerPath(rel) {
4516
4516
  const n = rel.replace(/\\/g, "/").replace(/^\.\//, "");
4517
4517
  return n === "meta/log-events" || n.startsWith("meta/log-events/");
4518
4518
  }
4519
+ function countEventLedgerFiles(vault) {
4520
+ return listFilesRecursive(join21(vault, "meta", "log-events")).length;
4521
+ }
4519
4522
  function measureDirtyVolume(vault) {
4520
4523
  const empty = (extra = {}) => ({
4521
4524
  porcelain_lines: 0,
@@ -6482,7 +6485,7 @@ async function runQuery(input) {
6482
6485
  }
6483
6486
  let pendingSources;
6484
6487
  if (input.includePending) {
6485
- const { runSourcesPending: runSourcesPending2 } = await import("./sources-A5GFRY3F.js");
6488
+ const { runSourcesPending: runSourcesPending2 } = await import("./sources-4KCCBNCO.js");
6486
6489
  const pending = await runSourcesPending2({
6487
6490
  vault: input.vault,
6488
6491
  match: input.text,
@@ -7847,6 +7850,8 @@ export {
7847
7850
  DEFAULT_CAPTURE_BUDGET,
7848
7851
  DEFAULT_NO_DECISION_STREAK,
7849
7852
  GateError,
7853
+ isEventLedgerPath,
7854
+ countEventLedgerFiles,
7850
7855
  measureDirtyVolume,
7851
7856
  evaluateDirtyVolumeGate,
7852
7857
  CAPTURE_HYGIENE_CONTRACT,
@@ -8,7 +8,7 @@ import {
8
8
  ok,
9
9
  scanSensitiveContent,
10
10
  scanVault
11
- } from "./chunk-XJR3XXP5.js";
11
+ } from "./chunk-SXNKZGNM.js";
12
12
 
13
13
  // src/utils/atomic-write.ts
14
14
  import { randomBytes } from "crypto";
@@ -6,7 +6,7 @@ import {
6
6
  readPage,
7
7
  scanSensitiveContent,
8
8
  splitFrontmatter
9
- } from "./chunk-XJR3XXP5.js";
9
+ } from "./chunk-SXNKZGNM.js";
10
10
 
11
11
  // src/utils/vault-path-safety.ts
12
12
  import { lstatSync, realpathSync } from "fs";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  atomicWriteText,
4
4
  buildRootIndexUniverse
5
- } from "./chunk-JHDN3FSF.js";
5
+ } from "./chunk-C5WOEVX3.js";
6
6
  import {
7
7
  authorizeRawOperation,
8
8
  buildSourceReferenceIndex,
@@ -22,7 +22,7 @@ import {
22
22
  resolveExistingRegularFileInsideVault,
23
23
  stripFencedBlocks,
24
24
  writeLogEvent
25
- } from "./chunk-QFRZDTJY.js";
25
+ } from "./chunk-LTSIHHKD.js";
26
26
  import {
27
27
  ExitCode,
28
28
  FleetManifestSchema,
@@ -41,7 +41,7 @@ import {
41
41
  scanVault,
42
42
  splitFrontmatter,
43
43
  vaultIoConcurrency
44
- } from "./chunk-XJR3XXP5.js";
44
+ } from "./chunk-SXNKZGNM.js";
45
45
 
46
46
  // src/utils/managed-write-preflight.ts
47
47
  import { existsSync as existsSync11 } from "fs";
@@ -5340,12 +5340,14 @@ async function runSyncLintDelta(input) {
5340
5340
 
5341
5341
  // src/utils/git.ts
5342
5342
  import { execFileSync as execFileSync2 } from "child_process";
5343
+ var GIT_OUTPUT_MAX_BUFFER = 16 * 1024 * 1024;
5343
5344
  function git(cwd, args, opts) {
5344
5345
  try {
5345
5346
  return execFileSync2("git", args, {
5346
5347
  cwd,
5347
5348
  encoding: "utf8",
5348
5349
  stdio: ["pipe", "pipe", "pipe"],
5350
+ maxBuffer: GIT_OUTPUT_MAX_BUFFER,
5349
5351
  ...opts?.timeoutMs !== void 0 ? { timeout: opts.timeoutMs } : {}
5350
5352
  }).trim();
5351
5353
  } catch {
@@ -5353,7 +5355,12 @@ function git(cwd, args, opts) {
5353
5355
  }
5354
5356
  }
5355
5357
  function gitStrict(cwd, args) {
5356
- return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5358
+ return execFileSync2("git", args, {
5359
+ cwd,
5360
+ encoding: "utf8",
5361
+ stdio: ["pipe", "pipe", "pipe"],
5362
+ maxBuffer: GIT_OUTPUT_MAX_BUFFER
5363
+ }).trim();
5357
5364
  }
5358
5365
 
5359
5366
  // src/utils/sync-lock.ts
@@ -5538,6 +5545,7 @@ var VAULT_HYGIENE_GENERATED_COMMIT_PATHS = VAULT_HYGIENE_GITIGNORE_PATTERNS.map(
5538
5545
  (pattern) => pattern.replace(/\/$/, "")
5539
5546
  );
5540
5547
  var VAULT_SYNC_FILTER_REQUIRED_EXCLUDES = [
5548
+ "/.WIKI_GIT_FROZEN",
5541
5549
  "remotely-save/data.json",
5542
5550
  ".skillwiki/sync.lock",
5543
5551
  ".skillwiki/managed-write.lock",
@@ -7442,6 +7450,7 @@ export {
7442
7450
  canSupersedeJournal,
7443
7451
  markJournalSuperseded,
7444
7452
  supersedeStaleReviewRequiredJournals,
7453
+ readSkillWikiConfig,
7445
7454
  resolveConfiguredSnapshotWorktree,
7446
7455
  DEFAULT_MANAGED_WRITE_WAIT_MS,
7447
7456
  MANAGED_WRITE_POLL_INTERVAL_MS,
@@ -389,6 +389,7 @@ var VaultSyncConfigSchema = z.object({
389
389
  "vault_sync.snapshot_profile": vaultSyncPathOrNone,
390
390
  "vault_sync.snapshot_script": vaultSyncPathOrNone,
391
391
  "vault_sync.snapshot_worktree": vaultSyncPathOrNone,
392
+ "vault_sync.fetch_projection": vaultSyncPathOrNone,
392
393
  "vault_sync.push_enabled": vaultSyncBool,
393
394
  "vault_sync.fuse_refresh_enabled": vaultSyncBool,
394
395
  "vault_sync.fuse_refresh_interval": vaultSyncDuration,
@@ -8,7 +8,7 @@ import {
8
8
  readLogEvents,
9
9
  resolveExistingRegularFileInsideVault,
10
10
  writeLogEvent
11
- } from "./chunk-QFRZDTJY.js";
11
+ } from "./chunk-LTSIHHKD.js";
12
12
  import {
13
13
  ExitCode,
14
14
  RawSourceSchema,
@@ -19,7 +19,7 @@ import {
19
19
  scanSensitiveContent,
20
20
  scanVault,
21
21
  splitFrontmatter
22
- } from "./chunk-XJR3XXP5.js";
22
+ } from "./chunk-SXNKZGNM.js";
23
23
 
24
24
  // src/commands/sources.ts
25
25
  import { readFile as readFile4 } from "fs/promises";
package/dist/cli.js CHANGED
@@ -9,11 +9,13 @@ import {
9
9
  SATELLITE_STALE_MS,
10
10
  buildVectorIndex,
11
11
  configPath,
12
+ countEventLedgerFiles,
12
13
  detectFuseMount,
13
14
  evaluateDirtyVolumeGate,
14
15
  evaluateSatelliteRunHealth,
15
16
  findPlugin,
16
17
  guardProtectedVaultWrite,
18
+ isEventLedgerPath,
17
19
  isFailedRunStatus,
18
20
  measureDirtyVolume,
19
21
  memoryAuthorityTiersRank,
@@ -53,7 +55,7 @@ import {
53
55
  snapshotterHealthChecks,
54
56
  upsertIndexEntry,
55
57
  vectorIndexStatus
56
- } from "./chunk-ZYVP52PM.js";
58
+ } from "./chunk-BXEG5OWD.js";
57
59
  import {
58
60
  normalizeDistTag,
59
61
  readCache,
@@ -66,7 +68,7 @@ import {
66
68
  UNMANAGED_START,
67
69
  renderRootIndex,
68
70
  writeRootIndexProjection
69
- } from "./chunk-ZUHA2EXE.js";
71
+ } from "./chunk-3KEMRVWR.js";
70
72
  import {
71
73
  FLEET_REL_PATH,
72
74
  REDACTED_MALFORMED_REFERENCE,
@@ -115,6 +117,7 @@ import {
115
117
  profileKey,
116
118
  readJournal,
117
119
  readLastOp,
120
+ readSkillWikiConfig,
118
121
  reconcileTaxonomyDocument,
119
122
  releaseOwnedSyncLock,
120
123
  renderVaultGitignoreTemplate,
@@ -157,12 +160,12 @@ import {
157
160
  supersedeStaleReviewRequiredJournals,
158
161
  taxonomyCommentForPage,
159
162
  writeDotenv
160
- } from "./chunk-6WE7E6VB.js";
163
+ } from "./chunk-OTESAW36.js";
161
164
  import {
162
165
  assertTargetInsideVault,
163
166
  atomicWriteText,
164
167
  prepareTypedPage
165
- } from "./chunk-JHDN3FSF.js";
168
+ } from "./chunk-C5WOEVX3.js";
166
169
  import {
167
170
  applySourceDisposition,
168
171
  decodeSourceActionApproval,
@@ -170,7 +173,7 @@ import {
170
173
  inventorySources,
171
174
  planSourceDisposition,
172
175
  runSourcesPending
173
- } from "./chunk-ECOEJABP.js";
176
+ } from "./chunk-UDOZKMCN.js";
174
177
  import {
175
178
  authorizeRawOperation,
176
179
  buildSourceReferenceIndex,
@@ -187,7 +190,7 @@ import {
187
190
  resolveExistingRegularFileInsideVault,
188
191
  validateLogEvent,
189
192
  writeLogEvent
190
- } from "./chunk-QFRZDTJY.js";
193
+ } from "./chunk-LTSIHHKD.js";
191
194
  import {
192
195
  ExitCode,
193
196
  MetaSchema,
@@ -208,10 +211,10 @@ import {
208
211
  scanVault,
209
212
  splitFrontmatter,
210
213
  vaultIoConcurrency
211
- } from "./chunk-XJR3XXP5.js";
214
+ } from "./chunk-SXNKZGNM.js";
212
215
 
213
216
  // src/cli.ts
214
- import { join as join43 } from "path";
217
+ import { join as join44 } from "path";
215
218
  import { Command } from "commander";
216
219
 
217
220
  // src/utils/output.ts
@@ -1604,21 +1607,21 @@ function rollbackPath(vault, path, stage2, stage3) {
1604
1607
  function hasMarkers(path, text) {
1605
1608
  const lines = text.split(/\r?\n/);
1606
1609
  let open2 = false;
1607
- let sep4 = false;
1610
+ let sep5 = false;
1608
1611
  for (const line of lines) {
1609
1612
  if (line.startsWith("<<<<<<< ")) {
1610
1613
  open2 = true;
1611
- sep4 = false;
1614
+ sep5 = false;
1612
1615
  continue;
1613
1616
  }
1614
1617
  if (line === "=======" && open2) {
1615
- sep4 = true;
1618
+ sep5 = true;
1616
1619
  continue;
1617
1620
  }
1618
- if (line.startsWith(">>>>>>> ") && open2 && sep4) return true;
1621
+ if (line.startsWith(">>>>>>> ") && open2 && sep5) return true;
1619
1622
  if (line.startsWith(">>>>>>> ")) {
1620
1623
  open2 = false;
1621
- sep4 = false;
1624
+ sep5 = false;
1622
1625
  }
1623
1626
  }
1624
1627
  return false;
@@ -3785,7 +3788,7 @@ ${fmRewritten}
3785
3788
  }
3786
3789
  let indexUpdated = false;
3787
3790
  if (!isRaw) {
3788
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-YP4JIMIQ.js");
3791
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-3ZMM53WL.js");
3789
3792
  const before = await readFile7(join19(input.vault, "index.md"), "utf8").catch(() => "");
3790
3793
  const fullTarget = relPath.replace(/\.md$/, "");
3791
3794
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -3906,10 +3909,10 @@ async function runRemove(input) {
3906
3909
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
3907
3910
  await unlink2(join20(input.vault, relPath));
3908
3911
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
3909
- const { readFile: readFile22 } = await import("fs/promises");
3912
+ const { readFile: readFile23 } = await import("fs/promises");
3910
3913
  const { join: pathJoin } = await import("path");
3911
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-YP4JIMIQ.js");
3912
- const before = await readFile22(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3914
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-3ZMM53WL.js");
3915
+ const before = await readFile23(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3913
3916
  const fullTarget = relPath.replace(/\.md$/, "");
3914
3917
  const bare = fullTarget.split("/").pop() ?? fullTarget;
3915
3918
  const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
@@ -7366,7 +7369,7 @@ async function acquireRawCaptureLock(path) {
7366
7369
  if (error.code !== "EEXIST") {
7367
7370
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
7368
7371
  }
7369
- await new Promise((resolve7) => setTimeout(resolve7, 10));
7372
+ await new Promise((resolve9) => setTimeout(resolve9, 10));
7370
7373
  }
7371
7374
  }
7372
7375
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
@@ -9328,8 +9331,83 @@ async function runStatus(input) {
9328
9331
  }
9329
9332
 
9330
9333
  // src/commands/copy-status.ts
9331
- import { existsSync as existsSync13 } from "fs";
9332
- import { join as join37 } from "path";
9334
+ import { existsSync as existsSync13, realpathSync as realpathSync5 } from "fs";
9335
+ import { isAbsolute as isAbsolute2, join as join38, relative as relative5, resolve as resolve8 } from "path";
9336
+
9337
+ // src/utils/fetch-projection.ts
9338
+ import { isAbsolute, join as join37, resolve as resolve7 } from "path";
9339
+ function inspectConfiguredFetchProjection(home) {
9340
+ if (!home) return { configured: false };
9341
+ const config = readSkillWikiConfig(join37(home, ".skillwiki", ".env"));
9342
+ const configured = config["vault_sync.fetch_projection"];
9343
+ if (!configured || configured === "none") return { configured: false };
9344
+ if (!isAbsolute(configured)) {
9345
+ return { configured: true, invalidDetail: "configured fetch projection path must be absolute" };
9346
+ }
9347
+ return { configured: true, path: resolve7(configured) };
9348
+ }
9349
+
9350
+ // src/utils/live-drift.ts
9351
+ import { createHash as createHash6 } from "crypto";
9352
+ import { readFile as readFile19 } from "fs/promises";
9353
+ import { relative as relative4, sep as sep3 } from "path";
9354
+
9355
+ // src/utils/git-promotion.ts
9356
+ function normalizedRelativePath(path) {
9357
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
9358
+ }
9359
+ function isGitPromotablePath(path) {
9360
+ const rel = normalizedRelativePath(path);
9361
+ if (/^(?:\.skillwiki|\.claude|\.obsidian|\.antigravitycli|\.playwright-cli|\.superpowers|\.snapshots|\.git|\.drafts)(?:\/|$)/.test(rel)) {
9362
+ return false;
9363
+ }
9364
+ if (/^(?:tmp|logs)(?:\/|$)/.test(rel) || isEventLedgerPath(rel)) return false;
9365
+ if (rel === "raw/._.DS_Store" || rel === "._.DS_Store" || rel.startsWith("._")) return false;
9366
+ if (rel.startsWith(".conflict") || rel.includes(".conflict-")) return false;
9367
+ return true;
9368
+ }
9369
+
9370
+ // src/utils/live-drift.ts
9371
+ function gitBlobOid(bytes) {
9372
+ const header = Buffer.from(`blob ${bytes.length}\0`);
9373
+ return createHash6("sha1").update(header).update(bytes).digest("hex");
9374
+ }
9375
+ function headBlobOids(projection) {
9376
+ const output = git(projection, ["ls-tree", "-rz", "-r", "HEAD"]);
9377
+ if (!output) return void 0;
9378
+ const blobs = /* @__PURE__ */ new Map();
9379
+ for (const entry of output.split("\0")) {
9380
+ if (!entry) continue;
9381
+ const match = entry.match(/^\d+\s+blob\s+([0-9a-f]{40})\t(.+)$/s);
9382
+ if (match) blobs.set(match[2], match[1]);
9383
+ }
9384
+ return blobs;
9385
+ }
9386
+ async function measureAuthoritativeLiveDrift(liveVault, projection) {
9387
+ const projectionBlobs = headBlobOids(projection);
9388
+ if (!projectionBlobs) return { unknown: true, detail: "projection HEAD unreadable" };
9389
+ const scan = await scanVault(liveVault);
9390
+ if (!scan.ok) return { unknown: true, detail: "live Markdown inventory unavailable" };
9391
+ let content = 0;
9392
+ const livePromotable = /* @__PURE__ */ new Set();
9393
+ for (const page of scan.data.allMarkdown) {
9394
+ const rel = relative4(liveVault, page.absPath).split(sep3).join("/");
9395
+ if (!isGitPromotablePath(rel)) continue;
9396
+ livePromotable.add(rel);
9397
+ const bytes = await readFile19(page.absPath);
9398
+ if (projectionBlobs.get(rel) !== gitBlobOid(bytes)) content += 1;
9399
+ }
9400
+ for (const rel of projectionBlobs.keys()) {
9401
+ if (rel.endsWith(".md") && isGitPromotablePath(rel) && !livePromotable.has(rel)) content += 1;
9402
+ }
9403
+ const events = await readLogEvents(liveVault);
9404
+ if (!events.ok) return { unknown: true, detail: "live event ledger unreadable" };
9405
+ return {
9406
+ content,
9407
+ ledger: events.data.length,
9408
+ detail: content > 0 || events.data.length > 0 ? "authoritative live data ahead of GitHub" : void 0
9409
+ };
9410
+ }
9333
9411
 
9334
9412
  // src/copy-status/copy-status.ts
9335
9413
  function liveRecord(p) {
@@ -9413,7 +9491,28 @@ function localRecord(p, githubOid) {
9413
9491
  p
9414
9492
  );
9415
9493
  }
9416
- return withDirty({ state: "unknown", detail: p.detail ?? "leaf clone unmeasured" }, p);
9494
+ return withDirty({ state: "unknown", detail: p.detail ?? "live-vault Git unmeasured" }, p);
9495
+ }
9496
+ function liveDriftRecord(p) {
9497
+ if (!p || p.unknown || p.content === void 0 && p.ledger === void 0) {
9498
+ return { state: "unknown", detail: p?.detail ?? "live drift unmeasured" };
9499
+ }
9500
+ const content = p.content ?? 0;
9501
+ const ledger = p.ledger ?? 0;
9502
+ const rec = {
9503
+ state: content > 0 || ledger > 0 ? "drifted" : "clean"
9504
+ };
9505
+ if (p.content !== void 0) rec.content = content;
9506
+ if (p.ledger !== void 0) rec.ledger = ledger;
9507
+ if (p.detail) rec.detail = p.detail;
9508
+ return rec;
9509
+ }
9510
+ function formatLiveDrift(rec) {
9511
+ const parts = [`live_drift: ${rec.state}`];
9512
+ if (rec.content !== void 0) parts.push(`content=${rec.content}`);
9513
+ if (rec.ledger !== void 0) parts.push(`ledger=${rec.ledger}`);
9514
+ if (rec.detail) parts.push(rec.detail);
9515
+ return parts.join(" ");
9417
9516
  }
9418
9517
  function formatPlane(name, rec) {
9419
9518
  const parts = [`${name}: ${rec.state}`];
@@ -9432,31 +9531,59 @@ function composeCopyStatus(input) {
9432
9531
  const live = liveRecord(input.live);
9433
9532
  const github = githubRecord(input.github);
9434
9533
  const local_git = localRecord(input.local, input.github.oid);
9534
+ const live_drift = liveDriftRecord(input.liveDrift);
9435
9535
  const humanHint = [
9436
9536
  formatPlane("live", live),
9437
9537
  formatPlane("github", github),
9438
- formatPlane("local_git", local_git)
9538
+ formatPlane("local_git", local_git),
9539
+ formatLiveDrift(live_drift)
9439
9540
  ].join("\n");
9440
- return { live, github, local_git, humanHint };
9541
+ return { live, github, local_git, live_drift, humanHint };
9441
9542
  }
9442
9543
  async function runCopyStatus(deps) {
9443
- const [live, github, local] = await Promise.all([
9544
+ const [live, github, local, liveDrift] = await Promise.all([
9444
9545
  deps.probeLive(),
9445
9546
  deps.probeGithub(),
9446
- deps.probeLocalGit()
9547
+ deps.probeLocalGit(),
9548
+ deps.probeLiveDrift?.()
9447
9549
  ]);
9448
- return composeCopyStatus({ live, github, local });
9550
+ return composeCopyStatus({ live, github, local, liveDrift });
9449
9551
  }
9450
9552
 
9451
9553
  // src/commands/copy-status.ts
9554
+ function comparablePath(path) {
9555
+ try {
9556
+ return realpathSync5(path);
9557
+ } catch {
9558
+ return resolve8(path);
9559
+ }
9560
+ }
9561
+ function pathsAreNested(left, right) {
9562
+ const rel = relative5(comparablePath(left), comparablePath(right));
9563
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
9564
+ }
9452
9565
  function defaultCopyStatusDeps(input) {
9566
+ const selection = inspectConfiguredFetchProjection(input.home);
9567
+ const sameAsLive = selection.path !== void 0 && comparablePath(selection.path) === comparablePath(input.vault);
9568
+ const nestedWithLive = selection.path !== void 0 && (pathsAreNested(selection.path, input.vault) || pathsAreNested(input.vault, selection.path));
9569
+ const configuredProjection = sameAsLive || nestedWithLive ? void 0 : selection.path;
9570
+ const projectionProblem = selection.invalidDetail ?? (sameAsLive ? "configured fetch projection must be distinct from live vault" : nestedWithLive ? "configured fetch projection and live vault must not be nested" : void 0);
9571
+ const gitVault = input.vault;
9572
+ const projectionWarning = selection.configured ? `configured fetch projection ignored for Git status; using live vault${projectionProblem ? ` (${projectionProblem})` : ""}` : void 0;
9573
+ const invalidProjectionDetail = projectionProblem ?? "configured fetch projection is not a git repository";
9574
+ const gitRootProblem = () => {
9575
+ if (!existsSync13(join38(gitVault, ".git"))) {
9576
+ return "vault is not a git repository";
9577
+ }
9578
+ return void 0;
9579
+ };
9453
9580
  return {
9454
9581
  probeLive() {
9455
9582
  if (input.s3Ok === true) return { reachable: true, detail: "MCP S3 ok" };
9456
9583
  if (input.s3Ok === false) return { reachable: false, detail: "MCP S3 not ok" };
9457
9584
  const remote = resolveWikiS3Remote({ home: input.home });
9458
9585
  if (!remote) {
9459
- return { unknown: true, detail: "leaf clone is not the live plane; S3 remote unconfigured" };
9586
+ return { unknown: true, detail: "Git worktree is not the live plane; S3 remote unconfigured" };
9460
9587
  }
9461
9588
  const reach = probeS3Reachability(remote);
9462
9589
  if (reach === "ok") return { reachable: true, detail: "S3 reachable" };
@@ -9464,18 +9591,17 @@ function defaultCopyStatusDeps(input) {
9464
9591
  return { unknown: true, detail: "S3 unmeasured" };
9465
9592
  },
9466
9593
  probeGithub() {
9467
- if (!existsSync13(join37(input.vault, ".git"))) {
9468
- return { unknown: true, detail: "vault is not a git repository" };
9469
- }
9470
- const ls = git(input.vault, ["ls-remote", "origin", "refs/heads/main"], {
9594
+ const problem = gitRootProblem();
9595
+ if (problem) return { unknown: true, detail: problem };
9596
+ const ls = git(gitVault, ["ls-remote", "origin", "refs/heads/main"], {
9471
9597
  timeoutMs: REMOTE_PROBE_TIMEOUT_MS
9472
9598
  });
9473
9599
  const oid = ls.split(/\s+/)[0];
9474
9600
  if (!oid) return { unknown: true, detail: "git ls-remote origin main failed" };
9475
- const originMain = git(input.vault, ["rev-parse", "--verify", "origin/main"]);
9601
+ const originMain = git(gitVault, ["rev-parse", "--verify", "origin/main"]);
9476
9602
  let ageHours;
9477
9603
  if (originMain && originMain === oid) {
9478
- const ct = git(input.vault, ["log", "-1", "--format=%ct", "origin/main"]);
9604
+ const ct = git(gitVault, ["log", "-1", "--format=%ct", "origin/main"]);
9479
9605
  const sec = Number.parseInt(ct, 10);
9480
9606
  if (Number.isFinite(sec) && sec > 0) {
9481
9607
  ageHours = Math.max(0, Math.round((Date.now() / 1e3 - sec) / 3600));
@@ -9484,31 +9610,40 @@ function defaultCopyStatusDeps(input) {
9484
9610
  return { oid, ageHours, detail: "git ls-remote origin main" };
9485
9611
  },
9486
9612
  probeLocalGit() {
9487
- if (!existsSync13(join37(input.vault, ".git"))) {
9488
- return { unknown: true, detail: "vault is not a git repository" };
9489
- }
9490
- const head = git(input.vault, ["rev-parse", "HEAD"]);
9613
+ const problem = gitRootProblem();
9614
+ if (problem) return { unknown: true, detail: problem };
9615
+ const head = git(gitVault, ["rev-parse", "HEAD"]);
9491
9616
  if (!head) return { unknown: true, detail: "HEAD unreadable" };
9492
- const behindRaw = git(input.vault, ["rev-list", "--count", "HEAD..origin/main"]);
9617
+ const behindRaw = git(gitVault, ["rev-list", "--count", "HEAD..origin/main"]);
9493
9618
  const behind = behindRaw === "" ? void 0 : Number.parseInt(behindRaw, 10);
9494
- const review = listReviewRequiredOps(input.vault)[0];
9619
+ const review = listReviewRequiredOps(gitVault)[0];
9495
9620
  const blockedReason = review ? `review-required:${review.opId}` : void 0;
9496
- const dirty = measureDirtyVolume(input.vault);
9621
+ const dirty = measureDirtyVolume(gitVault);
9497
9622
  const dirtyCount = dirty.is_git_repo ? dirty.expanded_files : void 0;
9498
9623
  const untrackedCount = dirty.is_git_repo ? dirty.untracked : void 0;
9499
- const ledgerUntracked = dirty.is_git_repo ? dirty.ledger_files : void 0;
9624
+ const ledgerUntracked = dirty.is_git_repo ? countEventLedgerFiles(gitVault) : void 0;
9500
9625
  const contentUntracked = dirty.is_git_repo ? dirty.content_files : void 0;
9501
- const dirtyHint = dirtyCount && dirtyCount > 0 ? ledgerUntracked && ledgerUntracked > 0 ? `event-ledger live-ahead of GitHub; do not git add` : `live-ahead of GitHub; do not git add` : void 0;
9626
+ const dirtyHint = ledgerUntracked && ledgerUntracked > 0 ? `event-ledger live-ahead of GitHub; do not git add` : dirtyCount && dirtyCount > 0 ? `live-ahead of GitHub; do not git add` : void 0;
9627
+ const detail = [dirtyHint, blockedReason, projectionWarning].filter(Boolean).join("; ") || void 0;
9502
9628
  return {
9503
9629
  head,
9504
9630
  behind: Number.isFinite(behind) ? behind : void 0,
9505
9631
  blockedReason,
9506
9632
  dirty: dirtyCount,
9507
9633
  untracked: untrackedCount,
9508
- ledger_untracked: dirtyCount && dirtyCount > 0 ? ledgerUntracked : void 0,
9509
- content_untracked: dirtyCount && dirtyCount > 0 ? contentUntracked : void 0,
9510
- detail: dirtyHint ?? blockedReason
9634
+ ledger_untracked: ledgerUntracked && ledgerUntracked > 0 ? ledgerUntracked : void 0,
9635
+ content_untracked: contentUntracked && contentUntracked > 0 ? contentUntracked : void 0,
9636
+ detail
9511
9637
  };
9638
+ },
9639
+ probeLiveDrift() {
9640
+ if (!configuredProjection) {
9641
+ return { unknown: true, detail: projectionProblem ?? "live drift requires a configured fetch projection" };
9642
+ }
9643
+ if (!existsSync13(join38(configuredProjection, ".git"))) {
9644
+ return { unknown: true, detail: invalidProjectionDetail };
9645
+ }
9646
+ return measureAuthoritativeLiveDrift(input.vault, configuredProjection);
9512
9647
  }
9513
9648
  };
9514
9649
  }
@@ -9522,7 +9657,7 @@ async function runCopyStatusCommand(input) {
9522
9657
 
9523
9658
  // src/commands/seed.ts
9524
9659
  import { mkdir as mkdir12, writeFile as writeFile8, stat as stat4 } from "fs/promises";
9525
- import { join as join38 } from "path";
9660
+ import { join as join39 } from "path";
9526
9661
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9527
9662
  var EXAMPLE_PAGES = {
9528
9663
  "entities/example-project.md": `---
@@ -9591,29 +9726,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
9591
9726
  `;
9592
9727
  async function runSeed(input) {
9593
9728
  try {
9594
- await stat4(join38(input.vault, "SCHEMA.md"));
9729
+ await stat4(join39(input.vault, "SCHEMA.md"));
9595
9730
  } catch {
9596
9731
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
9597
9732
  }
9598
9733
  const created = [];
9599
9734
  const skipped = [];
9600
9735
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
9601
- const absPath = join38(input.vault, relPath);
9736
+ const absPath = join39(input.vault, relPath);
9602
9737
  try {
9603
9738
  await stat4(absPath);
9604
9739
  skipped.push(relPath);
9605
9740
  } catch {
9606
- await mkdir12(join38(absPath, ".."), { recursive: true });
9741
+ await mkdir12(join39(absPath, ".."), { recursive: true });
9607
9742
  await writeFile8(absPath, content, "utf8");
9608
9743
  created.push(relPath);
9609
9744
  }
9610
9745
  }
9611
- const rawPath = join38(input.vault, "raw", "articles", "example-source.md");
9746
+ const rawPath = join39(input.vault, "raw", "articles", "example-source.md");
9612
9747
  try {
9613
9748
  await stat4(rawPath);
9614
9749
  skipped.push("raw/articles/example-source.md");
9615
9750
  } catch {
9616
- await mkdir12(join38(rawPath, ".."), { recursive: true });
9751
+ await mkdir12(join39(rawPath, ".."), { recursive: true });
9617
9752
  await writeFile8(rawPath, EXAMPLE_RAW, "utf8");
9618
9753
  created.push("raw/articles/example-source.md");
9619
9754
  }
@@ -9636,9 +9771,9 @@ async function runSeed(input) {
9636
9771
  }
9637
9772
 
9638
9773
  // src/commands/canvas.ts
9639
- import { readFile as readFile19, writeFile as writeFile9 } from "fs/promises";
9774
+ import { readFile as readFile20, writeFile as writeFile9 } from "fs/promises";
9640
9775
  import { existsSync as existsSync14 } from "fs";
9641
- import { join as join39 } from "path";
9776
+ import { join as join40 } from "path";
9642
9777
  var NODE_WIDTH = 240;
9643
9778
  var NODE_HEIGHT = 60;
9644
9779
  var COLUMN_SPACING = 400;
@@ -9716,7 +9851,7 @@ function buildCanvasEdges(adjacency) {
9716
9851
  return edges;
9717
9852
  }
9718
9853
  async function runCanvasGenerate(input) {
9719
- const graphPath = input.graphPath ?? join39(input.vault, ".skillwiki", "graph.json");
9854
+ const graphPath = input.graphPath ?? join40(input.vault, ".skillwiki", "graph.json");
9720
9855
  if (!existsSync14(graphPath)) {
9721
9856
  return {
9722
9857
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9728,7 +9863,7 @@ async function runCanvasGenerate(input) {
9728
9863
  }
9729
9864
  let raw;
9730
9865
  try {
9731
- raw = await readFile19(graphPath, "utf8");
9866
+ raw = await readFile20(graphPath, "utf8");
9732
9867
  } catch (e) {
9733
9868
  return {
9734
9869
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9754,7 +9889,7 @@ async function runCanvasGenerate(input) {
9754
9889
  const nodes = buildCanvasNodes(paths);
9755
9890
  const edges = buildCanvasEdges(graph.adjacency);
9756
9891
  const canvas = { nodes, edges };
9757
- const outPath = join39(input.vault, "vault-graph.canvas");
9892
+ const outPath = join40(input.vault, "vault-graph.canvas");
9758
9893
  try {
9759
9894
  await writeFile9(outPath, JSON.stringify(canvas, null, 2));
9760
9895
  } catch (e) {
@@ -9898,12 +10033,12 @@ async function runSourceDisposition(input) {
9898
10033
  }
9899
10034
 
9900
10035
  // src/utils/source-disposal.ts
9901
- import { createHash as createHash6 } from "crypto";
9902
- import { readFile as readFile21, unlink as unlink5 } from "fs/promises";
10036
+ import { createHash as createHash7 } from "crypto";
10037
+ import { readFile as readFile22, unlink as unlink5 } from "fs/promises";
9903
10038
 
9904
10039
  // src/utils/raw-asset-reference-index.ts
9905
- import { readdir as readdir7, readFile as readFile20 } from "fs/promises";
9906
- import { join as join40, relative as relative4, sep as sep3 } from "path";
10040
+ import { readdir as readdir7, readFile as readFile21 } from "fs/promises";
10041
+ import { join as join41, relative as relative6, sep as sep4 } from "path";
9907
10042
  var PREVIEW_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "pdf", "mp3", "wav", "m4a", "mp4", "webm"]);
9908
10043
  async function walkFiles(root, dir = root) {
9909
10044
  let entries;
@@ -9914,9 +10049,9 @@ async function walkFiles(root, dir = root) {
9914
10049
  }
9915
10050
  const out = [];
9916
10051
  for (const entry of entries) {
9917
- const path = join40(dir, entry.name);
10052
+ const path = join41(dir, entry.name);
9918
10053
  if (entry.isDirectory()) out.push(...await walkFiles(root, path));
9919
- else if (entry.isFile()) out.push(relative4(root, path).split(sep3).join("/"));
10054
+ else if (entry.isFile()) out.push(relative6(root, path).split(sep4).join("/"));
9920
10055
  }
9921
10056
  return out;
9922
10057
  }
@@ -9930,7 +10065,7 @@ function addInbound(map, target, source) {
9930
10065
  map.set(target, refs);
9931
10066
  }
9932
10067
  async function buildRawAssetReferenceIndex(vault) {
9933
- const assetRoot = join40(vault, "raw", "assets");
10068
+ const assetRoot = join41(vault, "raw", "assets");
9934
10069
  const relativeAssets = await walkFiles(assetRoot);
9935
10070
  const assets = relativeAssets.map((path) => `raw/assets/${path}`).sort((a, b) => a.localeCompare(b));
9936
10071
  const assetSet = new Set(assets);
@@ -9946,7 +10081,7 @@ async function buildRawAssetReferenceIndex(vault) {
9946
10081
  for (const page of scan.data.allMarkdown) {
9947
10082
  let text;
9948
10083
  try {
9949
- text = await readFile20(page.absPath, "utf8");
10084
+ text = await readFile21(page.absPath, "utf8");
9950
10085
  } catch {
9951
10086
  continue;
9952
10087
  }
@@ -9982,7 +10117,7 @@ async function buildRawAssetReferenceIndex(vault) {
9982
10117
 
9983
10118
  // src/utils/source-disposal.ts
9984
10119
  function sha256(bytes) {
9985
- return createHash6("sha256").update(bytes).digest("hex");
10120
+ return createHash7("sha256").update(bytes).digest("hex");
9986
10121
  }
9987
10122
  function exactTarget(path) {
9988
10123
  if (/[?*\[\]{}]/.test(path) || !path.includes("/") || path.endsWith("/")) return err("RAW_DISPOSAL_TARGET_INVALID", { path, message: "exact file path required; globs, basenames, and directories are refused" });
@@ -10006,7 +10141,7 @@ async function planSourceDisposal(input) {
10006
10141
  if (sensitive.length > 0) return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
10007
10142
  const target = await resolveExistingRegularFileInsideVault(input.vault, input.rawPath);
10008
10143
  if (!target.ok) return err("RAW_DISPOSAL_TARGET_INVALID", { path: input.rawPath, cause: target });
10009
- const bytes = await readFile21(target.data);
10144
+ const bytes = await readFile22(target.data);
10010
10145
  const size = bytes.byteLength;
10011
10146
  let typedReferences = [];
10012
10147
  let otherReferences = [];
@@ -10130,7 +10265,7 @@ async function runSourceDisposal(input) {
10130
10265
  import { existsSync as existsSync15, readFileSync as readFileSync16 } from "fs";
10131
10266
  import { execSync as nodeExecSync } from "child_process";
10132
10267
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
10133
- import { join as join41 } from "path";
10268
+ import { join as join42 } from "path";
10134
10269
  var SSH_TIMEOUT_MS = 15e3;
10135
10270
  var TIMER_UNIT = "agent-memory-trends.timer";
10136
10271
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -10360,7 +10495,7 @@ async function runFleetHealth(input) {
10360
10495
  const home = input.home ?? env.HOME ?? "";
10361
10496
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
10362
10497
  const vault = input.vault ?? env.WIKI_PATH;
10363
- const file = input.file ?? (vault ? join41(vault, FLEET_REL_PATH) : void 0);
10498
+ const file = input.file ?? (vault ? join42(vault, FLEET_REL_PATH) : void 0);
10364
10499
  if (!file) {
10365
10500
  return {
10366
10501
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -10445,12 +10580,12 @@ async function runFleetHealth(input) {
10445
10580
 
10446
10581
  // src/utils/auto-commit.ts
10447
10582
  import { existsSync as existsSync16 } from "fs";
10448
- import { isAbsolute, join as join42, normalize } from "path";
10583
+ import { isAbsolute as isAbsolute3, join as join43, normalize } from "path";
10449
10584
  function isValidLastOpPath(file) {
10450
10585
  if (!file || typeof file !== "string") return false;
10451
10586
  const trimmed = file.trim();
10452
10587
  if (trimmed.length === 0) return false;
10453
- if (isAbsolute(trimmed)) return false;
10588
+ if (isAbsolute3(trimmed)) return false;
10454
10589
  const normalized = normalize(trimmed).replace(/\\/g, "/");
10455
10590
  if (normalized.startsWith("../") || normalized === ".." || normalized.includes("/../")) return false;
10456
10591
  for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
@@ -10466,7 +10601,7 @@ async function postCommit(vault, exitCode) {
10466
10601
  const dotenv = await parseDotenvFile(configPath(home));
10467
10602
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
10468
10603
  if (autoCommit === "false") return;
10469
- if (!existsSync16(join42(vault, ".git"))) return;
10604
+ if (!existsSync16(join43(vault, ".git"))) return;
10470
10605
  const lastOps = readLastOp(vault);
10471
10606
  if (lastOps.length === 0) return;
10472
10607
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -10508,7 +10643,7 @@ async function postCommit(vault, exitCode) {
10508
10643
  import { readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "fs";
10509
10644
 
10510
10645
  // src/utils/mcp-token-map.ts
10511
- import { createHash as createHash7, randomBytes } from "crypto";
10646
+ import { createHash as createHash8, randomBytes } from "crypto";
10512
10647
  import yaml from "js-yaml";
10513
10648
  var HOST_ID_RE = /^[a-z][a-z0-9-]{1,62}$/;
10514
10649
  function parseMcpTokenMap(yamlText) {
@@ -10532,7 +10667,7 @@ function parseMcpTokenMap(yamlText) {
10532
10667
  function generateHostBearer(rng) {
10533
10668
  const bytes = (rng ?? (() => randomBytes(32)))();
10534
10669
  const raw = bytes.toString("base64url");
10535
- const hashHex = createHash7("sha256").update(raw, "utf8").digest("hex");
10670
+ const hashHex = createHash8("sha256").update(raw, "utf8").digest("hex");
10536
10671
  return { raw, hashHex };
10537
10672
  }
10538
10673
  function appendHostHash(yamlText, hashHex, hostId) {
@@ -10561,7 +10696,7 @@ function preflight(error) {
10561
10696
  }
10562
10697
  async function runMcpAuthIssueHost(input) {
10563
10698
  const isTty = input.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
10564
- const readFile22 = input.readFile ?? readFileSync17;
10699
+ const readFile23 = input.readFile ?? readFileSync17;
10565
10700
  const writeFile10 = input.writeFile ?? writeFileSync8;
10566
10701
  const stderrWrite = input.stderrWrite ?? ((s) => {
10567
10702
  process.stderr.write(s);
@@ -10574,7 +10709,7 @@ async function runMcpAuthIssueHost(input) {
10574
10709
  if (write && !isTty) return preflight("NO_TTY");
10575
10710
  let yamlText = "";
10576
10711
  try {
10577
- yamlText = readFile22(mapPath, "utf8");
10712
+ yamlText = readFile23(mapPath, "utf8");
10578
10713
  } catch (error) {
10579
10714
  if (!isEnoent(error)) throw error;
10580
10715
  }
@@ -10689,8 +10824,8 @@ async function emit(r, vault, opts) {
10689
10824
  printJson(r.result);
10690
10825
  }
10691
10826
  if (vault && opts?.postCommit !== false) await postCommit(vault, r.exitCode);
10692
- await new Promise((resolve7, reject) => {
10693
- process.stdout.write("", (error) => error ? reject(error) : resolve7());
10827
+ await new Promise((resolve9, reject) => {
10828
+ process.stdout.write("", (error) => error ? reject(error) : resolve9());
10694
10829
  });
10695
10830
  process.exit(r.exitCode);
10696
10831
  }
@@ -10753,7 +10888,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
10753
10888
  if (dirty) {
10754
10889
  return emit(dirty, void 0, { postCommit: false });
10755
10890
  }
10756
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-2Y5Q2WH4.js");
10891
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-T4WOM52R.js");
10757
10892
  const run = await runManagedWriteTransaction2({
10758
10893
  vault,
10759
10894
  command,
@@ -10783,7 +10918,7 @@ program.command("validate <file>").description("validate vault page frontmatter
10783
10918
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
10784
10919
  });
10785
10920
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10786
- const out = opts.out ?? join43(vault, ".skillwiki", "graph.json");
10921
+ const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
10787
10922
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
10788
10923
  });
10789
10924
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -11405,7 +11540,7 @@ program.command("status [vault]").description("output vault diagnostics").option
11405
11540
  langEnvValue: process.env.WIKI_LANG
11406
11541
  }), v.vault);
11407
11542
  });
11408
- program.command("copy-status [vault]").description("plane-tagged vault freshness: live S3 vs GitHub snapshot vs leaf clone").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11543
+ program.command("copy-status [vault]").description("plane-tagged vault freshness: live S3 vs GitHub snapshot vs live-vault Git").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11409
11544
  const v = await resolveVaultArg(vault, opts.wiki);
11410
11545
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11411
11546
  else emit(await runCopyStatusCommand({
@@ -4,9 +4,9 @@ import {
4
4
  UNMANAGED_START,
5
5
  renderRootIndex,
6
6
  writeRootIndexProjection
7
- } from "./chunk-ZUHA2EXE.js";
8
- import "./chunk-JHDN3FSF.js";
9
- import "./chunk-XJR3XXP5.js";
7
+ } from "./chunk-3KEMRVWR.js";
8
+ import "./chunk-C5WOEVX3.js";
9
+ import "./chunk-SXNKZGNM.js";
10
10
  export {
11
11
  UNMANAGED_END,
12
12
  UNMANAGED_START,
@@ -6,10 +6,10 @@ import {
6
6
  runManagedWritePeerGate,
7
7
  runManagedWritePreflight,
8
8
  runManagedWriteTransaction
9
- } from "./chunk-6WE7E6VB.js";
10
- import "./chunk-JHDN3FSF.js";
11
- import "./chunk-QFRZDTJY.js";
12
- import "./chunk-XJR3XXP5.js";
9
+ } from "./chunk-OTESAW36.js";
10
+ import "./chunk-C5WOEVX3.js";
11
+ import "./chunk-LTSIHHKD.js";
12
+ import "./chunk-SXNKZGNM.js";
13
13
  export {
14
14
  DEFAULT_MANAGED_WRITE_WAIT_MS,
15
15
  MANAGED_WRITE_POLL_INTERVAL_MS,
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-ZYVP52PM.js";
4
+ } from "./chunk-BXEG5OWD.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-ZUHA2EXE.js";
7
- import "./chunk-6WE7E6VB.js";
8
- import "./chunk-JHDN3FSF.js";
9
- import "./chunk-ECOEJABP.js";
10
- import "./chunk-QFRZDTJY.js";
11
- import "./chunk-XJR3XXP5.js";
6
+ import "./chunk-3KEMRVWR.js";
7
+ import "./chunk-OTESAW36.js";
8
+ import "./chunk-C5WOEVX3.js";
9
+ import "./chunk-UDOZKMCN.js";
10
+ import "./chunk-LTSIHHKD.js";
11
+ import "./chunk-SXNKZGNM.js";
12
12
 
13
13
  // src/mcp-entry.ts
14
14
  runSkillwikiMcpStdio().catch((error) => {
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runSourcesPending
4
+ } from "./chunk-UDOZKMCN.js";
5
+ import "./chunk-LTSIHHKD.js";
6
+ import "./chunk-SXNKZGNM.js";
7
+ export {
8
+ runSourcesPending
9
+ };
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # GitHub promotion policy shared by snapshot execution and CLI parity tests.
3
+ # This is intentionally narrower than S3 ownership.
4
+
5
+ snapshot_non_promotable_path() {
6
+ local p="${1#./}"
7
+ case "$p" in
8
+ .skillwiki|.skillwiki/*|.claude|.claude/*|.obsidian|.obsidian/*|.antigravitycli|.antigravitycli/*|.playwright-cli|.playwright-cli/*|.superpowers|.superpowers/*|.snapshots|.snapshots/*|.git|.git/*|.drafts|.drafts/*)
9
+ return 0 ;;
10
+ tmp|tmp/*|logs|logs/*|meta/log-events|meta/log-events/*)
11
+ return 0 ;;
12
+ raw/._.DS_Store|._.DS_Store)
13
+ return 0 ;;
14
+ ._*)
15
+ return 0 ;;
16
+ .conflict*|*.conflict-*)
17
+ return 0 ;;
18
+ esac
19
+ return 1
20
+ }
@@ -21,7 +21,37 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
21
21
  . "$SCRIPT_DIR/lib/git-operation-journal.sh"
22
22
  platform_detect_os
23
23
 
24
- WIKI_DIR="${WIKI_DIR:-$HOME/wiki}"
24
+ CONFIG_FILE="$HOME/.skillwiki/.env"
25
+ config_value() {
26
+ local key="$1"
27
+ [ -f "$CONFIG_FILE" ] || return 0
28
+ awk -F= -v key="$key" '$1==key { value=substr($0, index($0, "=") + 1); found=1 } END { if (found) print value }' "$CONFIG_FILE"
29
+ }
30
+
31
+ canonical_compare_path() {
32
+ local path="$1" probe suffix="" base
33
+ while [ "$path" != "/" ] && [ "${path%/}" != "$path" ]; do path="${path%/}"; done
34
+ probe="$path"
35
+ while [ ! -d "$probe" ] && [ "$probe" != "/" ]; do
36
+ base="$(basename "$probe")"
37
+ suffix="/$base$suffix"
38
+ probe="$(dirname "$probe")"
39
+ done
40
+ printf '%s%s\n' "$(cd "$probe" 2>/dev/null && pwd -P)" "$suffix"
41
+ }
42
+
43
+ LIVE_WIKI_DIR="${WIKI_PATH:-$(config_value WIKI_PATH)}"
44
+ LIVE_WIKI_DIR="${LIVE_WIKI_DIR:-$HOME/wiki}"
45
+ CONFIG_FETCH_PROJECTION="$(config_value vault_sync.fetch_projection)"
46
+ FETCH_PROJECTION="${WIKI_FETCH_PROJECTION:-$CONFIG_FETCH_PROJECTION}"
47
+ FETCH_PROJECTION_CONFIGURED=0
48
+ if [ -n "$FETCH_PROJECTION" ] && [ "$FETCH_PROJECTION" != "none" ]; then
49
+ WIKI_DIR="$FETCH_PROJECTION"
50
+ FETCH_PROJECTION_CONFIGURED=1
51
+ else
52
+ WIKI_DIR="$LIVE_WIKI_DIR"
53
+ fi
54
+ export WIKI_DIR
25
55
  BRANCH="${WIKI_BRANCH:-main}"
26
56
  STATE_DIR="$(platform_cache_dir)/wiki-fetch"
27
57
  STATE_FILE="$STATE_DIR/last-behind"
@@ -71,6 +101,25 @@ log() {
71
101
  printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"
72
102
  }
73
103
 
104
+ if [ "$FETCH_PROJECTION_CONFIGURED" -eq 1 ]; then
105
+ case "$WIKI_DIR" in
106
+ /*) ;;
107
+ *) log "ERROR: fetch projection path must be absolute: $WIKI_DIR"; exit 0 ;;
108
+ esac
109
+ LIVE_WIKI_IDENTITY="$(canonical_compare_path "$LIVE_WIKI_DIR")"
110
+ FETCH_PROJECTION_IDENTITY="$(canonical_compare_path "$WIKI_DIR")"
111
+ if [ "$LIVE_WIKI_IDENTITY" = "$FETCH_PROJECTION_IDENTITY" ]; then
112
+ log "ERROR: fetch projection must be distinct from live vault: $WIKI_DIR"
113
+ exit 0
114
+ fi
115
+ case "$FETCH_PROJECTION_IDENTITY/" in
116
+ "$LIVE_WIKI_IDENTITY/"*) log "ERROR: fetch projection and live vault must not be nested: $WIKI_DIR"; exit 0 ;;
117
+ esac
118
+ case "$LIVE_WIKI_IDENTITY/" in
119
+ "$FETCH_PROJECTION_IDENTITY/"*) log "ERROR: fetch projection and live vault must not be nested: $WIKI_DIR"; exit 0 ;;
120
+ esac
121
+ fi
122
+
74
123
  handle_existing_handoff() {
75
124
  local blocker reason op identity previous_identity notified_at now before after
76
125
  # Same altitude as managed-write preflight: a review-required whose target_oid
@@ -187,7 +236,11 @@ attempt_pull_on_delta() {
187
236
 
188
237
  # Guard: working tree present.
189
238
  if [ ! -d "$WIKI_DIR/.git" ]; then
190
- log "ERROR: $WIKI_DIR is not a git repo"
239
+ if [ "$FETCH_PROJECTION_CONFIGURED" -eq 1 ]; then
240
+ log "ERROR: configured fetch projection is not a git repo: $WIKI_DIR"
241
+ else
242
+ log "ERROR: $WIKI_DIR is not a git repo"
243
+ fi
191
244
  exit 0
192
245
  fi
193
246
 
@@ -69,6 +69,14 @@ elif [ -f "$SCRIPT_DIR/../../scripts/lib/runtime-manifest.sh" ]; then
69
69
  source "$SCRIPT_DIR/../../scripts/lib/runtime-manifest.sh"
70
70
  fi
71
71
 
72
+ if [ -f "$SCRIPT_DIR/lib/git-promotion-policy.sh" ]; then
73
+ source "$SCRIPT_DIR/lib/git-promotion-policy.sh"
74
+ elif [ -f "$SCRIPT_DIR/scripts/lib/git-promotion-policy.sh" ]; then
75
+ source "$SCRIPT_DIR/scripts/lib/git-promotion-policy.sh"
76
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/git-promotion-policy.sh" ]; then
77
+ source "$SCRIPT_DIR/../../scripts/lib/git-promotion-policy.sh"
78
+ fi
79
+
72
80
  if ! command -v vault_sync_scan_conflict_markers >/dev/null 2>&1; then
73
81
  echo "[wiki-snapshot] ERROR: conflict-marker helper unavailable; refusing to run." >&2
74
82
  exit 1
@@ -77,6 +85,10 @@ if ! command -v vault_sync_sha256 >/dev/null 2>&1; then
77
85
  echo "[wiki-snapshot] ERROR: SHA-256 helper unavailable; refusing to run." >&2
78
86
  exit 1
79
87
  fi
88
+ if ! command -v snapshot_non_promotable_path >/dev/null 2>&1; then
89
+ echo "[wiki-snapshot] ERROR: Git promotion policy helper unavailable; refusing to run." >&2
90
+ exit 1
91
+ fi
80
92
 
81
93
  # ── Guard: Linux-only operation ────────────────────────────
82
94
  platform_require linux
@@ -101,42 +113,43 @@ REPAIR_SCRIPT="${WIKI_GIT_REPAIR_SCRIPT:-$SCRIPT_DIR/wiki-git-repair-v3.sh}"
101
113
  MAX_S3_ONLY_NOTES="${WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES:-200}"
102
114
  MAX_TOMBSTONE_PRUNES="${WIKI_SNAPSHOT_MAX_TOMBSTONE_PRUNES:-10}"
103
115
 
104
- # Classified inventory: event ledger + local scratch are not GitHub-promotable notes.
105
- # Cap and rclone must share this class. Do not raise MAX_S3_ONLY_NOTES instead.
106
- # meta/log-events stay S3-authoritative; snapshot copies them separately for --events-from.
107
- snapshot_non_promotable_path() {
108
- local p="${1#./}"
109
- case "$p" in
110
- .skillwiki/*|.claude/*|.obsidian/*|.antigravitycli/*|.playwright-cli/*|.superpowers/*|.snapshots/*|.git/*|.drafts/*)
111
- return 0 ;;
112
- tmp/*|logs|logs/*|meta/log-events|meta/log-events/*)
113
- return 0 ;;
114
- raw/._.DS_Store|._.DS_Store)
115
- return 0 ;;
116
- ._*)
117
- return 0 ;;
118
- .conflict*|*.conflict-*)
119
- return 0 ;;
120
- esac
121
- return 1
116
+ # rclone leaves non-promotable and ignored files in the worktree. Passing the
117
+ # ignored directory names as negative pathspecs still makes `git add` exit 1,
118
+ # so build an exact NUL-delimited inventory of tracked/untracked/deleted paths
119
+ # and retain only the Git-promotion class. Already-tracked ledger history is
120
+ # left as-is until the attended branch-tip cleanup stages its deletions.
121
+ snapshot_promotable_pathspecs() {
122
+ local rel_path
123
+ while IFS= read -r -d '' rel_path; do
124
+ snapshot_non_promotable_path "$rel_path" || printf '%s\0' "$rel_path"
125
+ done < <(git ls-files --cached --others --deleted --exclude-standard -z)
122
126
  }
123
127
 
124
- # rclone exclude leaves leftover non-promotable files in the worktree.
125
- # `git add -A` would promote them (live 0.10.90 leak). Pathspecs match
126
- # snapshot_non_promotable_path. Already-tracked ledger history is left as-is.
127
128
  snapshot_git_add_promotable() {
128
- git add --all -- . \
129
- ':!.skillwiki' \
130
- ':!.claude' \
131
- ':!.obsidian' \
132
- ':!.antigravitycli' \
133
- ':!.playwright-cli' \
134
- ':!.superpowers' \
135
- ':!.snapshots' \
136
- ':!.drafts' \
137
- ':!tmp' \
138
- ':!logs' \
139
- ':!meta/log-events'
129
+ snapshot_promotable_pathspecs \
130
+ | git add --all --pathspec-from-file=- --pathspec-file-nul
131
+ }
132
+
133
+ snapshot_ledger_free_git_required() {
134
+ case "$(printf '%s' "${WIKI_SNAPSHOT_REQUIRE_LEDGER_FREE_GIT:-0}" | tr '[:upper:]' '[:lower:]')" in
135
+ 1|true|yes|on) return 0 ;;
136
+ *) return 1 ;;
137
+ esac
138
+ }
139
+
140
+ snapshot_assert_ledger_free_git() {
141
+ local context="${1:-snapshot gate}"
142
+
143
+ snapshot_ledger_free_git_required || return 0
144
+ if git -C "$SNAPSHOT_WORKTREE" ls-files -- meta/log-events 2>/dev/null | grep -q .; then
145
+ log "ERROR: ledger-free Git gate failed ($context); meta/log-events remains in index or HEAD"
146
+ return 1
147
+ fi
148
+ if git -C "$SNAPSHOT_WORKTREE" ls-tree -r --name-only HEAD -- meta/log-events 2>/dev/null | grep -q .; then
149
+ log "ERROR: ledger-free Git gate failed ($context); meta/log-events remains in index or HEAD"
150
+ return 1
151
+ fi
152
+ return 0
140
153
  }
141
154
 
142
155
  PROJECTION_PARITY_TIMEOUT_SECONDS="${WIKI_SNAPSHOT_PROJECTION_PARITY_TIMEOUT_SECONDS:-120}"
@@ -906,6 +919,9 @@ if [ "$(git -C "$SNAPSHOT_WORKTREE" rev-parse --is-inside-work-tree 2>/dev/null)
906
919
  fi
907
920
 
908
921
  refresh_git_baseline
922
+ if ! snapshot_assert_ledger_free_git "after baseline refresh"; then
923
+ exit 1
924
+ fi
909
925
 
910
926
  # Dual-path requires distinct live mutation vs Git convergence roots.
911
927
  # Same-path would materialize projections then immediately rclone-overwrite them.
@@ -1263,6 +1279,9 @@ if [ "$needs_repair" = true ]; then
1263
1279
  log "ERROR: could not refresh snapshot transaction receipt after repair"
1264
1280
  exit 1
1265
1281
  fi
1282
+ if ! snapshot_assert_ledger_free_git "after initial repair"; then
1283
+ exit 1
1284
+ fi
1266
1285
  fi
1267
1286
 
1268
1287
  if ! raw_dedup_guard; then
@@ -1276,6 +1295,9 @@ fi
1276
1295
  # Check for changes
1277
1296
  if [ -z "$(git status --porcelain)" ]; then
1278
1297
  log "No changes to commit"
1298
+ if ! snapshot_assert_ledger_free_git "before no-change completion"; then
1299
+ exit 1
1300
+ fi
1279
1301
  if ! snapshot_verify_git_receipt; then
1280
1302
  log "ERROR: snapshot transaction changed before no-change proof"
1281
1303
  exit 1
@@ -1353,12 +1375,19 @@ if [ "$PULL_SUCCESS" = false ]; then
1353
1375
  fi
1354
1376
  fi
1355
1377
 
1378
+ if ! snapshot_assert_ledger_free_git "after pull/rebase/repair"; then
1379
+ exit 1
1380
+ fi
1381
+
1356
1382
  # Push (with retry)
1357
1383
  echo "Pushing to origin..."
1358
1384
  PUSH_RETRIES=3
1359
1385
  PUSH_SUCCESS=false
1360
1386
 
1361
1387
  for i in $(seq 1 $PUSH_RETRIES); do
1388
+ if ! snapshot_assert_ledger_free_git "immediately before push attempt $i"; then
1389
+ exit 1
1390
+ fi
1362
1391
  if git push origin "$DEFAULT_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
1363
1392
  PUSH_SUCCESS=true
1364
1393
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.91",
3
+ "version": "0.10.93",
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.91",
3
+ "version": "0.10.93",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 21 prompt-only skills (wiki-*, proj-*, using-skillwiki, skillwiki-mcp) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.91",
3
+ "version": "0.10.93",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 21 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",
3
- "version": "0.10.91",
3
+ "version": "0.10.93",
4
4
  "description": "Project-aware Karpathy-style knowledge base: 21 prompt-only skills (wiki-*, proj-*, using-skillwiki, skillwiki-mcp) backed by HTTP MCP wiki_capture.",
5
5
  "author": {
6
6
  "name": "karlorz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.91",
3
+ "version": "0.10.93",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- runSourcesPending
4
- } from "./chunk-ECOEJABP.js";
5
- import "./chunk-QFRZDTJY.js";
6
- import "./chunk-XJR3XXP5.js";
7
- export {
8
- runSourcesPending
9
- };