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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,13 +3,17 @@ import {
3
3
  EVENT_LEDGER_PATH,
4
4
  LEDGER_PATH,
5
5
  LEGACY_LEDGER_PATH,
6
+ ServeLockHeldError,
7
+ acquireLock,
6
8
  appendEventLedgerEvent,
7
9
  atomicWriteText,
8
10
  buildKnowledgeMeta,
11
+ checkLockOrThrow,
9
12
  computeKnowledgeBasedAgentsMeta,
10
13
  computeKnowledgeTestIndex,
11
14
  deriveKnowledgeMetaLayer,
12
15
  deriveKnowledgeMetaTopologyType,
16
+ enrichDescriptions,
13
17
  ensureKnowledgeFresh,
14
18
  ensureParentDirectory,
15
19
  flushAndSyncEventLedger,
@@ -19,17 +23,21 @@ import {
19
23
  isSameKnowledgeTestIndex,
20
24
  loadActiveMeta,
21
25
  loadActiveMetaOrStale,
26
+ loadKbIdTypeMap,
22
27
  normalizeKnowledgePath,
28
+ readLockState,
23
29
  reconcileKnowledge,
30
+ releaseLock,
24
31
  resolveProjectRoot,
25
32
  runDoctorApplyLint,
33
+ runDoctorArchiveHistory,
26
34
  runDoctorCiteCoverage,
27
35
  runDoctorFix,
28
36
  runDoctorReport,
29
37
  sha256,
30
38
  stableStringify,
31
39
  writeKnowledgeMeta
32
- } from "./chunk-7N3FW5LX.js";
40
+ } from "./chunk-HAXROPQM.js";
33
41
 
34
42
  // src/index.ts
35
43
  import { existsSync as existsSync4 } from "fs";
@@ -42,6 +50,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
42
50
  // src/constants.ts
43
51
  var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
44
52
 
53
+ // src/services/first-reconcile-gate.ts
54
+ function gateWarning(result) {
55
+ if (result.status === "ready") return null;
56
+ if (result.status === "stale") {
57
+ return {
58
+ code: "meta_stale",
59
+ file: "<response>",
60
+ action_hint: "Initial reconcile still pending; results may use cached meta. Retry shortly or run `fab doctor --fix`."
61
+ };
62
+ }
63
+ return {
64
+ code: "reconcile_failed",
65
+ file: "<response>",
66
+ action_hint: "Reconcile failed at startup; run `fab doctor --fix` and restart the MCP server."
67
+ };
68
+ }
69
+ var state = {
70
+ firstReconcilePromise: null,
71
+ reconcileFailure: null,
72
+ settled: false
73
+ };
74
+ function setFirstReconcile(reconcilePromise) {
75
+ state.firstReconcilePromise = reconcilePromise.then(
76
+ () => {
77
+ state.settled = true;
78
+ },
79
+ (error) => {
80
+ state.reconcileFailure = error;
81
+ state.settled = true;
82
+ }
83
+ );
84
+ }
85
+ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
86
+ if (state.reconcileFailure !== null) {
87
+ return { status: "failed", error: state.reconcileFailure };
88
+ }
89
+ if (state.firstReconcilePromise === null || state.settled) {
90
+ return { status: "ready" };
91
+ }
92
+ let timer = null;
93
+ const timeoutPromise = new Promise((resolveTimeout) => {
94
+ timer = setTimeout(() => resolveTimeout("timeout"), timeoutMs);
95
+ });
96
+ try {
97
+ const winner = await Promise.race([
98
+ state.firstReconcilePromise.then(() => "reconcile"),
99
+ timeoutPromise
100
+ ]);
101
+ if (winner === "timeout") {
102
+ return { status: "stale" };
103
+ }
104
+ if (state.reconcileFailure !== null) {
105
+ return { status: "failed", error: state.reconcileFailure };
106
+ }
107
+ return { status: "ready" };
108
+ } finally {
109
+ if (timer !== null) clearTimeout(timer);
110
+ }
111
+ }
112
+
45
113
  // src/services/in-flight-tracker.ts
46
114
  function createInFlightTracker() {
47
115
  const active = /* @__PURE__ */ new Map();
@@ -133,7 +201,7 @@ async function extractKnowledge(projectRoot, input) {
133
201
  }
134
202
  }
135
203
  const sanitizedSlug = sanitizeSlug(input.slug);
136
- const sourceSessions = Array.isArray(input.source_sessions) && input.source_sessions.length > 0 ? input.source_sessions : input.source_session !== void 0 && input.source_session.length > 0 ? [input.source_session] : [];
204
+ const sourceSessions = input.source_sessions ?? [];
137
205
  const primarySession = sourceSessions[0] ?? "";
138
206
  const idempotencyKey = sha256(
139
207
  JSON.stringify({
@@ -217,7 +285,20 @@ async function extractKnowledge(projectRoot, input) {
217
285
  proposedReason: input.proposed_reason,
218
286
  sessionContext: input.session_context,
219
287
  relevanceScope,
220
- relevancePaths
288
+ relevancePaths,
289
+ // v2.0.0-rc.23 TASK-006 (a-C1): optional structured triage fields. Each is
290
+ // emitted as a YAML line only when caller-supplied; omitted lines preserve
291
+ // the historical pending-file shape.
292
+ intentClues: input.intent_clues,
293
+ techStack: input.tech_stack,
294
+ impact: input.impact,
295
+ mustReadIf: input.must_read_if,
296
+ // v2.0.0-rc.23 TASK-014 (F8c): optional S5 onboard-slot tag. Same emit
297
+ // discipline as the four a-C1 fields — bare YAML line iff caller-supplied,
298
+ // never in the idempotency_key hash. fabric-archive's first-run phase is
299
+ // the only producer; downstream `fab onboard-coverage` walks frontmatter
300
+ // looking for this exact key.
301
+ onboardSlot: input.onboard_slot
221
302
  });
222
303
  await atomicWriteText(absolutePath, fresh);
223
304
  await emitEventBestEffort(projectRoot, {
@@ -260,6 +341,24 @@ function renderFreshEntry(args) {
260
341
  const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
261
342
  frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
262
343
  }
344
+ if (args.intentClues !== void 0) {
345
+ const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
346
+ frontmatterLines.push(`intent_clues: [${body2}]`);
347
+ }
348
+ if (args.techStack !== void 0) {
349
+ const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
350
+ frontmatterLines.push(`tech_stack: [${body2}]`);
351
+ }
352
+ if (args.impact !== void 0) {
353
+ const body2 = args.impact.map((s) => quoteRelevancePath(s)).join(", ");
354
+ frontmatterLines.push(`impact: [${body2}]`);
355
+ }
356
+ if (args.mustReadIf !== void 0) {
357
+ frontmatterLines.push(`must_read_if: ${quoteRelevancePath(args.mustReadIf)}`);
358
+ }
359
+ if (args.onboardSlot !== void 0) {
360
+ frontmatterLines.push(`onboard_slot: ${args.onboardSlot}`);
361
+ }
263
362
  frontmatterLines.push(
264
363
  `x-fabric-idempotency-key: ${args.idempotencyKey}`,
265
364
  "---"
@@ -328,9 +427,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
328
427
  const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
329
428
  if (pathSection !== null) {
330
429
  for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
331
- const t2 = rawLine.trim();
332
- if (t2.startsWith("- ")) {
333
- existingPaths.push(t2.slice(2).trim());
430
+ const t = rawLine.trim();
431
+ if (t.startsWith("- ")) {
432
+ existingPaths.push(t.slice(2).trim());
334
433
  }
335
434
  }
336
435
  }
@@ -339,16 +438,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
339
438
  const bulletLines = [];
340
439
  let prose = [];
341
440
  for (const rawLine of noteBody.split(/\r?\n/u)) {
342
- const t2 = rawLine.trim();
343
- if (t2.length === 0) continue;
344
- if (t2.startsWith("- ")) {
441
+ const t = rawLine.trim();
442
+ if (t.length === 0) continue;
443
+ if (t.startsWith("- ")) {
345
444
  if (prose.length > 0) {
346
445
  existingNotes.push(prose.join(" ").trim());
347
446
  prose = [];
348
447
  }
349
- bulletLines.push(t2.slice(2).trim());
448
+ bulletLines.push(t.slice(2).trim());
350
449
  } else {
351
- prose.push(t2);
450
+ prose.push(t);
352
451
  }
353
452
  }
354
453
  if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
@@ -423,7 +522,7 @@ function registerExtractKnowledge(server, tracker) {
423
522
  server.registerTool(
424
523
  "fab_extract_knowledge",
425
524
  {
426
- description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (source_session, type, slug); repeat calls append evidence rather than overwrite. Skill-side tool \u2014 invoked at session-stop.",
525
+ description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (source_sessions[0], type, slug); repeat calls append evidence rather than overwrite. Skill-side tool \u2014 invoked at session-stop.",
427
526
  inputSchema: FabExtractKnowledgeInputShape,
428
527
  outputSchema: FabExtractKnowledgeOutputSchema.shape,
429
528
  annotations: fabExtractKnowledgeAnnotations
@@ -432,9 +531,14 @@ function registerExtractKnowledge(server, tracker) {
432
531
  const requestId = randomUUID();
433
532
  tracker?.enter(requestId);
434
533
  try {
534
+ const gateResult = await awaitFirstReconcileGate();
535
+ const gateWarn = gateWarning(gateResult);
435
536
  const projectRoot = resolveProjectRoot();
436
537
  const result = await extractKnowledge(projectRoot, input);
437
538
  const response = { ...result };
539
+ if (gateWarn) {
540
+ response.warnings = [gateWarn];
541
+ }
438
542
  const payloadLimits = readPayloadLimits(projectRoot);
439
543
  const serialized = JSON.stringify(response);
440
544
  enforcePayloadLimit(serialized, payloadLimits);
@@ -464,17 +568,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
464
568
  var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
465
569
  var selectionTokenCache = /* @__PURE__ */ new Map();
466
570
  async function planContext(projectRoot, input) {
467
- const metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
468
- const meta = metaResult.meta;
571
+ let metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
572
+ let meta = metaResult.meta;
573
+ let firstSeenPreviousRevision = metaResult.previous_revision_hash;
574
+ let autoHealedAccumulated = metaResult.auto_healed;
575
+ if (metaResult.auto_healed !== true && hasUndefinedDescription(meta)) {
576
+ try {
577
+ await reconcileKnowledge(projectRoot, { trigger: "auto-heal-description" });
578
+ const healedResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
579
+ meta = healedResult.meta;
580
+ autoHealedAccumulated = true;
581
+ firstSeenPreviousRevision = metaResult.previous_revision_hash;
582
+ metaResult = healedResult;
583
+ } catch {
584
+ }
585
+ }
469
586
  const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
470
587
  const uniquePaths = dedupePaths(input.paths);
471
588
  const allDescriptions = buildDescriptionIndex(meta);
472
589
  const relevanceTargetPaths = input.target_paths ?? uniquePaths;
473
- const entries = uniquePaths.map((path2) => {
474
- const profile = buildRequirementProfile(path2, input);
475
- const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
590
+ const entries = uniquePaths.map((path) => {
591
+ const profile = buildRequirementProfile(path, input);
592
+ const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
476
593
  return {
477
- path: path2,
594
+ path,
478
595
  requirement_profile: profile,
479
596
  description_index: descriptionIndex
480
597
  };
@@ -491,12 +608,14 @@ async function planContext(projectRoot, input) {
491
608
  description_index: sharedDescriptionIndex,
492
609
  preflight_diagnostics: buildPreflightDiagnostics(meta)
493
610
  },
494
- // v2.0.0-rc.22 Scope D T-D2: surface auto-heal pair only when a heal
495
- // actually fired. Keeping these fields absent on the steady-state path
496
- // means existing consumers see the same wire shape they always have.
497
- ...metaResult.auto_healed ? {
611
+ // v2.0.0-rc.22 Scope D T-D2 + rc.23 TASK-005 (a-B): surface auto-heal pair
612
+ // only when a heal actually fired (either revision-drift heal in
613
+ // loadActiveMetaOrStale or description-undefined heal driven from here).
614
+ // Keeping these fields absent on the steady-state path means existing
615
+ // consumers see the same wire shape they always have.
616
+ ...autoHealedAccumulated ? {
498
617
  auto_healed: true,
499
- previous_revision_hash: metaResult.previous_revision_hash
618
+ previous_revision_hash: firstSeenPreviousRevision
500
619
  } : {}
501
620
  };
502
621
  try {
@@ -519,15 +638,15 @@ async function planContext(projectRoot, input) {
519
638
  return result;
520
639
  }
521
640
  function readSelectionToken(token, now = Date.now()) {
522
- const state = selectionTokenCache.get(token);
523
- if (state === void 0) {
641
+ const state2 = selectionTokenCache.get(token);
642
+ if (state2 === void 0) {
524
643
  return void 0;
525
644
  }
526
- if (state.expires_at <= now) {
645
+ if (state2.expires_at <= now) {
527
646
  selectionTokenCache.delete(token);
528
647
  return void 0;
529
648
  }
530
- return state;
649
+ return state2;
531
650
  }
532
651
  function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
533
652
  const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
@@ -544,8 +663,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
544
663
  }
545
664
  function dedupePaths(paths) {
546
665
  const seenPaths = /* @__PURE__ */ new Set();
547
- return paths.flatMap((path2) => {
548
- const normalizedPath = normalizeKnowledgePath(path2);
666
+ return paths.flatMap((path) => {
667
+ const normalizedPath = normalizeKnowledgePath(path);
549
668
  if (seenPaths.has(normalizedPath)) {
550
669
  return [];
551
670
  }
@@ -553,8 +672,8 @@ function dedupePaths(paths) {
553
672
  return [normalizedPath];
554
673
  });
555
674
  }
556
- function buildRequirementProfile(path2, input) {
557
- const normalizedPath = normalizeKnowledgePath(path2);
675
+ function buildRequirementProfile(path, input) {
676
+ const normalizedPath = normalizeKnowledgePath(path);
558
677
  const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
559
678
  const knownTech = dedupeStableIds([
560
679
  ...input.known_tech ?? [],
@@ -566,7 +685,7 @@ function buildRequirementProfile(path2, input) {
566
685
  extension: extensionMatch?.[1] ?? "",
567
686
  known_tech: knownTech,
568
687
  user_intent: input.intent ?? "",
569
- detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
688
+ detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
570
689
  };
571
690
  }
572
691
  function buildDescriptionIndex(meta) {
@@ -647,6 +766,11 @@ function descriptionFromLegacyActivation(summary) {
647
766
  function shouldIncludeIndexItemForPath(_item, _meta, _path) {
648
767
  return true;
649
768
  }
769
+ function hasUndefinedDescription(meta) {
770
+ return Object.values(meta.nodes).some(
771
+ (node) => node.description === void 0 && node.activation?.description === void 0
772
+ );
773
+ }
650
774
  function buildPreflightDiagnostics(meta) {
651
775
  const missingDescriptionStableIds = Object.entries(meta.nodes).filter(([, node]) => node.description === void 0 && node.activation?.description === void 0).map(([nodeId, node]) => node.stable_id ?? nodeId).sort();
652
776
  if (missingDescriptionStableIds.length === 0) {
@@ -690,6 +814,8 @@ function registerPlanContext(server, tracker) {
690
814
  const requestId = randomUUID2();
691
815
  tracker?.enter(requestId);
692
816
  try {
817
+ const gateResult = await awaitFirstReconcileGate();
818
+ const gateWarn = gateWarning(gateResult);
693
819
  const projectRoot = resolveProjectRoot();
694
820
  const syncReport = await ensureKnowledgeFresh(projectRoot);
695
821
  const result = await planContext(projectRoot, {
@@ -703,7 +829,10 @@ function registerPlanContext(server, tracker) {
703
829
  });
704
830
  const response = {
705
831
  ...result,
706
- warnings: [...syncReport.warnings]
832
+ warnings: [
833
+ ...gateWarn ? [gateWarn] : [],
834
+ ...syncReport.warnings
835
+ ]
707
836
  };
708
837
  const payloadLimits = readPayloadLimits(projectRoot);
709
838
  const serialized = JSON.stringify(response);
@@ -952,7 +1081,7 @@ async function listPending(projectRoot, filters) {
952
1081
  }
953
1082
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
954
1083
  const itemTags = fm.tags ?? [];
955
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1084
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
956
1085
  if (!hasAll) continue;
957
1086
  }
958
1087
  if (filters?.created_after !== void 0) {
@@ -1125,8 +1254,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
1125
1254
  }
1126
1255
  return null;
1127
1256
  }
1128
- function inferTypeFromPath(path2) {
1129
- const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
1257
+ function inferTypeFromPath(path) {
1258
+ const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
1130
1259
  if (match === null) return null;
1131
1260
  const seg = match[1];
1132
1261
  if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
@@ -1134,8 +1263,8 @@ function inferTypeFromPath(path2) {
1134
1263
  }
1135
1264
  return null;
1136
1265
  }
1137
- function extractSlug(path2) {
1138
- const file = basename(path2).replace(/\.md$/u, "");
1266
+ function extractSlug(path) {
1267
+ const file = basename(path).replace(/\.md$/u, "");
1139
1268
  return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
1140
1269
  }
1141
1270
  async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
@@ -1252,7 +1381,7 @@ async function searchEntries(projectRoot, query, filters) {
1252
1381
  }
1253
1382
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
1254
1383
  const itemTags = fm.tags ?? [];
1255
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1384
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
1256
1385
  if (!hasAll) continue;
1257
1386
  }
1258
1387
  if (filters?.created_after !== void 0) {
@@ -1491,10 +1620,15 @@ function registerReview(server, tracker) {
1491
1620
  const requestId = randomUUID3();
1492
1621
  tracker?.enter(requestId);
1493
1622
  try {
1623
+ const gateResult = await awaitFirstReconcileGate();
1624
+ const gateWarn = gateWarning(gateResult);
1494
1625
  const narrowed = FabReviewInputSchema.parse(input);
1495
1626
  const projectRoot = resolveProjectRoot();
1496
1627
  const result = await reviewKnowledge(projectRoot, narrowed);
1497
- const response = result;
1628
+ const response = { ...result };
1629
+ if (gateWarn) {
1630
+ response.warnings = [gateWarn];
1631
+ }
1498
1632
  const payloadLimits = readPayloadLimits(projectRoot);
1499
1633
  const serialized = JSON.stringify(response);
1500
1634
  enforcePayloadLimit3(serialized, payloadLimits);
@@ -1522,62 +1656,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
1522
1656
  import { readFile as readFile4 } from "fs/promises";
1523
1657
  import { homedir as homedir3 } from "os";
1524
1658
  import { join as join4 } from "path";
1525
- var KNOWLEDGE_SECTION_NAMES = [
1526
- "MISSION_STATEMENT",
1527
- "MANDATORY_INJECTION",
1528
- "BUSINESS_LOGIC_CHUNKS",
1529
- "CONTEXT_INFO"
1530
- ];
1531
1659
  var PRIORITY_ORDER = {
1532
1660
  high: 0,
1533
1661
  medium: 1,
1534
1662
  low: 2
1535
1663
  };
1536
- function parseKnowledgeSections(content) {
1537
- const sections = /* @__PURE__ */ new Map();
1538
- const lines = content.split(/\r?\n/u);
1539
- let activeSection;
1540
- let activeSectionDepth = 0;
1541
- let buffer = [];
1542
- const flush = () => {
1543
- if (activeSection === void 0) {
1544
- return;
1545
- }
1546
- const text = buffer.join("\n").trim();
1547
- if (text.length === 0) {
1548
- buffer = [];
1549
- return;
1550
- }
1551
- sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
1552
- buffer = [];
1553
- };
1554
- for (const line of lines) {
1555
- const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
1556
- if (heading !== null) {
1557
- flush();
1558
- activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
1559
- activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
1560
- continue;
1561
- }
1562
- const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
1563
- if (ordinaryHeading !== null) {
1564
- if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
1565
- buffer.push(line);
1566
- continue;
1567
- }
1568
- flush();
1569
- activeSection = void 0;
1570
- activeSectionDepth = 0;
1571
- continue;
1572
- }
1573
- if (activeSection !== void 0) {
1574
- buffer.push(line);
1575
- }
1664
+ function extractBody(content) {
1665
+ const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
1666
+ if (match === null) {
1667
+ return content.replace(/^\uFEFF/u, "");
1576
1668
  }
1577
- flush();
1578
- return new Map(
1579
- Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
1580
- );
1669
+ return content.slice(match[0].length);
1581
1670
  }
1582
1671
  async function getKnowledgeSections(projectRoot, input) {
1583
1672
  const token = readSelectionToken(input.selection_token);
@@ -1592,21 +1681,7 @@ async function getKnowledgeSections(projectRoot, input) {
1592
1681
  const rules = [];
1593
1682
  for (const rule of selectedRules) {
1594
1683
  const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
1595
- const parsedSections = parseKnowledgeSections(content);
1596
- const sections = {};
1597
- for (const section of input.sections) {
1598
- const sectionContent = parsedSections.get(section);
1599
- sections[section] = sectionContent ?? "";
1600
- if (sectionContent === void 0) {
1601
- diagnostics.push({
1602
- code: "missing_section",
1603
- severity: "warn",
1604
- stable_id: rule.stable_id,
1605
- section,
1606
- message: `Rule ${rule.stable_id} does not define section ${section}.`
1607
- });
1608
- }
1609
- }
1684
+ const body = extractBody(content);
1610
1685
  const description = rule.node.description;
1611
1686
  if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
1612
1687
  diagnostics.push({
@@ -1620,7 +1695,7 @@ async function getKnowledgeSections(projectRoot, input) {
1620
1695
  stable_id: rule.stable_id,
1621
1696
  level: rule.level,
1622
1697
  path: rule.path,
1623
- sections
1698
+ body
1624
1699
  });
1625
1700
  }
1626
1701
  const result = {
@@ -1652,7 +1727,7 @@ async function getKnowledgeSections(projectRoot, input) {
1652
1727
  event_type: "knowledge_sections_fetched",
1653
1728
  selection_token: input.selection_token,
1654
1729
  target_paths: token.target_paths,
1655
- requested_sections: input.sections,
1730
+ requested_sections: [],
1656
1731
  final_stable_ids: result.selected_stable_ids,
1657
1732
  ai_selected_stable_ids: input.ai_selected_stable_ids,
1658
1733
  diagnostics,
@@ -1737,9 +1812,6 @@ function outputLevelOrder(level) {
1737
1812
  return 2;
1738
1813
  }
1739
1814
  }
1740
- function isKnowledgeSectionName(value) {
1741
- return KNOWLEDGE_SECTION_NAMES.includes(value);
1742
- }
1743
1815
  function resolveRuleSourcePath(projectRoot, contentRef) {
1744
1816
  if (contentRef.startsWith("~/.fabric/knowledge/")) {
1745
1817
  const home = process.env.FABRIC_HOME ?? homedir3();
@@ -1756,7 +1828,7 @@ function registerKnowledgeSections(server, tracker) {
1756
1828
  server.registerTool(
1757
1829
  "fab_get_knowledge_sections",
1758
1830
  {
1759
- description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
1831
+ description: "Fetch the full markdown body of one or more Fabric rules picked from fab_plan_context. Returns body strings keyed by stable_id (frontmatter stripped). Use after fab_plan_context returned selectable entries to load full rule content for LLM context injection \u2014 scan the body for whatever headings the rule defines (Summary / Why proposed / Session context / Evidence, etc.).",
1760
1832
  inputSchema: knowledgeSectionsInputSchema,
1761
1833
  outputSchema: knowledgeSectionsOutputSchema,
1762
1834
  annotations: knowledgeSectionsAnnotations
@@ -1765,12 +1837,17 @@ function registerKnowledgeSections(server, tracker) {
1765
1837
  const requestId = randomUUID4();
1766
1838
  tracker?.enter(requestId);
1767
1839
  try {
1840
+ const gateResult = await awaitFirstReconcileGate();
1841
+ const gateWarn = gateWarning(gateResult);
1768
1842
  const projectRoot = resolveProjectRoot();
1769
1843
  const syncReport = await ensureKnowledgeFresh(projectRoot);
1770
1844
  const result = await getKnowledgeSections(projectRoot, input);
1771
1845
  const response = {
1772
1846
  ...result,
1773
- warnings: [...syncReport.warnings]
1847
+ warnings: [
1848
+ ...gateWarn ? [gateWarn] : [],
1849
+ ...syncReport.warnings
1850
+ ]
1774
1851
  };
1775
1852
  const payloadLimits = readPayloadLimits(projectRoot);
1776
1853
  const serialized = JSON.stringify(response);
@@ -1796,99 +1873,6 @@ function registerKnowledgeSections(server, tracker) {
1796
1873
  );
1797
1874
  }
1798
1875
 
1799
- // src/services/serve-lock.ts
1800
- import fs from "fs";
1801
- import path from "path";
1802
- import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
1803
- import { IOFabricError } from "@fenglimg/fabric-shared/errors";
1804
- var LOCK_FILENAME = ".serve.lock";
1805
- var t = createTranslator(detectNodeLocale());
1806
- var ServeLockHeldError = class extends IOFabricError {
1807
- code = "SERVE_LOCK_HELD";
1808
- httpStatus = 423;
1809
- };
1810
- function lockPath(projectRoot) {
1811
- return path.join(projectRoot, ".fabric", LOCK_FILENAME);
1812
- }
1813
- function isAlive(pid) {
1814
- try {
1815
- process.kill(pid, 0);
1816
- return true;
1817
- } catch (e) {
1818
- const err = e;
1819
- if (err.code === "ESRCH") return false;
1820
- if (err.code === "EPERM") return true;
1821
- throw e;
1822
- }
1823
- }
1824
- function acquireLock(projectRoot, opts) {
1825
- const p = lockPath(projectRoot);
1826
- if (fs.existsSync(p)) {
1827
- let state = null;
1828
- try {
1829
- state = JSON.parse(fs.readFileSync(p, "utf8"));
1830
- } catch {
1831
- }
1832
- if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
1833
- throw new ServeLockHeldError(
1834
- `serve lock held by live PID ${state.pid}`,
1835
- {
1836
- actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1837
- details: state
1838
- }
1839
- );
1840
- }
1841
- if (state && state.pid && !isAlive(state.pid)) {
1842
- process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
1843
- `);
1844
- }
1845
- }
1846
- fs.mkdirSync(path.dirname(p), { recursive: true });
1847
- fs.writeFileSync(
1848
- p,
1849
- JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
1850
- );
1851
- }
1852
- function releaseLock(projectRoot) {
1853
- const p = lockPath(projectRoot);
1854
- try {
1855
- if (fs.existsSync(p)) {
1856
- const state = JSON.parse(fs.readFileSync(p, "utf8"));
1857
- if (state.pid === process.pid) {
1858
- fs.unlinkSync(p);
1859
- }
1860
- }
1861
- } catch {
1862
- }
1863
- }
1864
- function readLockState(projectRoot) {
1865
- const p = lockPath(projectRoot);
1866
- if (!fs.existsSync(p)) return null;
1867
- try {
1868
- return JSON.parse(fs.readFileSync(p, "utf8"));
1869
- } catch {
1870
- return null;
1871
- }
1872
- }
1873
- function checkLockOrThrow(projectRoot, opts) {
1874
- const state = readLockState(projectRoot);
1875
- if (state === null) return;
1876
- if (state.pid === process.pid) return;
1877
- if (!isAlive(state.pid)) {
1878
- process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
1879
- `);
1880
- return;
1881
- }
1882
- if (opts?.force) return;
1883
- throw new ServeLockHeldError(
1884
- `serve lock held by live PID ${state.pid}`,
1885
- {
1886
- actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1887
- details: state
1888
- }
1889
- );
1890
- }
1891
-
1892
1876
  // src/index.ts
1893
1877
  function writeStderr(message) {
1894
1878
  process.stderr.write(`${message}
@@ -1910,7 +1894,7 @@ function formatPreexistingRootMessage(projectRoot) {
1910
1894
  function createFabricServer(tracker) {
1911
1895
  const server = new McpServer({
1912
1896
  name: "fabric-knowledge-server",
1913
- version: "2.0.0-rc.22"
1897
+ version: "2.0.0-rc.25"
1914
1898
  });
1915
1899
  registerPlanContext(server, tracker);
1916
1900
  registerKnowledgeSections(server, tracker);
@@ -1925,10 +1909,10 @@ function createFabricServer(tracker) {
1925
1909
  },
1926
1910
  async (_uri) => {
1927
1911
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
1928
- const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1912
+ const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1929
1913
  let text = "";
1930
- if (existsSync4(path2)) {
1931
- text = await readFile5(path2, "utf8");
1914
+ if (existsSync4(path)) {
1915
+ text = await readFile5(path, "utf8");
1932
1916
  }
1933
1917
  return {
1934
1918
  contents: [
@@ -1946,13 +1930,6 @@ function createFabricServer(tracker) {
1946
1930
  async function startStdioServer() {
1947
1931
  const tracker = createInFlightTracker();
1948
1932
  const projectRoot = resolveProjectRoot();
1949
- const syncStart = Date.now();
1950
- const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
1951
- const syncDurationMs = Date.now() - syncStart;
1952
- process.stderr.write(
1953
- `[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
1954
- `
1955
- );
1956
1933
  const rootMsg = formatPreexistingRootMessage(projectRoot);
1957
1934
  if (rootMsg !== null) {
1958
1935
  process.stderr.write(`${rootMsg}
@@ -1961,6 +1938,21 @@ async function startStdioServer() {
1961
1938
  const server = createFabricServer(tracker);
1962
1939
  const transport = new StdioServerTransport();
1963
1940
  await server.connect(transport);
1941
+ const syncStart = Date.now();
1942
+ const backgroundReconcile = (async () => {
1943
+ const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
1944
+ const syncDurationMs = Date.now() - syncStart;
1945
+ process.stderr.write(
1946
+ `[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
1947
+ `
1948
+ );
1949
+ })().catch((error) => {
1950
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
1951
+ process.stderr.write(`[startup] rule sync FAILED: ${message}
1952
+ `);
1953
+ throw error;
1954
+ });
1955
+ setFirstReconcile(backgroundReconcile);
1964
1956
  const closeServer = async () => {
1965
1957
  await server.close();
1966
1958
  };
@@ -2010,7 +2002,7 @@ function createShutdownHandler(deps) {
2010
2002
  };
2011
2003
  }
2012
2004
  async function startHttpServer(options) {
2013
- const { createFabricHttpApp } = await import("./http-FF5NZCJK.js");
2005
+ const { createFabricHttpApp } = await import("./http-QBGLHCHA.js");
2014
2006
  const { port, projectRoot, host = "127.0.0.1", authToken } = options;
2015
2007
  const app = createFabricHttpApp({ projectRoot, host, authToken });
2016
2008
  return await new Promise((resolveServer, rejectServer) => {
@@ -2053,6 +2045,7 @@ export {
2053
2045
  createShutdownHandler,
2054
2046
  deriveKnowledgeMetaLayer,
2055
2047
  deriveKnowledgeMetaTopologyType,
2048
+ enrichDescriptions,
2056
2049
  ensureKnowledgeFresh,
2057
2050
  extractKnowledge,
2058
2051
  flushAndSyncEventLedger,
@@ -2061,6 +2054,7 @@ export {
2061
2054
  getLedgerPath,
2062
2055
  getLegacyLedgerPath,
2063
2056
  isSameKnowledgeTestIndex,
2057
+ loadKbIdTypeMap,
2064
2058
  planContext,
2065
2059
  readLockState,
2066
2060
  readSelectionToken,
@@ -2068,6 +2062,7 @@ export {
2068
2062
  releaseLock,
2069
2063
  reviewKnowledge,
2070
2064
  runDoctorApplyLint,
2065
+ runDoctorArchiveHistory,
2071
2066
  runDoctorCiteCoverage,
2072
2067
  runDoctorFix,
2073
2068
  runDoctorReport,