@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.
- package/README.md +2 -2
- package/dist/audit-PURSJJFH.js +734 -0
- package/dist/{chunk-YM4XATJF.js → chunk-722JU5BP.js} +2 -0
- package/dist/{chunk-QPAW6IYT.js → chunk-7V4XMLQ2.js} +3 -3
- package/dist/{chunk-7ZDXBOOU.js → chunk-ACSMNX3V.js} +44 -128
- package/dist/{chunk-PTGQAZEW.js → chunk-GGDVZCD6.js} +2 -4
- package/dist/{chunk-EOT63RDH.js → chunk-I5F5BHWI.js} +9 -0
- package/dist/chunk-PP7QVRXH.js +565 -0
- package/dist/chunk-SL77FXX7.js +54 -0
- package/dist/{chunk-3D7B2UAZ.js → chunk-VQKXTMWH.js} +44 -4
- package/dist/doctor-S6KPGS35.js +27 -0
- package/dist/index.js +91 -81
- package/dist/{info-7FKBTMVO.js → info-NJEY26H6.js} +91 -46
- package/dist/{context-7NUKXDB6.js → inspect-5YZMJPFM.js} +11 -11
- package/dist/{install-v2-I6PJ6IFT.js → install-v2-KGIDII4H.js} +163 -364
- package/dist/{plan-context-hint-G75R4P4J.js → plan-context-hint-5TNGH3R4.js} +1 -1
- package/dist/{store-HOCORVL3.js → store-GF4SFBMJ.js} +155 -57
- package/dist/{sync-DT5UJMMR.js → sync-3XCIRDPK.js} +3 -4
- package/dist/{uninstall-IFN2KYBK.js → uninstall-BG4ML4FC.js} +39 -10
- package/package.json +3 -7
- package/templates/hooks/cite-policy-evict.cjs +1 -1
- package/templates/hooks/configs/claude-code.json +1 -5
- package/templates/hooks/configs/codex-hooks.json +1 -5
- package/templates/hooks/fabric-hint.cjs +346 -138
- package/templates/hooks/knowledge-hint-broad.cjs +265 -75
- package/templates/hooks/knowledge-hint-narrow.cjs +3 -3
- package/templates/hooks/knowledge-pretooluse.cjs +111 -0
- package/templates/hooks/lib/banner-i18n.cjs +31 -12
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +1 -1
- package/templates/hooks/lib/event-writer.cjs +79 -0
- package/templates/hooks/lib/nudge-policy.cjs +11 -0
- package/templates/hooks/lib/theme.cjs +62 -0
- package/templates/hooks/post-tooluse-mutation.cjs +28 -39
- package/templates/skills/fabric-archive/SKILL.md +43 -12
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +5 -5
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +2 -2
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +6 -5
- package/templates/skills/{fabric-import/ref/checkpoint-state.md → fabric-archive/ref/source-checkpoint.md} +3 -3
- package/templates/skills/{fabric-import/ref/phase-3-dedup.md → fabric-archive/ref/source-dedup.md} +4 -4
- package/templates/skills/{fabric-import/ref/phase-2-mining.md → fabric-archive/ref/source-mining.md} +20 -20
- package/templates/skills/{fabric-import/ref/output-contract.md → fabric-archive/ref/source-output-contract.md} +3 -3
- package/templates/skills/{fabric-import/ref/state-recovery.md → fabric-archive/ref/source-state-recovery.md} +2 -2
- package/templates/skills/{fabric-import/ref/worked-examples.md → fabric-archive/ref/source-worked-examples.md} +10 -10
- package/templates/skills/fabric-archive/ref/worked-examples.md +3 -3
- package/templates/skills/fabric-review/SKILL.md +28 -15
- package/templates/skills/fabric-review/ref/cite-contract.md +2 -2
- package/templates/skills/fabric-review/ref/modify-flow.md +13 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +5 -5
- package/templates/skills/fabric-review/ref/relate-mode.md +33 -0
- package/templates/skills/fabric-review/ref/retire-mode.md +47 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +1 -1
- package/templates/skills/fabric-review/ref/worked-examples.md +5 -5
- package/templates/skills/fabric-store/SKILL.md +12 -27
- package/templates/skills/fabric-sync/SKILL.md +16 -35
- package/templates/skills/lib/shared-policy.md +6 -4
- package/dist/chunk-27HK6H5Y.js +0 -69
- package/dist/chunk-E7HJUU34.js +0 -1096
- package/dist/chunk-NLNH64A3.js +0 -43
- package/dist/chunk-QFIVFZRH.js +0 -13
- package/dist/doctor-MDTZWKBK.js +0 -24
- package/dist/metrics-HMFH4YHK.js +0 -135
- package/dist/scope-explain-HLJZ2M33.js +0 -48
- package/dist/status-4R3TM4FJ.js +0 -37
- package/dist/whoami-ITGEFWH4.js +0 -49
- package/templates/skills/fabric/SKILL.md +0 -100
- package/templates/skills/fabric-audit/SKILL.md +0 -63
- package/templates/skills/fabric-connect/SKILL.md +0 -48
- package/templates/skills/fabric-import/SKILL.md +0 -151
- 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
|
|
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-
|
|
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
|
-
//
|
|
208
|
-
//
|
|
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-
|
|
211
|
-
en: () => " Run /fabric-
|
|
212
|
-
"zh-CN-hybrid": () => " 是否调 /fabric-
|
|
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-
|
|
293
|
+
// " 📋 Fabric: 知识库稀疏,是否调 /fabric-archive 从 git 历史与现有文档回灌知识?"
|
|
275
294
|
// Note: leading two spaces are intentional (existing banner indent).
|
|
276
|
-
// params: {} — protected token /fabric-
|
|
295
|
+
// params: {} — protected token /fabric-archive verbatim.
|
|
277
296
|
broadImportBanner: {
|
|
278
|
-
"zh-CN": () => " 📋 Fabric: 知识库稀疏,是否调 /fabric-
|
|
297
|
+
"zh-CN": () => " 📋 Fabric: 知识库稀疏,是否调 /fabric-archive 从 git 历史与现有文档回灌知识?",
|
|
279
298
|
en: () =>
|
|
280
|
-
" 📋 Fabric: knowledge base is sparse — run /fabric-
|
|
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-
|
|
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-
|
|
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.
|
|
45
|
-
//
|
|
46
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
4
|
-
allowed-tools: Read, Glob, Grep, Bash,
|
|
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 (`
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
159
|
-
- NEVER infer or glob a project-local pending directory — persist through `
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
-
| `
|
|
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 (`
|
|
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`,
|