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

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;
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
+ };
794
1142
  }
795
1143
 
1144
+ // src/services/doctor.ts
1145
+ import { existsSync as existsSync4, mkdirSync, readdirSync, readFileSync, rmdirSync, renameSync, statSync as statSync3, unlinkSync } 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";
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,16 @@ 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 claudeHookLegacyPath = inspectClaudeHookLegacyPath(projectRoot);
1915
+ const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1916
+ const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
1917
+ const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
1918
+ const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
1544
1919
  const checks = [
1545
1920
  createBootstrapCheck(bootstrapExists),
1546
1921
  createTaxonomyCheck(taxonomyExists),
@@ -1550,17 +1925,28 @@ async function runDoctorReport(target) {
1550
1925
  createRuleContentRefCheck(meta),
1551
1926
  createRuleSectionsCheck(ruleSections),
1552
1927
  createRuleTestIndexCheck(ruleTestIndex),
1553
- createEventLedgerCheck(eventLedger)
1928
+ createEventLedgerCheck(eventLedger),
1929
+ createEventLedgerPartialWriteCheck(eventLedger),
1930
+ createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1931
+ createMetaManuallyDivergedCheck(metaManuallyDiverged),
1932
+ createRulesDirUnindexedCheck(rulesDirUnindexed),
1933
+ createStableIdCollisionCheck(stableIdCollision),
1934
+ createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
1935
+ createClaudeHookLegacyPathCheck(claudeHookLegacyPath),
1936
+ createPreexistingRootFilesCheck(preexistingRootFiles),
1937
+ createLegacyClientPathCheck(legacyClientPaths)
1554
1938
  ];
1555
1939
  const fixableErrors = collectIssues(checks, "fixable_error");
1556
1940
  const manualErrors = collectIssues(checks, "manual_error");
1557
1941
  const warnings = collectIssues(checks, "warning");
1942
+ const infos = collectIssues(checks, "info");
1558
1943
  return {
1559
1944
  status: reduceStatus(checks.map((check) => check.status)),
1560
1945
  checks,
1561
1946
  fixable_errors: fixableErrors,
1562
1947
  manual_errors: manualErrors,
1563
1948
  warnings,
1949
+ infos,
1564
1950
  summary: {
1565
1951
  target: projectRoot,
1566
1952
  framework: {
@@ -1576,8 +1962,9 @@ async function runDoctorReport(target) {
1576
1962
  fixableErrorCount: fixableErrors.length,
1577
1963
  manualErrorCount: manualErrors.length,
1578
1964
  warningCount: warnings.length,
1965
+ infoCount: infos.length,
1579
1966
  targetFiles: Object.fromEntries(
1580
- TARGET_FILE_PATHS.map((path) => [path, existsSync2(join7(projectRoot, path))])
1967
+ TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
1581
1968
  )
1582
1969
  }
1583
1970
  };
@@ -1595,16 +1982,57 @@ async function runDoctorFix(target) {
1595
1982
  fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
1596
1983
  }
1597
1984
  if (before.fixable_errors.some(
1598
- (issue) => ["agents_meta_missing", "agents_meta_stale", "rule_test_index_missing", "rule_test_index_stale"].includes(issue.code)
1985
+ (issue) => [
1986
+ "agents_meta_missing",
1987
+ "agents_meta_stale",
1988
+ "rule_test_index_missing",
1989
+ "rule_test_index_stale",
1990
+ "content_ref_missing",
1991
+ "rules_dir_unindexed"
1992
+ ].includes(issue.code)
1599
1993
  )) {
1600
- await writeRuleMeta(projectRoot, { source: "doctor_fix" });
1994
+ await reconcileRules(projectRoot, { trigger: "doctor" });
1601
1995
  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)
1996
+ (candidate) => [
1997
+ "agents_meta_missing",
1998
+ "agents_meta_stale",
1999
+ "rule_test_index_missing",
2000
+ "rule_test_index_stale",
2001
+ "content_ref_missing",
2002
+ "rules_dir_unindexed"
2003
+ ].includes(candidate.code)
1603
2004
  )) {
1604
2005
  fixed.push(issue);
1605
2006
  }
1606
2007
  contextCache.invalidate("meta_write", projectRoot);
1607
2008
  }
2009
+ if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
2010
+ const ledgerPath = getEventLedgerPath(projectRoot);
2011
+ const truncResult = await truncateLedgerToLastNewline(ledgerPath);
2012
+ await appendEventLedgerEvent(projectRoot, {
2013
+ event_type: "event_ledger_truncated",
2014
+ byte_offset: truncResult.truncated_bytes,
2015
+ byte_length: truncResult.truncated_bytes,
2016
+ corrupted_path: truncResult.corrupted_path
2017
+ });
2018
+ fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
2019
+ }
2020
+ if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
2021
+ await fixMcpConfigInWrongFile(projectRoot);
2022
+ fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
2023
+ }
2024
+ if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
2025
+ await fixClaudeSkillLegacyPath(projectRoot);
2026
+ fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
2027
+ }
2028
+ if (before.fixable_errors.some((issue) => issue.code === "claude_hook_legacy_path")) {
2029
+ await fixClaudeHookLegacyPath(projectRoot);
2030
+ fixed.push(findIssue(before.fixable_errors, "claude_hook_legacy_path"));
2031
+ }
2032
+ if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
2033
+ await fixLegacyClientPaths(projectRoot);
2034
+ fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
2035
+ }
1608
2036
  const report = await runDoctorReport(projectRoot);
1609
2037
  return {
1610
2038
  changed: fixed.length > 0,
@@ -1616,9 +2044,9 @@ async function runDoctorFix(target) {
1616
2044
  };
1617
2045
  }
1618
2046
  async function inspectForensic(projectRoot) {
1619
- const path = join7(projectRoot, ".fabric", "forensic.json");
2047
+ const path = join8(projectRoot, ".fabric", "forensic.json");
1620
2048
  try {
1621
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile6(path, "utf8")));
2049
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile7(path, "utf8")));
1622
2050
  return { present: true, valid: true, report: parsed };
1623
2051
  } catch (error) {
1624
2052
  if (isMissingFileError(error)) {
@@ -1628,9 +2056,9 @@ async function inspectForensic(projectRoot) {
1628
2056
  }
1629
2057
  }
1630
2058
  async function inspectInitContext(projectRoot) {
1631
- const path = join7(projectRoot, ".fabric", "init-context.json");
2059
+ const path = join8(projectRoot, ".fabric", "init-context.json");
1632
2060
  try {
1633
- JSON.parse(await readFile6(path, "utf8"));
2061
+ JSON.parse(await readFile7(path, "utf8"));
1634
2062
  return { exists: true, validJson: true };
1635
2063
  } catch (error) {
1636
2064
  if (isMissingFileError(error)) {
@@ -1639,11 +2067,32 @@ async function inspectInitContext(projectRoot) {
1639
2067
  return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
1640
2068
  }
1641
2069
  }
2070
+ function inspectMcpConfigInWrongFile(projectRoot) {
2071
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
2072
+ if (!existsSync4(settingsPath)) {
2073
+ return { hasWrongEntry: false, settingsPath };
2074
+ }
2075
+ try {
2076
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2077
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2078
+ return { hasWrongEntry: false, settingsPath };
2079
+ }
2080
+ const settings = parsed;
2081
+ const mcpServers = settings.mcpServers;
2082
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2083
+ return { hasWrongEntry: false, settingsPath };
2084
+ }
2085
+ const hasWrongEntry = "fabric" in mcpServers;
2086
+ return { hasWrongEntry, settingsPath };
2087
+ } catch {
2088
+ return { hasWrongEntry: false, settingsPath };
2089
+ }
2090
+ }
1642
2091
  async function inspectMeta(projectRoot) {
1643
- const metaPath = join7(projectRoot, ".fabric", "agents.meta.json");
2092
+ const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1644
2093
  const built = await tryBuildRuleMeta(projectRoot);
1645
2094
  try {
1646
- const raw = await readFile6(metaPath, "utf8");
2095
+ const raw = await readFile7(metaPath, "utf8");
1647
2096
  const meta = agentsMetaSchema4.parse(JSON.parse(raw));
1648
2097
  const contentRefIssues = inspectContentRefs(projectRoot, meta);
1649
2098
  const changed = built === null ? false : built.changed;
@@ -1702,7 +2151,7 @@ function inspectContentRefs(projectRoot, meta) {
1702
2151
  for (const node of Object.values(meta.nodes)) {
1703
2152
  const contentRef = normalizePath(node.content_ref ?? node.file);
1704
2153
  if (contentRef === ".fabric/bootstrap/README.md") {
1705
- if (!existsSync2(join7(projectRoot, contentRef))) {
2154
+ if (!existsSync4(join8(projectRoot, contentRef))) {
1706
2155
  missing.push(contentRef);
1707
2156
  }
1708
2157
  continue;
@@ -1711,7 +2160,7 @@ function inspectContentRefs(projectRoot, meta) {
1711
2160
  invalid.push(contentRef);
1712
2161
  continue;
1713
2162
  }
1714
- if (!existsSync2(join7(projectRoot, contentRef))) {
2163
+ if (!existsSync4(join8(projectRoot, contentRef))) {
1715
2164
  missing.push(contentRef);
1716
2165
  }
1717
2166
  }
@@ -1719,19 +2168,23 @@ function inspectContentRefs(projectRoot, meta) {
1719
2168
  }
1720
2169
  async function inspectEventLedger(projectRoot) {
1721
2170
  const path = getEventLedgerPath(projectRoot);
1722
- const exists = existsSync2(path);
2171
+ const exists = existsSync4(path);
1723
2172
  if (!exists) {
1724
- return { exists: false, writable: false, parseable: false, path };
2173
+ return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
1725
2174
  }
1726
2175
  try {
1727
2176
  await access(path, constants.W_OK);
1728
- await readEventLedger(projectRoot);
1729
- const raw = await readFile6(path, "utf8");
2177
+ const { warnings } = await readEventLedger(projectRoot);
2178
+ const raw = await readFile7(path, "utf8");
1730
2179
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
2180
+ const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
1731
2181
  return {
1732
2182
  exists: true,
1733
2183
  writable: true,
1734
2184
  parseable: invalidLine === void 0,
2185
+ hasPartialWrite: partialWarning !== void 0,
2186
+ partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
2187
+ partialWriteByteLength: partialWarning?.byte_length ?? 0,
1735
2188
  path,
1736
2189
  error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
1737
2190
  };
@@ -1740,6 +2193,9 @@ async function inspectEventLedger(projectRoot) {
1740
2193
  exists: true,
1741
2194
  writable: false,
1742
2195
  parseable: false,
2196
+ hasPartialWrite: false,
2197
+ partialWriteByteOffset: 0,
2198
+ partialWriteByteLength: 0,
1743
2199
  path,
1744
2200
  error: error instanceof Error ? error.message : String(error)
1745
2201
  };
@@ -1747,10 +2203,10 @@ async function inspectEventLedger(projectRoot) {
1747
2203
  }
1748
2204
  async function inspectRuleSections(projectRoot) {
1749
2205
  const invalidFiles = [];
1750
- const files = findRuleFiles(projectRoot);
2206
+ const files = findRuleFiles2(projectRoot);
1751
2207
  for (const file of files) {
1752
2208
  try {
1753
- parseRuleSections(await readFile6(join7(projectRoot, file), "utf8"));
2209
+ parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
1754
2210
  } catch (error) {
1755
2211
  invalidFiles.push({
1756
2212
  file,
@@ -1764,10 +2220,10 @@ async function inspectRuleSections(projectRoot) {
1764
2220
  };
1765
2221
  }
1766
2222
  async function inspectRuleTestIndex(projectRoot) {
1767
- const path = join7(projectRoot, ".fabric", "rule-test.index.json");
2223
+ const path = join8(projectRoot, ".fabric", "rule-test.index.json");
1768
2224
  const built = await tryBuildRuleMeta(projectRoot);
1769
2225
  try {
1770
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile6(path, "utf8")));
2226
+ const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile7(path, "utf8")));
1771
2227
  return {
1772
2228
  present: true,
1773
2229
  valid: true,
@@ -1788,13 +2244,13 @@ async function inspectRuleTestIndex(projectRoot) {
1788
2244
  }
1789
2245
  function createBootstrapCheck(exists) {
1790
2246
  if (!exists) {
1791
- return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.");
2247
+ 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
2248
  }
1793
2249
  return okCheck("Bootstrap README", ".fabric/bootstrap/README.md exists.");
1794
2250
  }
1795
2251
  function createTaxonomyCheck(exists) {
1796
2252
  if (!exists) {
1797
- return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.");
2253
+ 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
2254
  }
1799
2255
  return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
1800
2256
  }
@@ -1805,29 +2261,30 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
1805
2261
  "error",
1806
2262
  "manual_error",
1807
2263
  "forensic_missing",
1808
- `${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`
2264
+ `${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`,
2265
+ "Run `fab init` to regenerate .fabric/forensic.json."
1809
2266
  );
1810
2267
  }
1811
2268
  if (!forensic.valid) {
1812
- return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.");
2269
+ 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
2270
  }
1814
2271
  return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
1815
2272
  }
1816
2273
  function createInitContextCheck(initContext) {
1817
2274
  if (!initContext.exists) {
1818
- return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.");
2275
+ 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
2276
  }
1820
2277
  if (!initContext.validJson) {
1821
- return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.");
2278
+ 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
2279
  }
1823
2280
  return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
1824
2281
  }
1825
2282
  function createMetaCheck(meta) {
1826
2283
  if (!meta.present) {
1827
- return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.");
2284
+ 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
2285
  }
1829
2286
  if (!meta.valid) {
1830
- return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.");
2287
+ 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
2288
  }
1832
2289
  if (meta.stale) {
1833
2290
  return issueCheck(
@@ -1835,14 +2292,15 @@ function createMetaCheck(meta) {
1835
2292
  "error",
1836
2293
  "fixable_error",
1837
2294
  "agents_meta_stale",
1838
- `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`
2295
+ `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2296
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule files."
1839
2297
  );
1840
2298
  }
1841
2299
  return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/rules.`);
1842
2300
  }
1843
2301
  function createRuleContentRefCheck(meta) {
1844
2302
  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.");
2303
+ 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
2304
  }
1847
2305
  if (meta.invalidContentRefs.length > 0) {
1848
2306
  return issueCheck(
@@ -1850,16 +2308,18 @@ function createRuleContentRefCheck(meta) {
1850
2308
  "error",
1851
2309
  "manual_error",
1852
2310
  "content_ref_outside_rules",
1853
- `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`
2311
+ `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`,
2312
+ "Edit agents.meta.json to ensure all content_ref values point inside .fabric/rules/."
1854
2313
  );
1855
2314
  }
1856
2315
  if (meta.missingContentRefs.length > 0) {
1857
2316
  return issueCheck(
1858
2317
  "Rule content refs",
1859
2318
  "error",
1860
- "manual_error",
2319
+ "fixable_error",
1861
2320
  "content_ref_missing",
1862
- `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing.`
2321
+ `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
2322
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/rules/."
1863
2323
  );
1864
2324
  }
1865
2325
  return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
@@ -1871,46 +2331,77 @@ function createRuleSectionsCheck(snapshot) {
1871
2331
  "error",
1872
2332
  "manual_error",
1873
2333
  "rule_sections_invalid",
1874
- `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`
2334
+ `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
2335
+ "Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
1875
2336
  );
1876
2337
  }
1877
2338
  return okCheck("Rule sections", `${snapshot.checkedCount} .fabric/rules file${snapshot.checkedCount === 1 ? "" : "s"} parsed.`);
1878
2339
  }
1879
2340
  function createRuleTestIndexCheck(index) {
1880
2341
  if (!index.present) {
1881
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error);
2342
+ 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
2343
  }
1883
2344
  if (!index.valid) {
1884
- return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error);
2345
+ 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
2346
  }
1886
2347
  if (index.stale) {
1887
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.");
2348
+ 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
2349
  }
1889
2350
  return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
1890
2351
  }
1891
2352
  function createEventLedgerCheck(ledger) {
1892
2353
  if (!ledger.exists) {
1893
- return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.");
2354
+ 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
2355
  }
1895
2356
  if (!ledger.writable) {
1896
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.");
2357
+ 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
2358
  }
1898
2359
  if (!ledger.parseable) {
1899
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.");
2360
+ 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
2361
  }
1901
2362
  return okCheck("Event ledger", ".fabric/events.jsonl exists, is writable, and is parseable.");
1902
2363
  }
2364
+ function createMcpConfigInWrongFileCheck(inspection) {
2365
+ if (inspection.hasWrongEntry) {
2366
+ return issueCheck(
2367
+ "Claude MCP config location",
2368
+ "error",
2369
+ "fixable_error",
2370
+ "mcp_config_in_wrong_file",
2371
+ `.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.`,
2372
+ "Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab init` to write .mcp.json."
2373
+ );
2374
+ }
2375
+ return okCheck("Claude MCP config location", "mcpServers.fabric is not in .claude/settings.json.");
2376
+ }
2377
+ function createEventLedgerPartialWriteCheck(ledger) {
2378
+ if (!ledger.exists || !ledger.writable) {
2379
+ return okCheck("Event ledger partial write", "No partial-write check needed (ledger missing or not writable).");
2380
+ }
2381
+ if (ledger.hasPartialWrite) {
2382
+ return issueCheck(
2383
+ "Event ledger partial write",
2384
+ "error",
2385
+ "fixable_error",
2386
+ "event_ledger_partial_write",
2387
+ `events.jsonl has a partial write at byte offset ${ledger.partialWriteByteOffset} (${ledger.partialWriteByteLength} corrupted bytes). Run --fix to truncate and preserve corrupted bytes.`,
2388
+ "Run `fab doctor --fix` to truncate the partial write and restore events.jsonl to a valid state."
2389
+ );
2390
+ }
2391
+ return okCheck("Event ledger partial write", "events.jsonl has no partial trailing write.");
2392
+ }
1903
2393
  function okCheck(name, message) {
1904
2394
  return { name, status: "ok", message };
1905
2395
  }
1906
- function issueCheck(name, status, kind, code, message) {
2396
+ function issueCheck(name, status, kind, code, message, actionHint) {
1907
2397
  return {
1908
2398
  name,
1909
2399
  status,
1910
2400
  kind,
1911
2401
  code,
1912
2402
  fixable: kind === "fixable_error",
1913
- message
2403
+ message,
2404
+ actionHint
1914
2405
  };
1915
2406
  }
1916
2407
  function collectIssues(checks, kind) {
@@ -1927,10 +2418,403 @@ function findIssue(issues, code) {
1927
2418
  message: code
1928
2419
  };
1929
2420
  }
2421
+ async function inspectMetaManuallyDiverged(projectRoot) {
2422
+ const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
2423
+ if (!existsSync4(metaPath)) {
2424
+ return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2425
+ }
2426
+ let meta;
2427
+ try {
2428
+ const raw = await readFile7(metaPath, "utf8");
2429
+ meta = agentsMetaSchema4.parse(JSON.parse(raw));
2430
+ } catch (error) {
2431
+ return {
2432
+ extraMetaEntries: [],
2433
+ hashMismatchEntries: [],
2434
+ readable: false,
2435
+ error: error instanceof Error ? error.message : String(error)
2436
+ };
2437
+ }
2438
+ const extraMetaEntries = [];
2439
+ const hashMismatchEntries = [];
2440
+ for (const node of Object.values(meta.nodes)) {
2441
+ const contentRef = node.content_ref ?? node.file;
2442
+ const absPath = join8(projectRoot, contentRef);
2443
+ if (!existsSync4(absPath)) {
2444
+ extraMetaEntries.push(contentRef);
2445
+ continue;
2446
+ }
2447
+ try {
2448
+ const content = readFileSync(absPath, "utf8");
2449
+ const diskHash = sha256(content);
2450
+ if (node.hash !== "" && node.hash !== diskHash) {
2451
+ hashMismatchEntries.push(contentRef);
2452
+ }
2453
+ } catch {
2454
+ extraMetaEntries.push(contentRef);
2455
+ }
2456
+ }
2457
+ return { extraMetaEntries, hashMismatchEntries, readable: true };
2458
+ }
2459
+ function inspectRulesDirUnindexed(projectRoot, meta) {
2460
+ const rulesDir = join8(projectRoot, ".fabric", "rules");
2461
+ if (!existsSync4(rulesDir)) {
2462
+ return { unindexedFiles: [] };
2463
+ }
2464
+ const physicalMdFiles = /* @__PURE__ */ new Set();
2465
+ const stack = [rulesDir];
2466
+ while (stack.length > 0) {
2467
+ const dir = stack.pop();
2468
+ if (dir === void 0) {
2469
+ continue;
2470
+ }
2471
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2472
+ const abs = join8(dir, entry.name);
2473
+ if (entry.isDirectory()) {
2474
+ stack.push(abs);
2475
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2476
+ const rel = posix2.join(".fabric/rules", abs.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2477
+ physicalMdFiles.add(rel);
2478
+ }
2479
+ }
2480
+ }
2481
+ const indexedRefs = /* @__PURE__ */ new Set();
2482
+ if (meta.valid && meta.meta !== null) {
2483
+ for (const node of Object.values(meta.meta.nodes)) {
2484
+ const ref = normalizePath(node.content_ref ?? node.file);
2485
+ indexedRefs.add(ref);
2486
+ }
2487
+ }
2488
+ const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
2489
+ return { unindexedFiles };
2490
+ }
2491
+ function createRulesDirUnindexedCheck(inspection) {
2492
+ if (inspection.unindexedFiles.length > 0) {
2493
+ return issueCheck(
2494
+ "Rules dir unindexed",
2495
+ "error",
2496
+ "fixable_error",
2497
+ "rules_dir_unindexed",
2498
+ `${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.`,
2499
+ "Run `fab doctor --fix` to index the missing rule files."
2500
+ );
2501
+ }
2502
+ return okCheck("Rules dir unindexed", "All .fabric/rules/ .md files are indexed in agents.meta.json.");
2503
+ }
2504
+ async function inspectStableIdCollisions(projectRoot) {
2505
+ const rulesDir = join8(projectRoot, ".fabric", "rules");
2506
+ if (!existsSync4(rulesDir)) {
2507
+ return { collisions: [] };
2508
+ }
2509
+ const mdFiles = [];
2510
+ const stack = [rulesDir];
2511
+ while (stack.length > 0) {
2512
+ const dir = stack.pop();
2513
+ if (dir === void 0) {
2514
+ continue;
2515
+ }
2516
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2517
+ const abs = join8(dir, entry.name);
2518
+ if (entry.isDirectory()) {
2519
+ stack.push(abs);
2520
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2521
+ mdFiles.push(abs);
2522
+ }
2523
+ }
2524
+ }
2525
+ const stableIdToFiles = /* @__PURE__ */ new Map();
2526
+ 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;
2527
+ for (const absPath of mdFiles) {
2528
+ let source;
2529
+ try {
2530
+ source = await readFile7(absPath, "utf8");
2531
+ } catch {
2532
+ continue;
2533
+ }
2534
+ const match = DECLARED_ID_PATTERN.exec(source);
2535
+ if (match === null) {
2536
+ continue;
2537
+ }
2538
+ const stableId = match[1];
2539
+ const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2540
+ const existing = stableIdToFiles.get(stableId) ?? [];
2541
+ existing.push(relPath);
2542
+ stableIdToFiles.set(stableId, existing);
2543
+ }
2544
+ const collisions = [];
2545
+ for (const [stable_id, files] of stableIdToFiles) {
2546
+ if (files.length > 1) {
2547
+ collisions.push({ stable_id, files: files.sort() });
2548
+ }
2549
+ }
2550
+ return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
2551
+ }
2552
+ function createStableIdCollisionCheck(inspection) {
2553
+ if (inspection.collisions.length > 0) {
2554
+ const first = inspection.collisions[0];
2555
+ 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(", ")}.`;
2556
+ return issueCheck(
2557
+ "Stable ID collision",
2558
+ "warn",
2559
+ "warning",
2560
+ "stable_id_collision",
2561
+ `${detail} Edit one of the rule files to use a unique stable_id.`,
2562
+ "Edit one of the colliding rule files to declare a different `<!-- fab:rule-id X -->` value."
2563
+ );
2564
+ }
2565
+ return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/rules/.");
2566
+ }
2567
+ function createMetaManuallyDivergedCheck(inspection) {
2568
+ if (!inspection.readable) {
2569
+ return okCheck("Meta manual divergence", "agents.meta.json not readable; skipping divergence check.");
2570
+ }
2571
+ if (inspection.extraMetaEntries.length > 0) {
2572
+ return issueCheck(
2573
+ "Meta manual divergence",
2574
+ "warn",
2575
+ "warning",
2576
+ "meta_manually_diverged",
2577
+ `agents.meta.json has ${inspection.extraMetaEntries.length} entr${inspection.extraMetaEntries.length === 1 ? "y" : "ies"} with no backing file on disk. Run --fix to reconcile.`,
2578
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the rule files currently on disk."
2579
+ );
2580
+ }
2581
+ if (inspection.hashMismatchEntries.length > 0) {
2582
+ return issueCheck(
2583
+ "Meta manual divergence",
2584
+ "warn",
2585
+ "warning",
2586
+ "meta_manually_diverged",
2587
+ `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.`,
2588
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule file contents."
2589
+ );
2590
+ }
2591
+ return okCheck("Meta manual divergence", "agents.meta.json is consistent with rule files on disk.");
2592
+ }
2593
+ function inspectPreexistingRootFiles(projectRoot) {
2594
+ const candidates = ["CLAUDE.md", "AGENTS.md"];
2595
+ const detected = candidates.filter((name) => existsSync4(join8(projectRoot, name)));
2596
+ return { detected };
2597
+ }
2598
+ function createPreexistingRootFilesCheck(inspection) {
2599
+ if (inspection.detected.length === 0) {
2600
+ return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
2601
+ }
2602
+ return {
2603
+ name: "Preexisting root markdown",
2604
+ status: "ok",
2605
+ kind: "info",
2606
+ code: "preexisting_root_claude_md",
2607
+ fixable: false,
2608
+ message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
2609
+ actionHint: "Move rule content to `.fabric/rules/` if you want it available in MCP responses."
2610
+ };
2611
+ }
2612
+ function inspectClaudeSkillLegacyPath(projectRoot) {
2613
+ const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2614
+ const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2615
+ const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
2616
+ return { hasLegacy, legacyPath, newPath };
2617
+ }
2618
+ function createClaudeSkillLegacyPathCheck(inspection) {
2619
+ if (inspection.hasLegacy) {
2620
+ return issueCheck(
2621
+ "Claude skill path",
2622
+ "error",
2623
+ "fixable_error",
2624
+ "claude_skill_legacy_path",
2625
+ `.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).`,
2626
+ "Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
2627
+ );
2628
+ }
2629
+ return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
2630
+ }
2631
+ async function fixClaudeSkillLegacyPath(projectRoot) {
2632
+ const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2633
+ const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2634
+ if (!existsSync4(legacyPath)) {
2635
+ return;
2636
+ }
2637
+ mkdirSync(join8(newPath, ".."), { recursive: true });
2638
+ renameSync(legacyPath, newPath);
2639
+ const legacyDir = join8(legacyPath, "..");
2640
+ try {
2641
+ rmdirSync(legacyDir);
2642
+ } catch {
2643
+ }
2644
+ await appendEventLedgerEvent(projectRoot, {
2645
+ event_type: "claude_skill_path_migrated",
2646
+ from: legacyPath,
2647
+ to: newPath
2648
+ });
2649
+ }
2650
+ var LEGACY_HOOK_FILENAME = "agents-md-init-reminder.cjs";
2651
+ var NEW_HOOK_FILENAME = "fabric-init-reminder.cjs";
2652
+ function inspectClaudeHookLegacyPath(projectRoot) {
2653
+ const legacyHookPath = join8(projectRoot, ".claude", "hooks", LEGACY_HOOK_FILENAME);
2654
+ const newHookPath = join8(projectRoot, ".claude", "hooks", NEW_HOOK_FILENAME);
2655
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
2656
+ const hasLegacyFile = existsSync4(legacyHookPath);
2657
+ let hasLegacySettingsCommand = false;
2658
+ if (existsSync4(settingsPath)) {
2659
+ try {
2660
+ const raw = readFileSync(settingsPath, "utf8");
2661
+ hasLegacySettingsCommand = raw.includes(LEGACY_HOOK_FILENAME);
2662
+ } catch {
2663
+ }
2664
+ }
2665
+ return { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath };
2666
+ }
2667
+ function createClaudeHookLegacyPathCheck(inspection) {
2668
+ if (inspection.hasLegacyFile || inspection.hasLegacySettingsCommand) {
2669
+ return issueCheck(
2670
+ "Claude hook path",
2671
+ "error",
2672
+ "fixable_error",
2673
+ "claude_hook_legacy_path",
2674
+ `.claude/hooks/${LEGACY_HOOK_FILENAME} (or its reference in .claude/settings.json) exists at the legacy path. Run --fix to migrate to ${NEW_HOOK_FILENAME}.`,
2675
+ `Run \`fab doctor --fix\` to rename ${LEGACY_HOOK_FILENAME} to ${NEW_HOOK_FILENAME} and update .claude/settings.json hook commands.`
2676
+ );
2677
+ }
2678
+ return okCheck("Claude hook path", `.claude/hooks/${NEW_HOOK_FILENAME} is at the canonical path (or not present).`);
2679
+ }
2680
+ async function fixClaudeHookLegacyPath(projectRoot) {
2681
+ const { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath } = inspectClaudeHookLegacyPath(projectRoot);
2682
+ if (hasLegacyFile) {
2683
+ if (existsSync4(newHookPath)) {
2684
+ unlinkSync(legacyHookPath);
2685
+ } else {
2686
+ mkdirSync(join8(newHookPath, ".."), { recursive: true });
2687
+ renameSync(legacyHookPath, newHookPath);
2688
+ }
2689
+ }
2690
+ if (hasLegacySettingsCommand) {
2691
+ try {
2692
+ const raw = readFileSync(settingsPath, "utf8");
2693
+ const updated = raw.split(LEGACY_HOOK_FILENAME).join(NEW_HOOK_FILENAME);
2694
+ if (updated !== raw) {
2695
+ const parsed = JSON.parse(updated);
2696
+ await atomicWriteJson2(settingsPath, parsed);
2697
+ }
2698
+ } catch {
2699
+ }
2700
+ }
2701
+ if (hasLegacyFile || hasLegacySettingsCommand) {
2702
+ await appendEventLedgerEvent(projectRoot, {
2703
+ event_type: "claude_hook_path_migrated",
2704
+ from: legacyHookPath,
2705
+ to: newHookPath
2706
+ });
2707
+ }
2708
+ }
2709
+ function inspectLegacyClientPaths(projectRoot) {
2710
+ const configPath = join8(projectRoot, "fabric.config.json");
2711
+ if (!existsSync4(configPath)) {
2712
+ return { presentKeys: [] };
2713
+ }
2714
+ try {
2715
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2716
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2717
+ return { presentKeys: [] };
2718
+ }
2719
+ const config = parsed;
2720
+ const clientPaths = config.clientPaths;
2721
+ if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2722
+ return { presentKeys: [] };
2723
+ }
2724
+ const cp = clientPaths;
2725
+ const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
2726
+ return { presentKeys };
2727
+ } catch {
2728
+ return { presentKeys: [] };
2729
+ }
2730
+ }
2731
+ function createLegacyClientPathCheck(inspection) {
2732
+ if (inspection.presentKeys.length > 0) {
2733
+ return issueCheck(
2734
+ "Legacy client paths",
2735
+ "warn",
2736
+ "warning",
2737
+ "legacy_client_path_present",
2738
+ `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.`,
2739
+ "Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
2740
+ );
2741
+ }
2742
+ return okCheck("Legacy client paths", "No deprecated clientPaths keys found in fabric.config.json.");
2743
+ }
2744
+ async function fixLegacyClientPaths(projectRoot) {
2745
+ const configPath = join8(projectRoot, "fabric.config.json");
2746
+ if (!existsSync4(configPath)) {
2747
+ return;
2748
+ }
2749
+ let config;
2750
+ try {
2751
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2752
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2753
+ return;
2754
+ }
2755
+ config = parsed;
2756
+ } catch {
2757
+ return;
2758
+ }
2759
+ const clientPaths = config.clientPaths;
2760
+ if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2761
+ return;
2762
+ }
2763
+ const cp = clientPaths;
2764
+ const removed = [];
2765
+ for (const key of LEGACY_CLIENT_PATH_KEYS) {
2766
+ if (key in cp) {
2767
+ delete cp[key];
2768
+ removed.push(key);
2769
+ }
2770
+ }
2771
+ if (removed.length === 0) {
2772
+ return;
2773
+ }
2774
+ const updatedConfig = { ...config, clientPaths: cp };
2775
+ await atomicWriteJson2(configPath, updatedConfig, { indent: 2 });
2776
+ await appendEventLedgerEvent(projectRoot, {
2777
+ event_type: "legacy_client_path_present",
2778
+ removed
2779
+ });
2780
+ }
2781
+ async function fixMcpConfigInWrongFile(projectRoot) {
2782
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
2783
+ if (!existsSync4(settingsPath)) {
2784
+ return;
2785
+ }
2786
+ let settings;
2787
+ try {
2788
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2789
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2790
+ return;
2791
+ }
2792
+ settings = parsed;
2793
+ } catch {
2794
+ return;
2795
+ }
2796
+ const mcpServers = settings.mcpServers;
2797
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2798
+ return;
2799
+ }
2800
+ const { fabric: _removed, ...remainingServers } = mcpServers;
2801
+ const cleaned = { ...settings };
2802
+ if (Object.keys(remainingServers).length === 0) {
2803
+ delete cleaned.mcpServers;
2804
+ } else {
2805
+ cleaned.mcpServers = remainingServers;
2806
+ }
2807
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2808
+ await appendEventLedgerEvent(projectRoot, {
2809
+ event_type: "mcp_config_migrated",
2810
+ source: "doctor_fix",
2811
+ removed_from: ".claude/settings.json"
2812
+ });
2813
+ }
1930
2814
  async function writeDefaultBootstrap(projectRoot) {
1931
- const path = join7(projectRoot, ".fabric", "bootstrap", "README.md");
2815
+ const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
1932
2816
  await ensureParentDirectory(path);
1933
- await atomicWriteText(path, "# Fabric Bootstrap\n\nProject-specific Fabric bootstrap notes.\n");
2817
+ await atomicWriteText3(path, buildBootstrapContent(projectRoot));
1934
2818
  }
1935
2819
  async function ensureEventLedger(projectRoot) {
1936
2820
  const path = getEventLedgerPath(projectRoot);
@@ -1942,9 +2826,9 @@ function createFixMessage(fixed, report) {
1942
2826
  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
2827
  return `${fixedText} ${manualText}`;
1944
2828
  }
1945
- function findRuleFiles(projectRoot) {
1946
- const rulesRoot = join7(projectRoot, ".fabric", "rules");
1947
- if (!existsSync2(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
2829
+ function findRuleFiles2(projectRoot) {
2830
+ const rulesRoot = join8(projectRoot, ".fabric", "rules");
2831
+ if (!existsSync4(rulesRoot) || !statSync3(rulesRoot).isDirectory()) {
1948
2832
  return [];
1949
2833
  }
1950
2834
  const files = [];
@@ -1955,7 +2839,7 @@ function findRuleFiles(projectRoot) {
1955
2839
  continue;
1956
2840
  }
1957
2841
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1958
- const absolutePath = join7(current, entry.name);
2842
+ const absolutePath = join8(current, entry.name);
1959
2843
  const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
1960
2844
  if (entry.isDirectory()) {
1961
2845
  stack.push(absolutePath);
@@ -1981,7 +2865,7 @@ function normalizePath(path) {
1981
2865
  return posix2.normalize(path.split("\\").join("/"));
1982
2866
  }
1983
2867
  function collectEntryPoints(root) {
1984
- if (!existsSync2(root) || !statSync2(root).isDirectory()) {
2868
+ if (!existsSync4(root) || !statSync3(root).isDirectory()) {
1985
2869
  return [];
1986
2870
  }
1987
2871
  const entries = [];
@@ -1992,7 +2876,7 @@ function collectEntryPoints(root) {
1992
2876
  continue;
1993
2877
  }
1994
2878
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1995
- const absolutePath = join7(current, entry.name);
2879
+ const absolutePath = join8(current, entry.name);
1996
2880
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
1997
2881
  if (relativePath.length === 0) {
1998
2882
  continue;
@@ -2052,8 +2936,6 @@ function isMissingFileError(error) {
2052
2936
  export {
2053
2937
  AGENTS_MD_RESOURCE_URI,
2054
2938
  contextCache,
2055
- AgentsMetaFileMissingError,
2056
- AgentsMetaInvalidError,
2057
2939
  resolveProjectRoot,
2058
2940
  readAgentsMeta,
2059
2941
  LEDGER_PATH,
@@ -2066,10 +2948,7 @@ export {
2066
2948
  isNodeError,
2067
2949
  appendEventLedgerEvent,
2068
2950
  readEventLedger,
2069
- getRules,
2070
- planContext,
2071
- RULE_SECTION_NAMES,
2072
- getRuleSections,
2951
+ flushAndSyncEventLedger,
2073
2952
  buildRuleMeta,
2074
2953
  writeRuleMeta,
2075
2954
  computeRulesBasedAgentsMeta,
@@ -2078,6 +2957,12 @@ export {
2078
2957
  deriveRuleMetaTopologyType,
2079
2958
  isSameRuleTestIndex,
2080
2959
  stableStringify,
2960
+ invalidateRuleSyncCooldown,
2961
+ ensureRulesFresh,
2962
+ reconcileRules,
2963
+ getRules,
2964
+ planContext,
2965
+ getRuleSections,
2081
2966
  runDoctorReport,
2082
2967
  runDoctorFix
2083
2968
  };