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

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.
@@ -160,8 +160,8 @@ function getLegacyLedgerPath(projectRoot) {
160
160
  function getEventLedgerPath(projectRoot) {
161
161
  return join2(projectRoot, EVENT_LEDGER_PATH);
162
162
  }
163
- async function ensureParentDirectory(path) {
164
- await mkdir(dirname(path), { recursive: true });
163
+ async function ensureParentDirectory(path2) {
164
+ await mkdir(dirname(path2), { recursive: true });
165
165
  }
166
166
  function sha256(content) {
167
167
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
@@ -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 = {}) {
@@ -224,24 +241,24 @@ async function readEventLedger(projectRoot, options = {}) {
224
241
  const events = lines.map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
225
242
  return { events, warnings };
226
243
  }
227
- async function truncateLedgerToLastNewline(path) {
228
- const raw = await readFile2(path);
244
+ async function truncateLedgerToLastNewline(path2) {
245
+ const raw = await readFile2(path2);
229
246
  const content = raw.toString("utf8");
230
247
  if (content.endsWith("\n") || content.length === 0) {
231
248
  return { truncated_bytes: 0, corrupted_path: "" };
232
249
  }
233
250
  const lastNewlineIndex = content.lastIndexOf("\n");
234
251
  if (lastNewlineIndex === -1) {
235
- const corruptedPath2 = `${path}.corrupted.${Date.now()}`;
252
+ const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
236
253
  await writeFile(corruptedPath2, raw);
237
- await truncate(path, 0);
254
+ await truncate(path2, 0);
238
255
  return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
239
256
  }
240
257
  const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
241
258
  const corruptedBytes = raw.slice(keepByteLength);
242
- const corruptedPath = `${path}.corrupted.${Date.now()}`;
259
+ const corruptedPath = `${path2}.corrupted.${Date.now()}`;
243
260
  await writeFile(corruptedPath, corruptedBytes);
244
- await truncate(path, keepByteLength);
261
+ await truncate(path2, keepByteLength);
245
262
  return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
246
263
  }
247
264
  function parseEventLedgerLine(line, index) {
@@ -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()) {
@@ -739,8 +870,8 @@ function flattenKeys(value, keys = {}) {
739
870
  }
740
871
  return keys;
741
872
  }
742
- function toPosixPath(path) {
743
- return path.split(sep2).join("/");
873
+ function toPosixPath(path2) {
874
+ return path2.split(sep2).join("/");
744
875
  }
745
876
  function deriveRuleIdentity(file, source, existing) {
746
877
  const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
@@ -802,27 +933,33 @@ 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) {
825
- const sections = Array.from(source.matchAll(/^(?:#{2,6})\s+\[([A-Z_]+)\]\s*$/gmu)).map((match) => match[1]).filter((section, index, all) => all.indexOf(section) === index);
962
+ const sections = Array.from(source.matchAll(/^#{2,6}\s+(.+?)\s*$/gmu)).map((match) => match[1].trim()).filter((section, index, all) => section.length > 0 && all.indexOf(section) === index);
826
963
  return sections.length > 0 ? sections : void 0;
827
964
  }
828
965
  function extractDescriptionFromFrontmatter(frontmatter) {
@@ -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 {
@@ -985,42 +1147,46 @@ async function readMetaEntries(projectRoot) {
985
1147
  return map;
986
1148
  }
987
1149
  for (const node of Object.values(parsed.nodes ?? {})) {
988
- const path = node.content_ref ?? node.file;
1150
+ const path2 = node.content_ref ?? node.file;
989
1151
  const stable_id = node.stable_id;
990
1152
  const content_hash = node.hash;
991
- if (path !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
992
- map.set(path, { stable_id, path, content_hash });
1153
+ if (path2 !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
1154
+ map.set(path2, { stable_id, path: path2, content_hash });
993
1155
  }
994
1156
  }
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,28 @@ 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
+ const forceWriteForDescriptionHeal = trigger === "auto-heal-description";
1411
+ if (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal) {
1235
1412
  await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
1236
- await appendRuleSyncEvents(projectRoot, events);
1413
+ if (events.length > 0) {
1414
+ await appendRuleSyncEvents(projectRoot, events);
1415
+ }
1237
1416
  contextCache.invalidate("file_watch", projectRoot);
1417
+ contextCache.invalidate("meta_write", projectRoot);
1238
1418
  }
1239
1419
  const duration_ms = Date.now() - startTime;
1240
1420
  const reconciledFiles = events.map((e) => e.path);
1241
- if (trigger !== void 0 && events.length > 0) {
1421
+ if (trigger !== void 0 && (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal)) {
1242
1422
  if (trigger === "startup") {
1243
1423
  await appendEventLedgerEvent(projectRoot, {
1244
1424
  event_type: "meta_reconciled_on_startup",
@@ -1252,11 +1432,12 @@ async function reconcileKnowledge(projectRoot, opts) {
1252
1432
  reconciled_files: reconciledFiles,
1253
1433
  duration_ms,
1254
1434
  trigger,
1255
- source: "reconcileKnowledge"
1435
+ source: "reconcileKnowledge",
1436
+ ...events.length === 0 && revisionDrift ? { force_write_reason: "revision_drift" } : {}
1256
1437
  });
1257
1438
  }
1258
1439
  }
1259
- if (events.length === 0 && warnings.length === 0) {
1440
+ if (events.length === 0 && warnings.length === 0 && !revisionDrift) {
1260
1441
  return { status: "fresh", events: [], warnings: [] };
1261
1442
  }
1262
1443
  const status = warnings.length > 0 ? "errors" : "reconciled";
@@ -1268,13 +1449,106 @@ async function reconcileKnowledge(projectRoot, opts) {
1268
1449
  };
1269
1450
  }
1270
1451
 
1452
+ // src/services/serve-lock.ts
1453
+ import fs from "fs";
1454
+ import path from "path";
1455
+ import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
1456
+ import { IOFabricError as IOFabricError2 } from "@fenglimg/fabric-shared/errors";
1457
+ var LOCK_FILENAME = ".serve.lock";
1458
+ var t = createTranslator(detectNodeLocale());
1459
+ var ServeLockHeldError = class extends IOFabricError2 {
1460
+ code = "SERVE_LOCK_HELD";
1461
+ httpStatus = 423;
1462
+ };
1463
+ function lockPath(projectRoot) {
1464
+ return path.join(projectRoot, ".fabric", LOCK_FILENAME);
1465
+ }
1466
+ function isAlive(pid) {
1467
+ try {
1468
+ process.kill(pid, 0);
1469
+ return true;
1470
+ } catch (e) {
1471
+ const err = e;
1472
+ if (err.code === "ESRCH") return false;
1473
+ if (err.code === "EPERM") return true;
1474
+ throw e;
1475
+ }
1476
+ }
1477
+ function acquireLock(projectRoot, opts) {
1478
+ const p = lockPath(projectRoot);
1479
+ if (fs.existsSync(p)) {
1480
+ let state = null;
1481
+ try {
1482
+ state = JSON.parse(fs.readFileSync(p, "utf8"));
1483
+ } catch {
1484
+ }
1485
+ if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
1486
+ throw new ServeLockHeldError(
1487
+ `serve lock held by live PID ${state.pid}`,
1488
+ {
1489
+ actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1490
+ details: state
1491
+ }
1492
+ );
1493
+ }
1494
+ if (state && state.pid && !isAlive(state.pid)) {
1495
+ process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
1496
+ `);
1497
+ }
1498
+ }
1499
+ fs.mkdirSync(path.dirname(p), { recursive: true });
1500
+ fs.writeFileSync(
1501
+ p,
1502
+ JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
1503
+ );
1504
+ }
1505
+ function releaseLock(projectRoot) {
1506
+ const p = lockPath(projectRoot);
1507
+ try {
1508
+ if (fs.existsSync(p)) {
1509
+ const state = JSON.parse(fs.readFileSync(p, "utf8"));
1510
+ if (state.pid === process.pid) {
1511
+ fs.unlinkSync(p);
1512
+ }
1513
+ }
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ function readLockState(projectRoot) {
1518
+ const p = lockPath(projectRoot);
1519
+ if (!fs.existsSync(p)) return null;
1520
+ try {
1521
+ return JSON.parse(fs.readFileSync(p, "utf8"));
1522
+ } catch {
1523
+ return null;
1524
+ }
1525
+ }
1526
+ function checkLockOrThrow(projectRoot, opts) {
1527
+ const state = readLockState(projectRoot);
1528
+ if (state === null) return;
1529
+ if (state.pid === process.pid) return;
1530
+ if (!isAlive(state.pid)) {
1531
+ process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
1532
+ `);
1533
+ return;
1534
+ }
1535
+ if (opts?.force) return;
1536
+ throw new ServeLockHeldError(
1537
+ `serve lock held by live PID ${state.pid}`,
1538
+ {
1539
+ actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1540
+ details: state
1541
+ }
1542
+ );
1543
+ }
1544
+
1271
1545
  // src/services/doctor.ts
1272
1546
  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";
1547
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
1548
+ import { access, mkdir as mkdir4, readFile as readFile5, rename, unlink, writeFile as writeFile2 } from "fs/promises";
1275
1549
  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";
1550
+ import { homedir as homedir3 } from "os";
1551
+ import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
1278
1552
  import { minimatch } from "minimatch";
1279
1553
  import {
1280
1554
  agentsMetaSchema as agentsMetaSchema4,
@@ -1286,10 +1560,12 @@ import {
1286
1560
  BOOTSTRAP_CANONICAL,
1287
1561
  BOOTSTRAP_MARKER_BEGIN,
1288
1562
  BOOTSTRAP_MARKER_END,
1289
- BOOTSTRAP_REGEX
1563
+ BOOTSTRAP_REGEX,
1564
+ ONBOARD_SLOT_NAMES,
1565
+ ONBOARD_SLOT_TOTAL
1290
1566
  } from "@fenglimg/fabric-shared";
1291
1567
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1292
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1568
+ import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
1293
1569
  var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
1294
1570
  stable: 90,
1295
1571
  endorsed: 30,
@@ -1327,7 +1603,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
1327
1603
  "processes"
1328
1604
  ];
1329
1605
  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"];
1606
+ var KNOWLEDGE_SUBDIRS3 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1607
+ var BASELINE_FILENAME_LINT_BASELINE_IDS = /* @__PURE__ */ new Set([
1608
+ "KT-MOD-0001",
1609
+ // tech-stack
1610
+ "KT-MOD-0002",
1611
+ // module-structure
1612
+ "KT-MOD-0003",
1613
+ // readme-first-paragraph
1614
+ "KT-PRO-0001",
1615
+ // build-config
1616
+ "KT-PRO-0002",
1617
+ // ci-config
1618
+ "KT-GLD-0001"
1619
+ // code-style
1620
+ ]);
1621
+ var BASELINE_ID_PREFIXED_FILENAME_PATTERN = /^KT-[A-Z]+-\d+--.+\.md$/u;
1331
1622
  var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
1332
1623
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1333
1624
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -1386,6 +1677,7 @@ async function runDoctorReport(target) {
1386
1677
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1387
1678
  const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1388
1679
  const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
1680
+ const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
1389
1681
  const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1390
1682
  const counterDesync = inspectCounterDesync(meta);
1391
1683
  const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
@@ -1404,8 +1696,10 @@ async function runDoctorReport(target) {
1404
1696
  const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
1405
1697
  const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
1406
1698
  const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
1699
+ const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
1407
1700
  const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
1408
1701
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1702
+ const onboardCoverage = inspectOnboardCoverage(projectRoot);
1409
1703
  const checks = [
1410
1704
  createBootstrapAnchorCheck(bootstrapAnchor),
1411
1705
  // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
@@ -1418,6 +1712,11 @@ async function runDoctorReport(target) {
1418
1712
  createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
1419
1713
  createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
1420
1714
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
1715
+ // v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
1716
+ // knowledge_dir_missing — both are knowledge-layout invariants. manual_error
1717
+ // kind; resolution is manual file deletion (rc.23 TASK-012 (F8a) removed
1718
+ // the baseline-emit pipeline, so no auto-fix exists).
1719
+ createBaselineFilenameFormatCheck(baselineFilenameFormat),
1421
1720
  createForensicCheck(forensic, framework.kind, entryPoints.length),
1422
1721
  // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
1423
1722
  // is owned by the AI-side client init skill, not by `fabric install` CLI.
@@ -1470,6 +1769,10 @@ async function runDoctorReport(target) {
1470
1769
  createNarrowTooFewCheck(narrowTooFew),
1471
1770
  // rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
1472
1771
  createSessionHintsStaleCheck(sessionHintsStale),
1772
+ // rc.23 TASK-010 (e): stale .fabric/.serve.lock advisory. Info kind —
1773
+ // does not bump report status. `--fix` unlinks the corpse and emits
1774
+ // `serve_lock_cleared`.
1775
+ createStaleServeLockCheck(staleServeLock),
1473
1776
  // v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
1474
1777
  // Info kind — applies to pending entries only; canonical entries get
1475
1778
  // the fields written verbatim by fab_review.approve/modify.
@@ -1477,6 +1780,11 @@ async function runDoctorReport(target) {
1477
1780
  // rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
1478
1781
  // SKILL.md frontmatter that Codex CLI silently drops at load.
1479
1782
  createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
1783
+ // v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
1784
+ // Surfaces uncovered S5 onboard slots and recommends /fabric-archive
1785
+ // first-run phase. Sits adjacent to Skill markdown YAML — both are
1786
+ // Skill-adjacent advisories. --fix never mutates onboard state.
1787
+ createOnboardCoverageCheck(onboardCoverage),
1480
1788
  createPreexistingRootFilesCheck(preexistingRootFiles)
1481
1789
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1482
1790
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
@@ -1517,7 +1825,7 @@ async function runDoctorReport(target) {
1517
1825
  warningCount: warnings.length,
1518
1826
  infoCount: infos.length,
1519
1827
  targetFiles: Object.fromEntries(
1520
- TARGET_FILE_PATHS.map((path) => [path, existsSync4(join5(projectRoot, path))])
1828
+ TARGET_FILE_PATHS.map((path2) => [path2, existsSync4(join6(projectRoot, path2))])
1521
1829
  )
1522
1830
  }
1523
1831
  };
@@ -1531,11 +1839,11 @@ async function runDoctorFix(target) {
1531
1839
  )) {
1532
1840
  const migrated = await migrateBootstrapMarkers(projectRoot);
1533
1841
  fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
1534
- for (const path of migrated.paths) {
1842
+ for (const path2 of migrated.paths) {
1535
1843
  await appendEventLedgerEvent(projectRoot, {
1536
1844
  event_type: "bootstrap_marker_migrated",
1537
- path,
1538
- migrated_count: migrated.countPerPath[path] ?? 1,
1845
+ path: path2,
1846
+ migrated_count: migrated.countPerPath[path2] ?? 1,
1539
1847
  legacy_marker: "fabric:knowledge-base",
1540
1848
  new_marker: "fabric:bootstrap",
1541
1849
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -1544,9 +1852,9 @@ async function runDoctorFix(target) {
1544
1852
  }
1545
1853
  }
1546
1854
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
1547
- const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
1855
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
1548
1856
  await ensureParentDirectory(snapshotPath);
1549
- await atomicWriteText3(snapshotPath, BOOTSTRAP_CANONICAL);
1857
+ await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
1550
1858
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
1551
1859
  }
1552
1860
  if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
@@ -1566,26 +1874,23 @@ async function runDoctorFix(target) {
1566
1874
  fixed.push(findIssue(before.fixable_errors, "counter_desync"));
1567
1875
  contextCache.invalidate("meta_write", projectRoot);
1568
1876
  }
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
- )) {
1877
+ const reconcileCodes = [
1878
+ "agents_meta_missing",
1879
+ "agents_meta_stale",
1880
+ "knowledge_test_index_missing",
1881
+ "knowledge_test_index_stale",
1882
+ "content_ref_missing",
1883
+ "knowledge_dir_unindexed"
1884
+ ];
1885
+ if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
1579
1886
  await reconcileKnowledge(projectRoot, { trigger: "doctor" });
1580
1887
  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)
1888
+ (candidate) => reconcileCodes.includes(candidate.code)
1889
+ )) {
1890
+ fixed.push(issue);
1891
+ }
1892
+ for (const issue of before.warnings.filter(
1893
+ (candidate) => reconcileCodes.includes(candidate.code)
1589
1894
  )) {
1590
1895
  fixed.push(issue);
1591
1896
  }
@@ -1604,10 +1909,44 @@ async function runDoctorFix(target) {
1604
1909
  });
1605
1910
  fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
1606
1911
  }
1912
+ const rotateResult = await rotateEventLedgerIfNeeded(projectRoot);
1913
+ if (rotateResult.rotated && rotateResult.archivedCount > 0) {
1914
+ fixed.push({
1915
+ code: "event_ledger_rotated",
1916
+ name: "Event ledger rotated",
1917
+ message: `Rotated ${rotateResult.archivedCount} event(s) older than retention window to ${rotateResult.archivePath ?? "archive"}`,
1918
+ path: rotateResult.archivePath
1919
+ });
1920
+ }
1607
1921
  if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
1608
1922
  await fixMcpConfigInWrongFile(projectRoot);
1609
1923
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
1610
1924
  }
1925
+ if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
1926
+ const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
1927
+ if (lockInspection.present && !lockInspection.pidAlive) {
1928
+ const lockFilePath = join6(projectRoot, ".fabric", ".serve.lock");
1929
+ try {
1930
+ await unlink(lockFilePath);
1931
+ } catch (err) {
1932
+ const errno = err;
1933
+ if (errno.code !== "ENOENT") throw err;
1934
+ }
1935
+ await appendEventLedgerEvent(projectRoot, {
1936
+ event_type: "serve_lock_cleared",
1937
+ pid: lockInspection.pid,
1938
+ age_ms: lockInspection.ageMs,
1939
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1940
+ }).catch(() => {
1941
+ });
1942
+ fixed.push({
1943
+ code: "stale_serve_lock",
1944
+ name: "Serve lock",
1945
+ message: `Removed stale .fabric/.serve.lock (dead PID ${lockInspection.pid}).`,
1946
+ path: ".fabric/.serve.lock"
1947
+ });
1948
+ }
1949
+ }
1611
1950
  const report = await runDoctorReport(projectRoot);
1612
1951
  return {
1613
1952
  changed: fixed.length > 0,
@@ -1743,7 +2082,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1743
2082
  };
1744
2083
  }
1745
2084
  const detail = `${candidate.maturity} -> ${next}`;
1746
- const absPath = join5(projectRoot, candidate.path);
2085
+ const absPath = join6(projectRoot, candidate.path);
1747
2086
  try {
1748
2087
  const source = await readFile5(absPath, "utf8");
1749
2088
  const rewritten = rewriteFrontmatterMaturity(source, next);
@@ -1765,7 +2104,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1765
2104
  error: "rewrite produced byte-identical output"
1766
2105
  };
1767
2106
  }
1768
- await atomicWriteText3(absPath, rewritten);
2107
+ await atomicWriteText4(absPath, rewritten);
1769
2108
  try {
1770
2109
  await appendEventLedgerEvent(projectRoot, {
1771
2110
  event_type: "knowledge_demoted",
@@ -1775,7 +2114,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1775
2114
  });
1776
2115
  } catch (ledgerError) {
1777
2116
  try {
1778
- await atomicWriteText3(absPath, source);
2117
+ await atomicWriteText4(absPath, source);
1779
2118
  } catch (rollbackError) {
1780
2119
  return {
1781
2120
  kind: "knowledge_orphan_demote_required",
@@ -1810,19 +2149,19 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1810
2149
  }
1811
2150
  }
1812
2151
  async function applyStaleArchive(projectRoot, candidate, now) {
1813
- const sourceAbs = join5(projectRoot, candidate.path);
1814
- const destAbs = join5(projectRoot, candidate.archive_path);
2152
+ const sourceAbs = join6(projectRoot, candidate.path);
2153
+ const destAbs = join6(projectRoot, candidate.archive_path);
1815
2154
  const detail = `${candidate.path} -> ${candidate.archive_path}`;
1816
2155
  try {
1817
- await mkdir3(join5(destAbs, ".."), { recursive: true });
2156
+ await mkdir4(join6(destAbs, ".."), { recursive: true });
1818
2157
  try {
1819
2158
  await rename(sourceAbs, destAbs);
1820
2159
  } catch (renameError) {
1821
2160
  if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1822
2161
  const data = await readFile5(sourceAbs);
1823
2162
  await writeFile2(destAbs, data);
1824
- const { unlink } = await import("fs/promises");
1825
- await unlink(sourceAbs);
2163
+ const { unlink: unlink2 } = await import("fs/promises");
2164
+ await unlink2(sourceAbs);
1826
2165
  } else {
1827
2166
  throw renameError;
1828
2167
  }
@@ -1873,7 +2212,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
1873
2212
  async function applyPendingAutoArchive(projectRoot, candidate, now) {
1874
2213
  const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
1875
2214
  try {
1876
- await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
2215
+ await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
1877
2216
  let moved = false;
1878
2217
  if (candidate.layer === "team") {
1879
2218
  try {
@@ -1894,8 +2233,8 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1894
2233
  if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1895
2234
  const data = await readFile5(candidate.pending_path_abs);
1896
2235
  await writeFile2(candidate.archived_to_abs, data);
1897
- const { unlink } = await import("fs/promises");
1898
- await unlink(candidate.pending_path_abs);
2236
+ const { unlink: unlink2 } = await import("fs/promises");
2237
+ await unlink2(candidate.pending_path_abs);
1899
2238
  } else {
1900
2239
  throw renameError;
1901
2240
  }
@@ -1946,14 +2285,14 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1946
2285
  }
1947
2286
  function relativePosix(projectRoot, absolutePath) {
1948
2287
  const rel = nodeRelative(projectRoot, absolutePath);
1949
- return rel.split(sep4).join("/");
2288
+ return rel.split(sep3).join("/");
1950
2289
  }
1951
2290
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1952
2291
  const detail = `deleted (${candidate.age_days}d old)`;
1953
- const absPath = join5(projectRoot, candidate.path);
2292
+ const absPath = join6(projectRoot, candidate.path);
1954
2293
  try {
1955
- const { unlink } = await import("fs/promises");
1956
- await unlink(absPath);
2294
+ const { unlink: unlink2 } = await import("fs/promises");
2295
+ await unlink2(absPath);
1957
2296
  return {
1958
2297
  kind: "knowledge_session_hints_stale_cleanup",
1959
2298
  path: candidate.path,
@@ -1971,7 +2310,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1971
2310
  }
1972
2311
  }
1973
2312
  async function applyIndexDriftFix(projectRoot, inspection) {
1974
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2313
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
1975
2314
  const detailParts = [];
1976
2315
  try {
1977
2316
  const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
@@ -2007,9 +2346,9 @@ function truncateErrorMessage(error) {
2007
2346
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
2008
2347
  }
2009
2348
  async function inspectForensic(projectRoot) {
2010
- const path = join5(projectRoot, ".fabric", "forensic.json");
2349
+ const path2 = join6(projectRoot, ".fabric", "forensic.json");
2011
2350
  try {
2012
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
2351
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path2, "utf8")));
2013
2352
  return { present: true, valid: true, report: parsed };
2014
2353
  } catch (error) {
2015
2354
  if (isMissingFileError(error)) {
@@ -2019,12 +2358,12 @@ async function inspectForensic(projectRoot) {
2019
2358
  }
2020
2359
  }
2021
2360
  function inspectMcpConfigInWrongFile(projectRoot) {
2022
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
2361
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
2023
2362
  if (!existsSync4(settingsPath)) {
2024
2363
  return { hasWrongEntry: false, settingsPath };
2025
2364
  }
2026
2365
  try {
2027
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2366
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
2028
2367
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2029
2368
  return { hasWrongEntry: false, settingsPath };
2030
2369
  }
@@ -2040,7 +2379,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
2040
2379
  }
2041
2380
  }
2042
2381
  async function inspectMeta(projectRoot) {
2043
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2382
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
2044
2383
  const built = await tryBuildRuleMeta(projectRoot);
2045
2384
  try {
2046
2385
  const raw = await readFile5(metaPath, "utf8");
@@ -2113,22 +2452,22 @@ function inspectContentRefs(projectRoot, meta) {
2113
2452
  if (isPersonalKnowledge) {
2114
2453
  continue;
2115
2454
  }
2116
- if (!existsSync4(join5(projectRoot, contentRef))) {
2455
+ if (!existsSync4(join6(projectRoot, contentRef))) {
2117
2456
  missing.push(contentRef);
2118
2457
  }
2119
2458
  }
2120
2459
  return { missing, invalid };
2121
2460
  }
2122
2461
  async function inspectEventLedger(projectRoot) {
2123
- const path = getEventLedgerPath(projectRoot);
2124
- const exists = existsSync4(path);
2462
+ const path2 = getEventLedgerPath(projectRoot);
2463
+ const exists = existsSync4(path2);
2125
2464
  if (!exists) {
2126
- return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
2465
+ return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path: path2 };
2127
2466
  }
2128
2467
  try {
2129
- await access(path, constants.W_OK);
2468
+ await access(path2, constants.W_OK);
2130
2469
  const { warnings } = await readEventLedger(projectRoot);
2131
- const raw = await readFile5(path, "utf8");
2470
+ const raw = await readFile5(path2, "utf8");
2132
2471
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
2133
2472
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
2134
2473
  return {
@@ -2138,7 +2477,7 @@ async function inspectEventLedger(projectRoot) {
2138
2477
  hasPartialWrite: partialWarning !== void 0,
2139
2478
  partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
2140
2479
  partialWriteByteLength: partialWarning?.byte_length ?? 0,
2141
- path,
2480
+ path: path2,
2142
2481
  error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
2143
2482
  };
2144
2483
  } catch (error) {
@@ -2149,16 +2488,16 @@ async function inspectEventLedger(projectRoot) {
2149
2488
  hasPartialWrite: false,
2150
2489
  partialWriteByteOffset: 0,
2151
2490
  partialWriteByteLength: 0,
2152
- path,
2491
+ path: path2,
2153
2492
  error: error instanceof Error ? error.message : String(error)
2154
2493
  };
2155
2494
  }
2156
2495
  }
2157
2496
  async function inspectKnowledgeTestIndex(projectRoot) {
2158
- const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2497
+ const path2 = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2159
2498
  const built = await tryBuildRuleMeta(projectRoot);
2160
2499
  try {
2161
- const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
2500
+ const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path2, "utf8")));
2162
2501
  return {
2163
2502
  present: true,
2164
2503
  valid: true,
@@ -2179,8 +2518,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
2179
2518
  }
2180
2519
  function inspectBootstrapAnchor(projectRoot) {
2181
2520
  return {
2182
- hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
2183
- hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
2521
+ hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
2522
+ hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
2184
2523
  };
2185
2524
  }
2186
2525
  var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
@@ -2192,7 +2531,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
2192
2531
  async function inspectBootstrapMarkerMigration(target) {
2193
2532
  const filesNeedingMigration = [];
2194
2533
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
2195
- const abs = join5(target, rel);
2534
+ const abs = join6(target, rel);
2196
2535
  if (!existsSync4(abs)) {
2197
2536
  continue;
2198
2537
  }
@@ -2226,7 +2565,7 @@ function createBootstrapMarkerMigrationCheck(inspection) {
2226
2565
  );
2227
2566
  }
2228
2567
  async function inspectL1BootstrapSnapshotDrift(target) {
2229
- const abs = join5(target, ".fabric", "AGENTS.md");
2568
+ const abs = join6(target, ".fabric", "AGENTS.md");
2230
2569
  if (!existsSync4(abs)) {
2231
2570
  return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2232
2571
  }
@@ -2258,7 +2597,7 @@ function createL1BootstrapSnapshotDriftCheck(inspection) {
2258
2597
  );
2259
2598
  }
2260
2599
  async function inspectL2ManagedBlockDrift(target) {
2261
- const snapshotPath = join5(target, ".fabric", "AGENTS.md");
2600
+ const snapshotPath = join6(target, ".fabric", "AGENTS.md");
2262
2601
  if (!existsSync4(snapshotPath)) {
2263
2602
  return { status: "ok", drifted: [] };
2264
2603
  }
@@ -2268,7 +2607,7 @@ async function inspectL2ManagedBlockDrift(target) {
2268
2607
  } catch {
2269
2608
  return { status: "ok", drifted: [] };
2270
2609
  }
2271
- const projectRulesPath = join5(target, ".fabric", "project-rules.md");
2610
+ const projectRulesPath = join6(target, ".fabric", "project-rules.md");
2272
2611
  let expectedBody = snapshot;
2273
2612
  if (existsSync4(projectRulesPath)) {
2274
2613
  try {
@@ -2282,8 +2621,8 @@ ${projectRules}`;
2282
2621
  const drifted = [];
2283
2622
  let anyManagedBlockFound = false;
2284
2623
  const blockTargets = [
2285
- join5(target, "AGENTS.md"),
2286
- join5(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2624
+ join6(target, "AGENTS.md"),
2625
+ join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2287
2626
  ];
2288
2627
  for (const abs of blockTargets) {
2289
2628
  if (!existsSync4(abs)) {
@@ -2317,7 +2656,7 @@ ${projectRules}`;
2317
2656
  drifted.push({ path: abs, expected: expectedBody, actual: body });
2318
2657
  }
2319
2658
  }
2320
- const claudeMdPath = join5(target, "CLAUDE.md");
2659
+ const claudeMdPath = join6(target, "CLAUDE.md");
2321
2660
  if (existsSync4(claudeMdPath)) {
2322
2661
  let claudeContent;
2323
2662
  try {
@@ -2381,16 +2720,82 @@ function createBootstrapAnchorCheck(inspection) {
2381
2720
  return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
2382
2721
  }
2383
2722
  function inspectKnowledgeDirMissing(projectRoot) {
2384
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2723
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2385
2724
  const missingSubdirs = [];
2386
- for (const sub of KNOWLEDGE_SUBDIRS2) {
2387
- const path = join5(knowledgeRoot, sub);
2388
- if (!existsSync4(path)) {
2725
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
2726
+ const path2 = join6(knowledgeRoot, sub);
2727
+ if (!existsSync4(path2)) {
2389
2728
  missingSubdirs.push(`.fabric/knowledge/${sub}`);
2390
2729
  }
2391
2730
  }
2392
2731
  return { missingSubdirs };
2393
2732
  }
2733
+ function inspectBaselineFilenameFormat(projectRoot) {
2734
+ const offenders = [];
2735
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2736
+ if (!existsSync4(knowledgeRoot)) {
2737
+ return { offenders };
2738
+ }
2739
+ for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2740
+ const dir = join6(knowledgeRoot, sub);
2741
+ if (!existsSync4(dir)) {
2742
+ continue;
2743
+ }
2744
+ let entries;
2745
+ try {
2746
+ entries = readdirSync(dir, { withFileTypes: true });
2747
+ } catch {
2748
+ continue;
2749
+ }
2750
+ for (const entry of entries) {
2751
+ const entryName = entry.name;
2752
+ if (!entry.isFile() || !entryName.endsWith(".md")) {
2753
+ continue;
2754
+ }
2755
+ if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
2756
+ continue;
2757
+ }
2758
+ const abs = join6(dir, entryName);
2759
+ let source;
2760
+ try {
2761
+ source = readFileSync2(abs, "utf8");
2762
+ } catch {
2763
+ continue;
2764
+ }
2765
+ const id = extractKnowledgeFrontmatterId(source);
2766
+ if (id === null) {
2767
+ continue;
2768
+ }
2769
+ if (!BASELINE_FILENAME_LINT_BASELINE_IDS.has(id)) {
2770
+ continue;
2771
+ }
2772
+ offenders.push({
2773
+ path: posix.join(".fabric/knowledge", sub, entryName),
2774
+ stable_id: id
2775
+ });
2776
+ }
2777
+ }
2778
+ offenders.sort((a, b) => a.path.localeCompare(b.path));
2779
+ return { offenders };
2780
+ }
2781
+ function createBaselineFilenameFormatCheck(inspection) {
2782
+ if (inspection.offenders.length === 0) {
2783
+ return okCheck(
2784
+ "Baseline filename format",
2785
+ "All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
2786
+ );
2787
+ }
2788
+ const first = inspection.offenders[0];
2789
+ const detail = `${first.stable_id} at ${first.path}`;
2790
+ return issueCheck(
2791
+ "Baseline filename format",
2792
+ "error",
2793
+ "manual_error",
2794
+ "lint-baseline-filename-format",
2795
+ `${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}.`,
2796
+ "Delete the legacy bare-slug baseline file(s) manually \u2014 the baseline pipeline was removed in rc.23 and is no longer an auto-fix path."
2797
+ );
2798
+ }
2394
2799
  function createKnowledgeDirMissingCheck(inspection) {
2395
2800
  if (inspection.missingSubdirs.length > 0) {
2396
2801
  const list = inspection.missingSubdirs.join(", ");
@@ -2405,7 +2810,7 @@ function createKnowledgeDirMissingCheck(inspection) {
2405
2810
  }
2406
2811
  return okCheck(
2407
2812
  "Knowledge layout",
2408
- `All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
2813
+ `All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
2409
2814
  );
2410
2815
  }
2411
2816
  function createForensicCheck(forensic, frameworkKind, entryPointCount) {
@@ -2434,11 +2839,11 @@ function createMetaCheck(meta) {
2434
2839
  if (meta.stale) {
2435
2840
  return issueCheck(
2436
2841
  "Agents metadata",
2437
- "error",
2438
- "fixable_error",
2842
+ "warn",
2843
+ "warning",
2439
2844
  "agents_meta_stale",
2440
2845
  `.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."
2846
+ "Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
2442
2847
  );
2443
2848
  }
2444
2849
  return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
@@ -2551,7 +2956,7 @@ function findIssue(issues, code) {
2551
2956
  };
2552
2957
  }
2553
2958
  async function inspectMetaManuallyDiverged(projectRoot) {
2554
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2959
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
2555
2960
  if (!existsSync4(metaPath)) {
2556
2961
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2557
2962
  }
@@ -2571,13 +2976,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2571
2976
  const hashMismatchEntries = [];
2572
2977
  for (const node of Object.values(meta.nodes)) {
2573
2978
  const contentRef = node.content_ref ?? node.file;
2574
- const absPath = join5(projectRoot, contentRef);
2979
+ const absPath = join6(projectRoot, contentRef);
2575
2980
  if (!existsSync4(absPath)) {
2576
2981
  extraMetaEntries.push(contentRef);
2577
2982
  continue;
2578
2983
  }
2579
2984
  try {
2580
- const content = readFileSync(absPath, "utf8");
2985
+ const content = readFileSync2(absPath, "utf8");
2581
2986
  const diskHash = sha256(content);
2582
2987
  if (node.hash !== "" && node.hash !== diskHash) {
2583
2988
  hashMismatchEntries.push(contentRef);
@@ -2590,7 +2995,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2590
2995
  }
2591
2996
  function inspectKnowledgeDirUnindexed(projectRoot, meta) {
2592
2997
  const physicalMdFiles = /* @__PURE__ */ new Set();
2593
- collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2998
+ collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2594
2999
  if (physicalMdFiles.size === 0) {
2595
3000
  return { unindexedFiles: [] };
2596
3001
  }
@@ -2615,9 +3020,11 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
2615
3020
  continue;
2616
3021
  }
2617
3022
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2618
- const abs = join5(dir, entry.name);
3023
+ const abs = join6(dir, entry.name);
2619
3024
  if (entry.isDirectory()) {
2620
- stack.push(abs);
3025
+ if (entry.name !== "pending" && entry.name !== "archive") {
3026
+ stack.push(abs);
3027
+ }
2621
3028
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2622
3029
  const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
2623
3030
  out.add(rel);
@@ -2640,7 +3047,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
2640
3047
  }
2641
3048
  async function inspectStableIdCollisions(projectRoot) {
2642
3049
  const found = [];
2643
- const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
3050
+ const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
2644
3051
  if (existsSync4(knowledgeDir)) {
2645
3052
  const stack = [knowledgeDir];
2646
3053
  while (stack.length > 0) {
@@ -2649,7 +3056,7 @@ async function inspectStableIdCollisions(projectRoot) {
2649
3056
  continue;
2650
3057
  }
2651
3058
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2652
- const abs = join5(dir, entry.name);
3059
+ const abs = join6(dir, entry.name);
2653
3060
  if (entry.isDirectory()) {
2654
3061
  stack.push(abs);
2655
3062
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2719,7 +3126,7 @@ function inspectCounterDesync(meta) {
2719
3126
  ["guideline", "GLD"],
2720
3127
  ["pitfall", "PIT"],
2721
3128
  ["process", "PRO"]
2722
- ].find(([t]) => t === parsed.type)?.[1];
3129
+ ].find(([t2]) => t2 === parsed.type)?.[1];
2723
3130
  if (typeCode === void 0) {
2724
3131
  continue;
2725
3132
  }
@@ -2805,17 +3212,17 @@ function createMetaManuallyDivergedCheck(inspection) {
2805
3212
  }
2806
3213
  function inspectPreexistingRootFiles(projectRoot) {
2807
3214
  const candidates = ["CLAUDE.md", "AGENTS.md"];
2808
- const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
3215
+ const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
2809
3216
  return { detected };
2810
3217
  }
2811
3218
  async function inspectFilesystemEditFallback(projectRoot) {
2812
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3219
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2813
3220
  if (!existsSync4(knowledgeRoot)) {
2814
3221
  return { synthesized: 0, synthesizedStableIds: [] };
2815
3222
  }
2816
3223
  const canonicalIds = /* @__PURE__ */ new Set();
2817
3224
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2818
- const dir = join5(knowledgeRoot, typeDir);
3225
+ const dir = join6(knowledgeRoot, typeDir);
2819
3226
  if (!existsSync4(dir)) {
2820
3227
  continue;
2821
3228
  }
@@ -3017,12 +3424,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
3017
3424
  return Number.isFinite(parsed) ? parsed : null;
3018
3425
  }
3019
3426
  function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3020
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3427
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
3021
3428
  if (!existsSync4(knowledgeRoot)) {
3022
3429
  return;
3023
3430
  }
3024
3431
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3025
- const dir = join5(knowledgeRoot, typeDir);
3432
+ const dir = join6(knowledgeRoot, typeDir);
3026
3433
  if (!existsSync4(dir)) {
3027
3434
  continue;
3028
3435
  }
@@ -3041,10 +3448,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3041
3448
  continue;
3042
3449
  }
3043
3450
  const stableId = match[1];
3044
- const absPath = join5(dir, entry.name);
3451
+ const absPath = join6(dir, entry.name);
3045
3452
  let source;
3046
3453
  try {
3047
- source = readFileSync(absPath, "utf8");
3454
+ source = readFileSync2(absPath, "utf8");
3048
3455
  } catch {
3049
3456
  continue;
3050
3457
  }
@@ -3057,7 +3464,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3057
3464
  let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
3058
3465
  if (lastReferenceMs === 0) {
3059
3466
  try {
3060
- lastReferenceMs = statSync3(absPath).mtimeMs;
3467
+ lastReferenceMs = statSync4(absPath).mtimeMs;
3061
3468
  } catch {
3062
3469
  lastReferenceMs = 0;
3063
3470
  }
@@ -3117,8 +3524,8 @@ async function inspectStaleArchive(projectRoot, now) {
3117
3524
  return { candidates };
3118
3525
  }
3119
3526
  function* iteratePendingFiles(projectRoot, now) {
3120
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3121
- const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
3527
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
3528
+ const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
3122
3529
  for (const [layer, root, displayPrefix] of [
3123
3530
  ["team", teamRoot, ".fabric/knowledge/pending"],
3124
3531
  ["personal", personalRoot, "~/.fabric/knowledge/pending"]
@@ -3133,7 +3540,7 @@ function* iteratePendingFiles(projectRoot, now) {
3133
3540
  continue;
3134
3541
  }
3135
3542
  for (const typeDir of typeDirs) {
3136
- const dir = join5(root, typeDir);
3543
+ const dir = join6(root, typeDir);
3137
3544
  let entries;
3138
3545
  try {
3139
3546
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3144,17 +3551,17 @@ function* iteratePendingFiles(projectRoot, now) {
3144
3551
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
3145
3552
  continue;
3146
3553
  }
3147
- const absPath = join5(dir, entry.name);
3554
+ const absPath = join6(dir, entry.name);
3148
3555
  let source = "";
3149
3556
  try {
3150
- source = readFileSync(absPath, "utf8");
3557
+ source = readFileSync2(absPath, "utf8");
3151
3558
  } catch {
3152
3559
  continue;
3153
3560
  }
3154
3561
  const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
3155
3562
  let mtimeMs = 0;
3156
3563
  try {
3157
- mtimeMs = statSync3(absPath).mtimeMs;
3564
+ mtimeMs = statSync4(absPath).mtimeMs;
3158
3565
  } catch {
3159
3566
  mtimeMs = 0;
3160
3567
  }
@@ -3188,7 +3595,7 @@ function* iteratePendingFiles(projectRoot, now) {
3188
3595
  }
3189
3596
  }
3190
3597
  function resolvePersonalRootForPending() {
3191
- return process.env.FABRIC_HOME ?? homedir2();
3598
+ return process.env.FABRIC_HOME ?? homedir3();
3192
3599
  }
3193
3600
  function inspectPendingOverdue(projectRoot, now) {
3194
3601
  const candidates = [];
@@ -3219,7 +3626,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
3219
3626
  pending_path: visit.pending_path,
3220
3627
  pending_path_abs: visit.pending_path_abs,
3221
3628
  archived_to: archivedToRel,
3222
- archived_to_abs: join5(projectRoot, archivedToRel),
3629
+ archived_to_abs: join6(projectRoot, archivedToRel),
3223
3630
  age_days: visit.age_days
3224
3631
  });
3225
3632
  } else {
@@ -3228,7 +3635,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
3228
3635
  visit.type,
3229
3636
  visit.filename
3230
3637
  );
3231
- const archivedToAbs = join5(
3638
+ const archivedToAbs = join6(
3232
3639
  resolvePersonalRootForPending(),
3233
3640
  ".fabric",
3234
3641
  ".archive",
@@ -3252,11 +3659,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
3252
3659
  }
3253
3660
  function inspectUnderseeded(projectRoot) {
3254
3661
  const threshold = readUnderseedThresholdFromConfig(projectRoot);
3255
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3662
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
3256
3663
  let nodeCount = 0;
3257
3664
  if (existsSync4(knowledgeRoot)) {
3258
3665
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3259
- const dir = join5(knowledgeRoot, typeDir);
3666
+ const dir = join6(knowledgeRoot, typeDir);
3260
3667
  if (!existsSync4(dir)) continue;
3261
3668
  let entries;
3262
3669
  try {
@@ -3278,7 +3685,7 @@ function inspectUnderseeded(projectRoot) {
3278
3685
  };
3279
3686
  }
3280
3687
  function inspectSessionHintsStale(projectRoot, now) {
3281
- const cacheDir = join5(projectRoot, ".fabric", ".cache");
3688
+ const cacheDir = join6(projectRoot, ".fabric", ".cache");
3282
3689
  if (!existsSync4(cacheDir)) {
3283
3690
  return { candidates: [] };
3284
3691
  }
@@ -3293,10 +3700,10 @@ function inspectSessionHintsStale(projectRoot, now) {
3293
3700
  if (!entry.isFile()) continue;
3294
3701
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
3295
3702
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
3296
- const absPath = join5(cacheDir, entry.name);
3703
+ const absPath = join6(cacheDir, entry.name);
3297
3704
  let mtimeMs = 0;
3298
3705
  try {
3299
- mtimeMs = statSync3(absPath).mtimeMs;
3706
+ mtimeMs = statSync4(absPath).mtimeMs;
3300
3707
  } catch {
3301
3708
  continue;
3302
3709
  }
@@ -3310,6 +3717,20 @@ function inspectSessionHintsStale(projectRoot, now) {
3310
3717
  candidates.sort((a, b) => a.path.localeCompare(b.path));
3311
3718
  return { candidates };
3312
3719
  }
3720
+ function inspectStaleServeLock(projectRoot, now) {
3721
+ const state = readLockState(projectRoot);
3722
+ if (state === null) {
3723
+ return { present: false };
3724
+ }
3725
+ const ageMs = Math.max(0, now - state.acquiredAt);
3726
+ return {
3727
+ present: true,
3728
+ pid: state.pid,
3729
+ acquiredAt: state.acquiredAt,
3730
+ ageMs,
3731
+ pidAlive: isAlive(state.pid)
3732
+ };
3733
+ }
3313
3734
  function inspectNarrowTooFew(projectRoot, now) {
3314
3735
  let total = 0;
3315
3736
  let narrowWithPaths = 0;
@@ -3323,11 +3744,11 @@ function inspectNarrowTooFew(projectRoot, now) {
3323
3744
  const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
3324
3745
  const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
3325
3746
  const editFires = readCounterTimestamps(
3326
- join5(projectRoot, EDIT_COUNTER_FILE_REL),
3747
+ join6(projectRoot, EDIT_COUNTER_FILE_REL),
3327
3748
  windowStartMs
3328
3749
  );
3329
3750
  const silenceFires = readCounterTimestamps(
3330
- join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3751
+ join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3331
3752
  windowStartMs
3332
3753
  );
3333
3754
  const telemetrySkipped = editFires === 0;
@@ -3349,7 +3770,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
3349
3770
  if (!existsSync4(absPath)) return 0;
3350
3771
  let raw;
3351
3772
  try {
3352
- raw = readFileSync(absPath, "utf8");
3773
+ raw = readFileSync2(absPath, "utf8");
3353
3774
  } catch {
3354
3775
  return 0;
3355
3776
  }
@@ -3365,10 +3786,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
3365
3786
  return count;
3366
3787
  }
3367
3788
  function readUnderseedThresholdFromConfig(projectRoot) {
3368
- const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
3789
+ const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
3369
3790
  if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3370
3791
  try {
3371
- const raw = readFileSync(configPath, "utf8");
3792
+ const raw = readFileSync2(configPath, "utf8");
3372
3793
  const parsed = JSON.parse(raw);
3373
3794
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3374
3795
  const v = parsed.underseed_node_threshold;
@@ -3468,6 +3889,28 @@ function createSessionHintsStaleCheck(inspection) {
3468
3889
  "Run `fab doctor --apply-lint` to delete stale session-hints cache files."
3469
3890
  );
3470
3891
  }
3892
+ function createStaleServeLockCheck(inspection) {
3893
+ if (!inspection.present) {
3894
+ return okCheck("Serve lock", "No .fabric/.serve.lock present.");
3895
+ }
3896
+ if (inspection.pidAlive) {
3897
+ return okCheck(
3898
+ "Serve lock",
3899
+ `.fabric/.serve.lock held by live PID ${inspection.pid}.`
3900
+ );
3901
+ }
3902
+ const days = Math.floor(inspection.ageMs / MS_PER_DAY);
3903
+ const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
3904
+ const acquiredAgo = days >= 1 ? `${days} day${days === 1 ? "" : "s"} ago` : `${hours} hour${hours === 1 ? "" : "s"} ago`;
3905
+ return issueCheck(
3906
+ "Serve lock",
3907
+ "ok",
3908
+ "info",
3909
+ "stale_serve_lock",
3910
+ `[advisory] .fabric/.serve.lock holds dead PID ${inspection.pid} (acquired ${acquiredAgo}). Run \`fab doctor --fix\` to remove.`,
3911
+ "Run `fab doctor --fix` to remove the stale .fabric/.serve.lock."
3912
+ );
3913
+ }
3471
3914
  function extractKnowledgeFrontmatterRelevanceScope(source) {
3472
3915
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3473
3916
  const fm = FM_PATTERN.exec(source);
@@ -3498,11 +3941,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
3498
3941
  }
3499
3942
  function* iterateRelevanceFrontmatter(projectRoot) {
3500
3943
  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);
3944
+ const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3945
+ const absPath = join6(layerRoot, visit.type, visit.filename);
3503
3946
  let source;
3504
3947
  try {
3505
- source = readFileSync(absPath, "utf8");
3948
+ source = readFileSync2(absPath, "utf8");
3506
3949
  } catch {
3507
3950
  continue;
3508
3951
  }
@@ -3569,7 +4012,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3569
4012
  }
3570
4013
  let rootStat;
3571
4014
  try {
3572
- rootStat = statSync3(projectRoot);
4015
+ rootStat = statSync4(projectRoot);
3573
4016
  } catch {
3574
4017
  return [];
3575
4018
  }
@@ -3588,7 +4031,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3588
4031
  continue;
3589
4032
  }
3590
4033
  for (const entry of entries) {
3591
- const abs = join5(current, entry.name);
4034
+ const abs = join6(current, entry.name);
3592
4035
  const rel = normalizePath(abs.slice(projectRoot.length + 1));
3593
4036
  if (rel.length === 0) continue;
3594
4037
  if (entry.isDirectory()) {
@@ -3728,8 +4171,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3728
4171
  const candidates = [];
3729
4172
  let scannedCount = 0;
3730
4173
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3731
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3732
- const personalRoot = join5(
4174
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
4175
+ const personalRoot = join6(
3733
4176
  resolvePersonalRootForPending(),
3734
4177
  ".fabric",
3735
4178
  "knowledge",
@@ -3749,7 +4192,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3749
4192
  continue;
3750
4193
  }
3751
4194
  for (const typeDir of typeDirs) {
3752
- const dir = join5(root, typeDir);
4195
+ const dir = join6(root, typeDir);
3753
4196
  let entries;
3754
4197
  try {
3755
4198
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3760,10 +4203,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3760
4203
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
3761
4204
  continue;
3762
4205
  }
3763
- const absPath = join5(dir, entry.name);
4206
+ const absPath = join6(dir, entry.name);
3764
4207
  let source;
3765
4208
  try {
3766
- source = readFileSync(absPath, "utf8");
4209
+ source = readFileSync2(absPath, "utf8");
3767
4210
  } catch {
3768
4211
  continue;
3769
4212
  }
@@ -3847,7 +4290,7 @@ async function applyRelevanceFieldsMissing(candidate) {
3847
4290
  error: "fields already present at write time (no diff)"
3848
4291
  };
3849
4292
  }
3850
- await atomicWriteText3(candidate.pending_path_abs, rewritten);
4293
+ await atomicWriteText4(candidate.pending_path_abs, rewritten);
3851
4294
  return {
3852
4295
  kind: "knowledge_relevance_fields_missing",
3853
4296
  path: candidate.pending_path,
@@ -3891,7 +4334,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
3891
4334
  function inspectSkillMdYamlInvalid(projectRoot) {
3892
4335
  const candidates = [];
3893
4336
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
3894
- const rootAbs = join5(projectRoot, rootRel);
4337
+ const rootAbs = join6(projectRoot, rootRel);
3895
4338
  if (!existsSync4(rootAbs)) continue;
3896
4339
  let dirEntries;
3897
4340
  try {
@@ -3901,11 +4344,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
3901
4344
  }
3902
4345
  for (const dirEntry of dirEntries) {
3903
4346
  if (!dirEntry.isDirectory()) continue;
3904
- const skillFile = join5(rootAbs, dirEntry.name, "SKILL.md");
4347
+ const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
3905
4348
  if (!existsSync4(skillFile)) continue;
3906
4349
  let raw;
3907
4350
  try {
3908
- raw = readFileSync(skillFile, "utf8");
4351
+ raw = readFileSync2(skillFile, "utf8");
3909
4352
  } catch {
3910
4353
  continue;
3911
4354
  }
@@ -3972,6 +4415,117 @@ function createSkillMdYamlInvalidCheck(inspection) {
3972
4415
  'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
3973
4416
  );
3974
4417
  }
4418
+ var KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD = [
4419
+ "decisions",
4420
+ "pitfalls",
4421
+ "guidelines",
4422
+ "models",
4423
+ "processes"
4424
+ ];
4425
+ function inspectOnboardCoverage(projectRoot) {
4426
+ const filled = {};
4427
+ for (const slot of ONBOARD_SLOT_NAMES) {
4428
+ filled[slot] = [];
4429
+ }
4430
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
4431
+ if (existsSync4(knowledgeRoot)) {
4432
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
4433
+ const dir = join6(knowledgeRoot, typeDir);
4434
+ if (!existsSync4(dir)) continue;
4435
+ let entries;
4436
+ try {
4437
+ entries = readdirSync(dir, { withFileTypes: true });
4438
+ } catch {
4439
+ continue;
4440
+ }
4441
+ for (const entry of entries) {
4442
+ if (!entry.isFile()) continue;
4443
+ if (!entry.name.endsWith(".md")) continue;
4444
+ const filePath = join6(dir, entry.name);
4445
+ let content;
4446
+ try {
4447
+ content = readFileSync2(filePath, "utf8");
4448
+ } catch {
4449
+ continue;
4450
+ }
4451
+ const slot = readFrontmatterScalar(content, "onboard_slot");
4452
+ if (slot === void 0) continue;
4453
+ if (!ONBOARD_SLOT_NAMES.includes(slot)) continue;
4454
+ const stableId = readFrontmatterScalar(content, "id") ?? entry.name.replace(/\.md$/u, "");
4455
+ filled[slot].push(stableId);
4456
+ }
4457
+ }
4458
+ }
4459
+ for (const slot of ONBOARD_SLOT_NAMES) {
4460
+ filled[slot].sort();
4461
+ }
4462
+ const optedOut = readOnboardOptedOut(projectRoot);
4463
+ const missing = ONBOARD_SLOT_NAMES.filter((slot) => {
4464
+ if (filled[slot].length > 0) return false;
4465
+ if (optedOut.includes(slot)) return false;
4466
+ return true;
4467
+ });
4468
+ return { filled, missing, opted_out: optedOut };
4469
+ }
4470
+ function readOnboardOptedOut(projectRoot) {
4471
+ const path2 = join6(projectRoot, ".fabric", "fabric-config.json");
4472
+ if (!existsSync4(path2)) return [];
4473
+ let raw;
4474
+ try {
4475
+ raw = readFileSync2(path2, "utf8");
4476
+ } catch {
4477
+ return [];
4478
+ }
4479
+ let parsed;
4480
+ try {
4481
+ parsed = JSON.parse(raw);
4482
+ } catch {
4483
+ return [];
4484
+ }
4485
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4486
+ return [];
4487
+ }
4488
+ const list = parsed.onboard_slots_opted_out;
4489
+ if (!Array.isArray(list)) return [];
4490
+ return list.filter((v) => typeof v === "string");
4491
+ }
4492
+ function readFrontmatterScalar(content, key) {
4493
+ const match = /^---\n([\s\S]*?)\n---/u.exec(content);
4494
+ if (match === null) return void 0;
4495
+ const block = match[1];
4496
+ if (block === void 0) return void 0;
4497
+ for (const rawLine of block.split(/\r?\n/u)) {
4498
+ const line = rawLine.trim();
4499
+ const sep4 = line.indexOf(":");
4500
+ if (sep4 === -1) continue;
4501
+ if (line.slice(0, sep4).trim() !== key) continue;
4502
+ let value = line.slice(sep4 + 1).trim();
4503
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
4504
+ value = value.slice(1, -1);
4505
+ }
4506
+ return value;
4507
+ }
4508
+ return void 0;
4509
+ }
4510
+ function createOnboardCoverageCheck(inspection) {
4511
+ const filledCount = ONBOARD_SLOT_NAMES.filter(
4512
+ (slot) => inspection.filled[slot].length > 0
4513
+ ).length;
4514
+ if (inspection.missing.length === 0) {
4515
+ return okCheck(
4516
+ "Onboard coverage",
4517
+ `Onboard coverage: ${filledCount}/${ONBOARD_SLOT_TOTAL} \u2713 (opted-out: ${inspection.opted_out.length}).`
4518
+ );
4519
+ }
4520
+ return issueCheck(
4521
+ "Onboard coverage",
4522
+ "ok",
4523
+ "info",
4524
+ "onboard_coverage_incomplete",
4525
+ `Onboard slots not yet covered: [${inspection.missing.join(", ")}]. ${filledCount}/${ONBOARD_SLOT_TOTAL} filled; ${inspection.opted_out.length} opted-out.`,
4526
+ "Run /fabric-archive to onboard \u2014 the Skill's first-run phase will tour the project and propose pending entries for each unclaimed slot."
4527
+ );
4528
+ }
3975
4529
  function createNarrowTooFewCheck(inspection) {
3976
4530
  const { structural_flagged, telemetry_flagged } = inspection;
3977
4531
  if (!structural_flagged && !telemetry_flagged) {
@@ -4005,8 +4559,8 @@ function createNarrowTooFewCheck(inspection) {
4005
4559
  );
4006
4560
  }
4007
4561
  function resolvePersonalKnowledgeRoot() {
4008
- const home = process.env.FABRIC_HOME ?? homedir2();
4009
- return join5(home, ".fabric", "knowledge");
4562
+ const home = process.env.FABRIC_HOME ?? homedir3();
4563
+ return join6(home, ".fabric", "knowledge");
4010
4564
  }
4011
4565
  function parseStableIdFromCanonicalFilename(filename) {
4012
4566
  const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
@@ -4026,7 +4580,7 @@ function parseStableIdFromCanonicalFilename(filename) {
4026
4580
  };
4027
4581
  }
4028
4582
  function* iterateCanonicalFilenames(projectRoot) {
4029
- const teamRoot = join5(projectRoot, ".fabric", "knowledge");
4583
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge");
4030
4584
  const personalRoot = resolvePersonalKnowledgeRoot();
4031
4585
  for (const [layer, root, displayPrefix] of [
4032
4586
  ["team", teamRoot, ".fabric/knowledge"],
@@ -4036,7 +4590,7 @@ function* iterateCanonicalFilenames(projectRoot) {
4036
4590
  continue;
4037
4591
  }
4038
4592
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
4039
- const dir = join5(root, typeDir);
4593
+ const dir = join6(root, typeDir);
4040
4594
  if (!existsSync4(dir)) {
4041
4595
  continue;
4042
4596
  }
@@ -4196,7 +4750,7 @@ async function migrateBootstrapMarkers(projectRoot) {
4196
4750
  const paths = [];
4197
4751
  const countPerPath = {};
4198
4752
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
4199
- const abs = join5(projectRoot, rel);
4753
+ const abs = join6(projectRoot, rel);
4200
4754
  if (!existsSync4(abs)) {
4201
4755
  continue;
4202
4756
  }
@@ -4216,14 +4770,14 @@ async function migrateBootstrapMarkers(projectRoot) {
4216
4770
  if (rewritten === original) {
4217
4771
  continue;
4218
4772
  }
4219
- await atomicWriteText3(abs, rewritten);
4773
+ await atomicWriteText4(abs, rewritten);
4220
4774
  paths.push(abs);
4221
4775
  countPerPath[abs] = replacedCount;
4222
4776
  }
4223
4777
  return { paths, countPerPath };
4224
4778
  }
4225
4779
  async function rewriteThreeEndManagedBlocks(projectRoot) {
4226
- const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
4780
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
4227
4781
  if (!existsSync4(snapshotPath)) {
4228
4782
  return;
4229
4783
  }
@@ -4233,7 +4787,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
4233
4787
  } catch {
4234
4788
  return;
4235
4789
  }
4236
- const projectRulesPath = join5(projectRoot, ".fabric", "project-rules.md");
4790
+ const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
4237
4791
  const hasProjectRules = existsSync4(projectRulesPath);
4238
4792
  let expectedBody = snapshot;
4239
4793
  if (hasProjectRules) {
@@ -4249,8 +4803,8 @@ ${projectRules}`;
4249
4803
  ${expectedBody}
4250
4804
  ${BOOTSTRAP_MARKER_END}`;
4251
4805
  const blockTargets = [
4252
- join5(projectRoot, "AGENTS.md"),
4253
- join5(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4806
+ join6(projectRoot, "AGENTS.md"),
4807
+ join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4254
4808
  ];
4255
4809
  for (const abs of blockTargets) {
4256
4810
  if (!existsSync4(abs)) {
@@ -4281,9 +4835,9 @@ ${managedBlock}
4281
4835
  if (next === existing) {
4282
4836
  continue;
4283
4837
  }
4284
- await atomicWriteText3(abs, next);
4838
+ await atomicWriteText4(abs, next);
4285
4839
  }
4286
- const claudeMdPath = join5(projectRoot, "CLAUDE.md");
4840
+ const claudeMdPath = join6(projectRoot, "CLAUDE.md");
4287
4841
  if (existsSync4(claudeMdPath)) {
4288
4842
  let claudeContent;
4289
4843
  try {
@@ -4307,18 +4861,18 @@ ${managedBlock}
4307
4861
  ensureLine("@.fabric/project-rules.md");
4308
4862
  }
4309
4863
  if (updated !== claudeContent) {
4310
- await atomicWriteText3(claudeMdPath, updated);
4864
+ await atomicWriteText4(claudeMdPath, updated);
4311
4865
  }
4312
4866
  }
4313
4867
  }
4314
4868
  async function fixMcpConfigInWrongFile(projectRoot) {
4315
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
4869
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
4316
4870
  if (!existsSync4(settingsPath)) {
4317
4871
  return;
4318
4872
  }
4319
4873
  let settings;
4320
4874
  try {
4321
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
4875
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
4322
4876
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4323
4877
  return;
4324
4878
  }
@@ -4345,12 +4899,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
4345
4899
  });
4346
4900
  }
4347
4901
  async function ensureKnowledgeSubdirs(projectRoot) {
4348
- for (const sub of KNOWLEDGE_SUBDIRS2) {
4349
- await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
4902
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
4903
+ await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
4350
4904
  }
4351
4905
  }
4352
4906
  async function fixCounterDesync(projectRoot) {
4353
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
4907
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
4354
4908
  if (!existsSync4(metaPath)) {
4355
4909
  return;
4356
4910
  }
@@ -4380,9 +4934,9 @@ async function fixCounterDesync(projectRoot) {
4380
4934
  await atomicWriteJson2(metaPath, updated, { indent: 2 });
4381
4935
  }
4382
4936
  async function ensureEventLedger(projectRoot) {
4383
- const path = getEventLedgerPath(projectRoot);
4384
- await ensureParentDirectory(path);
4385
- await writeFile2(path, "", { encoding: "utf8", flag: "a" });
4937
+ const path2 = getEventLedgerPath(projectRoot);
4938
+ await ensureParentDirectory(path2);
4939
+ await writeFile2(path2, "", { encoding: "utf8", flag: "a" });
4386
4940
  }
4387
4941
  var CITE_POLICY_VERSION = "2.0.0-rc.20";
4388
4942
  async function ensureCitePolicyActivatedMarker(projectRoot) {
@@ -4409,6 +4963,14 @@ async function ensureCitePolicyActivatedMarker(projectRoot) {
4409
4963
  return { marker_ts: 0, emitted_now: false };
4410
4964
  }
4411
4965
  }
4966
+ function parseNoneSentinel(kbLineRaw) {
4967
+ if (typeof kbLineRaw !== "string" || kbLineRaw.length === 0) return "unspecified";
4968
+ const m = kbLineRaw.match(/^KB:\s*none\b\s*(?:\[([^\]]*)\])?\s*$/i);
4969
+ if (m === null) return "unspecified";
4970
+ const inner = (m[1] ?? "").trim().toLowerCase();
4971
+ if (inner === "no-relevant" || inner === "not-applicable") return inner;
4972
+ return "unspecified";
4973
+ }
4412
4974
  function categorizeCiteTag(tag) {
4413
4975
  if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
4414
4976
  return { category: tag };
@@ -4492,7 +5054,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
4492
5054
  break;
4493
5055
  }
4494
5056
  }
4495
- const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
5057
+ const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t2) => t2.client === options.client);
4496
5058
  let clientSessionIds = null;
4497
5059
  if (options.client !== "all") {
4498
5060
  clientSessionIds = /* @__PURE__ */ new Set();
@@ -4547,6 +5109,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
4547
5109
  return false;
4548
5110
  };
4549
5111
  const dismissedHistogram = {};
5112
+ const noneHistogram = {};
4550
5113
  const perClientAccum = /* @__PURE__ */ new Map();
4551
5114
  const emptyMetrics = () => ({
4552
5115
  edits_touched: 0,
@@ -4596,7 +5159,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
4596
5159
  dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
4597
5160
  break;
4598
5161
  }
4599
- case "none":
5162
+ case "none": {
5163
+ const sentinel = parseNoneSentinel(turn.kb_line_raw);
5164
+ noneHistogram[sentinel] = (noneHistogram[sentinel] ?? 0) + 1;
5165
+ break;
5166
+ }
4600
5167
  default:
4601
5168
  break;
4602
5169
  }
@@ -4650,6 +5217,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
4650
5217
  metrics,
4651
5218
  ...perClient !== void 0 ? { per_client: perClient } : {},
4652
5219
  ...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
5220
+ ...Object.keys(noneHistogram).length > 0 ? { none_reason_histogram: noneHistogram } : {},
4653
5221
  generated_at: generatedAt
4654
5222
  };
4655
5223
  }
@@ -4669,11 +5237,11 @@ function isValidJsonLine(line) {
4669
5237
  function normalizeTarget(targetInput) {
4670
5238
  return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
4671
5239
  }
4672
- function normalizePath(path) {
4673
- return posix.normalize(path.split("\\").join("/"));
5240
+ function normalizePath(path2) {
5241
+ return posix.normalize(path2.split("\\").join("/"));
4674
5242
  }
4675
5243
  function collectEntryPoints(root) {
4676
- if (!existsSync4(root) || !statSync3(root).isDirectory()) {
5244
+ if (!existsSync4(root) || !statSync4(root).isDirectory()) {
4677
5245
  return [];
4678
5246
  }
4679
5247
  const entries = [];
@@ -4684,7 +5252,7 @@ function collectEntryPoints(root) {
4684
5252
  continue;
4685
5253
  }
4686
5254
  for (const entry of readdirSync(current, { withFileTypes: true })) {
4687
- const absolutePath = join5(current, entry.name);
5255
+ const absolutePath = join6(current, entry.name);
4688
5256
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
4689
5257
  if (relativePath.length === 0) {
4690
5258
  continue;
@@ -4740,10 +5308,227 @@ function reduceStatus(statuses) {
4740
5308
  function isMissingFileError(error) {
4741
5309
  return error instanceof Error && "code" in error && error.code === "ENOENT";
4742
5310
  }
5311
+ var ENRICH_DESC_FIELDS = ["intent_clues", "tech_stack", "impact", "must_read_if"];
5312
+ var ENRICH_DESC_FIELD_PATTERNS = {
5313
+ intent_clues: /^intent_clues\s*:/mu,
5314
+ tech_stack: /^tech_stack\s*:/mu,
5315
+ impact: /^impact\s*:/mu,
5316
+ must_read_if: /^must_read_if\s*:/mu
5317
+ };
5318
+ async function enrichDescriptions(projectRoot, opts = {}) {
5319
+ const auto = opts.auto === true;
5320
+ const dryRun = opts.dryRun === true;
5321
+ const mode = auto ? "auto" : "interactive";
5322
+ const candidates = [];
5323
+ let scanned = 0;
5324
+ let modified = 0;
5325
+ let skipped = 0;
5326
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
5327
+ const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
5328
+ const absPath = join6(layerRoot, visit.type, visit.filename);
5329
+ scanned += 1;
5330
+ let source;
5331
+ try {
5332
+ source = await readFile5(absPath, "utf8");
5333
+ } catch {
5334
+ continue;
5335
+ }
5336
+ const fmMatch = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
5337
+ if (fmMatch === null) {
5338
+ candidates.push({
5339
+ path: visit.displayPath,
5340
+ missing: [...ENRICH_DESC_FIELDS],
5341
+ modified: false,
5342
+ added_fields: [],
5343
+ error: "frontmatter not parseable"
5344
+ });
5345
+ continue;
5346
+ }
5347
+ const block = fmMatch[1];
5348
+ const missing = ENRICH_DESC_FIELDS.filter(
5349
+ (field) => !ENRICH_DESC_FIELD_PATTERNS[field].test(block)
5350
+ );
5351
+ if (missing.length === 0) {
5352
+ skipped += 1;
5353
+ continue;
5354
+ }
5355
+ if (!auto || dryRun) {
5356
+ candidates.push({
5357
+ path: visit.displayPath,
5358
+ missing,
5359
+ modified: false,
5360
+ added_fields: []
5361
+ });
5362
+ continue;
5363
+ }
5364
+ const mustReadIf = synthesizeMustReadIfStub(source, visit.filename);
5365
+ const additions = [];
5366
+ for (const field of missing) {
5367
+ if (field === "must_read_if") {
5368
+ additions.push({ field, line: `must_read_if: ${yamlQuoteIfNeeded(mustReadIf)}` });
5369
+ } else {
5370
+ additions.push({ field, line: `${field}: []` });
5371
+ }
5372
+ }
5373
+ const trailing = block.endsWith("\n") ? "" : "\n";
5374
+ const replacedBlock = `${block}${trailing}${additions.map((a) => a.line).join("\n")}`;
5375
+ const blockStart = source.indexOf(block);
5376
+ if (blockStart < 0) {
5377
+ candidates.push({
5378
+ path: visit.displayPath,
5379
+ missing,
5380
+ modified: false,
5381
+ added_fields: [],
5382
+ error: "frontmatter block not located after match"
5383
+ });
5384
+ continue;
5385
+ }
5386
+ const rewritten = source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
5387
+ await atomicWriteText4(absPath, rewritten);
5388
+ modified += 1;
5389
+ candidates.push({
5390
+ path: visit.displayPath,
5391
+ missing,
5392
+ modified: true,
5393
+ added_fields: additions.map((a) => a.field)
5394
+ });
5395
+ await appendEventLedgerEvent(projectRoot, {
5396
+ event_type: "knowledge_enriched",
5397
+ path: visit.displayPath,
5398
+ added_fields: additions.map((a) => a.field),
5399
+ mode,
5400
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5401
+ }).catch(() => {
5402
+ });
5403
+ }
5404
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
5405
+ return { mode, dryRun, scanned, modified, skipped, candidates };
5406
+ }
5407
+ function synthesizeMustReadIfStub(source, filename) {
5408
+ const h1Match = /^#\s+(.+?)\s*$/mu.exec(source);
5409
+ let raw = h1Match !== null ? h1Match[1] : filename.replace(/^K[PT]-[A-Z]+-\d+--/, "").replace(/\.md$/u, "").replace(/-/g, " ");
5410
+ raw = raw.trim();
5411
+ if (raw.length === 0) {
5412
+ raw = "describes a knowledge invariant for this project";
5413
+ }
5414
+ if (raw.length > 120) {
5415
+ raw = `${raw.slice(0, 117)}...`;
5416
+ }
5417
+ return raw;
5418
+ }
5419
+ function yamlQuoteIfNeeded(value) {
5420
+ if (value.length === 0) {
5421
+ return '""';
5422
+ }
5423
+ if (/[:#"'\\[\]{},&*!|>%@`]/.test(value) || /^[\s-?]/.test(value) || /\s$/.test(value)) {
5424
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
5425
+ }
5426
+ return value;
5427
+ }
5428
+
5429
+ // src/services/load-active-meta.ts
5430
+ async function loadActiveMeta(projectRoot, opts = {}) {
5431
+ const onDisk = await readAgentsMeta(projectRoot);
5432
+ const previousRevisionHash = onDisk.revision;
5433
+ const derived = await buildKnowledgeMeta(projectRoot);
5434
+ if (derived.meta.revision === previousRevisionHash) {
5435
+ return {
5436
+ meta: onDisk,
5437
+ auto_healed: false,
5438
+ previous_revision_hash: previousRevisionHash,
5439
+ revision_hash: previousRevisionHash
5440
+ };
5441
+ }
5442
+ const written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5443
+ contextCache.invalidate("meta_write", projectRoot);
5444
+ await emitAutoHealEventBestEffort(projectRoot, {
5445
+ previous_revision_hash: previousRevisionHash,
5446
+ revision_hash: written.meta.revision,
5447
+ caller: opts.caller
5448
+ });
5449
+ return {
5450
+ meta: written.meta,
5451
+ auto_healed: true,
5452
+ previous_revision_hash: previousRevisionHash,
5453
+ revision_hash: written.meta.revision
5454
+ };
5455
+ }
5456
+ async function loadActiveMetaOrStale(projectRoot, opts = {}) {
5457
+ let onDisk;
5458
+ try {
5459
+ onDisk = await readAgentsMeta(projectRoot);
5460
+ } catch (error) {
5461
+ if (error instanceof AgentsMetaFileMissingError || error instanceof AgentsMetaInvalidError) {
5462
+ throw error;
5463
+ }
5464
+ throw error;
5465
+ }
5466
+ const previousRevisionHash = onDisk.revision;
5467
+ let derived;
5468
+ try {
5469
+ derived = await buildKnowledgeMeta(projectRoot);
5470
+ } catch (error) {
5471
+ return {
5472
+ meta: onDisk,
5473
+ auto_healed: false,
5474
+ previous_revision_hash: previousRevisionHash,
5475
+ revision_hash: previousRevisionHash,
5476
+ degraded: true,
5477
+ error: error instanceof Error ? error.message : String(error)
5478
+ };
5479
+ }
5480
+ if (derived.meta.revision === previousRevisionHash) {
5481
+ return {
5482
+ meta: onDisk,
5483
+ auto_healed: false,
5484
+ previous_revision_hash: previousRevisionHash,
5485
+ revision_hash: previousRevisionHash,
5486
+ degraded: false
5487
+ };
5488
+ }
5489
+ let written;
5490
+ try {
5491
+ written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5492
+ } catch (error) {
5493
+ return {
5494
+ meta: onDisk,
5495
+ auto_healed: false,
5496
+ previous_revision_hash: previousRevisionHash,
5497
+ revision_hash: previousRevisionHash,
5498
+ degraded: true,
5499
+ error: error instanceof Error ? error.message : String(error)
5500
+ };
5501
+ }
5502
+ contextCache.invalidate("meta_write", projectRoot);
5503
+ await emitAutoHealEventBestEffort(projectRoot, {
5504
+ previous_revision_hash: previousRevisionHash,
5505
+ revision_hash: written.meta.revision,
5506
+ caller: opts.caller
5507
+ });
5508
+ return {
5509
+ meta: written.meta,
5510
+ auto_healed: true,
5511
+ previous_revision_hash: previousRevisionHash,
5512
+ revision_hash: written.meta.revision,
5513
+ degraded: false
5514
+ };
5515
+ }
5516
+ async function emitAutoHealEventBestEffort(projectRoot, payload) {
5517
+ try {
5518
+ await appendEventLedgerEvent(projectRoot, {
5519
+ event_type: "knowledge_meta_auto_healed",
5520
+ previous_revision_hash: payload.previous_revision_hash,
5521
+ revision_hash: payload.revision_hash,
5522
+ trigger: "read",
5523
+ ...payload.caller !== void 0 ? { caller: payload.caller } : {}
5524
+ });
5525
+ } catch {
5526
+ }
5527
+ }
4743
5528
 
4744
5529
  // src/services/get-knowledge.ts
4745
5530
  import { readFile as readFile6 } from "fs/promises";
4746
- import { join as join6 } from "path";
5531
+ import { join as join7 } from "path";
4747
5532
  import { minimatch as minimatch2 } from "minimatch";
4748
5533
  var PRIORITY_ORDER = {
4749
5534
  high: 0,
@@ -4751,6 +5536,10 @@ var PRIORITY_ORDER = {
4751
5536
  low: 2
4752
5537
  };
4753
5538
  async function getKnowledge(projectRoot, input) {
5539
+ const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
5540
+ if (metaResult.auto_healed) {
5541
+ contextCache.invalidate("file_watch", projectRoot);
5542
+ }
4754
5543
  const context = await loadGetKnowledgeContext(projectRoot);
4755
5544
  const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
4756
5545
  const matchedNodes = matchRuleNodes(context.meta, input.path);
@@ -4783,7 +5572,7 @@ async function loadGetKnowledgeContext(projectRoot) {
4783
5572
  return cached;
4784
5573
  }
4785
5574
  const meta = await readAgentsMeta(projectRoot);
4786
- const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
5575
+ const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
4787
5576
  const context = {
4788
5577
  meta,
4789
5578
  l0Content,
@@ -4792,16 +5581,16 @@ async function loadGetKnowledgeContext(projectRoot) {
4792
5581
  contextCache.set("context", projectRoot, context);
4793
5582
  return context;
4794
5583
  }
4795
- async function resolveKnowledgeForPath(projectRoot, context, path, options = {}) {
4796
- const matchedNodes = matchRuleNodes(context.meta, path);
5584
+ async function resolveKnowledgeForPath(projectRoot, context, path2, options = {}) {
5585
+ const matchedNodes = matchRuleNodes(context.meta, path2);
4797
5586
  const loaded = await loadMatchedRules(projectRoot, matchedNodes);
4798
5587
  return buildKnowledgePayload(context, loaded, options);
4799
5588
  }
4800
5589
  function normalizeKnowledgePath(value) {
4801
5590
  return value.replaceAll("\\", "/");
4802
5591
  }
4803
- function matchRuleNodes(meta, path) {
4804
- const requestedPath = normalizeKnowledgePath(path);
5592
+ function matchRuleNodes(meta, path2) {
5593
+ const requestedPath = normalizeKnowledgePath(path2);
4805
5594
  return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
4806
5595
  const [leftId, leftNode] = left;
4807
5596
  const [rightId, rightNode] = right;
@@ -4922,13 +5711,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
4922
5711
  if (cached !== void 0) {
4923
5712
  return await cached;
4924
5713
  }
4925
- const pending = readFile6(join6(projectRoot, file), "utf8");
5714
+ const pending = readFile6(join7(projectRoot, file), "utf8");
4926
5715
  fileContentCache.set(file, pending);
4927
5716
  return await pending;
4928
5717
  }
4929
5718
 
4930
5719
  export {
4931
5720
  contextCache,
5721
+ AgentsMetaFileMissingError,
4932
5722
  resolveProjectRoot,
4933
5723
  readAgentsMeta,
4934
5724
  LEDGER_PATH,
@@ -4955,10 +5745,18 @@ export {
4955
5745
  invalidateKnowledgeSyncCooldown,
4956
5746
  ensureKnowledgeFresh,
4957
5747
  reconcileKnowledge,
5748
+ loadActiveMeta,
5749
+ loadActiveMetaOrStale,
4958
5750
  getKnowledge,
4959
5751
  normalizeKnowledgePath,
5752
+ ServeLockHeldError,
5753
+ acquireLock,
5754
+ releaseLock,
5755
+ readLockState,
5756
+ checkLockOrThrow,
4960
5757
  runDoctorReport,
4961
5758
  runDoctorFix,
4962
5759
  runDoctorApplyLint,
4963
- runDoctorCiteCoverage
5760
+ runDoctorCiteCoverage,
5761
+ enrichDescriptions
4964
5762
  };