@fenglimg/fabric-server 2.0.0-rc.22 → 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.
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,
@@ -20,7 +24,9 @@ import {
20
24
  loadActiveMeta,
21
25
  loadActiveMetaOrStale,
22
26
  normalizeKnowledgePath,
27
+ readLockState,
23
28
  reconcileKnowledge,
29
+ releaseLock,
24
30
  resolveProjectRoot,
25
31
  runDoctorApplyLint,
26
32
  runDoctorCiteCoverage,
@@ -29,7 +35,7 @@ import {
29
35
  sha256,
30
36
  stableStringify,
31
37
  writeKnowledgeMeta
32
- } from "./chunk-7N3FW5LX.js";
38
+ } from "./chunk-IRB77C6E.js";
33
39
 
34
40
  // src/index.ts
35
41
  import { existsSync as existsSync4 } from "fs";
@@ -42,6 +48,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
42
48
  // src/constants.ts
43
49
  var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
44
50
 
51
+ // src/services/first-reconcile-gate.ts
52
+ function gateWarning(result) {
53
+ if (result.status === "ready") return null;
54
+ if (result.status === "stale") {
55
+ return {
56
+ code: "meta_stale",
57
+ file: "<response>",
58
+ action_hint: "Initial reconcile still pending; results may use cached meta. Retry shortly or run `fab doctor --fix`."
59
+ };
60
+ }
61
+ return {
62
+ code: "reconcile_failed",
63
+ file: "<response>",
64
+ action_hint: "Reconcile failed at startup; run `fab doctor --fix` and restart the MCP server."
65
+ };
66
+ }
67
+ var state = {
68
+ firstReconcilePromise: null,
69
+ reconcileFailure: null,
70
+ settled: false
71
+ };
72
+ function setFirstReconcile(reconcilePromise) {
73
+ state.firstReconcilePromise = reconcilePromise.then(
74
+ () => {
75
+ state.settled = true;
76
+ },
77
+ (error) => {
78
+ state.reconcileFailure = error;
79
+ state.settled = true;
80
+ }
81
+ );
82
+ }
83
+ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
84
+ if (state.reconcileFailure !== null) {
85
+ return { status: "failed", error: state.reconcileFailure };
86
+ }
87
+ if (state.firstReconcilePromise === null || state.settled) {
88
+ return { status: "ready" };
89
+ }
90
+ let timer = null;
91
+ const timeoutPromise = new Promise((resolveTimeout) => {
92
+ timer = setTimeout(() => resolveTimeout("timeout"), timeoutMs);
93
+ });
94
+ try {
95
+ const winner = await Promise.race([
96
+ state.firstReconcilePromise.then(() => "reconcile"),
97
+ timeoutPromise
98
+ ]);
99
+ if (winner === "timeout") {
100
+ return { status: "stale" };
101
+ }
102
+ if (state.reconcileFailure !== null) {
103
+ return { status: "failed", error: state.reconcileFailure };
104
+ }
105
+ return { status: "ready" };
106
+ } finally {
107
+ if (timer !== null) clearTimeout(timer);
108
+ }
109
+ }
110
+
45
111
  // src/services/in-flight-tracker.ts
46
112
  function createInFlightTracker() {
47
113
  const active = /* @__PURE__ */ new Map();
@@ -133,7 +199,7 @@ async function extractKnowledge(projectRoot, input) {
133
199
  }
134
200
  }
135
201
  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] : [];
202
+ const sourceSessions = input.source_sessions ?? [];
137
203
  const primarySession = sourceSessions[0] ?? "";
138
204
  const idempotencyKey = sha256(
139
205
  JSON.stringify({
@@ -217,7 +283,20 @@ async function extractKnowledge(projectRoot, input) {
217
283
  proposedReason: input.proposed_reason,
218
284
  sessionContext: input.session_context,
219
285
  relevanceScope,
220
- relevancePaths
286
+ relevancePaths,
287
+ // v2.0.0-rc.23 TASK-006 (a-C1): optional structured triage fields. Each is
288
+ // emitted as a YAML line only when caller-supplied; omitted lines preserve
289
+ // the historical pending-file shape.
290
+ intentClues: input.intent_clues,
291
+ techStack: input.tech_stack,
292
+ impact: input.impact,
293
+ mustReadIf: input.must_read_if,
294
+ // v2.0.0-rc.23 TASK-014 (F8c): optional S5 onboard-slot tag. Same emit
295
+ // discipline as the four a-C1 fields — bare YAML line iff caller-supplied,
296
+ // never in the idempotency_key hash. fabric-archive's first-run phase is
297
+ // the only producer; downstream `fab onboard-coverage` walks frontmatter
298
+ // looking for this exact key.
299
+ onboardSlot: input.onboard_slot
221
300
  });
222
301
  await atomicWriteText(absolutePath, fresh);
223
302
  await emitEventBestEffort(projectRoot, {
@@ -260,6 +339,24 @@ function renderFreshEntry(args) {
260
339
  const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
261
340
  frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
262
341
  }
342
+ if (args.intentClues !== void 0) {
343
+ const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
344
+ frontmatterLines.push(`intent_clues: [${body2}]`);
345
+ }
346
+ if (args.techStack !== void 0) {
347
+ const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
348
+ frontmatterLines.push(`tech_stack: [${body2}]`);
349
+ }
350
+ if (args.impact !== void 0) {
351
+ const body2 = args.impact.map((s) => quoteRelevancePath(s)).join(", ");
352
+ frontmatterLines.push(`impact: [${body2}]`);
353
+ }
354
+ if (args.mustReadIf !== void 0) {
355
+ frontmatterLines.push(`must_read_if: ${quoteRelevancePath(args.mustReadIf)}`);
356
+ }
357
+ if (args.onboardSlot !== void 0) {
358
+ frontmatterLines.push(`onboard_slot: ${args.onboardSlot}`);
359
+ }
263
360
  frontmatterLines.push(
264
361
  `x-fabric-idempotency-key: ${args.idempotencyKey}`,
265
362
  "---"
@@ -328,9 +425,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
328
425
  const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
329
426
  if (pathSection !== null) {
330
427
  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());
428
+ const t = rawLine.trim();
429
+ if (t.startsWith("- ")) {
430
+ existingPaths.push(t.slice(2).trim());
334
431
  }
335
432
  }
336
433
  }
@@ -339,16 +436,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
339
436
  const bulletLines = [];
340
437
  let prose = [];
341
438
  for (const rawLine of noteBody.split(/\r?\n/u)) {
342
- const t2 = rawLine.trim();
343
- if (t2.length === 0) continue;
344
- if (t2.startsWith("- ")) {
439
+ const t = rawLine.trim();
440
+ if (t.length === 0) continue;
441
+ if (t.startsWith("- ")) {
345
442
  if (prose.length > 0) {
346
443
  existingNotes.push(prose.join(" ").trim());
347
444
  prose = [];
348
445
  }
349
- bulletLines.push(t2.slice(2).trim());
446
+ bulletLines.push(t.slice(2).trim());
350
447
  } else {
351
- prose.push(t2);
448
+ prose.push(t);
352
449
  }
353
450
  }
354
451
  if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
@@ -423,7 +520,7 @@ function registerExtractKnowledge(server, tracker) {
423
520
  server.registerTool(
424
521
  "fab_extract_knowledge",
425
522
  {
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.",
523
+ 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
524
  inputSchema: FabExtractKnowledgeInputShape,
428
525
  outputSchema: FabExtractKnowledgeOutputSchema.shape,
429
526
  annotations: fabExtractKnowledgeAnnotations
@@ -432,9 +529,14 @@ function registerExtractKnowledge(server, tracker) {
432
529
  const requestId = randomUUID();
433
530
  tracker?.enter(requestId);
434
531
  try {
532
+ const gateResult = await awaitFirstReconcileGate();
533
+ const gateWarn = gateWarning(gateResult);
435
534
  const projectRoot = resolveProjectRoot();
436
535
  const result = await extractKnowledge(projectRoot, input);
437
536
  const response = { ...result };
537
+ if (gateWarn) {
538
+ response.warnings = [gateWarn];
539
+ }
438
540
  const payloadLimits = readPayloadLimits(projectRoot);
439
541
  const serialized = JSON.stringify(response);
440
542
  enforcePayloadLimit(serialized, payloadLimits);
@@ -464,17 +566,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
464
566
  var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
465
567
  var selectionTokenCache = /* @__PURE__ */ new Map();
466
568
  async function planContext(projectRoot, input) {
467
- const metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
468
- const meta = metaResult.meta;
569
+ let metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
570
+ let meta = metaResult.meta;
571
+ let firstSeenPreviousRevision = metaResult.previous_revision_hash;
572
+ let autoHealedAccumulated = metaResult.auto_healed;
573
+ if (metaResult.auto_healed !== true && hasUndefinedDescription(meta)) {
574
+ try {
575
+ await reconcileKnowledge(projectRoot, { trigger: "auto-heal-description" });
576
+ const healedResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
577
+ meta = healedResult.meta;
578
+ autoHealedAccumulated = true;
579
+ firstSeenPreviousRevision = metaResult.previous_revision_hash;
580
+ metaResult = healedResult;
581
+ } catch {
582
+ }
583
+ }
469
584
  const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
470
585
  const uniquePaths = dedupePaths(input.paths);
471
586
  const allDescriptions = buildDescriptionIndex(meta);
472
587
  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));
588
+ const entries = uniquePaths.map((path) => {
589
+ const profile = buildRequirementProfile(path, input);
590
+ const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
476
591
  return {
477
- path: path2,
592
+ path,
478
593
  requirement_profile: profile,
479
594
  description_index: descriptionIndex
480
595
  };
@@ -491,12 +606,14 @@ async function planContext(projectRoot, input) {
491
606
  description_index: sharedDescriptionIndex,
492
607
  preflight_diagnostics: buildPreflightDiagnostics(meta)
493
608
  },
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 ? {
609
+ // v2.0.0-rc.22 Scope D T-D2 + rc.23 TASK-005 (a-B): surface auto-heal pair
610
+ // only when a heal actually fired (either revision-drift heal in
611
+ // loadActiveMetaOrStale or description-undefined heal driven from here).
612
+ // Keeping these fields absent on the steady-state path means existing
613
+ // consumers see the same wire shape they always have.
614
+ ...autoHealedAccumulated ? {
498
615
  auto_healed: true,
499
- previous_revision_hash: metaResult.previous_revision_hash
616
+ previous_revision_hash: firstSeenPreviousRevision
500
617
  } : {}
501
618
  };
502
619
  try {
@@ -519,15 +636,15 @@ async function planContext(projectRoot, input) {
519
636
  return result;
520
637
  }
521
638
  function readSelectionToken(token, now = Date.now()) {
522
- const state = selectionTokenCache.get(token);
523
- if (state === void 0) {
639
+ const state2 = selectionTokenCache.get(token);
640
+ if (state2 === void 0) {
524
641
  return void 0;
525
642
  }
526
- if (state.expires_at <= now) {
643
+ if (state2.expires_at <= now) {
527
644
  selectionTokenCache.delete(token);
528
645
  return void 0;
529
646
  }
530
- return state;
647
+ return state2;
531
648
  }
532
649
  function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
533
650
  const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
@@ -544,8 +661,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
544
661
  }
545
662
  function dedupePaths(paths) {
546
663
  const seenPaths = /* @__PURE__ */ new Set();
547
- return paths.flatMap((path2) => {
548
- const normalizedPath = normalizeKnowledgePath(path2);
664
+ return paths.flatMap((path) => {
665
+ const normalizedPath = normalizeKnowledgePath(path);
549
666
  if (seenPaths.has(normalizedPath)) {
550
667
  return [];
551
668
  }
@@ -553,8 +670,8 @@ function dedupePaths(paths) {
553
670
  return [normalizedPath];
554
671
  });
555
672
  }
556
- function buildRequirementProfile(path2, input) {
557
- const normalizedPath = normalizeKnowledgePath(path2);
673
+ function buildRequirementProfile(path, input) {
674
+ const normalizedPath = normalizeKnowledgePath(path);
558
675
  const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
559
676
  const knownTech = dedupeStableIds([
560
677
  ...input.known_tech ?? [],
@@ -566,7 +683,7 @@ function buildRequirementProfile(path2, input) {
566
683
  extension: extensionMatch?.[1] ?? "",
567
684
  known_tech: knownTech,
568
685
  user_intent: input.intent ?? "",
569
- detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
686
+ detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
570
687
  };
571
688
  }
572
689
  function buildDescriptionIndex(meta) {
@@ -647,6 +764,11 @@ function descriptionFromLegacyActivation(summary) {
647
764
  function shouldIncludeIndexItemForPath(_item, _meta, _path) {
648
765
  return true;
649
766
  }
767
+ function hasUndefinedDescription(meta) {
768
+ return Object.values(meta.nodes).some(
769
+ (node) => node.description === void 0 && node.activation?.description === void 0
770
+ );
771
+ }
650
772
  function buildPreflightDiagnostics(meta) {
651
773
  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
774
  if (missingDescriptionStableIds.length === 0) {
@@ -690,6 +812,8 @@ function registerPlanContext(server, tracker) {
690
812
  const requestId = randomUUID2();
691
813
  tracker?.enter(requestId);
692
814
  try {
815
+ const gateResult = await awaitFirstReconcileGate();
816
+ const gateWarn = gateWarning(gateResult);
693
817
  const projectRoot = resolveProjectRoot();
694
818
  const syncReport = await ensureKnowledgeFresh(projectRoot);
695
819
  const result = await planContext(projectRoot, {
@@ -703,7 +827,10 @@ function registerPlanContext(server, tracker) {
703
827
  });
704
828
  const response = {
705
829
  ...result,
706
- warnings: [...syncReport.warnings]
830
+ warnings: [
831
+ ...gateWarn ? [gateWarn] : [],
832
+ ...syncReport.warnings
833
+ ]
707
834
  };
708
835
  const payloadLimits = readPayloadLimits(projectRoot);
709
836
  const serialized = JSON.stringify(response);
@@ -952,7 +1079,7 @@ async function listPending(projectRoot, filters) {
952
1079
  }
953
1080
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
954
1081
  const itemTags = fm.tags ?? [];
955
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1082
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
956
1083
  if (!hasAll) continue;
957
1084
  }
958
1085
  if (filters?.created_after !== void 0) {
@@ -1125,8 +1252,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
1125
1252
  }
1126
1253
  return null;
1127
1254
  }
1128
- function inferTypeFromPath(path2) {
1129
- const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
1255
+ function inferTypeFromPath(path) {
1256
+ const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
1130
1257
  if (match === null) return null;
1131
1258
  const seg = match[1];
1132
1259
  if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
@@ -1134,8 +1261,8 @@ function inferTypeFromPath(path2) {
1134
1261
  }
1135
1262
  return null;
1136
1263
  }
1137
- function extractSlug(path2) {
1138
- const file = basename(path2).replace(/\.md$/u, "");
1264
+ function extractSlug(path) {
1265
+ const file = basename(path).replace(/\.md$/u, "");
1139
1266
  return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
1140
1267
  }
1141
1268
  async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
@@ -1252,7 +1379,7 @@ async function searchEntries(projectRoot, query, filters) {
1252
1379
  }
1253
1380
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
1254
1381
  const itemTags = fm.tags ?? [];
1255
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1382
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
1256
1383
  if (!hasAll) continue;
1257
1384
  }
1258
1385
  if (filters?.created_after !== void 0) {
@@ -1491,10 +1618,15 @@ function registerReview(server, tracker) {
1491
1618
  const requestId = randomUUID3();
1492
1619
  tracker?.enter(requestId);
1493
1620
  try {
1621
+ const gateResult = await awaitFirstReconcileGate();
1622
+ const gateWarn = gateWarning(gateResult);
1494
1623
  const narrowed = FabReviewInputSchema.parse(input);
1495
1624
  const projectRoot = resolveProjectRoot();
1496
1625
  const result = await reviewKnowledge(projectRoot, narrowed);
1497
- const response = result;
1626
+ const response = { ...result };
1627
+ if (gateWarn) {
1628
+ response.warnings = [gateWarn];
1629
+ }
1498
1630
  const payloadLimits = readPayloadLimits(projectRoot);
1499
1631
  const serialized = JSON.stringify(response);
1500
1632
  enforcePayloadLimit3(serialized, payloadLimits);
@@ -1522,62 +1654,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
1522
1654
  import { readFile as readFile4 } from "fs/promises";
1523
1655
  import { homedir as homedir3 } from "os";
1524
1656
  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
1657
  var PRIORITY_ORDER = {
1532
1658
  high: 0,
1533
1659
  medium: 1,
1534
1660
  low: 2
1535
1661
  };
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
- }
1662
+ function extractBody(content) {
1663
+ const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
1664
+ if (match === null) {
1665
+ return content.replace(/^\uFEFF/u, "");
1576
1666
  }
1577
- flush();
1578
- return new Map(
1579
- Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
1580
- );
1667
+ return content.slice(match[0].length);
1581
1668
  }
1582
1669
  async function getKnowledgeSections(projectRoot, input) {
1583
1670
  const token = readSelectionToken(input.selection_token);
@@ -1592,21 +1679,7 @@ async function getKnowledgeSections(projectRoot, input) {
1592
1679
  const rules = [];
1593
1680
  for (const rule of selectedRules) {
1594
1681
  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
- }
1682
+ const body = extractBody(content);
1610
1683
  const description = rule.node.description;
1611
1684
  if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
1612
1685
  diagnostics.push({
@@ -1620,7 +1693,7 @@ async function getKnowledgeSections(projectRoot, input) {
1620
1693
  stable_id: rule.stable_id,
1621
1694
  level: rule.level,
1622
1695
  path: rule.path,
1623
- sections
1696
+ body
1624
1697
  });
1625
1698
  }
1626
1699
  const result = {
@@ -1652,7 +1725,7 @@ async function getKnowledgeSections(projectRoot, input) {
1652
1725
  event_type: "knowledge_sections_fetched",
1653
1726
  selection_token: input.selection_token,
1654
1727
  target_paths: token.target_paths,
1655
- requested_sections: input.sections,
1728
+ requested_sections: [],
1656
1729
  final_stable_ids: result.selected_stable_ids,
1657
1730
  ai_selected_stable_ids: input.ai_selected_stable_ids,
1658
1731
  diagnostics,
@@ -1737,9 +1810,6 @@ function outputLevelOrder(level) {
1737
1810
  return 2;
1738
1811
  }
1739
1812
  }
1740
- function isKnowledgeSectionName(value) {
1741
- return KNOWLEDGE_SECTION_NAMES.includes(value);
1742
- }
1743
1813
  function resolveRuleSourcePath(projectRoot, contentRef) {
1744
1814
  if (contentRef.startsWith("~/.fabric/knowledge/")) {
1745
1815
  const home = process.env.FABRIC_HOME ?? homedir3();
@@ -1756,7 +1826,7 @@ function registerKnowledgeSections(server, tracker) {
1756
1826
  server.registerTool(
1757
1827
  "fab_get_knowledge_sections",
1758
1828
  {
1759
- description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
1829
+ 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
1830
  inputSchema: knowledgeSectionsInputSchema,
1761
1831
  outputSchema: knowledgeSectionsOutputSchema,
1762
1832
  annotations: knowledgeSectionsAnnotations
@@ -1765,12 +1835,17 @@ function registerKnowledgeSections(server, tracker) {
1765
1835
  const requestId = randomUUID4();
1766
1836
  tracker?.enter(requestId);
1767
1837
  try {
1838
+ const gateResult = await awaitFirstReconcileGate();
1839
+ const gateWarn = gateWarning(gateResult);
1768
1840
  const projectRoot = resolveProjectRoot();
1769
1841
  const syncReport = await ensureKnowledgeFresh(projectRoot);
1770
1842
  const result = await getKnowledgeSections(projectRoot, input);
1771
1843
  const response = {
1772
1844
  ...result,
1773
- warnings: [...syncReport.warnings]
1845
+ warnings: [
1846
+ ...gateWarn ? [gateWarn] : [],
1847
+ ...syncReport.warnings
1848
+ ]
1774
1849
  };
1775
1850
  const payloadLimits = readPayloadLimits(projectRoot);
1776
1851
  const serialized = JSON.stringify(response);
@@ -1796,99 +1871,6 @@ function registerKnowledgeSections(server, tracker) {
1796
1871
  );
1797
1872
  }
1798
1873
 
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
1874
  // src/index.ts
1893
1875
  function writeStderr(message) {
1894
1876
  process.stderr.write(`${message}
@@ -1910,7 +1892,7 @@ function formatPreexistingRootMessage(projectRoot) {
1910
1892
  function createFabricServer(tracker) {
1911
1893
  const server = new McpServer({
1912
1894
  name: "fabric-knowledge-server",
1913
- version: "2.0.0-rc.22"
1895
+ version: "2.0.0-rc.23"
1914
1896
  });
1915
1897
  registerPlanContext(server, tracker);
1916
1898
  registerKnowledgeSections(server, tracker);
@@ -1925,10 +1907,10 @@ function createFabricServer(tracker) {
1925
1907
  },
1926
1908
  async (_uri) => {
1927
1909
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
1928
- const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1910
+ const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1929
1911
  let text = "";
1930
- if (existsSync4(path2)) {
1931
- text = await readFile5(path2, "utf8");
1912
+ if (existsSync4(path)) {
1913
+ text = await readFile5(path, "utf8");
1932
1914
  }
1933
1915
  return {
1934
1916
  contents: [
@@ -1946,13 +1928,6 @@ function createFabricServer(tracker) {
1946
1928
  async function startStdioServer() {
1947
1929
  const tracker = createInFlightTracker();
1948
1930
  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
1931
  const rootMsg = formatPreexistingRootMessage(projectRoot);
1957
1932
  if (rootMsg !== null) {
1958
1933
  process.stderr.write(`${rootMsg}
@@ -1961,6 +1936,21 @@ async function startStdioServer() {
1961
1936
  const server = createFabricServer(tracker);
1962
1937
  const transport = new StdioServerTransport();
1963
1938
  await server.connect(transport);
1939
+ const syncStart = Date.now();
1940
+ const backgroundReconcile = (async () => {
1941
+ const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
1942
+ const syncDurationMs = Date.now() - syncStart;
1943
+ process.stderr.write(
1944
+ `[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
1945
+ `
1946
+ );
1947
+ })().catch((error) => {
1948
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
1949
+ process.stderr.write(`[startup] rule sync FAILED: ${message}
1950
+ `);
1951
+ throw error;
1952
+ });
1953
+ setFirstReconcile(backgroundReconcile);
1964
1954
  const closeServer = async () => {
1965
1955
  await server.close();
1966
1956
  };
@@ -2010,7 +2000,7 @@ function createShutdownHandler(deps) {
2010
2000
  };
2011
2001
  }
2012
2002
  async function startHttpServer(options) {
2013
- const { createFabricHttpApp } = await import("./http-FF5NZCJK.js");
2003
+ const { createFabricHttpApp } = await import("./http-ZBV6YUHD.js");
2014
2004
  const { port, projectRoot, host = "127.0.0.1", authToken } = options;
2015
2005
  const app = createFabricHttpApp({ projectRoot, host, authToken });
2016
2006
  return await new Promise((resolveServer, rejectServer) => {
@@ -2053,6 +2043,7 @@ export {
2053
2043
  createShutdownHandler,
2054
2044
  deriveKnowledgeMetaLayer,
2055
2045
  deriveKnowledgeMetaTopologyType,
2046
+ enrichDescriptions,
2056
2047
  ensureKnowledgeFresh,
2057
2048
  extractKnowledge,
2058
2049
  flushAndSyncEventLedger,