skillwiki 0.8.5-beta.4 → 0.8.5-beta.5

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/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-TPS5XD2J.js";
15
15
 
16
16
  // src/cli.ts
17
- import { join as join44 } from "path";
17
+ import { join as join45 } from "path";
18
18
  import { Command as Command2 } from "commander";
19
19
 
20
20
  // ../shared/src/exit-codes.ts
@@ -3047,8 +3047,9 @@ function buildCliSurface() {
3047
3047
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
3048
3048
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
3049
3049
  program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
3050
- program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
3050
+ program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--summary").option("--examples <n>").option("--wiki <name>");
3051
3051
  program2.command("config");
3052
+ program2.command("health").option("--wiki <name>").option("--sync <mode>").option("--no-fail").option("--out <path>").option("--examples <n>");
3052
3053
  program2.command("doctor");
3053
3054
  program2.command("status").option("--wiki <name>");
3054
3055
  program2.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
@@ -3271,6 +3272,58 @@ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_source_identi
3271
3272
  var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
3272
3273
  var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3273
3274
  var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3275
+ function shellQuote(value) {
3276
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3277
+ }
3278
+ function formatExample(item) {
3279
+ if (typeof item === "string") return item;
3280
+ if (item === null || item === void 0) return String(item);
3281
+ try {
3282
+ return JSON.stringify(item);
3283
+ } catch {
3284
+ return String(item);
3285
+ }
3286
+ }
3287
+ function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
3288
+ const safeLimit = Math.max(0, Math.min(examplesLimit, 10));
3289
+ const examples = bucket.items.slice(0, safeLimit).map(formatExample);
3290
+ return {
3291
+ kind: bucket.kind,
3292
+ severity,
3293
+ count: bucket.items.length,
3294
+ examples,
3295
+ examples_limit: safeLimit,
3296
+ sample_truncated: bucket.items.length > examples.length,
3297
+ details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
3298
+ };
3299
+ }
3300
+ function summarizeLintOutput(output, examplesLimit = 3) {
3301
+ const buckets = [
3302
+ ...output.by_severity.error.map((bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)),
3303
+ ...output.by_severity.warning.map((bucket) => summarizeBucket(bucket, "warning", output.vault.path, examplesLimit)),
3304
+ ...output.by_severity.info.map((bucket) => summarizeBucket(bucket, "info", output.vault.path, examplesLimit))
3305
+ ];
3306
+ const lines = [];
3307
+ lines.push(`errors: ${output.summary.errors}`);
3308
+ lines.push(`warnings: ${output.summary.warnings}`);
3309
+ lines.push(`info: ${output.summary.info}`);
3310
+ for (const bucket of buckets) {
3311
+ lines.push(` ${bucket.kind}: ${bucket.count}`);
3312
+ if (bucket.examples.length > 0) {
3313
+ lines.push(` e.g. ${bucket.examples[0]}`);
3314
+ }
3315
+ }
3316
+ return {
3317
+ vault: output.vault,
3318
+ summary: output.summary,
3319
+ buckets,
3320
+ details_included: false,
3321
+ truncated: false,
3322
+ fixed: output.fixed,
3323
+ unresolved: output.unresolved,
3324
+ humanHint: lines.join("\n")
3325
+ };
3326
+ }
3274
3327
  async function runLint(input) {
3275
3328
  if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
3276
3329
  return {
@@ -3903,17 +3956,18 @@ ${newBody}`;
3903
3956
  let fExit = ExitCode.OK;
3904
3957
  if (fSummary.errors > 0) fExit = ExitCode.LINT_HAS_ERRORS;
3905
3958
  else if (fSummary.warnings > 0 || fSummary.info > 0) fExit = ExitCode.LINT_HAS_WARNINGS;
3959
+ const output2 = {
3960
+ vault: { path: input.vault, source: input.source ?? "resolved" },
3961
+ summary: fSummary,
3962
+ by_severity: filtered,
3963
+ fixed,
3964
+ unresolved,
3965
+ humanHint: `--only ${input.only}
3966
+ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
3967
+ };
3906
3968
  return {
3907
3969
  exitCode: fExit,
3908
- result: ok({
3909
- vault: { path: input.vault, source: input.source ?? "resolved" },
3910
- summary: fSummary,
3911
- by_severity: filtered,
3912
- fixed,
3913
- unresolved,
3914
- humanHint: `--only ${input.only}
3915
- ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
3916
- })
3970
+ result: ok(input.summary ? summarizeLintOutput(output2, input.examplesLimit) : output2)
3917
3971
  };
3918
3972
  }
3919
3973
  const summary = {
@@ -3941,19 +3995,31 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
3941
3995
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3942
3996
  });
3943
3997
  }
3998
+ const output = {
3999
+ vault: { path: input.vault, source: input.source ?? "resolved" },
4000
+ summary,
4001
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
4002
+ fixed,
4003
+ unresolved,
4004
+ humanHint: hintLines.join("\n")
4005
+ };
3944
4006
  return {
3945
4007
  exitCode,
3946
- result: ok({
3947
- vault: { path: input.vault, source: input.source ?? "resolved" },
3948
- summary,
3949
- by_severity: { error: errorOut, warning: warningOut, info: infoOut },
3950
- fixed,
3951
- unresolved,
3952
- humanHint: hintLines.join("\n")
3953
- })
4008
+ result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
3954
4009
  };
3955
4010
  }
3956
4011
 
4012
+ // src/commands/health.ts
4013
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4014
+ import { dirname as dirname10, join as join28, resolve as resolve6 } from "path";
4015
+ import { platform as platform3 } from "os";
4016
+
4017
+ // src/commands/doctor.ts
4018
+ import { existsSync as existsSync10, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4019
+ import { join as join27, resolve as resolve5 } from "path";
4020
+ import { execSync as execSync2 } from "child_process";
4021
+ import { platform as platform2 } from "os";
4022
+
3957
4023
  // src/commands/config.ts
3958
4024
  import { readFile as readFile18 } from "fs/promises";
3959
4025
  import { existsSync as existsSync6 } from "fs";
@@ -4015,12 +4081,6 @@ async function runConfigPath(input) {
4015
4081
  return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
4016
4082
  }
4017
4083
 
4018
- // src/commands/doctor.ts
4019
- import { existsSync as existsSync10, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4020
- import { join as join27, resolve as resolve5 } from "path";
4021
- import { execSync as execSync2 } from "child_process";
4022
- import { platform as platform2 } from "os";
4023
-
4024
4084
  // src/utils/auto-update.ts
4025
4085
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
4026
4086
  import { join as join24, dirname as dirname9 } from "path";
@@ -5623,9 +5683,383 @@ async function runDoctor(input) {
5623
5683
  return { exitCode, result: ok({ checks, summary, humanHint }) };
5624
5684
  }
5625
5685
 
5686
+ // src/commands/health.ts
5687
+ function statusFromCounts(counts) {
5688
+ if ((counts.error ?? 0) > 0) return "error";
5689
+ if ((counts.warn ?? counts.warnings ?? 0) > 0) return "warn";
5690
+ if ((counts.info ?? 0) > 0) return "info";
5691
+ return "pass";
5692
+ }
5693
+ function maxStatus(statuses) {
5694
+ const order = { pass: 0, info: 1, warn: 2, error: 3, unknown: 4 };
5695
+ return statuses.reduce((max, status) => order[status] > order[max] ? status : max, "pass");
5696
+ }
5697
+ function bucketCount(buckets, kind) {
5698
+ return buckets.find((bucket) => bucket.kind === kind)?.count ?? 0;
5699
+ }
5700
+ function bucketEvidence(prefix, buckets, kinds) {
5701
+ return kinds.map((kind) => ({ kind, count: bucketCount(buckets, kind) })).filter((item) => item.count > 0).map((item) => `${prefix}.${item.kind}: ${item.count}`);
5702
+ }
5703
+ function shellQuote2(value) {
5704
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5705
+ }
5706
+ function lintCommand(vaultPath, args) {
5707
+ return `skillwiki lint ${shellQuote2(vaultPath)} ${args}`;
5708
+ }
5709
+ function deriveRiskFlags(lint, vaultSync, syncMode) {
5710
+ const flags = [];
5711
+ const integrityEvidence = bucketEvidence("lint", lint.buckets, [
5712
+ "broken_sources",
5713
+ "invalid_frontmatter",
5714
+ "broken_wikilinks",
5715
+ "raw_source_identity_conflict"
5716
+ ]);
5717
+ if (integrityEvidence.length > 0) {
5718
+ flags.push({
5719
+ id: "content_integrity_risk",
5720
+ status: "error",
5721
+ blocking: true,
5722
+ evidence: integrityEvidence,
5723
+ suggested_commands: [
5724
+ lintCommand(lint.vault.path, "--summary"),
5725
+ lintCommand(lint.vault.path, "--only broken_sources")
5726
+ ]
5727
+ });
5728
+ }
5729
+ const retrievalEvidence = bucketEvidence("lint", lint.buckets, [
5730
+ "tag_not_in_taxonomy",
5731
+ "orphans",
5732
+ "orphaned_project_pages",
5733
+ "missing_overview"
5734
+ ]);
5735
+ if (retrievalEvidence.length > 0) {
5736
+ const hasError = bucketCount(lint.buckets, "tag_not_in_taxonomy") > 0;
5737
+ flags.push({
5738
+ id: "retrieval_quality_risk",
5739
+ status: hasError ? "error" : "warn",
5740
+ blocking: hasError,
5741
+ evidence: retrievalEvidence,
5742
+ suggested_commands: [lintCommand(lint.vault.path, "--summary")]
5743
+ });
5744
+ }
5745
+ const duplicationEvidence = bucketEvidence("lint", lint.buckets, [
5746
+ "raw_dedup",
5747
+ "raw_body_duplicate",
5748
+ "raw_subdirectory_duplicate"
5749
+ ]);
5750
+ if (duplicationEvidence.length > 0) {
5751
+ flags.push({
5752
+ id: "content_duplication_risk",
5753
+ status: "warn",
5754
+ blocking: false,
5755
+ evidence: duplicationEvidence,
5756
+ suggested_commands: [lintCommand(lint.vault.path, "--only raw_dedup")]
5757
+ });
5758
+ }
5759
+ const syncEvidence = vaultSync.checks.filter((check2) => check2.status === "error" || check2.status === "warn").map((check2) => `vault_sync.${check2.id}: ${check2.status}`);
5760
+ if (syncEvidence.length > 0) {
5761
+ flags.push({
5762
+ id: "sync_visibility_risk",
5763
+ status: vaultSync.status === "error" ? "error" : "warn",
5764
+ blocking: syncMode === "required",
5765
+ evidence: syncEvidence,
5766
+ suggested_commands: ["vault-sync-install"]
5767
+ });
5768
+ }
5769
+ const backlogEvidence = bucketEvidence("lint", lint.buckets, [
5770
+ "stale_page",
5771
+ "page_too_large",
5772
+ "work_item_health",
5773
+ "log_rotate_needed"
5774
+ ]);
5775
+ if (backlogEvidence.length > 0) {
5776
+ flags.push({
5777
+ id: "maintenance_backlog",
5778
+ status: "warn",
5779
+ blocking: false,
5780
+ evidence: backlogEvidence,
5781
+ suggested_commands: [lintCommand(lint.vault.path, "--summary")]
5782
+ });
5783
+ }
5784
+ return flags;
5785
+ }
5786
+ function summarizeChecks(checks) {
5787
+ return {
5788
+ pass: checks.filter((check2) => check2.status === "pass").length,
5789
+ info: checks.filter((check2) => check2.status === "info").length,
5790
+ warn: checks.filter((check2) => check2.status === "warn").length,
5791
+ error: checks.filter((check2) => check2.status === "error").length,
5792
+ skipped: checks.filter((check2) => /skipped/i.test(check2.detail)).length
5793
+ };
5794
+ }
5795
+ function classifyLog(path, id, label, okPattern) {
5796
+ if (!existsSync11(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
5797
+ const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter(Boolean);
5798
+ const last = lines[lines.length - 1] ?? "";
5799
+ if (!last) return { id, label, status: "warn", detail: `log file empty: ${path}` };
5800
+ if (/fail|error/i.test(last)) return { id, label, status: "error", detail: last.slice(0, 120) };
5801
+ if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
5802
+ return { id, label, status: "warn", detail: last.slice(0, 120) };
5803
+ }
5804
+ function runVaultSyncHealth(home, syncMode) {
5805
+ if (syncMode === "off") {
5806
+ return {
5807
+ status: "pass",
5808
+ blocking: false,
5809
+ installed: false,
5810
+ summary: { pass: 0, info: 0, warn: 0, error: 0, skipped: 1 },
5811
+ checks: [{ id: "vault_sync_skipped", status: "pass", detail: "vault-sync checks skipped by --sync off" }]
5812
+ };
5813
+ }
5814
+ const isMac = platform3() === "darwin";
5815
+ const shareDir = isMac ? join28(home, "Library", "Application Support", "vault-sync", "bin") : join28(home, ".local", "share", "vault-sync", "bin");
5816
+ const logDir = isMac ? join28(home, "Library", "Logs") : join28(home, ".local", "state", "vault-sync", "log");
5817
+ const filterPath = join28(home, ".config", "rclone", "wiki-push-filters.txt");
5818
+ const checks = [];
5819
+ const pushScript = join28(shareDir, "wiki-push.sh");
5820
+ checks.push(existsSync11(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
5821
+ if (isMac) {
5822
+ const pushPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
5823
+ const fetchPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
5824
+ checks.push(existsSync11(pushPlist) && existsSync11(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
5825
+ checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
5826
+ } else {
5827
+ const pushTimer = join28(home, ".config", "systemd", "user", "wiki-push.timer");
5828
+ const fetchTimer = join28(home, ".config", "systemd", "user", "wiki-fetch.timer");
5829
+ const fuseTimer = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
5830
+ const fuseService = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
5831
+ checks.push(existsSync11(pushTimer) && existsSync11(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
5832
+ checks.push(existsSync11(fuseTimer) && existsSync11(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
5833
+ }
5834
+ checks.push(classifyLog(join28(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
5835
+ checks.push(classifyLog(join28(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
5836
+ if (!existsSync11(filterPath)) {
5837
+ checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
5838
+ } else {
5839
+ const content = readFileSync8(filterPath, "utf8");
5840
+ const missing = ["remotely-save/data.json", ".skillwiki/sync.lock", ".claude/settings.local.json"].filter((item) => !content.includes(item));
5841
+ checks.push(missing.length > 0 ? { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "warn", detail: `Missing excludes: ${missing.join(", ")}` } : { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "pass", detail: "Required excludes present" });
5842
+ }
5843
+ checks.push({ id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "pass", detail: "Not a snapshotter host \u2014 check skipped" });
5844
+ const summary = summarizeChecks(checks);
5845
+ return {
5846
+ status: statusFromCounts(summary),
5847
+ blocking: syncMode === "required",
5848
+ installed: checks.find((check2) => check2.id === "vault_sync_installed")?.status === "pass",
5849
+ summary,
5850
+ checks: checks.map((check2) => ({ id: check2.id, status: check2.status, detail: check2.detail }))
5851
+ };
5852
+ }
5853
+ function deriveQueryReadiness(lint) {
5854
+ const taxonomyErrors = bucketCount(lint.buckets, "tag_not_in_taxonomy");
5855
+ const brokenLinks = bucketCount(lint.buckets, "broken_wikilinks") + bucketCount(lint.buckets, "broken_sources");
5856
+ const orphanCount = bucketCount(lint.buckets, "orphans") + bucketCount(lint.buckets, "orphaned_project_pages");
5857
+ const missingOverview = bucketCount(lint.buckets, "missing_overview");
5858
+ const missingTldr = bucketCount(lint.buckets, "missing_tldr");
5859
+ let score = 100;
5860
+ score -= Math.min(40, taxonomyErrors * 2);
5861
+ score -= Math.min(30, brokenLinks * 10);
5862
+ score -= Math.min(20, orphanCount * 3);
5863
+ score -= Math.min(15, missingOverview);
5864
+ score -= Math.min(5, missingTldr);
5865
+ score = Math.max(0, score);
5866
+ const status = taxonomyErrors > 0 || brokenLinks > 0 ? "error" : score < 80 ? "warn" : "pass";
5867
+ return {
5868
+ status,
5869
+ blocking: false,
5870
+ summary: { score },
5871
+ signals: [
5872
+ { id: "taxonomy_errors", status: taxonomyErrors > 0 ? "error" : "pass", value: taxonomyErrors },
5873
+ { id: "broken_links", status: brokenLinks > 0 ? "error" : "pass", value: brokenLinks },
5874
+ { id: "orphan_count", status: orphanCount > 0 ? "warn" : "pass", value: orphanCount },
5875
+ { id: "missing_overview", status: missingOverview > 0 ? "warn" : "pass", value: missingOverview },
5876
+ { id: "missing_tldr", status: missingTldr > 0 ? "info" : "pass", value: missingTldr }
5877
+ ]
5878
+ };
5879
+ }
5880
+ function selfCheckReport(report) {
5881
+ const errors = [];
5882
+ const lint = report.components.lint;
5883
+ const sumBySeverity = (severity) => lint.buckets.filter((bucket) => bucket.severity === severity).reduce((sum, bucket) => sum + bucket.count, 0);
5884
+ if (lint.summary.errors !== sumBySeverity("error")) {
5885
+ errors.push(`lint.summary.errors=${lint.summary.errors} but error bucket counts sum to ${sumBySeverity("error")}`);
5886
+ }
5887
+ if (lint.summary.warnings !== sumBySeverity("warning")) {
5888
+ errors.push(`lint.summary.warnings=${lint.summary.warnings} but warning bucket counts sum to ${sumBySeverity("warning")}`);
5889
+ }
5890
+ if (lint.summary.info !== sumBySeverity("info")) {
5891
+ errors.push(`lint.summary.info=${lint.summary.info} but info bucket counts sum to ${sumBySeverity("info")}`);
5892
+ }
5893
+ for (const component of ["doctor", "lint", "vault_sync", "query_readiness", "source_freshness"]) {
5894
+ if (!report.coverage[component]) errors.push(`missing coverage entry for ${component}`);
5895
+ }
5896
+ for (const flag of report.risk_flags) {
5897
+ if (flag.evidence.length === 0) errors.push(`risk flag ${flag.id} has no evidence`);
5898
+ }
5899
+ return { status: errors.length > 0 ? "error" : "pass", errors };
5900
+ }
5901
+ function buildHumanHint(report) {
5902
+ const lines = [
5903
+ `vault: ${report.vault.path} (via ${report.vault.source})`,
5904
+ `overall: ${report.overall_status}`,
5905
+ `doctor: ${report.components.doctor.summary.pass} pass, ${report.components.doctor.summary.info} info, ${report.components.doctor.summary.warn} warn, ${report.components.doctor.summary.error} error`,
5906
+ `lint: ${report.components.lint.summary.errors} errors, ${report.components.lint.summary.warnings} warnings, ${report.components.lint.summary.info} info`
5907
+ ];
5908
+ for (const bucket of report.components.lint.buckets) {
5909
+ lines.push(` ${bucket.severity} ${bucket.kind}: ${bucket.count}`);
5910
+ }
5911
+ lines.push(`vault_sync: ${report.components.vault_sync.summary.pass} pass, ${report.components.vault_sync.summary.warn} warn, ${report.components.vault_sync.summary.error} error`);
5912
+ lines.push(`query_readiness: ${report.components.query_readiness.status} (${report.components.query_readiness.summary.score}/100)`);
5913
+ if (report.risk_flags.length > 0) {
5914
+ lines.push("risk flags:");
5915
+ for (const flag of report.risk_flags) {
5916
+ lines.push(` ${flag.id}: ${flag.status}${flag.blocking ? " (blocking)" : ""}`);
5917
+ }
5918
+ }
5919
+ if (report.self_check.status === "error") {
5920
+ lines.push("self-check: error");
5921
+ for (const error of report.self_check.errors) lines.push(` ${error}`);
5922
+ }
5923
+ return lines.join("\n");
5924
+ }
5925
+ function writeReport(out, report) {
5926
+ mkdirSync4(dirname10(out), { recursive: true });
5927
+ const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
5928
+ writeFileSync6(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
5929
+ renameSync(tmp, out);
5930
+ }
5931
+ async function runHealth(input) {
5932
+ const syncMode = input.sync ?? "optional";
5933
+ const doctor = await runDoctor({
5934
+ home: input.home,
5935
+ envValue: input.envValue,
5936
+ argv: input.argv,
5937
+ currentVersion: input.currentVersion,
5938
+ cwd: input.cwd
5939
+ });
5940
+ const lint = await runLint({
5941
+ vault: input.vault,
5942
+ source: input.vaultSource ?? "resolved",
5943
+ days: 90,
5944
+ lines: 200,
5945
+ logThreshold: 500,
5946
+ summary: true,
5947
+ examplesLimit: input.examplesLimit ?? 3
5948
+ });
5949
+ if (!doctor.result.ok) return { exitCode: doctor.exitCode, result: doctor.result };
5950
+ if (!lint.result.ok) return { exitCode: lint.exitCode, result: lint.result };
5951
+ const doctorStatus = statusFromCounts(doctor.result.data.summary);
5952
+ const vaultSync = runVaultSyncHealth(input.home, syncMode);
5953
+ const queryReadiness = deriveQueryReadiness(lint.result.data);
5954
+ const sourceFreshness = {
5955
+ status: bucketCount(lint.result.data.buckets, "stale_page") > 0 || bucketCount(lint.result.data.buckets, "file_source_url") > 0 ? "warn" : "pass",
5956
+ blocking: false,
5957
+ summary: {
5958
+ stale_pages: bucketCount(lint.result.data.buckets, "stale_page"),
5959
+ file_source_url: bucketCount(lint.result.data.buckets, "file_source_url"),
5960
+ stale_sections: bucketCount(lint.result.data.buckets, "stale_sections")
5961
+ }
5962
+ };
5963
+ const doctorComponent = {
5964
+ status: doctorStatus,
5965
+ blocking: false,
5966
+ summary: doctor.result.data.summary,
5967
+ checks: doctor.result.data.checks.map((check2) => ({ id: check2.id, status: check2.status, detail: check2.detail }))
5968
+ };
5969
+ const lintComponent = {
5970
+ status: statusFromCounts({ error: lint.result.data.summary.errors, warnings: lint.result.data.summary.warnings, info: lint.result.data.summary.info }),
5971
+ blocking: true,
5972
+ summary: lint.result.data.summary,
5973
+ buckets: lint.result.data.buckets,
5974
+ details_included: false,
5975
+ truncated: false
5976
+ };
5977
+ const riskFlags = deriveRiskFlags(lint.result.data, vaultSync, syncMode);
5978
+ const blockingStatuses = [
5979
+ lintComponent.blocking ? lintComponent.status : "pass",
5980
+ vaultSync.blocking ? vaultSync.status : "pass",
5981
+ ...riskFlags.filter((flag) => flag.blocking).map((flag) => flag.status)
5982
+ ];
5983
+ const advisoryStatuses = [
5984
+ doctorComponent.status,
5985
+ vaultSync.status,
5986
+ queryReadiness.status,
5987
+ sourceFreshness.status,
5988
+ ...riskFlags.filter((flag) => !flag.blocking).map((flag) => flag.status)
5989
+ ];
5990
+ const blockingStatus = maxStatus(blockingStatuses);
5991
+ const advisoryStatus = maxStatus(advisoryStatuses);
5992
+ const baseReport = {
5993
+ schema_version: 1,
5994
+ policy_version: 1,
5995
+ tool: { name: "skillwiki", version: input.currentVersion },
5996
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5997
+ command_kind: "diagnostic",
5998
+ vault: { path: input.vault, source: input.vaultSource ?? "resolved" },
5999
+ overall_status: maxStatus([blockingStatus, advisoryStatus]),
6000
+ blocking_status: blockingStatus,
6001
+ advisory_status: advisoryStatus,
6002
+ coverage: {
6003
+ doctor: { state: "checked", status: doctorStatus },
6004
+ lint: { state: "checked", status: lintComponent.status },
6005
+ vault_sync: syncMode === "off" ? { state: "skipped", status: "pass", reason: "--sync off" } : { state: "checked", status: vaultSync.status },
6006
+ query_readiness: { state: "checked", status: queryReadiness.status },
6007
+ source_freshness: { state: "checked", status: sourceFreshness.status }
6008
+ },
6009
+ components: {
6010
+ doctor: doctorComponent,
6011
+ lint: lintComponent,
6012
+ vault_sync: vaultSync,
6013
+ query_readiness: queryReadiness,
6014
+ source_freshness: sourceFreshness
6015
+ },
6016
+ risk_flags: riskFlags,
6017
+ details_included: false,
6018
+ truncated: false,
6019
+ mutated: false,
6020
+ post_commit_ran: false,
6021
+ report_complete: true,
6022
+ report_written: false,
6023
+ warnings: []
6024
+ };
6025
+ if (input.out) {
6026
+ baseReport.report_path = input.out;
6027
+ }
6028
+ if (input.out && resolve6(input.out).startsWith(resolve6(input.vault) + "/")) {
6029
+ baseReport.warnings.push({
6030
+ id: "report_inside_vault",
6031
+ message: "health report was written inside the vault; this may create sync churn"
6032
+ });
6033
+ }
6034
+ const self_check = selfCheckReport(baseReport);
6035
+ const reportWithoutHint = {
6036
+ ...baseReport,
6037
+ self_check,
6038
+ overall_status: self_check.status === "error" ? "unknown" : baseReport.overall_status,
6039
+ report_complete: self_check.status === "error" ? false : baseReport.report_complete
6040
+ };
6041
+ const report = {
6042
+ ...reportWithoutHint,
6043
+ humanHint: buildHumanHint(reportWithoutHint)
6044
+ };
6045
+ if (input.out) {
6046
+ report.report_written = true;
6047
+ writeReport(input.out, report);
6048
+ }
6049
+ let exitCode = ExitCode.OK;
6050
+ if (!input.noFail) {
6051
+ if (report.self_check.status === "error") exitCode = ExitCode.INTERNAL_ERROR;
6052
+ else if (report.blocking_status === "error") exitCode = ExitCode.LINT_HAS_ERRORS;
6053
+ else if (report.blocking_status === "warn" || report.advisory_status === "warn" || report.advisory_status === "error") {
6054
+ exitCode = ExitCode.LINT_HAS_WARNINGS;
6055
+ }
6056
+ }
6057
+ return { exitCode, result: ok(report) };
6058
+ }
6059
+
5626
6060
  // src/commands/archive.ts
5627
6061
  import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5628
- import { join as join28, dirname as dirname10 } from "path";
6062
+ import { join as join29, dirname as dirname11 } from "path";
5629
6063
  function countWikilinks(body, slug) {
5630
6064
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5631
6065
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -5653,7 +6087,7 @@ async function runArchive(input) {
5653
6087
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
5654
6088
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
5655
6089
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
5656
- const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
6090
+ const archivePath = join29("_archive", relPath).replace(/\\/g, "/");
5657
6091
  let cascade;
5658
6092
  if (input.cascade) {
5659
6093
  const wikilinkRefs = [];
@@ -5677,7 +6111,7 @@ async function runArchive(input) {
5677
6111
  const indexRefs = [];
5678
6112
  if (!isRaw) {
5679
6113
  try {
5680
- const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
6114
+ const idx = await readFile20(join29(input.vault, "index.md"), "utf8");
5681
6115
  idx.split("\n").forEach((line, i) => {
5682
6116
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
5683
6117
  });
@@ -5703,7 +6137,7 @@ async function runArchive(input) {
5703
6137
  }
5704
6138
  if (input.cascade && input.apply && cascade) {
5705
6139
  for (const ref of cascade.source_array_refs) {
5706
- const absPath = join28(input.vault, ref.page);
6140
+ const absPath = join29(input.vault, ref.page);
5707
6141
  const text = await readFile20(absPath, "utf8");
5708
6142
  const split = splitFrontmatter(text);
5709
6143
  if (!split.ok) continue;
@@ -5721,10 +6155,10 @@ ${fmRewritten}
5721
6155
  }
5722
6156
  }
5723
6157
  }
5724
- await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
6158
+ await mkdir9(dirname11(join29(input.vault, archivePath)), { recursive: true });
5725
6159
  let indexUpdated = false;
5726
6160
  if (!isRaw) {
5727
- const indexPath = join28(input.vault, "index.md");
6161
+ const indexPath = join29(input.vault, "index.md");
5728
6162
  try {
5729
6163
  const idx = await readFile20(indexPath, "utf8");
5730
6164
  const originalLines = idx.split("\n");
@@ -5737,7 +6171,7 @@ ${fmRewritten}
5737
6171
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
5738
6172
  }
5739
6173
  }
5740
- await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
6174
+ await rename7(join29(input.vault, relPath), join29(input.vault, archivePath));
5741
6175
  appendLastOp(input.vault, {
5742
6176
  operation: input.cascade ? "archive-cascade" : "archive",
5743
6177
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -6146,10 +6580,10 @@ ${newBody}`;
6146
6580
 
6147
6581
  // src/commands/update.ts
6148
6582
  import { execSync as execSync3 } from "child_process";
6149
- import { join as join29 } from "path";
6583
+ import { join as join30 } from "path";
6150
6584
 
6151
6585
  // src/utils/package-info.ts
6152
- import { readFileSync as readFileSync8 } from "fs";
6586
+ import { readFileSync as readFileSync9 } from "fs";
6153
6587
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
6154
6588
  return [
6155
6589
  new URL("../package.json", baseUrl),
@@ -6159,7 +6593,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
6159
6593
  function readCliPackageJson(baseUrl = import.meta.url) {
6160
6594
  for (const url of packageJsonCandidateUrls(baseUrl)) {
6161
6595
  try {
6162
- const pkg2 = JSON.parse(readFileSync8(url, "utf8"));
6596
+ const pkg2 = JSON.parse(readFileSync9(url, "utf8"));
6163
6597
  if (typeof pkg2.version === "string") {
6164
6598
  return { ...pkg2, version: pkg2.version };
6165
6599
  }
@@ -6176,7 +6610,7 @@ function resolveGlobalSkillsRoot() {
6176
6610
  encoding: "utf8",
6177
6611
  timeout: 5e3
6178
6612
  }).trim();
6179
- return join29(globalRoot, "skillwiki", "skills");
6613
+ return join30(globalRoot, "skillwiki", "skills");
6180
6614
  } catch {
6181
6615
  return null;
6182
6616
  }
@@ -6200,7 +6634,7 @@ async function runUpdate(input) {
6200
6634
  const pkg2 = readCliPackageJson();
6201
6635
  const currentVersion = pkg2.version;
6202
6636
  const tag = normalizeDistTag(input.distTag);
6203
- const target = join29(input.home, ".claude", "skills");
6637
+ const target = join30(input.home, ".claude", "skills");
6204
6638
  let latest;
6205
6639
  try {
6206
6640
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -6271,22 +6705,22 @@ async function runUpdate(input) {
6271
6705
 
6272
6706
  // src/commands/self-update.ts
6273
6707
  import { execSync as execSync4 } from "child_process";
6274
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6275
- import { join as join30 } from "path";
6708
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
6709
+ import { join as join31 } from "path";
6276
6710
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
6277
6711
  async function runSelfUpdate(input) {
6278
6712
  const currentVersion = readCliPackageJson().version;
6279
6713
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
6280
6714
  const distTag = normalizeDistTag(input.distTag);
6281
- const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
6282
- const hasLocalSource = existsSync11(localPkgPath);
6715
+ const localPkgPath = join31(sourceRoot, "packages", "cli", "package.json");
6716
+ const hasLocalSource = existsSync12(localPkgPath);
6283
6717
  if (input.check) {
6284
6718
  let availableVersion = null;
6285
6719
  let source;
6286
6720
  if (hasLocalSource) {
6287
6721
  source = "local";
6288
6722
  try {
6289
- availableVersion = JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? null;
6723
+ availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
6290
6724
  } catch {
6291
6725
  availableVersion = null;
6292
6726
  }
@@ -6344,7 +6778,7 @@ async function runSelfUpdate(input) {
6344
6778
  }
6345
6779
  const newVersion = (() => {
6346
6780
  try {
6347
- return JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? "unknown";
6781
+ return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
6348
6782
  } catch {
6349
6783
  return "unknown";
6350
6784
  }
@@ -6411,9 +6845,9 @@ async function runSelfUpdate(input) {
6411
6845
 
6412
6846
  // src/commands/transcripts.ts
6413
6847
  import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
6414
- import { join as join31 } from "path";
6848
+ import { join as join32 } from "path";
6415
6849
  async function runTranscripts(input) {
6416
- const dir = join31(input.vault, "raw", "transcripts");
6850
+ const dir = join32(input.vault, "raw", "transcripts");
6417
6851
  let entries;
6418
6852
  try {
6419
6853
  entries = await readdir5(dir, { withFileTypes: true });
@@ -6423,7 +6857,7 @@ async function runTranscripts(input) {
6423
6857
  const transcripts = [];
6424
6858
  for (const entry of entries) {
6425
6859
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6426
- const filePath = join31(dir, entry.name);
6860
+ const filePath = join32(dir, entry.name);
6427
6861
  const content = await readFile21(filePath, "utf8");
6428
6862
  const fm = extractFrontmatter(content);
6429
6863
  if (!fm.ok) continue;
@@ -6442,11 +6876,11 @@ async function runTranscripts(input) {
6442
6876
 
6443
6877
  // src/commands/project-index.ts
6444
6878
  import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
6445
- import { join as join32, dirname as dirname11 } from "path";
6879
+ import { join as join33, dirname as dirname12 } from "path";
6446
6880
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
6447
6881
  async function runProjectIndex(input) {
6448
6882
  const slug = input.slug;
6449
- const projectDir = join32(input.vault, "projects", slug);
6883
+ const projectDir = join33(input.vault, "projects", slug);
6450
6884
  try {
6451
6885
  await readdir6(projectDir);
6452
6886
  } catch {
@@ -6457,12 +6891,12 @@ async function runProjectIndex(input) {
6457
6891
  }
6458
6892
  const wikilinkPattern = `[[${slug}]]`;
6459
6893
  const entries = [];
6460
- const compoundDir = join32(input.vault, "projects", slug, "compound");
6894
+ const compoundDir = join33(input.vault, "projects", slug, "compound");
6461
6895
  try {
6462
6896
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
6463
6897
  for (const entry of compoundFiles) {
6464
6898
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6465
- const filePath = join32(compoundDir, entry.name);
6899
+ const filePath = join33(compoundDir, entry.name);
6466
6900
  let text;
6467
6901
  try {
6468
6902
  text = await readFile22(filePath, "utf8");
@@ -6482,13 +6916,13 @@ async function runProjectIndex(input) {
6482
6916
  for (const dir of LAYER2_DIRS) {
6483
6917
  let files;
6484
6918
  try {
6485
- files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
6919
+ files = await readdir6(join33(input.vault, dir), { withFileTypes: true });
6486
6920
  } catch {
6487
6921
  continue;
6488
6922
  }
6489
6923
  for (const entry of files) {
6490
6924
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6491
- const filePath = join32(input.vault, dir, entry.name);
6925
+ const filePath = join33(input.vault, dir, entry.name);
6492
6926
  let text;
6493
6927
  try {
6494
6928
  text = await readFile22(filePath, "utf8");
@@ -6512,7 +6946,7 @@ async function runProjectIndex(input) {
6512
6946
  const tb = typeOrder[b.type] ?? 99;
6513
6947
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
6514
6948
  });
6515
- const indexPath = join32(projectDir, "knowledge.md");
6949
+ const indexPath = join33(projectDir, "knowledge.md");
6516
6950
  let existing = false;
6517
6951
  let stale = false;
6518
6952
  try {
@@ -6556,7 +6990,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
6556
6990
  }
6557
6991
  if (input.apply) {
6558
6992
  try {
6559
- await mkdir10(dirname11(indexPath), { recursive: true });
6993
+ await mkdir10(dirname12(indexPath), { recursive: true });
6560
6994
  await writeFile11(indexPath, body, "utf8");
6561
6995
  } catch (e) {
6562
6996
  return {
@@ -6586,8 +7020,8 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
6586
7020
 
6587
7021
  // src/commands/compound.ts
6588
7022
  import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
6589
- import { join as join33 } from "path";
6590
- import { existsSync as existsSync12 } from "fs";
7023
+ import { join as join34 } from "path";
7024
+ import { existsSync as existsSync13 } from "fs";
6591
7025
  import { readFile as readFile23 } from "fs/promises";
6592
7026
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
6593
7027
  var FIELD_RE = {
@@ -6686,7 +7120,7 @@ function extractRetroFields(date, cycleName, block) {
6686
7120
  };
6687
7121
  }
6688
7122
  async function runCompound(input) {
6689
- const logPath = join33(input.vault, "log.md");
7123
+ const logPath = join34(input.vault, "log.md");
6690
7124
  let logText;
6691
7125
  try {
6692
7126
  logText = await readFile23(logPath, "utf8");
@@ -6696,7 +7130,7 @@ async function runCompound(input) {
6696
7130
  const entries = parseRetroEntries(logText);
6697
7131
  const promoted = [];
6698
7132
  const skipped = [];
6699
- const compoundDir = join33(input.vault, "projects", input.project, "compound");
7133
+ const compoundDir = join34(input.vault, "projects", input.project, "compound");
6700
7134
  for (const entry of entries) {
6701
7135
  const generalizeValue = entry.generalize.trim();
6702
7136
  if (!/^yes/i.test(generalizeValue)) {
@@ -6704,8 +7138,8 @@ async function runCompound(input) {
6704
7138
  continue;
6705
7139
  }
6706
7140
  const slug = slugify(entry.cycleName);
6707
- const compoundPath = join33(compoundDir, `${slug}.md`);
6708
- if (existsSync12(compoundPath)) {
7141
+ const compoundPath = join34(compoundDir, `${slug}.md`);
7142
+ if (existsSync13(compoundPath)) {
6709
7143
  skipped.push(entry.date);
6710
7144
  continue;
6711
7145
  }
@@ -6743,7 +7177,7 @@ async function runCompound(input) {
6743
7177
  ].join("\n");
6744
7178
  const content = frontmatter + "\n" + body;
6745
7179
  if (!input.dryRun) {
6746
- if (!existsSync12(compoundDir)) {
7180
+ if (!existsSync13(compoundDir)) {
6747
7181
  await mkdir11(compoundDir, { recursive: true });
6748
7182
  }
6749
7183
  await writeFile12(compoundPath, content, "utf8");
@@ -6765,16 +7199,16 @@ async function runCompound(input) {
6765
7199
  };
6766
7200
  }
6767
7201
  async function runCompoundDelete(input) {
6768
- const projectDir = join33(input.vault, "projects", input.project);
6769
- if (!existsSync12(projectDir)) {
7202
+ const projectDir = join34(input.vault, "projects", input.project);
7203
+ if (!existsSync13(projectDir)) {
6770
7204
  return {
6771
7205
  exitCode: ExitCode.PROJECT_NOT_FOUND,
6772
7206
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
6773
7207
  };
6774
7208
  }
6775
7209
  const entryName = input.entry.replace(/\.md$/, "");
6776
- const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
6777
- if (!existsSync12(compoundPath)) {
7210
+ const compoundPath = join34(projectDir, "compound", `${entryName}.md`);
7211
+ if (!existsSync13(compoundPath)) {
6778
7212
  return {
6779
7213
  exitCode: ExitCode.FILE_NOT_FOUND,
6780
7214
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -6807,8 +7241,8 @@ knowledge.md regenerated`
6807
7241
  };
6808
7242
  }
6809
7243
  async function runCompoundList(input) {
6810
- const compoundDir = join33(input.vault, "projects", input.project, "compound");
6811
- if (!existsSync12(compoundDir)) {
7244
+ const compoundDir = join34(input.vault, "projects", input.project, "compound");
7245
+ if (!existsSync13(compoundDir)) {
6812
7246
  return {
6813
7247
  exitCode: ExitCode.OK,
6814
7248
  result: ok({
@@ -6838,7 +7272,7 @@ could not read compound directory`
6838
7272
  const entries = [];
6839
7273
  for (const dirent of dirents) {
6840
7274
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
6841
- const filePath = join33(compoundDir, dirent.name);
7275
+ const filePath = join34(compoundDir, dirent.name);
6842
7276
  let text;
6843
7277
  try {
6844
7278
  text = await readFile23(filePath, "utf8");
@@ -6871,8 +7305,8 @@ no compound entries found`;
6871
7305
 
6872
7306
  // src/commands/observe.ts
6873
7307
  import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
6874
- import { existsSync as existsSync13, statSync as statSync4 } from "fs";
6875
- import { join as join34 } from "path";
7308
+ import { existsSync as existsSync14, statSync as statSync4 } from "fs";
7309
+ import { join as join35 } from "path";
6876
7310
  import { createHash as createHash4 } from "crypto";
6877
7311
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
6878
7312
  function slugify2(text) {
@@ -6895,13 +7329,13 @@ async function runObserve(input) {
6895
7329
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
6896
7330
  };
6897
7331
  }
6898
- if (!existsSync13(input.vault) || !statSync4(input.vault).isDirectory()) {
7332
+ if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
6899
7333
  return {
6900
7334
  exitCode: ExitCode.VAULT_PATH_INVALID,
6901
7335
  result: err("VAULT_PATH_INVALID", { path: input.vault })
6902
7336
  };
6903
7337
  }
6904
- const transcriptsDir = join34(input.vault, "raw", "transcripts");
7338
+ const transcriptsDir = join35(input.vault, "raw", "transcripts");
6905
7339
  try {
6906
7340
  await mkdir12(transcriptsDir, { recursive: true });
6907
7341
  } catch {
@@ -6913,7 +7347,7 @@ async function runObserve(input) {
6913
7347
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6914
7348
  const slug = slugify2(input.text);
6915
7349
  const fileName = `${today}-observation-${slug}.md`;
6916
- const filePath = join34(transcriptsDir, fileName);
7350
+ const filePath = join35(transcriptsDir, fileName);
6917
7351
  const body = `
6918
7352
  ${input.text.trim()}
6919
7353
  `;
@@ -6954,7 +7388,7 @@ ${input.text.trim()}
6954
7388
 
6955
7389
  // src/commands/ingest.ts
6956
7390
  import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
6957
- import { join as join35 } from "path";
7391
+ import { join as join36 } from "path";
6958
7392
  import { createHash as createHash5 } from "crypto";
6959
7393
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
6960
7394
  var TYPE_DIR = {
@@ -7128,8 +7562,8 @@ async function runIngest(input) {
7128
7562
  const rawRelPath = `raw/articles/${slug}.md`;
7129
7563
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
7130
7564
  const typedRelPath = `${typedDir}/${slug}.md`;
7131
- const rawAbsPath = join35(input.vault, rawRelPath);
7132
- const typedAbsPath = join35(input.vault, typedRelPath);
7565
+ const rawAbsPath = join36(input.vault, rawRelPath);
7566
+ const typedAbsPath = join36(input.vault, typedRelPath);
7133
7567
  const identity = assessSourceIdentity({
7134
7568
  rawPath: rawRelPath,
7135
7569
  sourceUrl: sourceUrl ?? void 0,
@@ -7211,7 +7645,7 @@ async function runIngest(input) {
7211
7645
  };
7212
7646
  }
7213
7647
  try {
7214
- await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
7648
+ await mkdir13(join36(input.vault, "raw", "articles"), { recursive: true });
7215
7649
  await writeFile14(rawAbsPath, rawContent, "utf8");
7216
7650
  } catch (e) {
7217
7651
  return {
@@ -7220,7 +7654,7 @@ async function runIngest(input) {
7220
7654
  };
7221
7655
  }
7222
7656
  try {
7223
- await mkdir13(join35(input.vault, typedDir), { recursive: true });
7657
+ await mkdir13(join36(input.vault, typedDir), { recursive: true });
7224
7658
  await writeFile14(typedAbsPath, typedContent, "utf8");
7225
7659
  } catch (e) {
7226
7660
  return {
@@ -7399,12 +7833,12 @@ ${body}`;
7399
7833
  }
7400
7834
 
7401
7835
  // src/commands/sync.ts
7402
- import { existsSync as existsSync15 } from "fs";
7403
- import { join as join37 } from "path";
7836
+ import { existsSync as existsSync16 } from "fs";
7837
+ import { join as join38 } from "path";
7404
7838
 
7405
7839
  // src/utils/sync-lock.ts
7406
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
7407
- import { join as join36 } from "path";
7840
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
7841
+ import { join as join37 } from "path";
7408
7842
  import { createHash as createHash6 } from "crypto";
7409
7843
  function getSessionId() {
7410
7844
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
@@ -7412,13 +7846,13 @@ function getSessionId() {
7412
7846
  return process.pid.toString();
7413
7847
  }
7414
7848
  function lockPath(vault) {
7415
- return join36(vault, ".skillwiki", "sync.lock");
7849
+ return join37(vault, ".skillwiki", "sync.lock");
7416
7850
  }
7417
7851
  function readLock(vault) {
7418
7852
  const path = lockPath(vault);
7419
- if (!existsSync14(path)) return null;
7853
+ if (!existsSync15(path)) return null;
7420
7854
  try {
7421
- const raw = readFileSync10(path, "utf8");
7855
+ const raw = readFileSync11(path, "utf8");
7422
7856
  return JSON.parse(raw);
7423
7857
  } catch {
7424
7858
  return null;
@@ -7431,9 +7865,9 @@ function isStale(lock, now) {
7431
7865
  }
7432
7866
  function acquireLock(vault, opts = {}) {
7433
7867
  const path = lockPath(vault);
7434
- const dir = join36(vault, ".skillwiki");
7435
- if (!existsSync14(dir)) {
7436
- mkdirSync4(dir, { recursive: true });
7868
+ const dir = join37(vault, ".skillwiki");
7869
+ if (!existsSync15(dir)) {
7870
+ mkdirSync5(dir, { recursive: true });
7437
7871
  }
7438
7872
  const sessionId = opts.sessionId ?? getSessionId();
7439
7873
  const summary = opts.summary ?? "skillwiki sync";
@@ -7452,7 +7886,7 @@ function acquireLock(vault, opts = {}) {
7452
7886
  };
7453
7887
  try {
7454
7888
  const content = JSON.stringify(lock, null, 2) + "\n";
7455
- writeFileSync6(path, content, { flag: "wx" });
7889
+ writeFileSync7(path, content, { flag: "wx" });
7456
7890
  return { ok: true, lock };
7457
7891
  } catch (e) {
7458
7892
  const err3 = e;
@@ -7472,12 +7906,12 @@ function acquireLock(vault, opts = {}) {
7472
7906
  function writeLockedFile(path, lock) {
7473
7907
  const tmp = path + ".tmp";
7474
7908
  const content = JSON.stringify(lock, null, 2) + "\n";
7475
- writeFileSync6(tmp, content);
7476
- renameSync(tmp, path);
7909
+ writeFileSync7(tmp, content);
7910
+ renameSync2(tmp, path);
7477
7911
  }
7478
7912
  function releaseLock(vault, opts = {}) {
7479
7913
  const path = lockPath(vault);
7480
- if (!existsSync14(path)) {
7914
+ if (!existsSync15(path)) {
7481
7915
  return { released: false };
7482
7916
  }
7483
7917
  const sessionId = opts.sessionId ?? getSessionId();
@@ -7506,7 +7940,7 @@ function releaseLock(vault, opts = {}) {
7506
7940
  function runSyncStatus(input) {
7507
7941
  const vault = input.vault;
7508
7942
  const includeStashes = input.includeStashes ?? false;
7509
- if (!existsSync15(join37(vault, ".git"))) {
7943
+ if (!existsSync16(join38(vault, ".git"))) {
7510
7944
  return {
7511
7945
  exitCode: ExitCode.VAULT_PATH_INVALID,
7512
7946
  result: ok({
@@ -7584,7 +8018,7 @@ function runSyncStatus(input) {
7584
8018
  }
7585
8019
  async function runSyncPush(input) {
7586
8020
  const vault = input.vault;
7587
- if (!existsSync15(join37(vault, ".git"))) {
8021
+ if (!existsSync16(join38(vault, ".git"))) {
7588
8022
  return {
7589
8023
  exitCode: ExitCode.VAULT_PATH_INVALID,
7590
8024
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -7707,7 +8141,7 @@ function enableGitLongPathsOnWindows(vault) {
7707
8141
  }
7708
8142
  async function runSyncPull(input) {
7709
8143
  const vault = input.vault;
7710
- if (!existsSync15(join37(vault, ".git"))) {
8144
+ if (!existsSync16(join38(vault, ".git"))) {
7711
8145
  return {
7712
8146
  exitCode: ExitCode.VAULT_PATH_INVALID,
7713
8147
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -7879,7 +8313,7 @@ function runSyncPeers(input) {
7879
8313
  }
7880
8314
  function runSyncLock(input) {
7881
8315
  const vault = input.vault;
7882
- if (!existsSync15(vault)) {
8316
+ if (!existsSync16(vault)) {
7883
8317
  return {
7884
8318
  exitCode: ExitCode.VAULT_PATH_INVALID,
7885
8319
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7914,7 +8348,7 @@ function runSyncLock(input) {
7914
8348
  }
7915
8349
  function runSyncUnlock(input) {
7916
8350
  const vault = input.vault;
7917
- if (!existsSync15(vault)) {
8351
+ if (!existsSync16(vault)) {
7918
8352
  return {
7919
8353
  exitCode: ExitCode.VAULT_PATH_INVALID,
7920
8354
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7947,8 +8381,8 @@ function runSyncUnlock(input) {
7947
8381
  }
7948
8382
 
7949
8383
  // src/commands/backup.ts
7950
- import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
7951
- import { join as join38, relative as relative3, dirname as dirname12 } from "path";
8384
+ import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
8385
+ import { join as join39, relative as relative3, dirname as dirname13 } from "path";
7952
8386
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7953
8387
 
7954
8388
  // src/utils/s3-client.ts
@@ -7972,7 +8406,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
7972
8406
  function* walkMarkdown(dir, base) {
7973
8407
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7974
8408
  if (SKIP_DIRS2.has(entry.name)) continue;
7975
- const full = join38(dir, entry.name);
8409
+ const full = join39(dir, entry.name);
7976
8410
  if (entry.isDirectory()) {
7977
8411
  yield* walkMarkdown(full, base);
7978
8412
  } else if (entry.name.endsWith(".md")) {
@@ -7995,7 +8429,7 @@ async function runBackupSync(input) {
7995
8429
  let failed = 0;
7996
8430
  const files = [...walkMarkdown(input.vault, input.vault)];
7997
8431
  for (const relPath of files) {
7998
- const absPath = join38(input.vault, relPath);
8432
+ const absPath = join39(input.vault, relPath);
7999
8433
  const localStat = statSync5(absPath);
8000
8434
  let needsUpload = true;
8001
8435
  try {
@@ -8014,7 +8448,7 @@ async function runBackupSync(input) {
8014
8448
  continue;
8015
8449
  }
8016
8450
  try {
8017
- const body = readFileSync11(absPath);
8451
+ const body = readFileSync12(absPath);
8018
8452
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
8019
8453
  uploaded++;
8020
8454
  } catch {
@@ -8071,7 +8505,7 @@ async function runBackupRestore(input) {
8071
8505
  const objects = list.Contents ?? [];
8072
8506
  for (const obj of objects) {
8073
8507
  if (!obj.Key) continue;
8074
- const localPath = join38(target, obj.Key);
8508
+ const localPath = join39(target, obj.Key);
8075
8509
  try {
8076
8510
  const localStat = statSync5(localPath);
8077
8511
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -8084,8 +8518,8 @@ async function runBackupRestore(input) {
8084
8518
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
8085
8519
  const body = await resp.Body?.transformToByteArray();
8086
8520
  if (body) {
8087
- mkdirSync5(dirname12(localPath), { recursive: true });
8088
- writeFileSync7(localPath, Buffer.from(body));
8521
+ mkdirSync6(dirname13(localPath), { recursive: true });
8522
+ writeFileSync8(localPath, Buffer.from(body));
8089
8523
  downloaded++;
8090
8524
  }
8091
8525
  } catch {
@@ -8117,11 +8551,11 @@ async function runBackupRestore(input) {
8117
8551
  }
8118
8552
 
8119
8553
  // src/commands/status.ts
8120
- import { existsSync as existsSync16, statSync as statSync6 } from "fs";
8554
+ import { existsSync as existsSync17, statSync as statSync6 } from "fs";
8121
8555
  import { readFile as readFile25 } from "fs/promises";
8122
- import { join as join39 } from "path";
8556
+ import { join as join40 } from "path";
8123
8557
  async function runStatus(input) {
8124
- if (!existsSync16(input.vault)) {
8558
+ if (!existsSync17(input.vault)) {
8125
8559
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
8126
8560
  }
8127
8561
  const scan = await scanVault(input.vault);
@@ -8146,7 +8580,7 @@ async function runStatus(input) {
8146
8580
  const compound = scan.data.compound.length;
8147
8581
  let schemaVersion = "v1";
8148
8582
  try {
8149
- const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
8583
+ const schemaContent = await readFile25(join40(input.vault, "SCHEMA.md"), "utf8");
8150
8584
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
8151
8585
  if (versionMatch) schemaVersion = versionMatch[1];
8152
8586
  } catch {
@@ -8207,7 +8641,7 @@ async function runStatus(input) {
8207
8641
 
8208
8642
  // src/commands/seed.ts
8209
8643
  import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
8210
- import { join as join40 } from "path";
8644
+ import { join as join41 } from "path";
8211
8645
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8212
8646
  var EXAMPLE_PAGES = {
8213
8647
  "entities/example-project.md": `---
@@ -8276,29 +8710,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
8276
8710
  `;
8277
8711
  async function runSeed(input) {
8278
8712
  try {
8279
- await stat8(join40(input.vault, "SCHEMA.md"));
8713
+ await stat8(join41(input.vault, "SCHEMA.md"));
8280
8714
  } catch {
8281
8715
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
8282
8716
  }
8283
8717
  const created = [];
8284
8718
  const skipped = [];
8285
8719
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
8286
- const absPath = join40(input.vault, relPath);
8720
+ const absPath = join41(input.vault, relPath);
8287
8721
  try {
8288
8722
  await stat8(absPath);
8289
8723
  skipped.push(relPath);
8290
8724
  } catch {
8291
- await mkdir14(join40(absPath, ".."), { recursive: true });
8725
+ await mkdir14(join41(absPath, ".."), { recursive: true });
8292
8726
  await writeFile15(absPath, content, "utf8");
8293
8727
  created.push(relPath);
8294
8728
  }
8295
8729
  }
8296
- const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
8730
+ const rawPath = join41(input.vault, "raw", "articles", "example-source.md");
8297
8731
  try {
8298
8732
  await stat8(rawPath);
8299
8733
  skipped.push("raw/articles/example-source.md");
8300
8734
  } catch {
8301
- await mkdir14(join40(rawPath, ".."), { recursive: true });
8735
+ await mkdir14(join41(rawPath, ".."), { recursive: true });
8302
8736
  await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
8303
8737
  created.push("raw/articles/example-source.md");
8304
8738
  }
@@ -8322,8 +8756,8 @@ async function runSeed(input) {
8322
8756
 
8323
8757
  // src/commands/canvas.ts
8324
8758
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
8325
- import { existsSync as existsSync17 } from "fs";
8326
- import { join as join41 } from "path";
8759
+ import { existsSync as existsSync18 } from "fs";
8760
+ import { join as join42 } from "path";
8327
8761
  var NODE_WIDTH = 240;
8328
8762
  var NODE_HEIGHT = 60;
8329
8763
  var COLUMN_SPACING = 400;
@@ -8401,8 +8835,8 @@ function buildCanvasEdges(adjacency) {
8401
8835
  return edges;
8402
8836
  }
8403
8837
  async function runCanvasGenerate(input) {
8404
- const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
8405
- if (!existsSync17(graphPath)) {
8838
+ const graphPath = input.graphPath ?? join42(input.vault, ".skillwiki", "graph.json");
8839
+ if (!existsSync18(graphPath)) {
8406
8840
  return {
8407
8841
  exitCode: ExitCode.FILE_NOT_FOUND,
8408
8842
  result: err("FILE_NOT_FOUND", {
@@ -8439,7 +8873,7 @@ async function runCanvasGenerate(input) {
8439
8873
  const nodes = buildCanvasNodes(paths);
8440
8874
  const edges = buildCanvasEdges(graph.adjacency);
8441
8875
  const canvas = { nodes, edges };
8442
- const outPath = join41(input.vault, "vault-graph.canvas");
8876
+ const outPath = join42(input.vault, "vault-graph.canvas");
8443
8877
  try {
8444
8878
  await writeFile16(outPath, JSON.stringify(canvas, null, 2));
8445
8879
  } catch (e) {
@@ -8462,7 +8896,7 @@ written: ${outPath}`
8462
8896
 
8463
8897
  // src/commands/query.ts
8464
8898
  import { readFile as readFile27, stat as stat9 } from "fs/promises";
8465
- import { join as join42 } from "path";
8899
+ import { join as join43 } from "path";
8466
8900
  var W_KEYWORD = 2;
8467
8901
  var W_SOURCE_OVERLAP = 4;
8468
8902
  var W_WIKILINK = 3;
@@ -8583,7 +9017,7 @@ function computeKeywordScore(terms, title, tags, body) {
8583
9017
  return score;
8584
9018
  }
8585
9019
  async function loadOrBuildGraph(vault) {
8586
- const graphPath = join42(vault, ".skillwiki", "graph.json");
9020
+ const graphPath = join43(vault, ".skillwiki", "graph.json");
8587
9021
  let needsBuild = false;
8588
9022
  try {
8589
9023
  const fileStat = await stat9(graphPath);
@@ -8605,14 +9039,14 @@ async function loadOrBuildGraph(vault) {
8605
9039
  }
8606
9040
 
8607
9041
  // src/utils/auto-commit.ts
8608
- import { existsSync as existsSync18 } from "fs";
8609
- import { join as join43 } from "path";
9042
+ import { existsSync as existsSync19 } from "fs";
9043
+ import { join as join44 } from "path";
8610
9044
  async function postCommit(vault, exitCode) {
8611
9045
  if (exitCode !== 0) return;
8612
9046
  const home = process.env.HOME ?? "";
8613
9047
  const dotenv = await parseDotenvFile(configPath(home));
8614
9048
  if (dotenv["AUTO_COMMIT"] === "false") return;
8615
- if (!existsSync18(join43(vault, ".git"))) return;
9049
+ if (!existsSync19(join44(vault, ".git"))) return;
8616
9050
  const lastOps = readLastOp(vault);
8617
9051
  if (lastOps.length === 0) return;
8618
9052
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -8645,10 +9079,10 @@ var pkg = readCliPackageJson();
8645
9079
  var program = new Command2();
8646
9080
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
8647
9081
  program.option("--human", "render terminal-readable output instead of JSON");
8648
- async function emit(r, vault) {
9082
+ async function emit(r, vault, opts) {
8649
9083
  if (program.opts().human) printHuman(r.result);
8650
9084
  else printJson(r.result);
8651
- if (vault) await postCommit(vault, r.exitCode);
9085
+ if (vault && opts?.postCommit !== false) await postCommit(vault, r.exitCode);
8652
9086
  process.exit(r.exitCode);
8653
9087
  }
8654
9088
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
@@ -8663,7 +9097,7 @@ program.command("validate <file>").description("validate vault page frontmatter
8663
9097
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
8664
9098
  });
8665
9099
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
8666
- const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
9100
+ const out = opts.out ?? join45(vault, ".skillwiki", "graph.json");
8667
9101
  emit(await runGraphBuild({ vault, out }), vault);
8668
9102
  });
8669
9103
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -8794,9 +9228,20 @@ program.command("log-append [vault]").description("append a single entry to the
8794
9228
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8795
9229
  else emit(await runLogAppend({ vault: v.vault, content: opts.content }), v.vault);
8796
9230
  });
8797
- program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9231
+ program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
8798
9232
  const v = await resolveVaultArg(vault, opts.wiki);
8799
9233
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9234
+ else if (opts.summary) emit(await runLint({
9235
+ vault: v.vault,
9236
+ source: vault ? "flag" : void 0,
9237
+ days: opts.days,
9238
+ lines: opts.lines,
9239
+ logThreshold: opts.logThreshold,
9240
+ fix: opts.fix ?? false,
9241
+ only: opts.only,
9242
+ summary: true,
9243
+ examplesLimit: opts.examples
9244
+ }), v.vault);
8800
9245
  else emit(await runLint({
8801
9246
  vault: v.vault,
8802
9247
  source: vault ? "flag" : void 0,
@@ -8807,6 +9252,27 @@ program.command("lint [vault]").description("run all vault health checks").optio
8807
9252
  only: opts.only
8808
9253
  }), v.vault);
8809
9254
  });
9255
+ program.command("health [vault]").description("bounded whole-system wiki health report").option("--wiki <name>", "wiki profile name").option("--sync <mode>", "vault-sync policy: optional|required|off", "optional").option("--no-fail", "always exit 0 while reporting status in JSON").option("--out <path>", "write JSON result envelope to this path").option("--examples <n>", "example count per lint bucket", (s) => parseInt(s, 10), 3).action(async (vault, opts) => {
9256
+ const sync = String(opts.sync ?? "optional");
9257
+ if (!["optional", "required", "off"].includes(sync)) {
9258
+ emit({ exitCode: ExitCode.USAGE, result: { ok: false, error: "USAGE", detail: "--sync must be optional, required, or off" } });
9259
+ }
9260
+ const v = await resolveVaultArg(vault, opts.wiki);
9261
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9262
+ else emit(await runHealth({
9263
+ vault: v.vault,
9264
+ vaultSource: vault ? "flag" : "resolved",
9265
+ home: process.env.HOME ?? "",
9266
+ envValue: process.env.WIKI_PATH,
9267
+ argv: process.argv,
9268
+ currentVersion: pkg.version,
9269
+ cwd: process.cwd(),
9270
+ sync,
9271
+ noFail: opts.fail === false,
9272
+ out: opts.out,
9273
+ examplesLimit: opts.examples
9274
+ }), void 0, { postCommit: false });
9275
+ });
8810
9276
  var configCmd = program.command("config").description("manage skillwiki configuration");
8811
9277
  configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
8812
9278
  configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));