skillwiki 0.10.9 → 0.10.10

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.
@@ -22,6 +22,7 @@ import {
22
22
  } from "./chunk-U34B2XQJ.js";
23
23
  import {
24
24
  CONFIG_KEYS,
25
+ git,
25
26
  isValidWikiProfileKey,
26
27
  listReviewRequiredOps,
27
28
  loadFleetManifestAndHost,
@@ -3212,12 +3213,13 @@ function buildCliSurface() {
3212
3213
  program.command("sync");
3213
3214
  program.command("backup");
3214
3215
  program.command("seed").option("--wiki <name>");
3215
- program.command("observe").requiredOption("--text <text>").option("--kind <kind>").option("--project <slug>").option("--wiki <name>");
3216
+ program.command("observe").requiredOption("--text <text>").option("--kind <kind>").option("--project <slug>").option("--severity <level>").option("--capture-budget <n>").option("--wiki <name>");
3216
3217
  program.command("session-brief").option("--project <slug>").option("--write").option("--wiki <name>");
3217
3218
  program.command("memory");
3218
3219
  program.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
3219
3220
  program.command("fleet");
3220
3221
  program.command("page");
3222
+ program.command("write-preflight").option("--command <name>").option("--dirty-threshold <n>").option("--skip-dirty").option("--prior-artifact-file <path>").option("--prior-artifact-text <text>").option("--consecutive-no-decision <n>").option("--no-decision-threshold <n>").option("--human-allow").option("--mission-kind <kind>").option("--skip-mission").option("--project <slug>").option("--capture-day <date>").option("--capture-budget <n>").option("--severity <level>").option("--skip-budget").option("--checks <list>").option("--wiki <name>");
3221
3223
  const graphCmd = program.commands.find((c) => c.name() === "graph");
3222
3224
  graphCmd.command("build").option("--out <path>").option("--wiki <name>");
3223
3225
  const canvasCmd = program.commands.find((c) => c.name() === "canvas");
@@ -7124,10 +7126,389 @@ function readCliPackageJson(baseUrl = import.meta.url) {
7124
7126
  throw new Error(`Could not locate skillwiki package.json from ${baseUrl}`);
7125
7127
  }
7126
7128
 
7129
+ // src/utils/vault-write-gates.ts
7130
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
7131
+ import { join as join26, relative as relative3 } from "path";
7132
+ var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
7133
+ var DEFAULT_CAPTURE_BUDGET = 20;
7134
+ var DEFAULT_NO_DECISION_STREAK = 3;
7135
+ var GateError = {
7136
+ VAULT_DIRTY_BACKLOG: "VAULT_DIRTY_BACKLOG",
7137
+ DIMINISHING_RETURNS: "DIMINISHING_RETURNS",
7138
+ CAPTURE_BUDGET_EXHAUSTED: "CAPTURE_BUDGET_EXHAUSTED",
7139
+ USAGE: "USAGE",
7140
+ VAULT_PATH_INVALID: "VAULT_PATH_INVALID"
7141
+ };
7142
+ var HYGIENE_COMMANDS = /* @__PURE__ */ new Set([
7143
+ "write-preflight",
7144
+ "sync status",
7145
+ "sync push",
7146
+ "sync pull",
7147
+ "sync lock",
7148
+ "sync unlock",
7149
+ "sync resolve-derived",
7150
+ "sync journal list",
7151
+ "sync journal clear-stale",
7152
+ "sync lint-delta",
7153
+ "sync peers",
7154
+ "work-complete",
7155
+ "work-validate",
7156
+ "log materialize",
7157
+ "log migrate-legacy",
7158
+ "index rebuild",
7159
+ "projections materialize",
7160
+ "doctor",
7161
+ "health",
7162
+ "lint",
7163
+ "status",
7164
+ "path",
7165
+ "fleet context",
7166
+ "fleet validate",
7167
+ "fleet health"
7168
+ ]);
7169
+ function isHygieneCommand(command) {
7170
+ const c = command.trim().toLowerCase().replace(/\s+/g, " ");
7171
+ if (HYGIENE_COMMANDS.has(c)) return true;
7172
+ if (c === "sync" || c.startsWith("sync ")) {
7173
+ const rest = c.slice(5).trim();
7174
+ if (!rest) return true;
7175
+ return HYGIENE_COMMANDS.has(`sync ${rest}`);
7176
+ }
7177
+ return false;
7178
+ }
7179
+ function measureDirtyVolume(vault) {
7180
+ const empty = (extra = {}) => ({
7181
+ porcelain_lines: 0,
7182
+ expanded_files: 0,
7183
+ modified: 0,
7184
+ untracked: 0,
7185
+ buckets: [],
7186
+ threshold: 0,
7187
+ over_threshold: false,
7188
+ is_git_repo: false,
7189
+ ...extra
7190
+ });
7191
+ if (!existsSync14(vault) || !statSync3(vault).isDirectory()) {
7192
+ return empty();
7193
+ }
7194
+ const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
7195
+ if (!gitDir) {
7196
+ return empty({ is_git_repo: false });
7197
+ }
7198
+ const porcelain = git(vault, ["status", "--porcelain"]);
7199
+ const lines = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
7200
+ let modified = 0;
7201
+ let untracked = 0;
7202
+ let expanded = 0;
7203
+ const bucketMap = /* @__PURE__ */ new Map();
7204
+ const addBucket = (rel, n) => {
7205
+ const top = rel.split(/[/\\]/)[0] || ".";
7206
+ bucketMap.set(top, (bucketMap.get(top) ?? 0) + n);
7207
+ };
7208
+ for (const line of lines) {
7209
+ const code = line.slice(0, 2);
7210
+ let pathPart = line.slice(3);
7211
+ if (pathPart.includes(" -> ")) {
7212
+ pathPart = pathPart.split(" -> ").pop() ?? pathPart;
7213
+ }
7214
+ const rel = pathPart.replace(/^"|"$/g, "").trim();
7215
+ if (!rel) continue;
7216
+ if (code === "??") {
7217
+ untracked += 1;
7218
+ const abs = join26(vault, rel);
7219
+ if (existsSync14(abs) && statSync3(abs).isDirectory()) {
7220
+ const files = listFilesRecursive(abs);
7221
+ expanded += files.length;
7222
+ addBucket(rel, files.length);
7223
+ } else {
7224
+ expanded += 1;
7225
+ addBucket(rel, 1);
7226
+ }
7227
+ } else {
7228
+ modified += 1;
7229
+ expanded += 1;
7230
+ addBucket(rel, 1);
7231
+ }
7232
+ }
7233
+ const buckets = [...bucketMap.entries()].map(([bucket, count]) => ({ bucket, count })).sort((a, b) => b.count - a.count);
7234
+ return {
7235
+ porcelain_lines: lines.length,
7236
+ expanded_files: expanded,
7237
+ modified,
7238
+ untracked,
7239
+ buckets,
7240
+ threshold: 0,
7241
+ over_threshold: false,
7242
+ is_git_repo: true
7243
+ };
7244
+ }
7245
+ function listFilesRecursive(dir) {
7246
+ const out = [];
7247
+ let entries;
7248
+ try {
7249
+ entries = readdirSync4(dir);
7250
+ } catch {
7251
+ return out;
7252
+ }
7253
+ for (const name of entries) {
7254
+ if (name === ".git") continue;
7255
+ const p = join26(dir, name);
7256
+ try {
7257
+ const st = statSync3(p);
7258
+ if (st.isDirectory()) out.push(...listFilesRecursive(p));
7259
+ else if (st.isFile()) out.push(p);
7260
+ } catch {
7261
+ }
7262
+ }
7263
+ return out;
7264
+ }
7265
+ function evaluateDirtyVolumeGate(input) {
7266
+ const threshold = input.threshold ?? DEFAULT_DIRTY_VOLUME_THRESHOLD;
7267
+ const report = measureDirtyVolume(input.vault);
7268
+ report.threshold = threshold;
7269
+ report.over_threshold = report.is_git_repo && report.expanded_files > threshold;
7270
+ if (input.skip) {
7271
+ return { allowed: true, reason: "skipped", report };
7272
+ }
7273
+ if (input.command && isHygieneCommand(input.command)) {
7274
+ return { allowed: true, reason: "hygiene", report };
7275
+ }
7276
+ if (!report.is_git_repo) {
7277
+ return { allowed: true, reason: "not_a_git_repo", report };
7278
+ }
7279
+ if (!report.over_threshold) {
7280
+ return { allowed: true, reason: "under_threshold", report };
7281
+ }
7282
+ const top = report.buckets.slice(0, 5).map((b) => `${b.bucket}:${b.count}`).join(", ");
7283
+ return {
7284
+ allowed: false,
7285
+ reason: "over_threshold",
7286
+ code: GateError.VAULT_DIRTY_BACKLOG,
7287
+ report,
7288
+ humanHint: `Vault dirty volume ${report.expanded_files} exceeds threshold ${threshold} (porcelain ${report.porcelain_lines}; buckets: ${top || "none"}). Triage/commit keep-set before more non-hygiene writes. Hygiene commands still allowed.`
7289
+ };
7290
+ }
7291
+ var SATURATION_PATTERNS = [
7292
+ /\benablement\s+\*\*saturated\*\*/i,
7293
+ /\bsaturated\b/i,
7294
+ /\bexplicit hold\b/i,
7295
+ /\bno further\b.*\bbatches\b/i,
7296
+ /\bpause (?:the )?job\b/i,
7297
+ /\bcancel research\b/i,
7298
+ /\bkill research\b/i,
7299
+ /\bdiminishing returns\b/i,
7300
+ /\bhuman send only\b/i,
7301
+ /\bno new decision\b/i,
7302
+ /\bHOLD:\b/,
7303
+ /\bstatus:\s*saturated\b/i,
7304
+ /\bmission:\s*cancelled\b/i,
7305
+ /\bcancelled mission\b/i
7306
+ ];
7307
+ function detectSaturationSignals(text) {
7308
+ if (!text) return [];
7309
+ const hits = [];
7310
+ for (const re of SATURATION_PATTERNS) {
7311
+ const m = text.match(re);
7312
+ if (m) hits.push(m[0]);
7313
+ }
7314
+ return hits;
7315
+ }
7316
+ function evaluateMissionCycleGate(input) {
7317
+ if (input.humanAllow) {
7318
+ return { allowed: true, reason: "human_allow", signals: [] };
7319
+ }
7320
+ const signals = detectSaturationSignals(input.priorArtifactText ?? "");
7321
+ if (signals.length > 0) {
7322
+ return {
7323
+ allowed: false,
7324
+ reason: "saturated_text",
7325
+ code: GateError.DIMINISHING_RETURNS,
7326
+ signals,
7327
+ humanHint: `Mission saturated/hold/stop signal(s) in prior artifact: ${signals.slice(0, 5).join("; ")}. Refuse new ${input.missionKind ?? "cycle"} artifact without --human-allow.`
7328
+ };
7329
+ }
7330
+ const streak = input.consecutiveNoNewDecision ?? 0;
7331
+ const thresh = input.noDecisionThreshold ?? DEFAULT_NO_DECISION_STREAK;
7332
+ if (streak >= thresh) {
7333
+ return {
7334
+ allowed: false,
7335
+ reason: "no_decision_streak",
7336
+ code: GateError.DIMINISHING_RETURNS,
7337
+ signals: [`no_new_decision_streak=${streak}`],
7338
+ humanHint: `${streak} consecutive no-new-decision cycles (\u2265 ${thresh}). Refuse further cycle artifacts without --human-allow.`
7339
+ };
7340
+ }
7341
+ return { allowed: true, reason: "clean", signals: [] };
7342
+ }
7343
+ function utcDay(d = /* @__PURE__ */ new Date()) {
7344
+ return d.toISOString().slice(0, 10);
7345
+ }
7346
+ var CAPTURE_HYGIENE_CONTRACT = `After each productive cycle (or when daily capture budget is reached), stage/commit the keep-set or discard noise so untracked investigate files for the project stay within the daily budget (default ${DEFAULT_CAPTURE_BUDGET}). P0 severity escapes the budget. Do not silently grow unbounded untracked investigate files.`;
7347
+ function listProjectDayCaptures(vault, project, day) {
7348
+ const found = [];
7349
+ const slug = project.replace(/^\[\[/, "").replace(/\]\]$/, "").trim();
7350
+ if (!slug || !existsSync14(vault)) return found;
7351
+ const consider = (abs, rel) => {
7352
+ if (!rel.endsWith(".md")) return;
7353
+ const base = rel.split(/[/\\]/).pop() ?? "";
7354
+ if (!base.startsWith(day)) return;
7355
+ const normRel = rel.replace(/\\/g, "/");
7356
+ const captureLike = /investigate|dev-loop|research-cycle|pilot-q|cycle-\d+|batch-|idle-no-new|observation/.test(normRel) || /(^|\/)raw\/transcripts\//.test(normRel) || /(^|\/)requirements\//.test(normRel);
7357
+ if (!captureLike && !/transcripts/.test(normRel)) {
7358
+ return;
7359
+ }
7360
+ const norm = rel.replace(/\\/g, "/");
7361
+ if (norm.startsWith(`projects/${slug}/`)) {
7362
+ found.push(norm);
7363
+ return;
7364
+ }
7365
+ if (norm.startsWith("raw/transcripts/")) {
7366
+ try {
7367
+ const body = readFileSync12(abs, "utf8");
7368
+ if (body.includes(`project: ${slug}`) || body.includes(`project: "[[${slug}]]"`) || body.includes(`project: [[${slug}]]`)) {
7369
+ found.push(norm);
7370
+ } else if (base.includes(slug)) {
7371
+ found.push(norm);
7372
+ }
7373
+ } catch {
7374
+ }
7375
+ }
7376
+ };
7377
+ const walk = (dir, relBase) => {
7378
+ if (!existsSync14(dir)) return;
7379
+ let entries;
7380
+ try {
7381
+ entries = readdirSync4(dir);
7382
+ } catch {
7383
+ return;
7384
+ }
7385
+ for (const name of entries) {
7386
+ const abs = join26(dir, name);
7387
+ const rel = relBase ? `${relBase}/${name}` : name;
7388
+ try {
7389
+ const st = statSync3(abs);
7390
+ if (st.isDirectory()) walk(abs, rel);
7391
+ else if (st.isFile()) consider(abs, rel);
7392
+ } catch {
7393
+ }
7394
+ }
7395
+ };
7396
+ walk(join26(vault, "raw", "transcripts"), "raw/transcripts");
7397
+ walk(join26(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
7398
+ walk(join26(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
7399
+ const workRoot = join26(vault, "projects", slug, "work");
7400
+ if (existsSync14(workRoot)) {
7401
+ try {
7402
+ for (const name of readdirSync4(workRoot)) {
7403
+ if (!name.startsWith(day)) continue;
7404
+ if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
7405
+ const abs = join26(workRoot, name);
7406
+ if (statSync3(abs).isDirectory()) {
7407
+ for (const f of listFilesRecursive(abs)) {
7408
+ const rel = relative3(vault, f).replace(/\\/g, "/");
7409
+ if (rel.endsWith(".md")) found.push(rel);
7410
+ }
7411
+ }
7412
+ }
7413
+ } catch {
7414
+ }
7415
+ }
7416
+ return [...new Set(found)].sort();
7417
+ }
7418
+ function evaluateCaptureBudget(input) {
7419
+ const day = input.day ?? utcDay();
7420
+ const budget = input.budget ?? DEFAULT_CAPTURE_BUDGET;
7421
+ const paths = listProjectDayCaptures(input.vault, input.project, day);
7422
+ const used = paths.length;
7423
+ const report = {
7424
+ project: input.project,
7425
+ day,
7426
+ budget,
7427
+ used,
7428
+ remaining: Math.max(0, budget - used),
7429
+ paths
7430
+ };
7431
+ if (input.skip) {
7432
+ return { allowed: true, reason: "skipped", report };
7433
+ }
7434
+ const sev = (input.severity ?? "").toString().trim().toUpperCase();
7435
+ if (sev === "P0" || sev === "0") {
7436
+ return { allowed: true, reason: "p0_escape", report };
7437
+ }
7438
+ if (used >= budget) {
7439
+ return {
7440
+ allowed: false,
7441
+ reason: "exhausted",
7442
+ code: GateError.CAPTURE_BUDGET_EXHAUSTED,
7443
+ report,
7444
+ humanHint: `Capture budget exhausted for project ${input.project} on ${day}: ${used}/${budget}. ${CAPTURE_HYGIENE_CONTRACT}`
7445
+ };
7446
+ }
7447
+ return { allowed: true, reason: "under_budget", report };
7448
+ }
7449
+ function runWritePreflight(input) {
7450
+ if (!input.vault || !existsSync14(input.vault)) {
7451
+ return err(GateError.VAULT_PATH_INVALID, { path: input.vault });
7452
+ }
7453
+ const want = new Set(input.checks ?? ["all"]);
7454
+ const runAll = want.has("all");
7455
+ const checks = {};
7456
+ const refused = [];
7457
+ if (runAll || want.has("dirty")) {
7458
+ const dirty = evaluateDirtyVolumeGate({
7459
+ vault: input.vault,
7460
+ threshold: input.dirtyThreshold,
7461
+ skip: input.skipDirty,
7462
+ command: input.command
7463
+ });
7464
+ checks.dirty = dirty;
7465
+ if (!dirty.allowed) {
7466
+ refused.push({ code: dirty.code, humanHint: dirty.humanHint });
7467
+ }
7468
+ }
7469
+ const missionRequested = runAll || want.has("mission") || input.priorArtifactText != null || input.consecutiveNoNewDecision != null;
7470
+ if (missionRequested && !input.skipMission) {
7471
+ if (want.has("mission") || input.priorArtifactText != null && input.priorArtifactText.length > 0 || input.consecutiveNoNewDecision != null && input.consecutiveNoNewDecision > 0) {
7472
+ const mission = evaluateMissionCycleGate({
7473
+ priorArtifactText: input.priorArtifactText,
7474
+ consecutiveNoNewDecision: input.consecutiveNoNewDecision,
7475
+ noDecisionThreshold: input.noDecisionThreshold,
7476
+ humanAllow: input.humanAllow,
7477
+ missionKind: input.missionKind
7478
+ });
7479
+ checks.mission = mission;
7480
+ if (!mission.allowed) {
7481
+ refused.push({ code: mission.code, humanHint: mission.humanHint });
7482
+ }
7483
+ }
7484
+ }
7485
+ if ((runAll || want.has("budget")) && input.project && !input.skipBudget) {
7486
+ const budget = evaluateCaptureBudget({
7487
+ vault: input.vault,
7488
+ project: input.project,
7489
+ day: input.captureDay,
7490
+ budget: input.captureBudget,
7491
+ severity: input.severity
7492
+ });
7493
+ checks.budget = budget;
7494
+ if (!budget.allowed) {
7495
+ refused.push({ code: budget.code, humanHint: budget.humanHint });
7496
+ }
7497
+ }
7498
+ const allowed = refused.length === 0;
7499
+ return ok({
7500
+ allowed,
7501
+ checks,
7502
+ refused,
7503
+ hygiene_contract: CAPTURE_HYGIENE_CONTRACT,
7504
+ humanHint: allowed ? "write preflight: allowed" : `write preflight: refused \u2014 ${refused.map((r) => r.code).join(", ")}`
7505
+ });
7506
+ }
7507
+
7127
7508
  // src/commands/observe.ts
7128
7509
  import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
7129
- import { existsSync as existsSync14, statSync as statSync3 } from "fs";
7130
- import { join as join26 } from "path";
7510
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
7511
+ import { join as join27 } from "path";
7131
7512
  import { createHash as createHash6 } from "crypto";
7132
7513
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
7133
7514
  function slugify(text) {
@@ -7150,13 +7531,33 @@ async function runObserve(input) {
7150
7531
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
7151
7532
  };
7152
7533
  }
7153
- if (!existsSync14(input.vault) || !statSync3(input.vault).isDirectory()) {
7534
+ if (!existsSync15(input.vault) || !statSync4(input.vault).isDirectory()) {
7154
7535
  return {
7155
7536
  exitCode: ExitCode.VAULT_PATH_INVALID,
7156
7537
  result: err("VAULT_PATH_INVALID", { path: input.vault })
7157
7538
  };
7158
7539
  }
7159
- const transcriptsDir = join26(input.vault, "raw", "transcripts");
7540
+ if (input.project && !input.skipCaptureBudget) {
7541
+ const budget = evaluateCaptureBudget({
7542
+ vault: input.vault,
7543
+ project: input.project,
7544
+ day: input.captureDay,
7545
+ budget: input.captureBudget,
7546
+ severity: input.severity
7547
+ });
7548
+ if (!budget.allowed) {
7549
+ return {
7550
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7551
+ result: err(GateError.CAPTURE_BUDGET_EXHAUSTED, {
7552
+ reason: budget.reason,
7553
+ report: budget.report,
7554
+ humanHint: budget.humanHint,
7555
+ hygiene_contract: CAPTURE_HYGIENE_CONTRACT
7556
+ })
7557
+ };
7558
+ }
7559
+ }
7560
+ const transcriptsDir = join27(input.vault, "raw", "transcripts");
7160
7561
  try {
7161
7562
  await mkdir6(transcriptsDir, { recursive: true });
7162
7563
  } catch {
@@ -7168,7 +7569,7 @@ async function runObserve(input) {
7168
7569
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7169
7570
  const slug = slugify(input.text);
7170
7571
  const fileName = `${today}-observation-${slug}.md`;
7171
- const filePath = join26(transcriptsDir, fileName);
7572
+ const filePath = join27(transcriptsDir, fileName);
7172
7573
  const body = `
7173
7574
  ${input.text.trim()}
7174
7575
  `;
@@ -7210,7 +7611,7 @@ ${input.text.trim()}
7210
7611
  // src/commands/memory.ts
7211
7612
  import { createHash as createHash7 } from "crypto";
7212
7613
  import { mkdir as mkdir7, readFile as readFile17, readdir as readdir5, stat as stat5, writeFile as writeFile5 } from "fs/promises";
7213
- import { basename as basename2, extname, join as join27, relative as relative3, sep as sep3 } from "path";
7614
+ import { basename as basename2, extname, join as join28, relative as relative4, sep as sep3 } from "path";
7214
7615
 
7215
7616
  // src/utils/memory-authority.ts
7216
7617
  var TIER_RANK = {
@@ -7329,8 +7730,8 @@ async function runMemoryIndex(input) {
7329
7730
  }
7330
7731
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
7331
7732
  const relCachePath = memoryCacheRelPath(input.project);
7332
- const absCachePath = join27(input.vault, relCachePath);
7333
- await mkdir7(join27(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
7733
+ const absCachePath = join28(input.vault, relCachePath);
7734
+ await mkdir7(join28(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
7334
7735
  await writeFile5(absCachePath, `${JSON.stringify({
7335
7736
  generated_at: generatedAt,
7336
7737
  project: input.project,
@@ -7522,7 +7923,7 @@ async function buildMemoryIndexState(pages, project) {
7522
7923
  }
7523
7924
  async function checkMemoryIndex(vault, project, current) {
7524
7925
  const relCachePath = memoryCacheRelPath(project);
7525
- const cacheText = await readIfExists2(join27(vault, relCachePath));
7926
+ const cacheText = await readIfExists2(join28(vault, relCachePath));
7526
7927
  if (!cacheText) {
7527
7928
  return {
7528
7929
  ok: true,
@@ -7931,7 +8332,7 @@ async function walkImportFiles(dir, out) {
7931
8332
  const entries = await readdir5(dir, { withFileTypes: true });
7932
8333
  for (const entry of entries) {
7933
8334
  if (entry.name === ".git" || entry.name === "node_modules") continue;
7934
- const path = join27(dir, entry.name);
8335
+ const path = join28(dir, entry.name);
7935
8336
  if (entry.isDirectory()) {
7936
8337
  await walkImportFiles(path, out);
7937
8338
  } else if (entry.isFile() && isImportCandidate(path)) {
@@ -7981,7 +8382,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
7981
8382
  const redacted = redactSensitiveContent(extracted, { file });
7982
8383
  const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
7983
8384
  const sourceSlug = slugify2(basename2(file, extname(file)));
7984
- const relSource = relative3(sourceRoot, file).split(sep3).join("/");
8385
+ const relSource = relative4(sourceRoot, file).split(sep3).join("/");
7985
8386
  const entry = {
7986
8387
  ...baseEntry,
7987
8388
  status: "ready",
@@ -7998,8 +8399,8 @@ async function writeImportCapture(vault, entry, today) {
7998
8399
  const content = hiddenString(entry, "__content");
7999
8400
  const project = hiddenString(entry, "__project");
8000
8401
  const relPath = await availableImportPath(vault, entry.proposed_path);
8001
- const absPath = join27(vault, relPath);
8002
- await mkdir7(join27(vault, "raw", "transcripts"), { recursive: true });
8402
+ const absPath = join28(vault, relPath);
8403
+ await mkdir7(join28(vault, "raw", "transcripts"), { recursive: true });
8003
8404
  await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
8004
8405
  const validation = await runValidate({ file: absPath });
8005
8406
  return {
@@ -8015,7 +8416,7 @@ async function availableImportPath(vault, proposed) {
8015
8416
  const stem = proposed.slice(0, -ext.length);
8016
8417
  let candidate = proposed;
8017
8418
  let i = 2;
8018
- while (await readIfExists2(join27(vault, candidate))) {
8419
+ while (await readIfExists2(join28(vault, candidate))) {
8019
8420
  candidate = `${stem}-${i}${ext}`;
8020
8421
  i++;
8021
8422
  }
@@ -8200,10 +8601,10 @@ function memoryCacheRelPath(project) {
8200
8601
  }
8201
8602
  async function readMemoryCache(vault, project) {
8202
8603
  if (project) {
8203
- const projectCache = await readIfExists2(join27(vault, memoryCacheRelPath(project)));
8604
+ const projectCache = await readIfExists2(join28(vault, memoryCacheRelPath(project)));
8204
8605
  if (projectCache) return projectCache;
8205
8606
  }
8206
- return readIfExists2(join27(vault, ".skillwiki", "memory-topics.json"));
8607
+ return readIfExists2(join28(vault, ".skillwiki", "memory-topics.json"));
8207
8608
  }
8208
8609
  function dedupePages(pages) {
8209
8610
  const seen = /* @__PURE__ */ new Set();
@@ -8295,7 +8696,7 @@ function slugify2(value) {
8295
8696
 
8296
8697
  // src/commands/query.ts
8297
8698
  import { readFile as readFile18, stat as stat6 } from "fs/promises";
8298
- import { join as join28 } from "path";
8699
+ import { join as join29 } from "path";
8299
8700
  var W_KEYWORD = 2;
8300
8701
  var W_SOURCE_OVERLAP = 4;
8301
8702
  var W_WIKILINK = 3;
@@ -8416,7 +8817,7 @@ function computeKeywordScore(terms, title, tags, body) {
8416
8817
  return score;
8417
8818
  }
8418
8819
  async function loadOrBuildGraph(vault) {
8419
- const graphPath = join28(vault, ".skillwiki", "graph.json");
8820
+ const graphPath = join29(vault, ".skillwiki", "graph.json");
8420
8821
  let needsBuild = false;
8421
8822
  try {
8422
8823
  const fileStat = await stat6(graphPath);
@@ -8445,7 +8846,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8445
8846
  import { z } from "zod";
8446
8847
 
8447
8848
  // src/mcp/vault-resolve.ts
8448
- import { join as join29, resolve as resolve7 } from "path";
8849
+ import { join as join30, resolve as resolve7 } from "path";
8449
8850
 
8450
8851
  // src/mcp/allowlist.ts
8451
8852
  import { resolve as resolve6, sep as sep4 } from "path";
@@ -8507,7 +8908,7 @@ async function resolveMcpVault(input) {
8507
8908
  return ok({ vault: vaultPath, source });
8508
8909
  }
8509
8910
  function defaultGraphOut(vault) {
8510
- return join29(vault, ".skillwiki", "graph.json");
8911
+ return join30(vault, ".skillwiki", "graph.json");
8511
8912
  }
8512
8913
 
8513
8914
  // src/mcp/result-format.ts
@@ -8524,7 +8925,7 @@ function formatToolResult(payload) {
8524
8925
  // src/mcp/audit-log.ts
8525
8926
  import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
8526
8927
  import { homedir } from "os";
8527
- import { join as join30 } from "path";
8928
+ import { join as join31 } from "path";
8528
8929
  function auditEnabled() {
8529
8930
  const v = process.env.SKILLWIKI_MCP_AUDIT;
8530
8931
  if (v === "0" || v === "false") return false;
@@ -8536,7 +8937,7 @@ function auditSink() {
8536
8937
  function auditFilePath() {
8537
8938
  const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
8538
8939
  if (custom && custom.length > 0) return custom;
8539
- return join30(homedir(), ".skillwiki", "mcp-audit.jsonl");
8940
+ return join31(homedir(), ".skillwiki", "mcp-audit.jsonl");
8540
8941
  }
8541
8942
  function auditMcpToolCall(entry) {
8542
8943
  if (!auditEnabled()) return;
@@ -8546,7 +8947,7 @@ function auditMcpToolCall(entry) {
8546
8947
  return;
8547
8948
  }
8548
8949
  const path = auditFilePath();
8549
- mkdirSync5(join30(path, ".."), { recursive: true });
8950
+ mkdirSync5(join31(path, ".."), { recursive: true });
8550
8951
  appendFileSync(path, line, "utf8");
8551
8952
  }
8552
8953
  async function runMcpToolHandler(tool, input, fn) {
@@ -8767,7 +9168,7 @@ function registerMcpMutatingTools(server) {
8767
9168
 
8768
9169
  // src/mcp/resources.ts
8769
9170
  import { readFile as readFile20 } from "fs/promises";
8770
- import { join as join32 } from "path";
9171
+ import { join as join33 } from "path";
8771
9172
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8772
9173
 
8773
9174
  // src/mcp/lint-bucket.ts
@@ -8896,8 +9297,8 @@ async function fetchQueryPreview(input) {
8896
9297
 
8897
9298
  // src/mcp/graph-html.ts
8898
9299
  import { readFile as readFile19 } from "fs/promises";
8899
- import { join as join31 } from "path";
8900
- import { existsSync as existsSync15 } from "fs";
9300
+ import { join as join32 } from "path";
9301
+ import { existsSync as existsSync16 } from "fs";
8901
9302
  var TYPE_COLORS = {
8902
9303
  entities: "#e74c3c",
8903
9304
  concepts: "#27ae60",
@@ -8965,9 +9366,9 @@ ${nodeSvg}
8965
9366
  return { html, node_count: nodes.length, edge_count: edges.length, truncated };
8966
9367
  }
8967
9368
  async function fetchGraphHtmlReport(input) {
8968
- const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
9369
+ const graphPath = input.graphPath ?? join32(input.vault, ".skillwiki", "graph.json");
8969
9370
  const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
8970
- if (!existsSync15(graphPath)) {
9371
+ if (!existsSync16(graphPath)) {
8971
9372
  return {
8972
9373
  exitCode: ExitCode.FILE_NOT_FOUND,
8973
9374
  result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
@@ -9039,7 +9440,7 @@ async function fetchStaleSummary(input) {
9039
9440
 
9040
9441
  // src/mcp/resources.ts
9041
9442
  async function readVaultFile(vault, rel) {
9042
- return readFile20(join32(vault, rel), "utf8");
9443
+ return readFile20(join33(vault, rel), "utf8");
9043
9444
  }
9044
9445
  async function tailLines(text, lines) {
9045
9446
  const parts = text.split(/\r?\n/);
@@ -9125,7 +9526,7 @@ function registerMcpResources(server) {
9125
9526
  if (!v.ok) {
9126
9527
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
9127
9528
  }
9128
- const path = join32(v.data.vault, ".skillwiki", "graph.json");
9529
+ const path = join33(v.data.vault, ".skillwiki", "graph.json");
9129
9530
  try {
9130
9531
  const raw = await readFile20(path, "utf8");
9131
9532
  const graph = JSON.parse(raw);
@@ -9500,6 +9901,13 @@ export {
9500
9901
  evaluateSatelliteRunHealth,
9501
9902
  runDoctor,
9502
9903
  readCliPackageJson,
9904
+ DEFAULT_DIRTY_VOLUME_THRESHOLD,
9905
+ DEFAULT_CAPTURE_BUDGET,
9906
+ DEFAULT_NO_DECISION_STREAK,
9907
+ GateError,
9908
+ evaluateDirtyVolumeGate,
9909
+ CAPTURE_HYGIENE_CONTRACT,
9910
+ runWritePreflight,
9503
9911
  runObserve,
9504
9912
  memoryAuthorityTiersRank,
9505
9913
  runMemoryTopics,
package/dist/cli.js CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CAPTURE_HYGIENE_CONTRACT,
4
+ DEFAULT_CAPTURE_BUDGET,
5
+ DEFAULT_DIRTY_VOLUME_THRESHOLD,
6
+ DEFAULT_NO_DECISION_STREAK,
7
+ GateError,
3
8
  SATELLITE_STALE_MS,
4
9
  acquireLock,
5
10
  acquireOwnedSyncLock,
@@ -9,6 +14,7 @@ import {
9
14
  buildRemoteObjectPath,
10
15
  clearLastOp,
11
16
  configPath,
17
+ evaluateDirtyVolumeGate,
12
18
  evaluateSatelliteRunHealth,
13
19
  extractCitationMarkers,
14
20
  extractTaxonomy,
@@ -66,13 +72,14 @@ import {
66
72
  runTagAudit,
67
73
  runTopicMapCheck,
68
74
  runValidate,
75
+ runWritePreflight,
69
76
  safeWritePage,
70
77
  satelliteLatestRunPath,
71
78
  scanConflictMarkerBlocksInText,
72
79
  taxonomyCommentForPage,
73
80
  upsertIndexEntry,
74
81
  writeLogEvent
75
- } from "./chunk-AVCGIWSL.js";
82
+ } from "./chunk-ZFEZZQFM.js";
76
83
  import {
77
84
  normalizeDistTag,
78
85
  readCache,
@@ -7086,6 +7093,67 @@ async function postCommit(vault, exitCode) {
7086
7093
  clearLastOp(vault);
7087
7094
  }
7088
7095
 
7096
+ // src/commands/write-preflight.ts
7097
+ import { existsSync as existsSync13, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
7098
+ async function runWritePreflightCommand(input) {
7099
+ if (!existsSync13(input.vault) || !statSync4(input.vault).isDirectory()) {
7100
+ return {
7101
+ exitCode: ExitCode.VAULT_PATH_INVALID,
7102
+ result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
7103
+ };
7104
+ }
7105
+ let priorText = input.priorArtifactText;
7106
+ if (input.priorArtifactFile) {
7107
+ if (!existsSync13(input.priorArtifactFile)) {
7108
+ return {
7109
+ exitCode: ExitCode.FILE_NOT_FOUND,
7110
+ result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
7111
+ };
7112
+ }
7113
+ priorText = readFileSync15(input.priorArtifactFile, "utf8");
7114
+ }
7115
+ const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
7116
+ const result = runWritePreflight({
7117
+ vault: input.vault,
7118
+ command: input.command,
7119
+ dirtyThreshold: input.dirtyThreshold ?? DEFAULT_DIRTY_VOLUME_THRESHOLD,
7120
+ skipDirty: input.skipDirty,
7121
+ priorArtifactText: priorText,
7122
+ consecutiveNoNewDecision: input.consecutiveNoNewDecision,
7123
+ noDecisionThreshold: input.noDecisionThreshold ?? DEFAULT_NO_DECISION_STREAK,
7124
+ humanAllow: input.humanAllow,
7125
+ missionKind: input.missionKind,
7126
+ skipMission: input.skipMission,
7127
+ project: input.project,
7128
+ captureDay: input.captureDay,
7129
+ captureBudget: input.captureBudget ?? DEFAULT_CAPTURE_BUDGET,
7130
+ severity: input.severity,
7131
+ skipBudget: input.skipBudget,
7132
+ checks: checkList
7133
+ });
7134
+ if (!result.ok) {
7135
+ const code = result.error === GateError.VAULT_PATH_INVALID ? ExitCode.VAULT_PATH_INVALID : ExitCode.PREFLIGHT_FAILED;
7136
+ return { exitCode: code, result };
7137
+ }
7138
+ if (!result.data.allowed) {
7139
+ const first = result.data.refused[0];
7140
+ return {
7141
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7142
+ result: err(first?.code ?? GateError.VAULT_DIRTY_BACKLOG, {
7143
+ ...result.data,
7144
+ hygiene_contract: CAPTURE_HYGIENE_CONTRACT
7145
+ })
7146
+ };
7147
+ }
7148
+ return {
7149
+ exitCode: ExitCode.OK,
7150
+ result: ok({
7151
+ ...result.data,
7152
+ hygiene_contract: CAPTURE_HYGIENE_CONTRACT
7153
+ })
7154
+ };
7155
+ }
7156
+
7089
7157
  // src/cli.ts
7090
7158
  var pkg = readCliPackageJson();
7091
7159
  var program = new Command();
@@ -7097,6 +7165,29 @@ async function emit(r, vault, opts) {
7097
7165
  if (vault && opts?.postCommit !== false) await postCommit(vault, r.exitCode);
7098
7166
  process.exit(r.exitCode);
7099
7167
  }
7168
+ function dirtyVolumeBlock(vault, command) {
7169
+ if (process.env.SKILLWIKI_SKIP_DIRTY_GATE === "1") return null;
7170
+ const thrRaw = process.env.SKILLWIKI_DIRTY_THRESHOLD;
7171
+ const threshold = thrRaw != null && thrRaw !== "" ? Number(thrRaw) : void 0;
7172
+ const gate = evaluateDirtyVolumeGate({
7173
+ vault,
7174
+ command,
7175
+ threshold: Number.isFinite(threshold) ? threshold : void 0
7176
+ });
7177
+ if (gate.allowed) return null;
7178
+ return {
7179
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7180
+ result: {
7181
+ ok: false,
7182
+ error: gate.code,
7183
+ detail: {
7184
+ reason: gate.reason,
7185
+ report: gate.report,
7186
+ humanHint: gate.humanHint
7187
+ }
7188
+ }
7189
+ };
7190
+ }
7100
7191
  async function emitGuardedVaultWrite(vault, command, run, opts) {
7101
7192
  const guard = await guardProtectedVaultWrite({
7102
7193
  vault,
@@ -7110,6 +7201,10 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
7110
7201
  if (guard.blocked) {
7111
7202
  return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
7112
7203
  }
7204
+ const dirty = dirtyVolumeBlock(vault, command);
7205
+ if (dirty) {
7206
+ return emit(dirty, void 0, { postCommit: false });
7207
+ }
7113
7208
  return emit(await run(), vault, opts);
7114
7209
  }
7115
7210
  async function emitManagedVaultWrite(vault, command, mutate, opts) {
@@ -7125,6 +7220,10 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
7125
7220
  if (guard.blocked) {
7126
7221
  return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
7127
7222
  }
7223
+ const dirty = dirtyVolumeBlock(vault, command);
7224
+ if (dirty) {
7225
+ return emit(dirty, void 0, { postCommit: false });
7226
+ }
7128
7227
  const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-7TWSEZJZ.js");
7129
7228
  const run = await runManagedWriteTransaction2({
7130
7229
  vault,
@@ -7815,7 +7914,7 @@ program.command("seed [vault]").description("populate a vault with example conte
7815
7914
  () => runSeed({ vault: v.vault })
7816
7915
  );
7817
7916
  });
7818
- program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "task").option("--project <slug>", "associated project slug (required for task/bug claim detection)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7917
+ program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "task").option("--project <slug>", "associated project slug (required for task/bug claim detection)").option("--severity <level>", "severity (P0 escapes daily capture budget)").option("--capture-budget <n>", "override daily capture budget", (s) => parseInt(s, 10)).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7819
7918
  const v = await resolveVaultArg(vault, opts.wiki);
7820
7919
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7821
7920
  else return emitGuardedVaultWrite(
@@ -7825,7 +7924,9 @@ program.command("observe [vault]").description("create a raw transcript observat
7825
7924
  vault: v.vault,
7826
7925
  text: opts.text,
7827
7926
  kind: opts.kind,
7828
- project: opts.project
7927
+ project: opts.project,
7928
+ severity: opts.severity,
7929
+ captureBudget: opts.captureBudget
7829
7930
  })
7830
7931
  );
7831
7932
  });
@@ -7985,6 +8086,29 @@ fleetCmd.command("health [vault]").description("read-only health probe for skill
7985
8086
  });
7986
8087
  emit(r, vault);
7987
8088
  });
8089
+ program.command("write-preflight [vault]").description("agent write gates: dirty volume, saturated mission stop, capture budget (analysis M1\u2013M3)").option("--command <name>", "command name for hygiene classification", "agent-write").option("--dirty-threshold <n>", "expanded dirty-file threshold", (s) => parseInt(s, 10)).option("--skip-dirty", "skip dirty-volume gate", false).option("--prior-artifact-file <path>", "prior cycle artifact path for saturation scan").option("--prior-artifact-text <text>", "prior cycle artifact text for saturation scan").option("--consecutive-no-decision <n>", "no-new-decision streak", (s) => parseInt(s, 10)).option("--no-decision-threshold <n>", "streak threshold", (s) => parseInt(s, 10)).option("--human-allow", "explicit human allow for saturated missions", false).option("--mission-kind <kind>", "mission kind label (pilot-q, research-cycle, \u2026)").option("--skip-mission", "skip mission saturation gate", false).option("--project <slug>", "project slug for capture budget").option("--capture-day <date>", "YYYY-MM-DD for capture budget").option("--capture-budget <n>", "daily capture budget", (s) => parseInt(s, 10)).option("--severity <level>", "severity (P0 escapes budget)").option("--skip-budget", "skip capture budget gate", false).option("--checks <list>", "comma list: dirty,mission,budget,all", "all").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
8090
+ const v = await resolveVaultArg(vault, opts.wiki);
8091
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8092
+ else emit(await runWritePreflightCommand({
8093
+ vault: v.vault,
8094
+ command: opts.command,
8095
+ dirtyThreshold: opts.dirtyThreshold,
8096
+ skipDirty: !!opts.skipDirty,
8097
+ priorArtifactFile: opts.priorArtifactFile,
8098
+ priorArtifactText: opts.priorArtifactText,
8099
+ consecutiveNoNewDecision: opts.consecutiveNoDecision,
8100
+ noDecisionThreshold: opts.noDecisionThreshold,
8101
+ humanAllow: !!opts.humanAllow,
8102
+ missionKind: opts.missionKind,
8103
+ skipMission: !!opts.skipMission,
8104
+ project: opts.project,
8105
+ captureDay: opts.captureDay,
8106
+ captureBudget: opts.captureBudget,
8107
+ severity: opts.severity,
8108
+ skipBudget: !!opts.skipBudget,
8109
+ checks: opts.checks
8110
+ }), void 0, { postCommit: false });
8111
+ });
7988
8112
  program.command("mcp").description("start stdio Model Context Protocol server (read-only vault tools)").action(async () => {
7989
8113
  await runSkillwikiMcpStdio();
7990
8114
  });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-AVCGIWSL.js";
4
+ } from "./chunk-ZFEZZQFM.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
  import "./chunk-U34B2XQJ.js";
7
7
  import "./chunk-TYN2IHBY.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
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.9",
3
+ "version": "0.10.10",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",