skillwiki 0.10.60 → 0.10.62

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.
@@ -17,6 +17,7 @@ import {
17
17
  buildWikilinkAdjacency,
18
18
  communityCohesion,
19
19
  git,
20
+ hasUnmergedPaths,
20
21
  isValidWikiProfileKey,
21
22
  listReviewRequiredOps,
22
23
  loadFleetManifestAndHost,
@@ -39,7 +40,7 @@ import {
39
40
  snapshotterAliasForLocalHost,
40
41
  toUndirectedWeighted,
41
42
  writeDotenv
42
- } from "./chunk-CUSHQB5N.js";
43
+ } from "./chunk-KZZUTQEA.js";
43
44
  import {
44
45
  atomicWriteText,
45
46
  prepareTypedPage
@@ -2909,6 +2910,104 @@ function checkPushAgeFromLog(logDir, logFile) {
2909
2910
  );
2910
2911
  }
2911
2912
  }
2913
+ var SNAPSHOT_RUN_HEADER = /=== Wiki Snapshot:/;
2914
+ var SNAPSHOT_SUCCESS_RECORD = "SNAPSHOT_COMPLETE schema=v1";
2915
+ var SNAPSHOT_INHIBITED_RECORD = "SNAPSHOT_INHIBITED schema=v1";
2916
+ var SNAPSHOT_TIMESTAMPED_FAIL = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} FAIL\b/;
2917
+ var SNAPSHOT_LOG_STAMP = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
2918
+ function isSnapshotSuccessLine(line) {
2919
+ return line.includes(SNAPSHOT_SUCCESS_RECORD);
2920
+ }
2921
+ function isSnapshotFailureLine(line) {
2922
+ if (line.includes(SNAPSHOT_INHIBITED_RECORD)) return true;
2923
+ if (line.includes("PREFLIGHT_FAILED")) return true;
2924
+ if (SNAPSHOT_TIMESTAMPED_FAIL.test(line)) return true;
2925
+ if (line.includes("ERROR")) return true;
2926
+ return false;
2927
+ }
2928
+ function parseSnapshotFailureMeta(lines) {
2929
+ let stamp = "";
2930
+ let reason = "";
2931
+ let operationId = "";
2932
+ for (let i = lines.length - 1; i >= 0; i--) {
2933
+ const line = lines[i];
2934
+ if (!stamp) {
2935
+ const m = line.match(SNAPSHOT_LOG_STAMP);
2936
+ if (m) stamp = m[1];
2937
+ }
2938
+ const trimmed = line.trim();
2939
+ if (trimmed.startsWith("{") && trimmed.includes("PREFLIGHT_FAILED")) {
2940
+ try {
2941
+ const parsed = JSON.parse(trimmed);
2942
+ const detail = parsed.detail;
2943
+ if (detail && typeof detail === "object") {
2944
+ if (!reason && typeof detail.reason === "string") reason = detail.reason;
2945
+ if (!operationId && typeof detail.operation_id === "string") operationId = detail.operation_id;
2946
+ }
2947
+ } catch {
2948
+ }
2949
+ }
2950
+ if (!reason) {
2951
+ const m = line.match(/\breason=(\S+)/) ?? line.match(/"reason"\s*:\s*"([^"]+)"/);
2952
+ if (m) reason = m[1];
2953
+ }
2954
+ if (!operationId) {
2955
+ const m = line.match(/\boperation_id=(\S+)/) ?? line.match(/\bopid=(\S+)/) ?? line.match(/"operation_id"\s*:\s*"([^"]+)"/);
2956
+ if (m) operationId = m[1];
2957
+ }
2958
+ }
2959
+ return { stamp, reason, operationId };
2960
+ }
2961
+ function splitSnapshotRuns(lines) {
2962
+ const runs = [];
2963
+ let current = null;
2964
+ for (const line of lines) {
2965
+ if (SNAPSHOT_RUN_HEADER.test(line)) {
2966
+ if (current) runs.push(current);
2967
+ current = { lines: [line], hasHeader: true };
2968
+ } else if (current?.hasHeader) {
2969
+ current.lines.push(line);
2970
+ } else if (isSnapshotFailureLine(line) || isSnapshotSuccessLine(line)) {
2971
+ if (current) {
2972
+ runs.push(current);
2973
+ current = null;
2974
+ }
2975
+ runs.push({ lines: [line], hasHeader: false });
2976
+ }
2977
+ }
2978
+ if (current) runs.push(current);
2979
+ return runs;
2980
+ }
2981
+ function countConsecutiveSnapshotFailures(logRecords) {
2982
+ const window = logRecords.length > 60 ? logRecords.slice(-60) : logRecords;
2983
+ const runs = splitSnapshotRuns(window);
2984
+ let count = 0;
2985
+ let mostRecentFail = "";
2986
+ let reason = "";
2987
+ let operationId = "";
2988
+ for (let i = runs.length - 1; i >= 0; i--) {
2989
+ const lines = runs[i].lines;
2990
+ if (lines.some(isSnapshotSuccessLine)) break;
2991
+ if (!lines.some(isSnapshotFailureLine)) continue;
2992
+ count++;
2993
+ if (count === 1) {
2994
+ const meta = parseSnapshotFailureMeta(lines);
2995
+ mostRecentFail = meta.stamp;
2996
+ reason = meta.reason;
2997
+ operationId = meta.operationId;
2998
+ }
2999
+ }
3000
+ return { count, mostRecentFail, reason, operationId };
3001
+ }
3002
+ function consecutiveSnapshotFailureDetail(result) {
3003
+ if (result.count >= 2) {
3004
+ let detail = `${result.count} consecutive snapshot failure(s); most recent: ${result.mostRecentFail || "unknown"}`;
3005
+ if (result.reason) detail += `; reason=${result.reason}`;
3006
+ if (result.operationId) detail += ` operation_id=${result.operationId}`;
3007
+ return detail;
3008
+ }
3009
+ return `${result.count} consecutive failure(s) in recent window (recurrence threshold: 2)`;
3010
+ }
2912
3011
  function snapshotterHealthChecks(scope, logDir, env) {
2913
3012
  const fixture = loadSnapshotFixture(env);
2914
3013
  const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
@@ -2995,20 +3094,8 @@ function snapshotterHealthChecks(scope, logDir, env) {
2995
3094
  if (serviceResult.status === "error" && sActive !== "active" && sActive !== "activating") {
2996
3095
  freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `latest service result failed: ${serviceResult.detail}`);
2997
3096
  }
2998
- let failCount = 0;
2999
- let mostRecentFail = "";
3000
- for (let i = logRecords.length - 1; i >= 0 && i >= logRecords.length - 60; i--) {
3001
- const line = logRecords[i];
3002
- if (/SNAPSHOT_COMPLETE schema=v1/.test(line)) break;
3003
- if (/ERROR/.test(line)) {
3004
- failCount++;
3005
- if (!mostRecentFail) {
3006
- const m = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
3007
- mostRecentFail = m ? m[1] : "unknown";
3008
- }
3009
- }
3010
- }
3011
- const consecutiveFailures = failCount >= 2 ? check("error", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", `${failCount} consecutive snapshot failure(s); most recent: ${mostRecentFail || "unknown"}`) : check("pass", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", `${failCount} consecutive failure(s) in recent window (recurrence threshold: 2)`);
3097
+ const failureStreak = countConsecutiveSnapshotFailures(logRecords);
3098
+ const consecutiveFailures = failureStreak.count >= 2 ? check("error", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", consecutiveSnapshotFailureDetail(failureStreak)) : check("pass", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", consecutiveSnapshotFailureDetail(failureStreak));
3012
3099
  return [jobs, serviceResult, freshness, consecutiveFailures];
3013
3100
  }
3014
3101
  function vaultSyncChecks(input) {
@@ -3336,26 +3423,71 @@ function checkVaultSyncPullHelper(home, env) {
3336
3423
  "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
3337
3424
  );
3338
3425
  }
3339
- function checkVaultSyncReviewRequiredJournals(vaultPath) {
3340
- if (!vaultPath || !existsSync12(join15(vaultPath, ".git"))) {
3426
+ function isGitDir(path) {
3427
+ return Boolean(path && existsSync12(join15(path, ".git")));
3428
+ }
3429
+ function checkVaultSyncReviewRequiredJournals(ctx) {
3430
+ const live = ctx.resolvedPath;
3431
+ let checked = live;
3432
+ if (!isGitDir(live) && ctx.vsConfig.role === "snapshotter") {
3433
+ const worktree = resolveConfiguredSnapshotWorktree(ctx.input.home);
3434
+ if (!worktree) {
3435
+ return check(
3436
+ "warn",
3437
+ "vault_sync_review_required_journals",
3438
+ "Review-required journals",
3439
+ "snapshotter has no configured snapshot worktree \u2014 journals not inspected"
3440
+ );
3441
+ }
3442
+ checked = worktree;
3443
+ }
3444
+ if (!isGitDir(checked)) {
3341
3445
  return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
3342
3446
  }
3343
3447
  try {
3344
- const ops = listReviewRequiredOps(vaultPath);
3448
+ const ops = listReviewRequiredOps(checked);
3345
3449
  if (ops.length === 0) {
3346
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
3450
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", `None at ${checked}`);
3347
3451
  }
3348
3452
  const sample = ops[0]?.opId ?? "?";
3349
3453
  return check(
3350
3454
  "warn",
3351
3455
  "vault_sync_review_required_journals",
3352
3456
  "Review-required journals",
3353
- `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
3457
+ `${ops.length} handoff(s) at ${checked}; oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
3354
3458
  );
3355
3459
  } catch {
3356
3460
  return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
3357
3461
  }
3358
3462
  }
3463
+ function checkSnapshotWorktreeUnmerged(ctx) {
3464
+ const id = "vault_sync_snapshot_worktree_unmerged";
3465
+ const label = "Vault sync snapshot worktree unmerged paths";
3466
+ if (ctx.vsConfig.role !== "snapshotter") {
3467
+ return check("pass", id, label, "Not a snapshotter host \u2014 check skipped");
3468
+ }
3469
+ const worktree = resolveConfiguredSnapshotWorktree(ctx.input.home);
3470
+ if (!worktree) {
3471
+ return check(
3472
+ "warn",
3473
+ id,
3474
+ label,
3475
+ "no configured snapshot worktree (vault_sync.snapshot_worktree or snapshot_profile required)"
3476
+ );
3477
+ }
3478
+ if (!isGitDir(worktree)) {
3479
+ return check("warn", id, label, `configured snapshot worktree is not a git repo: ${worktree}`);
3480
+ }
3481
+ try {
3482
+ const paths = hasUnmergedPaths(worktree);
3483
+ if (paths.length === 0) {
3484
+ return check("pass", id, label, `none at ${worktree}`);
3485
+ }
3486
+ return check("error", id, label, `unmerged paths at ${worktree}: ${paths.join(", ")}`);
3487
+ } catch {
3488
+ return check("warn", id, label, `could not inspect unmerged paths at ${worktree}`);
3489
+ }
3490
+ }
3359
3491
  var vaultSyncProbe = {
3360
3492
  id: "vault_sync",
3361
3493
  run(ctx) {
@@ -3369,7 +3501,8 @@ var vaultSyncProbe = {
3369
3501
  env: ctx.input.env ?? process.env
3370
3502
  }));
3371
3503
  checks.push(checkVaultSyncPullHelper(ctx.input.home, ctx.input.env ?? process.env));
3372
- checks.push(checkVaultSyncReviewRequiredJournals(ctx.resolvedPath));
3504
+ checks.push(checkVaultSyncReviewRequiredJournals(ctx));
3505
+ checks.push(checkSnapshotWorktreeUnmerged(ctx));
3373
3506
  return checks;
3374
3507
  }
3375
3508
  };
@@ -3417,6 +3417,8 @@ function buildCliSurface() {
3417
3417
  const snapshotMaintenanceCmd = program.commands.find((c) => c.name() === "snapshot-maintenance");
3418
3418
  const snapMaintJournalCmd = snapshotMaintenanceCmd.command("journal");
3419
3419
  snapMaintJournalCmd.command("clear-stale").option("--dry-run").option("--approve <id>").option("--reason <text>").option("--wiki <name>");
3420
+ const snapMaintProjectionCmd = snapshotMaintenanceCmd.command("projection-conflict");
3421
+ snapMaintProjectionCmd.command("repair").option("--dry-run").option("--approve <id>").option("--reason <text>").option("--wiki <name>");
3420
3422
  const memoryCmd = program.commands.find((c) => c.name() === "memory");
3421
3423
  memoryCmd.command("topics").option("--project <slug>").option("--limit <n>").option("--wiki <name>");
3422
3424
  memoryCmd.command("index").requiredOption("--project <slug>").option("--check").option("--if-stale").option("--wiki <name>");
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  snapshotterHealthChecks,
51
51
  upsertIndexEntry,
52
52
  vectorIndexStatus
53
- } from "./chunk-NTM3WRAV.js";
53
+ } from "./chunk-EK7SNIU3.js";
54
54
  import {
55
55
  normalizeDistTag,
56
56
  readCache,
@@ -151,7 +151,7 @@ import {
151
151
  supersedeStaleReviewRequiredJournals,
152
152
  taxonomyCommentForPage,
153
153
  writeDotenv
154
- } from "./chunk-CUSHQB5N.js";
154
+ } from "./chunk-KZZUTQEA.js";
155
155
  import {
156
156
  assertTargetInsideVault,
157
157
  atomicWriteText,
@@ -8457,13 +8457,15 @@ function runSyncJournalClearStale(input) {
8457
8457
  }
8458
8458
 
8459
8459
  // src/commands/snapshot-maintenance.ts
8460
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, appendFileSync } from "fs";
8460
+ import { existsSync as existsSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, appendFileSync } from "fs";
8461
8461
  import { createHash as createHash5 } from "crypto";
8462
8462
  import { execSync as execSync3, spawn } from "child_process";
8463
8463
  import { platform as platform2 } from "os";
8464
8464
  import { join as join34, resolve as resolvePath } from "path";
8465
8465
  var MAINTENANCE_SCHEMA_VERSION = 1;
8466
8466
  var MAINTENANCE_COMMAND = "snapshot-maintenance journal clear-stale";
8467
+ var REPAIR_COMMAND = "snapshot-maintenance projection-conflict repair";
8468
+ var PROJECTION_CONFLICT_ALLOWLIST = ["index.md", "log.md"];
8467
8469
  function canonicalize(p) {
8468
8470
  return resolvePath(p);
8469
8471
  }
@@ -8617,11 +8619,11 @@ function defaultAuditSink(home) {
8617
8619
  }
8618
8620
  };
8619
8621
  }
8620
- function makeAuditEvent(input, hostId, now, result, errorCode, approvalId) {
8622
+ function makeAuditEvent(input, hostId, now, result, errorCode, approvalId, command = MAINTENANCE_COMMAND) {
8621
8623
  return {
8622
8624
  ts: new Date(now).toISOString(),
8623
8625
  schema_version: MAINTENANCE_SCHEMA_VERSION,
8624
- command: MAINTENANCE_COMMAND,
8626
+ command,
8625
8627
  host: hostId,
8626
8628
  actor: input.env?.USER ?? process.env.USER ?? "unknown",
8627
8629
  session: input.sessionId ?? "unknown",
@@ -8740,6 +8742,244 @@ async function performSupersession(input, plan, audit, now) {
8740
8742
  })
8741
8743
  };
8742
8744
  }
8745
+ function computeRepairApprovalId(plan) {
8746
+ const paths = [...plan.unmerged_paths].sort().join(",");
8747
+ const payload = [
8748
+ `v${plan.schema_version}`,
8749
+ plan.command,
8750
+ plan.host_id,
8751
+ plan.snapshot_worktree,
8752
+ plan.git_directory,
8753
+ plan.branch,
8754
+ plan.head_oid,
8755
+ paths,
8756
+ normalizeReason(plan.operator_reason)
8757
+ ].join("|");
8758
+ return "smap1-" + createHash5("sha256").update(payload).digest("hex").slice(0, 32);
8759
+ }
8760
+ function nonMergeSequencerActive(repo) {
8761
+ const gitDir = git(repo, ["rev-parse", "--absolute-git-dir"]);
8762
+ if (!gitDir) return false;
8763
+ for (const m of ["CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
8764
+ if (existsSync10(join34(gitDir, m))) return true;
8765
+ }
8766
+ return existsSync10(join34(gitDir, "rebase-merge")) || existsSync10(join34(gitDir, "rebase-apply"));
8767
+ }
8768
+ function extraUnmergedPaths(unmerged) {
8769
+ const allow = new Set(PROJECTION_CONFLICT_ALLOWLIST);
8770
+ return unmerged.filter((p) => !allow.has(p));
8771
+ }
8772
+ async function authorizeRepairContext(input) {
8773
+ const env = input.env ?? process.env;
8774
+ const home = input.home ?? env.HOME ?? "";
8775
+ const audit = input.auditSink ?? defaultAuditSink(home);
8776
+ const now = input.now ?? Date.now();
8777
+ const liveVaultPath = input.liveVaultPath ? canonicalize(input.liveVaultPath) : await resolveLiveVault({ env, home });
8778
+ const fleetLoad = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
8779
+ vault: liveVaultPath,
8780
+ env,
8781
+ home,
8782
+ cwd: process.cwd(),
8783
+ osHostname: env.HOSTNAME,
8784
+ user: env.USER
8785
+ });
8786
+ if (!fleetLoad || !fleetLoad.hostId || fleetLoad.identityStatus !== "known") {
8787
+ audit(makeAuditEvent(input, fleetLoad?.hostId ?? "unknown", now, "refusal", "MAINTENANCE_UNKNOWN_IDENTITY", void 0, REPAIR_COMMAND));
8788
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNKNOWN_IDENTITY", "unknown or unresolved fleet identity; cannot authorize snapshot maintenance") };
8789
+ }
8790
+ const host = fleetLoad.manifest.hosts[fleetLoad.hostId];
8791
+ if (!host || host.role !== "snapshotter" || host.protected !== true) {
8792
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER", void 0, REPAIR_COMMAND));
8793
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER", `host '${fleetLoad.hostId}' is not a protected snapshotter (role=${host?.role ?? "missing"}, protected=${host?.protected ?? false})`) };
8794
+ }
8795
+ const configuredWorktree = resolveConfiguredSnapshotWorktree(home);
8796
+ if (!configuredWorktree) {
8797
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NO_CONFIGURED_WORKTREE", void 0, REPAIR_COMMAND));
8798
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_CONFIGURED_WORKTREE", "no configured snapshot worktree (vault_sync.snapshot_worktree or snapshot_profile required)") };
8799
+ }
8800
+ const requested = canonicalize(input.snapshotWorktree);
8801
+ if (requested !== canonicalize(configuredWorktree)) {
8802
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_WRONG_WORKTREE", void 0, REPAIR_COMMAND));
8803
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_WRONG_WORKTREE", `requested path '${requested}' is not the configured snapshot worktree '${canonicalize(configuredWorktree)}'`) };
8804
+ }
8805
+ if (!existsSync10(requested) || !existsSync10(join34(requested, ".git"))) {
8806
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_MISSING_GIT_REPO", void 0, REPAIR_COMMAND));
8807
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_MISSING_GIT_REPO", `snapshot worktree is not a git repository: ${requested}`) };
8808
+ }
8809
+ if (liveVaultPath && requested === canonicalize(liveVaultPath)) {
8810
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_LIVE_VAULT_TARGET", void 0, REPAIR_COMMAND));
8811
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_LIVE_VAULT_TARGET", "requested path is the live vault, not the snapshot worktree") };
8812
+ }
8813
+ if (nonMergeSequencerActive(requested)) {
8814
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_ACTIVE_SEQUENCER", void 0, REPAIR_COMMAND));
8815
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_ACTIVE_SEQUENCER", "git sequencer (rebase/cherry-pick/revert) is active") };
8816
+ }
8817
+ const unmerged = hasUnmergedPaths(requested);
8818
+ const extra = extraUnmergedPaths(unmerged);
8819
+ if (extra.length > 0) {
8820
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_UNMERGED_PATHS", void 0, REPAIR_COMMAND));
8821
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNMERGED_PATHS", `unmerged paths not in projection allowlist: ${extra.join(", ")}`) };
8822
+ }
8823
+ if (unmerged.length === 0) {
8824
+ audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NO_PROJECTION_CONFLICT", void 0, REPAIR_COMMAND));
8825
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_PROJECTION_CONFLICT", "no unmerged projection paths") };
8826
+ }
8827
+ return {
8828
+ hostId: fleetLoad.hostId,
8829
+ requested,
8830
+ gitDirectory: git(requested, ["rev-parse", "--absolute-git-dir"]) || "",
8831
+ branch: git(requested, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD",
8832
+ headOid: git(requested, ["rev-parse", "HEAD"]) || "",
8833
+ unmerged
8834
+ };
8835
+ }
8836
+ async function runProjectionConflictRepairDryRun(input) {
8837
+ const env = input.env ?? process.env;
8838
+ const home = input.home ?? env.HOME ?? "";
8839
+ const audit = input.auditSink ?? defaultAuditSink(home);
8840
+ const now = input.now ?? Date.now();
8841
+ const ctx = await authorizeRepairContext(input);
8842
+ if ("exitCode" in ctx) return ctx;
8843
+ const planBase = {
8844
+ schema_version: MAINTENANCE_SCHEMA_VERSION,
8845
+ command: REPAIR_COMMAND,
8846
+ host_id: ctx.hostId,
8847
+ snapshot_worktree: ctx.requested,
8848
+ git_directory: ctx.gitDirectory,
8849
+ branch: ctx.branch,
8850
+ head_oid: ctx.headOid,
8851
+ unmerged_paths: ctx.unmerged,
8852
+ allowlisted_paths: [...PROJECTION_CONFLICT_ALLOWLIST],
8853
+ operator_reason: normalizeReason(input.reason ?? "")
8854
+ };
8855
+ const approvalId = computeRepairApprovalId(planBase);
8856
+ const plan = { ...planBase, approval_id: approvalId };
8857
+ audit(makeAuditEvent(input, ctx.hostId, now, "dry-run", void 0, approvalId, REPAIR_COMMAND));
8858
+ return {
8859
+ exitCode: ExitCode.OK,
8860
+ result: ok({
8861
+ dry_run: true,
8862
+ plan: {
8863
+ schema_version: plan.schema_version,
8864
+ command: plan.command,
8865
+ host_id: plan.host_id,
8866
+ role: "snapshotter",
8867
+ protected: true,
8868
+ snapshot_worktree: plan.snapshot_worktree,
8869
+ snapshot_lock_path: input.snapshotLockPath ?? "/var/lock/wiki-snapshot.lock",
8870
+ git_directory: plan.git_directory,
8871
+ branch: plan.branch,
8872
+ head_oid: plan.head_oid,
8873
+ worktree_clean: false,
8874
+ active_sequencer: false,
8875
+ unmerged_paths: plan.unmerged_paths,
8876
+ eligible_journals: [],
8877
+ skipped_journals: [],
8878
+ approval_id: approvalId,
8879
+ operator_reason: plan.operator_reason
8880
+ },
8881
+ humanHint: `dry-run: rematerialize ${plan.unmerged_paths.join(", ")}; approval_id=${approvalId}`
8882
+ })
8883
+ };
8884
+ }
8885
+ async function rematerializeProjections(input, worktree) {
8886
+ if (input.renderIndex && input.renderLog) {
8887
+ const index = await input.renderIndex(worktree);
8888
+ if (!index.ok) return index;
8889
+ const log = await input.renderLog(worktree);
8890
+ if (!log.ok) return log;
8891
+ return ok({ index: index.data.text, log: log.data.text });
8892
+ }
8893
+ const indexProj = await renderRootIndex({ vault: worktree });
8894
+ if (!indexProj.ok) return indexProj;
8895
+ const events = await readLogEvents(worktree);
8896
+ if (!events.ok) return events;
8897
+ return ok({ index: indexProj.data.text, log: renderLogProjection(events.data) });
8898
+ }
8899
+ async function performProjectionRepair(input, plan, audit, now) {
8900
+ const rendered = await rematerializeProjections(input, plan.snapshot_worktree);
8901
+ if (!rendered.ok) {
8902
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_REMATERIALIZE_FAILED", plan.approval_id ?? void 0, REPAIR_COMMAND));
8903
+ return { exitCode: ExitCode.WRITE_FAILED, result: rendered };
8904
+ }
8905
+ try {
8906
+ writeFileSync6(join34(plan.snapshot_worktree, "index.md"), rendered.data.index);
8907
+ writeFileSync6(join34(plan.snapshot_worktree, "log.md"), rendered.data.log);
8908
+ gitStrict(plan.snapshot_worktree, ["add", "--", "index.md", "log.md"]);
8909
+ gitStrict(plan.snapshot_worktree, [
8910
+ "commit",
8911
+ "-m",
8912
+ "snapshot-maintenance: rematerialize projection conflict"
8913
+ ]);
8914
+ } catch (e) {
8915
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_COMMIT_FAILED", plan.approval_id ?? void 0, REPAIR_COMMAND));
8916
+ return { exitCode: ExitCode.WRITE_FAILED, result: refusalErr("MAINTENANCE_COMMIT_FAILED", String(e)) };
8917
+ }
8918
+ const remaining = hasUnmergedPaths(plan.snapshot_worktree);
8919
+ if (remaining.length > 0) {
8920
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_UNMERGED_PATHS", plan.approval_id ?? void 0, REPAIR_COMMAND));
8921
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNMERGED_PATHS", `unmerged paths remain: ${remaining.join(", ")}`) };
8922
+ }
8923
+ const commitOid = git(plan.snapshot_worktree, ["rev-parse", "HEAD"]);
8924
+ audit(makeAuditEvent(input, plan.host_id, now, "success", void 0, plan.approval_id ?? void 0, REPAIR_COMMAND));
8925
+ return {
8926
+ exitCode: ExitCode.OK,
8927
+ result: ok({
8928
+ dry_run: false,
8929
+ execution: {
8930
+ superseded: [],
8931
+ skipped: [],
8932
+ approval_id: plan.approval_id,
8933
+ no_op: false
8934
+ },
8935
+ humanHint: `execution: rematerialized index.md,log.md commit=${commitOid}`
8936
+ })
8937
+ };
8938
+ }
8939
+ async function runProjectionConflictRepairExecute(input) {
8940
+ const env = input.env ?? process.env;
8941
+ const home = input.home ?? env.HOME ?? "";
8942
+ const audit = input.auditSink ?? defaultAuditSink(home);
8943
+ const now = input.now ?? Date.now();
8944
+ const isTty = input.isTty ?? !!process.stdin.isTTY;
8945
+ if (!isTty) {
8946
+ audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_TTY", void 0, REPAIR_COMMAND));
8947
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_TTY", "snapshot maintenance requires an attended TTY") };
8948
+ }
8949
+ const reason = normalizeReason(input.reason ?? "");
8950
+ if (!reason) {
8951
+ audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_REASON", void 0, REPAIR_COMMAND));
8952
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_REASON", "snapshot maintenance requires a non-empty operator reason") };
8953
+ }
8954
+ if (!input.approvalId) {
8955
+ audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_APPROVAL_ID", void 0, REPAIR_COMMAND));
8956
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_APPROVAL_ID", "snapshot maintenance requires an approval ID from a prior dry run") };
8957
+ }
8958
+ const dryRunResult = await runProjectionConflictRepairDryRun(input);
8959
+ if (!dryRunResult.result.ok) return dryRunResult;
8960
+ const plan = dryRunResult.result.data.plan;
8961
+ if (!plan.approval_id) {
8962
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_NO_PROJECTION_CONFLICT", void 0, REPAIR_COMMAND));
8963
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_PROJECTION_CONFLICT", "no unmerged projection paths") };
8964
+ }
8965
+ if (plan.approval_id !== input.approvalId) {
8966
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_STALE_APPROVAL_ID", void 0, REPAIR_COMMAND));
8967
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_STALE_APPROVAL_ID", "approval ID does not match the current state; rerun dry run") };
8968
+ }
8969
+ if (!input.skipFlock) {
8970
+ const flockResult = acquireSnapshotFlock(plan.snapshot_lock_path);
8971
+ if (!flockResult.acquired) {
8972
+ audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_FLOCK_BUSY", void 0, REPAIR_COMMAND));
8973
+ return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_FLOCK_BUSY", `snapshot flock busy: ${plan.snapshot_lock_path}`) };
8974
+ }
8975
+ try {
8976
+ return await performProjectionRepair(input, plan, audit, now);
8977
+ } finally {
8978
+ releaseSnapshotFlock(flockResult);
8979
+ }
8980
+ }
8981
+ return performProjectionRepair(input, plan, audit, now);
8982
+ }
8743
8983
  function acquireSnapshotFlock(lockPath) {
8744
8984
  try {
8745
8985
  const child = spawn("bash", ["-c", `exec 9>"${lockPath}"; flock -n 9 || exit 1; while true; do sleep 3600; done`], {
@@ -8771,7 +9011,7 @@ function releaseSnapshotFlock(handle) {
8771
9011
  }
8772
9012
 
8773
9013
  // src/commands/backup.ts
8774
- import { statSync as statSync3, readdirSync as readdirSync2, readFileSync as readFileSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
9014
+ import { statSync as statSync3, readdirSync as readdirSync2, readFileSync as readFileSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
8775
9015
  import { join as join35, relative as relative3, dirname as dirname11 } from "path";
8776
9016
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
8777
9017
 
@@ -8909,7 +9149,7 @@ async function runBackupRestore(input) {
8909
9149
  const body = await resp.Body?.transformToByteArray();
8910
9150
  if (body) {
8911
9151
  mkdirSync5(dirname11(localPath), { recursive: true });
8912
- writeFileSync6(localPath, Buffer.from(body));
9152
+ writeFileSync7(localPath, Buffer.from(body));
8913
9153
  downloaded++;
8914
9154
  }
8915
9155
  } catch {
@@ -10170,7 +10410,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
10170
10410
  if (dirty) {
10171
10411
  return emit(dirty, void 0, { postCommit: false });
10172
10412
  }
10173
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-Z6MEAGQI.js");
10413
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-GJP4RN26.js");
10174
10414
  const run = await runManagedWriteTransaction2({
10175
10415
  vault,
10176
10416
  command,
@@ -11079,6 +11319,20 @@ snapMaintJournalCmd.command("clear-stale [snapshot-worktree]").description("supe
11079
11319
  emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "provide --dry-run for a plan, or --approve <id> --reason <text> to execute") });
11080
11320
  }
11081
11321
  });
11322
+ var snapMaintProjectionCmd = snapshotMaintenanceCmd.command("projection-conflict").description("attended projection-owned unmerged-path repair");
11323
+ snapMaintProjectionCmd.command("repair [snapshot-worktree]").description("rematerialize allowlisted UU log.md/index.md on a protected snapshotter (attended, one-shot)").option("--dry-run", "non-mutating plan + approval ID (no execution)", false).option("--approve <id>", "state-bound approval ID from a prior --dry-run").option("--reason <text>", "non-empty operator reason for the maintenance").option("--wiki <name>", "wiki profile name").action(async (snapshotWorktree, opts) => {
11324
+ if (!snapshotWorktree) {
11325
+ emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "snapshot-maintenance projection-conflict repair requires a snapshot-worktree path argument") });
11326
+ return;
11327
+ }
11328
+ if (opts.dryRun) {
11329
+ emit(await runProjectionConflictRepairDryRun({ snapshotWorktree, dryRun: true, reason: opts.reason }));
11330
+ } else if (opts.approve) {
11331
+ emit(await runProjectionConflictRepairExecute({ snapshotWorktree, dryRun: false, approvalId: opts.approve, reason: opts.reason }));
11332
+ } else {
11333
+ emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "provide --dry-run for a plan, or --approve <id> --reason <text> to execute") });
11334
+ }
11335
+ });
11082
11336
  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) => {
11083
11337
  const v = await resolveVaultArg(vault, opts.wiki);
11084
11338
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
@@ -6,7 +6,7 @@ import {
6
6
  runManagedWritePeerGate,
7
7
  runManagedWritePreflight,
8
8
  runManagedWriteTransaction
9
- } from "./chunk-CUSHQB5N.js";
9
+ } from "./chunk-KZZUTQEA.js";
10
10
  import "./chunk-NG72ZD4C.js";
11
11
  import "./chunk-6AMXNODT.js";
12
12
  import "./chunk-MHGUYPGE.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-NTM3WRAV.js";
4
+ } from "./chunk-EK7SNIU3.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
  import "./chunk-NHRRYAXT.js";
7
- import "./chunk-CUSHQB5N.js";
7
+ import "./chunk-KZZUTQEA.js";
8
8
  import "./chunk-NG72ZD4C.js";
9
9
  import "./chunk-JMV7YBQN.js";
10
10
  import "./chunk-6AMXNODT.js";
@@ -87,6 +87,15 @@ SNAPSHOT_WORKTREE="${WIKI_GIT_WORKTREE:-${SNAPSHOT_WORKTREE:-/root/wiki-git}}"
87
87
  LOCK_FILE="${WIKI_SNAPSHOT_LOCK:-/var/lock/wiki-snapshot.lock}"
88
88
  DEFAULT_LOG_DIR="$(platform_log_dir)"
89
89
  LOG_FILE="${WIKI_SNAPSHOT_LOG:-$DEFAULT_LOG_DIR/wiki-snapshot.log}"
90
+ INHIBIT_AFTER="${WIKI_SNAPSHOT_INHIBIT_AFTER:-3}"
91
+ INHIBIT_RECHECK_RUNS="${WIKI_SNAPSHOT_INHIBIT_RECHECK_RUNS:-6}"
92
+ INHIBIT_RECHECK_SECONDS="${WIKI_SNAPSHOT_INHIBIT_RECHECK_SECONDS:-21600}"
93
+ INHIBIT_STATE_FILE="${WIKI_SNAPSHOT_INHIBIT_STATE:-$(platform_cache_dir)/wiki-snapshot-inhibit.state}"
94
+ case "$INHIBIT_AFTER" in ''|*[!0-9]*) INHIBIT_AFTER=3 ;; esac
95
+ case "$INHIBIT_RECHECK_RUNS" in ''|*[!0-9]*) INHIBIT_RECHECK_RUNS=6 ;; esac
96
+ case "$INHIBIT_RECHECK_SECONDS" in ''|*[!0-9]*) INHIBIT_RECHECK_SECONDS=21600 ;; esac
97
+ if [ "$INHIBIT_AFTER" -lt 1 ]; then INHIBIT_AFTER=3; fi
98
+ if [ "$INHIBIT_RECHECK_RUNS" -lt 1 ]; then INHIBIT_RECHECK_RUNS=6; fi
90
99
  CLOUD_REMOTE="${CLOUD_REMOTE:-cloud:cloud/wiki}"
91
100
  REPAIR_SCRIPT="${WIKI_GIT_REPAIR_SCRIPT:-$SCRIPT_DIR/wiki-git-repair-v3.sh}"
92
101
  MAX_S3_ONLY_NOTES="${WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES:-200}"
@@ -337,6 +346,89 @@ log() {
337
346
  echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
338
347
  }
339
348
 
349
+ snapshot_inhibit_clear() {
350
+ rm -f "$INHIBIT_STATE_FILE" 2>/dev/null || true
351
+ }
352
+
353
+ snapshot_inhibit_read_field() {
354
+ local key="$1"
355
+ [ -f "$INHIBIT_STATE_FILE" ] || return 0
356
+ sed -n "s/^${key}=//p" "$INHIBIT_STATE_FILE" 2>/dev/null | head -1
357
+ }
358
+
359
+ snapshot_inhibit_write() {
360
+ local opid="$1" count="$2" first_seen="$3" last_full="$4" skips="$5"
361
+ local dir
362
+ dir="$(dirname "$INHIBIT_STATE_FILE")"
363
+ mkdir -p "$dir" 2>/dev/null || return 0
364
+ {
365
+ printf 'opid=%s\n' "$opid"
366
+ printf 'count=%s\n' "$count"
367
+ printf 'first_seen=%s\n' "$first_seen"
368
+ printf 'last_full_epoch=%s\n' "$last_full"
369
+ printf 'skips_since_full=%s\n' "$skips"
370
+ } > "$INHIBIT_STATE_FILE" 2>/dev/null || true
371
+ }
372
+
373
+ snapshot_parse_operation_id() {
374
+ local file="$1"
375
+ [ -f "$file" ] || return 0
376
+ sed -n 's/.*"operation_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$file" 2>/dev/null | head -1
377
+ }
378
+
379
+ snapshot_inhibit_should_skip() {
380
+ local count last_full skips now age
381
+ [ -f "$INHIBIT_STATE_FILE" ] || return 1
382
+ count="$(snapshot_inhibit_read_field count)"
383
+ last_full="$(snapshot_inhibit_read_field last_full_epoch)"
384
+ skips="$(snapshot_inhibit_read_field skips_since_full)"
385
+ case "$count" in ''|*[!0-9]*) count=0 ;; esac
386
+ case "$last_full" in ''|*[!0-9]*) last_full=0 ;; esac
387
+ case "$skips" in ''|*[!0-9]*) skips=0 ;; esac
388
+ if [ "$count" -lt "$INHIBIT_AFTER" ]; then
389
+ return 1
390
+ fi
391
+ now="$(date +%s)"
392
+ age=$((now - last_full))
393
+ if [ "$age" -ge "$INHIBIT_RECHECK_SECONDS" ]; then
394
+ return 1
395
+ fi
396
+ if [ "$skips" -ge "$INHIBIT_RECHECK_RUNS" ]; then
397
+ return 1
398
+ fi
399
+ return 0
400
+ }
401
+
402
+ snapshot_inhibit_mark_skip() {
403
+ local opid count first_seen last_full skips
404
+ opid="$(snapshot_inhibit_read_field opid)"
405
+ count="$(snapshot_inhibit_read_field count)"
406
+ first_seen="$(snapshot_inhibit_read_field first_seen)"
407
+ last_full="$(snapshot_inhibit_read_field last_full_epoch)"
408
+ skips="$(snapshot_inhibit_read_field skips_since_full)"
409
+ case "$skips" in ''|*[!0-9]*) skips=0 ;; esac
410
+ skips=$((skips + 1))
411
+ snapshot_inhibit_write "$opid" "$count" "$first_seen" "$last_full" "$skips"
412
+ }
413
+
414
+ snapshot_inhibit_record_failure() {
415
+ local opid="$1"
416
+ local now count first_seen
417
+ [ -n "$opid" ] || return 0
418
+ now="$(date +%s)"
419
+ if [ -f "$INHIBIT_STATE_FILE" ] && [ "$(snapshot_inhibit_read_field opid)" = "$opid" ]; then
420
+ count="$(snapshot_inhibit_read_field count)"
421
+ first_seen="$(snapshot_inhibit_read_field first_seen)"
422
+ case "$count" in ''|*[!0-9]*) count=0 ;; esac
423
+ count=$((count + 1))
424
+ [ -n "$first_seen" ] || first_seen="$now"
425
+ else
426
+ count=1
427
+ first_seen="$now"
428
+ fi
429
+ snapshot_inhibit_write "$opid" "$count" "$first_seen" "$now" 0
430
+ }
431
+
340
432
  snapshot_projection_hash_file() {
341
433
  local path="${1:-}"
342
434
  [ -f "$path" ] || return 1
@@ -522,6 +614,7 @@ emit_snapshot_complete() {
522
614
  log "ERROR: final snapshot proof failed head=$head_oid origin=$origin_oid"
523
615
  return 1
524
616
  fi
617
+ snapshot_inhibit_clear
525
618
  log "SNAPSHOT_COMPLETE schema=v1 outcome=${outcome} result=success ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) head=${head_oid} origin=${origin_oid}"
526
619
  return 0
527
620
  }
@@ -671,6 +764,12 @@ if ! snapshot_freeze_git_receipt; then
671
764
  exit 1
672
765
  fi
673
766
 
767
+ if snapshot_inhibit_should_skip; then
768
+ snapshot_inhibit_mark_skip
769
+ log "SNAPSHOT_INHIBITED schema=v1 opid=$(snapshot_inhibit_read_field opid) count=$(snapshot_inhibit_read_field count) first_seen=$(snapshot_inhibit_read_field first_seen)"
770
+ exit 1
771
+ fi
772
+
674
773
  # Single-authority root projections before FUSE/S3 pull promotion.
675
774
  # Mutation target is the live vault ($WIKI_DIR); Git pull/base-OID use
676
775
  # $SNAPSHOT_WORKTREE so FUSE/S3 hosts without a local Git HEAD still work.
@@ -691,11 +790,17 @@ if command -v "$SKILLWIKI_BIN" >/dev/null 2>&1; then
691
790
  exit 1
692
791
  ;;
693
792
  esac
793
+ proj_out="$(mktemp)"
694
794
  if ! "$SKILLWIKI_BIN" projections materialize "$WIKI_DIR" --write \
695
- --converge-vault "$SNAPSHOT_WORKTREE" >>"$LOG_FILE" 2>&1; then
795
+ --converge-vault "$SNAPSHOT_WORKTREE" >"$proj_out" 2>&1; then
796
+ cat "$proj_out" >>"$LOG_FILE" 2>/dev/null || true
797
+ snapshot_inhibit_record_failure "$(snapshot_parse_operation_id "$proj_out")"
798
+ rm -f "$proj_out"
696
799
  log "FAIL root projection materialization; snapshot promotion refused"
697
800
  exit 1
698
801
  fi
802
+ cat "$proj_out" >>"$LOG_FILE" 2>/dev/null || true
803
+ rm -f "$proj_out"
699
804
  log "OK projections materialize before snapshot sync"
700
805
  if ! snapshot_freeze_projection_expectations; then
701
806
  log "FAIL projection expectation freeze; snapshot promotion refused"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.60",
3
+ "version": "0.10.62",
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.60",
3
+ "version": "0.10.62",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 20 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.60",
3
+ "version": "0.10.62",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 20 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.60",
3
+ "version": "0.10.62",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",