@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.
package/dist/index.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import {
2
+ AgentsMetaFileMissingError,
2
3
  EVENT_LEDGER_PATH,
3
4
  LEDGER_PATH,
4
5
  LEGACY_LEDGER_PATH,
6
+ ServeLockHeldError,
7
+ acquireLock,
5
8
  appendEventLedgerEvent,
6
9
  atomicWriteText,
7
10
  buildKnowledgeMeta,
11
+ checkLockOrThrow,
8
12
  computeKnowledgeBasedAgentsMeta,
9
13
  computeKnowledgeTestIndex,
10
14
  deriveKnowledgeMetaLayer,
11
15
  deriveKnowledgeMetaTopologyType,
16
+ enrichDescriptions,
12
17
  ensureKnowledgeFresh,
13
18
  ensureParentDirectory,
14
19
  flushAndSyncEventLedger,
@@ -16,9 +21,12 @@ import {
16
21
  getLedgerPath,
17
22
  getLegacyLedgerPath,
18
23
  isSameKnowledgeTestIndex,
24
+ loadActiveMeta,
25
+ loadActiveMetaOrStale,
19
26
  normalizeKnowledgePath,
20
- readAgentsMeta,
27
+ readLockState,
21
28
  reconcileKnowledge,
29
+ releaseLock,
22
30
  resolveProjectRoot,
23
31
  runDoctorApplyLint,
24
32
  runDoctorCiteCoverage,
@@ -27,7 +35,7 @@ import {
27
35
  sha256,
28
36
  stableStringify,
29
37
  writeKnowledgeMeta
30
- } from "./chunk-7R6MFA7Y.js";
38
+ } from "./chunk-IRB77C6E.js";
31
39
 
32
40
  // src/index.ts
33
41
  import { existsSync as existsSync4 } from "fs";
@@ -40,6 +48,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
40
48
  // src/constants.ts
41
49
  var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
42
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
+
43
111
  // src/services/in-flight-tracker.ts
44
112
  function createInFlightTracker() {
45
113
  const active = /* @__PURE__ */ new Map();
@@ -123,8 +191,15 @@ function resolvePersonalRoot() {
123
191
  return process.env.FABRIC_HOME ?? homedir();
124
192
  }
125
193
  async function extractKnowledge(projectRoot, input) {
194
+ try {
195
+ await loadActiveMeta(projectRoot, { caller: "extractKnowledge" });
196
+ } catch (error) {
197
+ if (!(error instanceof AgentsMetaFileMissingError)) {
198
+ throw error;
199
+ }
200
+ }
126
201
  const sanitizedSlug = sanitizeSlug(input.slug);
127
- 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 ?? [];
128
203
  const primarySession = sourceSessions[0] ?? "";
129
204
  const idempotencyKey = sha256(
130
205
  JSON.stringify({
@@ -208,7 +283,20 @@ async function extractKnowledge(projectRoot, input) {
208
283
  proposedReason: input.proposed_reason,
209
284
  sessionContext: input.session_context,
210
285
  relevanceScope,
211
- 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
212
300
  });
213
301
  await atomicWriteText(absolutePath, fresh);
214
302
  await emitEventBestEffort(projectRoot, {
@@ -251,6 +339,24 @@ function renderFreshEntry(args) {
251
339
  const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
252
340
  frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
253
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
+ }
254
360
  frontmatterLines.push(
255
361
  `x-fabric-idempotency-key: ${args.idempotencyKey}`,
256
362
  "---"
@@ -319,9 +425,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
319
425
  const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
320
426
  if (pathSection !== null) {
321
427
  for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
322
- const t2 = rawLine.trim();
323
- if (t2.startsWith("- ")) {
324
- existingPaths.push(t2.slice(2).trim());
428
+ const t = rawLine.trim();
429
+ if (t.startsWith("- ")) {
430
+ existingPaths.push(t.slice(2).trim());
325
431
  }
326
432
  }
327
433
  }
@@ -330,16 +436,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
330
436
  const bulletLines = [];
331
437
  let prose = [];
332
438
  for (const rawLine of noteBody.split(/\r?\n/u)) {
333
- const t2 = rawLine.trim();
334
- if (t2.length === 0) continue;
335
- if (t2.startsWith("- ")) {
439
+ const t = rawLine.trim();
440
+ if (t.length === 0) continue;
441
+ if (t.startsWith("- ")) {
336
442
  if (prose.length > 0) {
337
443
  existingNotes.push(prose.join(" ").trim());
338
444
  prose = [];
339
445
  }
340
- bulletLines.push(t2.slice(2).trim());
446
+ bulletLines.push(t.slice(2).trim());
341
447
  } else {
342
- prose.push(t2);
448
+ prose.push(t);
343
449
  }
344
450
  }
345
451
  if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
@@ -414,7 +520,7 @@ function registerExtractKnowledge(server, tracker) {
414
520
  server.registerTool(
415
521
  "fab_extract_knowledge",
416
522
  {
417
- 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.",
418
524
  inputSchema: FabExtractKnowledgeInputShape,
419
525
  outputSchema: FabExtractKnowledgeOutputSchema.shape,
420
526
  annotations: fabExtractKnowledgeAnnotations
@@ -423,9 +529,14 @@ function registerExtractKnowledge(server, tracker) {
423
529
  const requestId = randomUUID();
424
530
  tracker?.enter(requestId);
425
531
  try {
532
+ const gateResult = await awaitFirstReconcileGate();
533
+ const gateWarn = gateWarning(gateResult);
426
534
  const projectRoot = resolveProjectRoot();
427
535
  const result = await extractKnowledge(projectRoot, input);
428
536
  const response = { ...result };
537
+ if (gateWarn) {
538
+ response.warnings = [gateWarn];
539
+ }
429
540
  const payloadLimits = readPayloadLimits(projectRoot);
430
541
  const serialized = JSON.stringify(response);
431
542
  enforcePayloadLimit(serialized, payloadLimits);
@@ -455,16 +566,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
455
566
  var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
456
567
  var selectionTokenCache = /* @__PURE__ */ new Map();
457
568
  async function planContext(projectRoot, input) {
458
- const meta = await readAgentsMeta(projectRoot);
459
- const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
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
+ }
584
+ const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
460
585
  const uniquePaths = dedupePaths(input.paths);
461
586
  const allDescriptions = buildDescriptionIndex(meta);
462
587
  const relevanceTargetPaths = input.target_paths ?? uniquePaths;
463
- const entries = uniquePaths.map((path2) => {
464
- const profile = buildRequirementProfile(path2, input);
465
- 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));
466
591
  return {
467
- path: path2,
592
+ path,
468
593
  requirement_profile: profile,
469
594
  description_index: descriptionIndex
470
595
  };
@@ -480,7 +605,16 @@ async function planContext(projectRoot, input) {
480
605
  shared: {
481
606
  description_index: sharedDescriptionIndex,
482
607
  preflight_diagnostics: buildPreflightDiagnostics(meta)
483
- }
608
+ },
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 ? {
615
+ auto_healed: true,
616
+ previous_revision_hash: firstSeenPreviousRevision
617
+ } : {}
484
618
  };
485
619
  try {
486
620
  await appendEventLedgerEvent(projectRoot, {
@@ -502,15 +636,15 @@ async function planContext(projectRoot, input) {
502
636
  return result;
503
637
  }
504
638
  function readSelectionToken(token, now = Date.now()) {
505
- const state = selectionTokenCache.get(token);
506
- if (state === void 0) {
639
+ const state2 = selectionTokenCache.get(token);
640
+ if (state2 === void 0) {
507
641
  return void 0;
508
642
  }
509
- if (state.expires_at <= now) {
643
+ if (state2.expires_at <= now) {
510
644
  selectionTokenCache.delete(token);
511
645
  return void 0;
512
646
  }
513
- return state;
647
+ return state2;
514
648
  }
515
649
  function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
516
650
  const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
@@ -527,8 +661,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
527
661
  }
528
662
  function dedupePaths(paths) {
529
663
  const seenPaths = /* @__PURE__ */ new Set();
530
- return paths.flatMap((path2) => {
531
- const normalizedPath = normalizeKnowledgePath(path2);
664
+ return paths.flatMap((path) => {
665
+ const normalizedPath = normalizeKnowledgePath(path);
532
666
  if (seenPaths.has(normalizedPath)) {
533
667
  return [];
534
668
  }
@@ -536,8 +670,8 @@ function dedupePaths(paths) {
536
670
  return [normalizedPath];
537
671
  });
538
672
  }
539
- function buildRequirementProfile(path2, input) {
540
- const normalizedPath = normalizeKnowledgePath(path2);
673
+ function buildRequirementProfile(path, input) {
674
+ const normalizedPath = normalizeKnowledgePath(path);
541
675
  const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
542
676
  const knownTech = dedupeStableIds([
543
677
  ...input.known_tech ?? [],
@@ -549,7 +683,7 @@ function buildRequirementProfile(path2, input) {
549
683
  extension: extensionMatch?.[1] ?? "",
550
684
  known_tech: knownTech,
551
685
  user_intent: input.intent ?? "",
552
- detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
686
+ detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
553
687
  };
554
688
  }
555
689
  function buildDescriptionIndex(meta) {
@@ -630,6 +764,11 @@ function descriptionFromLegacyActivation(summary) {
630
764
  function shouldIncludeIndexItemForPath(_item, _meta, _path) {
631
765
  return true;
632
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
+ }
633
772
  function buildPreflightDiagnostics(meta) {
634
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();
635
774
  if (missingDescriptionStableIds.length === 0) {
@@ -673,6 +812,8 @@ function registerPlanContext(server, tracker) {
673
812
  const requestId = randomUUID2();
674
813
  tracker?.enter(requestId);
675
814
  try {
815
+ const gateResult = await awaitFirstReconcileGate();
816
+ const gateWarn = gateWarning(gateResult);
676
817
  const projectRoot = resolveProjectRoot();
677
818
  const syncReport = await ensureKnowledgeFresh(projectRoot);
678
819
  const result = await planContext(projectRoot, {
@@ -686,7 +827,10 @@ function registerPlanContext(server, tracker) {
686
827
  });
687
828
  const response = {
688
829
  ...result,
689
- warnings: [...syncReport.warnings]
830
+ warnings: [
831
+ ...gateWarn ? [gateWarn] : [],
832
+ ...syncReport.warnings
833
+ ]
690
834
  };
691
835
  const payloadLimits = readPayloadLimits(projectRoot);
692
836
  const serialized = JSON.stringify(response);
@@ -935,7 +1079,7 @@ async function listPending(projectRoot, filters) {
935
1079
  }
936
1080
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
937
1081
  const itemTags = fm.tags ?? [];
938
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1082
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
939
1083
  if (!hasAll) continue;
940
1084
  }
941
1085
  if (filters?.created_after !== void 0) {
@@ -1108,8 +1252,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
1108
1252
  }
1109
1253
  return null;
1110
1254
  }
1111
- function inferTypeFromPath(path2) {
1112
- const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
1255
+ function inferTypeFromPath(path) {
1256
+ const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
1113
1257
  if (match === null) return null;
1114
1258
  const seg = match[1];
1115
1259
  if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
@@ -1117,8 +1261,8 @@ function inferTypeFromPath(path2) {
1117
1261
  }
1118
1262
  return null;
1119
1263
  }
1120
- function extractSlug(path2) {
1121
- const file = basename(path2).replace(/\.md$/u, "");
1264
+ function extractSlug(path) {
1265
+ const file = basename(path).replace(/\.md$/u, "");
1122
1266
  return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
1123
1267
  }
1124
1268
  async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
@@ -1235,7 +1379,7 @@ async function searchEntries(projectRoot, query, filters) {
1235
1379
  }
1236
1380
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
1237
1381
  const itemTags = fm.tags ?? [];
1238
- const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1382
+ const hasAll = filters.tags.every((t) => itemTags.includes(t));
1239
1383
  if (!hasAll) continue;
1240
1384
  }
1241
1385
  if (filters?.created_after !== void 0) {
@@ -1474,10 +1618,15 @@ function registerReview(server, tracker) {
1474
1618
  const requestId = randomUUID3();
1475
1619
  tracker?.enter(requestId);
1476
1620
  try {
1621
+ const gateResult = await awaitFirstReconcileGate();
1622
+ const gateWarn = gateWarning(gateResult);
1477
1623
  const narrowed = FabReviewInputSchema.parse(input);
1478
1624
  const projectRoot = resolveProjectRoot();
1479
1625
  const result = await reviewKnowledge(projectRoot, narrowed);
1480
- const response = result;
1626
+ const response = { ...result };
1627
+ if (gateWarn) {
1628
+ response.warnings = [gateWarn];
1629
+ }
1481
1630
  const payloadLimits = readPayloadLimits(projectRoot);
1482
1631
  const serialized = JSON.stringify(response);
1483
1632
  enforcePayloadLimit3(serialized, payloadLimits);
@@ -1505,62 +1654,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
1505
1654
  import { readFile as readFile4 } from "fs/promises";
1506
1655
  import { homedir as homedir3 } from "os";
1507
1656
  import { join as join4 } from "path";
1508
- var KNOWLEDGE_SECTION_NAMES = [
1509
- "MISSION_STATEMENT",
1510
- "MANDATORY_INJECTION",
1511
- "BUSINESS_LOGIC_CHUNKS",
1512
- "CONTEXT_INFO"
1513
- ];
1514
1657
  var PRIORITY_ORDER = {
1515
1658
  high: 0,
1516
1659
  medium: 1,
1517
1660
  low: 2
1518
1661
  };
1519
- function parseKnowledgeSections(content) {
1520
- const sections = /* @__PURE__ */ new Map();
1521
- const lines = content.split(/\r?\n/u);
1522
- let activeSection;
1523
- let activeSectionDepth = 0;
1524
- let buffer = [];
1525
- const flush = () => {
1526
- if (activeSection === void 0) {
1527
- return;
1528
- }
1529
- const text = buffer.join("\n").trim();
1530
- if (text.length === 0) {
1531
- buffer = [];
1532
- return;
1533
- }
1534
- sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
1535
- buffer = [];
1536
- };
1537
- for (const line of lines) {
1538
- const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
1539
- if (heading !== null) {
1540
- flush();
1541
- activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
1542
- activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
1543
- continue;
1544
- }
1545
- const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
1546
- if (ordinaryHeading !== null) {
1547
- if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
1548
- buffer.push(line);
1549
- continue;
1550
- }
1551
- flush();
1552
- activeSection = void 0;
1553
- activeSectionDepth = 0;
1554
- continue;
1555
- }
1556
- if (activeSection !== void 0) {
1557
- buffer.push(line);
1558
- }
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, "");
1559
1666
  }
1560
- flush();
1561
- return new Map(
1562
- Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
1563
- );
1667
+ return content.slice(match[0].length);
1564
1668
  }
1565
1669
  async function getKnowledgeSections(projectRoot, input) {
1566
1670
  const token = readSelectionToken(input.selection_token);
@@ -1568,28 +1672,14 @@ async function getKnowledgeSections(projectRoot, input) {
1568
1672
  throw new Error("selection_token is missing or expired");
1569
1673
  }
1570
1674
  validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
1571
- const meta = await readAgentsMeta(projectRoot);
1675
+ const { meta } = await loadActiveMeta(projectRoot, { caller: "getKnowledgeSections" });
1572
1676
  const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
1573
1677
  const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
1574
1678
  const diagnostics = [];
1575
1679
  const rules = [];
1576
1680
  for (const rule of selectedRules) {
1577
1681
  const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
1578
- const parsedSections = parseKnowledgeSections(content);
1579
- const sections = {};
1580
- for (const section of input.sections) {
1581
- const sectionContent = parsedSections.get(section);
1582
- sections[section] = sectionContent ?? "";
1583
- if (sectionContent === void 0) {
1584
- diagnostics.push({
1585
- code: "missing_section",
1586
- severity: "warn",
1587
- stable_id: rule.stable_id,
1588
- section,
1589
- message: `Rule ${rule.stable_id} does not define section ${section}.`
1590
- });
1591
- }
1592
- }
1682
+ const body = extractBody(content);
1593
1683
  const description = rule.node.description;
1594
1684
  if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
1595
1685
  diagnostics.push({
@@ -1603,7 +1693,7 @@ async function getKnowledgeSections(projectRoot, input) {
1603
1693
  stable_id: rule.stable_id,
1604
1694
  level: rule.level,
1605
1695
  path: rule.path,
1606
- sections
1696
+ body
1607
1697
  });
1608
1698
  }
1609
1699
  const result = {
@@ -1635,7 +1725,7 @@ async function getKnowledgeSections(projectRoot, input) {
1635
1725
  event_type: "knowledge_sections_fetched",
1636
1726
  selection_token: input.selection_token,
1637
1727
  target_paths: token.target_paths,
1638
- requested_sections: input.sections,
1728
+ requested_sections: [],
1639
1729
  final_stable_ids: result.selected_stable_ids,
1640
1730
  ai_selected_stable_ids: input.ai_selected_stable_ids,
1641
1731
  diagnostics,
@@ -1720,9 +1810,6 @@ function outputLevelOrder(level) {
1720
1810
  return 2;
1721
1811
  }
1722
1812
  }
1723
- function isKnowledgeSectionName(value) {
1724
- return KNOWLEDGE_SECTION_NAMES.includes(value);
1725
- }
1726
1813
  function resolveRuleSourcePath(projectRoot, contentRef) {
1727
1814
  if (contentRef.startsWith("~/.fabric/knowledge/")) {
1728
1815
  const home = process.env.FABRIC_HOME ?? homedir3();
@@ -1739,7 +1826,7 @@ function registerKnowledgeSections(server, tracker) {
1739
1826
  server.registerTool(
1740
1827
  "fab_get_knowledge_sections",
1741
1828
  {
1742
- 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.).",
1743
1830
  inputSchema: knowledgeSectionsInputSchema,
1744
1831
  outputSchema: knowledgeSectionsOutputSchema,
1745
1832
  annotations: knowledgeSectionsAnnotations
@@ -1748,12 +1835,17 @@ function registerKnowledgeSections(server, tracker) {
1748
1835
  const requestId = randomUUID4();
1749
1836
  tracker?.enter(requestId);
1750
1837
  try {
1838
+ const gateResult = await awaitFirstReconcileGate();
1839
+ const gateWarn = gateWarning(gateResult);
1751
1840
  const projectRoot = resolveProjectRoot();
1752
1841
  const syncReport = await ensureKnowledgeFresh(projectRoot);
1753
1842
  const result = await getKnowledgeSections(projectRoot, input);
1754
1843
  const response = {
1755
1844
  ...result,
1756
- warnings: [...syncReport.warnings]
1845
+ warnings: [
1846
+ ...gateWarn ? [gateWarn] : [],
1847
+ ...syncReport.warnings
1848
+ ]
1757
1849
  };
1758
1850
  const payloadLimits = readPayloadLimits(projectRoot);
1759
1851
  const serialized = JSON.stringify(response);
@@ -1779,99 +1871,6 @@ function registerKnowledgeSections(server, tracker) {
1779
1871
  );
1780
1872
  }
1781
1873
 
1782
- // src/services/serve-lock.ts
1783
- import fs from "fs";
1784
- import path from "path";
1785
- import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
1786
- import { IOFabricError } from "@fenglimg/fabric-shared/errors";
1787
- var LOCK_FILENAME = ".serve.lock";
1788
- var t = createTranslator(detectNodeLocale());
1789
- var ServeLockHeldError = class extends IOFabricError {
1790
- code = "SERVE_LOCK_HELD";
1791
- httpStatus = 423;
1792
- };
1793
- function lockPath(projectRoot) {
1794
- return path.join(projectRoot, ".fabric", LOCK_FILENAME);
1795
- }
1796
- function isAlive(pid) {
1797
- try {
1798
- process.kill(pid, 0);
1799
- return true;
1800
- } catch (e) {
1801
- const err = e;
1802
- if (err.code === "ESRCH") return false;
1803
- if (err.code === "EPERM") return true;
1804
- throw e;
1805
- }
1806
- }
1807
- function acquireLock(projectRoot, opts) {
1808
- const p = lockPath(projectRoot);
1809
- if (fs.existsSync(p)) {
1810
- let state = null;
1811
- try {
1812
- state = JSON.parse(fs.readFileSync(p, "utf8"));
1813
- } catch {
1814
- }
1815
- if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
1816
- throw new ServeLockHeldError(
1817
- `serve lock held by live PID ${state.pid}`,
1818
- {
1819
- actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1820
- details: state
1821
- }
1822
- );
1823
- }
1824
- if (state && state.pid && !isAlive(state.pid)) {
1825
- process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
1826
- `);
1827
- }
1828
- }
1829
- fs.mkdirSync(path.dirname(p), { recursive: true });
1830
- fs.writeFileSync(
1831
- p,
1832
- JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
1833
- );
1834
- }
1835
- function releaseLock(projectRoot) {
1836
- const p = lockPath(projectRoot);
1837
- try {
1838
- if (fs.existsSync(p)) {
1839
- const state = JSON.parse(fs.readFileSync(p, "utf8"));
1840
- if (state.pid === process.pid) {
1841
- fs.unlinkSync(p);
1842
- }
1843
- }
1844
- } catch {
1845
- }
1846
- }
1847
- function readLockState(projectRoot) {
1848
- const p = lockPath(projectRoot);
1849
- if (!fs.existsSync(p)) return null;
1850
- try {
1851
- return JSON.parse(fs.readFileSync(p, "utf8"));
1852
- } catch {
1853
- return null;
1854
- }
1855
- }
1856
- function checkLockOrThrow(projectRoot, opts) {
1857
- const state = readLockState(projectRoot);
1858
- if (state === null) return;
1859
- if (state.pid === process.pid) return;
1860
- if (!isAlive(state.pid)) {
1861
- process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
1862
- `);
1863
- return;
1864
- }
1865
- if (opts?.force) return;
1866
- throw new ServeLockHeldError(
1867
- `serve lock held by live PID ${state.pid}`,
1868
- {
1869
- actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1870
- details: state
1871
- }
1872
- );
1873
- }
1874
-
1875
1874
  // src/index.ts
1876
1875
  function writeStderr(message) {
1877
1876
  process.stderr.write(`${message}
@@ -1893,7 +1892,7 @@ function formatPreexistingRootMessage(projectRoot) {
1893
1892
  function createFabricServer(tracker) {
1894
1893
  const server = new McpServer({
1895
1894
  name: "fabric-knowledge-server",
1896
- version: "2.0.0-rc.21"
1895
+ version: "2.0.0-rc.23"
1897
1896
  });
1898
1897
  registerPlanContext(server, tracker);
1899
1898
  registerKnowledgeSections(server, tracker);
@@ -1908,10 +1907,10 @@ function createFabricServer(tracker) {
1908
1907
  },
1909
1908
  async (_uri) => {
1910
1909
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
1911
- const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1910
+ const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
1912
1911
  let text = "";
1913
- if (existsSync4(path2)) {
1914
- text = await readFile5(path2, "utf8");
1912
+ if (existsSync4(path)) {
1913
+ text = await readFile5(path, "utf8");
1915
1914
  }
1916
1915
  return {
1917
1916
  contents: [
@@ -1929,13 +1928,6 @@ function createFabricServer(tracker) {
1929
1928
  async function startStdioServer() {
1930
1929
  const tracker = createInFlightTracker();
1931
1930
  const projectRoot = resolveProjectRoot();
1932
- const syncStart = Date.now();
1933
- const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
1934
- const syncDurationMs = Date.now() - syncStart;
1935
- process.stderr.write(
1936
- `[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
1937
- `
1938
- );
1939
1931
  const rootMsg = formatPreexistingRootMessage(projectRoot);
1940
1932
  if (rootMsg !== null) {
1941
1933
  process.stderr.write(`${rootMsg}
@@ -1944,6 +1936,21 @@ async function startStdioServer() {
1944
1936
  const server = createFabricServer(tracker);
1945
1937
  const transport = new StdioServerTransport();
1946
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);
1947
1954
  const closeServer = async () => {
1948
1955
  await server.close();
1949
1956
  };
@@ -1993,7 +2000,7 @@ function createShutdownHandler(deps) {
1993
2000
  };
1994
2001
  }
1995
2002
  async function startHttpServer(options) {
1996
- const { createFabricHttpApp } = await import("./http-JGWQGUZS.js");
2003
+ const { createFabricHttpApp } = await import("./http-ZBV6YUHD.js");
1997
2004
  const { port, projectRoot, host = "127.0.0.1", authToken } = options;
1998
2005
  const app = createFabricHttpApp({ projectRoot, host, authToken });
1999
2006
  return await new Promise((resolveServer, rejectServer) => {
@@ -2036,6 +2043,7 @@ export {
2036
2043
  createShutdownHandler,
2037
2044
  deriveKnowledgeMetaLayer,
2038
2045
  deriveKnowledgeMetaTopologyType,
2046
+ enrichDescriptions,
2039
2047
  ensureKnowledgeFresh,
2040
2048
  extractKnowledge,
2041
2049
  flushAndSyncEventLedger,