@fenglimg/fabric-cli 2.0.0-rc.34 → 2.0.0-rc.36

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-XSUIX6AD.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.36",
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.36" : "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.36",
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.36",
23
+ "@fenglimg/fabric-shared": "2.0.0-rc.36"
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,