skillwiki 0.9.56 → 0.9.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  import {
4
3
  cachePath,
5
4
  normalizeDistTag,
@@ -2888,6 +2888,7 @@ function buildCliSurface() {
2888
2888
  syncCmd.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
2889
2889
  syncCmd.command("unlock").option("--force").option("--wiki <name>");
2890
2890
  syncCmd.command("peers").option("--wiki <name>");
2891
+ syncCmd.command("lint-delta").option("--base-ref <ref>").option("--wiki <name>");
2891
2892
  const backupCmd = program.commands.find((c) => c.name() === "backup");
2892
2893
  backupCmd.command("sync").option("--dry-run").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--prune").option("--wiki <name>");
2893
2894
  backupCmd.command("restore").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--target <dir>").option("--wiki <name>");
@@ -4250,6 +4251,159 @@ ${split.data.body}`;
4250
4251
  result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
4251
4252
  };
4252
4253
  }
4254
+ function lintIssueFingerprint(bucket, item) {
4255
+ const page = extractIssuePage(item);
4256
+ const detail = normalizeIssueDetail(item);
4257
+ return `${bucket}\0${page}\0${detail}`;
4258
+ }
4259
+ function extractIssuePage(item) {
4260
+ if (typeof item === "string") {
4261
+ const m = item.match(/^([^:]+?)(?::\s|$)/);
4262
+ return (m?.[1] ?? item).trim();
4263
+ }
4264
+ if (item && typeof item === "object") {
4265
+ const obj = item;
4266
+ for (const key of ["path", "file", "page", "relPath"]) {
4267
+ if (typeof obj[key] === "string") return obj[key];
4268
+ }
4269
+ }
4270
+ return "";
4271
+ }
4272
+ function normalizeIssueDetail(item) {
4273
+ if (typeof item === "string") {
4274
+ return item.replace(/\s+/g, " ").trim();
4275
+ }
4276
+ try {
4277
+ return JSON.stringify(item, Object.keys(item).sort());
4278
+ } catch {
4279
+ return String(item);
4280
+ }
4281
+ }
4282
+ function collectLintErrorFingerprints(output) {
4283
+ const fps = /* @__PURE__ */ new Set();
4284
+ for (const bucket of output.by_severity.error) {
4285
+ for (const item of bucket.items) {
4286
+ fps.add(lintIssueFingerprint(bucket.kind, item));
4287
+ }
4288
+ }
4289
+ return fps;
4290
+ }
4291
+ async function runSyncLintDelta(input) {
4292
+ const { mkdtempSync, rmSync, existsSync: fsExists } = await import("fs");
4293
+ const { join: pathJoin } = await import("path");
4294
+ const { tmpdir } = await import("os");
4295
+ const { execFileSync: execFileSync2 } = await import("child_process");
4296
+ const vault = input.vault;
4297
+ const baseRef = input.baseRef ?? "origin/main";
4298
+ const days = input.days ?? 90;
4299
+ const lines = input.lines ?? 200;
4300
+ const logThreshold = input.logThreshold ?? 500;
4301
+ if (!fsExists(pathJoin(vault, ".git"))) {
4302
+ return {
4303
+ exitCode: ExitCode.VAULT_PATH_INVALID,
4304
+ result: err("NOT_A_GIT_REPO", { path: vault })
4305
+ };
4306
+ }
4307
+ try {
4308
+ execFileSync2("git", ["rev-parse", "--verify", baseRef], {
4309
+ cwd: vault,
4310
+ stdio: ["pipe", "pipe", "pipe"]
4311
+ });
4312
+ } catch {
4313
+ return {
4314
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4315
+ result: err("LINT_DELTA_BASE_UNAVAILABLE", {
4316
+ baseRef,
4317
+ message: `base ref ${baseRef} does not resolve \u2014 fail closed`
4318
+ })
4319
+ };
4320
+ }
4321
+ const fullLint = await runLint({ vault, days, lines, logThreshold });
4322
+ if (!fullLint.result.ok) {
4323
+ return {
4324
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4325
+ result: err("LINT_DELTA_FULL_FAILED", { detail: fullLint.result })
4326
+ };
4327
+ }
4328
+ const fullOutput = fullLint.result.data;
4329
+ if (!("by_severity" in fullOutput) || !fullOutput.by_severity) {
4330
+ return {
4331
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4332
+ result: err("LINT_DELTA_MALFORMED", { message: "full lint missing by_severity" })
4333
+ };
4334
+ }
4335
+ const fullFps = collectLintErrorFingerprints(fullOutput);
4336
+ const tmpRoot = mkdtempSync(pathJoin(tmpdir(), "skillwiki-lint-delta-"));
4337
+ try {
4338
+ const archive = execFileSync2("git", ["archive", "--format=tar", baseRef], {
4339
+ cwd: vault,
4340
+ stdio: ["pipe", "pipe", "pipe"],
4341
+ maxBuffer: 256 * 1024 * 1024
4342
+ });
4343
+ execFileSync2("tar", ["-xf", "-"], {
4344
+ cwd: tmpRoot,
4345
+ input: archive,
4346
+ stdio: ["pipe", "pipe", "pipe"]
4347
+ });
4348
+ if (!fsExists(pathJoin(tmpRoot, "SCHEMA.md"))) {
4349
+ }
4350
+ const baseLint = await runLint({ vault: tmpRoot, days, lines, logThreshold });
4351
+ if (!baseLint.result.ok) {
4352
+ return {
4353
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4354
+ result: err("LINT_DELTA_BASE_LINT_FAILED", {
4355
+ baseRef,
4356
+ detail: baseLint.result
4357
+ })
4358
+ };
4359
+ }
4360
+ const baseOutput = baseLint.result.data;
4361
+ if (!("by_severity" in baseOutput) || !baseOutput.by_severity) {
4362
+ return {
4363
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4364
+ result: err("LINT_DELTA_MALFORMED", { message: "base lint missing by_severity" })
4365
+ };
4366
+ }
4367
+ const baseFps = collectLintErrorFingerprints(baseOutput);
4368
+ const newFps = [];
4369
+ const resolvedFps = [];
4370
+ for (const fp of fullFps) {
4371
+ if (!baseFps.has(fp)) newFps.push(fp);
4372
+ }
4373
+ for (const fp of baseFps) {
4374
+ if (!fullFps.has(fp)) resolvedFps.push(fp);
4375
+ }
4376
+ newFps.sort();
4377
+ resolvedFps.sort();
4378
+ const fullList = [...fullFps].sort();
4379
+ const output = {
4380
+ full_errors: fullFps.size,
4381
+ base_errors: baseFps.size,
4382
+ new_errors: newFps.length,
4383
+ resolved_errors: resolvedFps.length,
4384
+ full_fingerprints: fullList,
4385
+ new_fingerprints: newFps,
4386
+ resolved_fingerprints: resolvedFps,
4387
+ base_ref: baseRef,
4388
+ humanHint: newFps.length > 0 ? `lint delta: ${newFps.length} new error(s) vs ${baseRef} (full=${fullFps.size}, base=${baseFps.size}, resolved=${resolvedFps.length})` : fullFps.size > 0 ? `lint delta: 0 new errors vs ${baseRef}; inherited full_errors=${fullFps.size} (base=${baseFps.size}, resolved=${resolvedFps.length})` : `lint delta: clean (0 errors) vs ${baseRef}`
4389
+ };
4390
+ const exitCode = output.new_errors > 0 ? ExitCode.LINT_HAS_ERRORS : fullLint.exitCode === ExitCode.LINT_HAS_WARNINGS ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
4391
+ return { exitCode, result: ok(output) };
4392
+ } catch (e) {
4393
+ return {
4394
+ exitCode: ExitCode.LINT_HAS_ERRORS,
4395
+ result: err("LINT_DELTA_ARCHIVE_FAILED", {
4396
+ baseRef,
4397
+ message: String(e)
4398
+ })
4399
+ };
4400
+ } finally {
4401
+ try {
4402
+ rmSync(tmpRoot, { recursive: true, force: true });
4403
+ } catch {
4404
+ }
4405
+ }
4406
+ }
4253
4407
 
4254
4408
  // src/commands/config.ts
4255
4409
  import { readFile as readFile13 } from "fs/promises";
@@ -9507,6 +9661,7 @@ export {
9507
9661
  fixPathTooLong,
9508
9662
  assessSourceIdentity,
9509
9663
  runLint,
9664
+ runSyncLintDelta,
9510
9665
  configPath,
9511
9666
  runConfigGet,
9512
9667
  runConfigSet,
package/dist/cli.js CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  runQuery,
67
67
  runSkillwikiMcpStdio,
68
68
  runStale,
69
+ runSyncLintDelta,
69
70
  runTagAudit,
70
71
  runTopicMapCheck,
71
72
  runValidate,
@@ -76,7 +77,7 @@ import {
76
77
  snapshotterAliasForLocalHost,
77
78
  splitFrontmatter,
78
79
  writeDotenv
79
- } from "./chunk-SE5URAJR.js";
80
+ } from "./chunk-NIPZVIHT.js";
80
81
  import {
81
82
  normalizeDistTag,
82
83
  readCache,
@@ -3725,15 +3726,51 @@ async function runSyncPush(input) {
3725
3726
  })
3726
3727
  };
3727
3728
  }
3728
- const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
3729
- if (lintResult.result.ok && lintResult.result.data.summary.errors > 0) {
3730
- return {
3731
- exitCode: ExitCode.LINT_HAS_ERRORS,
3732
- result: err("LINT_ERRORS_BLOCK_PUSH", {
3733
- errors: lintResult.result.data.summary.errors,
3734
- buckets: lintResult.result.data.by_severity.error
3735
- })
3736
- };
3729
+ let delta = { full_errors: 0, base_errors: 0, new_errors: 0, resolved_errors: 0 };
3730
+ const preferredBase = git(vault, ["rev-parse", "--verify", "origin/main"]) ? "origin/main" : git(vault, ["rev-parse", "--verify", "origin/HEAD"]) ? "origin/HEAD" : "";
3731
+ if (preferredBase) {
3732
+ const deltaResult = await runSyncLintDelta({ vault, baseRef: preferredBase });
3733
+ if (!deltaResult.result.ok) {
3734
+ return {
3735
+ exitCode: ExitCode.LINT_HAS_ERRORS,
3736
+ result: err("LINT_DELTA_UNAVAILABLE", {
3737
+ message: "lint-delta evidence missing or failed \u2014 fail closed",
3738
+ detail: deltaResult.result
3739
+ })
3740
+ };
3741
+ }
3742
+ delta = deltaResult.result.data;
3743
+ if (delta.new_errors > 0) {
3744
+ return {
3745
+ exitCode: ExitCode.LINT_HAS_ERRORS,
3746
+ result: err("LINT_NEW_ERRORS_BLOCK_PUSH", {
3747
+ full_errors: delta.full_errors,
3748
+ base_errors: delta.base_errors,
3749
+ new_errors: delta.new_errors,
3750
+ resolved_errors: delta.resolved_errors,
3751
+ new_fingerprints: deltaResult.result.data.new_fingerprints
3752
+ })
3753
+ };
3754
+ }
3755
+ } else {
3756
+ const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
3757
+ if (lintResult.result.ok) {
3758
+ const fullErrors = lintResult.result.data.summary.errors;
3759
+ delta = { full_errors: fullErrors, base_errors: 0, new_errors: fullErrors, resolved_errors: 0 };
3760
+ if (fullErrors > 0) {
3761
+ const buckets = "by_severity" in lintResult.result.data ? lintResult.result.data.by_severity.error : [];
3762
+ return {
3763
+ exitCode: ExitCode.LINT_HAS_ERRORS,
3764
+ result: err("LINT_ERRORS_BLOCK_PUSH", {
3765
+ errors: fullErrors,
3766
+ buckets,
3767
+ message: "no origin base ref for delta; absolute lint errors block push"
3768
+ })
3769
+ };
3770
+ }
3771
+ } else {
3772
+ delta = { full_errors: 0, base_errors: 0, new_errors: 0, resolved_errors: 0 };
3773
+ }
3737
3774
  }
3738
3775
  try {
3739
3776
  stageVaultContentChanges(vault);
@@ -3776,6 +3813,7 @@ async function runSyncPush(input) {
3776
3813
  })
3777
3814
  };
3778
3815
  }
3816
+ const inheritedNote = delta.full_errors > 0 ? `; lint full=${delta.full_errors} base=${delta.base_errors} new=${delta.new_errors} resolved=${delta.resolved_errors} (inherited debt only)` : `; lint full=0 new=0`;
3779
3817
  return {
3780
3818
  exitCode: ExitCode.OK,
3781
3819
  result: ok({
@@ -3783,7 +3821,11 @@ async function runSyncPush(input) {
3783
3821
  commit_message: commitMessage,
3784
3822
  pushed,
3785
3823
  path_fixes: pathFixes,
3786
- humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}`
3824
+ lint_full_errors: delta.full_errors,
3825
+ lint_base_errors: delta.base_errors,
3826
+ lint_new_errors: delta.new_errors,
3827
+ lint_resolved_errors: delta.resolved_errors,
3828
+ humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}${inheritedNote}`
3787
3829
  })
3788
3830
  };
3789
3831
  }
@@ -5498,6 +5540,11 @@ syncCmd.command("peers [vault]").description("list active locks and recent wiki-
5498
5540
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5499
5541
  else emit(runSyncPeers({ vault: v.vault, sessionId: getCliSessionId() }));
5500
5542
  });
5543
+ syncCmd.command("lint-delta [vault]").description("compare lint errors against a base ref; block only on new errors").option("--base-ref <ref>", "base git ref to compare against", "origin/main").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5544
+ const v = await resolveVaultArg(vault, opts.wiki);
5545
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5546
+ else emit(await runSyncLintDelta({ vault: v.vault, baseRef: opts.baseRef }));
5547
+ });
5501
5548
  var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
5502
5549
  backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5503
5550
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-SE5URAJR.js";
4
+ } from "./chunk-NIPZVIHT.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
 
7
7
  // src/mcp-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.56",
3
+ "version": "0.9.58",
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.9.56",
3
+ "version": "0.9.58",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 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.9.56",
3
+ "version": "0.9.58",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
package/skills/README.md CHANGED
@@ -1,11 +1,22 @@
1
1
  # @skillwiki/skills
2
2
 
3
- Prompt-only Markdown skills for Claude Code. Installed via `skillwiki install`.
3
+ Prompt-only Markdown skills for Claude Code. Installed via `skillwiki install`
4
+ or the Claude/Codex/Antigravity plugin packaging paths.
5
+
6
+ Current package inventory: **18 skills**.
4
7
 
5
8
  | Namespace | Skills |
6
9
  |---|---|
7
- | `wiki-*` | `wiki-init`, `wiki-ingest`, `wiki-query`, `wiki-lint`, `wiki-crystallize`, `wiki-audit` |
10
+ | `wiki-*` | `wiki-init`, `wiki-ingest`, `wiki-query`, `wiki-lint`, `wiki-crystallize`, `wiki-audit`, `wiki-archive`, `wiki-reingest`, `wiki-adapter-prd`, `wiki-add-task`, `wiki-sync`, `wiki-canvas`, `wiki-gate-plan-mode` |
8
11
  | `proj-*` | `proj-init`, `proj-work`, `proj-distill`, `proj-decide` |
12
+ | onboarding | `using-skillwiki` |
13
+
14
+ Verify the live inventory from source:
15
+
16
+ ```bash
17
+ find packages/skills -mindepth 2 -maxdepth 2 -name SKILL.md -print | sort
18
+ bash scripts/verify-manifests.sh
19
+ ```
9
20
 
10
21
  Each top-level skill subdirectory holds one canonical `SKILL.md`. The nested
11
22
  `skills/<skill>/SKILL.md` tree mirrors those files for Codex plugin discovery;
@@ -20,3 +31,6 @@ Codex-specific hook files. That root exposes `hooks/hooks-codex.json` and
20
31
  Run `npm run materialize:plugins` from the repository root after changing
21
32
  canonical skill, agent, or hook assets. Run
22
33
  `npm run materialize:plugins:check` for read-only drift detection.
34
+
35
+ The sibling `vault-sync` plugin ships six additional operational skills under
36
+ `packages/vault-sync/skills/` and is packaged separately from this skill set.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.9.56",
3
+ "version": "0.9.58",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -44,8 +44,8 @@ On every invocation, count `wiki-sync:*` stashes older than 24 hours via `skillw
44
44
 
45
45
  ### Push workflow
46
46
  4. If vault is dirty, ask the user to review uncommitted changes before proceeding.
47
- 5. Run `skillwiki lint <vault>`. If errors exist, stop and report — do not push lint errors to remote.
48
- 6. If lint passes (errors = 0), stage and commit:
47
+ 5. Run `skillwiki sync lint-delta <vault> --base-ref origin/main`. Block only when `new_errors > 0`. Report full/base/new/resolved. Malformed delta evidence fails closed — do not push.
48
+ 6. If lint-delta allows (new_errors = 0), stage and commit:
49
49
  - `git -C <vault> add -A`
50
50
  - `git -C <vault> commit -m "sync: vault update $(date -u +%Y-%m-%dT%H:%MZ)"`
51
51
  7. Run `git -C <vault> push origin HEAD`. Report result.
@@ -62,7 +62,7 @@ On every invocation, count `wiki-sync:*` stashes older than 24 hours via `skillw
62
62
  MSG="wiki-sync:${SESSION_ID}:${CWD_HASH}:${ISO_TS}:pre-pull"
63
63
  git -C "$VAULT" stash push -m "$MSG"
64
64
  ```
65
- 11. Run `git -C <vault> pull --rebase origin HEAD`. Report result.
65
+ 11. Prefer the canonical pull helper (`wiki-pull-with-auto-resolve.sh` / vault-presync `--execute`) so stale-clean rebase state uses recovery-ref + `rebase --quit`, active rebases fail closed, and only fully materialized commits are dropped. If invoking git directly: `git -C <vault> pull --rebase origin HEAD`. Report result.
66
66
  12. If a stash was created, pop it: `git -C <vault> stash pop`.
67
67
  13. If conflicts occur during stash pop, identify them and present to the user for resolution (see Conflict Resolution below).
68
68
  14. Run `skillwiki lint <vault>` after pull to verify vault integrity.
@@ -240,3 +240,20 @@ bash ~/.hermes/scripts/wiki-snapshot.sh # Re-sync fresh
240
240
  - Modifying files in `raw/` to resolve conflicts (N9 — archive and re-ingest instead).
241
241
  - Stashing without the `wiki-sync:...` name format (breaks peer detection).
242
242
  - Force-deleting a peer's lockfile (use `--force` only if peer is confirmed dead).
243
+
244
+ ## Convergence safeguards (2026-07-11)
245
+
246
+ ### Rebase-state classification
247
+ - `stale-clean`: recovery ref at tip + `git rebase --quit` (preserves advanced tip; never abort-reset to orig-head).
248
+ - `active` (REBASE_HEAD / UU paths): leave untouched; fail closed.
249
+ - Recovery refs live under `refs/vault-sync/recovery/<UTC timestamp>`.
250
+
251
+ ### Materialized-commit proof
252
+ Drop a local commit from rebase only when every path is proven present on the target ref (exact blobs; byte-identical added `## ` log sections). Partial/raw/rename mismatches retain or stop.
253
+
254
+ ### Lint-delta fail-closed
255
+ - Fingerprints: `<bucket>\0<page>\0<normalized-detail>`
256
+ - CLI: `skillwiki sync lint-delta <vault> --base-ref origin/main`
257
+ - Block publication only when `new_errors > 0`; inherited full debt remains visible.
258
+ - Missing/malformed delta evidence blocks (never silent lint skip).
259
+
@@ -44,8 +44,8 @@ On every invocation, count `wiki-sync:*` stashes older than 24 hours via `skillw
44
44
 
45
45
  ### Push workflow
46
46
  4. If vault is dirty, ask the user to review uncommitted changes before proceeding.
47
- 5. Run `skillwiki lint <vault>`. If errors exist, stop and report — do not push lint errors to remote.
48
- 6. If lint passes (errors = 0), stage and commit:
47
+ 5. Run `skillwiki sync lint-delta <vault> --base-ref origin/main`. Block only when `new_errors > 0`. Report full/base/new/resolved. Malformed delta evidence fails closed — do not push.
48
+ 6. If lint-delta allows (new_errors = 0), stage and commit:
49
49
  - `git -C <vault> add -A`
50
50
  - `git -C <vault> commit -m "sync: vault update $(date -u +%Y-%m-%dT%H:%MZ)"`
51
51
  7. Run `git -C <vault> push origin HEAD`. Report result.
@@ -62,7 +62,7 @@ On every invocation, count `wiki-sync:*` stashes older than 24 hours via `skillw
62
62
  MSG="wiki-sync:${SESSION_ID}:${CWD_HASH}:${ISO_TS}:pre-pull"
63
63
  git -C "$VAULT" stash push -m "$MSG"
64
64
  ```
65
- 11. Run `git -C <vault> pull --rebase origin HEAD`. Report result.
65
+ 11. Prefer the canonical pull helper (`wiki-pull-with-auto-resolve.sh` / vault-presync `--execute`) so stale-clean rebase state uses recovery-ref + `rebase --quit`, active rebases fail closed, and only fully materialized commits are dropped. If invoking git directly: `git -C <vault> pull --rebase origin HEAD`. Report result.
66
66
  12. If a stash was created, pop it: `git -C <vault> stash pop`.
67
67
  13. If conflicts occur during stash pop, identify them and present to the user for resolution (see Conflict Resolution below).
68
68
  14. Run `skillwiki lint <vault>` after pull to verify vault integrity.
@@ -240,3 +240,20 @@ bash ~/.hermes/scripts/wiki-snapshot.sh # Re-sync fresh
240
240
  - Modifying files in `raw/` to resolve conflicts (N9 — archive and re-ingest instead).
241
241
  - Stashing without the `wiki-sync:...` name format (breaks peer detection).
242
242
  - Force-deleting a peer's lockfile (use `--force` only if peer is confirmed dead).
243
+
244
+ ## Convergence safeguards (2026-07-11)
245
+
246
+ ### Rebase-state classification
247
+ - `stale-clean`: recovery ref at tip + `git rebase --quit` (preserves advanced tip; never abort-reset to orig-head).
248
+ - `active` (REBASE_HEAD / UU paths): leave untouched; fail closed.
249
+ - Recovery refs live under `refs/vault-sync/recovery/<UTC timestamp>`.
250
+
251
+ ### Materialized-commit proof
252
+ Drop a local commit from rebase only when every path is proven present on the target ref (exact blobs; byte-identical added `## ` log sections). Partial/raw/rename mismatches retain or stop.
253
+
254
+ ### Lint-delta fail-closed
255
+ - Fingerprints: `<bucket>\0<page>\0<normalized-detail>`
256
+ - CLI: `skillwiki sync lint-delta <vault> --base-ref origin/main`
257
+ - Block publication only when `new_errors > 0`; inherited full debt remains visible.
258
+ - Missing/malformed delta evidence blocks (never silent lint skip).
259
+