@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.
- package/dist/{chunk-PTFSYO4Y.js → chunk-EGGZFXMO.js} +1051 -166
- package/dist/{http-6LFZLHCN.js → http-Q7GIL23Y.js} +82 -83
- package/dist/index.d.ts +147 -3
- package/dist/index.js +329 -140
- package/dist/static/assets/index-DeTFBeTM.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +10 -4
- package/dist/static/assets/index-DEc8gkD_.js +0 -10
|
@@ -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
|
|
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
|
|
158
|
+
return join2(projectRoot, LEDGER_PATH);
|
|
102
159
|
}
|
|
103
160
|
function getLegacyLedgerPath(projectRoot) {
|
|
104
|
-
return
|
|
161
|
+
return join2(projectRoot, LEGACY_LEDGER_PATH);
|
|
105
162
|
}
|
|
106
163
|
function getEventLedgerPath(projectRoot) {
|
|
107
|
-
return
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
176
|
-
import { existsSync, statSync } from "fs";
|
|
177
|
-
import { isAbsolute, join as
|
|
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 =
|
|
190
|
-
const ruleTestIndexPath =
|
|
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 =
|
|
204
|
-
const ruleTestIndexPath =
|
|
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
|
|
320
|
+
await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
212
321
|
`);
|
|
213
|
-
await
|
|
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(
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
361
|
-
if (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
517
|
-
if (!
|
|
625
|
+
const bootstrapPath = join3(projectRoot, contentRef);
|
|
626
|
+
if (!existsSync2(bootstrapPath)) {
|
|
518
627
|
return void 0;
|
|
519
628
|
}
|
|
520
|
-
const hash = sha256(await
|
|
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/
|
|
726
|
-
import {
|
|
727
|
-
import {
|
|
728
|
-
import {
|
|
729
|
-
import {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
|
769
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
777
|
-
|
|
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
|
-
|
|
780
|
-
} catch
|
|
781
|
-
if (
|
|
782
|
-
|
|
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
|
-
|
|
960
|
+
return { event: null, warning: null };
|
|
785
961
|
}
|
|
786
|
-
let
|
|
962
|
+
let content;
|
|
787
963
|
try {
|
|
788
|
-
|
|
789
|
-
} catch
|
|
790
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
|
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 =
|
|
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
|
|
903
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1543
|
-
const
|
|
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,
|
|
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) => [
|
|
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
|
|
1994
|
+
await reconcileRules(projectRoot, { trigger: "doctor" });
|
|
1601
1995
|
for (const issue of before.fixable_errors.filter(
|
|
1602
|
-
(candidate) => [
|
|
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 =
|
|
2047
|
+
const path = join8(projectRoot, ".fabric", "forensic.json");
|
|
1620
2048
|
try {
|
|
1621
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
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 =
|
|
2059
|
+
const path = join8(projectRoot, ".fabric", "init-context.json");
|
|
1632
2060
|
try {
|
|
1633
|
-
JSON.parse(await
|
|
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 =
|
|
2092
|
+
const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
|
|
1644
2093
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
1645
2094
|
try {
|
|
1646
|
-
const raw = await
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
2206
|
+
const files = findRuleFiles2(projectRoot);
|
|
1751
2207
|
for (const file of files) {
|
|
1752
2208
|
try {
|
|
1753
|
-
parseRuleSections(await
|
|
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 =
|
|
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
|
|
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
|
-
"
|
|
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 =
|
|
2815
|
+
const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
|
|
1932
2816
|
await ensureParentDirectory(path);
|
|
1933
|
-
await
|
|
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
|
|
1946
|
-
const rulesRoot =
|
|
1947
|
-
if (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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
|
};
|