skillwiki 0.10.25 → 0.10.26

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.
@@ -787,20 +787,75 @@ function extractBodyWikilinks(body) {
787
787
  return out;
788
788
  }
789
789
 
790
+ // src/utils/wikilink-resolver.ts
791
+ var EXCLUDED_PREFIXES = [
792
+ ".git/",
793
+ ".skillwiki/",
794
+ "_archive/",
795
+ "drafts/",
796
+ "tmp/"
797
+ ];
798
+ var RESOLVABLE_PROJECT_ARTIFACT_RE = /^projects\/[^/]+\/(?:knowledge\.md|README\.md|(?:compound|requirements|architecture|history|work)\/(?:[^/]+\/)*[^/]+\.md)$/i;
799
+ function normalizeTarget(target) {
800
+ return target.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\.md$/i, "").replace(/^\/+/, "").replace(/\/+/g, "/").toLowerCase();
801
+ }
802
+ function isResolvableProjectArtifact(relPath) {
803
+ return RESOLVABLE_PROJECT_ARTIFACT_RE.test(relPath);
804
+ }
805
+ function isResolvablePath(relPath) {
806
+ const normalized = relPath.replace(/\\/g, "/").replace(/^\.\//, "");
807
+ if (EXCLUDED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;
808
+ return !normalized.startsWith("projects/") || isResolvableProjectArtifact(normalized);
809
+ }
810
+ function buildWikilinkResolver(pages) {
811
+ const exact = /* @__PURE__ */ new Map();
812
+ const byBasename = /* @__PURE__ */ new Map();
813
+ for (const page of pages) {
814
+ if (!isResolvablePath(page.relPath)) continue;
815
+ const normalizedPath = normalizeTarget(page.relPath);
816
+ exact.set(normalizedPath, page.relPath);
817
+ const basename3 = normalizedPath.split("/").pop();
818
+ const candidates = byBasename.get(basename3) ?? [];
819
+ candidates.push(page.relPath);
820
+ byBasename.set(basename3, candidates);
821
+ }
822
+ for (const candidates of byBasename.values()) candidates.sort();
823
+ return {
824
+ resolve(target) {
825
+ const normalized = normalizeTarget(target);
826
+ const exactPath = exact.get(normalized);
827
+ if (exactPath) return { target, path: exactPath, ambiguous: false };
828
+ if (normalized.startsWith("projects/")) {
829
+ return {
830
+ target,
831
+ ambiguous: false,
832
+ reason: isResolvablePath(`${normalized}.md`) ? "missing" : "unsupported"
833
+ };
834
+ }
835
+ const candidates = byBasename.get(normalized) ?? [];
836
+ if (candidates.length === 1) {
837
+ return { target, path: candidates[0], ambiguous: false };
838
+ }
839
+ if (candidates.length > 1) {
840
+ return { target, ambiguous: true, reason: "ambiguous" };
841
+ }
842
+ return { target, ambiguous: false, reason: "missing" };
843
+ }
844
+ };
845
+ }
846
+
790
847
  // src/utils/community.ts
791
- async function buildWikilinkAdjacency(typedKnowledge, pageTextCache) {
848
+ async function buildWikilinkAdjacency(typedKnowledge, pageTextCache, allPages = typedKnowledge) {
792
849
  const adjacency = {};
793
- const slugToPath = {};
794
- for (const p of typedKnowledge) {
795
- const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
796
- slugToPath[slug] = p.relPath;
797
- }
850
+ const resolver = buildWikilinkResolver(allPages);
798
851
  await mapWithConcurrency(typedKnowledge, vaultIoConcurrency(), async (p) => {
799
852
  const text = await readPageCached(p, pageTextCache);
800
853
  const split = splitFrontmatter(text);
801
854
  const body = split.ok ? split.data.body : text;
802
855
  const links = extractBodyWikilinks(body);
803
- adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
856
+ const targets = links.map((target) => resolver.resolve(target).path).filter((x) => Boolean(x));
857
+ adjacency[p.relPath] = targets;
858
+ for (const target of targets) adjacency[target] ??= [];
804
859
  });
805
860
  return adjacency;
806
861
  }
@@ -917,7 +972,7 @@ function findSparseCommunities(adj, opts = {}) {
917
972
  async function runGraphBuild(input) {
918
973
  const scan = await scanVault(input.vault);
919
974
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
920
- const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
975
+ const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge, void 0, scan.data.allMarkdown);
921
976
  const adamicAdar = computeAdamicAdar(adjacency);
922
977
  const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
923
978
  try {
@@ -926,9 +981,10 @@ async function runGraphBuild(input) {
926
981
  } catch (e) {
927
982
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
928
983
  }
984
+ const node_count = Object.keys(adjacency).length;
929
985
  return {
930
986
  exitCode: ExitCode.OK,
931
- result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
987
+ result: ok({ out_path: input.out, node_count, edge_count, humanHint: `nodes: ${node_count}, edges: ${edge_count}
932
988
  written: ${input.out}` })
933
989
  };
934
990
  }
@@ -1282,23 +1338,12 @@ async function validateCompoundReferences(vault, existingScan, pageTextCache) {
1282
1338
  return ok(findings);
1283
1339
  }
1284
1340
 
1285
- // src/utils/slug.ts
1286
- function buildSlugMap(pages) {
1287
- const map = /* @__PURE__ */ new Map();
1288
- for (const p of pages) {
1289
- const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
1290
- map.set(slug.toLowerCase(), slug);
1291
- }
1292
- return map;
1293
- }
1294
-
1295
1341
  // src/commands/links.ts
1296
1342
  async function runLinks(input) {
1297
1343
  const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
1298
1344
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
1299
1345
  const scan = scanResult.data;
1300
- const allPages = [...scan.typedKnowledge, ...scan.raw, ...scan.workItems, ...scan.compound];
1301
- const slugs = buildSlugMap(allPages);
1346
+ const resolver = buildWikilinkResolver(scan.allMarkdown);
1302
1347
  const perPage = await mapWithConcurrency(scan.typedKnowledge, vaultIoConcurrency(), async (p) => {
1303
1348
  const text = await readPageCached(p, input.pageTextCache);
1304
1349
  const split = splitFrontmatter(text);
@@ -1306,8 +1351,8 @@ async function runLinks(input) {
1306
1351
  const lines = body.split("\n");
1307
1352
  const broken2 = [];
1308
1353
  for (const slug of extractBodyWikilinks(body)) {
1309
- const tail = slug.split("/").pop().replace(/\.md$/, "");
1310
- if (!slugs.has(tail.toLowerCase())) {
1354
+ const resolution = resolver.resolve(slug);
1355
+ if (!resolution.path) {
1311
1356
  const line = lines.findIndex((l) => l.includes(`[[${slug}`));
1312
1357
  broken2.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
1313
1358
  }
@@ -1738,8 +1783,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
1738
1783
 
1739
1784
  `;
1740
1785
  for (const item of items) {
1741
- const pageRef = item.page.replace(/\.md$/, "");
1742
- body += `- [[${pageRef}]] \u2014 ${item.title}
1786
+ body += `- [[${item.page.replace(/\.md$/, "")}]] \u2014 ${item.title}
1743
1787
  `;
1744
1788
  }
1745
1789
  body += "\n";
@@ -3077,7 +3121,11 @@ import { join as join16, relative as relative2, sep as sep2 } from "path";
3077
3121
  async function runSparseCommunity(input) {
3078
3122
  const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
3079
3123
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
3080
- const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge, input.pageTextCache);
3124
+ const adjacency = await buildWikilinkAdjacency(
3125
+ scan.data.typedKnowledge,
3126
+ input.pageTextCache,
3127
+ scan.data.allMarkdown
3128
+ );
3081
3129
  const communities = findSparseCommunities(adjacency, {
3082
3130
  minSize: input.minSize,
3083
3131
  maxCohesion: input.maxCohesion
@@ -4092,8 +4140,7 @@ async function runLint(input) {
4092
4140
  if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
4093
4141
  const pathCheck = await runPathTooLong({ vault: lintVault, scan });
4094
4142
  if (pathCheck.result.ok && pathCheck.result.data.violations.length > 0) buckets.path_too_long = pathCheck.result.data.violations;
4095
- const allPages = [...scan.typedKnowledge, ...scan.raw, ...scan.workItems, ...scan.compound];
4096
- const slugs = buildSlugMap(allPages);
4143
+ const wikilinkResolver = buildWikilinkResolver(scan.allMarkdown);
4097
4144
  {
4098
4145
  const allPageResults = await mapWithConcurrency(scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4099
4146
  const sensitiveFlags2 = [];
@@ -4196,8 +4243,7 @@ async function runLint(input) {
4196
4243
  const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
4197
4244
  for (const link of fmLinks) {
4198
4245
  const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
4199
- const tail = target.split("/").pop().replace(/\.md$/, "");
4200
- if (!slugs.has(tail.toLowerCase())) {
4246
+ if (!wikilinkResolver.resolve(target).path) {
4201
4247
  result.fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
4202
4248
  }
4203
4249
  }
@@ -7211,8 +7257,9 @@ async function vaultMetrics(resolvedPath) {
7211
7257
  const scan = await scanVault(scanRoot);
7212
7258
  if (!scan.ok) return noVault();
7213
7259
  const tk = scan.data.typedKnowledge;
7260
+ const typedCount = tk.length;
7214
7261
  const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
7215
- const adj = await buildWikilinkAdjacency(tk);
7262
+ const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
7216
7263
  const g = toUndirectedWeighted(adj);
7217
7264
  const nodes = [...g.keys()];
7218
7265
  const total = nodes.length;
@@ -7239,7 +7286,7 @@ async function vaultMetrics(resolvedPath) {
7239
7286
  } catch {
7240
7287
  }
7241
7288
  return [
7242
- check("info", "vault_metric_pages", "Vault pages by type", `${total} typed (${perType})`),
7289
+ check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
7243
7290
  check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
7244
7291
  check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
7245
7292
  check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
@@ -8957,6 +9004,8 @@ var W_WIKILINK = 3;
8957
9004
  var W_ADAMIC_ADAR = 1.5;
8958
9005
  var W_TYPE_AFFINITY = 1;
8959
9006
  var NON_SEED_FACTOR = 0.4;
9007
+ var HISTORICAL_CYCLE_FACTOR = 0.55;
9008
+ var HISTORICAL_CYCLE_RE = /(?:^|\/)(?:\d{4}-\d{2}-\d{2}-)?.*\b(?:daily|deep|maintenance|research|office[- ]hours|sleep)\b.*\b(?:cycle|run|review|research)\b/i;
8960
9009
  var CONCEPT_INDICATORS = /* @__PURE__ */ new Set([
8961
9010
  "what",
8962
9011
  "how",
@@ -8996,11 +9045,26 @@ async function runQuery(input) {
8996
9045
  const split = splitFrontmatter(text);
8997
9046
  const body = split.ok ? split.data.body : text;
8998
9047
  const keywordScore = computeKeywordScore(queryTerms, title, tags, body);
8999
- pages.push({ relPath: p.relPath, title, type, tags, sources, keywordScore });
9048
+ pages.push({
9049
+ relPath: p.relPath,
9050
+ title,
9051
+ type,
9052
+ tags,
9053
+ sources,
9054
+ keywordScore,
9055
+ historicalCycle: isHistoricalCyclePage(p.relPath, title)
9056
+ });
9000
9057
  }
9001
- const seedPaths = new Set(
9002
- pages.filter((p) => p.keywordScore > 0).map((p) => p.relPath)
9003
- );
9058
+ const seedPaths = /* @__PURE__ */ new Set();
9059
+ let historicalCyclePageCount = 0;
9060
+ let hasDirectOperationalSeed = false;
9061
+ for (const page of pages) {
9062
+ if (page.historicalCycle) historicalCyclePageCount += 1;
9063
+ if (page.keywordScore <= 0) continue;
9064
+ seedPaths.add(page.relPath);
9065
+ if (!page.historicalCycle) hasDirectOperationalSeed = true;
9066
+ }
9067
+ const suppressRepetitiveHistoricalCycles = historicalCyclePageCount >= 3 && hasDirectOperationalSeed;
9004
9068
  const results = pages.map((page) => {
9005
9069
  const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
9006
9070
  const wikilink = scoreWikilink(page.relPath, seedPaths, graph);
@@ -9009,9 +9073,10 @@ async function runQuery(input) {
9009
9073
  const isSeed = page.keywordScore > 0;
9010
9074
  const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink * W_WIKILINK + aa * W_ADAMIC_ADAR;
9011
9075
  const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
9076
+ const guardedComposite = suppressRepetitiveHistoricalCycles && page.historicalCycle ? composite * HISTORICAL_CYCLE_FACTOR : composite;
9012
9077
  return {
9013
9078
  path: page.relPath,
9014
- score: Math.round(composite * 1e3) / 1e3,
9079
+ score: Math.round(guardedComposite * 1e3) / 1e3,
9015
9080
  title: page.title,
9016
9081
  type: page.type
9017
9082
  };
@@ -9028,9 +9093,18 @@ async function runQuery(input) {
9028
9093
  }
9029
9094
  const humanHint = results.length === 0 ? pendingSources && pendingSources.length > 0 ? `no matching typed pages found
9030
9095
  ${pendingSources.length} matching pending source(s)` : "no matching pages found" : results.map((r) => `${r.path} (score: ${r.score})`).join("\n");
9096
+ const rankingGuardrails = suppressRepetitiveHistoricalCycles ? {
9097
+ repetitive_historical_cycles_suppressed: true,
9098
+ historical_cycle_page_count: historicalCyclePageCount
9099
+ } : void 0;
9031
9100
  return {
9032
9101
  exitCode: ExitCode.OK,
9033
- result: ok({ results, ...pendingSources ? { pending_sources: pendingSources } : {}, humanHint })
9102
+ result: ok({
9103
+ results,
9104
+ ...pendingSources ? { pending_sources: pendingSources } : {},
9105
+ ...rankingGuardrails ? { ranking_guardrails: rankingGuardrails } : {},
9106
+ humanHint
9107
+ })
9034
9108
  };
9035
9109
  }
9036
9110
  function scoreSourceOverlap(page, allPages, seedPaths) {
@@ -9069,6 +9143,9 @@ function scoreTypeAffinity(pageType, queryTerms) {
9069
9143
  if (!hasConceptIntent && pageType === "entity") return 0.5;
9070
9144
  return 0;
9071
9145
  }
9146
+ function isHistoricalCyclePage(relPath, title) {
9147
+ return HISTORICAL_CYCLE_RE.test(`${relPath} ${title}`);
9148
+ }
9072
9149
  function tokenize(text) {
9073
9150
  return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
9074
9151
  }
package/dist/cli.js CHANGED
@@ -81,7 +81,7 @@ import {
81
81
  snapshotterHealthChecks,
82
82
  taxonomyCommentForPage,
83
83
  upsertIndexEntry
84
- } from "./chunk-SYKSL3JQ.js";
84
+ } from "./chunk-VXMHHXVP.js";
85
85
  import {
86
86
  normalizeDistTag,
87
87
  readCache,
@@ -4873,14 +4873,14 @@ function dateFromPath(path) {
4873
4873
  }
4874
4874
 
4875
4875
  // src/commands/ingest.ts
4876
- import { readFile as readFile13, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
4876
+ import { readFile as readFile13, open, unlink as unlink4, mkdir as mkdir10 } from "fs/promises";
4877
4877
  import { join as join26 } from "path";
4878
4878
  import { createHash as createHash4 } from "crypto";
4879
4879
 
4880
4880
  // src/commands/page-publish.ts
4881
4881
  import { readFileSync as readFileSync12, realpathSync } from "fs";
4882
- import { readFile as readFile12 } from "fs/promises";
4883
- import { join as join25, resolve as resolve3 } from "path";
4882
+ import { mkdir as mkdir9, readFile as readFile12 } from "fs/promises";
4883
+ import { dirname as dirname6, join as join25, resolve as resolve3 } from "path";
4884
4884
 
4885
4885
  // src/utils/publication-approval.ts
4886
4886
  import { createHash as createHash3 } from "crypto";
@@ -5140,6 +5140,8 @@ function errorExitCode(error) {
5140
5140
  return ExitCode.APPROVAL_MISMATCH;
5141
5141
  case "TARGET_DRIFT":
5142
5142
  return ExitCode.TARGET_DRIFT;
5143
+ case "PROJECT_NOT_FOUND":
5144
+ return ExitCode.PROJECT_NOT_FOUND;
5143
5145
  case "USAGE":
5144
5146
  return ExitCode.USAGE;
5145
5147
  case "EVENT_IDENTITY_COLLISION":
@@ -5149,6 +5151,48 @@ function errorExitCode(error) {
5149
5151
  return ExitCode.INVALID_FRONTMATTER;
5150
5152
  }
5151
5153
  }
5154
+ function projectSlugForTarget(target) {
5155
+ const match = target.match(/^projects\/([^/]+)\/(?:requirements|work|architecture|history|compound)\//);
5156
+ return match?.[1];
5157
+ }
5158
+ function projectSlugsForPublication(target, content) {
5159
+ const slugs = /* @__PURE__ */ new Set();
5160
+ const pathSlug = projectSlugForTarget(target);
5161
+ if (pathSlug) slugs.add(pathSlug);
5162
+ const frontmatter = extractFrontmatter(content);
5163
+ if (frontmatter.ok && Array.isArray(frontmatter.data.provenance_projects)) {
5164
+ for (const entry of frontmatter.data.provenance_projects) {
5165
+ const match = String(entry).match(/^\[\[([^\]]+)\]\]$/);
5166
+ if (match && /^[a-z0-9][a-z0-9-]*$/i.test(match[1])) slugs.add(match[1]);
5167
+ }
5168
+ }
5169
+ return [...slugs].sort();
5170
+ }
5171
+ async function refreshProjectIndexForTarget(vault, projectSlugs, today) {
5172
+ const paths = [];
5173
+ for (const slug of projectSlugs) {
5174
+ const rendered = await renderProjectIndex(vault, slug, { today });
5175
+ if (!rendered.ok) return rendered;
5176
+ const indexPath = join25(vault, rendered.data.index_path);
5177
+ try {
5178
+ await mkdir9(dirname6(indexPath), { recursive: true });
5179
+ } catch (error) {
5180
+ return err("WRITE_FAILED", { path: indexPath, message: String(error) });
5181
+ }
5182
+ const written = await atomicWriteText(indexPath, rendered.data.text);
5183
+ if (!written.ok) return written;
5184
+ try {
5185
+ const visible = await readFile12(indexPath, "utf8");
5186
+ if (visible !== rendered.data.text) {
5187
+ return err("WRITE_FAILED", { path: indexPath, message: "project index verification failed" });
5188
+ }
5189
+ } catch (error) {
5190
+ return err("WRITE_FAILED", { path: indexPath, message: String(error) });
5191
+ }
5192
+ if (written.data.changed) paths.push(rendered.data.index_path);
5193
+ }
5194
+ return ok({ changed: paths.length > 0, paths });
5195
+ }
5152
5196
  function resolveRootAggregateMode(env = process.env) {
5153
5197
  if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
5154
5198
  return "dual";
@@ -5248,6 +5292,7 @@ function emptyLockedState() {
5248
5292
  taxonomyAdded: [],
5249
5293
  pageChanged: false,
5250
5294
  indexUpdated: false,
5295
+ projectIndexUpdated: false,
5251
5296
  published: false,
5252
5297
  changed: /* @__PURE__ */ new Set()
5253
5298
  };
@@ -5335,6 +5380,15 @@ async function runLockedPrimaryStages(input, vault, deps) {
5335
5380
  }
5336
5381
  const verifyHook = await observeStage(deps, "verify");
5337
5382
  if (verifyHook) return lockedFailure("verify", state, verifyHook);
5383
+ const projectSlugs = projectSlugsForPublication(input.page.target, input.page.content);
5384
+ if (projectSlugs.length > 0) {
5385
+ const projectIndex = await refreshProjectIndexForTarget(vault, projectSlugs, input.date);
5386
+ if (!projectIndex.ok) return lockedFailure("project-index", state, projectIndex);
5387
+ state.projectIndexUpdated = projectIndex.data.changed;
5388
+ for (const path of projectIndex.data.paths) state.changed.add(path);
5389
+ const projectIndexHook = await observeStage(deps, "project-index");
5390
+ if (projectIndexHook) return lockedFailure("project-index", state, projectIndexHook);
5391
+ }
5338
5392
  const index = await upsertIndexEntry({
5339
5393
  vault,
5340
5394
  target: input.page.target,
@@ -5372,7 +5426,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
5372
5426
  })
5373
5427
  };
5374
5428
  }
5375
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null, approvalToken) {
5429
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, projectIndexUpdated, logAppended, filesChanged, dryRun = false, receipt = null, approvalToken) {
5376
5430
  return {
5377
5431
  exitCode: ExitCode.OK,
5378
5432
  result: ok({
@@ -5382,6 +5436,7 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
5382
5436
  taxonomy_added: [...taxonomyAdded],
5383
5437
  page_changed: pageChanged,
5384
5438
  index_updated: indexUpdated,
5439
+ project_index_updated: projectIndexUpdated,
5385
5440
  log_appended: logAppended,
5386
5441
  operation_id: input.operationId,
5387
5442
  dry_run: dryRun,
@@ -5449,6 +5504,21 @@ async function previewPreparedPagePublication(input, vault) {
5449
5504
  type: input.page.type
5450
5505
  });
5451
5506
  if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
5507
+ let projectIndexUpdated = false;
5508
+ const projectIndexPaths = [];
5509
+ for (const projectSlug of projectSlugsForPublication(input.page.target, input.page.content)) {
5510
+ const renderedProject = await renderProjectIndex(vault, projectSlug, { today: input.date });
5511
+ if (!renderedProject.ok) return { exitCode: errorExitCode(renderedProject.error), result: renderedProject };
5512
+ const projectIndexPath = join25(vault, renderedProject.data.index_path);
5513
+ const projectIndexChanged = await readPageChanged(projectIndexPath, renderedProject.data.text);
5514
+ if (!projectIndexChanged.ok) {
5515
+ return { exitCode: errorExitCode(projectIndexChanged.error), result: projectIndexChanged };
5516
+ }
5517
+ if (projectIndexChanged.data || pageChanged.data) {
5518
+ projectIndexUpdated = true;
5519
+ projectIndexPaths.push(renderedProject.data.index_path);
5520
+ }
5521
+ }
5452
5522
  const logPath = join25(vault, "log.md");
5453
5523
  let logText;
5454
5524
  try {
@@ -5461,6 +5531,7 @@ async function previewPreparedPagePublication(input, vault) {
5461
5531
  const filesChanged = [
5462
5532
  ...reconciled.data.changed ? ["SCHEMA.md"] : [],
5463
5533
  ...pageChanged.data ? [input.page.target] : [],
5534
+ ...projectIndexPaths,
5464
5535
  ...index.data.changed ? ["index.md"] : [],
5465
5536
  ...logAppended ? ["log.md"] : []
5466
5537
  ];
@@ -5471,6 +5542,7 @@ async function previewPreparedPagePublication(input, vault) {
5471
5542
  reconciled.data.added,
5472
5543
  pageChanged.data,
5473
5544
  index.data.changed,
5545
+ projectIndexUpdated,
5474
5546
  logAppended,
5475
5547
  filesChanged,
5476
5548
  true,
@@ -5622,6 +5694,7 @@ async function publishPreparedPageWithReceipt(input, vault, writeReceipt, deps =
5622
5694
  state.taxonomyAdded,
5623
5695
  state.pageChanged,
5624
5696
  state.indexUpdated,
5697
+ state.projectIndexUpdated,
5625
5698
  logAppended,
5626
5699
  [...state.changed],
5627
5700
  false,
@@ -5951,7 +6024,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5951
6024
  );
5952
6025
  if (!input.dryRun) {
5953
6026
  try {
5954
- await mkdir9(join26(input.vault, typedDir), { recursive: true });
6027
+ await mkdir10(join26(input.vault, typedDir), { recursive: true });
5955
6028
  } catch (error) {
5956
6029
  return {
5957
6030
  exitCode: ExitCode.WRITE_FAILED,
@@ -6009,7 +6082,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
6009
6082
  preflight: pagePublishDeps.preflight,
6010
6083
  mutate: async (receipt) => {
6011
6084
  try {
6012
- await mkdir9(join26(input.vault, "raw", "articles"), { recursive: true });
6085
+ await mkdir10(join26(input.vault, "raw", "articles"), { recursive: true });
6013
6086
  } catch (error) {
6014
6087
  return {
6015
6088
  exitCode: ExitCode.WRITE_FAILED,
@@ -6443,12 +6516,12 @@ async function runTagReconcile(input) {
6443
6516
 
6444
6517
  // src/commands/project-page-publish.ts
6445
6518
  import { realpathSync as realpathSync4 } from "fs";
6446
- import { mkdir as mkdir10, readFile as readFile15 } from "fs/promises";
6447
- import { dirname as dirname8, join as join29, resolve as resolve5 } from "path";
6519
+ import { mkdir as mkdir11, readFile as readFile15 } from "fs/promises";
6520
+ import { dirname as dirname9, join as join29, resolve as resolve5 } from "path";
6448
6521
 
6449
6522
  // src/utils/architecture-page.ts
6450
6523
  import { lstatSync, realpathSync as realpathSync2 } from "fs";
6451
- import { dirname as dirname6, posix as posix2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
6524
+ import { dirname as dirname7, posix as posix2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
6452
6525
  var PROJECT_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
6453
6526
  var ARCH_TARGET_RE = /^projects\/([a-z0-9][a-z0-9-]*)\/architecture\/([a-z0-9][a-z0-9._-]*\.md)$/;
6454
6527
  function invalidFrontmatter(target, issues) {
@@ -6495,7 +6568,7 @@ function assertArchitectureTargetInsideVault(vault, target, project) {
6495
6568
  return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
6496
6569
  }
6497
6570
  const absolutePath = resolve4(vaultReal, target);
6498
- const parent = dirname6(absolutePath);
6571
+ const parent = dirname7(absolutePath);
6499
6572
  let parentReal;
6500
6573
  try {
6501
6574
  parentReal = realpathSync2(parent);
@@ -6605,7 +6678,7 @@ import {
6605
6678
  writeFileSync as writeFileSync5
6606
6679
  } from "fs";
6607
6680
  import { homedir } from "os";
6608
- import { dirname as dirname7, join as join28 } from "path";
6681
+ import { dirname as dirname8, join as join28 } from "path";
6609
6682
  var PUBLICATION_JOURNAL_SCHEMA = "skillwiki-publication-operation-v1";
6610
6683
  var PHASE_ORDER2 = [
6611
6684
  "locked",
@@ -6654,7 +6727,7 @@ function ensurePrivateDir(dir) {
6654
6727
  }
6655
6728
  }
6656
6729
  function atomicWriteJson(path, body) {
6657
- ensurePrivateDir(dirname7(path));
6730
+ ensurePrivateDir(dirname8(path));
6658
6731
  const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
6659
6732
  writeFileSync5(tmp, body, { encoding: "utf8", mode: 384 });
6660
6733
  try {
@@ -7276,7 +7349,7 @@ async function runLockedStages(prepared, vault, deps, journalHome) {
7276
7349
  }
7277
7350
  if (!phaseAtLeast(currentPhase, "page")) {
7278
7351
  try {
7279
- await mkdir10(dirname8(prepared.targetPath), { recursive: true });
7352
+ await mkdir11(dirname9(prepared.targetPath), { recursive: true });
7280
7353
  } catch (error) {
7281
7354
  return {
7282
7355
  ok: false,
@@ -7372,7 +7445,7 @@ async function runLockedStages(prepared, vault, deps, journalHome) {
7372
7445
  }
7373
7446
  const knowledgePath = join29(vault, rendered.data.index_path);
7374
7447
  try {
7375
- await mkdir10(dirname8(knowledgePath), { recursive: true });
7448
+ await mkdir11(dirname9(knowledgePath), { recursive: true });
7376
7449
  } catch (error) {
7377
7450
  return {
7378
7451
  ok: false,
@@ -8018,22 +8091,22 @@ async function runSyncPush(input) {
8018
8091
  })
8019
8092
  };
8020
8093
  }
8021
- function enumerateStashes(vault) {
8022
- const output = git(vault, ["log", "--format=%gd%x09%s%x09%ct", "-g", "stash"]);
8094
+ function enumerateStashes(vault, nowMs = Date.now()) {
8095
+ const output = git(vault, ["log", "--format=%gd%x09%H%x09%s%x09%ct", "-g", "stash"]);
8023
8096
  if (!output) return [];
8024
- const now = Date.now();
8025
8097
  const stashes = [];
8026
8098
  const lines = output.split("\n").filter((l) => l.trim().length > 0);
8027
8099
  for (const line of lines) {
8028
8100
  const parts = line.split(" ");
8029
- if (parts.length < 3) continue;
8101
+ if (parts.length < 4) continue;
8030
8102
  const ref = parts[0];
8031
- const message = parts[1];
8032
- const ctStr = parts[2];
8103
+ const oid = parts[1];
8104
+ const message = parts[2];
8105
+ const ctStr = parts[3];
8033
8106
  const ct = parseInt(ctStr, 10);
8034
8107
  if (isNaN(ct)) continue;
8035
- const age_minutes = Math.floor((now - ct * 1e3) / (60 * 1e3));
8036
- stashes.push({ ref, message, age_minutes });
8108
+ const age_minutes = Math.floor((nowMs - ct * 1e3) / (60 * 1e3));
8109
+ stashes.push({ ref, oid, message, age_minutes });
8037
8110
  }
8038
8111
  return stashes;
8039
8112
  }
@@ -8112,16 +8185,73 @@ async function runSyncPull(input) {
8112
8185
  })
8113
8186
  };
8114
8187
  }
8188
+ var STASH_RECENT_MINUTES = 120;
8189
+ var MANAGED_WRITER_PATTERNS = [
8190
+ ["wiki-push", /(?:^|[\s/\\])wiki-push(?:\.[a-z0-9_-]+)?(?:\s|$)/i],
8191
+ ["rclone", /(?:^|[\s/\\])rclone(?:\.[a-z0-9_-]+)?(?:\s|$)/i],
8192
+ ["vault-sync", /(?:^|[\s/\\])vault-sync(?:\.[a-z0-9_-]+)?(?:\s|$)/i]
8193
+ ];
8194
+ function classifyStash(ageMinutes, sessionId, currentSession) {
8195
+ if (sessionId && sessionId === currentSession) return "self_or_local_recovery_stash";
8196
+ if (sessionId && ageMinutes <= STASH_RECENT_MINUTES) return "recent_known_peer_stash";
8197
+ if (ageMinutes > STASH_RECENT_MINUTES) return "stale_stash_backlog";
8198
+ return "unknown_stash_ownership";
8199
+ }
8200
+ function classifyManagedWriterProcesses(snapshot, currentPid = process.pid) {
8201
+ const kinds = /* @__PURE__ */ new Set();
8202
+ let count2 = 0;
8203
+ for (const line of snapshot.split(/\r?\n/)) {
8204
+ const trimmed = line.trim();
8205
+ if (!trimmed) continue;
8206
+ let pid;
8207
+ let processName = "";
8208
+ const csvMatch = trimmed.match(/^"([^"]*)","(\d+)"(?:,|$)/);
8209
+ if (csvMatch) {
8210
+ processName = csvMatch[1];
8211
+ pid = Number(csvMatch[2]);
8212
+ } else {
8213
+ const pidMatch = trimmed.match(/^(\d+)\s+(.*)$/);
8214
+ if (!pidMatch) continue;
8215
+ pid = Number(pidMatch[1]);
8216
+ processName = pidMatch[2];
8217
+ }
8218
+ if (pid === currentPid) continue;
8219
+ const kind = MANAGED_WRITER_PATTERNS.find(([, pattern]) => pattern.test(processName))?.[0];
8220
+ if (!kind) continue;
8221
+ count2 += 1;
8222
+ kinds.add(kind);
8223
+ }
8224
+ return { count: count2, kinds: [...kinds].sort(), blocking: count2 > 0 };
8225
+ }
8226
+ function managedWriterSnapshot() {
8227
+ try {
8228
+ if (process.platform === "win32") {
8229
+ return execFileSync3("tasklist", ["/FO", "CSV", "/NH"], {
8230
+ encoding: "utf8",
8231
+ stdio: ["pipe", "pipe", "pipe"]
8232
+ });
8233
+ }
8234
+ return execFileSync3("ps", ["-axo", "pid=,command="], {
8235
+ encoding: "utf8",
8236
+ stdio: ["pipe", "pipe", "pipe"]
8237
+ });
8238
+ } catch {
8239
+ return "";
8240
+ }
8241
+ }
8115
8242
  function runSyncPeers(input) {
8116
8243
  const vault = input.vault;
8244
+ const currentSession = input.sessionId ?? getSessionId();
8117
8245
  const locks = [];
8118
8246
  const existingLock = readLock(vault);
8119
8247
  if (existingLock) {
8120
- const self = existingLock.session_id === (input.sessionId ?? getSessionId());
8248
+ const self = existingLock.session_id === currentSession;
8121
8249
  locks.push({ ...existingLock, is_self: self });
8122
8250
  }
8123
- const allStashes = enumerateStashes(vault);
8251
+ const allStashes = enumerateStashes(vault, input.nowMs);
8124
8252
  const stashes = [];
8253
+ const wikiSyncAudit = [];
8254
+ const otherStashAudit = [];
8125
8255
  for (const stash of allStashes) {
8126
8256
  let actualMessage = stash.message;
8127
8257
  const prefixMatch = stash.message.match(/^On [^:]+:\s*(.*)/);
@@ -8129,29 +8259,63 @@ function runSyncPeers(input) {
8129
8259
  actualMessage = prefixMatch[1];
8130
8260
  }
8131
8261
  const match = actualMessage.match(/^wiki-sync:([^:]+):([^:]+):(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z):(.*)$/);
8132
- if (!match) continue;
8133
- const session_id = match[1];
8134
- const cwd_hash = match[2];
8135
- const timestamp = match[3];
8136
- const summary = match[4];
8137
- stashes.push({
8262
+ if (match) {
8263
+ const session_id = match[1];
8264
+ const cwd_hash = match[2];
8265
+ const timestamp = match[3];
8266
+ const summary = match[4];
8267
+ stashes.push({
8268
+ ref: stash.ref,
8269
+ oid: stash.oid,
8270
+ session_id,
8271
+ cwd_hash,
8272
+ timestamp,
8273
+ summary,
8274
+ age_minutes: stash.age_minutes
8275
+ });
8276
+ wikiSyncAudit.push({
8277
+ ref: stash.ref,
8278
+ oid: stash.oid,
8279
+ age_minutes: stash.age_minutes,
8280
+ classification: classifyStash(stash.age_minutes, session_id, currentSession),
8281
+ format: "wiki-sync",
8282
+ session_id
8283
+ });
8284
+ continue;
8285
+ }
8286
+ const vaultSyncMatch = actualMessage.match(/^vault-sync\s+op=([^\s]+)(?:\s+.*)?$/i);
8287
+ const operationId2 = vaultSyncMatch?.[1];
8288
+ const manualPeer = /^peer$/i.test(actualMessage.trim());
8289
+ const format = operationId2 ? "vault-sync" : manualPeer ? "manual" : "unknown";
8290
+ const classification = operationId2 ? stash.age_minutes <= STASH_RECENT_MINUTES ? "recent_known_peer_stash" : "stale_stash_backlog" : "unknown_stash_ownership";
8291
+ otherStashAudit.push({
8138
8292
  ref: stash.ref,
8139
- session_id,
8140
- cwd_hash,
8141
- timestamp,
8142
- summary,
8143
- age_minutes: stash.age_minutes
8293
+ oid: stash.oid,
8294
+ age_minutes: stash.age_minutes,
8295
+ classification,
8296
+ format,
8297
+ ...operationId2 ? { operation_id: operationId2 } : {}
8144
8298
  });
8145
8299
  }
8300
+ const stashAudit = [...wikiSyncAudit, ...otherStashAudit];
8301
+ const managedWriters = classifyManagedWriterProcesses(
8302
+ input.processSnapshot ?? managedWriterSnapshot()
8303
+ );
8146
8304
  const hintParts = [];
8147
8305
  if (locks.length > 0) hintParts.push(`${locks.length} lock(s)`);
8148
8306
  if (stashes.length > 0) hintParts.push(`${stashes.length} wiki-sync stash(es)`);
8307
+ if (managedWriters.count > 0) hintParts.push(`${managedWriters.count} live writer overlap(s)`);
8308
+ if (stashAudit.length > 0) hintParts.push(`${stashAudit.length} stash audit item(s)`);
8309
+ const blocking = locks.some((lock) => !lock.is_self) || managedWriters.blocking || stashAudit.some((entry) => entry.classification === "recent_known_peer_stash");
8149
8310
  const humanHint = hintParts.length > 0 ? hintParts.join(", ") : "no peers detected";
8150
8311
  return {
8151
8312
  exitCode: ExitCode.OK,
8152
8313
  result: ok({
8153
8314
  locks,
8154
8315
  stashes,
8316
+ stash_audit: stashAudit,
8317
+ managed_writers: managedWriters,
8318
+ blocking,
8155
8319
  humanHint
8156
8320
  })
8157
8321
  };
@@ -8585,7 +8749,7 @@ function releaseSnapshotFlock(handle) {
8585
8749
 
8586
8750
  // src/commands/backup.ts
8587
8751
  import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
8588
- import { join as join32, relative as relative3, dirname as dirname9 } from "path";
8752
+ import { join as join32, relative as relative3, dirname as dirname10 } from "path";
8589
8753
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
8590
8754
 
8591
8755
  // src/utils/s3-client.ts
@@ -8721,7 +8885,7 @@ async function runBackupRestore(input) {
8721
8885
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
8722
8886
  const body = await resp.Body?.transformToByteArray();
8723
8887
  if (body) {
8724
- mkdirSync5(dirname9(localPath), { recursive: true });
8888
+ mkdirSync5(dirname10(localPath), { recursive: true });
8725
8889
  writeFileSync6(localPath, Buffer.from(body));
8726
8890
  downloaded++;
8727
8891
  }
@@ -8843,7 +9007,7 @@ async function runStatus(input) {
8843
9007
  }
8844
9008
 
8845
9009
  // src/commands/seed.ts
8846
- import { mkdir as mkdir11, writeFile as writeFile8, stat as stat4 } from "fs/promises";
9010
+ import { mkdir as mkdir12, writeFile as writeFile8, stat as stat4 } from "fs/promises";
8847
9011
  import { join as join34 } from "path";
8848
9012
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8849
9013
  var EXAMPLE_PAGES = {
@@ -8925,7 +9089,7 @@ async function runSeed(input) {
8925
9089
  await stat4(absPath);
8926
9090
  skipped.push(relPath);
8927
9091
  } catch {
8928
- await mkdir11(join34(absPath, ".."), { recursive: true });
9092
+ await mkdir12(join34(absPath, ".."), { recursive: true });
8929
9093
  await writeFile8(absPath, content, "utf8");
8930
9094
  created.push(relPath);
8931
9095
  }
@@ -8935,7 +9099,7 @@ async function runSeed(input) {
8935
9099
  await stat4(rawPath);
8936
9100
  skipped.push("raw/articles/example-source.md");
8937
9101
  } catch {
8938
- await mkdir11(join34(rawPath, ".."), { recursive: true });
9102
+ await mkdir12(join34(rawPath, ".."), { recursive: true });
8939
9103
  await writeFile8(rawPath, EXAMPLE_RAW, "utf8");
8940
9104
  created.push("raw/articles/example-source.md");
8941
9105
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-SYKSL3JQ.js";
4
+ } from "./chunk-VXMHHXVP.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
  import "./chunk-QBZYEEBD.js";
7
7
  import "./chunk-PQG26AGJ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.25",
3
+ "version": "0.10.26",
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.25",
3
+ "version": "0.10.26",
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.25",
3
+ "version": "0.10.26",
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.25",
3
+ "version": "0.10.26",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",