@fenglimg/fabric-server 2.0.0-rc.21 → 2.0.0-rc.22

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.
@@ -172,13 +172,18 @@ function isNodeError(error) {
172
172
 
173
173
  // src/services/event-ledger.ts
174
174
  import { randomUUID } from "crypto";
175
- import { existsSync, fsyncSync, openSync, closeSync } from "fs";
176
- import { readFile as readFile2, truncate, writeFile } from "fs/promises";
175
+ import { existsSync, fsyncSync, openSync, closeSync, readFileSync, statSync } from "fs";
176
+ import { appendFile, mkdir as mkdir2, readFile as readFile2, truncate, writeFile } from "fs/promises";
177
+ import { join as join3 } from "path";
177
178
  import {
178
179
  eventLedgerEventSchema
179
180
  } from "@fenglimg/fabric-shared";
180
- import { createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
181
+ import { atomicWriteText as atomicWriteText2, createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
181
182
  var ledgerQueue = createLedgerWriteQueue();
183
+ var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
184
+ var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
185
+ var EVENT_LEDGER_ARCHIVE_DIR = ".fabric/events.archive";
186
+ var warnedOversize = false;
182
187
  async function appendEventLedgerEvent(projectRoot, event) {
183
188
  const eventPath = getEventLedgerPath(projectRoot);
184
189
  const nextEvent = eventLedgerEventSchema.parse({
@@ -190,6 +195,18 @@ async function appendEventLedgerEvent(projectRoot, event) {
190
195
  });
191
196
  await ensureParentDirectory(eventPath);
192
197
  await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
198
+ if (!warnedOversize) {
199
+ try {
200
+ const size = statSync(eventPath).size;
201
+ if (size > EVENT_LEDGER_SIZE_WARN_BYTES) {
202
+ warnedOversize = true;
203
+ process.stderr.write(
204
+ 'fabric: events.jsonl > 50MB, run "fab doctor --fix" to rotate\n'
205
+ );
206
+ }
207
+ } catch {
208
+ }
209
+ }
193
210
  return nextEvent;
194
211
  }
195
212
  async function readEventLedger(projectRoot, options = {}) {
@@ -265,6 +282,120 @@ function createDerivedId(index, line) {
265
282
  function isNodeError2(error) {
266
283
  return error instanceof Error;
267
284
  }
285
+ async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
286
+ const eventPath = getEventLedgerPath(projectRoot);
287
+ return ledgerQueue.runExclusive(eventPath, async () => {
288
+ const now = opts.now ?? /* @__PURE__ */ new Date();
289
+ const retentionDays = resolveRetentionDays(projectRoot, opts.retentionDays);
290
+ const cutoffMs = now.getTime() - retentionDays * 864e5;
291
+ let raw;
292
+ try {
293
+ raw = await readFile2(eventPath, "utf8");
294
+ } catch (error) {
295
+ if (isNodeError2(error) && error.code === "ENOENT") {
296
+ return { rotated: false, archivedCount: 0, keptCount: 0 };
297
+ }
298
+ throw error;
299
+ }
300
+ if (raw.length === 0) {
301
+ return { rotated: false, archivedCount: 0, keptCount: 0 };
302
+ }
303
+ const hasTrailingNewline = raw.endsWith("\n");
304
+ const segments = raw.split(/\r?\n/);
305
+ let keptTail = "";
306
+ if (!hasTrailingNewline && segments.length > 0) {
307
+ keptTail = segments.pop() ?? "";
308
+ }
309
+ const archived = [];
310
+ const kept = [];
311
+ for (const line of segments) {
312
+ const trimmed = line.trim();
313
+ if (trimmed.length === 0) continue;
314
+ let ts;
315
+ try {
316
+ const parsed = JSON.parse(trimmed);
317
+ const candidate = parsed["ts"];
318
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
319
+ ts = candidate;
320
+ }
321
+ } catch {
322
+ }
323
+ if (ts !== void 0 && ts < cutoffMs) {
324
+ archived.push(trimmed);
325
+ } else {
326
+ kept.push(trimmed);
327
+ }
328
+ }
329
+ if (archived.length === 0) {
330
+ return {
331
+ rotated: false,
332
+ archivedCount: 0,
333
+ keptCount: kept.length
334
+ };
335
+ }
336
+ const yyyymmdd = formatUtcDate(now);
337
+ const archiveDirAbsolute = join3(projectRoot, EVENT_LEDGER_ARCHIVE_DIR);
338
+ const archiveFilename = `events-rotated-${yyyymmdd}.jsonl`;
339
+ const archiveAbsolutePath = join3(archiveDirAbsolute, archiveFilename);
340
+ const archiveRelativePath = `${EVENT_LEDGER_ARCHIVE_DIR}/${archiveFilename}`;
341
+ await mkdir2(archiveDirAbsolute, { recursive: true });
342
+ await appendFile(
343
+ archiveAbsolutePath,
344
+ archived.map((line) => `${line}
345
+ `).join(""),
346
+ "utf8"
347
+ );
348
+ const auditEvent = eventLedgerEventSchema.parse({
349
+ kind: "fabric-event",
350
+ id: `event:${randomUUID()}`,
351
+ ts: now.getTime(),
352
+ schema_version: 1,
353
+ event_type: "events_rotated",
354
+ cutoff_ts: new Date(cutoffMs).toISOString(),
355
+ archived_count: archived.length,
356
+ kept_count: kept.length,
357
+ archive_path: archiveRelativePath
358
+ });
359
+ const newMainLines = [JSON.stringify(auditEvent), ...kept];
360
+ let newMainContent = newMainLines.join("\n") + "\n";
361
+ if (keptTail.length > 0) {
362
+ newMainContent += keptTail;
363
+ }
364
+ await atomicWriteText2(eventPath, newMainContent);
365
+ return {
366
+ rotated: true,
367
+ archivedCount: archived.length,
368
+ keptCount: kept.length,
369
+ archivePath: archiveRelativePath
370
+ };
371
+ });
372
+ }
373
+ function resolveRetentionDays(projectRoot, override) {
374
+ if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
375
+ return override;
376
+ }
377
+ const configPath = join3(projectRoot, ".fabric", "fabric-config.json");
378
+ try {
379
+ if (existsSync(configPath)) {
380
+ const raw = readFileSync(configPath, "utf8");
381
+ const parsed = JSON.parse(raw);
382
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
383
+ const v = parsed.fabric_event_retention_days;
384
+ if (v === 7 || v === 30 || v === 90) {
385
+ return v;
386
+ }
387
+ }
388
+ }
389
+ } catch {
390
+ }
391
+ return EVENT_LEDGER_DEFAULT_RETENTION_DAYS;
392
+ }
393
+ function formatUtcDate(date) {
394
+ const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
395
+ const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
396
+ const dd = date.getUTCDate().toString().padStart(2, "0");
397
+ return `${yyyy}-${mm}-${dd}`;
398
+ }
268
399
  function flushAndSyncEventLedger(projectRoot) {
269
400
  const ledgerPath = getEventLedgerPath(projectRoot);
270
401
  if (!existsSync(ledgerPath)) return;
@@ -277,10 +408,10 @@ function flushAndSyncEventLedger(projectRoot) {
277
408
  }
278
409
 
279
410
  // src/services/knowledge-meta-builder.ts
280
- import { mkdir as mkdir2, readdir, readFile as readFile3 } from "fs/promises";
281
- import { existsSync as existsSync2, statSync } from "fs";
411
+ import { mkdir as mkdir3, readdir, readFile as readFile3 } from "fs/promises";
412
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
282
413
  import { homedir } from "os";
283
- import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
414
+ import { isAbsolute, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
284
415
  import {
285
416
  KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
286
417
  agentsMetaSchema as agentsMetaSchema3,
@@ -296,12 +427,12 @@ import {
296
427
  StableIdSchema,
297
428
  parseKnowledgeId
298
429
  } from "@fenglimg/fabric-shared";
299
- import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
430
+ import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
300
431
  async function buildKnowledgeMeta(projectRootInput) {
301
432
  const projectRoot = normalizeProjectRoot(projectRootInput);
302
433
  assertExistingDirectory(projectRoot);
303
- const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
304
- const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
434
+ const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
435
+ const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
305
436
  const existingMeta = await readExistingMeta(metaPath);
306
437
  const existingKnowledgeTestIndex = await readExistingKnowledgeTestIndex(knowledgeTestIndexPath);
307
438
  const meta = await computeKnowledgeBasedAgentsMeta(projectRoot, existingMeta);
@@ -314,18 +445,18 @@ async function buildKnowledgeMeta(projectRootInput) {
314
445
  }
315
446
  async function writeKnowledgeMeta(projectRootInput, options) {
316
447
  const projectRoot = normalizeProjectRoot(projectRootInput);
317
- const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
318
- const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
448
+ const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
449
+ const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
319
450
  const existingMeta = await readExistingMeta(metaPath);
320
451
  const result = await buildKnowledgeMeta(projectRoot);
321
452
  if (!result.changed) {
322
453
  return result;
323
454
  }
324
455
  await ensureParentDirectory(metaPath);
325
- await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
456
+ await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
326
457
  `);
327
458
  await ensureParentDirectory(knowledgeTestIndexPath);
328
- await atomicWriteText2(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
459
+ await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
329
460
  `);
330
461
  if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
331
462
  await recordBaselineSynced(projectRoot, {
@@ -342,7 +473,7 @@ async function writeKnowledgeMeta(projectRootInput, options) {
342
473
  async function computeKnowledgeBasedAgentsMeta(projectRootInput, existingMeta) {
343
474
  const projectRoot = normalizeProjectRoot(projectRootInput);
344
475
  assertExistingDirectory(projectRoot);
345
- const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
476
+ const previousMeta = existingMeta ?? await readExistingMeta(join4(projectRoot, ".fabric", "agents.meta.json"));
346
477
  const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
347
478
  const ruleFiles = await findKnowledgeFiles(projectRoot);
348
479
  const nodes = {};
@@ -433,7 +564,7 @@ function normalizeProjectRoot(projectRoot) {
433
564
  return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
434
565
  }
435
566
  function assertExistingDirectory(projectRoot) {
436
- if (!existsSync2(projectRoot) || !statSync(projectRoot).isDirectory()) {
567
+ if (!existsSync2(projectRoot) || !statSync2(projectRoot).isDirectory()) {
437
568
  throw new Error(`Target directory does not exist: ${projectRoot}`);
438
569
  }
439
570
  }
@@ -477,17 +608,17 @@ function resolvePersonalRoot() {
477
608
  }
478
609
  function resolveContentRefPath(projectRoot, contentRef) {
479
610
  if (contentRef.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
480
- return join3(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
611
+ return join4(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
481
612
  }
482
- return join3(projectRoot, contentRef);
613
+ return join4(projectRoot, contentRef);
483
614
  }
484
615
  async function findKnowledgeFiles(projectRoot) {
485
- const teamRoot = join3(projectRoot, ".fabric", "knowledge");
486
- const personalRoot = join3(resolvePersonalRoot(), ".fabric", "knowledge");
616
+ const teamRoot = join4(projectRoot, ".fabric", "knowledge");
617
+ const personalRoot = join4(resolvePersonalRoot(), ".fabric", "knowledge");
487
618
  try {
488
- await mkdir2(personalRoot, { recursive: true });
619
+ await mkdir3(personalRoot, { recursive: true });
489
620
  for (const sub of KNOWLEDGE_SUBDIRS) {
490
- await mkdir2(join3(personalRoot, sub), { recursive: true });
621
+ await mkdir3(join4(personalRoot, sub), { recursive: true });
491
622
  }
492
623
  } catch {
493
624
  }
@@ -496,11 +627,11 @@ async function findKnowledgeFiles(projectRoot) {
496
627
  [teamRoot, TEAM_CONTENT_REF_PREFIX],
497
628
  [personalRoot, PERSONAL_CONTENT_REF_PREFIX]
498
629
  ]) {
499
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
630
+ if (!existsSync2(root) || !statSync2(root).isDirectory()) {
500
631
  continue;
501
632
  }
502
633
  for (const subdir of KNOWLEDGE_SUBDIRS) {
503
- const dir = join3(root, subdir);
634
+ const dir = join4(root, subdir);
504
635
  let entries;
505
636
  try {
506
637
  entries = await readdir(dir, { withFileTypes: true });
@@ -524,7 +655,7 @@ async function findFabricVerifyAnnotations(projectRoot) {
524
655
  const annotations = [];
525
656
  const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
526
657
  for (const testFile of files) {
527
- const source = await readFile3(join3(projectRoot, testFile), "utf8");
658
+ const source = await readFile3(join4(projectRoot, testFile), "utf8");
528
659
  const testHash = sha256(source);
529
660
  const lines = source.split(/\r?\n/u);
530
661
  for (const [index, line] of lines.entries()) {
@@ -552,7 +683,7 @@ async function findTestFiles(projectRoot) {
552
683
  continue;
553
684
  }
554
685
  for (const entry of await readdir(current, { withFileTypes: true })) {
555
- const absolutePath = join3(current, entry.name);
686
+ const absolutePath = join4(current, entry.name);
556
687
  const relativePath = toPosixPath(relative(projectRoot, absolutePath));
557
688
  const [rootSegment] = relativePath.split("/");
558
689
  if (entry.isDirectory()) {
@@ -802,23 +933,29 @@ function extractRuleDescription(source) {
802
933
  if (summary === void 0 || summary.length === 0) {
803
934
  return void 0;
804
935
  }
936
+ const knowledge = frontmatter !== null ? extractKnowledgeFieldsFromFrontmatter(frontmatter[1]) : void 0;
805
937
  return {
806
938
  summary,
807
939
  intent_clues: [],
808
940
  tech_stack: [],
809
941
  impact: [],
810
942
  must_read_if: summary,
811
- // v2.0 knowledge fields are absent in heading-only fallback.
812
- id: void 0,
813
- knowledge_type: void 0,
814
- maturity: void 0,
815
- knowledge_layer: void 0,
816
- layer_reason: void 0,
817
- created_at: void 0,
818
- tags: void 0,
819
- // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all.
820
- relevance_scope: "broad",
821
- relevance_paths: []
943
+ // v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
944
+ // when fully absent (no `---` block), all knowledge fields stay
945
+ // undefined, matching the original heading-only fallback contract.
946
+ id: knowledge?.id,
947
+ knowledge_type: knowledge?.knowledge_type,
948
+ maturity: knowledge?.maturity,
949
+ knowledge_layer: knowledge?.knowledge_layer,
950
+ layer_reason: knowledge?.layer_reason,
951
+ created_at: knowledge?.created_at,
952
+ tags: knowledge?.tags,
953
+ // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
954
+ // when frontmatter exists, honor its declared values (extractKnowledge
955
+ // FieldsFromFrontmatter already applies the broad-default for missing
956
+ // or malformed scopes).
957
+ relevance_scope: knowledge?.relevance_scope ?? "broad",
958
+ relevance_paths: knowledge?.relevance_paths ?? []
822
959
  };
823
960
  }
824
961
  function extractRuleSections(source) {
@@ -960,9 +1097,34 @@ function isNodeError3(error) {
960
1097
 
961
1098
  // src/services/knowledge-sync.ts
962
1099
  import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
963
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
964
- import { join as join4, relative as relative2, sep as sep3 } from "path";
1100
+ import { existsSync as existsSync3, statSync as statSync3 } from "fs";
1101
+ import { homedir as homedir2 } from "os";
1102
+ import { join as join5 } from "path";
965
1103
  import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
1104
+ var PERSONAL_CONTENT_REF_PREFIX2 = "~/.fabric/knowledge/";
1105
+ var TEAM_CONTENT_REF_PREFIX2 = ".fabric/knowledge/";
1106
+ var KNOWLEDGE_SUBDIRS2 = [
1107
+ "decisions",
1108
+ "pitfalls",
1109
+ "guidelines",
1110
+ "models",
1111
+ "processes",
1112
+ "pending"
1113
+ ];
1114
+ function resolvePersonalRoot2() {
1115
+ return process.env.FABRIC_HOME ?? homedir2();
1116
+ }
1117
+ function resolveContentRefPath2(projectRoot, relPath) {
1118
+ if (relPath.startsWith(PERSONAL_CONTENT_REF_PREFIX2)) {
1119
+ return join5(
1120
+ resolvePersonalRoot2(),
1121
+ ".fabric",
1122
+ "knowledge",
1123
+ relPath.slice(PERSONAL_CONTENT_REF_PREFIX2.length)
1124
+ );
1125
+ }
1126
+ return join5(projectRoot, relPath);
1127
+ }
966
1128
  var lastSyncState = /* @__PURE__ */ new Map();
967
1129
  var freshSyncCooldown = /* @__PURE__ */ new Map();
968
1130
  var SYNC_COOLDOWN_MS = 500;
@@ -970,7 +1132,7 @@ function invalidateKnowledgeSyncCooldown(projectRoot) {
970
1132
  freshSyncCooldown.delete(projectRoot);
971
1133
  }
972
1134
  async function readMetaEntries(projectRoot) {
973
- const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
1135
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
974
1136
  const map = /* @__PURE__ */ new Map();
975
1137
  let raw;
976
1138
  try {
@@ -995,32 +1157,36 @@ async function readMetaEntries(projectRoot) {
995
1157
  return map;
996
1158
  }
997
1159
  async function findRuleFiles(projectRoot) {
998
- const knowledgeRoot = join4(projectRoot, ".fabric", "knowledge");
999
- if (!existsSync3(knowledgeRoot) || !statSync2(knowledgeRoot).isDirectory()) {
1000
- return [];
1001
- }
1160
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge");
1161
+ const personalRoot = join5(resolvePersonalRoot2(), ".fabric", "knowledge");
1002
1162
  const files = [];
1003
- const stack = [knowledgeRoot];
1004
- while (stack.length > 0) {
1005
- const current = stack.pop();
1006
- if (current === void 0) {
1163
+ for (const [root, prefix] of [
1164
+ [teamRoot, TEAM_CONTENT_REF_PREFIX2],
1165
+ [personalRoot, PERSONAL_CONTENT_REF_PREFIX2]
1166
+ ]) {
1167
+ if (!existsSync3(root) || !statSync3(root).isDirectory()) {
1007
1168
  continue;
1008
1169
  }
1009
- for (const entry of await readdir2(current, { withFileTypes: true })) {
1010
- const absolutePath = join4(current, entry.name);
1011
- if (entry.isDirectory()) {
1012
- stack.push(absolutePath);
1013
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1014
- const rel = toPosixPath2(relative2(projectRoot, absolutePath));
1015
- files.push(rel);
1170
+ for (const subdir of KNOWLEDGE_SUBDIRS2) {
1171
+ const dir = join5(root, subdir);
1172
+ if (!existsSync3(dir) || !statSync3(dir).isDirectory()) {
1173
+ continue;
1174
+ }
1175
+ let entries;
1176
+ try {
1177
+ entries = await readdir2(dir, { withFileTypes: true });
1178
+ } catch {
1179
+ continue;
1180
+ }
1181
+ for (const entry of entries) {
1182
+ if (entry.isFile() && entry.name.endsWith(".md")) {
1183
+ files.push(`${prefix}${subdir}/${entry.name}`);
1184
+ }
1016
1185
  }
1017
1186
  }
1018
1187
  }
1019
1188
  return files.sort();
1020
1189
  }
1021
- function toPosixPath2(p) {
1022
- return p.split(sep3).join("/");
1023
- }
1024
1190
  function validateFrontmatter(source, filePath, throwOnInvalid) {
1025
1191
  if (!source.startsWith("---")) {
1026
1192
  return null;
@@ -1066,7 +1232,7 @@ function validateFrontmatter(source, filePath, throwOnInvalid) {
1066
1232
  return null;
1067
1233
  }
1068
1234
  async function processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter) {
1069
- const absPath = join4(projectRoot, relPath);
1235
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1070
1236
  try {
1071
1237
  await stat(absPath);
1072
1238
  } catch {
@@ -1165,7 +1331,7 @@ async function ensureKnowledgeFresh(projectRoot, opts) {
1165
1331
  }
1166
1332
  for (const [relPath, entry] of metaEntries) {
1167
1333
  if (!ruleFiles.includes(relPath)) {
1168
- const absPath = join4(projectRoot, relPath);
1334
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1169
1335
  if (!existsSync3(absPath)) {
1170
1336
  events.push({
1171
1337
  type: "rule_removed",
@@ -1217,7 +1383,7 @@ async function reconcileKnowledge(projectRoot, opts) {
1217
1383
  }
1218
1384
  for (const [relPath, entry] of metaEntries) {
1219
1385
  if (!ruleFiles.includes(relPath)) {
1220
- const absPath = join4(projectRoot, relPath);
1386
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1221
1387
  if (!existsSync3(absPath)) {
1222
1388
  events.push({
1223
1389
  type: "rule_removed",
@@ -1231,14 +1397,27 @@ async function reconcileKnowledge(projectRoot, opts) {
1231
1397
  }
1232
1398
  }
1233
1399
  }
1234
- if (events.length > 0) {
1400
+ let revisionDrift = false;
1401
+ try {
1402
+ const derived = await buildKnowledgeMeta(projectRoot);
1403
+ const onDisk = await readAgentsMeta(projectRoot);
1404
+ revisionDrift = onDisk.revision !== derived.meta.revision;
1405
+ } catch (error) {
1406
+ if (!(error instanceof AgentsMetaFileMissingError) && !(error instanceof AgentsMetaInvalidError)) {
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ if (events.length > 0 || revisionDrift) {
1235
1411
  await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
1236
- await appendRuleSyncEvents(projectRoot, events);
1412
+ if (events.length > 0) {
1413
+ await appendRuleSyncEvents(projectRoot, events);
1414
+ }
1237
1415
  contextCache.invalidate("file_watch", projectRoot);
1416
+ contextCache.invalidate("meta_write", projectRoot);
1238
1417
  }
1239
1418
  const duration_ms = Date.now() - startTime;
1240
1419
  const reconciledFiles = events.map((e) => e.path);
1241
- if (trigger !== void 0 && events.length > 0) {
1420
+ if (trigger !== void 0 && (events.length > 0 || revisionDrift)) {
1242
1421
  if (trigger === "startup") {
1243
1422
  await appendEventLedgerEvent(projectRoot, {
1244
1423
  event_type: "meta_reconciled_on_startup",
@@ -1252,11 +1431,12 @@ async function reconcileKnowledge(projectRoot, opts) {
1252
1431
  reconciled_files: reconciledFiles,
1253
1432
  duration_ms,
1254
1433
  trigger,
1255
- source: "reconcileKnowledge"
1434
+ source: "reconcileKnowledge",
1435
+ ...events.length === 0 && revisionDrift ? { force_write_reason: "revision_drift" } : {}
1256
1436
  });
1257
1437
  }
1258
1438
  }
1259
- if (events.length === 0 && warnings.length === 0) {
1439
+ if (events.length === 0 && warnings.length === 0 && !revisionDrift) {
1260
1440
  return { status: "fresh", events: [], warnings: [] };
1261
1441
  }
1262
1442
  const status = warnings.length > 0 ? "errors" : "reconciled";
@@ -1270,11 +1450,11 @@ async function reconcileKnowledge(projectRoot, opts) {
1270
1450
 
1271
1451
  // src/services/doctor.ts
1272
1452
  import { execFileSync } from "child_process";
1273
- import { existsSync as existsSync4, readdirSync, readFileSync, statSync as statSync3 } from "fs";
1274
- import { access, mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
1453
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
1454
+ import { access, mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
1275
1455
  import { constants } from "fs";
1276
- import { homedir as homedir2 } from "os";
1277
- import { isAbsolute as isAbsolute2, join as join5, posix, relative as nodeRelative, resolve as resolve3, sep as sep4 } from "path";
1456
+ import { homedir as homedir3 } from "os";
1457
+ import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
1278
1458
  import { minimatch } from "minimatch";
1279
1459
  import {
1280
1460
  agentsMetaSchema as agentsMetaSchema4,
@@ -1289,7 +1469,7 @@ import {
1289
1469
  BOOTSTRAP_REGEX
1290
1470
  } from "@fenglimg/fabric-shared";
1291
1471
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1292
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1472
+ import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
1293
1473
  var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
1294
1474
  stable: 90,
1295
1475
  endorsed: 30,
@@ -1327,7 +1507,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
1327
1507
  "processes"
1328
1508
  ];
1329
1509
  var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
1330
- var KNOWLEDGE_SUBDIRS2 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1510
+ var KNOWLEDGE_SUBDIRS3 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1511
+ var BASELINE_FILENAME_LINT_BASELINE_IDS = /* @__PURE__ */ new Set([
1512
+ "KT-MOD-0001",
1513
+ // tech-stack
1514
+ "KT-MOD-0002",
1515
+ // module-structure
1516
+ "KT-MOD-0003",
1517
+ // readme-first-paragraph
1518
+ "KT-PRO-0001",
1519
+ // build-config
1520
+ "KT-PRO-0002",
1521
+ // ci-config
1522
+ "KT-GLD-0001"
1523
+ // code-style
1524
+ ]);
1525
+ var BASELINE_ID_PREFIXED_FILENAME_PATTERN = /^KT-[A-Z]+-\d+--.+\.md$/u;
1331
1526
  var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
1332
1527
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1333
1528
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -1386,6 +1581,7 @@ async function runDoctorReport(target) {
1386
1581
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1387
1582
  const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1388
1583
  const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
1584
+ const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
1389
1585
  const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1390
1586
  const counterDesync = inspectCounterDesync(meta);
1391
1587
  const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
@@ -1418,6 +1614,10 @@ async function runDoctorReport(target) {
1418
1614
  createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
1419
1615
  createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
1420
1616
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
1617
+ // v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
1618
+ // knowledge_dir_missing — both are knowledge-layout invariants. manual_error
1619
+ // kind; resolution delegates to `fab scan` (no --fix path).
1620
+ createBaselineFilenameFormatCheck(baselineFilenameFormat),
1421
1621
  createForensicCheck(forensic, framework.kind, entryPoints.length),
1422
1622
  // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
1423
1623
  // is owned by the AI-side client init skill, not by `fabric install` CLI.
@@ -1517,7 +1717,7 @@ async function runDoctorReport(target) {
1517
1717
  warningCount: warnings.length,
1518
1718
  infoCount: infos.length,
1519
1719
  targetFiles: Object.fromEntries(
1520
- TARGET_FILE_PATHS.map((path) => [path, existsSync4(join5(projectRoot, path))])
1720
+ TARGET_FILE_PATHS.map((path) => [path, existsSync4(join6(projectRoot, path))])
1521
1721
  )
1522
1722
  }
1523
1723
  };
@@ -1544,9 +1744,9 @@ async function runDoctorFix(target) {
1544
1744
  }
1545
1745
  }
1546
1746
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
1547
- const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
1747
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
1548
1748
  await ensureParentDirectory(snapshotPath);
1549
- await atomicWriteText3(snapshotPath, BOOTSTRAP_CANONICAL);
1749
+ await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
1550
1750
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
1551
1751
  }
1552
1752
  if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
@@ -1566,26 +1766,23 @@ async function runDoctorFix(target) {
1566
1766
  fixed.push(findIssue(before.fixable_errors, "counter_desync"));
1567
1767
  contextCache.invalidate("meta_write", projectRoot);
1568
1768
  }
1569
- if (before.fixable_errors.some(
1570
- (issue) => [
1571
- "agents_meta_missing",
1572
- "agents_meta_stale",
1573
- "knowledge_test_index_missing",
1574
- "knowledge_test_index_stale",
1575
- "content_ref_missing",
1576
- "knowledge_dir_unindexed"
1577
- ].includes(issue.code)
1578
- )) {
1769
+ const reconcileCodes = [
1770
+ "agents_meta_missing",
1771
+ "agents_meta_stale",
1772
+ "knowledge_test_index_missing",
1773
+ "knowledge_test_index_stale",
1774
+ "content_ref_missing",
1775
+ "knowledge_dir_unindexed"
1776
+ ];
1777
+ if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
1579
1778
  await reconcileKnowledge(projectRoot, { trigger: "doctor" });
1580
1779
  for (const issue of before.fixable_errors.filter(
1581
- (candidate) => [
1582
- "agents_meta_missing",
1583
- "agents_meta_stale",
1584
- "knowledge_test_index_missing",
1585
- "knowledge_test_index_stale",
1586
- "content_ref_missing",
1587
- "knowledge_dir_unindexed"
1588
- ].includes(candidate.code)
1780
+ (candidate) => reconcileCodes.includes(candidate.code)
1781
+ )) {
1782
+ fixed.push(issue);
1783
+ }
1784
+ for (const issue of before.warnings.filter(
1785
+ (candidate) => reconcileCodes.includes(candidate.code)
1589
1786
  )) {
1590
1787
  fixed.push(issue);
1591
1788
  }
@@ -1604,6 +1801,15 @@ async function runDoctorFix(target) {
1604
1801
  });
1605
1802
  fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
1606
1803
  }
1804
+ const rotateResult = await rotateEventLedgerIfNeeded(projectRoot);
1805
+ if (rotateResult.rotated && rotateResult.archivedCount > 0) {
1806
+ fixed.push({
1807
+ code: "event_ledger_rotated",
1808
+ name: "Event ledger rotated",
1809
+ message: `Rotated ${rotateResult.archivedCount} event(s) older than retention window to ${rotateResult.archivePath ?? "archive"}`,
1810
+ path: rotateResult.archivePath
1811
+ });
1812
+ }
1607
1813
  if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
1608
1814
  await fixMcpConfigInWrongFile(projectRoot);
1609
1815
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
@@ -1743,7 +1949,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1743
1949
  };
1744
1950
  }
1745
1951
  const detail = `${candidate.maturity} -> ${next}`;
1746
- const absPath = join5(projectRoot, candidate.path);
1952
+ const absPath = join6(projectRoot, candidate.path);
1747
1953
  try {
1748
1954
  const source = await readFile5(absPath, "utf8");
1749
1955
  const rewritten = rewriteFrontmatterMaturity(source, next);
@@ -1765,7 +1971,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1765
1971
  error: "rewrite produced byte-identical output"
1766
1972
  };
1767
1973
  }
1768
- await atomicWriteText3(absPath, rewritten);
1974
+ await atomicWriteText4(absPath, rewritten);
1769
1975
  try {
1770
1976
  await appendEventLedgerEvent(projectRoot, {
1771
1977
  event_type: "knowledge_demoted",
@@ -1775,7 +1981,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1775
1981
  });
1776
1982
  } catch (ledgerError) {
1777
1983
  try {
1778
- await atomicWriteText3(absPath, source);
1984
+ await atomicWriteText4(absPath, source);
1779
1985
  } catch (rollbackError) {
1780
1986
  return {
1781
1987
  kind: "knowledge_orphan_demote_required",
@@ -1810,11 +2016,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1810
2016
  }
1811
2017
  }
1812
2018
  async function applyStaleArchive(projectRoot, candidate, now) {
1813
- const sourceAbs = join5(projectRoot, candidate.path);
1814
- const destAbs = join5(projectRoot, candidate.archive_path);
2019
+ const sourceAbs = join6(projectRoot, candidate.path);
2020
+ const destAbs = join6(projectRoot, candidate.archive_path);
1815
2021
  const detail = `${candidate.path} -> ${candidate.archive_path}`;
1816
2022
  try {
1817
- await mkdir3(join5(destAbs, ".."), { recursive: true });
2023
+ await mkdir4(join6(destAbs, ".."), { recursive: true });
1818
2024
  try {
1819
2025
  await rename(sourceAbs, destAbs);
1820
2026
  } catch (renameError) {
@@ -1873,7 +2079,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
1873
2079
  async function applyPendingAutoArchive(projectRoot, candidate, now) {
1874
2080
  const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
1875
2081
  try {
1876
- await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
2082
+ await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
1877
2083
  let moved = false;
1878
2084
  if (candidate.layer === "team") {
1879
2085
  try {
@@ -1946,11 +2152,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1946
2152
  }
1947
2153
  function relativePosix(projectRoot, absolutePath) {
1948
2154
  const rel = nodeRelative(projectRoot, absolutePath);
1949
- return rel.split(sep4).join("/");
2155
+ return rel.split(sep3).join("/");
1950
2156
  }
1951
2157
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1952
2158
  const detail = `deleted (${candidate.age_days}d old)`;
1953
- const absPath = join5(projectRoot, candidate.path);
2159
+ const absPath = join6(projectRoot, candidate.path);
1954
2160
  try {
1955
2161
  const { unlink } = await import("fs/promises");
1956
2162
  await unlink(absPath);
@@ -1971,7 +2177,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1971
2177
  }
1972
2178
  }
1973
2179
  async function applyIndexDriftFix(projectRoot, inspection) {
1974
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2180
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
1975
2181
  const detailParts = [];
1976
2182
  try {
1977
2183
  const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
@@ -2007,7 +2213,7 @@ function truncateErrorMessage(error) {
2007
2213
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
2008
2214
  }
2009
2215
  async function inspectForensic(projectRoot) {
2010
- const path = join5(projectRoot, ".fabric", "forensic.json");
2216
+ const path = join6(projectRoot, ".fabric", "forensic.json");
2011
2217
  try {
2012
2218
  const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
2013
2219
  return { present: true, valid: true, report: parsed };
@@ -2019,12 +2225,12 @@ async function inspectForensic(projectRoot) {
2019
2225
  }
2020
2226
  }
2021
2227
  function inspectMcpConfigInWrongFile(projectRoot) {
2022
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
2228
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
2023
2229
  if (!existsSync4(settingsPath)) {
2024
2230
  return { hasWrongEntry: false, settingsPath };
2025
2231
  }
2026
2232
  try {
2027
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2233
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
2028
2234
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2029
2235
  return { hasWrongEntry: false, settingsPath };
2030
2236
  }
@@ -2040,7 +2246,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
2040
2246
  }
2041
2247
  }
2042
2248
  async function inspectMeta(projectRoot) {
2043
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2249
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
2044
2250
  const built = await tryBuildRuleMeta(projectRoot);
2045
2251
  try {
2046
2252
  const raw = await readFile5(metaPath, "utf8");
@@ -2113,7 +2319,7 @@ function inspectContentRefs(projectRoot, meta) {
2113
2319
  if (isPersonalKnowledge) {
2114
2320
  continue;
2115
2321
  }
2116
- if (!existsSync4(join5(projectRoot, contentRef))) {
2322
+ if (!existsSync4(join6(projectRoot, contentRef))) {
2117
2323
  missing.push(contentRef);
2118
2324
  }
2119
2325
  }
@@ -2155,7 +2361,7 @@ async function inspectEventLedger(projectRoot) {
2155
2361
  }
2156
2362
  }
2157
2363
  async function inspectKnowledgeTestIndex(projectRoot) {
2158
- const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2364
+ const path = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2159
2365
  const built = await tryBuildRuleMeta(projectRoot);
2160
2366
  try {
2161
2367
  const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
@@ -2179,8 +2385,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
2179
2385
  }
2180
2386
  function inspectBootstrapAnchor(projectRoot) {
2181
2387
  return {
2182
- hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
2183
- hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
2388
+ hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
2389
+ hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
2184
2390
  };
2185
2391
  }
2186
2392
  var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
@@ -2192,7 +2398,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
2192
2398
  async function inspectBootstrapMarkerMigration(target) {
2193
2399
  const filesNeedingMigration = [];
2194
2400
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
2195
- const abs = join5(target, rel);
2401
+ const abs = join6(target, rel);
2196
2402
  if (!existsSync4(abs)) {
2197
2403
  continue;
2198
2404
  }
@@ -2226,7 +2432,7 @@ function createBootstrapMarkerMigrationCheck(inspection) {
2226
2432
  );
2227
2433
  }
2228
2434
  async function inspectL1BootstrapSnapshotDrift(target) {
2229
- const abs = join5(target, ".fabric", "AGENTS.md");
2435
+ const abs = join6(target, ".fabric", "AGENTS.md");
2230
2436
  if (!existsSync4(abs)) {
2231
2437
  return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2232
2438
  }
@@ -2258,7 +2464,7 @@ function createL1BootstrapSnapshotDriftCheck(inspection) {
2258
2464
  );
2259
2465
  }
2260
2466
  async function inspectL2ManagedBlockDrift(target) {
2261
- const snapshotPath = join5(target, ".fabric", "AGENTS.md");
2467
+ const snapshotPath = join6(target, ".fabric", "AGENTS.md");
2262
2468
  if (!existsSync4(snapshotPath)) {
2263
2469
  return { status: "ok", drifted: [] };
2264
2470
  }
@@ -2268,7 +2474,7 @@ async function inspectL2ManagedBlockDrift(target) {
2268
2474
  } catch {
2269
2475
  return { status: "ok", drifted: [] };
2270
2476
  }
2271
- const projectRulesPath = join5(target, ".fabric", "project-rules.md");
2477
+ const projectRulesPath = join6(target, ".fabric", "project-rules.md");
2272
2478
  let expectedBody = snapshot;
2273
2479
  if (existsSync4(projectRulesPath)) {
2274
2480
  try {
@@ -2282,8 +2488,8 @@ ${projectRules}`;
2282
2488
  const drifted = [];
2283
2489
  let anyManagedBlockFound = false;
2284
2490
  const blockTargets = [
2285
- join5(target, "AGENTS.md"),
2286
- join5(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2491
+ join6(target, "AGENTS.md"),
2492
+ join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2287
2493
  ];
2288
2494
  for (const abs of blockTargets) {
2289
2495
  if (!existsSync4(abs)) {
@@ -2317,7 +2523,7 @@ ${projectRules}`;
2317
2523
  drifted.push({ path: abs, expected: expectedBody, actual: body });
2318
2524
  }
2319
2525
  }
2320
- const claudeMdPath = join5(target, "CLAUDE.md");
2526
+ const claudeMdPath = join6(target, "CLAUDE.md");
2321
2527
  if (existsSync4(claudeMdPath)) {
2322
2528
  let claudeContent;
2323
2529
  try {
@@ -2381,16 +2587,82 @@ function createBootstrapAnchorCheck(inspection) {
2381
2587
  return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
2382
2588
  }
2383
2589
  function inspectKnowledgeDirMissing(projectRoot) {
2384
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2590
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2385
2591
  const missingSubdirs = [];
2386
- for (const sub of KNOWLEDGE_SUBDIRS2) {
2387
- const path = join5(knowledgeRoot, sub);
2592
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
2593
+ const path = join6(knowledgeRoot, sub);
2388
2594
  if (!existsSync4(path)) {
2389
2595
  missingSubdirs.push(`.fabric/knowledge/${sub}`);
2390
2596
  }
2391
2597
  }
2392
2598
  return { missingSubdirs };
2393
2599
  }
2600
+ function inspectBaselineFilenameFormat(projectRoot) {
2601
+ const offenders = [];
2602
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2603
+ if (!existsSync4(knowledgeRoot)) {
2604
+ return { offenders };
2605
+ }
2606
+ for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2607
+ const dir = join6(knowledgeRoot, sub);
2608
+ if (!existsSync4(dir)) {
2609
+ continue;
2610
+ }
2611
+ let entries;
2612
+ try {
2613
+ entries = readdirSync(dir, { withFileTypes: true });
2614
+ } catch {
2615
+ continue;
2616
+ }
2617
+ for (const entry of entries) {
2618
+ const entryName = entry.name;
2619
+ if (!entry.isFile() || !entryName.endsWith(".md")) {
2620
+ continue;
2621
+ }
2622
+ if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
2623
+ continue;
2624
+ }
2625
+ const abs = join6(dir, entryName);
2626
+ let source;
2627
+ try {
2628
+ source = readFileSync2(abs, "utf8");
2629
+ } catch {
2630
+ continue;
2631
+ }
2632
+ const id = extractKnowledgeFrontmatterId(source);
2633
+ if (id === null) {
2634
+ continue;
2635
+ }
2636
+ if (!BASELINE_FILENAME_LINT_BASELINE_IDS.has(id)) {
2637
+ continue;
2638
+ }
2639
+ offenders.push({
2640
+ path: posix.join(".fabric/knowledge", sub, entryName),
2641
+ stable_id: id
2642
+ });
2643
+ }
2644
+ }
2645
+ offenders.sort((a, b) => a.path.localeCompare(b.path));
2646
+ return { offenders };
2647
+ }
2648
+ function createBaselineFilenameFormatCheck(inspection) {
2649
+ if (inspection.offenders.length === 0) {
2650
+ return okCheck(
2651
+ "Baseline filename format",
2652
+ "All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
2653
+ );
2654
+ }
2655
+ const first = inspection.offenders[0];
2656
+ const detail = `${first.stable_id} at ${first.path}`;
2657
+ return issueCheck(
2658
+ "Baseline filename format",
2659
+ "error",
2660
+ "manual_error",
2661
+ "lint-baseline-filename-format",
2662
+ `${inspection.offenders.length} baseline knowledge file${inspection.offenders.length === 1 ? "" : "s"} use${inspection.offenders.length === 1 ? "s" : ""} the deprecated bare-slug filename format and must be migrated to \`\${id}--\${slug}.md\`. First: ${detail}.`,
2663
+ "Run `fab scan` to auto-migrate baseline filenames to the canonical `${id}--${slug}.md` format."
2664
+ );
2665
+ }
2394
2666
  function createKnowledgeDirMissingCheck(inspection) {
2395
2667
  if (inspection.missingSubdirs.length > 0) {
2396
2668
  const list = inspection.missingSubdirs.join(", ");
@@ -2405,7 +2677,7 @@ function createKnowledgeDirMissingCheck(inspection) {
2405
2677
  }
2406
2678
  return okCheck(
2407
2679
  "Knowledge layout",
2408
- `All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
2680
+ `All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
2409
2681
  );
2410
2682
  }
2411
2683
  function createForensicCheck(forensic, frameworkKind, entryPointCount) {
@@ -2434,11 +2706,11 @@ function createMetaCheck(meta) {
2434
2706
  if (meta.stale) {
2435
2707
  return issueCheck(
2436
2708
  "Agents metadata",
2437
- "error",
2438
- "fixable_error",
2709
+ "warn",
2710
+ "warning",
2439
2711
  "agents_meta_stale",
2440
2712
  `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2441
- "Run `fab doctor --fix` to reconcile agents.meta.json with the current knowledge files."
2713
+ "Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
2442
2714
  );
2443
2715
  }
2444
2716
  return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
@@ -2551,7 +2823,7 @@ function findIssue(issues, code) {
2551
2823
  };
2552
2824
  }
2553
2825
  async function inspectMetaManuallyDiverged(projectRoot) {
2554
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2826
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
2555
2827
  if (!existsSync4(metaPath)) {
2556
2828
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2557
2829
  }
@@ -2571,13 +2843,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2571
2843
  const hashMismatchEntries = [];
2572
2844
  for (const node of Object.values(meta.nodes)) {
2573
2845
  const contentRef = node.content_ref ?? node.file;
2574
- const absPath = join5(projectRoot, contentRef);
2846
+ const absPath = join6(projectRoot, contentRef);
2575
2847
  if (!existsSync4(absPath)) {
2576
2848
  extraMetaEntries.push(contentRef);
2577
2849
  continue;
2578
2850
  }
2579
2851
  try {
2580
- const content = readFileSync(absPath, "utf8");
2852
+ const content = readFileSync2(absPath, "utf8");
2581
2853
  const diskHash = sha256(content);
2582
2854
  if (node.hash !== "" && node.hash !== diskHash) {
2583
2855
  hashMismatchEntries.push(contentRef);
@@ -2590,7 +2862,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2590
2862
  }
2591
2863
  function inspectKnowledgeDirUnindexed(projectRoot, meta) {
2592
2864
  const physicalMdFiles = /* @__PURE__ */ new Set();
2593
- collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2865
+ collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2594
2866
  if (physicalMdFiles.size === 0) {
2595
2867
  return { unindexedFiles: [] };
2596
2868
  }
@@ -2615,7 +2887,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
2615
2887
  continue;
2616
2888
  }
2617
2889
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2618
- const abs = join5(dir, entry.name);
2890
+ const abs = join6(dir, entry.name);
2619
2891
  if (entry.isDirectory()) {
2620
2892
  stack.push(abs);
2621
2893
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2640,7 +2912,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
2640
2912
  }
2641
2913
  async function inspectStableIdCollisions(projectRoot) {
2642
2914
  const found = [];
2643
- const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
2915
+ const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
2644
2916
  if (existsSync4(knowledgeDir)) {
2645
2917
  const stack = [knowledgeDir];
2646
2918
  while (stack.length > 0) {
@@ -2649,7 +2921,7 @@ async function inspectStableIdCollisions(projectRoot) {
2649
2921
  continue;
2650
2922
  }
2651
2923
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2652
- const abs = join5(dir, entry.name);
2924
+ const abs = join6(dir, entry.name);
2653
2925
  if (entry.isDirectory()) {
2654
2926
  stack.push(abs);
2655
2927
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2805,17 +3077,17 @@ function createMetaManuallyDivergedCheck(inspection) {
2805
3077
  }
2806
3078
  function inspectPreexistingRootFiles(projectRoot) {
2807
3079
  const candidates = ["CLAUDE.md", "AGENTS.md"];
2808
- const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
3080
+ const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
2809
3081
  return { detected };
2810
3082
  }
2811
3083
  async function inspectFilesystemEditFallback(projectRoot) {
2812
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3084
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2813
3085
  if (!existsSync4(knowledgeRoot)) {
2814
3086
  return { synthesized: 0, synthesizedStableIds: [] };
2815
3087
  }
2816
3088
  const canonicalIds = /* @__PURE__ */ new Set();
2817
3089
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2818
- const dir = join5(knowledgeRoot, typeDir);
3090
+ const dir = join6(knowledgeRoot, typeDir);
2819
3091
  if (!existsSync4(dir)) {
2820
3092
  continue;
2821
3093
  }
@@ -3017,12 +3289,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
3017
3289
  return Number.isFinite(parsed) ? parsed : null;
3018
3290
  }
3019
3291
  function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3020
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3292
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
3021
3293
  if (!existsSync4(knowledgeRoot)) {
3022
3294
  return;
3023
3295
  }
3024
3296
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3025
- const dir = join5(knowledgeRoot, typeDir);
3297
+ const dir = join6(knowledgeRoot, typeDir);
3026
3298
  if (!existsSync4(dir)) {
3027
3299
  continue;
3028
3300
  }
@@ -3041,10 +3313,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3041
3313
  continue;
3042
3314
  }
3043
3315
  const stableId = match[1];
3044
- const absPath = join5(dir, entry.name);
3316
+ const absPath = join6(dir, entry.name);
3045
3317
  let source;
3046
3318
  try {
3047
- source = readFileSync(absPath, "utf8");
3319
+ source = readFileSync2(absPath, "utf8");
3048
3320
  } catch {
3049
3321
  continue;
3050
3322
  }
@@ -3057,7 +3329,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3057
3329
  let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
3058
3330
  if (lastReferenceMs === 0) {
3059
3331
  try {
3060
- lastReferenceMs = statSync3(absPath).mtimeMs;
3332
+ lastReferenceMs = statSync4(absPath).mtimeMs;
3061
3333
  } catch {
3062
3334
  lastReferenceMs = 0;
3063
3335
  }
@@ -3117,8 +3389,8 @@ async function inspectStaleArchive(projectRoot, now) {
3117
3389
  return { candidates };
3118
3390
  }
3119
3391
  function* iteratePendingFiles(projectRoot, now) {
3120
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3121
- const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
3392
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
3393
+ const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
3122
3394
  for (const [layer, root, displayPrefix] of [
3123
3395
  ["team", teamRoot, ".fabric/knowledge/pending"],
3124
3396
  ["personal", personalRoot, "~/.fabric/knowledge/pending"]
@@ -3133,7 +3405,7 @@ function* iteratePendingFiles(projectRoot, now) {
3133
3405
  continue;
3134
3406
  }
3135
3407
  for (const typeDir of typeDirs) {
3136
- const dir = join5(root, typeDir);
3408
+ const dir = join6(root, typeDir);
3137
3409
  let entries;
3138
3410
  try {
3139
3411
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3144,17 +3416,17 @@ function* iteratePendingFiles(projectRoot, now) {
3144
3416
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
3145
3417
  continue;
3146
3418
  }
3147
- const absPath = join5(dir, entry.name);
3419
+ const absPath = join6(dir, entry.name);
3148
3420
  let source = "";
3149
3421
  try {
3150
- source = readFileSync(absPath, "utf8");
3422
+ source = readFileSync2(absPath, "utf8");
3151
3423
  } catch {
3152
3424
  continue;
3153
3425
  }
3154
3426
  const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
3155
3427
  let mtimeMs = 0;
3156
3428
  try {
3157
- mtimeMs = statSync3(absPath).mtimeMs;
3429
+ mtimeMs = statSync4(absPath).mtimeMs;
3158
3430
  } catch {
3159
3431
  mtimeMs = 0;
3160
3432
  }
@@ -3188,7 +3460,7 @@ function* iteratePendingFiles(projectRoot, now) {
3188
3460
  }
3189
3461
  }
3190
3462
  function resolvePersonalRootForPending() {
3191
- return process.env.FABRIC_HOME ?? homedir2();
3463
+ return process.env.FABRIC_HOME ?? homedir3();
3192
3464
  }
3193
3465
  function inspectPendingOverdue(projectRoot, now) {
3194
3466
  const candidates = [];
@@ -3219,7 +3491,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
3219
3491
  pending_path: visit.pending_path,
3220
3492
  pending_path_abs: visit.pending_path_abs,
3221
3493
  archived_to: archivedToRel,
3222
- archived_to_abs: join5(projectRoot, archivedToRel),
3494
+ archived_to_abs: join6(projectRoot, archivedToRel),
3223
3495
  age_days: visit.age_days
3224
3496
  });
3225
3497
  } else {
@@ -3228,7 +3500,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
3228
3500
  visit.type,
3229
3501
  visit.filename
3230
3502
  );
3231
- const archivedToAbs = join5(
3503
+ const archivedToAbs = join6(
3232
3504
  resolvePersonalRootForPending(),
3233
3505
  ".fabric",
3234
3506
  ".archive",
@@ -3252,11 +3524,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
3252
3524
  }
3253
3525
  function inspectUnderseeded(projectRoot) {
3254
3526
  const threshold = readUnderseedThresholdFromConfig(projectRoot);
3255
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3527
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
3256
3528
  let nodeCount = 0;
3257
3529
  if (existsSync4(knowledgeRoot)) {
3258
3530
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3259
- const dir = join5(knowledgeRoot, typeDir);
3531
+ const dir = join6(knowledgeRoot, typeDir);
3260
3532
  if (!existsSync4(dir)) continue;
3261
3533
  let entries;
3262
3534
  try {
@@ -3278,7 +3550,7 @@ function inspectUnderseeded(projectRoot) {
3278
3550
  };
3279
3551
  }
3280
3552
  function inspectSessionHintsStale(projectRoot, now) {
3281
- const cacheDir = join5(projectRoot, ".fabric", ".cache");
3553
+ const cacheDir = join6(projectRoot, ".fabric", ".cache");
3282
3554
  if (!existsSync4(cacheDir)) {
3283
3555
  return { candidates: [] };
3284
3556
  }
@@ -3293,10 +3565,10 @@ function inspectSessionHintsStale(projectRoot, now) {
3293
3565
  if (!entry.isFile()) continue;
3294
3566
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
3295
3567
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
3296
- const absPath = join5(cacheDir, entry.name);
3568
+ const absPath = join6(cacheDir, entry.name);
3297
3569
  let mtimeMs = 0;
3298
3570
  try {
3299
- mtimeMs = statSync3(absPath).mtimeMs;
3571
+ mtimeMs = statSync4(absPath).mtimeMs;
3300
3572
  } catch {
3301
3573
  continue;
3302
3574
  }
@@ -3323,11 +3595,11 @@ function inspectNarrowTooFew(projectRoot, now) {
3323
3595
  const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
3324
3596
  const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
3325
3597
  const editFires = readCounterTimestamps(
3326
- join5(projectRoot, EDIT_COUNTER_FILE_REL),
3598
+ join6(projectRoot, EDIT_COUNTER_FILE_REL),
3327
3599
  windowStartMs
3328
3600
  );
3329
3601
  const silenceFires = readCounterTimestamps(
3330
- join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3602
+ join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3331
3603
  windowStartMs
3332
3604
  );
3333
3605
  const telemetrySkipped = editFires === 0;
@@ -3349,7 +3621,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
3349
3621
  if (!existsSync4(absPath)) return 0;
3350
3622
  let raw;
3351
3623
  try {
3352
- raw = readFileSync(absPath, "utf8");
3624
+ raw = readFileSync2(absPath, "utf8");
3353
3625
  } catch {
3354
3626
  return 0;
3355
3627
  }
@@ -3365,10 +3637,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
3365
3637
  return count;
3366
3638
  }
3367
3639
  function readUnderseedThresholdFromConfig(projectRoot) {
3368
- const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
3640
+ const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
3369
3641
  if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3370
3642
  try {
3371
- const raw = readFileSync(configPath, "utf8");
3643
+ const raw = readFileSync2(configPath, "utf8");
3372
3644
  const parsed = JSON.parse(raw);
3373
3645
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3374
3646
  const v = parsed.underseed_node_threshold;
@@ -3498,11 +3770,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
3498
3770
  }
3499
3771
  function* iterateRelevanceFrontmatter(projectRoot) {
3500
3772
  for (const visit of iterateCanonicalFilenames(projectRoot)) {
3501
- const layerRoot = visit.layer === "team" ? join5(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3502
- const absPath = join5(layerRoot, visit.type, visit.filename);
3773
+ const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3774
+ const absPath = join6(layerRoot, visit.type, visit.filename);
3503
3775
  let source;
3504
3776
  try {
3505
- source = readFileSync(absPath, "utf8");
3777
+ source = readFileSync2(absPath, "utf8");
3506
3778
  } catch {
3507
3779
  continue;
3508
3780
  }
@@ -3569,7 +3841,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3569
3841
  }
3570
3842
  let rootStat;
3571
3843
  try {
3572
- rootStat = statSync3(projectRoot);
3844
+ rootStat = statSync4(projectRoot);
3573
3845
  } catch {
3574
3846
  return [];
3575
3847
  }
@@ -3588,7 +3860,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3588
3860
  continue;
3589
3861
  }
3590
3862
  for (const entry of entries) {
3591
- const abs = join5(current, entry.name);
3863
+ const abs = join6(current, entry.name);
3592
3864
  const rel = normalizePath(abs.slice(projectRoot.length + 1));
3593
3865
  if (rel.length === 0) continue;
3594
3866
  if (entry.isDirectory()) {
@@ -3728,8 +4000,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3728
4000
  const candidates = [];
3729
4001
  let scannedCount = 0;
3730
4002
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3731
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3732
- const personalRoot = join5(
4003
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
4004
+ const personalRoot = join6(
3733
4005
  resolvePersonalRootForPending(),
3734
4006
  ".fabric",
3735
4007
  "knowledge",
@@ -3749,7 +4021,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3749
4021
  continue;
3750
4022
  }
3751
4023
  for (const typeDir of typeDirs) {
3752
- const dir = join5(root, typeDir);
4024
+ const dir = join6(root, typeDir);
3753
4025
  let entries;
3754
4026
  try {
3755
4027
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3760,10 +4032,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3760
4032
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
3761
4033
  continue;
3762
4034
  }
3763
- const absPath = join5(dir, entry.name);
4035
+ const absPath = join6(dir, entry.name);
3764
4036
  let source;
3765
4037
  try {
3766
- source = readFileSync(absPath, "utf8");
4038
+ source = readFileSync2(absPath, "utf8");
3767
4039
  } catch {
3768
4040
  continue;
3769
4041
  }
@@ -3847,7 +4119,7 @@ async function applyRelevanceFieldsMissing(candidate) {
3847
4119
  error: "fields already present at write time (no diff)"
3848
4120
  };
3849
4121
  }
3850
- await atomicWriteText3(candidate.pending_path_abs, rewritten);
4122
+ await atomicWriteText4(candidate.pending_path_abs, rewritten);
3851
4123
  return {
3852
4124
  kind: "knowledge_relevance_fields_missing",
3853
4125
  path: candidate.pending_path,
@@ -3891,7 +4163,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
3891
4163
  function inspectSkillMdYamlInvalid(projectRoot) {
3892
4164
  const candidates = [];
3893
4165
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
3894
- const rootAbs = join5(projectRoot, rootRel);
4166
+ const rootAbs = join6(projectRoot, rootRel);
3895
4167
  if (!existsSync4(rootAbs)) continue;
3896
4168
  let dirEntries;
3897
4169
  try {
@@ -3901,11 +4173,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
3901
4173
  }
3902
4174
  for (const dirEntry of dirEntries) {
3903
4175
  if (!dirEntry.isDirectory()) continue;
3904
- const skillFile = join5(rootAbs, dirEntry.name, "SKILL.md");
4176
+ const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
3905
4177
  if (!existsSync4(skillFile)) continue;
3906
4178
  let raw;
3907
4179
  try {
3908
- raw = readFileSync(skillFile, "utf8");
4180
+ raw = readFileSync2(skillFile, "utf8");
3909
4181
  } catch {
3910
4182
  continue;
3911
4183
  }
@@ -4005,8 +4277,8 @@ function createNarrowTooFewCheck(inspection) {
4005
4277
  );
4006
4278
  }
4007
4279
  function resolvePersonalKnowledgeRoot() {
4008
- const home = process.env.FABRIC_HOME ?? homedir2();
4009
- return join5(home, ".fabric", "knowledge");
4280
+ const home = process.env.FABRIC_HOME ?? homedir3();
4281
+ return join6(home, ".fabric", "knowledge");
4010
4282
  }
4011
4283
  function parseStableIdFromCanonicalFilename(filename) {
4012
4284
  const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
@@ -4026,7 +4298,7 @@ function parseStableIdFromCanonicalFilename(filename) {
4026
4298
  };
4027
4299
  }
4028
4300
  function* iterateCanonicalFilenames(projectRoot) {
4029
- const teamRoot = join5(projectRoot, ".fabric", "knowledge");
4301
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge");
4030
4302
  const personalRoot = resolvePersonalKnowledgeRoot();
4031
4303
  for (const [layer, root, displayPrefix] of [
4032
4304
  ["team", teamRoot, ".fabric/knowledge"],
@@ -4036,7 +4308,7 @@ function* iterateCanonicalFilenames(projectRoot) {
4036
4308
  continue;
4037
4309
  }
4038
4310
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
4039
- const dir = join5(root, typeDir);
4311
+ const dir = join6(root, typeDir);
4040
4312
  if (!existsSync4(dir)) {
4041
4313
  continue;
4042
4314
  }
@@ -4196,7 +4468,7 @@ async function migrateBootstrapMarkers(projectRoot) {
4196
4468
  const paths = [];
4197
4469
  const countPerPath = {};
4198
4470
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
4199
- const abs = join5(projectRoot, rel);
4471
+ const abs = join6(projectRoot, rel);
4200
4472
  if (!existsSync4(abs)) {
4201
4473
  continue;
4202
4474
  }
@@ -4216,14 +4488,14 @@ async function migrateBootstrapMarkers(projectRoot) {
4216
4488
  if (rewritten === original) {
4217
4489
  continue;
4218
4490
  }
4219
- await atomicWriteText3(abs, rewritten);
4491
+ await atomicWriteText4(abs, rewritten);
4220
4492
  paths.push(abs);
4221
4493
  countPerPath[abs] = replacedCount;
4222
4494
  }
4223
4495
  return { paths, countPerPath };
4224
4496
  }
4225
4497
  async function rewriteThreeEndManagedBlocks(projectRoot) {
4226
- const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
4498
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
4227
4499
  if (!existsSync4(snapshotPath)) {
4228
4500
  return;
4229
4501
  }
@@ -4233,7 +4505,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
4233
4505
  } catch {
4234
4506
  return;
4235
4507
  }
4236
- const projectRulesPath = join5(projectRoot, ".fabric", "project-rules.md");
4508
+ const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
4237
4509
  const hasProjectRules = existsSync4(projectRulesPath);
4238
4510
  let expectedBody = snapshot;
4239
4511
  if (hasProjectRules) {
@@ -4249,8 +4521,8 @@ ${projectRules}`;
4249
4521
  ${expectedBody}
4250
4522
  ${BOOTSTRAP_MARKER_END}`;
4251
4523
  const blockTargets = [
4252
- join5(projectRoot, "AGENTS.md"),
4253
- join5(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4524
+ join6(projectRoot, "AGENTS.md"),
4525
+ join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4254
4526
  ];
4255
4527
  for (const abs of blockTargets) {
4256
4528
  if (!existsSync4(abs)) {
@@ -4281,9 +4553,9 @@ ${managedBlock}
4281
4553
  if (next === existing) {
4282
4554
  continue;
4283
4555
  }
4284
- await atomicWriteText3(abs, next);
4556
+ await atomicWriteText4(abs, next);
4285
4557
  }
4286
- const claudeMdPath = join5(projectRoot, "CLAUDE.md");
4558
+ const claudeMdPath = join6(projectRoot, "CLAUDE.md");
4287
4559
  if (existsSync4(claudeMdPath)) {
4288
4560
  let claudeContent;
4289
4561
  try {
@@ -4307,18 +4579,18 @@ ${managedBlock}
4307
4579
  ensureLine("@.fabric/project-rules.md");
4308
4580
  }
4309
4581
  if (updated !== claudeContent) {
4310
- await atomicWriteText3(claudeMdPath, updated);
4582
+ await atomicWriteText4(claudeMdPath, updated);
4311
4583
  }
4312
4584
  }
4313
4585
  }
4314
4586
  async function fixMcpConfigInWrongFile(projectRoot) {
4315
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
4587
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
4316
4588
  if (!existsSync4(settingsPath)) {
4317
4589
  return;
4318
4590
  }
4319
4591
  let settings;
4320
4592
  try {
4321
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
4593
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
4322
4594
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4323
4595
  return;
4324
4596
  }
@@ -4345,12 +4617,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
4345
4617
  });
4346
4618
  }
4347
4619
  async function ensureKnowledgeSubdirs(projectRoot) {
4348
- for (const sub of KNOWLEDGE_SUBDIRS2) {
4349
- await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
4620
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
4621
+ await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
4350
4622
  }
4351
4623
  }
4352
4624
  async function fixCounterDesync(projectRoot) {
4353
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
4625
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
4354
4626
  if (!existsSync4(metaPath)) {
4355
4627
  return;
4356
4628
  }
@@ -4673,7 +4945,7 @@ function normalizePath(path) {
4673
4945
  return posix.normalize(path.split("\\").join("/"));
4674
4946
  }
4675
4947
  function collectEntryPoints(root) {
4676
- if (!existsSync4(root) || !statSync3(root).isDirectory()) {
4948
+ if (!existsSync4(root) || !statSync4(root).isDirectory()) {
4677
4949
  return [];
4678
4950
  }
4679
4951
  const entries = [];
@@ -4684,7 +4956,7 @@ function collectEntryPoints(root) {
4684
4956
  continue;
4685
4957
  }
4686
4958
  for (const entry of readdirSync(current, { withFileTypes: true })) {
4687
- const absolutePath = join5(current, entry.name);
4959
+ const absolutePath = join6(current, entry.name);
4688
4960
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
4689
4961
  if (relativePath.length === 0) {
4690
4962
  continue;
@@ -4741,9 +5013,109 @@ function isMissingFileError(error) {
4741
5013
  return error instanceof Error && "code" in error && error.code === "ENOENT";
4742
5014
  }
4743
5015
 
5016
+ // src/services/load-active-meta.ts
5017
+ async function loadActiveMeta(projectRoot, opts = {}) {
5018
+ const onDisk = await readAgentsMeta(projectRoot);
5019
+ const previousRevisionHash = onDisk.revision;
5020
+ const derived = await buildKnowledgeMeta(projectRoot);
5021
+ if (derived.meta.revision === previousRevisionHash) {
5022
+ return {
5023
+ meta: onDisk,
5024
+ auto_healed: false,
5025
+ previous_revision_hash: previousRevisionHash,
5026
+ revision_hash: previousRevisionHash
5027
+ };
5028
+ }
5029
+ const written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5030
+ contextCache.invalidate("meta_write", projectRoot);
5031
+ await emitAutoHealEventBestEffort(projectRoot, {
5032
+ previous_revision_hash: previousRevisionHash,
5033
+ revision_hash: written.meta.revision,
5034
+ caller: opts.caller
5035
+ });
5036
+ return {
5037
+ meta: written.meta,
5038
+ auto_healed: true,
5039
+ previous_revision_hash: previousRevisionHash,
5040
+ revision_hash: written.meta.revision
5041
+ };
5042
+ }
5043
+ async function loadActiveMetaOrStale(projectRoot, opts = {}) {
5044
+ let onDisk;
5045
+ try {
5046
+ onDisk = await readAgentsMeta(projectRoot);
5047
+ } catch (error) {
5048
+ if (error instanceof AgentsMetaFileMissingError || error instanceof AgentsMetaInvalidError) {
5049
+ throw error;
5050
+ }
5051
+ throw error;
5052
+ }
5053
+ const previousRevisionHash = onDisk.revision;
5054
+ let derived;
5055
+ try {
5056
+ derived = await buildKnowledgeMeta(projectRoot);
5057
+ } catch (error) {
5058
+ return {
5059
+ meta: onDisk,
5060
+ auto_healed: false,
5061
+ previous_revision_hash: previousRevisionHash,
5062
+ revision_hash: previousRevisionHash,
5063
+ degraded: true,
5064
+ error: error instanceof Error ? error.message : String(error)
5065
+ };
5066
+ }
5067
+ if (derived.meta.revision === previousRevisionHash) {
5068
+ return {
5069
+ meta: onDisk,
5070
+ auto_healed: false,
5071
+ previous_revision_hash: previousRevisionHash,
5072
+ revision_hash: previousRevisionHash,
5073
+ degraded: false
5074
+ };
5075
+ }
5076
+ let written;
5077
+ try {
5078
+ written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5079
+ } catch (error) {
5080
+ return {
5081
+ meta: onDisk,
5082
+ auto_healed: false,
5083
+ previous_revision_hash: previousRevisionHash,
5084
+ revision_hash: previousRevisionHash,
5085
+ degraded: true,
5086
+ error: error instanceof Error ? error.message : String(error)
5087
+ };
5088
+ }
5089
+ contextCache.invalidate("meta_write", projectRoot);
5090
+ await emitAutoHealEventBestEffort(projectRoot, {
5091
+ previous_revision_hash: previousRevisionHash,
5092
+ revision_hash: written.meta.revision,
5093
+ caller: opts.caller
5094
+ });
5095
+ return {
5096
+ meta: written.meta,
5097
+ auto_healed: true,
5098
+ previous_revision_hash: previousRevisionHash,
5099
+ revision_hash: written.meta.revision,
5100
+ degraded: false
5101
+ };
5102
+ }
5103
+ async function emitAutoHealEventBestEffort(projectRoot, payload) {
5104
+ try {
5105
+ await appendEventLedgerEvent(projectRoot, {
5106
+ event_type: "knowledge_meta_auto_healed",
5107
+ previous_revision_hash: payload.previous_revision_hash,
5108
+ revision_hash: payload.revision_hash,
5109
+ trigger: "read",
5110
+ ...payload.caller !== void 0 ? { caller: payload.caller } : {}
5111
+ });
5112
+ } catch {
5113
+ }
5114
+ }
5115
+
4744
5116
  // src/services/get-knowledge.ts
4745
5117
  import { readFile as readFile6 } from "fs/promises";
4746
- import { join as join6 } from "path";
5118
+ import { join as join7 } from "path";
4747
5119
  import { minimatch as minimatch2 } from "minimatch";
4748
5120
  var PRIORITY_ORDER = {
4749
5121
  high: 0,
@@ -4751,6 +5123,10 @@ var PRIORITY_ORDER = {
4751
5123
  low: 2
4752
5124
  };
4753
5125
  async function getKnowledge(projectRoot, input) {
5126
+ const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
5127
+ if (metaResult.auto_healed) {
5128
+ contextCache.invalidate("file_watch", projectRoot);
5129
+ }
4754
5130
  const context = await loadGetKnowledgeContext(projectRoot);
4755
5131
  const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
4756
5132
  const matchedNodes = matchRuleNodes(context.meta, input.path);
@@ -4783,7 +5159,7 @@ async function loadGetKnowledgeContext(projectRoot) {
4783
5159
  return cached;
4784
5160
  }
4785
5161
  const meta = await readAgentsMeta(projectRoot);
4786
- const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
5162
+ const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
4787
5163
  const context = {
4788
5164
  meta,
4789
5165
  l0Content,
@@ -4922,13 +5298,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
4922
5298
  if (cached !== void 0) {
4923
5299
  return await cached;
4924
5300
  }
4925
- const pending = readFile6(join6(projectRoot, file), "utf8");
5301
+ const pending = readFile6(join7(projectRoot, file), "utf8");
4926
5302
  fileContentCache.set(file, pending);
4927
5303
  return await pending;
4928
5304
  }
4929
5305
 
4930
5306
  export {
4931
5307
  contextCache,
5308
+ AgentsMetaFileMissingError,
4932
5309
  resolveProjectRoot,
4933
5310
  readAgentsMeta,
4934
5311
  LEDGER_PATH,
@@ -4955,6 +5332,8 @@ export {
4955
5332
  invalidateKnowledgeSyncCooldown,
4956
5333
  ensureKnowledgeFresh,
4957
5334
  reconcileKnowledge,
5335
+ loadActiveMeta,
5336
+ loadActiveMetaOrStale,
4958
5337
  getKnowledge,
4959
5338
  normalizeKnowledgePath,
4960
5339
  runDoctorReport,