@fenglimg/fabric-server 2.3.0-rc.11 → 2.3.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +18 -8
- package/dist/index.js +479 -203
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { existsSync as
|
|
2
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3
3
|
import { readFile as readFile22 } from "fs/promises";
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join28, resolve as resolve4 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -10,8 +10,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
10
10
|
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
11
11
|
|
|
12
12
|
// src/meta-reader.ts
|
|
13
|
+
import { existsSync } from "fs";
|
|
13
14
|
import { readFile } from "fs/promises";
|
|
14
|
-
import { join } from "path";
|
|
15
|
+
import { dirname, join } from "path";
|
|
15
16
|
import { agentsMetaSchema } from "@fenglimg/fabric-shared";
|
|
16
17
|
import { IOFabricError } from "@fenglimg/fabric-shared/errors";
|
|
17
18
|
|
|
@@ -98,8 +99,22 @@ var contextCache = new ContextCache(5e3);
|
|
|
98
99
|
|
|
99
100
|
// src/meta-reader.ts
|
|
100
101
|
import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
|
|
101
|
-
function resolveProjectRoot() {
|
|
102
|
-
|
|
102
|
+
function resolveProjectRoot(startCwd) {
|
|
103
|
+
const envOverride = process.env.FABRIC_PROJECT_ROOT;
|
|
104
|
+
if (typeof envOverride === "string" && envOverride.length > 0) return envOverride;
|
|
105
|
+
const claudeRoot = process.env.CLAUDE_PROJECT_DIR;
|
|
106
|
+
if (typeof claudeRoot === "string" && claudeRoot.length > 0) return claudeRoot;
|
|
107
|
+
const start = typeof startCwd === "string" && startCwd.length > 0 ? startCwd : process.cwd();
|
|
108
|
+
let dir = start;
|
|
109
|
+
let firstFabric = null;
|
|
110
|
+
for (let i = 0; i < 64; i++) {
|
|
111
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
112
|
+
if (firstFabric === null && existsSync(join(dir, ".fabric"))) firstFabric = dir;
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir) break;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
return firstFabric ?? start;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
// src/services/event-ledger.ts
|
|
@@ -107,7 +122,7 @@ import { randomUUID } from "crypto";
|
|
|
107
122
|
import {
|
|
108
123
|
closeSync,
|
|
109
124
|
createReadStream,
|
|
110
|
-
existsSync,
|
|
125
|
+
existsSync as existsSync2,
|
|
111
126
|
fsyncSync,
|
|
112
127
|
openSync,
|
|
113
128
|
readFileSync,
|
|
@@ -127,7 +142,7 @@ import {
|
|
|
127
142
|
} from "@fenglimg/fabric-shared/node/atomic-write";
|
|
128
143
|
|
|
129
144
|
// src/services/_shared.ts
|
|
130
|
-
import { dirname, join as join2, resolve, sep } from "path";
|
|
145
|
+
import { dirname as dirname2, join as join2, resolve, sep } from "path";
|
|
131
146
|
import { createHash } from "crypto";
|
|
132
147
|
import { mkdir } from "fs/promises";
|
|
133
148
|
import { PathEscapeError } from "@fenglimg/fabric-shared/errors";
|
|
@@ -165,7 +180,7 @@ function extractBody(content) {
|
|
|
165
180
|
return content.slice(match[0].length);
|
|
166
181
|
}
|
|
167
182
|
async function ensureParentDirectory(path) {
|
|
168
|
-
await mkdir(
|
|
183
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
169
184
|
}
|
|
170
185
|
function sha256(content) {
|
|
171
186
|
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
@@ -606,7 +621,7 @@ function resolveRetentionDays(projectRoot, override) {
|
|
|
606
621
|
}
|
|
607
622
|
const configPath = join3(projectRoot, ".fabric", "fabric-config.json");
|
|
608
623
|
try {
|
|
609
|
-
if (
|
|
624
|
+
if (existsSync2(configPath)) {
|
|
610
625
|
const raw = readFileSync(configPath, "utf8");
|
|
611
626
|
const parsed = JSON.parse(raw);
|
|
612
627
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
@@ -628,7 +643,7 @@ function formatUtcDate(date) {
|
|
|
628
643
|
}
|
|
629
644
|
function flushAndSyncEventLedger(projectRoot) {
|
|
630
645
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
631
|
-
if (!
|
|
646
|
+
if (!existsSync2(ledgerPath)) return;
|
|
632
647
|
const fd = openSync(ledgerPath, "r+");
|
|
633
648
|
try {
|
|
634
649
|
fsyncSync(fd);
|
|
@@ -700,14 +715,14 @@ function appendPayloadWarning(warnings, guardResult, actionHint) {
|
|
|
700
715
|
}
|
|
701
716
|
|
|
702
717
|
// src/config-loader.ts
|
|
703
|
-
import { existsSync as
|
|
718
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
704
719
|
import { join as join4 } from "path";
|
|
705
720
|
import { selectionTokenTtlMsSchema, planContextTopKSchema } from "@fenglimg/fabric-shared";
|
|
706
721
|
var PLAN_CONTEXT_TOP_K_DEFAULT = 24;
|
|
707
722
|
var RECALL_RELEVANCE_RATIO_DEFAULT = 0.25;
|
|
708
723
|
function readFabricConfig(projectRoot) {
|
|
709
724
|
const configPath = join4(projectRoot, ".fabric", "fabric-config.json");
|
|
710
|
-
if (!
|
|
725
|
+
if (!existsSync3(configPath)) {
|
|
711
726
|
return {};
|
|
712
727
|
}
|
|
713
728
|
const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
@@ -887,7 +902,7 @@ function readFusion(projectRoot) {
|
|
|
887
902
|
function readConflictLintThreshold(projectRoot) {
|
|
888
903
|
try {
|
|
889
904
|
const cfgPath = join4(projectRoot, ".fabric", "fabric-config.json");
|
|
890
|
-
if (!
|
|
905
|
+
if (!existsSync3(cfgPath)) return void 0;
|
|
891
906
|
const parsed = JSON.parse(readFileSync2(cfgPath, "utf8"));
|
|
892
907
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
893
908
|
const v = parsed.conflict_lint_similarity_threshold;
|
|
@@ -950,7 +965,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
|
950
965
|
}
|
|
951
966
|
|
|
952
967
|
// src/services/extract-knowledge.ts
|
|
953
|
-
import { existsSync as
|
|
968
|
+
import { existsSync as existsSync4 } from "fs";
|
|
954
969
|
import { readFile as readFile5 } from "fs/promises";
|
|
955
970
|
import { join as join7 } from "path";
|
|
956
971
|
import {
|
|
@@ -1340,6 +1355,7 @@ async function readSetFingerprint(refs) {
|
|
|
1340
1355
|
return parts.sort().join("\n");
|
|
1341
1356
|
}
|
|
1342
1357
|
var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
1358
|
+
var DEPRECATED_LINE = /^deprecated:\s*true\s*$/mu;
|
|
1343
1359
|
function readSemanticScope(source, layer, project) {
|
|
1344
1360
|
if (layer === "personal") {
|
|
1345
1361
|
return "personal";
|
|
@@ -1359,6 +1375,9 @@ function filterByActiveProject(entries, activeProject) {
|
|
|
1359
1375
|
(e) => scopeRoot(e.semanticScope) !== "project" || e.semanticScope === current
|
|
1360
1376
|
);
|
|
1361
1377
|
}
|
|
1378
|
+
function filterOutDeprecated(entries) {
|
|
1379
|
+
return entries.filter((entry) => !entry.deprecated);
|
|
1380
|
+
}
|
|
1362
1381
|
function activeProjectOf(projectRoot) {
|
|
1363
1382
|
const ap = loadProjectConfig2(projectRoot)?.active_project;
|
|
1364
1383
|
return ap !== void 0 && ap.length > 0 ? ap : void 0;
|
|
@@ -1426,6 +1445,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1426
1445
|
alias: ref.alias,
|
|
1427
1446
|
layer,
|
|
1428
1447
|
semanticScope: readSemanticScope(source, layer, ref.project),
|
|
1448
|
+
deprecated: DEPRECATED_LINE.test(source),
|
|
1429
1449
|
source
|
|
1430
1450
|
};
|
|
1431
1451
|
}));
|
|
@@ -1434,7 +1454,9 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1434
1454
|
async function buildCrossStoreRawItems(projectRoot) {
|
|
1435
1455
|
const items = [];
|
|
1436
1456
|
const activeProject = activeProjectOf(projectRoot);
|
|
1437
|
-
for (const entry of
|
|
1457
|
+
for (const entry of filterOutDeprecated(
|
|
1458
|
+
filterByActiveProject(await walkReadSetStores(projectRoot), activeProject)
|
|
1459
|
+
)) {
|
|
1438
1460
|
const baseDescription = extractRuleDescription(entry.source);
|
|
1439
1461
|
if (baseDescription === void 0) {
|
|
1440
1462
|
continue;
|
|
@@ -1474,7 +1496,7 @@ async function buildKnowledgeCensus(projectRoot) {
|
|
|
1474
1496
|
};
|
|
1475
1497
|
try {
|
|
1476
1498
|
const activeProject = activeProjectOf(projectRoot);
|
|
1477
|
-
const all = await walkReadSetStores(projectRoot);
|
|
1499
|
+
const all = filterOutDeprecated(await walkReadSetStores(projectRoot));
|
|
1478
1500
|
const kept = filterByActiveProject(all, activeProject);
|
|
1479
1501
|
census.dropped_other_project = all.length - kept.length;
|
|
1480
1502
|
for (const entry of kept) {
|
|
@@ -1503,9 +1525,8 @@ async function buildAlwaysActiveBodies(projectRoot) {
|
|
|
1503
1525
|
const out = [];
|
|
1504
1526
|
try {
|
|
1505
1527
|
const activeProject = activeProjectOf(projectRoot);
|
|
1506
|
-
for (const entry of
|
|
1507
|
-
await walkReadSetStores(projectRoot),
|
|
1508
|
-
activeProject
|
|
1528
|
+
for (const entry of filterOutDeprecated(
|
|
1529
|
+
filterByActiveProject(await walkReadSetStores(projectRoot), activeProject)
|
|
1509
1530
|
)) {
|
|
1510
1531
|
const desc = extractRuleDescription(entry.source);
|
|
1511
1532
|
if (desc === void 0) continue;
|
|
@@ -2223,7 +2244,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
2223
2244
|
const effectiveIdempotencyKey = chosenKey;
|
|
2224
2245
|
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
2225
2246
|
await ensureParentDirectory(absolutePath);
|
|
2226
|
-
if (
|
|
2247
|
+
if (existsSync4(absolutePath)) {
|
|
2227
2248
|
const existing = await readFile5(absolutePath, "utf8");
|
|
2228
2249
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2229
2250
|
if (existingKey === effectiveIdempotencyKey) {
|
|
@@ -2350,7 +2371,7 @@ async function resolveDisambiguatedSlugPath(args) {
|
|
|
2350
2371
|
slug: candidateSlug
|
|
2351
2372
|
})
|
|
2352
2373
|
);
|
|
2353
|
-
if (!
|
|
2374
|
+
if (!existsSync4(candidatePath)) {
|
|
2354
2375
|
return {
|
|
2355
2376
|
absolutePath: candidatePath,
|
|
2356
2377
|
sanitizedSlug: candidateSlug,
|
|
@@ -2589,11 +2610,11 @@ function readFrontmatterKey(content, key) {
|
|
|
2589
2610
|
}
|
|
2590
2611
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
2591
2612
|
const line = rawLine.trim();
|
|
2592
|
-
const
|
|
2593
|
-
if (
|
|
2594
|
-
const k = line.slice(0,
|
|
2613
|
+
const sep6 = line.indexOf(":");
|
|
2614
|
+
if (sep6 === -1) continue;
|
|
2615
|
+
const k = line.slice(0, sep6).trim();
|
|
2595
2616
|
if (k === key) {
|
|
2596
|
-
return line.slice(
|
|
2617
|
+
return line.slice(sep6 + 1).trim();
|
|
2597
2618
|
}
|
|
2598
2619
|
}
|
|
2599
2620
|
return void 0;
|
|
@@ -2605,6 +2626,58 @@ async function emitEventBestEffort(projectRoot, event) {
|
|
|
2605
2626
|
}
|
|
2606
2627
|
}
|
|
2607
2628
|
|
|
2629
|
+
// src/services/doctor-unbound-project.ts
|
|
2630
|
+
import { loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
|
|
2631
|
+
function detectUnboundProject(projectRoot) {
|
|
2632
|
+
const config = loadProjectConfig3(projectRoot);
|
|
2633
|
+
const alias = config?.active_write_store;
|
|
2634
|
+
if (typeof alias !== "string" || alias.length === 0) {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
const missing = [];
|
|
2638
|
+
if (typeof config?.project_id !== "string" || config.project_id.length === 0) {
|
|
2639
|
+
missing.push("project_id");
|
|
2640
|
+
}
|
|
2641
|
+
if (typeof config?.active_project !== "string" || config.active_project.length === 0) {
|
|
2642
|
+
missing.push("active_project");
|
|
2643
|
+
}
|
|
2644
|
+
return missing.length > 0 ? { alias, missing } : null;
|
|
2645
|
+
}
|
|
2646
|
+
function createUnboundProjectCheck(t, violation) {
|
|
2647
|
+
if (violation === null) {
|
|
2648
|
+
return {
|
|
2649
|
+
name: t("doctor.check.unbound_project.name"),
|
|
2650
|
+
status: "ok",
|
|
2651
|
+
message: t("doctor.check.unbound_project.ok")
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
return {
|
|
2655
|
+
name: t("doctor.check.unbound_project.name"),
|
|
2656
|
+
status: "warn",
|
|
2657
|
+
kind: "warning",
|
|
2658
|
+
code: "unbound_project",
|
|
2659
|
+
fixable: false,
|
|
2660
|
+
message: t("doctor.check.unbound_project.message", {
|
|
2661
|
+
alias: violation.alias,
|
|
2662
|
+
missing: violation.missing.join(" + ")
|
|
2663
|
+
}),
|
|
2664
|
+
actionHint: t("doctor.check.unbound_project.remediation")
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/services/write-scope-warning.ts
|
|
2669
|
+
function unsealedProjectScopeWarning(projectRoot) {
|
|
2670
|
+
const violation = detectUnboundProject(projectRoot);
|
|
2671
|
+
if (violation === null) {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
code: "project_scope_unsealed",
|
|
2676
|
+
file: "<response>",
|
|
2677
|
+
action_hint: `Write store '${violation.alias}' is bound but this repo is missing ${violation.missing.join(", ")} \u2014 team-layer entries will land flat under knowledge/<type>/ (semantic_scope: team) instead of the project-partitioned knowledge/projects/<id>/<type>/. Run \`fabric doctor --fix\` to seal the project coordinate, or ignore if this is genuinely cross-project team knowledge.`
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2608
2681
|
// src/tools/extract-knowledge.ts
|
|
2609
2682
|
function registerExtractKnowledge(server, tracker) {
|
|
2610
2683
|
server.registerTool(
|
|
@@ -2642,6 +2715,10 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
2642
2715
|
if (gateWarn) {
|
|
2643
2716
|
response.warnings = [gateWarn];
|
|
2644
2717
|
}
|
|
2718
|
+
const scopeWarn = unsealedProjectScopeWarning(projectRoot);
|
|
2719
|
+
if (scopeWarn) {
|
|
2720
|
+
response.warnings = [...response.warnings ?? [], scopeWarn];
|
|
2721
|
+
}
|
|
2645
2722
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
2646
2723
|
const serialized = JSON.stringify(response);
|
|
2647
2724
|
const guardResult = enforcePayloadLimit(serialized, payloadLimits);
|
|
@@ -3271,10 +3348,10 @@ async function planContext(projectRoot, input) {
|
|
|
3271
3348
|
});
|
|
3272
3349
|
}
|
|
3273
3350
|
const topKIds = new Set(topKCandidates.map((item) => item.stable_id));
|
|
3274
|
-
const
|
|
3275
|
-
const omittedCandidateCount = retrievalDropped.length;
|
|
3351
|
+
const retrievalDroppedInitial = rankedCandidates.filter((item) => !topKIds.has(item.stable_id)).map((item) => ({ id: item.stable_id, reason: "retrieval_budget" }));
|
|
3276
3352
|
let candidates = topKCandidates;
|
|
3277
3353
|
const relatedAppended = {};
|
|
3354
|
+
const appendedIds = /* @__PURE__ */ new Set();
|
|
3278
3355
|
if (input.include_related === true) {
|
|
3279
3356
|
const inTopK = new Set(topKCandidates.flatMap((item) => relatedLookupKeys(item.stable_id)));
|
|
3280
3357
|
const rankedById = /* @__PURE__ */ new Map();
|
|
@@ -3286,7 +3363,6 @@ async function planContext(projectRoot, input) {
|
|
|
3286
3363
|
}
|
|
3287
3364
|
}
|
|
3288
3365
|
const appended = [];
|
|
3289
|
-
const appendedIds = /* @__PURE__ */ new Set();
|
|
3290
3366
|
for (const surfaced of topKCandidates) {
|
|
3291
3367
|
for (const rel of surfaced.description.related ?? []) {
|
|
3292
3368
|
if (inTopK.has(rel)) continue;
|
|
@@ -3302,6 +3378,8 @@ async function planContext(projectRoot, input) {
|
|
|
3302
3378
|
candidates = [...topKCandidates, ...appended];
|
|
3303
3379
|
}
|
|
3304
3380
|
}
|
|
3381
|
+
const retrievalDropped = retrievalDroppedInitial.filter((d) => !appendedIds.has(d.id));
|
|
3382
|
+
const omittedCandidateCount = retrievalDropped.length;
|
|
3305
3383
|
const entries = uniquePaths.map((path) => ({
|
|
3306
3384
|
path,
|
|
3307
3385
|
requirement_profile: buildRequirementProfile(path, input)
|
|
@@ -3882,7 +3960,6 @@ function relatedLookupKeys(stableId) {
|
|
|
3882
3960
|
}
|
|
3883
3961
|
|
|
3884
3962
|
// src/services/recall.ts
|
|
3885
|
-
var RECALL_DIRECTIVE = "These entries are auto-accounted as citations for edits whose paths overlap this recall \u2014 no first-line cite needed. Speak up only to dismiss one you judge inapplicable: `dismissed: <id> (<reason>)`.";
|
|
3886
3963
|
async function recall(projectRoot, input) {
|
|
3887
3964
|
const planResult = await planContext(projectRoot, input);
|
|
3888
3965
|
const {
|
|
@@ -3938,25 +4015,51 @@ async function recall(projectRoot, input) {
|
|
|
3938
4015
|
}
|
|
3939
4016
|
const nextSteps = buildNextSteps(planResult, paths, candidateById, candidateLookup);
|
|
3940
4017
|
const pathByStableId = new Map(paths.map((p) => [p.stable_id, p]));
|
|
3941
|
-
const entries = planRest.candidates.map((c
|
|
4018
|
+
const entries = planRest.candidates.map((c) => {
|
|
3942
4019
|
const readPath = pathByStableId.get(c.stable_id);
|
|
3943
4020
|
const scored = candidateScores?.get(c.stable_id);
|
|
3944
4021
|
return {
|
|
3945
4022
|
stable_id: c.stable_id,
|
|
3946
|
-
rank: index + 1,
|
|
3947
4023
|
description: slimDescription(c.description),
|
|
3948
4024
|
...readPath ? { read_path: readPath.path } : {},
|
|
3949
|
-
|
|
4025
|
+
// TASK-004: flatten { alias } → alias string on the wire.
|
|
4026
|
+
...readPath?.store ? { store_alias: readPath.store.alias } : {},
|
|
3950
4027
|
...isAlwaysActive(c) ? { body_in_context: true } : {},
|
|
3951
|
-
|
|
4028
|
+
// TASK-004: entry.score dropped (final===score invariant, KT-PIT-0036);
|
|
4029
|
+
// consumers read score_breakdown.final when opt-in enabled.
|
|
4030
|
+
// TASK-006: score_breakdown is now opt-in via include_score_breakdown —
|
|
4031
|
+
// steady-state recall omits it (~4.8KB saved on a 24-entry sample). The
|
|
4032
|
+
// plan-context service layer still writes candidate_scores unconditionally,
|
|
4033
|
+
// so the debug surface is available on demand without perturbing ranking.
|
|
4034
|
+
...scored && input.include_score_breakdown === true ? { score_breakdown: scored.score_breakdown } : {}
|
|
3952
4035
|
};
|
|
3953
4036
|
});
|
|
3954
|
-
const {
|
|
4037
|
+
const {
|
|
4038
|
+
entries: _reqProfiles,
|
|
4039
|
+
candidates: _candidates,
|
|
4040
|
+
stale: _stale,
|
|
4041
|
+
intent: _intent,
|
|
4042
|
+
dropped: droppedList,
|
|
4043
|
+
preflight_diagnostics: preflightList,
|
|
4044
|
+
...planRestNoLists
|
|
4045
|
+
} = planRest;
|
|
4046
|
+
const retrievalDroppedCount = (droppedList ?? []).filter(
|
|
4047
|
+
(d) => d.reason === "retrieval_budget"
|
|
4048
|
+
).length;
|
|
4049
|
+
const payloadDroppedCount = (droppedList ?? []).filter((d) => d.reason === "payload_budget").length;
|
|
4050
|
+
const droppedIds = (droppedList ?? []).map((d) => d.id);
|
|
3955
4051
|
return {
|
|
3956
4052
|
...planRestNoLists,
|
|
3957
4053
|
entries,
|
|
3958
|
-
|
|
3959
|
-
...
|
|
4054
|
+
...nextSteps.length > 0 ? { next_steps: nextSteps } : {},
|
|
4055
|
+
...droppedIds.length > 0 ? { dropped_ids: droppedIds } : {},
|
|
4056
|
+
...retrievalDroppedCount > 0 || payloadDroppedCount > 0 ? {
|
|
4057
|
+
dropped_reasons: {
|
|
4058
|
+
...retrievalDroppedCount > 0 ? { retrieval_budget: retrievalDroppedCount } : {},
|
|
4059
|
+
...payloadDroppedCount > 0 ? { payload_budget: payloadDroppedCount } : {}
|
|
4060
|
+
}
|
|
4061
|
+
} : {},
|
|
4062
|
+
...preflightList !== void 0 && preflightList.length > 0 ? { preflight_diagnostics: preflightList } : {}
|
|
3960
4063
|
};
|
|
3961
4064
|
}
|
|
3962
4065
|
var ALWAYS_ACTIVE_TYPES2 = /* @__PURE__ */ new Set(["models", "guidelines"]);
|
|
@@ -3967,8 +4070,8 @@ function isAlwaysActive(candidate) {
|
|
|
3967
4070
|
function slimDescription(d) {
|
|
3968
4071
|
return {
|
|
3969
4072
|
summary: d.summary,
|
|
3970
|
-
must_read_if: d.must_read_if,
|
|
3971
|
-
|
|
4073
|
+
...d.must_read_if !== d.summary ? { must_read_if: d.must_read_if } : {},
|
|
4074
|
+
...Array.isArray(d.impact) && d.impact.length > 0 ? { impact: d.impact } : {},
|
|
3972
4075
|
...d.knowledge_type !== void 0 ? { knowledge_type: d.knowledge_type } : {}
|
|
3973
4076
|
};
|
|
3974
4077
|
}
|
|
@@ -4010,7 +4113,7 @@ function registerRecall(server, tracker) {
|
|
|
4010
4113
|
server.registerTool(
|
|
4011
4114
|
"fab_recall",
|
|
4012
4115
|
{
|
|
4013
|
-
description: "Recall the Fabric knowledge relevant to the files you are about to touch. Pass candidate `paths` (+ optional `intent`) and receive a single ranked `entries[]` list \u2014
|
|
4116
|
+
description: "Recall the Fabric knowledge relevant to the files you are about to touch. Pass candidate `paths` (+ optional `intent`) and receive a single ranked `entries[]` list (best-first \u2014 array index IS the rank). Each entry carries `description.summary` (always) plus optional `description.must_read_if` (omitted when identical to summary), `description.impact` (\u26A0\uFE0F consequence hints), `description.knowledge_type`, `read_path` (on-disk file for the body), `store_alias`, and `body_in_context:true` when the body is already injected at SessionStart. Full frontmatter fields (intent_clues / tech_stack / related / tags / relevance_paths) are NOT on the wire \u2014 Read the `read_path` to load them. Pass `ids` to scope surfaced read_paths, `include_related:true` to append one-hop neighbours, `include_score_breakdown:true` for numeric ranking diagnostics.",
|
|
4014
4117
|
inputSchema: recallInputSchema,
|
|
4015
4118
|
outputSchema: recallOutputSchema,
|
|
4016
4119
|
annotations: recallAnnotations
|
|
@@ -4026,7 +4129,8 @@ function registerRecall(server, tracker) {
|
|
|
4026
4129
|
target_paths,
|
|
4027
4130
|
layer_filter,
|
|
4028
4131
|
ids,
|
|
4029
|
-
include_related
|
|
4132
|
+
include_related,
|
|
4133
|
+
include_score_breakdown
|
|
4030
4134
|
}) => {
|
|
4031
4135
|
const requestId = randomUUID3();
|
|
4032
4136
|
const startedAt = process.hrtime.bigint();
|
|
@@ -4050,7 +4154,8 @@ function registerRecall(server, tracker) {
|
|
|
4050
4154
|
// declared layer restriction instead of silently discarding it.
|
|
4051
4155
|
layer_filter,
|
|
4052
4156
|
ids,
|
|
4053
|
-
include_related
|
|
4157
|
+
include_related,
|
|
4158
|
+
include_score_breakdown
|
|
4054
4159
|
};
|
|
4055
4160
|
const result = await recall(projectRoot, input);
|
|
4056
4161
|
const response = {
|
|
@@ -4269,15 +4374,15 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
4269
4374
|
|
|
4270
4375
|
// src/services/review.ts
|
|
4271
4376
|
import { execFileSync } from "child_process";
|
|
4272
|
-
import { existsSync as
|
|
4377
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4273
4378
|
import { readFile as readFile9, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
4274
4379
|
import { homedir } from "os";
|
|
4275
4380
|
import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
4276
4381
|
|
|
4277
4382
|
// src/services/promotion-gate.ts
|
|
4278
4383
|
function toLocalId(id) {
|
|
4279
|
-
const
|
|
4280
|
-
return
|
|
4384
|
+
const sep6 = id.indexOf(":");
|
|
4385
|
+
return sep6 === -1 ? id : id.slice(sep6 + 1);
|
|
4281
4386
|
}
|
|
4282
4387
|
async function hasUnresolvedDismissal(projectRoot, id) {
|
|
4283
4388
|
const target = toLocalId(id);
|
|
@@ -4308,10 +4413,10 @@ async function hasUnresolvedDismissal(projectRoot, id) {
|
|
|
4308
4413
|
}
|
|
4309
4414
|
|
|
4310
4415
|
// src/services/review.ts
|
|
4311
|
-
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as
|
|
4416
|
+
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
|
|
4312
4417
|
|
|
4313
4418
|
// src/services/pending-dedupe.ts
|
|
4314
|
-
import { existsSync as
|
|
4419
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4315
4420
|
import { readdir, readFile as readFile8, unlink } from "fs/promises";
|
|
4316
4421
|
import { join as join11 } from "path";
|
|
4317
4422
|
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
@@ -4387,7 +4492,7 @@ async function mergePendingTwins(projectRoot) {
|
|
|
4387
4492
|
}
|
|
4388
4493
|
for (const type of PENDING_TYPES) {
|
|
4389
4494
|
const dir = join11(pendingBase2, type);
|
|
4390
|
-
if (!
|
|
4495
|
+
if (!existsSync5(dir)) continue;
|
|
4391
4496
|
let names;
|
|
4392
4497
|
try {
|
|
4393
4498
|
names = (await readdir(dir)).filter((n) => n.endsWith(".md"));
|
|
@@ -4484,6 +4589,14 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
4484
4589
|
}
|
|
4485
4590
|
case "modify-layer":
|
|
4486
4591
|
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
4592
|
+
// v2.3 batch content-modify: the array-native flush path for the
|
|
4593
|
+
// fabric-review maintain loop. Collapses N per-item modify round-trips
|
|
4594
|
+
// (each paying a first-reconcile gate wait) into one call.
|
|
4595
|
+
case "modify-content-batch":
|
|
4596
|
+
return {
|
|
4597
|
+
action: "modify-content-batch",
|
|
4598
|
+
modified: await modifyContentBatch(projectRoot, input.items)
|
|
4599
|
+
};
|
|
4487
4600
|
case "defer":
|
|
4488
4601
|
return {
|
|
4489
4602
|
action: "defer",
|
|
@@ -4494,6 +4607,19 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
4494
4607
|
input.reason
|
|
4495
4608
|
)
|
|
4496
4609
|
};
|
|
4610
|
+
// retire (W3-C): mark canonical entries deprecated in place. Same in-place
|
|
4611
|
+
// frontmatter-merge path as modify (resolveModifyTarget + rewriteFrontmatterMerge),
|
|
4612
|
+
// batched over pending_paths like reject/defer. Never deletes a file.
|
|
4613
|
+
case "retire":
|
|
4614
|
+
return {
|
|
4615
|
+
action: "retire",
|
|
4616
|
+
retired: await retireAll(
|
|
4617
|
+
projectRoot,
|
|
4618
|
+
input.pending_paths,
|
|
4619
|
+
input.superseded_by,
|
|
4620
|
+
input.reason
|
|
4621
|
+
)
|
|
4622
|
+
};
|
|
4497
4623
|
default: {
|
|
4498
4624
|
const exhaustive = input;
|
|
4499
4625
|
throw new Error(`unsupported action: ${JSON.stringify(exhaustive)}`);
|
|
@@ -4591,7 +4717,7 @@ async function listPending(projectRoot, filters) {
|
|
|
4591
4717
|
for (const source of sources) {
|
|
4592
4718
|
for (const type of typesToScan) {
|
|
4593
4719
|
const dir = join12(source.root, type);
|
|
4594
|
-
if (!
|
|
4720
|
+
if (!existsSync6(dir)) {
|
|
4595
4721
|
continue;
|
|
4596
4722
|
}
|
|
4597
4723
|
let entries;
|
|
@@ -4729,7 +4855,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4729
4855
|
);
|
|
4730
4856
|
allocatedId = stableId;
|
|
4731
4857
|
const newFilename = `${stableId}--${slug}.md`;
|
|
4732
|
-
const promoteProject = layer === "team" ?
|
|
4858
|
+
const promoteProject = layer === "team" ? loadProjectConfig4(projectRoot)?.active_project : void 0;
|
|
4733
4859
|
targetAbs = join12(
|
|
4734
4860
|
resolveStoreCanonicalBase(layer, projectRoot, promoteProject),
|
|
4735
4861
|
pluralType,
|
|
@@ -4743,7 +4869,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4743
4869
|
await atomicWriteText(targetAbs, rewritten);
|
|
4744
4870
|
writtenTarget = true;
|
|
4745
4871
|
if (sourceIsStore) {
|
|
4746
|
-
if (
|
|
4872
|
+
if (existsSync6(sourceAbs)) {
|
|
4747
4873
|
await unlink2(sourceAbs);
|
|
4748
4874
|
}
|
|
4749
4875
|
} else if (sourceOrigin === "team") {
|
|
@@ -4753,12 +4879,12 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4753
4879
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4754
4880
|
});
|
|
4755
4881
|
} catch {
|
|
4756
|
-
if (
|
|
4882
|
+
if (existsSync6(sourceAbs)) {
|
|
4757
4883
|
await unlink2(sourceAbs);
|
|
4758
4884
|
}
|
|
4759
4885
|
}
|
|
4760
4886
|
} else {
|
|
4761
|
-
if (
|
|
4887
|
+
if (existsSync6(sourceAbs)) {
|
|
4762
4888
|
await unlink2(sourceAbs);
|
|
4763
4889
|
}
|
|
4764
4890
|
}
|
|
@@ -4770,7 +4896,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4770
4896
|
});
|
|
4771
4897
|
return { pending_path: pendingPath, stable_id: stableId };
|
|
4772
4898
|
} catch (err) {
|
|
4773
|
-
if (writtenTarget && targetAbs !== void 0 &&
|
|
4899
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync6(targetAbs)) {
|
|
4774
4900
|
try {
|
|
4775
4901
|
await unlink2(targetAbs);
|
|
4776
4902
|
} catch {
|
|
@@ -4791,7 +4917,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4791
4917
|
for (const pendingPath of pendingPaths) {
|
|
4792
4918
|
try {
|
|
4793
4919
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4794
|
-
if (
|
|
4920
|
+
if (existsSync6(sandboxed.abs)) {
|
|
4795
4921
|
const content = await readFile9(sandboxed.abs, "utf8");
|
|
4796
4922
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4797
4923
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
@@ -4860,6 +4986,23 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
4860
4986
|
pending_path: pendingPath
|
|
4861
4987
|
};
|
|
4862
4988
|
}
|
|
4989
|
+
async function modifyContentBatch(projectRoot, items) {
|
|
4990
|
+
const results = [];
|
|
4991
|
+
for (const item of items) {
|
|
4992
|
+
const { layer: _droppedLayer, ...contentChanges } = item.changes;
|
|
4993
|
+
try {
|
|
4994
|
+
await modifyEntry(projectRoot, item.pending_path, contentChanges);
|
|
4995
|
+
results.push({ pending_path: item.pending_path, ok: true });
|
|
4996
|
+
} catch (error) {
|
|
4997
|
+
results.push({
|
|
4998
|
+
pending_path: item.pending_path,
|
|
4999
|
+
ok: false,
|
|
5000
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
return results;
|
|
5005
|
+
}
|
|
4863
5006
|
function pickModifyEventValues(source, fields) {
|
|
4864
5007
|
const out = {};
|
|
4865
5008
|
for (const field of fields) {
|
|
@@ -4874,7 +5017,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
4874
5017
|
} catch {
|
|
4875
5018
|
return null;
|
|
4876
5019
|
}
|
|
4877
|
-
if (
|
|
5020
|
+
if (existsSync6(sandboxed.abs)) {
|
|
4878
5021
|
return {
|
|
4879
5022
|
absPath: sandboxed.abs,
|
|
4880
5023
|
isInProjectTree: sandboxed.isInProjectTree,
|
|
@@ -4921,7 +5064,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4921
5064
|
pluralType,
|
|
4922
5065
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
4923
5066
|
);
|
|
4924
|
-
const flipProject = toLayer === "team" ?
|
|
5067
|
+
const flipProject = toLayer === "team" ? loadProjectConfig4(projectRoot)?.active_project : void 0;
|
|
4925
5068
|
const toAbs = join12(
|
|
4926
5069
|
resolveStoreCanonicalBase(toLayer, projectRoot, flipProject),
|
|
4927
5070
|
pluralType,
|
|
@@ -4960,7 +5103,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4960
5103
|
}
|
|
4961
5104
|
}
|
|
4962
5105
|
await atomicWriteText(toAbs, rewritten);
|
|
4963
|
-
if (!moved &&
|
|
5106
|
+
if (!moved && existsSync6(target.absPath) && target.absPath !== toAbs) {
|
|
4964
5107
|
await unlink2(target.absPath);
|
|
4965
5108
|
}
|
|
4966
5109
|
const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
|
|
@@ -5223,7 +5366,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
5223
5366
|
let stableId;
|
|
5224
5367
|
try {
|
|
5225
5368
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
5226
|
-
if (
|
|
5369
|
+
if (existsSync6(sandboxed.abs)) {
|
|
5227
5370
|
const content = await readFile9(sandboxed.abs, "utf8");
|
|
5228
5371
|
stableId = parseFrontmatter(content).id;
|
|
5229
5372
|
const patch = {
|
|
@@ -5249,6 +5392,52 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
5249
5392
|
}
|
|
5250
5393
|
return deferred;
|
|
5251
5394
|
}
|
|
5395
|
+
async function retireAll(projectRoot, pendingPaths, supersededBy, reason) {
|
|
5396
|
+
const retired = [];
|
|
5397
|
+
for (const pendingPath of pendingPaths) {
|
|
5398
|
+
const result = await retireOne(projectRoot, pendingPath, supersededBy, reason);
|
|
5399
|
+
if (result !== null) {
|
|
5400
|
+
retired.push(result);
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
return retired;
|
|
5404
|
+
}
|
|
5405
|
+
async function retireOne(projectRoot, pendingPath, supersededBy, reason) {
|
|
5406
|
+
const target = resolveModifyTarget(projectRoot, pendingPath);
|
|
5407
|
+
if (target === null) {
|
|
5408
|
+
return null;
|
|
5409
|
+
}
|
|
5410
|
+
const content = await readFile9(target.absPath, "utf8");
|
|
5411
|
+
const fm = parseFrontmatter(content);
|
|
5412
|
+
const patch = {
|
|
5413
|
+
deprecated: true,
|
|
5414
|
+
...supersededBy !== void 0 ? { superseded_by: supersededBy } : {}
|
|
5415
|
+
};
|
|
5416
|
+
const merged = rewriteFrontmatterMerge(content, patch);
|
|
5417
|
+
await atomicWriteText(target.absPath, merged);
|
|
5418
|
+
const changedFields = supersededBy !== void 0 ? ["deprecated", "superseded_by"] : ["deprecated"];
|
|
5419
|
+
const before = { deprecated: fm.deprecated ?? null };
|
|
5420
|
+
const after = { deprecated: true };
|
|
5421
|
+
if (supersededBy !== void 0) {
|
|
5422
|
+
before.superseded_by = fm.superseded_by ?? null;
|
|
5423
|
+
after.superseded_by = supersededBy;
|
|
5424
|
+
}
|
|
5425
|
+
await emitEventBestEffort2(projectRoot, {
|
|
5426
|
+
event_type: "knowledge_modified",
|
|
5427
|
+
...fm.id !== void 0 ? { stable_id: fm.id } : {},
|
|
5428
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5429
|
+
path: pendingPath,
|
|
5430
|
+
changed_fields: changedFields,
|
|
5431
|
+
before,
|
|
5432
|
+
after,
|
|
5433
|
+
reason: reason !== void 0 ? `retire:${pendingPath}: ${reason}` : `retire:${pendingPath}`
|
|
5434
|
+
});
|
|
5435
|
+
return {
|
|
5436
|
+
path: pendingPath,
|
|
5437
|
+
...fm.id !== void 0 ? { stable_id: fm.id } : {},
|
|
5438
|
+
...supersededBy !== void 0 ? { superseded_by: supersededBy } : {}
|
|
5439
|
+
};
|
|
5440
|
+
}
|
|
5252
5441
|
function parseFrontmatter(content) {
|
|
5253
5442
|
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
5254
5443
|
if (match === null) {
|
|
@@ -5262,10 +5451,10 @@ function parseFrontmatter(content) {
|
|
|
5262
5451
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
5263
5452
|
const line = rawLine.trim();
|
|
5264
5453
|
if (line.length === 0) continue;
|
|
5265
|
-
const
|
|
5266
|
-
if (
|
|
5267
|
-
const key = line.slice(0,
|
|
5268
|
-
const value = line.slice(
|
|
5454
|
+
const sep6 = line.indexOf(":");
|
|
5455
|
+
if (sep6 === -1) continue;
|
|
5456
|
+
const key = line.slice(0, sep6).trim();
|
|
5457
|
+
const value = line.slice(sep6 + 1).trim();
|
|
5269
5458
|
switch (key) {
|
|
5270
5459
|
case "id":
|
|
5271
5460
|
out.id = stripQuotes(value);
|
|
@@ -5322,6 +5511,14 @@ function parseFrontmatter(content) {
|
|
|
5322
5511
|
case "last_review_confirmed_at":
|
|
5323
5512
|
out.last_review_confirmed_at = stripQuotes(value);
|
|
5324
5513
|
break;
|
|
5514
|
+
case "deprecated":
|
|
5515
|
+
if (value === "true" || value === "false") {
|
|
5516
|
+
out.deprecated = value === "true";
|
|
5517
|
+
}
|
|
5518
|
+
break;
|
|
5519
|
+
case "superseded_by":
|
|
5520
|
+
out.superseded_by = stripQuotes(value);
|
|
5521
|
+
break;
|
|
5325
5522
|
default:
|
|
5326
5523
|
break;
|
|
5327
5524
|
}
|
|
@@ -5391,12 +5588,14 @@ ${content}`;
|
|
|
5391
5588
|
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
5392
5589
|
if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
|
|
5393
5590
|
if (patch.last_review_confirmed_at !== void 0) updates.last_review_confirmed_at = `last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`;
|
|
5591
|
+
if (patch.deprecated !== void 0) updates.deprecated = `deprecated: ${patch.deprecated}`;
|
|
5592
|
+
if (patch.superseded_by !== void 0) updates.superseded_by = `superseded_by: ${quoteIfNeeded(patch.superseded_by)}`;
|
|
5394
5593
|
const lines = block.split(/\r?\n/u);
|
|
5395
5594
|
const seen = /* @__PURE__ */ new Set();
|
|
5396
5595
|
const newLines = [];
|
|
5397
5596
|
for (const line of lines) {
|
|
5398
|
-
const
|
|
5399
|
-
const key =
|
|
5597
|
+
const sep6 = line.indexOf(":");
|
|
5598
|
+
const key = sep6 === -1 ? "" : line.slice(0, sep6).trim();
|
|
5400
5599
|
if (key in updates) {
|
|
5401
5600
|
newLines.push(updates[key]);
|
|
5402
5601
|
seen.add(key);
|
|
@@ -5431,6 +5630,8 @@ function appendPatchLines(lines, patch) {
|
|
|
5431
5630
|
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
5432
5631
|
if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
|
|
5433
5632
|
if (patch.last_review_confirmed_at !== void 0) lines.push(`last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`);
|
|
5633
|
+
if (patch.deprecated !== void 0) lines.push(`deprecated: ${patch.deprecated}`);
|
|
5634
|
+
if (patch.superseded_by !== void 0) lines.push(`superseded_by: ${quoteIfNeeded(patch.superseded_by)}`);
|
|
5434
5635
|
}
|
|
5435
5636
|
function flowArrayElement(value) {
|
|
5436
5637
|
if (/[\n\r,\[\]{}"#:]/u.test(value) || /^\s|\s$/u.test(value)) {
|
|
@@ -5465,7 +5666,7 @@ function registerReview(server, tracker) {
|
|
|
5465
5666
|
server.registerTool(
|
|
5466
5667
|
"fab_review",
|
|
5467
5668
|
{
|
|
5468
|
-
description: "Review pending knowledge entries in resolved store-backed knowledge/pending/. Discriminated by `action`; required fields per action: list \u2192 (filters optional); approve \u2192 pending_paths[\u22651]; reject \u2192 pending_paths[\u22651] + reason; modify / modify-content \u2192 pending_path + changes; modify-layer \u2192 pending_path + changes.layer(team|personal); search \u2192 query; defer \u2192 pending_paths[\u22651] (until/reason optional). approve allocates a stable_id and promotes to the canonical store knowledge path. Skill-side tool \u2014 invoked by fabric-review.",
|
|
5669
|
+
description: "Review pending knowledge entries in resolved store-backed knowledge/pending/. Discriminated by `action`; required fields per action: list \u2192 (filters optional); approve \u2192 pending_paths[\u22651]; reject \u2192 pending_paths[\u22651] + reason; modify / modify-content \u2192 pending_path + changes; modify-layer \u2192 pending_path + changes.layer(team|personal); modify-content-batch \u2192 items[{pending_path, changes}][\u22651] (batch content edits; per-item partial-failure reported in modified[]); search \u2192 query; defer \u2192 pending_paths[\u22651] (until/reason optional); retire \u2192 pending_paths[\u22651] (superseded_by/reason optional; marks canonical entries deprecated in place \u2014 deprecate-over-delete \u2014 so they stop surfacing in recall/broad indexes). approve allocates a stable_id and promotes to the canonical store knowledge path. Skill-side tool \u2014 invoked by fabric-review.",
|
|
5469
5670
|
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
5470
5671
|
// authoritative cross-field contract still lives in FabReviewInputSchema
|
|
5471
5672
|
// (discriminatedUnion) and is enforced inside the handler via
|
|
@@ -5487,6 +5688,12 @@ function registerReview(server, tracker) {
|
|
|
5487
5688
|
if (gateWarn) {
|
|
5488
5689
|
response.warnings = [gateWarn];
|
|
5489
5690
|
}
|
|
5691
|
+
if (narrowed.action !== "retire") {
|
|
5692
|
+
const scopeWarn = unsealedProjectScopeWarning(projectRoot);
|
|
5693
|
+
if (scopeWarn) {
|
|
5694
|
+
response.warnings = [...response.warnings ?? [], scopeWarn];
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5490
5697
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
5491
5698
|
const serialized = JSON.stringify(response);
|
|
5492
5699
|
const guardResult = enforcePayloadLimit4(serialized, payloadLimits);
|
|
@@ -5573,7 +5780,7 @@ function registerPending(server, tracker) {
|
|
|
5573
5780
|
// src/services/doctor.ts
|
|
5574
5781
|
import { access as access4, readFile as readFile19, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
|
|
5575
5782
|
import { constants as constants2 } from "fs";
|
|
5576
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5783
|
+
import { isAbsolute as isAbsolute2, join as join25, posix as posix5, resolve as resolve3 } from "path";
|
|
5577
5784
|
import {
|
|
5578
5785
|
createTranslator,
|
|
5579
5786
|
forensicReportSchema,
|
|
@@ -5977,7 +6184,7 @@ function createLayerMismatchCheck(t, inspection) {
|
|
|
5977
6184
|
|
|
5978
6185
|
// src/services/doctor-relevance-paths.ts
|
|
5979
6186
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5980
|
-
import { existsSync as
|
|
6187
|
+
import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
|
|
5981
6188
|
import { join as join14, sep as sep3 } from "path";
|
|
5982
6189
|
import { minimatch } from "minimatch";
|
|
5983
6190
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
@@ -5999,7 +6206,7 @@ function expandGlob(rawGlob) {
|
|
|
5999
6206
|
return rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
6000
6207
|
}
|
|
6001
6208
|
function collectWorkspacePaths(projectRoot) {
|
|
6002
|
-
if (!
|
|
6209
|
+
if (!existsSync7(projectRoot)) {
|
|
6003
6210
|
return [];
|
|
6004
6211
|
}
|
|
6005
6212
|
try {
|
|
@@ -6409,8 +6616,8 @@ function createStaleArchiveCheck(t, inspection) {
|
|
|
6409
6616
|
// src/services/doctor-knowledge-promotion.ts
|
|
6410
6617
|
var PROVEN_RELATED_INDEGREE_THRESHOLD = 3;
|
|
6411
6618
|
function toLocalId2(id) {
|
|
6412
|
-
const
|
|
6413
|
-
return
|
|
6619
|
+
const sep6 = id.indexOf(":");
|
|
6620
|
+
return sep6 === -1 ? id : id.slice(sep6 + 1);
|
|
6414
6621
|
}
|
|
6415
6622
|
function computeRelatedInDegree(entries) {
|
|
6416
6623
|
const indegree = /* @__PURE__ */ new Map();
|
|
@@ -6690,45 +6897,6 @@ function createScopeLintCheck(t, violations) {
|
|
|
6690
6897
|
};
|
|
6691
6898
|
}
|
|
6692
6899
|
|
|
6693
|
-
// src/services/doctor-unbound-project.ts
|
|
6694
|
-
import { loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
|
|
6695
|
-
function detectUnboundProject(projectRoot) {
|
|
6696
|
-
const config = loadProjectConfig4(projectRoot);
|
|
6697
|
-
const alias = config?.active_write_store;
|
|
6698
|
-
if (typeof alias !== "string" || alias.length === 0) {
|
|
6699
|
-
return null;
|
|
6700
|
-
}
|
|
6701
|
-
const missing = [];
|
|
6702
|
-
if (typeof config?.project_id !== "string" || config.project_id.length === 0) {
|
|
6703
|
-
missing.push("project_id");
|
|
6704
|
-
}
|
|
6705
|
-
if (typeof config?.active_project !== "string" || config.active_project.length === 0) {
|
|
6706
|
-
missing.push("active_project");
|
|
6707
|
-
}
|
|
6708
|
-
return missing.length > 0 ? { alias, missing } : null;
|
|
6709
|
-
}
|
|
6710
|
-
function createUnboundProjectCheck(t, violation) {
|
|
6711
|
-
if (violation === null) {
|
|
6712
|
-
return {
|
|
6713
|
-
name: t("doctor.check.unbound_project.name"),
|
|
6714
|
-
status: "ok",
|
|
6715
|
-
message: t("doctor.check.unbound_project.ok")
|
|
6716
|
-
};
|
|
6717
|
-
}
|
|
6718
|
-
return {
|
|
6719
|
-
name: t("doctor.check.unbound_project.name"),
|
|
6720
|
-
status: "warn",
|
|
6721
|
-
kind: "warning",
|
|
6722
|
-
code: "unbound_project",
|
|
6723
|
-
fixable: false,
|
|
6724
|
-
message: t("doctor.check.unbound_project.message", {
|
|
6725
|
-
alias: violation.alias,
|
|
6726
|
-
missing: violation.missing.join(" + ")
|
|
6727
|
-
}),
|
|
6728
|
-
actionHint: t("doctor.check.unbound_project.remediation")
|
|
6729
|
-
};
|
|
6730
|
-
}
|
|
6731
|
-
|
|
6732
6900
|
// src/services/doctor-write-route-lint.ts
|
|
6733
6901
|
import { loadProjectConfig as loadProjectConfig5 } from "@fenglimg/fabric-shared";
|
|
6734
6902
|
function detectWriteRouteUnbound(projectRoot) {
|
|
@@ -6776,9 +6944,100 @@ function createWriteRouteUnboundCheck(t, violations) {
|
|
|
6776
6944
|
};
|
|
6777
6945
|
}
|
|
6778
6946
|
|
|
6947
|
+
// src/services/doctor-stray-fabric-dir.ts
|
|
6948
|
+
import { readdirSync as readdirSync2, rename as renameCb } from "fs";
|
|
6949
|
+
import { promisify } from "util";
|
|
6950
|
+
import { join as join17, sep as sep4 } from "path";
|
|
6951
|
+
var rename = promisify(renameCb);
|
|
6952
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
6953
|
+
"node_modules",
|
|
6954
|
+
".git",
|
|
6955
|
+
"dist",
|
|
6956
|
+
"build",
|
|
6957
|
+
"coverage",
|
|
6958
|
+
".next",
|
|
6959
|
+
".turbo",
|
|
6960
|
+
"Library",
|
|
6961
|
+
"Temp",
|
|
6962
|
+
".claude",
|
|
6963
|
+
".codex",
|
|
6964
|
+
".workflow"
|
|
6965
|
+
]);
|
|
6966
|
+
var MAX_DEPTH = 8;
|
|
6967
|
+
function detectStrayFabricDirs(projectRoot) {
|
|
6968
|
+
const strays = [];
|
|
6969
|
+
walk(projectRoot, projectRoot, 0, strays);
|
|
6970
|
+
return strays;
|
|
6971
|
+
}
|
|
6972
|
+
function walk(root, current, depth, out) {
|
|
6973
|
+
if (depth > MAX_DEPTH) return;
|
|
6974
|
+
let entries;
|
|
6975
|
+
try {
|
|
6976
|
+
entries = readdirSync2(current, { withFileTypes: true });
|
|
6977
|
+
} catch {
|
|
6978
|
+
return;
|
|
6979
|
+
}
|
|
6980
|
+
for (const entry of entries) {
|
|
6981
|
+
if (!entry.isDirectory()) continue;
|
|
6982
|
+
if (entry.name === ".fabric") {
|
|
6983
|
+
if (current !== root) {
|
|
6984
|
+
out.push(join17(current, entry.name));
|
|
6985
|
+
}
|
|
6986
|
+
continue;
|
|
6987
|
+
}
|
|
6988
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
6989
|
+
walk(root, join17(current, entry.name), depth + 1, out);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
function relative2(root, abs) {
|
|
6993
|
+
const prefix = root.endsWith(sep4) ? root : root + sep4;
|
|
6994
|
+
return abs.startsWith(prefix) ? abs.slice(prefix.length) : abs;
|
|
6995
|
+
}
|
|
6996
|
+
function createStrayFabricDirCheck(t, strays, projectRoot) {
|
|
6997
|
+
if (strays.length === 0) {
|
|
6998
|
+
return {
|
|
6999
|
+
name: t("doctor.check.stray_fabric_dir_detected.name"),
|
|
7000
|
+
status: "ok",
|
|
7001
|
+
message: t("doctor.check.stray_fabric_dir_detected.ok")
|
|
7002
|
+
};
|
|
7003
|
+
}
|
|
7004
|
+
const summary = strays.slice(0, 5).map((abs) => relative2(projectRoot, abs)).join(", ");
|
|
7005
|
+
return {
|
|
7006
|
+
name: t("doctor.check.stray_fabric_dir_detected.name"),
|
|
7007
|
+
status: "warn",
|
|
7008
|
+
kind: "warning",
|
|
7009
|
+
code: "stray_fabric_dir_detected",
|
|
7010
|
+
fixable: true,
|
|
7011
|
+
message: t("doctor.check.stray_fabric_dir_detected.message", {
|
|
7012
|
+
count: String(strays.length),
|
|
7013
|
+
dirs: summary
|
|
7014
|
+
}),
|
|
7015
|
+
actionHint: t("doctor.check.stray_fabric_dir_detected.remediation")
|
|
7016
|
+
};
|
|
7017
|
+
}
|
|
7018
|
+
async function fixStrayFabricDirs(strays, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
7019
|
+
const stamp = nowIso.replace(/[:.]/g, "-");
|
|
7020
|
+
const results = [];
|
|
7021
|
+
for (const from of strays) {
|
|
7022
|
+
const to = `${from}.stale-${stamp}`;
|
|
7023
|
+
try {
|
|
7024
|
+
await rename(from, to);
|
|
7025
|
+
results.push({ from, to, ok: true });
|
|
7026
|
+
} catch (err) {
|
|
7027
|
+
results.push({
|
|
7028
|
+
from,
|
|
7029
|
+
to,
|
|
7030
|
+
ok: false,
|
|
7031
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7032
|
+
});
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
return results;
|
|
7036
|
+
}
|
|
7037
|
+
|
|
6779
7038
|
// src/services/doctor-store-counters.ts
|
|
6780
|
-
import { existsSync as
|
|
6781
|
-
import { join as
|
|
7039
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
7040
|
+
import { join as join18 } from "path";
|
|
6782
7041
|
import {
|
|
6783
7042
|
buildStoreResolveInput as buildStoreResolveInput6,
|
|
6784
7043
|
createStoreResolver as createStoreResolver6,
|
|
@@ -6805,7 +7064,7 @@ function resolveCounterStores(projectRoot) {
|
|
|
6805
7064
|
return readSet.stores.map((entry) => ({
|
|
6806
7065
|
uuid: entry.store_uuid,
|
|
6807
7066
|
alias: entry.alias,
|
|
6808
|
-
dir:
|
|
7067
|
+
dir: join18(
|
|
6809
7068
|
globalRoot,
|
|
6810
7069
|
storeRelativePathForMount5(
|
|
6811
7070
|
input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
@@ -6833,13 +7092,13 @@ function readEntryId(file) {
|
|
|
6833
7092
|
function computeStoreDiskMax(storeDir) {
|
|
6834
7093
|
const max = defaultAgentsMetaCounters();
|
|
6835
7094
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS2) {
|
|
6836
|
-
const dir =
|
|
6837
|
-
if (!
|
|
7095
|
+
const dir = join18(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
7096
|
+
if (!existsSync8(dir)) {
|
|
6838
7097
|
continue;
|
|
6839
7098
|
}
|
|
6840
7099
|
let names;
|
|
6841
7100
|
try {
|
|
6842
|
-
names =
|
|
7101
|
+
names = readdirSync3(dir);
|
|
6843
7102
|
} catch {
|
|
6844
7103
|
continue;
|
|
6845
7104
|
}
|
|
@@ -6847,7 +7106,7 @@ function computeStoreDiskMax(storeDir) {
|
|
|
6847
7106
|
if (!name.endsWith(".md")) {
|
|
6848
7107
|
continue;
|
|
6849
7108
|
}
|
|
6850
|
-
const parsed = parseKnowledgeId2(readEntryId(
|
|
7109
|
+
const parsed = parseKnowledgeId2(readEntryId(join18(dir, name)) ?? "");
|
|
6851
7110
|
if (parsed === null) {
|
|
6852
7111
|
continue;
|
|
6853
7112
|
}
|
|
@@ -6929,8 +7188,8 @@ function createStoreCounterCheck(t, drifts) {
|
|
|
6929
7188
|
}
|
|
6930
7189
|
|
|
6931
7190
|
// src/services/doctor-store-orphan.ts
|
|
6932
|
-
import { readdirSync as
|
|
6933
|
-
import { join as
|
|
7191
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
7192
|
+
import { join as join19 } from "path";
|
|
6934
7193
|
import {
|
|
6935
7194
|
STORES_ROOT_DIR,
|
|
6936
7195
|
addMountedStore,
|
|
@@ -6943,7 +7202,7 @@ import {
|
|
|
6943
7202
|
var STORE_BY_ALIAS_DIR = "by-alias";
|
|
6944
7203
|
function listDir(dir) {
|
|
6945
7204
|
try {
|
|
6946
|
-
return
|
|
7205
|
+
return readdirSync4(dir);
|
|
6947
7206
|
} catch {
|
|
6948
7207
|
return [];
|
|
6949
7208
|
}
|
|
@@ -6954,15 +7213,15 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot7()) {
|
|
|
6954
7213
|
return [];
|
|
6955
7214
|
}
|
|
6956
7215
|
const registered = new Set(config.stores.map((s) => s.store_uuid));
|
|
6957
|
-
const storesRoot =
|
|
7216
|
+
const storesRoot = join19(globalRoot, STORES_ROOT_DIR);
|
|
6958
7217
|
const orphans = [];
|
|
6959
7218
|
for (const group of listDir(storesRoot)) {
|
|
6960
7219
|
if (group === STORE_BY_ALIAS_DIR) {
|
|
6961
7220
|
continue;
|
|
6962
7221
|
}
|
|
6963
|
-
const groupDir =
|
|
7222
|
+
const groupDir = join19(storesRoot, group);
|
|
6964
7223
|
for (const mount of listDir(groupDir)) {
|
|
6965
|
-
const dir =
|
|
7224
|
+
const dir = join19(groupDir, mount);
|
|
6966
7225
|
const identity = readStoreIdentity(dir);
|
|
6967
7226
|
if (identity === null || registered.has(identity.store_uuid)) {
|
|
6968
7227
|
continue;
|
|
@@ -7029,7 +7288,7 @@ function createStoreOrphanCheck(t, orphans) {
|
|
|
7029
7288
|
|
|
7030
7289
|
// src/services/doctor-project-registry-drift.ts
|
|
7031
7290
|
import { readdir as readdir3, rmdir } from "fs/promises";
|
|
7032
|
-
import { join as
|
|
7291
|
+
import { join as join20 } from "path";
|
|
7033
7292
|
import {
|
|
7034
7293
|
addStoreProject,
|
|
7035
7294
|
buildStoreResolveInput as buildStoreResolveInput7,
|
|
@@ -7058,7 +7317,7 @@ function resolveDriftStores(projectRoot) {
|
|
|
7058
7317
|
return {
|
|
7059
7318
|
uuid: entry.store_uuid,
|
|
7060
7319
|
alias: entry.alias,
|
|
7061
|
-
dir:
|
|
7320
|
+
dir: join20(globalRoot, storeRelativePathForMount6(mounted ?? { store_uuid: entry.store_uuid }))
|
|
7062
7321
|
};
|
|
7063
7322
|
});
|
|
7064
7323
|
}
|
|
@@ -7070,14 +7329,14 @@ async function listDir2(dir) {
|
|
|
7070
7329
|
}
|
|
7071
7330
|
}
|
|
7072
7331
|
async function listProjectFolders(storeDir) {
|
|
7073
|
-
const projectsRoot =
|
|
7332
|
+
const projectsRoot = join20(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR);
|
|
7074
7333
|
const reserved = /* @__PURE__ */ new Set([PROJECTS_DIR, ...STORE_KNOWLEDGE_TYPE_DIRS3]);
|
|
7075
7334
|
return (await listDir2(projectsRoot)).filter((id) => STORE_PROJECT_ID_PATTERN2.test(id) && !reserved.has(id)).sort();
|
|
7076
7335
|
}
|
|
7077
7336
|
async function folderHasEntries(storeDir, projectId) {
|
|
7078
|
-
const base =
|
|
7337
|
+
const base = join20(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR, projectId);
|
|
7079
7338
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
7080
|
-
const names = await listDir2(
|
|
7339
|
+
const names = await listDir2(join20(base, type));
|
|
7081
7340
|
if (names.some((name) => name.endsWith(".md"))) {
|
|
7082
7341
|
return true;
|
|
7083
7342
|
}
|
|
@@ -7122,14 +7381,14 @@ async function fixProjectRegistryDrift(projectRoot) {
|
|
|
7122
7381
|
continue;
|
|
7123
7382
|
}
|
|
7124
7383
|
try {
|
|
7125
|
-
const base =
|
|
7384
|
+
const base = join20(
|
|
7126
7385
|
finding.store_dir,
|
|
7127
7386
|
STORE_LAYOUT3.knowledgeDir,
|
|
7128
7387
|
PROJECTS_DIR,
|
|
7129
7388
|
finding.project_id
|
|
7130
7389
|
);
|
|
7131
7390
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
7132
|
-
await rmdir(
|
|
7391
|
+
await rmdir(join20(base, type)).catch(() => void 0);
|
|
7133
7392
|
}
|
|
7134
7393
|
await rmdir(base);
|
|
7135
7394
|
pruned.push(finding);
|
|
@@ -7224,7 +7483,7 @@ import {
|
|
|
7224
7483
|
|
|
7225
7484
|
// src/services/events-jsonl-gates.ts
|
|
7226
7485
|
import { promises as fs } from "fs";
|
|
7227
|
-
import { existsSync as
|
|
7486
|
+
import { existsSync as existsSync9 } from "fs";
|
|
7228
7487
|
var EVENTS_JSONL_SIZE_WARN_BYTES = 10 * 1024 * 1024;
|
|
7229
7488
|
var METRICS_STALE_WARN_MS = 10 * 60 * 1e3;
|
|
7230
7489
|
var ROTATION_OVERDUE_WARN_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
@@ -7246,7 +7505,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
7246
7505
|
if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
|
|
7247
7506
|
}
|
|
7248
7507
|
let metricsStalenessMs = null;
|
|
7249
|
-
if (
|
|
7508
|
+
if (existsSync9(metricsPath)) {
|
|
7250
7509
|
try {
|
|
7251
7510
|
const stat5 = await fs.stat(metricsPath);
|
|
7252
7511
|
metricsStalenessMs = Math.max(0, now.getTime() - stat5.mtimeMs);
|
|
@@ -7289,7 +7548,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
7289
7548
|
|
|
7290
7549
|
// src/services/doctor-skill-lints.ts
|
|
7291
7550
|
import { readdir as readdir4, readFile as readFile13 } from "fs/promises";
|
|
7292
|
-
import { join as
|
|
7551
|
+
import { join as join21, posix as posix2 } from "path";
|
|
7293
7552
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
7294
7553
|
var FABRIC_CONTRACT_SKILL_SLUGS = ["fabric-archive", "fabric-review", "fabric-store", "fabric-sync"];
|
|
7295
7554
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
@@ -7344,8 +7603,8 @@ async function listMarkdownFiles(dir) {
|
|
|
7344
7603
|
async function inspectSkillRefMirror(projectRoot) {
|
|
7345
7604
|
const driftedPaths = [];
|
|
7346
7605
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
7347
|
-
const claudeRef =
|
|
7348
|
-
const codexRef =
|
|
7606
|
+
const claudeRef = join21(projectRoot, ".claude", "skills", slug, "ref");
|
|
7607
|
+
const codexRef = join21(projectRoot, ".codex", "skills", slug, "ref");
|
|
7349
7608
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
7350
7609
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
7351
7610
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -7362,8 +7621,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
7362
7621
|
let codexBody;
|
|
7363
7622
|
try {
|
|
7364
7623
|
[claudeBody, codexBody] = await Promise.all([
|
|
7365
|
-
readFile13(
|
|
7366
|
-
readFile13(
|
|
7624
|
+
readFile13(join21(claudeRef, fname), "utf8"),
|
|
7625
|
+
readFile13(join21(codexRef, fname), "utf8")
|
|
7367
7626
|
]);
|
|
7368
7627
|
} catch {
|
|
7369
7628
|
continue;
|
|
@@ -7382,7 +7641,7 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
7382
7641
|
const overSize = [];
|
|
7383
7642
|
let highestSeverity = "ok";
|
|
7384
7643
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
7385
|
-
const skillMdPath =
|
|
7644
|
+
const skillMdPath = join21(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
7386
7645
|
let body;
|
|
7387
7646
|
try {
|
|
7388
7647
|
body = await readFile13(skillMdPath, "utf8");
|
|
@@ -7407,7 +7666,7 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
7407
7666
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
7408
7667
|
const ANTI_TRIGGER_PATTERN = /\bNOT\b|ONLY when|do not|不要|不是|非/u;
|
|
7409
7668
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
7410
|
-
const skillMdPath =
|
|
7669
|
+
const skillMdPath = join21(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
7411
7670
|
let body;
|
|
7412
7671
|
try {
|
|
7413
7672
|
body = await readFile13(skillMdPath, "utf8");
|
|
@@ -7450,8 +7709,8 @@ async function inspectSkillContract(projectRoot) {
|
|
|
7450
7709
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
7451
7710
|
const client = rootRel.startsWith(".claude") ? ".claude" : ".codex";
|
|
7452
7711
|
for (const slug of FABRIC_CONTRACT_SKILL_SLUGS) {
|
|
7453
|
-
const skillDir =
|
|
7454
|
-
const skillMdPath =
|
|
7712
|
+
const skillDir = join21(projectRoot, rootRel, slug);
|
|
7713
|
+
const skillMdPath = join21(skillDir, "SKILL.md");
|
|
7455
7714
|
let body;
|
|
7456
7715
|
try {
|
|
7457
7716
|
body = await readFile13(skillMdPath, "utf8");
|
|
@@ -7467,7 +7726,7 @@ async function inspectSkillContract(projectRoot) {
|
|
|
7467
7726
|
detail: token
|
|
7468
7727
|
});
|
|
7469
7728
|
}
|
|
7470
|
-
const refDir =
|
|
7729
|
+
const refDir = join21(skillDir, "ref");
|
|
7471
7730
|
let refFiles;
|
|
7472
7731
|
try {
|
|
7473
7732
|
refFiles = (await readdir4(refDir)).filter((name) => name.endsWith(".md"));
|
|
@@ -7491,7 +7750,7 @@ async function inspectSkillContract(projectRoot) {
|
|
|
7491
7750
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
7492
7751
|
const candidates = [];
|
|
7493
7752
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
7494
|
-
const rootAbs =
|
|
7753
|
+
const rootAbs = join21(projectRoot, rootRel);
|
|
7495
7754
|
let dirEntries;
|
|
7496
7755
|
try {
|
|
7497
7756
|
dirEntries = await readdir4(rootAbs, { withFileTypes: true });
|
|
@@ -7500,7 +7759,7 @@ async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
7500
7759
|
}
|
|
7501
7760
|
for (const dirEntry of dirEntries) {
|
|
7502
7761
|
if (!dirEntry.isDirectory()) continue;
|
|
7503
|
-
const skillFile =
|
|
7762
|
+
const skillFile = join21(rootAbs, dirEntry.name, "SKILL.md");
|
|
7504
7763
|
let raw;
|
|
7505
7764
|
try {
|
|
7506
7765
|
raw = await readFile13(skillFile, "utf8");
|
|
@@ -7646,7 +7905,7 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
7646
7905
|
|
|
7647
7906
|
// src/services/doctor-retired-references-lint.ts
|
|
7648
7907
|
import { readdir as readdir5, readFile as readFile14, stat as stat3 } from "fs/promises";
|
|
7649
|
-
import { join as
|
|
7908
|
+
import { join as join22, posix as posix3, relative as relative3 } from "path";
|
|
7650
7909
|
var RETIRED_TOKENS = [
|
|
7651
7910
|
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
7652
7911
|
{ token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
|
|
@@ -7669,7 +7928,7 @@ var RETIRED_TOKENS = [
|
|
|
7669
7928
|
];
|
|
7670
7929
|
var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
|
|
7671
7930
|
var SKILL_DIRS = [".claude/skills", ".codex/skills"];
|
|
7672
|
-
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md",
|
|
7931
|
+
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join22(".fabric", "AGENTS.md")];
|
|
7673
7932
|
function isCommentLine(line) {
|
|
7674
7933
|
const t = line.trim();
|
|
7675
7934
|
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
@@ -7683,7 +7942,7 @@ async function walkFiles(dir, exts) {
|
|
|
7683
7942
|
}
|
|
7684
7943
|
const out = [];
|
|
7685
7944
|
for (const e of entries) {
|
|
7686
|
-
const full =
|
|
7945
|
+
const full = join22(dir, e.name);
|
|
7687
7946
|
if (e.isDirectory()) {
|
|
7688
7947
|
out.push(...await walkFiles(full, exts));
|
|
7689
7948
|
} else if (exts.some((ext) => e.name.endsWith(ext))) {
|
|
@@ -7707,9 +7966,9 @@ function scanText(rel, text, skipComments, hits) {
|
|
|
7707
7966
|
async function inspectRetiredReferences(projectRoot) {
|
|
7708
7967
|
const hits = [];
|
|
7709
7968
|
let scannedFiles = 0;
|
|
7710
|
-
const toRel = (p) => posix3.normalize(
|
|
7969
|
+
const toRel = (p) => posix3.normalize(relative3(projectRoot, p).split("\\").join("/"));
|
|
7711
7970
|
for (const rel of BOOTSTRAP_FILES) {
|
|
7712
|
-
const abs =
|
|
7971
|
+
const abs = join22(projectRoot, rel);
|
|
7713
7972
|
try {
|
|
7714
7973
|
if ((await stat3(abs)).isFile()) {
|
|
7715
7974
|
scannedFiles += 1;
|
|
@@ -7719,7 +7978,7 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
7719
7978
|
}
|
|
7720
7979
|
}
|
|
7721
7980
|
for (const dir of SKILL_DIRS) {
|
|
7722
|
-
for (const file of await walkFiles(
|
|
7981
|
+
for (const file of await walkFiles(join22(projectRoot, dir), [".md"])) {
|
|
7723
7982
|
scannedFiles += 1;
|
|
7724
7983
|
try {
|
|
7725
7984
|
scanText(toRel(file), await readFile14(file, "utf8"), false, hits);
|
|
@@ -7728,7 +7987,7 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
7728
7987
|
}
|
|
7729
7988
|
}
|
|
7730
7989
|
for (const dir of HOOK_DIRS) {
|
|
7731
|
-
for (const file of await walkFiles(
|
|
7990
|
+
for (const file of await walkFiles(join22(projectRoot, dir), [".cjs"])) {
|
|
7732
7991
|
scannedFiles += 1;
|
|
7733
7992
|
try {
|
|
7734
7993
|
scanText(toRel(file), await readFile14(file, "utf8"), true, hits);
|
|
@@ -7767,7 +8026,7 @@ function createRetiredReferenceCheck(t, inspection) {
|
|
|
7767
8026
|
// src/services/doctor-hooks-lints.ts
|
|
7768
8027
|
import { constants } from "fs";
|
|
7769
8028
|
import { access, readdir as readdir6, readFile as readFile15, stat as stat4 } from "fs/promises";
|
|
7770
|
-
import { join as
|
|
8029
|
+
import { join as join23, posix as posix4 } from "path";
|
|
7771
8030
|
import { Script } from "vm";
|
|
7772
8031
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
7773
8032
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -7829,11 +8088,11 @@ async function isFile(absPath) {
|
|
|
7829
8088
|
}
|
|
7830
8089
|
}
|
|
7831
8090
|
async function inspectHooksWired(projectRoot) {
|
|
7832
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
8091
|
+
const claudeEntries = await readDirectoryFileNames(join23(projectRoot, ".claude"));
|
|
7833
8092
|
if (claudeEntries === null) {
|
|
7834
8093
|
return { status: "skipped", missingHooks: [] };
|
|
7835
8094
|
}
|
|
7836
|
-
const settingsPath =
|
|
8095
|
+
const settingsPath = join23(projectRoot, ".claude", "settings.json");
|
|
7837
8096
|
let parsed;
|
|
7838
8097
|
try {
|
|
7839
8098
|
parsed = JSON.parse(await readFile15(settingsPath, "utf8"));
|
|
@@ -7865,8 +8124,8 @@ async function inspectHooksWired(projectRoot) {
|
|
|
7865
8124
|
}
|
|
7866
8125
|
async function inspectHookCacheWritability(projectRoot) {
|
|
7867
8126
|
const relPath = posix4.join(".fabric", ".cache");
|
|
7868
|
-
const fabricDir =
|
|
7869
|
-
const cacheDir =
|
|
8127
|
+
const fabricDir = join23(projectRoot, ".fabric");
|
|
8128
|
+
const cacheDir = join23(projectRoot, ".fabric", ".cache");
|
|
7870
8129
|
try {
|
|
7871
8130
|
try {
|
|
7872
8131
|
const cacheStats = await stat4(cacheDir);
|
|
@@ -7915,12 +8174,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
7915
8174
|
async function inspectHooksContentDrift(projectRoot) {
|
|
7916
8175
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
7917
8176
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
7918
|
-
const absDir =
|
|
8177
|
+
const absDir = join23(projectRoot, dir);
|
|
7919
8178
|
const entries = await readDirectoryFileNames(absDir);
|
|
7920
8179
|
if (entries === null) continue;
|
|
7921
8180
|
for (const name of entries) {
|
|
7922
8181
|
if (!name.endsWith(".cjs")) continue;
|
|
7923
|
-
const abs =
|
|
8182
|
+
const abs = join23(absDir, name);
|
|
7924
8183
|
if (!await isFile(abs)) continue;
|
|
7925
8184
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
7926
8185
|
arr.push({ client, abs });
|
|
@@ -7957,12 +8216,12 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
7957
8216
|
const issues = [];
|
|
7958
8217
|
let scanned = 0;
|
|
7959
8218
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
7960
|
-
const absDir =
|
|
8219
|
+
const absDir = join23(projectRoot, dir);
|
|
7961
8220
|
const entries = await readDirectoryFileNames(absDir);
|
|
7962
8221
|
if (entries === null) continue;
|
|
7963
8222
|
for (const name of entries) {
|
|
7964
8223
|
if (!name.endsWith(".cjs")) continue;
|
|
7965
|
-
const abs =
|
|
8224
|
+
const abs = join23(absDir, name);
|
|
7966
8225
|
const displayPath = `${dir}/${name}`;
|
|
7967
8226
|
if (!await isFile(abs)) continue;
|
|
7968
8227
|
scanned += 1;
|
|
@@ -8106,7 +8365,7 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
8106
8365
|
|
|
8107
8366
|
// src/services/doctor-bootstrap-lints.ts
|
|
8108
8367
|
import { access as access2, readFile as readFile16 } from "fs/promises";
|
|
8109
|
-
import { join as
|
|
8368
|
+
import { join as join24 } from "path";
|
|
8110
8369
|
import {
|
|
8111
8370
|
BOOTSTRAP_MARKER_BEGIN,
|
|
8112
8371
|
BOOTSTRAP_MARKER_END,
|
|
@@ -8138,13 +8397,13 @@ async function fileExists(path) {
|
|
|
8138
8397
|
}
|
|
8139
8398
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
8140
8399
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
8141
|
-
fileExists(
|
|
8142
|
-
fileExists(
|
|
8400
|
+
fileExists(join24(projectRoot, "AGENTS.md")),
|
|
8401
|
+
fileExists(join24(projectRoot, "CLAUDE.md"))
|
|
8143
8402
|
]);
|
|
8144
8403
|
return { hasAgentsMd, hasClaudeMd };
|
|
8145
8404
|
}
|
|
8146
8405
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
8147
|
-
const abs =
|
|
8406
|
+
const abs = join24(target, ".fabric", "AGENTS.md");
|
|
8148
8407
|
const canonical = resolveBootstrapCanonical();
|
|
8149
8408
|
let onDisk;
|
|
8150
8409
|
try {
|
|
@@ -8174,14 +8433,14 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
8174
8433
|
);
|
|
8175
8434
|
}
|
|
8176
8435
|
async function inspectL2ManagedBlockDrift(target) {
|
|
8177
|
-
const snapshotPath =
|
|
8436
|
+
const snapshotPath = join24(target, ".fabric", "AGENTS.md");
|
|
8178
8437
|
let snapshot;
|
|
8179
8438
|
try {
|
|
8180
8439
|
snapshot = await readFile16(snapshotPath, "utf8");
|
|
8181
8440
|
} catch {
|
|
8182
8441
|
return { status: "ok", drifted: [] };
|
|
8183
8442
|
}
|
|
8184
|
-
const projectRulesPath =
|
|
8443
|
+
const projectRulesPath = join24(target, ".fabric", "project-rules.md");
|
|
8185
8444
|
let expectedBody = snapshot;
|
|
8186
8445
|
try {
|
|
8187
8446
|
const projectRules = await readFile16(projectRulesPath, "utf8");
|
|
@@ -8193,7 +8452,7 @@ ${projectRules}`;
|
|
|
8193
8452
|
const drifted = [];
|
|
8194
8453
|
let anyManagedBlockFound = false;
|
|
8195
8454
|
const blockTargets = [
|
|
8196
|
-
|
|
8455
|
+
join24(target, "AGENTS.md")
|
|
8197
8456
|
];
|
|
8198
8457
|
for (const abs of blockTargets) {
|
|
8199
8458
|
let content;
|
|
@@ -8221,7 +8480,7 @@ ${projectRules}`;
|
|
|
8221
8480
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
8222
8481
|
}
|
|
8223
8482
|
}
|
|
8224
|
-
const claudeMdPath =
|
|
8483
|
+
const claudeMdPath = join24(target, "CLAUDE.md");
|
|
8225
8484
|
try {
|
|
8226
8485
|
const claudeContent = await readFile16(claudeMdPath, "utf8");
|
|
8227
8486
|
anyManagedBlockFound = true;
|
|
@@ -9453,7 +9712,7 @@ async function runDoctorReport(target) {
|
|
|
9453
9712
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
9454
9713
|
const targetFiles = Object.fromEntries(
|
|
9455
9714
|
await Promise.all(
|
|
9456
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
9715
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join25(projectRoot, path))])
|
|
9457
9716
|
)
|
|
9458
9717
|
);
|
|
9459
9718
|
const checks = [
|
|
@@ -9597,6 +9856,14 @@ async function runDoctorReport(target) {
|
|
|
9597
9856
|
// config-level check, no resolver, aligned with KT-DEC-0048 read-tolerant
|
|
9598
9857
|
// doctrine.
|
|
9599
9858
|
createWriteRouteUnboundCheck(t, detectWriteRouteUnbound(projectRoot)),
|
|
9859
|
+
// rc.11 stray_fabric_dir_detected — walks the project tree for any
|
|
9860
|
+
// `.fabric/` directory other than `<projectRoot>/.fabric`. Historical
|
|
9861
|
+
// residue from the pre-rc.10 hook / pre-rc.11 server-side resolveProjectRoot
|
|
9862
|
+
// fault mode where a subprocess whose cwd was a subdirectory of the repo
|
|
9863
|
+
// created a stray `<subdir>/.fabric/` (scattering events.jsonl / metrics.jsonl
|
|
9864
|
+
// / .cache). Adjacent to write_route_target_unbound — both catch config /
|
|
9865
|
+
// filesystem cross-references that drifted through migrations.
|
|
9866
|
+
createStrayFabricDirCheck(t, detectStrayFabricDirs(projectRoot), projectRoot),
|
|
9600
9867
|
// rc.31 BUG-G2/G5: promote-ledger invariant. Sits adjacent to onboard
|
|
9601
9868
|
// coverage — both are observability advisories built off events.jsonl.
|
|
9602
9869
|
...promoteLedgerInvariant === null ? [] : [createPromoteLedgerInvariantCheck(t, promoteLedgerInvariant)],
|
|
@@ -9672,7 +9939,7 @@ async function runDoctorFix(target) {
|
|
|
9672
9939
|
const fixed = [];
|
|
9673
9940
|
const ledgerWarnings = [];
|
|
9674
9941
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
9675
|
-
const snapshotPath =
|
|
9942
|
+
const snapshotPath = join25(projectRoot, ".fabric", "AGENTS.md");
|
|
9676
9943
|
await ensureParentDirectory(snapshotPath);
|
|
9677
9944
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
9678
9945
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -9696,6 +9963,15 @@ async function runDoctorFix(target) {
|
|
|
9696
9963
|
fixed.push(findIssue(before.warnings, "store_orphan"));
|
|
9697
9964
|
}
|
|
9698
9965
|
}
|
|
9966
|
+
if (before.warnings.some((issue) => issue.code === "stray_fabric_dir_detected")) {
|
|
9967
|
+
const strays = detectStrayFabricDirs(projectRoot);
|
|
9968
|
+
if (strays.length > 0) {
|
|
9969
|
+
const results = await fixStrayFabricDirs(strays);
|
|
9970
|
+
if (results.some((r) => r.ok)) {
|
|
9971
|
+
fixed.push(findIssue(before.warnings, "stray_fabric_dir_detected"));
|
|
9972
|
+
}
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9699
9975
|
const registryDriftFound = before.manual_errors.some((issue) => issue.code === "project_registry_drift") || before.warnings.some((issue) => issue.code === "project_registry_drift") || before.infos.some((issue) => issue.code === "project_registry_drift");
|
|
9700
9976
|
if (registryDriftFound) {
|
|
9701
9977
|
const result = await fixProjectRegistryDrift(projectRoot);
|
|
@@ -9757,7 +10033,7 @@ async function runDoctorFix(target) {
|
|
|
9757
10033
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
9758
10034
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
9759
10035
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
9760
|
-
const lockFilePath =
|
|
10036
|
+
const lockFilePath = join25(projectRoot, ".fabric", ".serve.lock");
|
|
9761
10037
|
try {
|
|
9762
10038
|
await unlink4(lockFilePath);
|
|
9763
10039
|
} catch (err) {
|
|
@@ -9874,7 +10150,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
9874
10150
|
}
|
|
9875
10151
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
9876
10152
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
9877
|
-
const absPath =
|
|
10153
|
+
const absPath = join25(projectRoot, candidate.path);
|
|
9878
10154
|
try {
|
|
9879
10155
|
const { unlink: unlink5 } = await import("fs/promises");
|
|
9880
10156
|
await unlink5(absPath);
|
|
@@ -9899,7 +10175,7 @@ function truncateErrorMessage(error) {
|
|
|
9899
10175
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
9900
10176
|
}
|
|
9901
10177
|
async function inspectForensic(projectRoot) {
|
|
9902
|
-
const path =
|
|
10178
|
+
const path = join25(projectRoot, ".fabric", "forensic.json");
|
|
9903
10179
|
try {
|
|
9904
10180
|
const parsed = forensicReportSchema.parse(JSON.parse(await readFile19(path, "utf8")));
|
|
9905
10181
|
return { present: true, valid: true, report: parsed };
|
|
@@ -10476,7 +10752,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
10476
10752
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
10477
10753
|
const detected = [];
|
|
10478
10754
|
for (const name of candidates) {
|
|
10479
|
-
if (await pathExists(
|
|
10755
|
+
if (await pathExists(join25(projectRoot, name))) {
|
|
10480
10756
|
detected.push(name);
|
|
10481
10757
|
}
|
|
10482
10758
|
}
|
|
@@ -10561,7 +10837,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
10561
10837
|
return map;
|
|
10562
10838
|
}
|
|
10563
10839
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
10564
|
-
const cacheDir =
|
|
10840
|
+
const cacheDir = join25(projectRoot, ".fabric", ".cache");
|
|
10565
10841
|
let entries;
|
|
10566
10842
|
try {
|
|
10567
10843
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -10573,7 +10849,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
10573
10849
|
if (!entry.isFile()) continue;
|
|
10574
10850
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
10575
10851
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
10576
|
-
const absPath =
|
|
10852
|
+
const absPath = join25(cacheDir, entry.name);
|
|
10577
10853
|
let mtimeMs = 0;
|
|
10578
10854
|
try {
|
|
10579
10855
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
@@ -10605,7 +10881,7 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
10605
10881
|
};
|
|
10606
10882
|
}
|
|
10607
10883
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
10608
|
-
const configPath =
|
|
10884
|
+
const configPath = join25(projectRoot, ".fabric", "fabric-config.json");
|
|
10609
10885
|
try {
|
|
10610
10886
|
const raw = await readFile19(configPath, "utf8");
|
|
10611
10887
|
const parsed = JSON.parse(raw);
|
|
@@ -10723,7 +10999,7 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
10723
10999
|
return { filled, missing, opted_out: optedOut };
|
|
10724
11000
|
}
|
|
10725
11001
|
async function readOnboardOptedOut(projectRoot) {
|
|
10726
|
-
const path =
|
|
11002
|
+
const path = join25(projectRoot, ".fabric", "fabric-config.json");
|
|
10727
11003
|
let raw;
|
|
10728
11004
|
try {
|
|
10729
11005
|
raw = await readFile19(path, "utf8");
|
|
@@ -10750,10 +11026,10 @@ function readFrontmatterScalar(content, key) {
|
|
|
10750
11026
|
if (block === void 0) return void 0;
|
|
10751
11027
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
10752
11028
|
const line = rawLine.trim();
|
|
10753
|
-
const
|
|
10754
|
-
if (
|
|
10755
|
-
if (line.slice(0,
|
|
10756
|
-
let value = line.slice(
|
|
11029
|
+
const sep6 = line.indexOf(":");
|
|
11030
|
+
if (sep6 === -1) continue;
|
|
11031
|
+
if (line.slice(0, sep6).trim() !== key) continue;
|
|
11032
|
+
let value = line.slice(sep6 + 1).trim();
|
|
10757
11033
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
10758
11034
|
value = value.slice(1, -1);
|
|
10759
11035
|
}
|
|
@@ -10828,7 +11104,7 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
10828
11104
|
}
|
|
10829
11105
|
}
|
|
10830
11106
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
10831
|
-
const snapshotPath =
|
|
11107
|
+
const snapshotPath = join25(projectRoot, ".fabric", "AGENTS.md");
|
|
10832
11108
|
if (!await pathExists(snapshotPath)) {
|
|
10833
11109
|
return;
|
|
10834
11110
|
}
|
|
@@ -10838,7 +11114,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
10838
11114
|
} catch {
|
|
10839
11115
|
return;
|
|
10840
11116
|
}
|
|
10841
|
-
const projectRulesPath =
|
|
11117
|
+
const projectRulesPath = join25(projectRoot, ".fabric", "project-rules.md");
|
|
10842
11118
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
10843
11119
|
let expectedBody = snapshot;
|
|
10844
11120
|
if (hasProjectRules) {
|
|
@@ -10854,7 +11130,7 @@ ${projectRules}`;
|
|
|
10854
11130
|
${expectedBody}
|
|
10855
11131
|
${BOOTSTRAP_MARKER_END2}`;
|
|
10856
11132
|
const blockTargets = [
|
|
10857
|
-
|
|
11133
|
+
join25(projectRoot, "AGENTS.md")
|
|
10858
11134
|
];
|
|
10859
11135
|
for (const abs of blockTargets) {
|
|
10860
11136
|
if (!await pathExists(abs)) {
|
|
@@ -10887,7 +11163,7 @@ ${managedBlock}
|
|
|
10887
11163
|
}
|
|
10888
11164
|
await atomicWriteText4(abs, next);
|
|
10889
11165
|
}
|
|
10890
|
-
const claudeMdPath =
|
|
11166
|
+
const claudeMdPath = join25(projectRoot, "CLAUDE.md");
|
|
10891
11167
|
if (await pathExists(claudeMdPath)) {
|
|
10892
11168
|
let claudeContent;
|
|
10893
11169
|
try {
|
|
@@ -10957,7 +11233,7 @@ async function collectEntryPoints(root) {
|
|
|
10957
11233
|
continue;
|
|
10958
11234
|
}
|
|
10959
11235
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
10960
|
-
const absolutePath =
|
|
11236
|
+
const absolutePath = join25(current, entry.name);
|
|
10961
11237
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
10962
11238
|
if (relativePath.length === 0) {
|
|
10963
11239
|
continue;
|
|
@@ -11172,7 +11448,7 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
11172
11448
|
|
|
11173
11449
|
// src/services/why-not-surfaced.ts
|
|
11174
11450
|
import { readFile as readFile20 } from "fs/promises";
|
|
11175
|
-
import { basename as basename3, join as
|
|
11451
|
+
import { basename as basename3, join as join26 } from "path";
|
|
11176
11452
|
import {
|
|
11177
11453
|
buildStoreResolveInput as buildStoreResolveInput8,
|
|
11178
11454
|
createStoreResolver as createStoreResolver8,
|
|
@@ -11212,7 +11488,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
11212
11488
|
const allStores = input.mountedStores.map((s) => ({
|
|
11213
11489
|
store_uuid: s.store_uuid,
|
|
11214
11490
|
alias: s.alias,
|
|
11215
|
-
dir:
|
|
11491
|
+
dir: join26(globalRoot, storeRelativePathForMount7(s))
|
|
11216
11492
|
}));
|
|
11217
11493
|
const refs = await readKnowledgeAcrossStores4(allStores);
|
|
11218
11494
|
const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
|
|
@@ -11326,8 +11602,8 @@ async function inspectRelatedGraph(projectRoot) {
|
|
|
11326
11602
|
}
|
|
11327
11603
|
|
|
11328
11604
|
// src/services/store-precheck.ts
|
|
11329
|
-
import { existsSync as
|
|
11330
|
-
import { join as
|
|
11605
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
11606
|
+
import { join as join27 } from "path";
|
|
11331
11607
|
import {
|
|
11332
11608
|
buildStoreResolveInput as buildStoreResolveInput9,
|
|
11333
11609
|
createStoreResolver as createStoreResolver9,
|
|
@@ -11340,7 +11616,7 @@ function clearPrecheckCache() {
|
|
|
11340
11616
|
cache.clear();
|
|
11341
11617
|
}
|
|
11342
11618
|
function evaluateStoreDir(storeDir, identity) {
|
|
11343
|
-
if (!
|
|
11619
|
+
if (!existsSync10(storeDir)) {
|
|
11344
11620
|
return {
|
|
11345
11621
|
uuid: identity.uuid,
|
|
11346
11622
|
alias: identity.alias,
|
|
@@ -11348,10 +11624,10 @@ function evaluateStoreDir(storeDir, identity) {
|
|
|
11348
11624
|
reason: `directory not found at ${storeDir}`
|
|
11349
11625
|
};
|
|
11350
11626
|
}
|
|
11351
|
-
const storeJsonPath =
|
|
11352
|
-
const gitDirPath =
|
|
11353
|
-
const hasStoreJson =
|
|
11354
|
-
const hasGitDir =
|
|
11627
|
+
const storeJsonPath = join27(storeDir, "store.json");
|
|
11628
|
+
const gitDirPath = join27(storeDir, ".git");
|
|
11629
|
+
const hasStoreJson = existsSync10(storeJsonPath) && isValidStoreJson(storeJsonPath);
|
|
11630
|
+
const hasGitDir = existsSync10(gitDirPath);
|
|
11355
11631
|
if (!hasStoreJson && !hasGitDir) {
|
|
11356
11632
|
return {
|
|
11357
11633
|
uuid: identity.uuid,
|
|
@@ -11384,7 +11660,7 @@ async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobal
|
|
|
11384
11660
|
const readSet = createStoreResolver9().resolveReadSet(input);
|
|
11385
11661
|
const stores = readSet.stores.map((entry) => {
|
|
11386
11662
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
11387
|
-
const storeDir =
|
|
11663
|
+
const storeDir = join27(
|
|
11388
11664
|
globalRoot,
|
|
11389
11665
|
storeRelativePathForMount8(mounted ?? { store_uuid: entry.store_uuid })
|
|
11390
11666
|
);
|
|
@@ -11488,7 +11764,7 @@ function stopRotationTick(projectRoot) {
|
|
|
11488
11764
|
|
|
11489
11765
|
// src/services/rehydrate-state.ts
|
|
11490
11766
|
import { execFile } from "child_process";
|
|
11491
|
-
import { promisify } from "util";
|
|
11767
|
+
import { promisify as promisify2 } from "util";
|
|
11492
11768
|
import { agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
|
|
11493
11769
|
import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-shared/errors";
|
|
11494
11770
|
|
|
@@ -11624,7 +11900,7 @@ async function pathExists2(path) {
|
|
|
11624
11900
|
}
|
|
11625
11901
|
|
|
11626
11902
|
// src/services/rehydrate-state.ts
|
|
11627
|
-
var execFileAsync =
|
|
11903
|
+
var execFileAsync = promisify2(execFile);
|
|
11628
11904
|
var AGENTS_META_GIT_PATH = ".fabric/agents.meta.json";
|
|
11629
11905
|
var HistoryStateNotFoundError = class extends IOFabricError2 {
|
|
11630
11906
|
code = "HISTORY_STATE_NOT_FOUND";
|
|
@@ -11777,8 +12053,8 @@ function formatError(error) {
|
|
|
11777
12053
|
}
|
|
11778
12054
|
function formatPreexistingRootMessage(projectRoot) {
|
|
11779
12055
|
const preexisting = [];
|
|
11780
|
-
if (
|
|
11781
|
-
if (
|
|
12056
|
+
if (existsSync11(join28(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
12057
|
+
if (existsSync11(join28(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
11782
12058
|
if (preexisting.length === 0) return null;
|
|
11783
12059
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from mounted stores via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
11784
12060
|
}
|
|
@@ -11805,7 +12081,7 @@ function createFabricServer(tracker) {
|
|
|
11805
12081
|
const server = new McpServer(
|
|
11806
12082
|
{
|
|
11807
12083
|
name: "fabric-knowledge-server",
|
|
11808
|
-
version: "2.3.0-rc.
|
|
12084
|
+
version: "2.3.0-rc.12"
|
|
11809
12085
|
},
|
|
11810
12086
|
{
|
|
11811
12087
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -11825,9 +12101,9 @@ function createFabricServer(tracker) {
|
|
|
11825
12101
|
},
|
|
11826
12102
|
async (_uri) => {
|
|
11827
12103
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
11828
|
-
const path =
|
|
12104
|
+
const path = join28(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
11829
12105
|
let text = "";
|
|
11830
|
-
if (
|
|
12106
|
+
if (existsSync11(path)) {
|
|
11831
12107
|
text = await readFile22(path, "utf8");
|
|
11832
12108
|
}
|
|
11833
12109
|
return {
|