@fenglimg/fabric-cli 2.0.0-rc.34 → 2.0.0-rc.35
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/README.md +1 -1
- package/dist/{chunk-SRX7WZUG.js → chunk-BATF4PEJ.js} +2 -2
- package/dist/{chunk-5N3KXIVI.js → chunk-XVS4F3P6.js} +32 -0
- package/dist/{config-5CH4EJQ2.js → config-XJIPZNUP.js} +1 -1
- package/dist/{doctor-E26YO67D.js → doctor-2FCRAWDZ.js} +23 -8
- package/dist/index.js +7 -7
- package/dist/{install-XCRX34CX.js → install-HOTE5BPA.js} +59 -4
- package/dist/{onboard-coverage-6MN3CYHT.js → onboard-coverage-MFCAEBDO.js} +4 -4
- package/dist/{plan-context-hint-CXTLNVSV.js → plan-context-hint-UQLRKGBZ.js} +2 -2
- package/dist/{uninstall-Q7V55BXH.js → uninstall-BIJ5GLEU.js} +1 -1
- package/package.json +3 -4
- package/templates/hooks/cite-policy-evict.cjs +1 -1
- package/templates/hooks/knowledge-hint-broad.cjs +26 -6
- package/templates/hooks/knowledge-hint-narrow.cjs +106 -1
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +1 -1
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +10 -10
- package/templates/skills/fabric-import/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-review/ref/i18n-policy.md +1 -1
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ var dismissSlotCmd = defineCommand({
|
|
|
107
107
|
const next = [...optedOut, slot];
|
|
108
108
|
const merged = { ...config, onboard_slots_opted_out: next };
|
|
109
109
|
await atomicWriteJson(configPath, merged);
|
|
110
|
-
console.log(`Dismissed onboard slot "${slot}". Run \`
|
|
110
|
+
console.log(`Dismissed onboard slot "${slot}". Run \`fabric config onboard-reset ${slot}\` to re-open.`);
|
|
111
111
|
} catch (err) {
|
|
112
112
|
const message = err instanceof Error ? err.message : String(err);
|
|
113
113
|
console.error(`dismiss-slot failed: ${message}`);
|
|
@@ -153,7 +153,7 @@ var onboardResetCmd = defineCommand({
|
|
|
153
153
|
const next = optedOut.filter((s) => s !== slot);
|
|
154
154
|
const merged = { ...config, onboard_slots_opted_out: next };
|
|
155
155
|
await atomicWriteJson(configPath, merged);
|
|
156
|
-
console.log(`Reset onboard slot "${slot}"; it will appear in \`
|
|
156
|
+
console.log(`Reset onboard slot "${slot}"; it will appear in \`fabric onboard-coverage\` as missing again.`);
|
|
157
157
|
} catch (err) {
|
|
158
158
|
const message = err instanceof Error ? err.message : String(err);
|
|
159
159
|
console.error(`onboard-reset failed: ${message}`);
|
|
@@ -76,6 +76,10 @@ var SKILL_DESTINATIONS = {
|
|
|
76
76
|
".codex/skills/fabric-import/SKILL.md"
|
|
77
77
|
]
|
|
78
78
|
};
|
|
79
|
+
var DEPRECATED_SKILL_DIRS = [
|
|
80
|
+
".claude/skills/fabric-init",
|
|
81
|
+
".codex/skills/fabric-init"
|
|
82
|
+
];
|
|
79
83
|
var HOOK_SCRIPT_DESTINATIONS = {
|
|
80
84
|
fabricHint: [
|
|
81
85
|
".claude/hooks/fabric-hint.cjs",
|
|
@@ -223,6 +227,33 @@ async function installFabricImportSkill(projectRoot, _options = {}) {
|
|
|
223
227
|
results.push(...await installSkillRefFiles(projectRoot, "fabric-import"));
|
|
224
228
|
return results;
|
|
225
229
|
}
|
|
230
|
+
async function cleanupDeprecatedSkills(projectRoot) {
|
|
231
|
+
const results = [];
|
|
232
|
+
for (const rel of DEPRECATED_SKILL_DIRS) {
|
|
233
|
+
const target = join2(projectRoot, rel);
|
|
234
|
+
if (!existsSync2(target)) {
|
|
235
|
+
results.push({ step: "skill-deprecated-cleanup", path: target, status: "skipped", message: "absent" });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await rm(target, { recursive: true, force: true });
|
|
240
|
+
results.push({
|
|
241
|
+
step: "skill-deprecated-cleanup",
|
|
242
|
+
path: target,
|
|
243
|
+
status: "written",
|
|
244
|
+
message: "removed-deprecated"
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
results.push({
|
|
248
|
+
step: "skill-deprecated-cleanup",
|
|
249
|
+
path: target,
|
|
250
|
+
status: "error",
|
|
251
|
+
message: error instanceof Error ? error.message : String(error)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
226
257
|
async function installSkillRefFiles(projectRoot, skillSlug) {
|
|
227
258
|
let refTemplateDir;
|
|
228
259
|
try {
|
|
@@ -790,6 +821,7 @@ export {
|
|
|
790
821
|
installFabricArchiveSkill,
|
|
791
822
|
installFabricReviewSkill,
|
|
792
823
|
installFabricImportSkill,
|
|
824
|
+
cleanupDeprecatedSkills,
|
|
793
825
|
installArchiveHintHook,
|
|
794
826
|
installKnowledgeHintBroadHook,
|
|
795
827
|
installKnowledgeHintNarrowHook,
|
|
@@ -72,6 +72,16 @@ var doctorCommand = defineCommand({
|
|
|
72
72
|
description: t("cli.doctor.args.yes.description"),
|
|
73
73
|
default: false
|
|
74
74
|
},
|
|
75
|
+
// rc.35 TASK-12 (P0-11): expose maintainer-audience actionHints. By
|
|
76
|
+
// default the renderer folds remediation strings that target Fabric
|
|
77
|
+
// contributors (edit `packages/cli/templates/...`, interpret G1-G5
|
|
78
|
+
// cite-goodhart codes, etc.) since npm end users have no actionable
|
|
79
|
+
// lever for them. --verbose shows them.
|
|
80
|
+
verbose: {
|
|
81
|
+
type: "boolean",
|
|
82
|
+
description: t("cli.doctor.args.verbose.description"),
|
|
83
|
+
default: false
|
|
84
|
+
},
|
|
75
85
|
// rc.20 TASK-05: cite policy adherence report (read-only). Skips standard
|
|
76
86
|
// inspections entirely — different output surface. Mutually exclusive
|
|
77
87
|
// with --fix / --fix-knowledge (enforced in run()).
|
|
@@ -284,7 +294,7 @@ var doctorCommand = defineCommand({
|
|
|
284
294
|
} else if (fix && args["dry-run"] === true) {
|
|
285
295
|
writeStdout(dt("cli.doctor.fix-dry-run-banner"));
|
|
286
296
|
}
|
|
287
|
-
renderHumanReport(report, dt);
|
|
297
|
+
renderHumanReport(report, dt, args.verbose === true);
|
|
288
298
|
}
|
|
289
299
|
await emitDoctorRunEventBestEffort(resolution.target, {
|
|
290
300
|
mode: fixKnowledge ? "fix-knowledge" : "lint",
|
|
@@ -307,14 +317,15 @@ var doctorCommand = defineCommand({
|
|
|
307
317
|
}
|
|
308
318
|
});
|
|
309
319
|
var doctor_default = doctorCommand;
|
|
310
|
-
function renderHumanReport(report, dt) {
|
|
320
|
+
function renderHumanReport(report, dt, verbose) {
|
|
311
321
|
writeStdout(`${renderStatus(report.status)} ${paint.ai("fabric doctor")} ${paint.human(report.summary.target)}`);
|
|
312
322
|
for (const check of report.checks) {
|
|
313
323
|
writeStdout(`${renderStatus(check.status)} ${check.name}: ${check.message}`);
|
|
314
324
|
}
|
|
315
|
-
|
|
316
|
-
writeIssueSection(dt("doctor.section.
|
|
317
|
-
writeIssueSection(dt("doctor.section.
|
|
325
|
+
const opts = { verbose, dt };
|
|
326
|
+
writeIssueSection(dt("doctor.section.fixable"), report.fixable_errors, opts);
|
|
327
|
+
writeIssueSection(dt("doctor.section.manual"), report.manual_errors, opts);
|
|
328
|
+
writeIssueSection(dt("doctor.section.warnings"), report.warnings, opts);
|
|
318
329
|
renderPayloadLimits(report, dt);
|
|
319
330
|
}
|
|
320
331
|
function renderPayloadLimits(report, dt) {
|
|
@@ -344,7 +355,7 @@ function renderFixKnowledgeMutations(fixKnowledgeReport, dt) {
|
|
|
344
355
|
writeStdout(`${marker} ${mutation.kind}: ${mutation.path} [${mutation.detail}]${errSuffix}`);
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
|
-
function writeIssueSection(title, issues) {
|
|
358
|
+
function writeIssueSection(title, issues, options) {
|
|
348
359
|
if (issues.length === 0) {
|
|
349
360
|
return;
|
|
350
361
|
}
|
|
@@ -353,7 +364,11 @@ function writeIssueSection(title, issues) {
|
|
|
353
364
|
for (const issue of issues) {
|
|
354
365
|
writeStdout(`- ${issue.code}: ${issue.message}`);
|
|
355
366
|
if (issue.actionHint !== void 0 && issue.actionHint.length > 0) {
|
|
356
|
-
|
|
367
|
+
if (issue.audience === "maintainer" && !options.verbose) {
|
|
368
|
+
writeStdout(` \u2192 ${options.dt("doctor.maintainer-hint-folded")}`);
|
|
369
|
+
} else {
|
|
370
|
+
writeStdout(` \u2192 ${issue.actionHint}`);
|
|
371
|
+
}
|
|
357
372
|
}
|
|
358
373
|
}
|
|
359
374
|
}
|
|
@@ -600,7 +615,7 @@ function appendContractSection(lines, report, dt) {
|
|
|
600
615
|
}
|
|
601
616
|
}
|
|
602
617
|
function renderEnrichDescriptionsReport(report, dt) {
|
|
603
|
-
const header = `${symbol.ok} ${paint.ai("
|
|
618
|
+
const header = `${symbol.ok} ${paint.ai("fabric doctor --enrich-descriptions")} mode=${report.mode}${report.dryRun ? " (dry-run)" : ""} scanned=${report.scanned} modified=${report.modified} skipped=${report.skipped}`;
|
|
604
619
|
writeStdout(header);
|
|
605
620
|
if (report.candidates.length === 0) {
|
|
606
621
|
writeStdout(dt("doctor.enrich.allComplete"));
|
package/dist/index.js
CHANGED
|
@@ -11,22 +11,22 @@ import { defineCommand, runMain } from "citty";
|
|
|
11
11
|
|
|
12
12
|
// src/commands/index.ts
|
|
13
13
|
var allCommands = {
|
|
14
|
-
install: () => import("./install-
|
|
15
|
-
doctor: () => import("./doctor-
|
|
14
|
+
install: () => import("./install-HOTE5BPA.js").then((module) => module.default),
|
|
15
|
+
doctor: () => import("./doctor-2FCRAWDZ.js").then((module) => module.default),
|
|
16
16
|
serve: () => import("./serve-43JTEM3U.js").then((module) => module.default),
|
|
17
|
-
uninstall: () => import("./uninstall-
|
|
18
|
-
config: () => import("./config-
|
|
19
|
-
"plan-context-hint": () => import("./plan-context-hint-
|
|
17
|
+
uninstall: () => import("./uninstall-BIJ5GLEU.js").then((module) => module.default),
|
|
18
|
+
config: () => import("./config-XJIPZNUP.js").then((module) => module.default),
|
|
19
|
+
"plan-context-hint": () => import("./plan-context-hint-UQLRKGBZ.js").then((module) => module.default),
|
|
20
20
|
// v2.0.0-rc.23 TASK-014 (F8c): S5 onboard-slot coverage. Used by the
|
|
21
21
|
// fabric-archive Skill's first-run phase to detect unclaimed slots.
|
|
22
|
-
"onboard-coverage": () => import("./onboard-coverage-
|
|
22
|
+
"onboard-coverage": () => import("./onboard-coverage-MFCAEBDO.js").then((module) => module.default)
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
// src/index.ts
|
|
26
26
|
var main = defineCommand({
|
|
27
27
|
meta: {
|
|
28
28
|
name: "fabric",
|
|
29
|
-
version: "2.0.0-rc.
|
|
29
|
+
version: "2.0.0-rc.35",
|
|
30
30
|
description: t("cli.main.description")
|
|
31
31
|
},
|
|
32
32
|
subCommands: allCommands
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
installMcpClients
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BATF4PEJ.js";
|
|
5
5
|
import {
|
|
6
|
+
cleanupDeprecatedSkills,
|
|
6
7
|
installArchiveHintHook,
|
|
7
8
|
installCitePolicyEvictHook,
|
|
8
9
|
installFabricArchiveSkill,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
writeCodexBootstrapManagedBlock,
|
|
20
21
|
writeCursorBootstrapManagedBlock,
|
|
21
22
|
writeFabricAgentsSnapshot
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-XVS4F3P6.js";
|
|
23
24
|
import {
|
|
24
25
|
detectClientSupports
|
|
25
26
|
} from "./chunk-MF3OTILQ.js";
|
|
@@ -342,7 +343,7 @@ async function buildForensicReport(targetInput) {
|
|
|
342
343
|
const report = {
|
|
343
344
|
version: "1.0",
|
|
344
345
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
345
|
-
generated_by: `
|
|
346
|
+
generated_by: `fabric-cli@${getCliVersion()}`,
|
|
346
347
|
target,
|
|
347
348
|
project_name: readProjectName(target),
|
|
348
349
|
framework,
|
|
@@ -1350,7 +1351,7 @@ function readProjectName(target) {
|
|
|
1350
1351
|
return basename(target);
|
|
1351
1352
|
}
|
|
1352
1353
|
function getCliVersion() {
|
|
1353
|
-
return true ? "2.0.0-rc.
|
|
1354
|
+
return true ? "2.0.0-rc.35" : "unknown";
|
|
1354
1355
|
}
|
|
1355
1356
|
function sortRecord(record) {
|
|
1356
1357
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -1387,6 +1388,11 @@ var installCommand = defineCommand({
|
|
|
1387
1388
|
type: "boolean",
|
|
1388
1389
|
description: t("cli.install.args.yes.description"),
|
|
1389
1390
|
default: false
|
|
1391
|
+
},
|
|
1392
|
+
"force-skills-only": {
|
|
1393
|
+
type: "boolean",
|
|
1394
|
+
description: t("cli.install.args.force-skills-only.description"),
|
|
1395
|
+
default: false
|
|
1390
1396
|
}
|
|
1391
1397
|
},
|
|
1392
1398
|
async run({ args }) {
|
|
@@ -1394,9 +1400,56 @@ var installCommand = defineCommand({
|
|
|
1394
1400
|
}
|
|
1395
1401
|
});
|
|
1396
1402
|
var install_default = installCommand;
|
|
1403
|
+
async function runSkillsOnlyRefresh(targetInput) {
|
|
1404
|
+
const target = normalizeTarget3(targetInput);
|
|
1405
|
+
const metaPath = join4(target, ".fabric", "agents.meta.json");
|
|
1406
|
+
if (!existsSync4(metaPath)) {
|
|
1407
|
+
const message = t("cli.install.force-skills-only.uninitialised.message");
|
|
1408
|
+
const hint = t("cli.install.force-skills-only.uninitialised.hint");
|
|
1409
|
+
process.stderr.write(`${message}
|
|
1410
|
+
${hint}
|
|
1411
|
+
`);
|
|
1412
|
+
process.exitCode = 1;
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
console.log(formatInitStageHeader(t("cli.install.force-skills-only.banner")));
|
|
1416
|
+
const results = [];
|
|
1417
|
+
results.push(...await cleanupDeprecatedSkills(target));
|
|
1418
|
+
results.push(...await installFabricArchiveSkill(target));
|
|
1419
|
+
results.push(...await installFabricReviewSkill(target));
|
|
1420
|
+
results.push(...await installFabricImportSkill(target));
|
|
1421
|
+
let written = 0;
|
|
1422
|
+
let skipped = 0;
|
|
1423
|
+
let errors = 0;
|
|
1424
|
+
for (const r of results) {
|
|
1425
|
+
if (r.status === "written") written += 1;
|
|
1426
|
+
else if (r.status === "skipped") skipped += 1;
|
|
1427
|
+
else if (r.status === "error") errors += 1;
|
|
1428
|
+
}
|
|
1429
|
+
console.log(
|
|
1430
|
+
t("cli.install.force-skills-only.summary", {
|
|
1431
|
+
written: String(written),
|
|
1432
|
+
skipped: String(skipped),
|
|
1433
|
+
errors: String(errors)
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
if (errors > 0) {
|
|
1437
|
+
for (const r of results) {
|
|
1438
|
+
if (r.status === "error") {
|
|
1439
|
+
process.stderr.write(` ${r.step} ${r.path}: ${r.message ?? "error"}
|
|
1440
|
+
`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
process.exitCode = 1;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1397
1446
|
async function runInitCommand(args) {
|
|
1398
1447
|
const logger = createDebugLogger(args.debug);
|
|
1399
1448
|
const resolution = resolveDevMode(args.target, process.cwd());
|
|
1449
|
+
if (args["force-skills-only"] === true) {
|
|
1450
|
+
await runSkillsOnlyRefresh(resolution.target);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1400
1453
|
const intent = resolveInitCliIntent(args, resolution.target);
|
|
1401
1454
|
const fabricInitialized = existsSync4(join4(intent.target, ".fabric", "events.jsonl"));
|
|
1402
1455
|
if (fabricInitialized) {
|
|
@@ -1850,6 +1903,7 @@ async function executeInitStagePlan(plan, stageName) {
|
|
|
1850
1903
|
switch (stage.name) {
|
|
1851
1904
|
case "bootstrap": {
|
|
1852
1905
|
const installResults = [];
|
|
1906
|
+
installResults.push(...await runBestEffort("skill-deprecated-cleanup", () => cleanupDeprecatedSkills(plan.target)));
|
|
1853
1907
|
installResults.push(...await runBestEffort("skill-install", () => installFabricArchiveSkill(plan.target)));
|
|
1854
1908
|
installResults.push(...await runBestEffort("skill-review-install", () => installFabricReviewSkill(plan.target)));
|
|
1855
1909
|
installResults.push(...await runBestEffort("skill-import-install", () => installFabricImportSkill(plan.target)));
|
|
@@ -2402,5 +2456,6 @@ export {
|
|
|
2402
2456
|
installCommand,
|
|
2403
2457
|
resolveInitExecutionPlanWithWizard,
|
|
2404
2458
|
runInitCommand,
|
|
2459
|
+
runSkillsOnlyRefresh,
|
|
2405
2460
|
shouldUseInitWizard
|
|
2406
2461
|
};
|
|
@@ -160,7 +160,7 @@ function renderHumanReadable(report) {
|
|
|
160
160
|
detail = entries.join(", ");
|
|
161
161
|
} else if (report.opted_out.includes(slot)) {
|
|
162
162
|
status = "opted-out";
|
|
163
|
-
detail = "(user-dismissed; run `
|
|
163
|
+
detail = "(user-dismissed; run `fabric config onboard-reset` to re-open)";
|
|
164
164
|
} else {
|
|
165
165
|
status = "missing";
|
|
166
166
|
detail = "(run /fabric-archive to onboard)";
|
|
@@ -174,12 +174,12 @@ var onboardCoverageCommand = defineCommand({
|
|
|
174
174
|
name: "onboard-coverage",
|
|
175
175
|
// v2.0.0-rc.29 TASK-008 (BUG-L2): route description strings through t()
|
|
176
176
|
// (mirrors serve.ts pattern). Previously this command was English-only
|
|
177
|
-
// even when the rest of `
|
|
177
|
+
// even when the rest of `fabric --help` rendered zh-CN, so Chinese-locale
|
|
178
178
|
// users saw an isolated English block under --help.
|
|
179
179
|
description: t("cli.onboard-coverage.description"),
|
|
180
|
-
// Mirrors `plan-context-hint`: hidden from `
|
|
180
|
+
// Mirrors `plan-context-hint`: hidden from `fabric --help` so the top-level
|
|
181
181
|
// banner stays focused on install/doctor/serve/config. The command stays
|
|
182
|
-
// callable directly from Skills via `
|
|
182
|
+
// callable directly from Skills via `fabric onboard-coverage --json`.
|
|
183
183
|
hidden: true
|
|
184
184
|
},
|
|
185
185
|
args: {
|
|
@@ -11,9 +11,9 @@ var planContextHintCommand = defineCommand({
|
|
|
11
11
|
meta: {
|
|
12
12
|
name: "plan-context-hint",
|
|
13
13
|
description: "Emit versioned knowledge hint JSON to stdout. Used by rc.6 hooks and the fabric-import skill.",
|
|
14
|
-
// rc.15 TASK-004 (C8): hidden from `
|
|
14
|
+
// rc.15 TASK-004 (C8): hidden from `fabric --help` listing. The command stays
|
|
15
15
|
// callable so hook scripts and the fabric-import skill can still invoke
|
|
16
|
-
// it via `
|
|
16
|
+
// it via `fabric plan-context-hint ...`; it just no longer appears in the
|
|
17
17
|
// top-level usage banner alongside install/doctor/serve/uninstall/config.
|
|
18
18
|
hidden: true
|
|
19
19
|
},
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"fab": "dist/index.js",
|
|
7
6
|
"fabric": "dist/index.js"
|
|
8
7
|
},
|
|
9
8
|
"main": "./dist/index.js",
|
|
@@ -20,8 +19,8 @@
|
|
|
20
19
|
"tree-sitter-javascript": "^0.25.0",
|
|
21
20
|
"tree-sitter-typescript": "^0.23.2",
|
|
22
21
|
"web-tree-sitter": "^0.26.8",
|
|
23
|
-
"@fenglimg/fabric-server": "2.0.0-rc.
|
|
24
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
22
|
+
"@fenglimg/fabric-server": "2.0.0-rc.35",
|
|
23
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.35"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|
|
27
26
|
"@types/node": "^22.15.0",
|
|
@@ -141,7 +141,7 @@ function renderReminder(turnCount, interval) {
|
|
|
141
141
|
"decisions/pitfalls cite MUST end with contract: → <operator> [<operator>...] where operator ∈ {edit:<glob> !edit:<glob> require:<symbol> forbid:<symbol> skip:<reason>}.",
|
|
142
142
|
"skip reasons: sequencing | conditional | semantic | aesthetic | architectural | other:<text>.",
|
|
143
143
|
"KB: none sentinels: [no-relevant] (queried but nothing matched) | [not-applicable] (pure exploration / read-only / user Q&A).",
|
|
144
|
-
"Audit:
|
|
144
|
+
"Audit: fabric doctor --cite-coverage — this rule does not block work, only records.",
|
|
145
145
|
].join("\n");
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -61,6 +61,7 @@ const { dirname, join } = require("node:path");
|
|
|
61
61
|
// (TASK-002). Variant is resolved ONCE per main() invocation via
|
|
62
62
|
// readFabricLanguage(cwd) and threaded into renderBanner — no fs in render path.
|
|
63
63
|
const { renderBanner, readFabricLanguage } = require("./lib/banner-i18n.cjs");
|
|
64
|
+
const { resolveOpaqueSummaries } = require("./lib/summary-fallback.cjs");
|
|
64
65
|
|
|
65
66
|
// -----------------------------------------------------------------------------
|
|
66
67
|
// rc.12: SessionStart broad-menu is now unconditionally emitted on every
|
|
@@ -415,12 +416,11 @@ const MATURITY_DRAFT = "draft";
|
|
|
415
416
|
* Spawn `fabric plan-context-hint --all` and return parsed JSON. Returns
|
|
416
417
|
* null on any failure (ENOENT, non-zero exit, malformed JSON). Never throws.
|
|
417
418
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* return null — the hook stays silent rather than nagging about install state.
|
|
419
|
+
* If `fabric` is not on PATH, return null — the hook stays silent rather
|
|
420
|
+
* than nagging about install state.
|
|
421
421
|
*/
|
|
422
422
|
function invokePlanContextHint(cwd) {
|
|
423
|
-
const candidates = ["fabric"
|
|
423
|
+
const candidates = ["fabric"];
|
|
424
424
|
// rc.31 NEW-6: capture the last meaningful failure so we can surface it on
|
|
425
425
|
// stderr before fail-open. Without this, hook silently swallows backend
|
|
426
426
|
// crashes (e.g. agents_meta_invalid → plan-context-hint exits with stderr
|
|
@@ -439,7 +439,7 @@ function invokePlanContextHint(cwd) {
|
|
|
439
439
|
continue; // spawn throw (extremely rare) — try next candidate
|
|
440
440
|
}
|
|
441
441
|
// ENOENT surfaces as error on the result object. Skip silently for ENOENT
|
|
442
|
-
// (bin not installed is
|
|
442
|
+
// (bin not installed is the only legitimate reason to bail).
|
|
443
443
|
if (res.error) {
|
|
444
444
|
if (res.error.code !== "ENOENT") {
|
|
445
445
|
lastFailure = { bin, reason: String(res.error.message || res.error.code || res.error) };
|
|
@@ -738,6 +738,26 @@ function main(env, stdio) {
|
|
|
738
738
|
? { ...payload, entries: payload.entries.slice(0, topK) }
|
|
739
739
|
: payload;
|
|
740
740
|
|
|
741
|
+
// rc.35 TASK-06 (P0-10.b): summary-fallback substitution. Entries whose
|
|
742
|
+
// description.summary equals stable_id render as "<id> · <id>" and the
|
|
743
|
+
// AI skips fetching them; the fallback reads `## Summary` from the
|
|
744
|
+
// entry's .md file and swaps in the first paragraph. Best-effort —
|
|
745
|
+
// failure leaves the original opaque summary untouched.
|
|
746
|
+
let resolvedPayload = slicedPayload;
|
|
747
|
+
try {
|
|
748
|
+
if (slicedPayload && Array.isArray(slicedPayload.entries)) {
|
|
749
|
+
const resolvedEntries = resolveOpaqueSummaries(
|
|
750
|
+
slicedPayload.entries,
|
|
751
|
+
cwd,
|
|
752
|
+
typeof slicedPayload.revision_hash === "string" ? slicedPayload.revision_hash : "",
|
|
753
|
+
);
|
|
754
|
+
resolvedPayload = { ...slicedPayload, entries: resolvedEntries };
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
// resolveOpaqueSummaries swallows its own errors; this catch is belt
|
|
758
|
+
// + suspenders for any unexpected exception from the lib layer.
|
|
759
|
+
}
|
|
760
|
+
|
|
741
761
|
// rc.8 underseed self-check: decide whether to surface the one-line
|
|
742
762
|
// `/fabric-import` recommendation banner alongside the broad summary.
|
|
743
763
|
const recommendImport = shouldRecommendImport(cwd);
|
|
@@ -749,7 +769,7 @@ function main(env, stdio) {
|
|
|
749
769
|
// for the agent's working memory. rc.33 W2-5 reintroduces an opt-in
|
|
750
770
|
// hours-based cooldown via fabric-config (see gate above).
|
|
751
771
|
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
752
|
-
const lines = renderSummary(
|
|
772
|
+
const lines = renderSummary(resolvedPayload, summaryMaxLen);
|
|
753
773
|
|
|
754
774
|
if (recommendImport) {
|
|
755
775
|
// rc.16 TASK-003: resolve fabric_language ONCE per invocation (only when
|
|
@@ -75,6 +75,12 @@ const {
|
|
|
75
75
|
} = require("node:fs");
|
|
76
76
|
const { dirname, join } = require("node:path");
|
|
77
77
|
|
|
78
|
+
// rc.35 TASK-06 (P0-10.b): summary-fallback. Substitutes opaque entries
|
|
79
|
+
// (where description.summary === stable_id) with a snippet read from the
|
|
80
|
+
// entry's .md `## Summary` section. Caches results in
|
|
81
|
+
// `.fabric/.cache/summary-fallback.json` keyed by revision_hash.
|
|
82
|
+
const { resolveOpaqueSummaries } = require("./lib/summary-fallback.cjs");
|
|
83
|
+
|
|
78
84
|
// -----------------------------------------------------------------------------
|
|
79
85
|
// CONSTANTS
|
|
80
86
|
// -----------------------------------------------------------------------------
|
|
@@ -95,6 +101,14 @@ const DEFAULT_SUMMARY_MAX_LEN = 80;
|
|
|
95
101
|
const EDIT_COUNTER_DIR_REL = join(".fabric", ".cache");
|
|
96
102
|
const EDIT_COUNTER_FILE = "edit-counter";
|
|
97
103
|
|
|
104
|
+
// rc.35 TASK-07 (P0-2): events.jsonl path. PreToolUse Edit fires append a
|
|
105
|
+
// `edit_intent_checked` event (ledger_source: 'hook') so doctor cite-
|
|
106
|
+
// coverage's editsTouched metric sees actual edit signals. Without this
|
|
107
|
+
// signal the entire cite-policy contract validation is structurally inert
|
|
108
|
+
// (rc.30 audit P0-2: 18582 turns / 240 edits / 0 events).
|
|
109
|
+
const EVENTS_LEDGER_DIR_REL = ".fabric";
|
|
110
|
+
const EVENTS_LEDGER_FILE = "events.jsonl";
|
|
111
|
+
|
|
98
112
|
// rc.6 TASK-023 (E6): hint-silence-counter sidecar — companion to the
|
|
99
113
|
// edit-counter above. Where edit-counter records every PreToolUse fire
|
|
100
114
|
// (numerator-agnostic), the silence-counter records only those fires that
|
|
@@ -317,6 +331,72 @@ function extractPaths(toolInput) {
|
|
|
317
331
|
* array. The fire-count signal is preserved; the activity overview
|
|
318
332
|
* just contributes nothing from those lines.
|
|
319
333
|
*/
|
|
334
|
+
/**
|
|
335
|
+
* rc.35 TASK-07 (P0-2): append one `edit_intent_checked` event per touched
|
|
336
|
+
* path to `.fabric/events.jsonl`. Carries `ledger_source: 'hook'` so doctor
|
|
337
|
+
* cite-coverage can distinguish hook-originated edit signals from
|
|
338
|
+
* AI/human-originated `appendLedgerEntry` calls.
|
|
339
|
+
*
|
|
340
|
+
* Best-effort:
|
|
341
|
+
* - Skips silently when `.fabric/` does not exist (project not init'd).
|
|
342
|
+
* - Skips silently when paths is empty (counter signal is preserved by
|
|
343
|
+
* the sibling appendEditCounter call; cite-coverage only cares about
|
|
344
|
+
* non-empty path events).
|
|
345
|
+
* - ANY error (mkdir, append, JSON throw) is swallowed — the hook must
|
|
346
|
+
* remain non-blocking per the rc.6 contract.
|
|
347
|
+
*
|
|
348
|
+
* Atomicity:
|
|
349
|
+
* - One JSON line per path. Append on small writes (< PIPE_BUF, ~4KB on
|
|
350
|
+
* POSIX) is atomic at the OS level, so concurrent PreToolUse fires
|
|
351
|
+
* from parallel sessions interleave cleanly without partial writes.
|
|
352
|
+
*/
|
|
353
|
+
function appendEditIntentToLedger(projectRoot, now, paths, toolName) {
|
|
354
|
+
try {
|
|
355
|
+
const fabricDir = join(projectRoot, EVENTS_LEDGER_DIR_REL);
|
|
356
|
+
// No .fabric/ → project not initialised. Bail before any write.
|
|
357
|
+
if (!existsSync(fabricDir)) return;
|
|
358
|
+
const { isAbsolute: pathIsAbsolute, relative: pathRelative } = require("node:path");
|
|
359
|
+
const pathList = Array.isArray(paths)
|
|
360
|
+
? paths
|
|
361
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
362
|
+
.map((p) => {
|
|
363
|
+
if (pathIsAbsolute(p)) {
|
|
364
|
+
const rel = pathRelative(projectRoot, p);
|
|
365
|
+
return rel.startsWith("..") ? null : rel;
|
|
366
|
+
}
|
|
367
|
+
// Already-relative paths: drop ones that escape the project tree.
|
|
368
|
+
return p.startsWith("..") ? null : p;
|
|
369
|
+
})
|
|
370
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
371
|
+
// Use forward slashes for cross-platform consistency on disk.
|
|
372
|
+
.map((p) => p.split(/[\\/]/).join("/"))
|
|
373
|
+
: [];
|
|
374
|
+
if (pathList.length === 0) return;
|
|
375
|
+
const tsMs = now instanceof Date ? now.getTime() : Number(now);
|
|
376
|
+
const ledgerEntryId = `hook:${randomUUID()}`;
|
|
377
|
+
const intent = typeof toolName === "string" && toolName.length > 0 ? toolName : "edit";
|
|
378
|
+
const lines = pathList
|
|
379
|
+
.map((p) => JSON.stringify({
|
|
380
|
+
kind: "fabric-event",
|
|
381
|
+
id: `event:${randomUUID()}`,
|
|
382
|
+
ts: tsMs,
|
|
383
|
+
schema_version: 1,
|
|
384
|
+
event_type: "edit_intent_checked",
|
|
385
|
+
path: p,
|
|
386
|
+
compliant: true,
|
|
387
|
+
intent,
|
|
388
|
+
ledger_entry_id: ledgerEntryId,
|
|
389
|
+
ledger_source: "hook",
|
|
390
|
+
matched_rule_context_ts: null,
|
|
391
|
+
window_ms: 0,
|
|
392
|
+
}))
|
|
393
|
+
.join("\n") + "\n";
|
|
394
|
+
appendFileSync(join(fabricDir, EVENTS_LEDGER_FILE), lines, "utf8");
|
|
395
|
+
} catch {
|
|
396
|
+
// Silent — events ledger failure must never block the edit.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
320
400
|
function appendEditCounter(projectRoot, now, paths) {
|
|
321
401
|
try {
|
|
322
402
|
const dir = join(projectRoot, EDIT_COUNTER_DIR_REL);
|
|
@@ -1079,6 +1159,11 @@ function main(env, stdio) {
|
|
|
1079
1159
|
}
|
|
1080
1160
|
if (!(env && env.skipCounter === true)) {
|
|
1081
1161
|
appendEditCounter(cwd, now, paths);
|
|
1162
|
+
// rc.35 TASK-07 (P0-2): mirror the edit-counter sidecar into the
|
|
1163
|
+
// events.jsonl ledger so doctor cite-coverage's editsTouched metric
|
|
1164
|
+
// sees actual edit signals. Best-effort — failure is swallowed inside
|
|
1165
|
+
// appendEditIntentToLedger and does not block the hook.
|
|
1166
|
+
appendEditIntentToLedger(cwd, now, paths, toolName);
|
|
1082
1167
|
}
|
|
1083
1168
|
|
|
1084
1169
|
// E2 path is conditional on a recognized tool + extractable paths.
|
|
@@ -1237,7 +1322,21 @@ function main(env, stdio) {
|
|
|
1237
1322
|
}
|
|
1238
1323
|
|
|
1239
1324
|
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
1240
|
-
|
|
1325
|
+
// rc.35 TASK-06 (P0-10.b): substitute opaque summaries before render.
|
|
1326
|
+
// Same lib used by the broad hook — opaque entries seen from both call
|
|
1327
|
+
// sites share a single .fabric/.cache/summary-fallback.json file.
|
|
1328
|
+
// Best-effort — any failure leaves the original opaque summary intact.
|
|
1329
|
+
let resolvedEntries = dedupDecision.filtered;
|
|
1330
|
+
try {
|
|
1331
|
+
resolvedEntries = resolveOpaqueSummaries(
|
|
1332
|
+
dedupDecision.filtered,
|
|
1333
|
+
cwd,
|
|
1334
|
+
currentRevisionHash,
|
|
1335
|
+
);
|
|
1336
|
+
} catch {
|
|
1337
|
+
// resolveOpaqueSummaries swallows its own errors; defensive catch.
|
|
1338
|
+
}
|
|
1339
|
+
const lines = renderSummary({ ...cliPayload, entries: resolvedEntries }, summaryMaxLen);
|
|
1241
1340
|
if (lines.length === 0) return;
|
|
1242
1341
|
|
|
1243
1342
|
// Stderr: human-facing breadcrumb + legacy contract.
|
|
@@ -1294,6 +1393,10 @@ module.exports = {
|
|
|
1294
1393
|
renderSummary,
|
|
1295
1394
|
truncateSummary,
|
|
1296
1395
|
formatEntryLine,
|
|
1396
|
+
// rc.35 TASK-07 (P0-2): cite-infrastructure wire-up. Exported so the
|
|
1397
|
+
// integration test can drive the writer directly without standing up the
|
|
1398
|
+
// entire PreToolUse main() flow.
|
|
1399
|
+
appendEditIntentToLedger,
|
|
1297
1400
|
// rc.6 TASK-021 (E3) — session-hints cache exports for tests / future
|
|
1298
1401
|
// consumers (TASK-023 silence-counter telemetry will reuse the same
|
|
1299
1402
|
// session-id resolution + cache shape).
|
|
@@ -1323,6 +1426,8 @@ module.exports = {
|
|
|
1323
1426
|
EDIT_COUNTER_FILE,
|
|
1324
1427
|
HINT_SILENCE_COUNTER_DIR_REL,
|
|
1325
1428
|
HINT_SILENCE_COUNTER_FILE,
|
|
1429
|
+
EVENTS_LEDGER_DIR_REL,
|
|
1430
|
+
EVENTS_LEDGER_FILE,
|
|
1326
1431
|
EDIT_TOOL_NAMES,
|
|
1327
1432
|
SESSION_HINTS_DIR_REL,
|
|
1328
1433
|
SESSION_HINTS_FILE_PREFIX,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rc.35 TASK-06 (P0-10.b) — summary-fallback library.
|
|
3
|
+
*
|
|
4
|
+
* Resolves opaque hint entries (where `entry.summary === entry.id` so the
|
|
5
|
+
* AI sees no information beyond the id) by reading the entry's markdown
|
|
6
|
+
* file at `.fabric/knowledge/<type>/<id>--<slug>.md`, extracting the first
|
|
7
|
+
* paragraph under `## Summary`, and substituting that text into the entry
|
|
8
|
+
* before the hook renders it.
|
|
9
|
+
*
|
|
10
|
+
* Caching: results are stored in `.fabric/.cache/summary-fallback.json`
|
|
11
|
+
* keyed by the current `revision_hash` returned by plan-context-hint. The
|
|
12
|
+
* cache is wiped wholesale when the revision changes (cheap invariant —
|
|
13
|
+
* any meta rev bump implies entry text MAY have moved). Per-process call
|
|
14
|
+
* also benefits from in-memory dedup since the same opaque id may appear
|
|
15
|
+
* across narrow + broad paths.
|
|
16
|
+
*
|
|
17
|
+
* Design contract:
|
|
18
|
+
* - Never throw. ANY failure (cache read, fs scan, file read) degrades
|
|
19
|
+
* to a no-op — the original opaque summary is left untouched. Hooks
|
|
20
|
+
* must remain best-effort.
|
|
21
|
+
* - Idempotent over identical inputs. Two calls in succession with the
|
|
22
|
+
* same revision_hash + entries set produce zero disk reads on the
|
|
23
|
+
* second call.
|
|
24
|
+
*
|
|
25
|
+
* Public API (module.exports):
|
|
26
|
+
* resolveOpaqueSummaries(entries, projectRoot, revisionHash) — returns
|
|
27
|
+
* a NEW array of entries with `summary` substituted for opaque cases.
|
|
28
|
+
* Original `entry.id` is preserved verbatim.
|
|
29
|
+
*
|
|
30
|
+
* _extractFirstSummaryParagraph(md) — pure helper, exposed for testing.
|
|
31
|
+
*
|
|
32
|
+
* _readCache / _writeCache — exposed for testing.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } = require("node:fs");
|
|
36
|
+
const { join } = require("node:path");
|
|
37
|
+
|
|
38
|
+
const CACHE_DIR_REL = ".fabric/.cache";
|
|
39
|
+
const CACHE_FILE_REL = ".fabric/.cache/summary-fallback.json";
|
|
40
|
+
const KNOWLEDGE_DIR_REL = ".fabric/knowledge";
|
|
41
|
+
const SUMMARY_MAX_LEN = 80;
|
|
42
|
+
const KNOWLEDGE_TYPE_DIRS = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
43
|
+
|
|
44
|
+
function _isOpaque(entry) {
|
|
45
|
+
if (!entry || typeof entry.id !== "string" || typeof entry.summary !== "string") {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return entry.summary.trim() === entry.id.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pure helper: extract the first paragraph under a `## Summary` heading.
|
|
53
|
+
*
|
|
54
|
+
* - `## Summary` is case-insensitive but level-sensitive (only H2).
|
|
55
|
+
* - First paragraph = lines until blank line or next heading.
|
|
56
|
+
* - Collapses whitespace + trims; returns `""` if no summary section or
|
|
57
|
+
* the section is empty.
|
|
58
|
+
*/
|
|
59
|
+
function _extractFirstSummaryParagraph(md) {
|
|
60
|
+
if (typeof md !== "string" || md.length === 0) return "";
|
|
61
|
+
const lines = md.split(/\r?\n/);
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
if (/^##\s+summary\s*$/i.test(lines[i].trim())) {
|
|
65
|
+
i += 1;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
if (i >= lines.length) return "";
|
|
71
|
+
// Skip blank lines after the heading
|
|
72
|
+
while (i < lines.length && lines[i].trim().length === 0) i += 1;
|
|
73
|
+
// Collect until the next blank line or next heading
|
|
74
|
+
const buf = [];
|
|
75
|
+
while (i < lines.length) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
if (line.trim().length === 0) break;
|
|
78
|
+
if (/^#{1,6}\s/.test(line.trim())) break;
|
|
79
|
+
buf.push(line.trim());
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
const flat = buf.join(" ").replace(/\s+/g, " ").trim();
|
|
83
|
+
if (flat.length === 0) return "";
|
|
84
|
+
if (flat.length <= SUMMARY_MAX_LEN) return flat;
|
|
85
|
+
return `${flat.slice(0, SUMMARY_MAX_LEN - 1)}…`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _readCache(projectRoot) {
|
|
89
|
+
const cachePath = join(projectRoot, CACHE_FILE_REL);
|
|
90
|
+
if (!existsSync(cachePath)) return null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = readFileSync(cachePath, "utf8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (parsed && typeof parsed === "object" && typeof parsed.revision === "string" && parsed.summaries && typeof parsed.summaries === "object") {
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore — caller treats null as no-cache
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _writeCache(projectRoot, payload) {
|
|
104
|
+
try {
|
|
105
|
+
const cacheDir = join(projectRoot, CACHE_DIR_REL);
|
|
106
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
107
|
+
const cachePath = join(projectRoot, CACHE_FILE_REL);
|
|
108
|
+
writeFileSync(cachePath, JSON.stringify(payload), "utf8");
|
|
109
|
+
} catch {
|
|
110
|
+
// Best-effort — failing to persist cache is not an error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Scan `.fabric/knowledge/<type>/` for the canonical `<id>--<slug>.md`
|
|
116
|
+
* matching `stableId`. Tries the most likely type-dir first based on the
|
|
117
|
+
* entry's `type` hint, then falls back to scanning all canonical type
|
|
118
|
+
* directories. Returns the absolute path or null.
|
|
119
|
+
*
|
|
120
|
+
* The id→file mapping is unique by construction (stable_id is allocated
|
|
121
|
+
* once per file), so the first match wins.
|
|
122
|
+
*/
|
|
123
|
+
function _findEntryFile(projectRoot, stableId, typeHint) {
|
|
124
|
+
const baseDir = join(projectRoot, KNOWLEDGE_DIR_REL);
|
|
125
|
+
if (!existsSync(baseDir)) return null;
|
|
126
|
+
const tryOrder = [];
|
|
127
|
+
if (typeof typeHint === "string" && typeHint.length > 0) {
|
|
128
|
+
// Accept both singular and plural hints — find the plural form.
|
|
129
|
+
const lower = typeHint.toLowerCase();
|
|
130
|
+
const plural = KNOWLEDGE_TYPE_DIRS.find((d) => d === lower || d.startsWith(lower));
|
|
131
|
+
if (plural) tryOrder.push(plural);
|
|
132
|
+
}
|
|
133
|
+
for (const t of KNOWLEDGE_TYPE_DIRS) {
|
|
134
|
+
if (!tryOrder.includes(t)) tryOrder.push(t);
|
|
135
|
+
}
|
|
136
|
+
const prefix = `${stableId}--`;
|
|
137
|
+
for (const t of tryOrder) {
|
|
138
|
+
const typeDir = join(baseDir, t);
|
|
139
|
+
if (!existsSync(typeDir)) continue;
|
|
140
|
+
let files;
|
|
141
|
+
try {
|
|
142
|
+
files = readdirSync(typeDir);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const f of files) {
|
|
147
|
+
if (f.startsWith(prefix) && f.endsWith(".md")) {
|
|
148
|
+
return join(typeDir, f);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _resolveOne(projectRoot, entry) {
|
|
156
|
+
const filePath = _findEntryFile(projectRoot, entry.id, entry.type);
|
|
157
|
+
if (filePath === null) return "";
|
|
158
|
+
let md;
|
|
159
|
+
try {
|
|
160
|
+
md = readFileSync(filePath, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
return _extractFirstSummaryParagraph(md);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Main API. Returns a new array of entries with `summary` swapped for
|
|
169
|
+
* the extracted fallback wherever the original summary was opaque AND
|
|
170
|
+
* the fallback extraction yielded a non-empty string. Non-opaque entries
|
|
171
|
+
* pass through unchanged.
|
|
172
|
+
*/
|
|
173
|
+
function resolveOpaqueSummaries(entries, projectRoot, revisionHash) {
|
|
174
|
+
if (!Array.isArray(entries) || entries.length === 0) return entries;
|
|
175
|
+
const cache = _readCache(projectRoot);
|
|
176
|
+
const cachedSummaries = cache && cache.revision === revisionHash && cache.summaries ? cache.summaries : {};
|
|
177
|
+
const nextCacheSummaries = { ...cachedSummaries };
|
|
178
|
+
let cacheChanged = cache === null || cache.revision !== revisionHash;
|
|
179
|
+
const result = entries.map((entry) => {
|
|
180
|
+
if (!_isOpaque(entry)) return entry;
|
|
181
|
+
const id = entry.id;
|
|
182
|
+
let fallback;
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(cachedSummaries, id)) {
|
|
184
|
+
fallback = cachedSummaries[id];
|
|
185
|
+
} else {
|
|
186
|
+
fallback = _resolveOne(projectRoot, entry);
|
|
187
|
+
nextCacheSummaries[id] = fallback;
|
|
188
|
+
cacheChanged = true;
|
|
189
|
+
}
|
|
190
|
+
if (typeof fallback === "string" && fallback.length > 0) {
|
|
191
|
+
return { ...entry, summary: fallback };
|
|
192
|
+
}
|
|
193
|
+
return entry;
|
|
194
|
+
});
|
|
195
|
+
if (cacheChanged) {
|
|
196
|
+
_writeCache(projectRoot, { revision: revisionHash, summaries: nextCacheSummaries });
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
resolveOpaqueSummaries,
|
|
203
|
+
_extractFirstSummaryParagraph,
|
|
204
|
+
_readCache,
|
|
205
|
+
_writeCache,
|
|
206
|
+
_findEntryFile,
|
|
207
|
+
_isOpaque,
|
|
208
|
+
SUMMARY_MAX_LEN,
|
|
209
|
+
KNOWLEDGE_TYPE_DIRS,
|
|
210
|
+
};
|
|
@@ -53,7 +53,7 @@ Graceful degradation: missing digest cache → single-session fallback. Missing
|
|
|
53
53
|
|
|
54
54
|
### Phase 1.5 — First-run Onboard (ref-only)
|
|
55
55
|
|
|
56
|
-
**SKIP this phase entirely unless** entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `
|
|
56
|
+
**SKIP this phase entirely unless** entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `fabric onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5, silently fall through to Phase 0.
|
|
57
57
|
|
|
58
58
|
`Read ref/phase-1-5-onboard.md` for the Step 1-4 coverage check → user prompt → tour-and-propose procedure.
|
|
59
59
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| `fab_extract_knowledge` MCP call (Phase 4) | One call per confirmed candidate, writes to `.fabric/knowledge/pending/<slug>.md` | SKIPPED. Phase 4 renders "would write N pending entries" preview table instead. |
|
|
8
8
|
| `session_archive_attempted` event (Phase 4.5) | Appended to `.fabric/events.jsonl` for every session in scope | SKIPPED entirely. No ledger entry. |
|
|
9
9
|
| `fab_review reject` (Phase 3 user-dismissed branch) | Invoked when user types `撤销` / `reject` after self-archive proposal | SKIPPED. The dismissal is rendered to console but no MCP write occurs. |
|
|
10
|
-
| `
|
|
10
|
+
| `fabric onboard-coverage` slot writes (Phase 1.5 fill-all / dismiss-all) | Each `Bash("fabric config dismiss-slot <slot>")` invocation runs | SKIPPED. Slot decisions are shown as "would dismiss/propose" preview. |
|
|
11
11
|
| `.fabric/.cache/session-digests/<session_id>.md` reads | Read freely (read-side, safe) | Read freely — same as normal. |
|
|
12
12
|
| Stop-hook / archive-hint stdin/stdout | Read-only inspection of `.fabric/events.jsonl` | Same — no change. |
|
|
13
13
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
`今日复盘` = E5 entry point. Default scope = today. Falls back to historical scan if today yields no candidates (silent-skip per Phase 4.5).
|
|
10
10
|
|
|
11
|
-
E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。
|
|
11
|
+
E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。fabric 端**零代码**——不提供 `fabric schedule` 子命令,亦不内嵌 daemon。用户基于自己的执行环境二选一接入: `/loop`(Claude Code 原生,推荐) 或 OS cron(跨平台 fallback)。
|
|
12
12
|
|
|
13
13
|
### /loop sample (primary path for Claude Code)
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ Rendering rule:
|
|
|
34
34
|
|
|
35
35
|
- `fabric_language === "zh-CN"` → emit the zh-CN variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
36
36
|
- `fabric_language === "en"` → emit the en variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
37
|
-
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `
|
|
37
|
+
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
|
|
38
38
|
- `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
|
|
39
39
|
|
|
40
40
|
Protected tokens (`fab_extract_knowledge`, `relevance_scope`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Phase 1.5 — First-run Onboard Phase (ref)
|
|
2
2
|
|
|
3
|
-
> **Loaded on demand.** SKILL.md hot path only runs this when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `
|
|
3
|
+
> **Loaded on demand.** SKILL.md hot path only runs this when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `fabric onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5 entries OR fully-covered workspaces, this entire phase is skipped — no reason to load.
|
|
4
4
|
|
|
5
5
|
## Phase 1.5 — First-run Onboard Phase
|
|
6
6
|
|
|
@@ -55,7 +55,7 @@ tone.
|
|
|
55
55
|
A first-time user whose ONLY invocations ever come via hook (never an
|
|
56
56
|
explicit `/fabric-archive`) will not see the onboard prompt; the 5
|
|
57
57
|
onboard slots remain empty. Mitigation: documentation tells users to
|
|
58
|
-
run an explicit `
|
|
58
|
+
run an explicit `fabric archive` at least once to populate the onboard
|
|
59
59
|
baseline.
|
|
60
60
|
|
|
61
61
|
##### Worked example
|
|
@@ -82,7 +82,7 @@ $ /fabric-archive
|
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
85
|
-
After F8a removed the auto-`
|
|
85
|
+
After F8a removed the auto-`fabric scan` baseline pipeline, a freshly installed
|
|
86
86
|
Fabric workspace ships with an EMPTY `.fabric/knowledge/` tree. Five fixed
|
|
87
87
|
**S5 onboard slots** capture the "project tone" baseline that the AI needs
|
|
88
88
|
for high-quality plan_context retrieval from day one:
|
|
@@ -98,10 +98,10 @@ gathering, so coverage state is fresh for the session.
|
|
|
98
98
|
|
|
99
99
|
#### Step 1 — Check coverage
|
|
100
100
|
|
|
101
|
-
Invoke `
|
|
101
|
+
Invoke `fabric onboard-coverage --json` and parse the JSON payload:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
|
|
104
|
+
fabric onboard-coverage --json
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
Expected shape:
|
|
@@ -147,8 +147,8 @@ proposed entry counts toward coverage once approved via fab_review.
|
|
|
147
147
|
| User choice | Action |
|
|
148
148
|
|----------------|--------|
|
|
149
149
|
| `fill-all` | For EACH slot in `missing`, run Step 4 (Tour-and-propose). All proposals share session_id; one batch review at the end (Phase 3). |
|
|
150
|
-
| `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `
|
|
151
|
-
| `dismiss-all` | For EACH slot in `missing`, invoke `Bash("
|
|
150
|
+
| `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `fabric config dismiss-slot <slot>`; `skip` → leave for next archive run. |
|
|
151
|
+
| `dismiss-all` | For EACH slot in `missing`, invoke `Bash("fabric config dismiss-slot <slot>")`. Print a one-line confirmation each. Skip to Phase 0. |
|
|
152
152
|
| `skip` | No-op. Slots remain in `missing` for the next archive run. Skip to Phase 0. |
|
|
153
153
|
|
|
154
154
|
#### Step 4 — Tour-and-propose (per-slot)
|
|
@@ -174,7 +174,7 @@ After Read-ing the slot-specific sources, classify the observation:
|
|
|
174
174
|
|
|
175
175
|
Call `fab_extract_knowledge` with the inferred fields PLUS `onboard_slot:
|
|
176
176
|
<slot>`. The pending file's frontmatter will carry the slot label, and the
|
|
177
|
-
next `
|
|
177
|
+
next `fabric onboard-coverage` run will see the slot as filled (once approved
|
|
178
178
|
via fab_review).
|
|
179
179
|
|
|
180
180
|
Example:
|
|
@@ -201,9 +201,9 @@ mcp__fabric__fab_extract_knowledge({
|
|
|
201
201
|
|
|
202
202
|
- MUST run BEFORE Phase 2 evidence gathering — onboard is a separate flow,
|
|
203
203
|
not interleaved with session-archive candidates.
|
|
204
|
-
- MUST call `
|
|
204
|
+
- MUST call `fabric onboard-coverage --json` before deciding; never assume
|
|
205
205
|
coverage state.
|
|
206
|
-
- NEVER fill a slot that is in `opted_out` — `
|
|
206
|
+
- NEVER fill a slot that is in `opted_out` — `fabric onboard-coverage` already
|
|
207
207
|
excludes those from `missing`, but the Skill MUST NOT re-propose them
|
|
208
208
|
even if the user asks "fill all of them" — the dismiss is intentional.
|
|
209
209
|
- NEVER prompt the user when `missing.length === 0` — silent skip.
|
|
@@ -29,7 +29,7 @@ Rendering rule:
|
|
|
29
29
|
|
|
30
30
|
- `fabric_language === "zh-CN"` → emit the zh-CN variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
31
31
|
- `fabric_language === "en"` → emit the en variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
32
|
-
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `
|
|
32
|
+
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
|
|
33
33
|
- `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
|
|
34
34
|
|
|
35
35
|
Protected tokens (`fab_extract_knowledge`, `fab_review`, `relevance_scope`,
|
|
@@ -33,7 +33,7 @@ Rendering rule:
|
|
|
33
33
|
|
|
34
34
|
- `fabric_language === "zh-CN"` → emit the zh-CN variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
35
35
|
- `fabric_language === "en"` → emit the en variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
36
|
-
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `
|
|
36
|
+
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
|
|
37
37
|
- `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
|
|
38
38
|
|
|
39
39
|
Protected tokens (`fab_review`, `relevance_scope`, `relevance_paths`,
|