@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fenglimg/fabric-cli
2
2
 
3
- `fabric` 是 Fabric 的主命令,`fab` 是永久别名,两者等价。
3
+ `fabric` 是 Fabric CLI 主命令。
4
4
 
5
5
  ## 快速开始
6
6
 
@@ -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 \`fab config onboard-reset ${slot}\` to re-open.`);
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 \`fab onboard-coverage\` as missing again.`);
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,
@@ -3,7 +3,7 @@ import {
3
3
  configCmd,
4
4
  config_default,
5
5
  installMcpClients
6
- } from "./chunk-SRX7WZUG.js";
6
+ } from "./chunk-BATF4PEJ.js";
7
7
  import "./chunk-MF3OTILQ.js";
8
8
  import "./chunk-PWLW3B57.js";
9
9
  export {
@@ -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
- writeIssueSection(dt("doctor.section.fixable"), report.fixable_errors);
316
- writeIssueSection(dt("doctor.section.manual"), report.manual_errors);
317
- writeIssueSection(dt("doctor.section.warnings"), report.warnings);
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
- writeStdout(` \u2192 ${issue.actionHint}`);
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("fab doctor --enrich-descriptions")} mode=${report.mode}${report.dryRun ? " (dry-run)" : ""} scanned=${report.scanned} modified=${report.modified} skipped=${report.skipped}`;
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-XCRX34CX.js").then((module) => module.default),
15
- doctor: () => import("./doctor-E26YO67D.js").then((module) => module.default),
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-Q7V55BXH.js").then((module) => module.default),
18
- config: () => import("./config-5CH4EJQ2.js").then((module) => module.default),
19
- "plan-context-hint": () => import("./plan-context-hint-CXTLNVSV.js").then((module) => module.default),
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-6MN3CYHT.js").then((module) => module.default)
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.34",
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-SRX7WZUG.js";
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-5N3KXIVI.js";
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: `fab-cli@${getCliVersion()}`,
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.34" : "unknown";
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 `fab config onboard-reset` to re-open)";
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 `fab --help` rendered zh-CN, so Chinese-locale
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 `fab --help` so the top-level
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 `fab onboard-coverage --json`.
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 `fab --help` listing. The command stays
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 `fab plan-context-hint ...`; it just no longer appears in the
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
  },
@@ -7,7 +7,7 @@ import {
7
7
  HOOK_SCRIPT_DESTINATIONS,
8
8
  SKILL_DESTINATIONS,
9
9
  fabricAgentsSnapshotPath
10
- } from "./chunk-5N3KXIVI.js";
10
+ } from "./chunk-XVS4F3P6.js";
11
11
  import {
12
12
  detectClientSupports,
13
13
  resolveClients
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.0.0-rc.34",
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.34",
24
- "@fenglimg/fabric-shared": "2.0.0-rc.34"
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: fab doctor --cite-coverage — this rule does not block work, only records.",
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
- * spawn strategy: try `fabric` first (user-PATH install) then `fab` (the
419
- * alternate bin name shipped by @fenglimg/fabric-cli). If neither is on PATH,
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", "fab"];
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 expected for `fabric` when only `fab` is shipped).
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(slicedPayload, summaryMaxLen);
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
- const lines = renderSummary({ ...cliPayload, entries: dedupDecision.filtered }, summaryMaxLen);
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 `fab onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5, silently fall through to Phase 0.
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
- | `fab onboard-coverage` slot writes (Phase 1.5 fill-all / dismiss-all) | Each `Bash("fab config dismiss-slot <slot>")` invocation runs | SKIPPED. Slot decisions are shown as "would dismiss/propose" preview. |
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` 周期触发的入口形态。fab 端**零代码**——不提供 `fab schedule` 子命令,亦不内嵌 daemon。用户基于自己的执行环境二选一接入: `/loop`(Claude Code 原生,推荐) 或 OS cron(跨平台 fallback)。
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. `fab install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
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 `fab 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.
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 `fab archive` at least once to populate the onboard
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-`fab scan` baseline pipeline, a freshly installed
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 `fab onboard-coverage --json` and parse the JSON payload:
101
+ Invoke `fabric onboard-coverage --json` and parse the JSON payload:
102
102
 
103
103
  ```bash
104
- fab onboard-coverage --json
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` → `fab config dismiss-slot <slot>`; `skip` → leave for next archive run. |
151
- | `dismiss-all` | For EACH slot in `missing`, invoke `Bash("fab config dismiss-slot <slot>")`. Print a one-line confirmation each. Skip to Phase 0. |
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 `fab onboard-coverage` run will see the slot as filled (once approved
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 `fab onboard-coverage --json` before deciding; never assume
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` — `fab onboard-coverage` already
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. `fab install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
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. `fab install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
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`,