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.
- package/dist/{chunk-NTM3WRAV.js → chunk-EK7SNIU3.js} +154 -21
- package/dist/{chunk-CUSHQB5N.js → chunk-KZZUTQEA.js} +2 -0
- package/dist/cli.js +262 -8
- package/dist/{managed-write-preflight-Z6MEAGQI.js → managed-write-preflight-GJP4RN26.js} +1 -1
- package/dist/skillwiki-mcp.js +2 -2
- package/dist/vault-sync/scripts/wiki-snapshot.sh +106 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
|
@@ -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-
|
|
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
|
-
|
|
2999
|
-
|
|
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
|
|
3340
|
-
|
|
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(
|
|
3448
|
+
const ops = listReviewRequiredOps(checked);
|
|
3345
3449
|
if (ops.length === 0) {
|
|
3346
|
-
return check("pass", "vault_sync_review_required_journals", "Review-required journals",
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
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 });
|
package/dist/skillwiki-mcp.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runSkillwikiMcpStdio
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EK7SNIU3.js";
|
|
5
5
|
import "./chunk-7I2TPIV5.js";
|
|
6
6
|
import "./chunk-NHRRYAXT.js";
|
|
7
|
-
import "./chunk-
|
|
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"
|
|
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.
|
|
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": {
|