@fenglimg/fabric-cli 2.2.0-rc.9 → 2.3.0-rc.1

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.
Files changed (76) hide show
  1. package/README.md +2 -2
  2. package/dist/audit-PURSJJFH.js +734 -0
  3. package/dist/{chunk-YM4XATJF.js → chunk-722JU5BP.js} +2 -0
  4. package/dist/{chunk-QPAW6IYT.js → chunk-7V4XMLQ2.js} +3 -3
  5. package/dist/{chunk-7ZDXBOOU.js → chunk-ACSMNX3V.js} +44 -128
  6. package/dist/{chunk-PTGQAZEW.js → chunk-GGDVZCD6.js} +2 -4
  7. package/dist/{chunk-EOT63RDH.js → chunk-I5F5BHWI.js} +9 -0
  8. package/dist/chunk-PP7QVRXH.js +565 -0
  9. package/dist/chunk-SL77FXX7.js +54 -0
  10. package/dist/{chunk-3D7B2UAZ.js → chunk-VQKXTMWH.js} +44 -4
  11. package/dist/doctor-S6KPGS35.js +27 -0
  12. package/dist/index.js +91 -81
  13. package/dist/{info-7FKBTMVO.js → info-NJEY26H6.js} +91 -46
  14. package/dist/{context-7NUKXDB6.js → inspect-5YZMJPFM.js} +11 -11
  15. package/dist/{install-v2-I6PJ6IFT.js → install-v2-KGIDII4H.js} +163 -364
  16. package/dist/{plan-context-hint-G75R4P4J.js → plan-context-hint-5TNGH3R4.js} +1 -1
  17. package/dist/{store-HOCORVL3.js → store-GF4SFBMJ.js} +155 -57
  18. package/dist/{sync-DT5UJMMR.js → sync-3XCIRDPK.js} +3 -4
  19. package/dist/{uninstall-IFN2KYBK.js → uninstall-BG4ML4FC.js} +39 -10
  20. package/package.json +3 -7
  21. package/templates/hooks/cite-policy-evict.cjs +1 -1
  22. package/templates/hooks/configs/claude-code.json +1 -5
  23. package/templates/hooks/configs/codex-hooks.json +1 -5
  24. package/templates/hooks/fabric-hint.cjs +346 -138
  25. package/templates/hooks/knowledge-hint-broad.cjs +265 -75
  26. package/templates/hooks/knowledge-hint-narrow.cjs +3 -3
  27. package/templates/hooks/knowledge-pretooluse.cjs +111 -0
  28. package/templates/hooks/lib/banner-i18n.cjs +31 -12
  29. package/templates/hooks/lib/bindings-snapshot-reader.cjs +1 -1
  30. package/templates/hooks/lib/event-writer.cjs +79 -0
  31. package/templates/hooks/lib/nudge-policy.cjs +11 -0
  32. package/templates/hooks/lib/theme.cjs +62 -0
  33. package/templates/hooks/post-tooluse-mutation.cjs +28 -39
  34. package/templates/skills/fabric-archive/SKILL.md +43 -12
  35. package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
  36. package/templates/skills/fabric-archive/ref/i18n-policy.md +1 -1
  37. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +5 -5
  38. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +2 -2
  39. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
  40. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +1 -1
  41. package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +1 -1
  42. package/templates/skills/fabric-archive/ref/phase-3-classify.md +1 -1
  43. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +1 -1
  44. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +6 -5
  45. package/templates/skills/{fabric-import/ref/checkpoint-state.md → fabric-archive/ref/source-checkpoint.md} +3 -3
  46. package/templates/skills/{fabric-import/ref/phase-3-dedup.md → fabric-archive/ref/source-dedup.md} +4 -4
  47. package/templates/skills/{fabric-import/ref/phase-2-mining.md → fabric-archive/ref/source-mining.md} +20 -20
  48. package/templates/skills/{fabric-import/ref/output-contract.md → fabric-archive/ref/source-output-contract.md} +3 -3
  49. package/templates/skills/{fabric-import/ref/state-recovery.md → fabric-archive/ref/source-state-recovery.md} +2 -2
  50. package/templates/skills/{fabric-import/ref/worked-examples.md → fabric-archive/ref/source-worked-examples.md} +10 -10
  51. package/templates/skills/fabric-archive/ref/worked-examples.md +3 -3
  52. package/templates/skills/fabric-review/SKILL.md +28 -15
  53. package/templates/skills/fabric-review/ref/cite-contract.md +2 -2
  54. package/templates/skills/fabric-review/ref/modify-flow.md +13 -1
  55. package/templates/skills/fabric-review/ref/per-mode-flows.md +5 -5
  56. package/templates/skills/fabric-review/ref/relate-mode.md +33 -0
  57. package/templates/skills/fabric-review/ref/retire-mode.md +47 -0
  58. package/templates/skills/fabric-review/ref/semantic-check.md +1 -1
  59. package/templates/skills/fabric-review/ref/worked-examples.md +5 -5
  60. package/templates/skills/fabric-store/SKILL.md +12 -27
  61. package/templates/skills/fabric-sync/SKILL.md +16 -35
  62. package/templates/skills/lib/shared-policy.md +6 -4
  63. package/dist/chunk-27HK6H5Y.js +0 -69
  64. package/dist/chunk-E7HJUU34.js +0 -1096
  65. package/dist/chunk-NLNH64A3.js +0 -43
  66. package/dist/chunk-QFIVFZRH.js +0 -13
  67. package/dist/doctor-MDTZWKBK.js +0 -24
  68. package/dist/metrics-HMFH4YHK.js +0 -135
  69. package/dist/scope-explain-HLJZ2M33.js +0 -48
  70. package/dist/status-4R3TM4FJ.js +0 -37
  71. package/dist/whoami-ITGEFWH4.js +0 -49
  72. package/templates/skills/fabric/SKILL.md +0 -100
  73. package/templates/skills/fabric-audit/SKILL.md +0 -63
  74. package/templates/skills/fabric-connect/SKILL.md +0 -48
  75. package/templates/skills/fabric-import/SKILL.md +0 -151
  76. package/templates/skills/fabric-import/ref/i18n-policy.md +0 -78
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // ux-w2-6: the SINGLE PreToolUse hook. Previously the Edit|Write|MultiEdit
3
+ // matcher carried TWO commands — knowledge-hint-narrow.cjs (narrow KB hint) and
4
+ // cite-policy-evict.cjs (recall-before-edit nudge) — so a single edit produced
5
+ // TWO additionalContext envelopes (双弹). This orchestrator runs both in ONE
6
+ // process and merges their output into ONE envelope.
7
+ //
8
+ // narrow.cjs and cite-policy-evict.cjs stay as standalone modules (their full
9
+ // contract test-suites are unchanged); this entry imports them as libs, reads
10
+ // stdin ONCE (stdin is single-read), hands the parsed payload to each via the
11
+ // `env.payload` test seam, captures each one's stdout, and folds the two
12
+ // envelopes into a single `{ systemMessage?, hookSpecificOutput.additionalContext? }`.
13
+ // Each sub-hook stays best-effort/silent-on-failure, so a throw in one never
14
+ // blocks the edit or suppresses the other (KT-DEC-0007).
15
+
16
+ const narrow = require("./knowledge-hint-narrow.cjs");
17
+ const cite = require("./cite-policy-evict.cjs");
18
+
19
+ function readStdinPayload() {
20
+ try {
21
+ const raw = require("node:fs").readFileSync(0, "utf8");
22
+ if (!raw || raw.trim().length === 0) return null;
23
+ return JSON.parse(raw);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ // Parse a captured stdout chunk as a Claude Code hook envelope. Returns null for
30
+ // empty / non-JSON output (e.g. a stderr-only codex breadcrumb leaves stdout
31
+ // empty). Tolerant: a malformed line is ignored, never thrown.
32
+ function parseEnvelope(text) {
33
+ const trimmed = typeof text === "string" ? text.trim() : "";
34
+ if (trimmed.length === 0) return null;
35
+ try {
36
+ return JSON.parse(trimmed);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // Fold narrow + cite envelopes into one. additionalContext (AI sink) is
43
+ // concatenated (narrow hint first, then the cite nudge); systemMessage (human
44
+ // sink) likewise. Either side may be absent.
45
+ function mergeEnvelopes(narrowText, citeText) {
46
+ const a = parseEnvelope(narrowText);
47
+ const b = parseEnvelope(citeText);
48
+ if (a === null && b === null) return null;
49
+
50
+ const aiParts = [];
51
+ const humanParts = [];
52
+ let eventName = "PreToolUse";
53
+ for (const env of [a, b]) {
54
+ if (env === null) continue;
55
+ const hso = env.hookSpecificOutput;
56
+ if (hso && typeof hso.additionalContext === "string" && hso.additionalContext.length > 0) {
57
+ aiParts.push(hso.additionalContext);
58
+ if (typeof hso.hookEventName === "string") eventName = hso.hookEventName;
59
+ }
60
+ if (typeof env.systemMessage === "string" && env.systemMessage.length > 0) {
61
+ humanParts.push(env.systemMessage);
62
+ }
63
+ }
64
+
65
+ const merged = {};
66
+ if (humanParts.length > 0) merged.systemMessage = humanParts.join("\n");
67
+ if (aiParts.length > 0) {
68
+ merged.hookSpecificOutput = {
69
+ hookEventName: eventName,
70
+ additionalContext: aiParts.join("\n"),
71
+ };
72
+ }
73
+ return Object.keys(merged).length > 0 ? `${JSON.stringify(merged)}\n` : null;
74
+ }
75
+
76
+ async function main(env, stdio) {
77
+ try {
78
+ const out = (stdio && stdio.stdout) || process.stdout;
79
+ const err = (stdio && stdio.stderr) || process.stderr;
80
+ // Read stdin ONCE and share the parsed payload with both sub-hooks (stdin is
81
+ // a single-read stream — the prior two-command wiring read it twice).
82
+ const payload = env && env.payload !== undefined ? env.payload : readStdinPayload();
83
+ const sub = { ...(env || {}), payload };
84
+
85
+ const narrowChunks = [];
86
+ const citeChunks = [];
87
+ const capture = (sink) => ({ write: (c) => sink.push(String(c)) });
88
+
89
+ try {
90
+ await narrow.main(sub, { stdout: capture(narrowChunks), stderr: err });
91
+ } catch {
92
+ // narrow best-effort — never block the edit
93
+ }
94
+ try {
95
+ await cite.main(sub, { stdout: capture(citeChunks), stderr: err });
96
+ } catch {
97
+ // cite best-effort — never block the edit
98
+ }
99
+
100
+ const merged = mergeEnvelopes(narrowChunks.join(""), citeChunks.join(""));
101
+ if (merged !== null) out.write(merged);
102
+ } catch {
103
+ // Silent — the PreToolUse hook MUST NEVER block the edit on its own failure.
104
+ }
105
+ }
106
+
107
+ module.exports = { main, mergeEnvelopes, parseEnvelope };
108
+
109
+ if (require.main === module) {
110
+ main();
111
+ }
@@ -28,15 +28,16 @@
28
28
  *
29
29
  * - STRINGS — exported for test introspection only (read-only by convention).
30
30
  *
31
- * Banner keys (11 total):
31
+ * Banner keys:
32
32
  * Signal A (archive): archiveLine1, archiveActivity, archiveCta
33
+ * Archive backlog: backlogLine1, backlogCta
33
34
  * Signal B (review): reviewLine1, reviewCta
34
35
  * Signal C (import): importLine1, importCta
35
36
  * Signal D (maintenance): maintenanceLine1Never, maintenanceLine1Aged, maintenanceLine2
36
37
  * Broad hook: broadImportBanner
37
38
  *
38
39
  * Protected tokens — NEVER translated, kept verbatim across all 4 variants:
39
- * - Slash commands: /fabric-archive, /fabric-review, /fabric-import
40
+ * - Slash commands: /fabric-archive, /fabric-review, /fabric-archive
40
41
  * - CLI commands: `fabric doctor --lint`
41
42
  * - Numeric / template substrings the existing tests assert on:
42
43
  * "${hoursElapsed.toFixed(1)}h" (e.g. "25.0h"), "阈值 ${N}h",
@@ -165,6 +166,23 @@ const STRINGS = {
165
166
  "zh-CN-hybrid": () => " 是否调 /fabric-archive 检查值得归档的决策/踩坑/复用?",
166
167
  },
167
168
 
169
+ // ---- Archive backlog (cross-session safety net, crack 2) ------------------
170
+ // Replaces the old global-24h archive timer: counts DEAD sessions (session
171
+ // ended / idle) carrying unarchived high-value work. Substring "${count}" is
172
+ // addressable for tests. params: { count: number }
173
+ backlogLine1: {
174
+ "zh-CN": (p) => `📋 Fabric: ${p.count} 个已结束的会话有未归档的高价值改动。`,
175
+ en: (p) => `📋 Fabric: ${p.count} ended session(s) carry unarchived high-value work.`,
176
+ "zh-CN-hybrid": (p) => `📋 Fabric: ${p.count} 个已结束的会话有未归档的高价值改动。`,
177
+ },
178
+
179
+ // params: {} — protected token /fabric-archive verbatim across all variants.
180
+ backlogCta: {
181
+ "zh-CN": () => " 是否调 /fabric-archive 跨会话补归档这些遗漏?",
182
+ en: () => " Run /fabric-archive to sweep these missed sessions across the backlog?",
183
+ "zh-CN-hybrid": () => " 是否调 /fabric-archive 跨会话补归档这些遗漏?",
184
+ },
185
+
168
186
  // ---- Signal B: review -----------------------------------------------------
169
187
  // Source (zh-CN): fabric-hint.cjs:651 `📋 Fabric: 已积累 ${stats.count} 条待审核知识${ageSuffix}。`
170
188
  // params: { count, ageSuffix } — ageSuffix is " / 最早一条 N.N 天前" or "" (zh-CN only)
@@ -204,12 +222,13 @@ const STRINGS = {
204
222
  `📋 Fabric: 知识库节点数 ${p.nodeCount}/${p.threshold},距 init_scan_completed ${p.hoursSinceInit}h。`,
205
223
  },
206
224
 
207
- // Source (zh-CN): fabric-hint.cjs:698 ` 是否调 /fabric-import git 历史与现有文档回灌知识?`
208
- // params: {} protected token /fabric-import verbatim across all variants.
225
+ // W3-C: fabric-import folded into fabric-archive `source` mode — the underseed
226
+ // cold-start nudge now points at /fabric-archive (its source mode). Protected
227
+ // token /fabric-archive verbatim across all variants.
209
228
  importCta: {
210
- "zh-CN": () => " 是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
211
- en: () => " Run /fabric-import to backfill knowledge from git history and existing docs?",
212
- "zh-CN-hybrid": () => " 是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
229
+ "zh-CN": () => " 是否调 /fabric-archive 的 source mode 从 git 历史与现有文档回灌知识?",
230
+ en: () => " Run /fabric-archive source mode to backfill knowledge from git history and existing docs?",
231
+ "zh-CN-hybrid": () => " 是否调 /fabric-archive 的 source mode 从 git 历史与现有文档回灌知识?",
213
232
  },
214
233
 
215
234
  // ---- Signal D: maintenance -----------------------------------------------
@@ -271,15 +290,15 @@ const STRINGS = {
271
290
 
272
291
  // ---- Broad hook: import recommendation ------------------------------------
273
292
  // Source (zh-CN): knowledge-hint-broad.cjs:262
274
- // " 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?"
293
+ // " 📋 Fabric: 知识库稀疏,是否调 /fabric-archive 从 git 历史与现有文档回灌知识?"
275
294
  // Note: leading two spaces are intentional (existing banner indent).
276
- // params: {} — protected token /fabric-import verbatim.
295
+ // params: {} — protected token /fabric-archive verbatim.
277
296
  broadImportBanner: {
278
- "zh-CN": () => " 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
297
+ "zh-CN": () => " 📋 Fabric: 知识库稀疏,是否调 /fabric-archive 从 git 历史与现有文档回灌知识?",
279
298
  en: () =>
280
- " 📋 Fabric: knowledge base is sparse — run /fabric-import to backfill from git history and existing docs?",
299
+ " 📋 Fabric: knowledge base is sparse — run /fabric-archive to backfill from git history and existing docs?",
281
300
  "zh-CN-hybrid": () =>
282
- " 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
301
+ " 📋 Fabric: 知识库稀疏,是否调 /fabric-archive 从 git 历史与现有文档回灌知识?",
283
302
  },
284
303
 
285
304
  // ---- Broad hook: meta auto-refresh breadcrumb (rc.22 Scope D T-D4) -------
@@ -153,7 +153,7 @@ function liveKnowledgeStats(snapshot) {
153
153
  // out-of-band (store grew via git pull / cross-workspace sync), so trusting it
154
154
  // re-introduced exactly the false-nudge this whole field cures — observed a
155
155
  // store with 61 live canonical entries whose cached count was frozen at 1,
156
- // mis-firing the "knowledge sparse → /fabric-import" underseed nudge AND
156
+ // mis-firing the "knowledge sparse → /fabric-archive" underseed nudge AND
157
157
  // defeating the fabric-import `canonical > 50 → SKIP` guard. read_set carries
158
158
  // no resolved store root either (alias/uuid only), so a live recount is
159
159
  // impossible without re-resolution (which hooks must not do). Return null
@@ -0,0 +1,79 @@
1
+ // ux-w2-9: the SINGLE guarded events.jsonl write path for .cjs hooks.
2
+ //
3
+ // Before this, every hook (knowledge-hint-broad / fabric-hint / post-tooluse-
4
+ // mutation) hand-stamped the event envelope (kind/id/ts/schema_version) and
5
+ // called appendLockedLine directly. A forgotten field or a non-string event_type
6
+ // produced a row the doctor's event-ledger Zod read (event_ledger_schema_compat)
7
+ // would reject. This module centralizes the stamp + guard so every cjs-written
8
+ // row satisfies the same envelope contract the TS appendEventLedgerEvent enforces
9
+ // via Zod — without pulling Zod into the no-build .cjs runtime.
10
+ //
11
+ // Contract (mirrors eventLedgerEventSchema's envelope):
12
+ // - event_type MUST be a non-empty string (the discriminator). Missing/blank →
13
+ // the event is REJECTED (returns false), never written.
14
+ // - kind / schema_version are FORCED to the ledger constants.
15
+ // - id / ts are stamped only when absent (caller may pre-stamp for determinism).
16
+ // - Never throws — hooks must never block; a write failure returns false.
17
+
18
+ const { join } = require("node:path");
19
+ const { appendLockedLine } = require("./injection-log.cjs");
20
+
21
+ const EVENT_KIND = "fabric-event";
22
+ const SCHEMA_VERSION = 1;
23
+
24
+ function safeUuid() {
25
+ try {
26
+ // eslint-disable-next-line global-require
27
+ return require("node:crypto").randomUUID();
28
+ } catch {
29
+ // crypto unavailable (exotic runtime) → time+rand fallback, still unique enough.
30
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
31
+ }
32
+ }
33
+
34
+ // Stamp the canonical envelope onto a caller event, or return null when the event
35
+ // fails the guard (not an object / missing event_type). Pure (no I/O).
36
+ function stampEvent(event) {
37
+ if (!event || typeof event !== "object" || Array.isArray(event)) return null;
38
+ if (typeof event.event_type !== "string" || event.event_type.length === 0) return null;
39
+ return {
40
+ ...event,
41
+ kind: EVENT_KIND,
42
+ schema_version: SCHEMA_VERSION,
43
+ id: typeof event.id === "string" && event.id.length > 0 ? event.id : `event:${safeUuid()}`,
44
+ ts: typeof event.ts === "number" ? event.ts : Date.now(),
45
+ };
46
+ }
47
+
48
+ // Append ONE guarded event line to <fabricDir>/events.jsonl. Returns true on a
49
+ // successful write, false if the event failed the guard or the append errored.
50
+ function appendEvent(fabricDir, event) {
51
+ const stamped = stampEvent(event);
52
+ if (stamped === null) return false;
53
+ try {
54
+ appendLockedLine(join(fabricDir, "events.jsonl"), JSON.stringify(stamped) + "\n");
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ // Append MANY guarded events in a SINGLE locked write (preserves the batched
62
+ // append the post-tooluse hook relied on). Invalid events are dropped; returns
63
+ // the count actually written. A zero-valid batch performs no write.
64
+ function appendEvents(fabricDir, events) {
65
+ if (!Array.isArray(events)) return 0;
66
+ const stamped = events.map(stampEvent).filter((e) => e !== null);
67
+ if (stamped.length === 0) return 0;
68
+ try {
69
+ appendLockedLine(
70
+ join(fabricDir, "events.jsonl"),
71
+ stamped.map((e) => JSON.stringify(e)).join("\n") + "\n",
72
+ );
73
+ return stamped.length;
74
+ } catch {
75
+ return 0;
76
+ }
77
+ }
78
+
79
+ module.exports = { appendEvent, appendEvents, stampEvent };
@@ -103,6 +103,17 @@ function resolveHumanSink(projectRoot, event, gate) {
103
103
  // 4. global human-channel mute.
104
104
  if (mode === "silent") return { emitHuman: false, verbosity, mode };
105
105
 
106
+ // 4b. v2.2 C1 (W5): the `stop` human nudge (archive cadence) defaults to QUIET.
107
+ // The edit-count / session signal already lives in the events.jsonl ledger as
108
+ // queryable telemetry (KT-DEC-0030), so the Stop hook should NOT carry a
109
+ // real-time human-observation UI that interrupts execution flow — find the
110
+ // specific session after the fact instead (user directive 2026-06-22). It stays
111
+ // OBSERVE-only by default; opt back in explicitly via observe.stop=true (handled
112
+ // at step 3) or the verbose preset. SessionStart / pre_tool_use are unaffected.
113
+ if (event === "stop" && mode !== "verbose") {
114
+ return { emitHuman: false, verbosity, mode };
115
+ }
116
+
106
117
  // 5. preset default — emit at the preset's verbosity.
107
118
  return { emitHuman: true, verbosity, mode };
108
119
  }
@@ -0,0 +1,62 @@
1
+ // ux-w2-5: the .cjs mirror of packages/shared/src/theme.ts — the hook-side half
2
+ // of Fabric's single terminal theme. Byte-locked to the TS source by
3
+ // theme-parity.test.ts (G-THEME): the PALETTE / ANSI tables and the paint/symbol
4
+ // render primitives MUST produce byte-identical output here and in the CLI, so a
5
+ // SessionStart hint and `fabric install` paint the same colours. No deps (no Ink,
6
+ // no picocolors) — pure ANSI, matching the Ink-exit direction (W3-A).
7
+
8
+ const ANSI = {
9
+ reset: "\x1b[0m",
10
+ bold: "\x1b[1m",
11
+ dim: "\x1b[2m",
12
+ };
13
+
14
+ const PALETTE = {
15
+ success: "\x1b[38;2;46;204;113m", // emerald
16
+ warn: "\x1b[38;2;241;196;15m", // amber
17
+ error: "\x1b[38;2;231;76;60m", // alizarin
18
+ drift: "\x1b[38;2;155;89;182m", // amethyst
19
+ ai: "\x1b[38;2;52;152;219m", // peter-river blue
20
+ human: "\x1b[38;2;26;188;156m", // turquoise
21
+ accent: "\x1b[38;2;155;89;182m", // amethyst
22
+ muted: ANSI.dim,
23
+ };
24
+
25
+ function isColorEnabled(env, isTTY) {
26
+ const e = env || process.env;
27
+ if (e.NO_COLOR) return false;
28
+ const force = e.FORCE_COLOR;
29
+ if (force !== undefined) return force !== "0" && force.toLowerCase() !== "false";
30
+ return Boolean(isTTY === undefined ? process.stdout.isTTY : isTTY);
31
+ }
32
+
33
+ function paint(token, text, colorOn) {
34
+ const on = colorOn === undefined ? isColorEnabled() : colorOn;
35
+ if (!on) return text;
36
+ return `${PALETTE[token]}${text}${ANSI.reset}`;
37
+ }
38
+
39
+ const SYMBOL_ASCII = { ok: "[ok]", warn: "[warn]", error: "[error]" };
40
+ const SYMBOL_GLYPH = { ok: "[ok] ✓", warn: "[warn] !", error: "[error] x" };
41
+ const SYMBOL_TOKEN = { ok: "success", warn: "warn", error: "error" };
42
+
43
+ function symbol(kind, colorOn) {
44
+ const on = colorOn === undefined ? isColorEnabled() : colorOn;
45
+ return on ? paint(SYMBOL_TOKEN[kind], SYMBOL_GLYPH[kind], true) : SYMBOL_ASCII[kind];
46
+ }
47
+
48
+ // W3-B structural primitives — HUD-shared layer (C-003), byte-mirror of the TS
49
+ // source (packages/shared/src/theme.ts), pinned by theme-parity.test.ts.
50
+ function sectionBar(title, colorOn) {
51
+ const on = colorOn === undefined ? isColorEnabled() : colorOn;
52
+ return on ? `${ANSI.bold}${PALETTE.accent}▌ ${title}${ANSI.reset}` : `# ${title}`;
53
+ }
54
+
55
+ const SCOPE_BADGE_TOKEN = { team: "drift", project: "ai", personal: "human" };
56
+ function scopeBadge(scope, colorOn) {
57
+ const on = colorOn === undefined ? isColorEnabled() : colorOn;
58
+ const text = `[${scope}]`;
59
+ return on ? paint(SCOPE_BADGE_TOKEN[scope], text, true) : text;
60
+ }
61
+
62
+ module.exports = { ANSI, PALETTE, isColorEnabled, paint, symbol, SYMBOL_ASCII, sectionBar, scopeBadge };
@@ -41,9 +41,10 @@ const { isAbsolute, join, relative } = require("node:path");
41
41
 
42
42
  // W1-01 (ISS-011) parity: route every shared-ledger append through the
43
43
  // advisory-lock primitive so concurrent PostToolUse fires (multi-window /
44
- // parallel edits) never interleave a partial line. Best-effort, drop-on-
45
- // contention same primitive the narrow/broad hooks use.
46
- const { appendLockedLine } = require("./lib/injection-log.cjs");
44
+ // parallel edits) never interleave a partial line. ux-w2-9: route batched event
45
+ // writes through the single guarded event-writer (envelope stamp + event_type
46
+ // guard + advisory-lock append) so every row satisfies the event-ledger schema.
47
+ const { appendEvents } = require("./lib/event-writer.cjs");
47
48
 
48
49
  const FABRIC_DIR_REL = ".fabric";
49
50
  const EVENTS_LEDGER_FILE = "events.jsonl";
@@ -223,23 +224,17 @@ function appendFileMutated(projectRoot, now, paths, toolCallId, toolName, sessio
223
224
  typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
224
225
  const validToolName =
225
226
  typeof toolName === "string" && toolName.length > 0 ? toolName : null;
226
- const lines =
227
- pathList
228
- .map((p) =>
229
- JSON.stringify({
230
- kind: "fabric-event",
231
- id: `event:${randomUUID()}`,
232
- ts: tsMs,
233
- schema_version: 1,
234
- ...(validSessionId ? { session_id: validSessionId } : {}),
235
- event_type: "file_mutated",
236
- path: p,
237
- tool_call_id: callId,
238
- ...(validToolName ? { tool_name: validToolName } : {}),
239
- }),
240
- )
241
- .join("\n") + "\n";
242
- appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), lines);
227
+ appendEvents(
228
+ fabricDir,
229
+ pathList.map((p) => ({
230
+ ts: tsMs,
231
+ ...(validSessionId ? { session_id: validSessionId } : {}),
232
+ event_type: "file_mutated",
233
+ path: p,
234
+ tool_call_id: callId,
235
+ ...(validToolName ? { tool_name: validToolName } : {}),
236
+ })),
237
+ );
243
238
  } catch {
244
239
  // Silent — marker failure must never block the tool pipeline.
245
240
  }
@@ -290,25 +285,19 @@ function appendKnowledgeBodyRead(projectRoot, now, paths, toolCallId, toolName,
290
285
  typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
291
286
  const validToolName =
292
287
  typeof toolName === "string" && toolName.length > 0 ? toolName : null;
293
- const lines =
294
- reads
295
- .map((r) =>
296
- JSON.stringify({
297
- kind: "fabric-event",
298
- id: `event:${randomUUID()}`,
299
- ts: tsMs,
300
- schema_version: 1,
301
- ...(validSessionId ? { session_id: validSessionId } : {}),
302
- event_type: "knowledge_body_read",
303
- stable_id: r.stable_id,
304
- ...(r.store ? { store: r.store } : {}),
305
- path: r.path,
306
- tool_call_id: callId,
307
- ...(validToolName ? { tool_name: validToolName } : {}),
308
- }),
309
- )
310
- .join("\n") + "\n";
311
- appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), lines);
288
+ appendEvents(
289
+ fabricDir,
290
+ reads.map((r) => ({
291
+ ts: tsMs,
292
+ ...(validSessionId ? { session_id: validSessionId } : {}),
293
+ event_type: "knowledge_body_read",
294
+ stable_id: r.stable_id,
295
+ ...(r.store ? { store: r.store } : {}),
296
+ path: r.path,
297
+ tool_call_id: callId,
298
+ ...(validToolName ? { tool_name: validToolName } : {}),
299
+ })),
300
+ );
312
301
  } catch {
313
302
  // Silent — marker failure must never block the tool pipeline.
314
303
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: fabric-archive
3
- description: 归档对话洞察到 active write store 的 pending knowledge (NOT code review). Triggers 以后/always/never/下次/记一下;wrong-turn-revert;decision-confirm;dismissal-reason;/fabric-archive.
4
- allowed-tools: Read, Glob, Grep, Bash, mcp__fabric__fab_extract_knowledge
3
+ description: 归档对话洞察(default)+ 冷启动从 git log/docs 回灌(source mode)到 active write store 的 pending knowledge (NOT code review). Triggers 归档/记一下/always·never/wrong-turn-revert;source mode bootstrap fabric/import 历史/mine commit.
4
+ allowed-tools: Read, Glob, Grep, Bash, mcp__fabric__fab_archive_scan, mcp__fabric__fab_propose, mcp__fabric__fab_pending, mcp__fabric__fab_review
5
5
  ---
6
6
 
7
7
  > **Surface**: Skill (LLM judgment over session digests). See [`docs/surfaces.md`](https://github.com/fenglimg/fabric/blob/main/docs/surfaces.md).
@@ -29,10 +29,27 @@ rc.37 NEW-9 collapsed the flow to **3 macro-phases**; the legacy fine-grained ph
29
29
 
30
30
  - **GATHER** = Phase 0 (range) → 0.5 (config) → 1 (ledger scan via `fab_archive_scan`) → [1.5 onboard] → 2 (candidates).
31
31
  - **REVIEW** = Phase 2.5 (viability gate) → 3 (classify / layer / slug + batch review) → 3.5 (relevance scope + relevance_paths) → 3.7 (semantic scope / audience axis). The single user review round lives here.
32
- - **PERSIST** = Phase 4 (`fab_extract_knowledge`, one call per candidate) → 4.5 (archive-attempt ledger).
32
+ - **PERSIST** = Phase 4 (`fab_propose`, one call per candidate) → 4.5 (archive-attempt ledger).
33
33
 
34
34
  Sub-step chain: `0 → 0.5 → 1 → [1.5] → 2 → 2.5 → 3 → 3.5 → 3.7 → 4 → 4.5`. Each below is a navigator stub — full procedure, decision tables, and worked examples live in `ref/`.
35
35
 
36
+ ## Source Mode — cold-start bootstrap (W3-C: 吸收原 fabric-import)
37
+
38
+ **Default mode** GATHERs candidates from the cross-session digest ledger (Phase 1, `fab_archive_scan`). **Source mode** swaps ONLY the GATHER source: it mines `git log` + `docs/*.md` for one-time per-project cold-start — the REVIEW (classify / layer / scope / semantic_scope) and PERSIST (`fab_propose`) pipeline below is **shared, byte-for-byte the same contract**. No new write path: mined entries land as `team`-layer pending via the same `fab_propose`, then dedupe vs canonical via `fab_review`.
39
+
40
+ **Enter source mode when** the user explicitly asks to bootstrap / import history / mine commits, OR the SessionStart hook fired `shouldRecommendImport()`. Else stay in default mode. **SKIP** when `.fabric/` missing (→ `fabric install`), canonical count > `import_skip_canonical_threshold` (default 50), or checkpoint `phase=complete` + `last_checkpoint_at <24h`.
41
+
42
+ Source-mode pipeline (replaces GATHER; REVIEW+PERSIST unchanged):
43
+
44
+ 1. **Init / checkpoint** — read/init `.fabric/.import-state.json` (single resumability source; atomic `Write .tmp` → `Bash mv`). Corruption → `Read ref/source-state-recovery.md`. Full state schema + 6-step resume → `Read ref/source-checkpoint.md`.
45
+ 2. **Init-scan reference (NO re-implement)** — `fabric onboard-coverage --json` + `fab_pending action="search"` to learn existing canonical titles for the negative filter. `fabric install` already produced the baseline; source mode references it, never redoes it.
46
+ 3. **Mine (git + docs)** — `git log --since="<window> months ago"` (conventional prefix → type signal) + `docs/*.md`; classify into the 5 types; `fab_propose` per candidate. **Source-mode scope lock (NON-NEGOTIABLE): every mined entry `relevance_scope="broad"` + `relevance_paths=[]`** — LLM-inferred narrow lies about applicability; narrowing is deferred to `fab_review.modify` post-import. Cap `import_max_pending_per_run` (default 10). Full mining procedure, conventional-prefix table, `--dry-run` template → `Read ref/source-mining.md`.
47
+ 4. **Dedupe vs canonical** — for each pending, `fab_pending action="search"` (top 5 by type), classify duplicate / subsumption / subsumption-with-novelty / contradiction / genuinely-new, then `fab_review` reject / modify. `fab_pending` does NOT compare meaning — semantic compare is the LLM's job. Full 5-way classification → `Read ref/source-dedup.md`.
48
+
49
+ Source-mode config knobs (read from `.fabric/fabric-config.json`, defaults if absent): `import_window_first_run_months` (60), `import_window_rerun_months` (2), `import_max_pending_per_run` (10), `import_max_commits_scan` (500), `import_skip_canonical_threshold` (50).
50
+
51
+ Source-mode output roll-up + worked examples → `Read ref/source-output-contract.md` / `ref/source-worked-examples.md`. Source mode requires an **explicit target store** (E7) — never auto-route mined entries; resolve writable candidates via `fabric info scope team` and `AskUserQuestion` for the alias when more than one exists.
52
+
36
53
  ### Phase 0 — Range Resolution
37
54
 
38
55
  Parse user's prompt for time-window (`今日` / `last week`), topic keyword (`rc.20`), or literal `session_id` reference; emit `session_id[]` OR `"all"` sentinel that constrains Phase 1 collection. LLM-as-parser contract — no parser code.
@@ -45,7 +62,7 @@ Read `.fabric/fabric-config.json`; resolve `archive_max_candidates_per_batch` (d
45
62
 
46
63
  ### Phase 0.6 — Store routing (v2.1 multi-store)
47
64
 
48
- Archives land in the **active write store** for the entry's scope — NEVER pick a store yourself. Run `fabric scope-explain team` (or the relevant scope) to get the resolved `writeTarget`; that is where `fab_extract_knowledge` persists. Single-store → the lone store (back-compat). After persisting, **echo the target store alias** (`归档到 store '<alias>'`). Personal-scope entries route to the personal store (never the shared team store, R5#3). Do NOT read `~/.fabric` store trees directly — go through `scope-explain` / the MCP write path.
65
+ Archives land in the **active write store** for the entry's scope — NEVER pick a store yourself. Run `fabric info scope team` (or the relevant scope) to get the resolved `writeTarget`; that is where `fab_propose` persists. Single-store → the lone store (back-compat). After persisting, **echo the target store alias** (`归档到 store '<alias>'`). Personal-scope entries route to the personal store (never the shared team store, R5#3). Do NOT read `~/.fabric` store trees directly — go through `fabric info scope` / the MCP write path.
49
66
 
50
67
  ### UX i18n Policy
51
68
 
@@ -65,6 +82,18 @@ The deterministic ledger scan now runs **server-side** — call `fab_archive_sca
65
82
 
66
83
  Then (LLM side, Boundary B): for each returned `session_id`, load `.fabric/.cache/session-digests/<session_id>.md`, concatenate into a `### Cross-session digest` block, and populate `source_sessions[]` + `session_context` for Phase 4. Cap at `archive_digest_max_sessions`. Missing digest files degrade silently.
67
84
 
85
+ **Coverage transparency (crack 3 — cheap recall backstop).** BEFORE collecting candidates, surface the scan's watermark + drops to the user so a human can act as the recall detector and manually override (`--range <session_id>` to force a dropped session back in). This is the affordable substitute for the (deferred) periodic cold-eval miss-rate audit — show, don't hide, what the deterministic filter skipped:
86
+
87
+ ```
88
+ 📋 归档覆盖到 <covered_through_ts 转人类可读时间>。
89
+ 纳入会话: <session_ids.length> 个。
90
+ 跳过 <dropped.length> 个: <每个 {session_id 短码} (reason)>
91
+ reason 含义: user_dismissed=用户曾拒绝 / cooldown=12h 防抖内 / no_new_signal=自上次归档无新高价值活
92
+ 若某个被跳过的会话其实有该归档的内容,显式 `fabric-archive --range <session_id>` 强制纳入。
93
+ ```
94
+
95
+ Render `dropped` only when non-empty; render the watermark line always. en variant mirrors the same fields. Keep it ONE compact block — this is a backstop affordance, not a report.
96
+
68
97
  `Read ref/phase-1-cross-session.md` for the filter state machine + digest-stitch + graceful-degradation notes. The hand-rolled `tail -n 200` scan is retired — `fab_archive_scan` is the source of truth.
69
98
 
70
99
  Graceful degradation: missing digest cache → single-session fallback. Missing `session_archive_attempted` events (pre-rc.25) → legacy "scan everything since anchor" behaviour.
@@ -83,7 +112,7 @@ Gather raw evidence: tail `.fabric/events.jsonl` since last `knowledge_proposed`
83
112
 
84
113
  Coarse viability check. **PASS**: user_explicit_invoke OR ≥1 archive signal hit. rc.37 NEW-4 folds the legacy 8 signals into **3 categories**: (1) **User-driven knowledge expression** (normative language `always`/`never`/`以后`/`记一下`/`永远不要`, OR decision-with-rationale, OR dismissal-with-reason); (2) **Reflective discovery** (wrong-turn-and-revert, OR long diagnostic loop, OR a named reusable pattern); (3) **Concrete artifact change** (new dependency diff, OR a formalized multi-step procedure).
85
114
 
86
- Pre-PASS HARD gate (rc.37 NEW-4): per candidate, run `fab_review action="search"` against the mounted read-set; duplicate canonical → drop the candidate (anti-signal #4). Silently writing a near-duplicate is the highest-noise failure mode.
115
+ Pre-PASS HARD gate (rc.37 NEW-4): per candidate, run `fab_pending action="search"` against the mounted read-set; duplicate canonical → drop the candidate (anti-signal #4). Silently writing a near-duplicate is the highest-noise failure mode.
87
116
 
88
117
  **FAIL → branch**: E1/E3/E5 silent-skip (`outcome='skipped_no_signal'`); E2/E4 render gate-FAIL (`outcome='viability_failed'`) and MUST include the force-archive escape hatch (zh-CN: `如需强制归档,请显式调用 fabric-archive` / en: `To force-archive, explicitly invoke fabric-archive`).
89
118
 
@@ -93,6 +122,8 @@ Pre-PASS HARD gate (rc.37 NEW-4): per candidate, run `fab_review action="search"
93
122
 
94
123
  For each candidate, propose **type** ∈ {model, decision, guideline, pitfall, process}, **layer** ∈ {team, personal} via the verbatim heuristic below, **slug** (kebab-case 2-5 words, 20-40 chars, unique within type+layer bucket), **summary** (1-2 sentences).
95
124
 
125
+ > **Self-sufficiency standard — guideline / model summaries (KT-GLD-0001/0006).** These two types land in the SessionStart **ALWAYS-ACTIVE** sink as a single INDEX line with NO body injected — so the summary IS the operative rule the agent acts on. Author it as a self-contained imperative that states the thesis (the *what* + the operative *so-what*), e.g. `改源码前先读 bootstrap+compiler config;scripts 为 init 主执行边界`. A topic label that only points at the body (`Code style guidelines`, `Scope model`) is NOT acceptable here — the reader can't act on it without a fetch, breaking the always-active contract. decision/pitfall/process summaries are exempt (they surface as `must_read_if` triggers, deliberately pointers). Do NOT self-judge sufficiency in this phase (curse-of-knowledge rubber-stamps — KT-GLD-0006); authoring to the standard is the write-time floor, the zero-context cold-eval at review time is the real gate.
126
+
96
127
  #### Layer Classification Heuristic (verbatim, contract-locked)
97
128
 
98
129
  > - **强 team**: 引用本项目代码、团队共识用语("we decided")、fabric-import 路径产物、业务领域、绑定本项目代码的 pitfall
@@ -123,7 +154,7 @@ Sets the entry's **audience** `semantic_scope` (orthogonal to `layer`=store and
123
154
 
124
155
  ### Phase 4 — Persist via MCP
125
156
 
126
- For each user-confirmed candidate, call `fab_extract_knowledge` ONCE (NEVER batch). Required: `source_sessions[]`, `recent_paths[]` (cap 20), `user_messages_summary`, `type` (plural form: decisions/pitfalls/guidelines/models/processes), `slug`, `layer`, `relevance_scope`, `relevance_paths[]`, `proposed_reason` (enum), `session_context` (3-5 line narrative). One OPTIONAL audience field: `semantic_scope` — pass `team` ONLY when Phase 3.7 classified a team candidate as team-wide (cross-project); OMIT otherwise so the engine auto-derives `project:<active_project>` (or `team` when the repo has no `active_project`). Four OPTIONAL rc.23 triage fields (`intent_clues`, `tech_stack`, `impact`, `must_read_if`) — populate when clean, **omit rather than guess**.
157
+ For each user-confirmed candidate, call `fab_propose` ONCE (NEVER batch). Required: `source_sessions[]`, `recent_paths[]` (cap 20), `user_messages_summary`, `type` (plural form: decisions/pitfalls/guidelines/models/processes), `slug`, `layer`, `relevance_scope`, `relevance_paths[]`, `proposed_reason` (enum), `session_context` (3-5 line narrative). One OPTIONAL audience field: `semantic_scope` — pass `team` ONLY when Phase 3.7 classified a team candidate as team-wide (cross-project); OMIT otherwise so the engine auto-derives `project:<active_project>` (or `team` when the repo has no `active_project`). Four OPTIONAL rc.23 triage fields (`intent_clues`, `tech_stack`, `impact`, `must_read_if`) — populate when clean, **omit rather than guess**.
127
158
 
128
159
  Server returns `{ pending_path, idempotency_key }`. Display `pending_path` for the user. `idempotency_key = sha256({source_session, type, slug})` — repeated calls SAFE (server merges evidence).
129
160
 
@@ -149,25 +180,25 @@ MANDATORY closing step on EVERY invocation (Phase 4 success path + every early-e
149
180
  - WHEN `active_project` is set AND `layer=team`, MUST present `[semantic_scope=...]` (`team` for team-wide, or `project:<active_project>` for this-project-only) plus a one-line `Audience reasoning:` citing this-project-only vs team-wide (Phase 3.7).
150
181
  - MUST classify against the canonical singular nouns: model / decision / guideline / pitfall / process. NEVER invent new types.
151
182
  - MUST cap the batch at `archive_max_candidates_per_batch` candidates (config-resolved, default 8); drop weaker ones over the cap.
152
- - MUST display the resolved `pending_path` returned by `fab_extract_knowledge` so the user can verify.
183
+ - MUST display the resolved `pending_path` returned by `fab_propose` so the user can verify.
153
184
  - MUST treat user inline edits to type/layer/slug/relevance_scope/relevance_paths/semantic_scope as authoritative replacements before Phase 2.
154
185
  - MUST skip rather than guess when an observation does not fit any of the 5 types.
155
186
 
156
187
  ### WRITE Rules
157
188
 
158
- - NEVER write a knowledge entry directly to the filesystem; the only legal write path is `mcp__fabric__fab_extract_knowledge`.
159
- - NEVER infer or glob a project-local pending directory — persist through `fab_extract_knowledge` and use the returned store-resolved `pending_path`; promotion to canonical knowledge is fab_review concern, NOT this skill.
189
+ - NEVER write a knowledge entry directly to the filesystem; the only legal write path is `mcp__fabric__fab_propose`.
190
+ - NEVER infer or glob a project-local pending directory — persist through `fab_propose` and use the returned store-resolved `pending_path`; promotion to canonical knowledge is fab_review concern, NOT this skill.
160
191
  - NEVER include an `id` field anywhere — pending entries have no id (late-bind on approve).
161
192
  - NEVER classify a candidate as `personal` when a 强 team signal applies. Default to team on ambiguity.
162
193
  - NEVER emit a non-empty `relevance_paths` when `relevance_scope=broad` — broad MUST always carry `relevance_paths=[]`.
163
194
  - NEVER emit a non-empty `relevance_paths` when `layer=personal` — personal forces `relevance_scope=broad` + `relevance_paths=[]`.
164
195
  - v2.0.0-rc.37 NEW-7 widened Phase 3.5: `edit_paths` ∪ `user_mentioned_paths` drives `relevance_paths`; `read_paths` flows separately to `evidence_paths` (structured frontmatter, not body markdown). NEVER lift body regex / symbol extraction into `relevance_paths` — those remain reserved for v2.1+.
165
- - NEVER batch multiple candidates into a single fab_extract_knowledge call; one call per candidate.
196
+ - NEVER batch multiple candidates into a single fab_propose call; one call per candidate.
166
197
  - NEVER paraphrase the verbatim layer heuristic block above — the Chinese text is contract-locked.
167
- - MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `semantic_scope`, `active_project`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`, `related`, `KT→KP`.
198
+ - MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `fab_propose`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `semantic_scope`, `active_project`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`, `related`, `KT→KP`.
168
199
 
169
200
  ## Worked Examples / E5 Cron / Dry-run (ref-only)
170
201
 
171
- - **Worked examples** (3 end-to-end fab_extract_knowledge calls: decision/team, pitfall/team, guideline/personal): `Read ref/worked-examples.md`
202
+ - **Worked examples** (3 end-to-end fab_propose calls: decision/team, pitfall/team, guideline/personal): `Read ref/worked-examples.md`
172
203
  - **E5 Scheduled Daily Recap** (only when entry_point=E5_cron — OS cron, `/loop`, or scheduled trigger): `Read ref/e5-cron-recap.md`
173
204
  - **Dry-run Scope** (authoritative catalogue of all writes suspended by `--dry-run`): `Read ref/dry-run-scope.md`
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Write operation | Normal mode | Dry-run mode |
6
6
  |---|---|---|
7
- | `fab_extract_knowledge` MCP call (Phase 4) | One call per confirmed candidate, writes to the active write store and returns `pending_path` | SKIPPED. Phase 4 renders "would write N pending entries" preview table instead. |
7
+ | `fab_propose` MCP call (Phase 4) | One call per confirmed candidate, writes to the active write store and returns `pending_path` | 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. |
@@ -43,7 +43,7 @@ Rendering rule:
43
43
  - `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_recall`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
44
44
  - `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
45
45
 
46
- Protected tokens (`fab_extract_knowledge`, `relevance_scope`,
46
+ Protected tokens (`fab_propose`, `relevance_scope`,
47
47
  `relevance_paths`, `narrow`, `broad`, `source_sessions`, `proposed_reason`,
48
48
  `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`,
49
49
  `pending_path`, `layer`, `team`, `personal`,