@fenglimg/fabric-server 1.7.0 → 1.8.0-rc.1

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.
@@ -82,29 +82,86 @@ var ContextCache = class {
82
82
  };
83
83
  var contextCache = new ContextCache(5e3);
84
84
 
85
+ // src/meta-reader.ts
86
+ import { readFile } from "fs/promises";
87
+ import { join } from "path";
88
+ import { agentsMetaSchema } from "@fenglimg/fabric-shared";
89
+ import { IOFabricError } from "@fenglimg/fabric-shared/errors";
90
+ import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
91
+ var AgentsMetaFileMissingError = class extends IOFabricError {
92
+ constructor(metaPath, opts) {
93
+ super(`Fabric agents metadata file is missing: ${metaPath}`, {
94
+ actionHint: opts?.actionHint ?? "Run `fab init` to scaffold the .fabric/agents.meta.json file"
95
+ });
96
+ this.metaPath = metaPath;
97
+ }
98
+ metaPath;
99
+ code = "FABRIC_META_MISSING";
100
+ httpStatus = 404;
101
+ };
102
+ var AgentsMetaInvalidError = class extends IOFabricError {
103
+ constructor(metaPath, cause, opts) {
104
+ const detail = cause instanceof Error ? cause.message : String(cause);
105
+ super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`, {
106
+ actionHint: opts?.actionHint ?? "Check the agents.meta.json file for schema errors and regenerate if needed"
107
+ });
108
+ this.metaPath = metaPath;
109
+ }
110
+ metaPath;
111
+ code = "FABRIC_META_INVALID";
112
+ httpStatus = 500;
113
+ };
114
+ function getAgentsMetaPath(projectRoot) {
115
+ return join(projectRoot, ".fabric", "agents.meta.json");
116
+ }
117
+ function resolveProjectRoot() {
118
+ return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
119
+ }
120
+ async function readAgentsMeta(projectRoot) {
121
+ const cached = contextCache.get("meta", projectRoot);
122
+ if (cached !== void 0) {
123
+ return cached;
124
+ }
125
+ const metaPath = getAgentsMetaPath(projectRoot);
126
+ let raw;
127
+ try {
128
+ raw = await readFile(metaPath, "utf8");
129
+ } catch (error) {
130
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
131
+ throw new AgentsMetaFileMissingError(metaPath);
132
+ }
133
+ throw error;
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = agentsMetaSchema.parse(JSON.parse(raw));
138
+ } catch (error) {
139
+ throw new AgentsMetaInvalidError(metaPath, error);
140
+ }
141
+ contextCache.set("meta", projectRoot, parsed);
142
+ return parsed;
143
+ }
144
+
85
145
  // src/services/_shared.ts
86
- import { dirname, join, resolve, sep } from "path";
146
+ import { dirname, join as join2, resolve, sep } from "path";
87
147
  import { createHash } from "crypto";
88
- import { mkdir, rename, writeFile } from "fs/promises";
148
+ import { mkdir } from "fs/promises";
149
+ import { PathEscapeError } from "@fenglimg/fabric-shared/errors";
150
+ import { atomicWriteText, atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
89
151
  var FABRIC_DIR = ".fabric";
90
152
  var LEDGER_FILE = ".intent-ledger.jsonl";
91
153
  var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
92
154
  var LEGACY_LEDGER_PATH = LEDGER_FILE;
93
155
  var EVENT_LEDGER_FILE = "events.jsonl";
94
156
  var EVENT_LEDGER_PATH = `${FABRIC_DIR}/${EVENT_LEDGER_FILE}`;
95
- async function atomicWriteText(path, content) {
96
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
97
- await writeFile(tempPath, content, "utf8");
98
- await rename(tempPath, path);
99
- }
100
157
  function getLedgerPath(projectRoot) {
101
- return join(projectRoot, LEDGER_PATH);
158
+ return join2(projectRoot, LEDGER_PATH);
102
159
  }
103
160
  function getLegacyLedgerPath(projectRoot) {
104
- return join(projectRoot, LEGACY_LEDGER_PATH);
161
+ return join2(projectRoot, LEGACY_LEDGER_PATH);
105
162
  }
106
163
  function getEventLedgerPath(projectRoot) {
107
- return join(projectRoot, EVENT_LEDGER_PATH);
164
+ return join2(projectRoot, EVENT_LEDGER_PATH);
108
165
  }
109
166
  async function ensureParentDirectory(path) {
110
167
  await mkdir(dirname(path), { recursive: true });
@@ -118,10 +175,13 @@ function isNodeError(error) {
118
175
 
119
176
  // src/services/event-ledger.ts
120
177
  import { randomUUID } from "crypto";
121
- import { appendFile, readFile } from "fs/promises";
178
+ import { existsSync, fsyncSync, openSync, closeSync } from "fs";
179
+ import { readFile as readFile2, truncate, writeFile } from "fs/promises";
122
180
  import {
123
181
  eventLedgerEventSchema
124
182
  } from "@fenglimg/fabric-shared";
183
+ import { createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
184
+ var ledgerQueue = createLedgerWriteQueue();
125
185
  async function appendEventLedgerEvent(projectRoot, event) {
126
186
  const eventPath = getEventLedgerPath(projectRoot);
127
187
  const nextEvent = eventLedgerEventSchema.parse({
@@ -132,22 +192,60 @@ async function appendEventLedgerEvent(projectRoot, event) {
132
192
  schema_version: 1
133
193
  });
134
194
  await ensureParentDirectory(eventPath);
135
- await appendFile(eventPath, `${JSON.stringify(nextEvent)}
136
- `, "utf8");
195
+ await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
137
196
  return nextEvent;
138
197
  }
139
198
  async function readEventLedger(projectRoot, options = {}) {
140
199
  const eventPath = getEventLedgerPath(projectRoot);
141
200
  let raw;
142
201
  try {
143
- raw = await readFile(eventPath, "utf8");
202
+ raw = await readFile2(eventPath, "utf8");
144
203
  } catch (error) {
145
204
  if (isNodeError2(error) && error.code === "ENOENT") {
146
- return [];
205
+ return { events: [], warnings: [] };
147
206
  }
148
207
  throw error;
149
208
  }
150
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
209
+ const warnings = [];
210
+ const lines = raw.split(/\r?\n/);
211
+ const hasTrailingNewline = raw.endsWith("\n");
212
+ let partialLine;
213
+ if (!hasTrailingNewline && lines.length > 0) {
214
+ partialLine = lines.pop();
215
+ }
216
+ if (partialLine !== void 0 && partialLine.trim().length > 0) {
217
+ const fullContentBeforePartial = raw.slice(0, raw.length - partialLine.length);
218
+ const byteOffset = Buffer.byteLength(fullContentBeforePartial, "utf8");
219
+ const byteLength = Buffer.byteLength(partialLine, "utf8");
220
+ warnings.push({
221
+ kind: "partial_write_at_tail",
222
+ byte_offset: byteOffset,
223
+ byte_length: byteLength,
224
+ snippet_first_120: partialLine.slice(0, 120)
225
+ });
226
+ }
227
+ const events = lines.map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
228
+ return { events, warnings };
229
+ }
230
+ async function truncateLedgerToLastNewline(path) {
231
+ const raw = await readFile2(path);
232
+ const content = raw.toString("utf8");
233
+ if (content.endsWith("\n") || content.length === 0) {
234
+ return { truncated_bytes: 0, corrupted_path: "" };
235
+ }
236
+ const lastNewlineIndex = content.lastIndexOf("\n");
237
+ if (lastNewlineIndex === -1) {
238
+ const corruptedPath2 = `${path}.corrupted.${Date.now()}`;
239
+ await writeFile(corruptedPath2, raw);
240
+ await truncate(path, 0);
241
+ return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
242
+ }
243
+ const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
244
+ const corruptedBytes = raw.slice(keepByteLength);
245
+ const corruptedPath = `${path}.corrupted.${Date.now()}`;
246
+ await writeFile(corruptedPath, corruptedBytes);
247
+ await truncate(path, keepByteLength);
248
+ return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
151
249
  }
152
250
  function parseEventLedgerLine(line, index) {
153
251
  try {
@@ -170,24 +268,35 @@ function createDerivedId(index, line) {
170
268
  function isNodeError2(error) {
171
269
  return error instanceof Error;
172
270
  }
271
+ function flushAndSyncEventLedger(projectRoot) {
272
+ const ledgerPath = getEventLedgerPath(projectRoot);
273
+ if (!existsSync(ledgerPath)) return;
274
+ const fd = openSync(ledgerPath, "r+");
275
+ try {
276
+ fsyncSync(fd);
277
+ } finally {
278
+ closeSync(fd);
279
+ }
280
+ }
173
281
 
174
282
  // src/services/rule-meta-builder.ts
175
- import { readdir, readFile as readFile2 } from "fs/promises";
176
- import { existsSync, statSync } from "fs";
177
- import { isAbsolute, join as join2, relative, resolve as resolve2, sep as sep2 } from "path";
283
+ import { readdir, readFile as readFile3 } from "fs/promises";
284
+ import { existsSync as existsSync2, statSync } from "fs";
285
+ import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
178
286
  import {
179
287
  RULE_TEST_INDEX_SCHEMA_VERSION,
180
- agentsMetaSchema,
288
+ agentsMetaSchema as agentsMetaSchema3,
181
289
  deriveAgentsMetaLayer,
182
290
  deriveAgentsMetaStableId,
183
291
  deriveAgentsMetaTopologyType,
184
292
  ruleTestIndexSchema
185
293
  } from "@fenglimg/fabric-shared";
294
+ import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
186
295
  async function buildRuleMeta(projectRootInput) {
187
296
  const projectRoot = normalizeProjectRoot(projectRootInput);
188
297
  assertExistingDirectory(projectRoot);
189
- const metaPath = join2(projectRoot, ".fabric", "agents.meta.json");
190
- const ruleTestIndexPath = join2(projectRoot, ".fabric", "rule-test.index.json");
298
+ const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
299
+ const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
191
300
  const existingMeta = await readExistingMeta(metaPath);
192
301
  const existingRuleTestIndex = await readExistingRuleTestIndex(ruleTestIndexPath);
193
302
  const meta = await computeRulesBasedAgentsMeta(projectRoot, existingMeta);
@@ -200,17 +309,17 @@ async function buildRuleMeta(projectRootInput) {
200
309
  }
201
310
  async function writeRuleMeta(projectRootInput, options) {
202
311
  const projectRoot = normalizeProjectRoot(projectRootInput);
203
- const metaPath = join2(projectRoot, ".fabric", "agents.meta.json");
204
- const ruleTestIndexPath = join2(projectRoot, ".fabric", "rule-test.index.json");
312
+ const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
313
+ const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
205
314
  const existingMeta = await readExistingMeta(metaPath);
206
315
  const result = await buildRuleMeta(projectRoot);
207
316
  if (!result.changed) {
208
317
  return result;
209
318
  }
210
319
  await ensureParentDirectory(metaPath);
211
- await atomicWriteText(metaPath, `${JSON.stringify(result.meta, null, 2)}
320
+ await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
212
321
  `);
213
- await atomicWriteText(ruleTestIndexPath, `${JSON.stringify(result.ruleTestIndex, null, 2)}
322
+ await atomicWriteText2(ruleTestIndexPath, `${JSON.stringify(result.ruleTestIndex, null, 2)}
214
323
  `);
215
324
  if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
216
325
  await recordBaselineSynced(projectRoot, {
@@ -227,7 +336,7 @@ async function writeRuleMeta(projectRootInput, options) {
227
336
  async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
228
337
  const projectRoot = normalizeProjectRoot(projectRootInput);
229
338
  assertExistingDirectory(projectRoot);
230
- const previousMeta = existingMeta ?? await readExistingMeta(join2(projectRoot, ".fabric", "agents.meta.json"));
339
+ const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
231
340
  const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
232
341
  const ruleFiles = await findFabricRuleFiles(projectRoot);
233
342
  const nodes = {};
@@ -236,7 +345,7 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
236
345
  nodes.L0 = bootstrapNode;
237
346
  }
238
347
  for (const contentRef of ruleFiles) {
239
- const source = await readFile2(join2(projectRoot, contentRef), "utf8");
348
+ const source = await readFile3(join3(projectRoot, contentRef), "utf8");
240
349
  const existing = existingByContentRef.get(contentRef);
241
350
  const id = deriveNodeId(contentRef);
242
351
  const hash = sha256(source);
@@ -320,14 +429,14 @@ function normalizeProjectRoot(projectRoot) {
320
429
  return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
321
430
  }
322
431
  function assertExistingDirectory(projectRoot) {
323
- if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
432
+ if (!existsSync2(projectRoot) || !statSync(projectRoot).isDirectory()) {
324
433
  throw new Error(`Target directory does not exist: ${projectRoot}`);
325
434
  }
326
435
  }
327
436
  async function readExistingMeta(metaPath) {
328
437
  let raw;
329
438
  try {
330
- raw = await readFile2(metaPath, "utf8");
439
+ raw = await readFile3(metaPath, "utf8");
331
440
  } catch (error) {
332
441
  if (isNodeError3(error) && error.code === "ENOENT") {
333
442
  return void 0;
@@ -335,7 +444,7 @@ async function readExistingMeta(metaPath) {
335
444
  throw error;
336
445
  }
337
446
  try {
338
- return agentsMetaSchema.parse(JSON.parse(raw));
447
+ return agentsMetaSchema3.parse(JSON.parse(raw));
339
448
  } catch {
340
449
  return void 0;
341
450
  }
@@ -343,7 +452,7 @@ async function readExistingMeta(metaPath) {
343
452
  async function readExistingRuleTestIndex(indexPath) {
344
453
  let raw;
345
454
  try {
346
- raw = await readFile2(indexPath, "utf8");
455
+ raw = await readFile3(indexPath, "utf8");
347
456
  } catch (error) {
348
457
  if (isNodeError3(error) && error.code === "ENOENT") {
349
458
  return void 0;
@@ -357,8 +466,8 @@ async function readExistingRuleTestIndex(indexPath) {
357
466
  }
358
467
  }
359
468
  async function findFabricRuleFiles(projectRoot) {
360
- const rulesRoot = join2(projectRoot, ".fabric", "rules");
361
- if (!existsSync(rulesRoot) || !statSync(rulesRoot).isDirectory()) {
469
+ const rulesRoot = join3(projectRoot, ".fabric", "rules");
470
+ if (!existsSync2(rulesRoot) || !statSync(rulesRoot).isDirectory()) {
362
471
  return [];
363
472
  }
364
473
  const files = [];
@@ -369,7 +478,7 @@ async function findFabricRuleFiles(projectRoot) {
369
478
  continue;
370
479
  }
371
480
  for (const entry of await readdir(current, { withFileTypes: true })) {
372
- const absolutePath = join2(current, entry.name);
481
+ const absolutePath = join3(current, entry.name);
373
482
  const relativePath = toPosixPath(relative(projectRoot, absolutePath));
374
483
  if (entry.isDirectory()) {
375
484
  stack.push(absolutePath);
@@ -385,7 +494,7 @@ async function findFabricVerifyAnnotations(projectRoot) {
385
494
  const annotations = [];
386
495
  const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
387
496
  for (const testFile of files) {
388
- const source = await readFile2(join2(projectRoot, testFile), "utf8");
497
+ const source = await readFile3(join3(projectRoot, testFile), "utf8");
389
498
  const testHash = sha256(source);
390
499
  const lines = source.split(/\r?\n/u);
391
500
  for (const [index, line] of lines.entries()) {
@@ -413,7 +522,7 @@ async function findTestFiles(projectRoot) {
413
522
  continue;
414
523
  }
415
524
  for (const entry of await readdir(current, { withFileTypes: true })) {
416
- const absolutePath = join2(current, entry.name);
525
+ const absolutePath = join3(current, entry.name);
417
526
  const relativePath = toPosixPath(relative(projectRoot, absolutePath));
418
527
  const [rootSegment] = relativePath.split("/");
419
528
  if (entry.isDirectory()) {
@@ -513,11 +622,11 @@ function createDefaultNodeMeta(contentRef) {
513
622
  }
514
623
  async function createBootstrapNode(projectRoot, existing) {
515
624
  const contentRef = ".fabric/bootstrap/README.md";
516
- const bootstrapPath = join2(projectRoot, contentRef);
517
- if (!existsSync(bootstrapPath)) {
625
+ const bootstrapPath = join3(projectRoot, contentRef);
626
+ if (!existsSync2(bootstrapPath)) {
518
627
  return void 0;
519
628
  }
520
- const hash = sha256(await readFile2(bootstrapPath, "utf8"));
629
+ const hash = sha256(await readFile3(bootstrapPath, "utf8"));
521
630
  const identity = {
522
631
  stableId: existing?.stable_id ?? deriveAgentsMetaStableId(contentRef),
523
632
  identitySource: existing?.identity_source ?? "derived"
@@ -722,80 +831,335 @@ function isNodeError3(error) {
722
831
  return error instanceof Error;
723
832
  }
724
833
 
725
- // src/services/doctor.ts
726
- import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
727
- import { access, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
728
- import { constants } from "fs";
729
- import { isAbsolute as isAbsolute3, join as join7, posix as posix2, resolve as resolve4 } from "path";
730
- import {
731
- agentsMetaSchema as agentsMetaSchema4,
732
- forensicReportSchema,
733
- ruleTestIndexSchema as ruleTestIndexSchema2
734
- } from "@fenglimg/fabric-shared";
735
- import { detectFramework } from "@fenglimg/fabric-shared/node";
736
-
737
- // src/services/rule-sections.ts
738
- import { readFile as readFile5 } from "fs/promises";
739
- import { join as join6 } from "path";
740
-
741
- // src/meta-reader.ts
742
- import { readFile as readFile3 } from "fs/promises";
743
- import { join as join3 } from "path";
744
- import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
745
- import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
746
- var AgentsMetaFileMissingError = class extends Error {
747
- constructor(metaPath) {
748
- super(`Fabric agents metadata file is missing: ${metaPath}`);
749
- this.metaPath = metaPath;
750
- this.name = "AgentsMetaFileMissingError";
834
+ // src/services/rule-sync.ts
835
+ import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
836
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
837
+ import { join as join4, relative as relative2, sep as sep3 } from "path";
838
+ import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
839
+ var lastSyncState = /* @__PURE__ */ new Map();
840
+ var freshSyncCooldown = /* @__PURE__ */ new Map();
841
+ var SYNC_COOLDOWN_MS = 500;
842
+ function invalidateRuleSyncCooldown(projectRoot) {
843
+ freshSyncCooldown.delete(projectRoot);
844
+ }
845
+ async function readMetaEntries(projectRoot) {
846
+ const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
847
+ const map = /* @__PURE__ */ new Map();
848
+ let raw;
849
+ try {
850
+ raw = await readFile4(metaPath, "utf8");
851
+ } catch {
852
+ return map;
751
853
  }
752
- metaPath;
753
- code = "FABRIC_META_MISSING";
754
- };
755
- var AgentsMetaInvalidError = class extends Error {
756
- constructor(metaPath, cause) {
757
- const detail = cause instanceof Error ? cause.message : String(cause);
758
- super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`);
759
- this.metaPath = metaPath;
760
- this.name = "AgentsMetaInvalidError";
854
+ let parsed;
855
+ try {
856
+ parsed = JSON.parse(raw);
857
+ } catch {
858
+ return map;
859
+ }
860
+ for (const node of Object.values(parsed.nodes ?? {})) {
861
+ const path = node.content_ref ?? node.file;
862
+ const stable_id = node.stable_id;
863
+ const content_hash = node.hash;
864
+ if (path !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
865
+ map.set(path, { stable_id, path, content_hash });
866
+ }
761
867
  }
762
- metaPath;
763
- code = "FABRIC_META_INVALID";
764
- };
765
- function getAgentsMetaPath(projectRoot) {
766
- return join3(projectRoot, ".fabric", "agents.meta.json");
868
+ return map;
767
869
  }
768
- function resolveProjectRoot() {
769
- return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
870
+ async function findRuleFiles(projectRoot) {
871
+ const rulesRoot = join4(projectRoot, ".fabric", "rules");
872
+ if (!existsSync3(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
873
+ return [];
874
+ }
875
+ const files = [];
876
+ const stack = [rulesRoot];
877
+ while (stack.length > 0) {
878
+ const current = stack.pop();
879
+ if (current === void 0) {
880
+ continue;
881
+ }
882
+ for (const entry of await readdir2(current, { withFileTypes: true })) {
883
+ const absolutePath = join4(current, entry.name);
884
+ if (entry.isDirectory()) {
885
+ stack.push(absolutePath);
886
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
887
+ const rel = toPosixPath2(relative2(projectRoot, absolutePath));
888
+ files.push(rel);
889
+ }
890
+ }
891
+ }
892
+ return files.sort();
770
893
  }
771
- async function readAgentsMeta(projectRoot) {
772
- const cached = contextCache.get("meta", projectRoot);
773
- if (cached !== void 0) {
774
- return cached;
894
+ function toPosixPath2(p) {
895
+ return p.split(sep3).join("/");
896
+ }
897
+ function validateFrontmatter(source, filePath, throwOnInvalid) {
898
+ if (!source.startsWith("---")) {
899
+ return null;
775
900
  }
776
- const metaPath = getAgentsMetaPath(projectRoot);
777
- let raw;
901
+ const endIdx = source.indexOf("\n---", 3);
902
+ if (endIdx === -1) {
903
+ const msg = `Unterminated YAML frontmatter in ${filePath}`;
904
+ if (throwOnInvalid) {
905
+ throw new RuleValidationError(msg, {
906
+ actionHint: "Run `fab doctor --fix` to repair frontmatter",
907
+ fixable: true,
908
+ details: { file: filePath }
909
+ });
910
+ }
911
+ return {
912
+ code: "rule_frontmatter_invalid",
913
+ file: filePath,
914
+ action_hint: "Run `fab doctor --fix` to repair frontmatter"
915
+ };
916
+ }
917
+ const frontmatter = source.slice(3, endIdx).trim();
918
+ for (const line of frontmatter.split("\n")) {
919
+ const trimmed = line.trim();
920
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
921
+ continue;
922
+ }
923
+ if (!trimmed.includes(":") && !trimmed.startsWith("-")) {
924
+ const msg = `Invalid YAML frontmatter line "${trimmed}" in ${filePath}`;
925
+ if (throwOnInvalid) {
926
+ throw new RuleValidationError(msg, {
927
+ actionHint: "Run `fab doctor --fix` to repair frontmatter",
928
+ fixable: true,
929
+ details: { file: filePath, line: trimmed }
930
+ });
931
+ }
932
+ return {
933
+ code: "rule_frontmatter_invalid",
934
+ file: filePath,
935
+ action_hint: "Run `fab doctor --fix` to repair frontmatter"
936
+ };
937
+ }
938
+ }
939
+ return null;
940
+ }
941
+ async function processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter) {
942
+ const absPath = join4(projectRoot, relPath);
778
943
  try {
779
- raw = await readFile3(metaPath, "utf8");
780
- } catch (error) {
781
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
782
- throw new AgentsMetaFileMissingError(metaPath);
944
+ await stat(absPath);
945
+ } catch {
946
+ if (metaEntry !== void 0) {
947
+ return {
948
+ event: {
949
+ type: "rule_removed",
950
+ stable_id: metaEntry.stable_id,
951
+ path: relPath,
952
+ prev_hash: metaEntry.content_hash,
953
+ new_hash: null,
954
+ changed_fields: ["content"],
955
+ source
956
+ },
957
+ warning: null
958
+ };
783
959
  }
784
- throw error;
960
+ return { event: null, warning: null };
785
961
  }
786
- let parsed;
962
+ let content;
787
963
  try {
788
- parsed = agentsMetaSchema2.parse(JSON.parse(raw));
789
- } catch (error) {
790
- throw new AgentsMetaInvalidError(metaPath, error);
964
+ content = await readFile4(absPath, "utf8");
965
+ } catch {
966
+ return { event: null, warning: null };
967
+ }
968
+ const newHash = sha256(content);
969
+ const now = Date.now();
970
+ const debounce = lastSyncState.get(absPath);
971
+ if (debounce !== void 0 && newHash === debounce.hash && now - debounce.ts < 500) {
972
+ return { event: null, warning: null };
973
+ }
974
+ if (metaEntry !== void 0 && newHash === metaEntry.content_hash) {
975
+ lastSyncState.set(absPath, { ts: now, hash: newHash });
976
+ return { event: null, warning: null };
977
+ }
978
+ const warning = validateFrontmatter(content, relPath, throwOnInvalidFrontmatter);
979
+ if (warning !== null) {
980
+ lastSyncState.set(absPath, { ts: now, hash: newHash });
981
+ return { event: null, warning };
982
+ }
983
+ const prevHash = metaEntry?.content_hash ?? debounce?.hash ?? null;
984
+ const stableId = metaEntry?.stable_id ?? relPath;
985
+ const eventType = metaEntry === void 0 ? "rule_added" : "rule_content_changed";
986
+ lastSyncState.set(absPath, { ts: now, hash: newHash });
987
+ return {
988
+ event: {
989
+ type: eventType,
990
+ stable_id: stableId,
991
+ path: relPath,
992
+ prev_hash: prevHash,
993
+ new_hash: newHash,
994
+ changed_fields: ["content"],
995
+ source
996
+ },
997
+ warning: null
998
+ };
999
+ }
1000
+ async function appendRuleSyncEvents(projectRoot, events) {
1001
+ if (events.length === 0) {
1002
+ return;
1003
+ }
1004
+ const driftedIds = events.map((e) => e.stable_id);
1005
+ const missingFiles = events.filter((e) => e.type === "rule_removed").map((e) => e.path);
1006
+ const staleFiles = events.filter((e) => e.type !== "rule_removed").map((e) => e.path);
1007
+ if (missingFiles.length > 0 || staleFiles.length > 0) {
1008
+ await appendEventLedgerEvent(projectRoot, {
1009
+ event_type: "rule_drift_detected",
1010
+ drifted_stable_ids: driftedIds,
1011
+ missing_files: missingFiles,
1012
+ stale_files: staleFiles
1013
+ });
791
1014
  }
792
- contextCache.set("meta", projectRoot, parsed);
793
- return parsed;
794
1015
  }
1016
+ async function ensureRulesFresh(projectRoot, opts) {
1017
+ const mode = opts?.mode ?? "incremental";
1018
+ const cooldownExpiry = freshSyncCooldown.get(projectRoot);
1019
+ if (cooldownExpiry !== void 0 && Date.now() < cooldownExpiry && mode !== "full") {
1020
+ return { status: "fresh", events: [], warnings: [] };
1021
+ }
1022
+ const throwOnInvalidFrontmatter = opts?.throwOnInvalidFrontmatter ?? false;
1023
+ const source = "ensureRulesFresh";
1024
+ const events = [];
1025
+ const warnings = [];
1026
+ const metaEntries = await readMetaEntries(projectRoot);
1027
+ const ruleFiles = await findRuleFiles(projectRoot);
1028
+ const filesToCheck = ruleFiles;
1029
+ for (const relPath of filesToCheck) {
1030
+ const metaEntry = metaEntries.get(relPath);
1031
+ const result = await processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter);
1032
+ if (result.event !== null) {
1033
+ events.push(result.event);
1034
+ }
1035
+ if (result.warning !== null) {
1036
+ warnings.push(result.warning);
1037
+ }
1038
+ }
1039
+ for (const [relPath, entry] of metaEntries) {
1040
+ if (!ruleFiles.includes(relPath)) {
1041
+ const absPath = join4(projectRoot, relPath);
1042
+ if (!existsSync3(absPath)) {
1043
+ events.push({
1044
+ type: "rule_removed",
1045
+ stable_id: entry.stable_id,
1046
+ path: relPath,
1047
+ prev_hash: entry.content_hash,
1048
+ new_hash: null,
1049
+ changed_fields: ["content"],
1050
+ source
1051
+ });
1052
+ }
1053
+ }
1054
+ }
1055
+ if (events.length === 0 && warnings.length === 0) {
1056
+ freshSyncCooldown.set(projectRoot, Date.now() + SYNC_COOLDOWN_MS);
1057
+ return { status: "fresh", events: [], warnings: [] };
1058
+ }
1059
+ if (events.length > 0) {
1060
+ await appendRuleSyncEvents(projectRoot, events);
1061
+ contextCache.invalidate("file_watch", projectRoot);
1062
+ }
1063
+ freshSyncCooldown.delete(projectRoot);
1064
+ const status = warnings.length > 0 ? "errors" : "reconciled";
1065
+ return {
1066
+ status,
1067
+ events,
1068
+ warnings,
1069
+ reconciled_files: events.map((e) => e.path)
1070
+ };
1071
+ }
1072
+ async function reconcileRules(projectRoot, opts) {
1073
+ freshSyncCooldown.delete(projectRoot);
1074
+ const trigger = opts?.trigger;
1075
+ const startTime = Date.now();
1076
+ const source = "reconcileRules";
1077
+ const events = [];
1078
+ const warnings = [];
1079
+ const metaEntries = await readMetaEntries(projectRoot);
1080
+ const ruleFiles = await findRuleFiles(projectRoot);
1081
+ for (const relPath of ruleFiles) {
1082
+ const metaEntry = metaEntries.get(relPath);
1083
+ const result = await processSingleFile(projectRoot, relPath, metaEntry, source, false);
1084
+ if (result.event !== null) {
1085
+ events.push(result.event);
1086
+ }
1087
+ if (result.warning !== null) {
1088
+ warnings.push(result.warning);
1089
+ }
1090
+ }
1091
+ for (const [relPath, entry] of metaEntries) {
1092
+ if (!ruleFiles.includes(relPath)) {
1093
+ const absPath = join4(projectRoot, relPath);
1094
+ if (!existsSync3(absPath)) {
1095
+ events.push({
1096
+ type: "rule_removed",
1097
+ stable_id: entry.stable_id,
1098
+ path: relPath,
1099
+ prev_hash: entry.content_hash,
1100
+ new_hash: null,
1101
+ changed_fields: ["content"],
1102
+ source
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ if (events.length > 0) {
1108
+ await writeRuleMeta(projectRoot, { source: "sync_meta" });
1109
+ await appendRuleSyncEvents(projectRoot, events);
1110
+ contextCache.invalidate("file_watch", projectRoot);
1111
+ }
1112
+ const duration_ms = Date.now() - startTime;
1113
+ const reconciledFiles = events.map((e) => e.path);
1114
+ if (trigger !== void 0 && events.length > 0) {
1115
+ if (trigger === "startup") {
1116
+ await appendEventLedgerEvent(projectRoot, {
1117
+ event_type: "meta_reconciled_on_startup",
1118
+ reconciled_files: reconciledFiles,
1119
+ duration_ms,
1120
+ source: "reconcileRules"
1121
+ });
1122
+ } else {
1123
+ await appendEventLedgerEvent(projectRoot, {
1124
+ event_type: "meta_reconciled",
1125
+ reconciled_files: reconciledFiles,
1126
+ duration_ms,
1127
+ trigger,
1128
+ source: "reconcileRules"
1129
+ });
1130
+ }
1131
+ }
1132
+ if (events.length === 0 && warnings.length === 0) {
1133
+ return { status: "fresh", events: [], warnings: [] };
1134
+ }
1135
+ const status = warnings.length > 0 ? "errors" : "reconciled";
1136
+ return {
1137
+ status,
1138
+ events,
1139
+ warnings,
1140
+ reconciled_files: reconciledFiles
1141
+ };
1142
+ }
1143
+
1144
+ // src/services/doctor.ts
1145
+ import { existsSync as existsSync4, mkdirSync, readdirSync, readFileSync, rmdirSync, renameSync, statSync as statSync3 } from "fs";
1146
+ import { access, readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
1147
+ import { constants } from "fs";
1148
+ import { isAbsolute as isAbsolute3, join as join8, posix as posix2, resolve as resolve4 } from "path";
1149
+ import {
1150
+ agentsMetaSchema as agentsMetaSchema4,
1151
+ forensicReportSchema,
1152
+ ruleTestIndexSchema as ruleTestIndexSchema2
1153
+ } from "@fenglimg/fabric-shared";
1154
+ import { detectFramework } from "@fenglimg/fabric-shared/node";
1155
+
1156
+ // src/services/rule-sections.ts
1157
+ import { readFile as readFile6 } from "fs/promises";
1158
+ import { join as join7 } from "path";
795
1159
 
796
1160
  // src/services/audit-log.ts
797
- import { open, stat } from "fs/promises";
798
- import { isAbsolute as isAbsolute2, join as join4, posix, relative as relative2, resolve as resolve3 } from "path";
1161
+ import { open, stat as stat2 } from "fs/promises";
1162
+ import { isAbsolute as isAbsolute2, join as join5, posix, relative as relative3, resolve as resolve3 } from "path";
799
1163
  var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
800
1164
  var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
801
1165
  async function appendGetRulesAuditEvent(projectRoot, input) {
@@ -842,7 +1206,7 @@ async function appendRuleSelectionAuditEvent(projectRoot, input) {
842
1206
  function normalizeAuditPath(projectRoot, value) {
843
1207
  const normalizedProjectRoot = resolve3(projectRoot);
844
1208
  const candidate = isAbsolute2(value) ? resolve3(value) : resolve3(normalizedProjectRoot, value);
845
- const relativePath = relative2(normalizedProjectRoot, candidate);
1209
+ const relativePath = relative3(normalizedProjectRoot, candidate);
846
1210
  if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
847
1211
  return posix.normalize(relativePath.split("\\").join("/"));
848
1212
  }
@@ -899,8 +1263,8 @@ async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata =
899
1263
  }
900
1264
 
901
1265
  // src/services/get-rules.ts
902
- import { readFile as readFile4 } from "fs/promises";
903
- import { join as join5 } from "path";
1266
+ import { readFile as readFile5 } from "fs/promises";
1267
+ import { join as join6 } from "path";
904
1268
  import { minimatch } from "minimatch";
905
1269
  var PRIORITY_ORDER = {
906
1270
  high: 0,
@@ -939,7 +1303,7 @@ async function loadGetRulesContext(projectRoot) {
939
1303
  return cached;
940
1304
  }
941
1305
  const meta = await readAgentsMeta(projectRoot);
942
- const l0Content = await readFile4(join5(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
1306
+ const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
943
1307
  const context = {
944
1308
  meta,
945
1309
  l0Content,
@@ -1078,7 +1442,7 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
1078
1442
  if (cached !== void 0) {
1079
1443
  return await cached;
1080
1444
  }
1081
- const pending = readFile4(join5(projectRoot, file), "utf8");
1445
+ const pending = readFile5(join6(projectRoot, file), "utf8");
1082
1446
  fileContentCache.set(file, pending);
1083
1447
  return await pending;
1084
1448
  }
@@ -1376,7 +1740,7 @@ async function getRuleSections(projectRoot, input) {
1376
1740
  const diagnostics = [];
1377
1741
  const rules = [];
1378
1742
  for (const rule of selectedRules) {
1379
- const content = await readFile5(join6(projectRoot, rule.path), "utf8");
1743
+ const content = await readFile6(join7(projectRoot, rule.path), "utf8");
1380
1744
  const parsedSections = parseRuleSections(content);
1381
1745
  const sections = {};
1382
1746
  for (const section of input.sections) {
@@ -1498,6 +1862,9 @@ function pickSelectionReasons(selectedStableIds, reasons) {
1498
1862
  }
1499
1863
 
1500
1864
  // src/services/doctor.ts
1865
+ import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1866
+ import { buildBootstrapContent, FABRIC_BOOTSTRAP_PATH } from "@fenglimg/fabric-shared/node/bootstrap-guide";
1867
+ var LEGACY_CLIENT_PATH_KEYS = ["windsurf", "rooCode", "geminiCLI"];
1501
1868
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1502
1869
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1503
1870
  ".fabric",
@@ -1539,8 +1906,15 @@ async function runDoctorReport(target) {
1539
1906
  inspectRuleSections(projectRoot),
1540
1907
  inspectRuleTestIndex(projectRoot)
1541
1908
  ]);
1542
- const taxonomyExists = existsSync2(join7(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
1543
- const bootstrapExists = existsSync2(join7(projectRoot, ".fabric", "bootstrap", "README.md"));
1909
+ const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1910
+ const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1911
+ const rulesDirUnindexed = inspectRulesDirUnindexed(projectRoot, meta);
1912
+ const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1913
+ const claudeSkillLegacyPath = inspectClaudeSkillLegacyPath(projectRoot);
1914
+ const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1915
+ const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
1916
+ const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
1917
+ const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
1544
1918
  const checks = [
1545
1919
  createBootstrapCheck(bootstrapExists),
1546
1920
  createTaxonomyCheck(taxonomyExists),
@@ -1550,17 +1924,27 @@ async function runDoctorReport(target) {
1550
1924
  createRuleContentRefCheck(meta),
1551
1925
  createRuleSectionsCheck(ruleSections),
1552
1926
  createRuleTestIndexCheck(ruleTestIndex),
1553
- createEventLedgerCheck(eventLedger)
1927
+ createEventLedgerCheck(eventLedger),
1928
+ createEventLedgerPartialWriteCheck(eventLedger),
1929
+ createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1930
+ createMetaManuallyDivergedCheck(metaManuallyDiverged),
1931
+ createRulesDirUnindexedCheck(rulesDirUnindexed),
1932
+ createStableIdCollisionCheck(stableIdCollision),
1933
+ createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
1934
+ createPreexistingRootFilesCheck(preexistingRootFiles),
1935
+ createLegacyClientPathCheck(legacyClientPaths)
1554
1936
  ];
1555
1937
  const fixableErrors = collectIssues(checks, "fixable_error");
1556
1938
  const manualErrors = collectIssues(checks, "manual_error");
1557
1939
  const warnings = collectIssues(checks, "warning");
1940
+ const infos = collectIssues(checks, "info");
1558
1941
  return {
1559
1942
  status: reduceStatus(checks.map((check) => check.status)),
1560
1943
  checks,
1561
1944
  fixable_errors: fixableErrors,
1562
1945
  manual_errors: manualErrors,
1563
1946
  warnings,
1947
+ infos,
1564
1948
  summary: {
1565
1949
  target: projectRoot,
1566
1950
  framework: {
@@ -1576,8 +1960,9 @@ async function runDoctorReport(target) {
1576
1960
  fixableErrorCount: fixableErrors.length,
1577
1961
  manualErrorCount: manualErrors.length,
1578
1962
  warningCount: warnings.length,
1963
+ infoCount: infos.length,
1579
1964
  targetFiles: Object.fromEntries(
1580
- TARGET_FILE_PATHS.map((path) => [path, existsSync2(join7(projectRoot, path))])
1965
+ TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
1581
1966
  )
1582
1967
  }
1583
1968
  };
@@ -1595,16 +1980,53 @@ async function runDoctorFix(target) {
1595
1980
  fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
1596
1981
  }
1597
1982
  if (before.fixable_errors.some(
1598
- (issue) => ["agents_meta_missing", "agents_meta_stale", "rule_test_index_missing", "rule_test_index_stale"].includes(issue.code)
1983
+ (issue) => [
1984
+ "agents_meta_missing",
1985
+ "agents_meta_stale",
1986
+ "rule_test_index_missing",
1987
+ "rule_test_index_stale",
1988
+ "content_ref_missing",
1989
+ "rules_dir_unindexed"
1990
+ ].includes(issue.code)
1599
1991
  )) {
1600
- await writeRuleMeta(projectRoot, { source: "doctor_fix" });
1992
+ await reconcileRules(projectRoot, { trigger: "doctor" });
1601
1993
  for (const issue of before.fixable_errors.filter(
1602
- (candidate) => ["agents_meta_missing", "agents_meta_stale", "rule_test_index_missing", "rule_test_index_stale"].includes(candidate.code)
1994
+ (candidate) => [
1995
+ "agents_meta_missing",
1996
+ "agents_meta_stale",
1997
+ "rule_test_index_missing",
1998
+ "rule_test_index_stale",
1999
+ "content_ref_missing",
2000
+ "rules_dir_unindexed"
2001
+ ].includes(candidate.code)
1603
2002
  )) {
1604
2003
  fixed.push(issue);
1605
2004
  }
1606
2005
  contextCache.invalidate("meta_write", projectRoot);
1607
2006
  }
2007
+ if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
2008
+ const ledgerPath = getEventLedgerPath(projectRoot);
2009
+ const truncResult = await truncateLedgerToLastNewline(ledgerPath);
2010
+ await appendEventLedgerEvent(projectRoot, {
2011
+ event_type: "event_ledger_truncated",
2012
+ byte_offset: truncResult.truncated_bytes,
2013
+ byte_length: truncResult.truncated_bytes,
2014
+ corrupted_path: truncResult.corrupted_path
2015
+ });
2016
+ fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
2017
+ }
2018
+ if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
2019
+ await fixMcpConfigInWrongFile(projectRoot);
2020
+ fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
2021
+ }
2022
+ if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
2023
+ await fixClaudeSkillLegacyPath(projectRoot);
2024
+ fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
2025
+ }
2026
+ if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
2027
+ await fixLegacyClientPaths(projectRoot);
2028
+ fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
2029
+ }
1608
2030
  const report = await runDoctorReport(projectRoot);
1609
2031
  return {
1610
2032
  changed: fixed.length > 0,
@@ -1616,9 +2038,9 @@ async function runDoctorFix(target) {
1616
2038
  };
1617
2039
  }
1618
2040
  async function inspectForensic(projectRoot) {
1619
- const path = join7(projectRoot, ".fabric", "forensic.json");
2041
+ const path = join8(projectRoot, ".fabric", "forensic.json");
1620
2042
  try {
1621
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile6(path, "utf8")));
2043
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile7(path, "utf8")));
1622
2044
  return { present: true, valid: true, report: parsed };
1623
2045
  } catch (error) {
1624
2046
  if (isMissingFileError(error)) {
@@ -1628,9 +2050,9 @@ async function inspectForensic(projectRoot) {
1628
2050
  }
1629
2051
  }
1630
2052
  async function inspectInitContext(projectRoot) {
1631
- const path = join7(projectRoot, ".fabric", "init-context.json");
2053
+ const path = join8(projectRoot, ".fabric", "init-context.json");
1632
2054
  try {
1633
- JSON.parse(await readFile6(path, "utf8"));
2055
+ JSON.parse(await readFile7(path, "utf8"));
1634
2056
  return { exists: true, validJson: true };
1635
2057
  } catch (error) {
1636
2058
  if (isMissingFileError(error)) {
@@ -1639,11 +2061,32 @@ async function inspectInitContext(projectRoot) {
1639
2061
  return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
1640
2062
  }
1641
2063
  }
2064
+ function inspectMcpConfigInWrongFile(projectRoot) {
2065
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
2066
+ if (!existsSync4(settingsPath)) {
2067
+ return { hasWrongEntry: false, settingsPath };
2068
+ }
2069
+ try {
2070
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2071
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2072
+ return { hasWrongEntry: false, settingsPath };
2073
+ }
2074
+ const settings = parsed;
2075
+ const mcpServers = settings.mcpServers;
2076
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2077
+ return { hasWrongEntry: false, settingsPath };
2078
+ }
2079
+ const hasWrongEntry = "fabric" in mcpServers;
2080
+ return { hasWrongEntry, settingsPath };
2081
+ } catch {
2082
+ return { hasWrongEntry: false, settingsPath };
2083
+ }
2084
+ }
1642
2085
  async function inspectMeta(projectRoot) {
1643
- const metaPath = join7(projectRoot, ".fabric", "agents.meta.json");
2086
+ const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1644
2087
  const built = await tryBuildRuleMeta(projectRoot);
1645
2088
  try {
1646
- const raw = await readFile6(metaPath, "utf8");
2089
+ const raw = await readFile7(metaPath, "utf8");
1647
2090
  const meta = agentsMetaSchema4.parse(JSON.parse(raw));
1648
2091
  const contentRefIssues = inspectContentRefs(projectRoot, meta);
1649
2092
  const changed = built === null ? false : built.changed;
@@ -1702,7 +2145,7 @@ function inspectContentRefs(projectRoot, meta) {
1702
2145
  for (const node of Object.values(meta.nodes)) {
1703
2146
  const contentRef = normalizePath(node.content_ref ?? node.file);
1704
2147
  if (contentRef === ".fabric/bootstrap/README.md") {
1705
- if (!existsSync2(join7(projectRoot, contentRef))) {
2148
+ if (!existsSync4(join8(projectRoot, contentRef))) {
1706
2149
  missing.push(contentRef);
1707
2150
  }
1708
2151
  continue;
@@ -1711,7 +2154,7 @@ function inspectContentRefs(projectRoot, meta) {
1711
2154
  invalid.push(contentRef);
1712
2155
  continue;
1713
2156
  }
1714
- if (!existsSync2(join7(projectRoot, contentRef))) {
2157
+ if (!existsSync4(join8(projectRoot, contentRef))) {
1715
2158
  missing.push(contentRef);
1716
2159
  }
1717
2160
  }
@@ -1719,19 +2162,23 @@ function inspectContentRefs(projectRoot, meta) {
1719
2162
  }
1720
2163
  async function inspectEventLedger(projectRoot) {
1721
2164
  const path = getEventLedgerPath(projectRoot);
1722
- const exists = existsSync2(path);
2165
+ const exists = existsSync4(path);
1723
2166
  if (!exists) {
1724
- return { exists: false, writable: false, parseable: false, path };
2167
+ return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
1725
2168
  }
1726
2169
  try {
1727
2170
  await access(path, constants.W_OK);
1728
- await readEventLedger(projectRoot);
1729
- const raw = await readFile6(path, "utf8");
2171
+ const { warnings } = await readEventLedger(projectRoot);
2172
+ const raw = await readFile7(path, "utf8");
1730
2173
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
2174
+ const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
1731
2175
  return {
1732
2176
  exists: true,
1733
2177
  writable: true,
1734
2178
  parseable: invalidLine === void 0,
2179
+ hasPartialWrite: partialWarning !== void 0,
2180
+ partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
2181
+ partialWriteByteLength: partialWarning?.byte_length ?? 0,
1735
2182
  path,
1736
2183
  error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
1737
2184
  };
@@ -1740,6 +2187,9 @@ async function inspectEventLedger(projectRoot) {
1740
2187
  exists: true,
1741
2188
  writable: false,
1742
2189
  parseable: false,
2190
+ hasPartialWrite: false,
2191
+ partialWriteByteOffset: 0,
2192
+ partialWriteByteLength: 0,
1743
2193
  path,
1744
2194
  error: error instanceof Error ? error.message : String(error)
1745
2195
  };
@@ -1747,10 +2197,10 @@ async function inspectEventLedger(projectRoot) {
1747
2197
  }
1748
2198
  async function inspectRuleSections(projectRoot) {
1749
2199
  const invalidFiles = [];
1750
- const files = findRuleFiles(projectRoot);
2200
+ const files = findRuleFiles2(projectRoot);
1751
2201
  for (const file of files) {
1752
2202
  try {
1753
- parseRuleSections(await readFile6(join7(projectRoot, file), "utf8"));
2203
+ parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
1754
2204
  } catch (error) {
1755
2205
  invalidFiles.push({
1756
2206
  file,
@@ -1764,10 +2214,10 @@ async function inspectRuleSections(projectRoot) {
1764
2214
  };
1765
2215
  }
1766
2216
  async function inspectRuleTestIndex(projectRoot) {
1767
- const path = join7(projectRoot, ".fabric", "rule-test.index.json");
2217
+ const path = join8(projectRoot, ".fabric", "rule-test.index.json");
1768
2218
  const built = await tryBuildRuleMeta(projectRoot);
1769
2219
  try {
1770
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile6(path, "utf8")));
2220
+ const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile7(path, "utf8")));
1771
2221
  return {
1772
2222
  present: true,
1773
2223
  valid: true,
@@ -1788,13 +2238,13 @@ async function inspectRuleTestIndex(projectRoot) {
1788
2238
  }
1789
2239
  function createBootstrapCheck(exists) {
1790
2240
  if (!exists) {
1791
- return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.");
2241
+ return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.", "Run `fab doctor --fix` to generate the bootstrap guide.");
1792
2242
  }
1793
2243
  return okCheck("Bootstrap README", ".fabric/bootstrap/README.md exists.");
1794
2244
  }
1795
2245
  function createTaxonomyCheck(exists) {
1796
2246
  if (!exists) {
1797
- return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.");
2247
+ return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.", "Run `fab init` to regenerate project scaffolding including INITIAL_TAXONOMY.md.");
1798
2248
  }
1799
2249
  return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
1800
2250
  }
@@ -1805,29 +2255,30 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
1805
2255
  "error",
1806
2256
  "manual_error",
1807
2257
  "forensic_missing",
1808
- `${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`
2258
+ `${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`,
2259
+ "Run `fab init` to regenerate .fabric/forensic.json."
1809
2260
  );
1810
2261
  }
1811
2262
  if (!forensic.valid) {
1812
- return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.");
2263
+ return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.", "Run `fab init` to regenerate .fabric/forensic.json.");
1813
2264
  }
1814
2265
  return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
1815
2266
  }
1816
2267
  function createInitContextCheck(initContext) {
1817
2268
  if (!initContext.exists) {
1818
- return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.");
2269
+ return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.", "Run the fabric-init skill in Claude Code or Codex CLI to complete initialization. See docs/migration-1.8.md FAQ.");
1819
2270
  }
1820
2271
  if (!initContext.validJson) {
1821
- return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.");
2272
+ return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.", "Delete .fabric/init-context.json and run `fab init` to regenerate it.");
1822
2273
  }
1823
2274
  return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
1824
2275
  }
1825
2276
  function createMetaCheck(meta) {
1826
2277
  if (!meta.present) {
1827
- return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.");
2278
+ return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/rules/.");
1828
2279
  }
1829
2280
  if (!meta.valid) {
1830
- return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.");
2281
+ return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
1831
2282
  }
1832
2283
  if (meta.stale) {
1833
2284
  return issueCheck(
@@ -1835,14 +2286,15 @@ function createMetaCheck(meta) {
1835
2286
  "error",
1836
2287
  "fixable_error",
1837
2288
  "agents_meta_stale",
1838
- `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`
2289
+ `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2290
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule files."
1839
2291
  );
1840
2292
  }
1841
2293
  return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/rules.`);
1842
2294
  }
1843
2295
  function createRuleContentRefCheck(meta) {
1844
2296
  if (!meta.valid) {
1845
- return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.");
2297
+ return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.", "Fix agents.meta.json first: run `fab doctor --fix`.");
1846
2298
  }
1847
2299
  if (meta.invalidContentRefs.length > 0) {
1848
2300
  return issueCheck(
@@ -1850,16 +2302,18 @@ function createRuleContentRefCheck(meta) {
1850
2302
  "error",
1851
2303
  "manual_error",
1852
2304
  "content_ref_outside_rules",
1853
- `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`
2305
+ `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`,
2306
+ "Edit agents.meta.json to ensure all content_ref values point inside .fabric/rules/."
1854
2307
  );
1855
2308
  }
1856
2309
  if (meta.missingContentRefs.length > 0) {
1857
2310
  return issueCheck(
1858
2311
  "Rule content refs",
1859
2312
  "error",
1860
- "manual_error",
2313
+ "fixable_error",
1861
2314
  "content_ref_missing",
1862
- `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing.`
2315
+ `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
2316
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/rules/."
1863
2317
  );
1864
2318
  }
1865
2319
  return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
@@ -1871,46 +2325,77 @@ function createRuleSectionsCheck(snapshot) {
1871
2325
  "error",
1872
2326
  "manual_error",
1873
2327
  "rule_sections_invalid",
1874
- `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`
2328
+ `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
2329
+ "Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
1875
2330
  );
1876
2331
  }
1877
2332
  return okCheck("Rule sections", `${snapshot.checkedCount} .fabric/rules file${snapshot.checkedCount === 1 ? "" : "s"} parsed.`);
1878
2333
  }
1879
2334
  function createRuleTestIndexCheck(index) {
1880
2335
  if (!index.present) {
1881
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error);
2336
+ return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/rule-test.index.json.");
1882
2337
  }
1883
2338
  if (!index.valid) {
1884
- return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error);
2339
+ return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error, "Delete .fabric/rule-test.index.json and run `fab doctor --fix` to regenerate it.");
1885
2340
  }
1886
2341
  if (index.stale) {
1887
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.");
2342
+ return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the rule-test index.");
1888
2343
  }
1889
2344
  return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
1890
2345
  }
1891
2346
  function createEventLedgerCheck(ledger) {
1892
2347
  if (!ledger.exists) {
1893
- return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.");
2348
+ return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.", "Run `fab doctor --fix` to create .fabric/events.jsonl.");
1894
2349
  }
1895
2350
  if (!ledger.writable) {
1896
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.");
2351
+ return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.", "Check file permissions on .fabric/events.jsonl and ensure no other process holds a write lock.");
1897
2352
  }
1898
2353
  if (!ledger.parseable) {
1899
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.");
2354
+ return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.", "Delete .fabric/events.jsonl and run `fab doctor --fix` to recreate it.");
1900
2355
  }
1901
2356
  return okCheck("Event ledger", ".fabric/events.jsonl exists, is writable, and is parseable.");
1902
2357
  }
2358
+ function createMcpConfigInWrongFileCheck(inspection) {
2359
+ if (inspection.hasWrongEntry) {
2360
+ return issueCheck(
2361
+ "Claude MCP config location",
2362
+ "error",
2363
+ "fixable_error",
2364
+ "mcp_config_in_wrong_file",
2365
+ `.claude/settings.json contains mcpServers.fabric \u2014 this file is for hooks/permissions only. Run --fix to remove it, then re-run fab init to write .mcp.json.`,
2366
+ "Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab init` to write .mcp.json."
2367
+ );
2368
+ }
2369
+ return okCheck("Claude MCP config location", "mcpServers.fabric is not in .claude/settings.json.");
2370
+ }
2371
+ function createEventLedgerPartialWriteCheck(ledger) {
2372
+ if (!ledger.exists || !ledger.writable) {
2373
+ return okCheck("Event ledger partial write", "No partial-write check needed (ledger missing or not writable).");
2374
+ }
2375
+ if (ledger.hasPartialWrite) {
2376
+ return issueCheck(
2377
+ "Event ledger partial write",
2378
+ "error",
2379
+ "fixable_error",
2380
+ "event_ledger_partial_write",
2381
+ `events.jsonl has a partial write at byte offset ${ledger.partialWriteByteOffset} (${ledger.partialWriteByteLength} corrupted bytes). Run --fix to truncate and preserve corrupted bytes.`,
2382
+ "Run `fab doctor --fix` to truncate the partial write and restore events.jsonl to a valid state."
2383
+ );
2384
+ }
2385
+ return okCheck("Event ledger partial write", "events.jsonl has no partial trailing write.");
2386
+ }
1903
2387
  function okCheck(name, message) {
1904
2388
  return { name, status: "ok", message };
1905
2389
  }
1906
- function issueCheck(name, status, kind, code, message) {
2390
+ function issueCheck(name, status, kind, code, message, actionHint) {
1907
2391
  return {
1908
2392
  name,
1909
2393
  status,
1910
2394
  kind,
1911
2395
  code,
1912
2396
  fixable: kind === "fixable_error",
1913
- message
2397
+ message,
2398
+ actionHint
1914
2399
  };
1915
2400
  }
1916
2401
  function collectIssues(checks, kind) {
@@ -1927,10 +2412,344 @@ function findIssue(issues, code) {
1927
2412
  message: code
1928
2413
  };
1929
2414
  }
2415
+ async function inspectMetaManuallyDiverged(projectRoot) {
2416
+ const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
2417
+ if (!existsSync4(metaPath)) {
2418
+ return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2419
+ }
2420
+ let meta;
2421
+ try {
2422
+ const raw = await readFile7(metaPath, "utf8");
2423
+ meta = agentsMetaSchema4.parse(JSON.parse(raw));
2424
+ } catch (error) {
2425
+ return {
2426
+ extraMetaEntries: [],
2427
+ hashMismatchEntries: [],
2428
+ readable: false,
2429
+ error: error instanceof Error ? error.message : String(error)
2430
+ };
2431
+ }
2432
+ const extraMetaEntries = [];
2433
+ const hashMismatchEntries = [];
2434
+ for (const node of Object.values(meta.nodes)) {
2435
+ const contentRef = node.content_ref ?? node.file;
2436
+ const absPath = join8(projectRoot, contentRef);
2437
+ if (!existsSync4(absPath)) {
2438
+ extraMetaEntries.push(contentRef);
2439
+ continue;
2440
+ }
2441
+ try {
2442
+ const content = readFileSync(absPath, "utf8");
2443
+ const diskHash = sha256(content);
2444
+ if (node.hash !== "" && node.hash !== diskHash) {
2445
+ hashMismatchEntries.push(contentRef);
2446
+ }
2447
+ } catch {
2448
+ extraMetaEntries.push(contentRef);
2449
+ }
2450
+ }
2451
+ return { extraMetaEntries, hashMismatchEntries, readable: true };
2452
+ }
2453
+ function inspectRulesDirUnindexed(projectRoot, meta) {
2454
+ const rulesDir = join8(projectRoot, ".fabric", "rules");
2455
+ if (!existsSync4(rulesDir)) {
2456
+ return { unindexedFiles: [] };
2457
+ }
2458
+ const physicalMdFiles = /* @__PURE__ */ new Set();
2459
+ const stack = [rulesDir];
2460
+ while (stack.length > 0) {
2461
+ const dir = stack.pop();
2462
+ if (dir === void 0) {
2463
+ continue;
2464
+ }
2465
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2466
+ const abs = join8(dir, entry.name);
2467
+ if (entry.isDirectory()) {
2468
+ stack.push(abs);
2469
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2470
+ const rel = posix2.join(".fabric/rules", abs.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2471
+ physicalMdFiles.add(rel);
2472
+ }
2473
+ }
2474
+ }
2475
+ const indexedRefs = /* @__PURE__ */ new Set();
2476
+ if (meta.valid && meta.meta !== null) {
2477
+ for (const node of Object.values(meta.meta.nodes)) {
2478
+ const ref = normalizePath(node.content_ref ?? node.file);
2479
+ indexedRefs.add(ref);
2480
+ }
2481
+ }
2482
+ const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
2483
+ return { unindexedFiles };
2484
+ }
2485
+ function createRulesDirUnindexedCheck(inspection) {
2486
+ if (inspection.unindexedFiles.length > 0) {
2487
+ return issueCheck(
2488
+ "Rules dir unindexed",
2489
+ "error",
2490
+ "fixable_error",
2491
+ "rules_dir_unindexed",
2492
+ `${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/rules/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing rule files.`,
2493
+ "Run `fab doctor --fix` to index the missing rule files."
2494
+ );
2495
+ }
2496
+ return okCheck("Rules dir unindexed", "All .fabric/rules/ .md files are indexed in agents.meta.json.");
2497
+ }
2498
+ async function inspectStableIdCollisions(projectRoot) {
2499
+ const rulesDir = join8(projectRoot, ".fabric", "rules");
2500
+ if (!existsSync4(rulesDir)) {
2501
+ return { collisions: [] };
2502
+ }
2503
+ const mdFiles = [];
2504
+ const stack = [rulesDir];
2505
+ while (stack.length > 0) {
2506
+ const dir = stack.pop();
2507
+ if (dir === void 0) {
2508
+ continue;
2509
+ }
2510
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2511
+ const abs = join8(dir, entry.name);
2512
+ if (entry.isDirectory()) {
2513
+ stack.push(abs);
2514
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2515
+ mdFiles.push(abs);
2516
+ }
2517
+ }
2518
+ }
2519
+ const stableIdToFiles = /* @__PURE__ */ new Map();
2520
+ const DECLARED_ID_PATTERN = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u;
2521
+ for (const absPath of mdFiles) {
2522
+ let source;
2523
+ try {
2524
+ source = await readFile7(absPath, "utf8");
2525
+ } catch {
2526
+ continue;
2527
+ }
2528
+ const match = DECLARED_ID_PATTERN.exec(source);
2529
+ if (match === null) {
2530
+ continue;
2531
+ }
2532
+ const stableId = match[1];
2533
+ const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2534
+ const existing = stableIdToFiles.get(stableId) ?? [];
2535
+ existing.push(relPath);
2536
+ stableIdToFiles.set(stableId, existing);
2537
+ }
2538
+ const collisions = [];
2539
+ for (const [stable_id, files] of stableIdToFiles) {
2540
+ if (files.length > 1) {
2541
+ collisions.push({ stable_id, files: files.sort() });
2542
+ }
2543
+ }
2544
+ return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
2545
+ }
2546
+ function createStableIdCollisionCheck(inspection) {
2547
+ if (inspection.collisions.length > 0) {
2548
+ const first = inspection.collisions[0];
2549
+ const detail = inspection.collisions.length === 1 ? `stable_id "${first.stable_id}" is declared in ${first.files.length} files: ${first.files.join(", ")}.` : `${inspection.collisions.length} stable_id collision${inspection.collisions.length === 1 ? "" : "s"} detected. First: "${first.stable_id}" in ${first.files.join(", ")}.`;
2550
+ return issueCheck(
2551
+ "Stable ID collision",
2552
+ "warn",
2553
+ "warning",
2554
+ "stable_id_collision",
2555
+ `${detail} Edit one of the rule files to use a unique stable_id.`,
2556
+ "Edit one of the colliding rule files to declare a different `<!-- fab:rule-id X -->` value."
2557
+ );
2558
+ }
2559
+ return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/rules/.");
2560
+ }
2561
+ function createMetaManuallyDivergedCheck(inspection) {
2562
+ if (!inspection.readable) {
2563
+ return okCheck("Meta manual divergence", "agents.meta.json not readable; skipping divergence check.");
2564
+ }
2565
+ if (inspection.extraMetaEntries.length > 0) {
2566
+ return issueCheck(
2567
+ "Meta manual divergence",
2568
+ "warn",
2569
+ "warning",
2570
+ "meta_manually_diverged",
2571
+ `agents.meta.json has ${inspection.extraMetaEntries.length} entr${inspection.extraMetaEntries.length === 1 ? "y" : "ies"} with no backing file on disk. Run --fix to reconcile.`,
2572
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the rule files currently on disk."
2573
+ );
2574
+ }
2575
+ if (inspection.hashMismatchEntries.length > 0) {
2576
+ return issueCheck(
2577
+ "Meta manual divergence",
2578
+ "warn",
2579
+ "warning",
2580
+ "meta_manually_diverged",
2581
+ `agents.meta.json has ${inspection.hashMismatchEntries.length} entr${inspection.hashMismatchEntries.length === 1 ? "y" : "ies"} whose hash does not match the file on disk. Run --fix to reconcile.`,
2582
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule file contents."
2583
+ );
2584
+ }
2585
+ return okCheck("Meta manual divergence", "agents.meta.json is consistent with rule files on disk.");
2586
+ }
2587
+ function inspectPreexistingRootFiles(projectRoot) {
2588
+ const candidates = ["CLAUDE.md", "AGENTS.md"];
2589
+ const detected = candidates.filter((name) => existsSync4(join8(projectRoot, name)));
2590
+ return { detected };
2591
+ }
2592
+ function createPreexistingRootFilesCheck(inspection) {
2593
+ if (inspection.detected.length === 0) {
2594
+ return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
2595
+ }
2596
+ return {
2597
+ name: "Preexisting root markdown",
2598
+ status: "ok",
2599
+ kind: "info",
2600
+ code: "preexisting_root_claude_md",
2601
+ fixable: false,
2602
+ message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
2603
+ actionHint: "Move rule content to `.fabric/rules/` if you want it available in MCP responses."
2604
+ };
2605
+ }
2606
+ function inspectClaudeSkillLegacyPath(projectRoot) {
2607
+ const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2608
+ const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2609
+ const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
2610
+ return { hasLegacy, legacyPath, newPath };
2611
+ }
2612
+ function createClaudeSkillLegacyPathCheck(inspection) {
2613
+ if (inspection.hasLegacy) {
2614
+ return issueCheck(
2615
+ "Claude skill path",
2616
+ "error",
2617
+ "fixable_error",
2618
+ "claude_skill_legacy_path",
2619
+ `.claude/skills/agents-md-init/SKILL.md exists at the legacy path. Run --fix to migrate it to .claude/skills/fabric-init/SKILL.md (user edits preserved).`,
2620
+ "Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
2621
+ );
2622
+ }
2623
+ return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
2624
+ }
2625
+ async function fixClaudeSkillLegacyPath(projectRoot) {
2626
+ const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2627
+ const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2628
+ if (!existsSync4(legacyPath)) {
2629
+ return;
2630
+ }
2631
+ mkdirSync(join8(newPath, ".."), { recursive: true });
2632
+ renameSync(legacyPath, newPath);
2633
+ const legacyDir = join8(legacyPath, "..");
2634
+ try {
2635
+ rmdirSync(legacyDir);
2636
+ } catch {
2637
+ }
2638
+ await appendEventLedgerEvent(projectRoot, {
2639
+ event_type: "claude_skill_path_migrated",
2640
+ from: legacyPath,
2641
+ to: newPath
2642
+ });
2643
+ }
2644
+ function inspectLegacyClientPaths(projectRoot) {
2645
+ const configPath = join8(projectRoot, "fabric.config.json");
2646
+ if (!existsSync4(configPath)) {
2647
+ return { presentKeys: [] };
2648
+ }
2649
+ try {
2650
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2651
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2652
+ return { presentKeys: [] };
2653
+ }
2654
+ const config = parsed;
2655
+ const clientPaths = config.clientPaths;
2656
+ if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2657
+ return { presentKeys: [] };
2658
+ }
2659
+ const cp = clientPaths;
2660
+ const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
2661
+ return { presentKeys };
2662
+ } catch {
2663
+ return { presentKeys: [] };
2664
+ }
2665
+ }
2666
+ function createLegacyClientPathCheck(inspection) {
2667
+ if (inspection.presentKeys.length > 0) {
2668
+ return issueCheck(
2669
+ "Legacy client paths",
2670
+ "warn",
2671
+ "warning",
2672
+ "legacy_client_path_present",
2673
+ `fabric.config.json contains deprecated clientPaths keys: ${inspection.presentKeys.join(", ")}. These clients are removed in 1.8.0; run --fix to clean now or accept the upcoming removal.`,
2674
+ "Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
2675
+ );
2676
+ }
2677
+ return okCheck("Legacy client paths", "No deprecated clientPaths keys found in fabric.config.json.");
2678
+ }
2679
+ async function fixLegacyClientPaths(projectRoot) {
2680
+ const configPath = join8(projectRoot, "fabric.config.json");
2681
+ if (!existsSync4(configPath)) {
2682
+ return;
2683
+ }
2684
+ let config;
2685
+ try {
2686
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2687
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2688
+ return;
2689
+ }
2690
+ config = parsed;
2691
+ } catch {
2692
+ return;
2693
+ }
2694
+ const clientPaths = config.clientPaths;
2695
+ if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2696
+ return;
2697
+ }
2698
+ const cp = clientPaths;
2699
+ const removed = [];
2700
+ for (const key of LEGACY_CLIENT_PATH_KEYS) {
2701
+ if (key in cp) {
2702
+ delete cp[key];
2703
+ removed.push(key);
2704
+ }
2705
+ }
2706
+ if (removed.length === 0) {
2707
+ return;
2708
+ }
2709
+ const updatedConfig = { ...config, clientPaths: cp };
2710
+ await atomicWriteJson2(configPath, updatedConfig, { indent: 2 });
2711
+ await appendEventLedgerEvent(projectRoot, {
2712
+ event_type: "legacy_client_path_present",
2713
+ removed
2714
+ });
2715
+ }
2716
+ async function fixMcpConfigInWrongFile(projectRoot) {
2717
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
2718
+ if (!existsSync4(settingsPath)) {
2719
+ return;
2720
+ }
2721
+ let settings;
2722
+ try {
2723
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2724
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2725
+ return;
2726
+ }
2727
+ settings = parsed;
2728
+ } catch {
2729
+ return;
2730
+ }
2731
+ const mcpServers = settings.mcpServers;
2732
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2733
+ return;
2734
+ }
2735
+ const { fabric: _removed, ...remainingServers } = mcpServers;
2736
+ const cleaned = { ...settings };
2737
+ if (Object.keys(remainingServers).length === 0) {
2738
+ delete cleaned.mcpServers;
2739
+ } else {
2740
+ cleaned.mcpServers = remainingServers;
2741
+ }
2742
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2743
+ await appendEventLedgerEvent(projectRoot, {
2744
+ event_type: "mcp_config_migrated",
2745
+ source: "doctor_fix",
2746
+ removed_from: ".claude/settings.json"
2747
+ });
2748
+ }
1930
2749
  async function writeDefaultBootstrap(projectRoot) {
1931
- const path = join7(projectRoot, ".fabric", "bootstrap", "README.md");
2750
+ const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
1932
2751
  await ensureParentDirectory(path);
1933
- await atomicWriteText(path, "# Fabric Bootstrap\n\nProject-specific Fabric bootstrap notes.\n");
2752
+ await atomicWriteText3(path, buildBootstrapContent(projectRoot));
1934
2753
  }
1935
2754
  async function ensureEventLedger(projectRoot) {
1936
2755
  const path = getEventLedgerPath(projectRoot);
@@ -1942,9 +2761,9 @@ function createFixMessage(fixed, report) {
1942
2761
  const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
1943
2762
  return `${fixedText} ${manualText}`;
1944
2763
  }
1945
- function findRuleFiles(projectRoot) {
1946
- const rulesRoot = join7(projectRoot, ".fabric", "rules");
1947
- if (!existsSync2(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
2764
+ function findRuleFiles2(projectRoot) {
2765
+ const rulesRoot = join8(projectRoot, ".fabric", "rules");
2766
+ if (!existsSync4(rulesRoot) || !statSync3(rulesRoot).isDirectory()) {
1948
2767
  return [];
1949
2768
  }
1950
2769
  const files = [];
@@ -1955,7 +2774,7 @@ function findRuleFiles(projectRoot) {
1955
2774
  continue;
1956
2775
  }
1957
2776
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1958
- const absolutePath = join7(current, entry.name);
2777
+ const absolutePath = join8(current, entry.name);
1959
2778
  const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
1960
2779
  if (entry.isDirectory()) {
1961
2780
  stack.push(absolutePath);
@@ -1981,7 +2800,7 @@ function normalizePath(path) {
1981
2800
  return posix2.normalize(path.split("\\").join("/"));
1982
2801
  }
1983
2802
  function collectEntryPoints(root) {
1984
- if (!existsSync2(root) || !statSync2(root).isDirectory()) {
2803
+ if (!existsSync4(root) || !statSync3(root).isDirectory()) {
1985
2804
  return [];
1986
2805
  }
1987
2806
  const entries = [];
@@ -1992,7 +2811,7 @@ function collectEntryPoints(root) {
1992
2811
  continue;
1993
2812
  }
1994
2813
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1995
- const absolutePath = join7(current, entry.name);
2814
+ const absolutePath = join8(current, entry.name);
1996
2815
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
1997
2816
  if (relativePath.length === 0) {
1998
2817
  continue;
@@ -2052,8 +2871,6 @@ function isMissingFileError(error) {
2052
2871
  export {
2053
2872
  AGENTS_MD_RESOURCE_URI,
2054
2873
  contextCache,
2055
- AgentsMetaFileMissingError,
2056
- AgentsMetaInvalidError,
2057
2874
  resolveProjectRoot,
2058
2875
  readAgentsMeta,
2059
2876
  LEDGER_PATH,
@@ -2066,10 +2883,7 @@ export {
2066
2883
  isNodeError,
2067
2884
  appendEventLedgerEvent,
2068
2885
  readEventLedger,
2069
- getRules,
2070
- planContext,
2071
- RULE_SECTION_NAMES,
2072
- getRuleSections,
2886
+ flushAndSyncEventLedger,
2073
2887
  buildRuleMeta,
2074
2888
  writeRuleMeta,
2075
2889
  computeRulesBasedAgentsMeta,
@@ -2078,6 +2892,12 @@ export {
2078
2892
  deriveRuleMetaTopologyType,
2079
2893
  isSameRuleTestIndex,
2080
2894
  stableStringify,
2895
+ invalidateRuleSyncCooldown,
2896
+ ensureRulesFresh,
2897
+ reconcileRules,
2898
+ getRules,
2899
+ planContext,
2900
+ getRuleSections,
2081
2901
  runDoctorReport,
2082
2902
  runDoctorFix
2083
2903
  };