@fenglimg/fabric-cli 1.8.0-rc.3 → 2.0.0-rc.10

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 (49) hide show
  1. package/README.md +6 -6
  2. package/dist/chunk-6ICJICVU.js +10 -0
  3. package/dist/chunk-AW3G7ZH5.js +576 -0
  4. package/dist/chunk-HQLEHH4O.js +321 -0
  5. package/dist/chunk-MT3R57VG.js +1000 -0
  6. package/dist/{chunk-QPCRBQ5Y.js → chunk-OBQU6NHO.js} +1 -52
  7. package/dist/chunk-WPTA74BY.js +184 -0
  8. package/dist/chunk-WWNXR34K.js +49 -0
  9. package/dist/doctor-RILCO5OG.js +282 -0
  10. package/dist/hooks-NX32PPEN.js +13 -0
  11. package/dist/index.js +8 -5
  12. package/dist/{init-7EYGUJNJ.js → init-SAVH4SKE.js} +281 -1235
  13. package/dist/plan-context-hint-QMUPAXIB.js +98 -0
  14. package/dist/scan-ELSNCSKS.js +22 -0
  15. package/dist/{serve-466QXQ5Q.js → serve-NGLXHDYC.js} +8 -4
  16. package/dist/uninstall-DBAR2JBS.js +1082 -0
  17. package/package.json +3 -3
  18. package/templates/agents-md/AGENTS.md.template +55 -17
  19. package/templates/bootstrap/CLAUDE.md +1 -1
  20. package/templates/bootstrap/codex-AGENTS-header.md +1 -1
  21. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
  22. package/templates/hooks/configs/README.md +73 -0
  23. package/templates/hooks/configs/claude-code.json +37 -0
  24. package/templates/hooks/configs/codex-hooks.json +20 -0
  25. package/templates/hooks/configs/cursor-hooks.json +20 -0
  26. package/templates/hooks/fabric-hint.cjs +1337 -0
  27. package/templates/hooks/knowledge-hint-broad.cjs +612 -0
  28. package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
  29. package/templates/hooks/lib/session-digest-writer.cjs +172 -0
  30. package/templates/skills/fabric-archive/SKILL.md +486 -0
  31. package/templates/skills/fabric-import/SKILL.md +560 -0
  32. package/templates/skills/fabric-review/SKILL.md +382 -0
  33. package/dist/chunk-NMMUETVK.js +0 -216
  34. package/dist/doctor-F52XWWZC.js +0 -98
  35. package/dist/scan-NNBNGIZG.js +0 -12
  36. package/templates/agents-md/variants/cocos.md +0 -20
  37. package/templates/agents-md/variants/next.md +0 -20
  38. package/templates/agents-md/variants/vite.md +0 -20
  39. package/templates/bootstrap/GEMINI.md +0 -8
  40. package/templates/bootstrap/roo-fabric.md +0 -5
  41. package/templates/bootstrap/windsurf-fabric.md +0 -5
  42. package/templates/claude-hooks/fabric-init-reminder.cjs +0 -18
  43. package/templates/claude-skills/fabric-init/SKILL.md +0 -163
  44. package/templates/codex-hooks/fabric-session-start.cjs +0 -19
  45. package/templates/codex-hooks/fabric-stop-reminder.cjs +0 -18
  46. package/templates/codex-skills/fabric-init/SKILL.md +0 -162
  47. package/templates/husky/pre-commit +0 -9
  48. package/templates/skill-source/fabric-init/SOURCE.md +0 -157
  49. package/templates/skill-source/fabric-init/clients.json +0 -17
@@ -0,0 +1,382 @@
1
+ ---
2
+ name: fabric-review
3
+ description: Use this skill to review pending knowledge entries in `.fabric/knowledge/pending/` — list, approve (late-bind id allocation), reject, modify (incl. layer flip), search, defer. Mode is inferred from invocation context (recent user message + events.jsonl tail + pending count) — NEVER asked. Per-item actions (approve / reject / modify / defer) are surfaced via AskUserQuestion because they are genuine human-judgment choices.
4
+ allowed-tools: Read, Glob, Grep, Bash, Edit, mcp__fabric__fab_review
5
+ ---
6
+
7
+ > **Surface**: This is a Skill (AI-driven, per-entry human-judgment routing). See [`docs/surfaces.md`](https://github.com/fenglimg/fabric/blob/main/docs/surfaces.md) for the CLI / Skill / MCP boundary.
8
+
9
+ ## Precondition
10
+
11
+ This skill is invoked when one of the following holds:
12
+
13
+ - The Stop-hook printed a stdout JSON pointer of shape `{"decision":"block","reason":"..."}` carrying a `signal=review` (pending overflow: ≥10 entries or oldest pending age ≥7 days)
14
+ - The user typed an explicit review request (e.g. "review knowledge", "show pending", "approve what's queued", "what's stale", "look at KT-D-7")
15
+ - A task end where the agent itself判定 review backlog has crossed the overflow threshold
16
+
17
+ If none of the above hold, stop the skill immediately and tell the user `没有触发 review 信号;如需手动 review 请显式调用 fabric-review`.
18
+
19
+ This skill is `Infer-not-Ask` for mode and `Ask-when-genuine` for per-item actions:
20
+
21
+ - Mode (pending / topic / health / revisit) is INFERRED from context — NEVER surfaced via AskUserQuestion
22
+ - Per-item action (approve / reject / modify / defer) IS surfaced via AskUserQuestion — the user must judge
23
+ - Layer-flip target (team vs personal) IS surfaced via AskUserQuestion when modify path includes layer change
24
+
25
+ Required preconditions before any fab_review call:
26
+
27
+ - `.fabric/` directory exists in the project (or `~/.fabric/` for personal layer)
28
+ - `mcp__fabric__fab_review` MCP tool is registered and reachable
29
+ - `.fabric/agents.meta.json` is present (the id allocator reads it on approve)
30
+ - `.fabric/events.jsonl` exists (tolerate ENOENT — empty ledger is normal first-run)
31
+
32
+ ## Mode Inference (System Infers — NEVER Ask)
33
+
34
+ > Verbatim from rc.3 locked decisions:
35
+ > "review 永远走 fabric-review skill,**模式从上下文推断**(4 种 mode:pending queue / by topic / health overview / revisit existing)"
36
+ > "**AskUserQuestion 仅在真有选择时用**——'何种 mode' 不是真选择(系统能推断),'approve/reject/modify 单条' 是真选择"
37
+
38
+ The skill MUST infer one of {`pending`, `topic`, `health`, `revisit`} before any user-facing output. NEVER call `AskUserQuestion` to ask the user which mode to use — the system MUST infer.
39
+
40
+ ### 3-Step Inference Algorithm
41
+
42
+ **Step 1 — Recent user message keyword scan.** Read the user's most recent invocation message (or the Stop-hook reason text). Match against keyword sets in this priority order:
43
+
44
+ | Keywords (zh-CN + en) | Inferred mode |
45
+ |---|---|
46
+ | "approve", "review pending", "promote", "what's queued", "审核 pending", "通过" | `pending` |
47
+ | "search for X about Y", "find entries about <topic>", "关于…的知识", "找一下 <topic>" | `topic` |
48
+ | "what's stale", "demote old", "health check", "过期的", "陈旧的", "整理一下" | `health` |
49
+ | "look at <id>", "revisit KT-…", "show <slug>", "再看下 <id>", "回顾" | `revisit` |
50
+
51
+ If exactly one row matches, lock that mode and skip to Step 3.
52
+
53
+ **Step 2 — events.jsonl tail scan.** If Step 1 yielded zero or multiple matches, read the tail (last 200 lines) of `.fabric/events.jsonl`:
54
+
55
+ - Count `knowledge_proposed` events since the last `knowledge_promoted` event. If `>5` recent proposals → infer `pending` (write side has piled up; review is overdue).
56
+ - Count `knowledge_demoted` or `lint`-class events in the last 24h. If `≥1` → infer `health` (corpus quality signal already firing).
57
+ - If a recent `knowledge_layer_changed` event exists for the entry the user just referenced → infer `revisit`.
58
+
59
+ **Step 3 — Pending count default.** If Step 1 and Step 2 both produced no signal, glob `.fabric/knowledge/pending/**/*.md`:
60
+
61
+ - If pending count `≥10` OR oldest pending file mtime is `>7 days` ago → infer `pending` (overflow signal — same threshold the Stop-hook uses).
62
+ - Otherwise → default to `pending` (most common review entry point).
63
+
64
+ ### Inference Examples (Sample User Messages → Expected Mode)
65
+
66
+ - "review the pending knowledge" → `pending` (Step 1 keyword "review pending")
67
+ - "find anything about deepMerge" → `topic` (Step 1 keyword "find … about")
68
+ - "anything stale in our knowledge base?" → `health` (Step 1 keyword "stale")
69
+ - "look at KT-D-7" → `revisit` (Step 1 keyword "look at <id>")
70
+ - (Stop-hook fired with signal=review, no user typing) → `pending` (Step 3 default, overflow threshold tripped)
71
+
72
+ ### Anti-Pattern (Hard Rule)
73
+
74
+ NEVER emit an `AskUserQuestion` whose options include {pending, topic, health, revisit}. The user does not pick the mode. If inference is genuinely ambiguous after all 3 steps, default to `pending` and proceed; the user can always cancel and redirect.
75
+
76
+ ## Per-Mode Flow
77
+
78
+ Each mode produces user-facing output, then routes per-item or per-batch decisions through `fab_review` actions. Display body = zh-CN summaries (M3 style); section headings = EN.
79
+
80
+ ### Mode: pending — Approve / Reject / Modify Backlog
81
+
82
+ 1. Call `fab_review` with `action: "list"`, no filters (or `filters.layer="both"` if user explicitly mentioned both layers).
83
+ 2. Server returns `items[]` (each = `{pending_path, type, layer, maturity, tags?, title?, summary?}`).
84
+ 3. Before presenting, perform **Semantic Check** (see below) by issuing one or more `action: "search"` calls scoped by `filters.type` to surface possible duplicates / contradictions among already-canonical entries.
85
+ 4. For each pending item, render a per-item block. v2.0.0-rc.7 T6: render
86
+ `proposed_reason` (frontmatter) + `## Why proposed` line (body, 1-line enum
87
+ explanation) + first line of `## Session context` so future-self has full
88
+ context without re-reading the transcript:
89
+
90
+ ```md
91
+ ## [type=decisions] [layer=team] pending_path=knowledge/pending/decisions/single-cjs-hook.md
92
+ Title: 单 .cjs hook 跨客户端
93
+ Summary: 三客户端 stdout JSON 格式一致,单脚本即可。
94
+ Maturity: draft Tags: [hook, cli]
95
+ Proposed reason: decision-confirmation — ≥2 候选方案经权衡后确认选型。
96
+ Session context: Session goal: ship Stop-hook for v2 release.
97
+ ⚠ Possible duplicate of KT-D-0007 (similarity 0.78 on title + summary)
98
+ ```
99
+
100
+ The Skill MUST read `proposed_reason` from the pending file's frontmatter
101
+ (parse the YAML block, key `proposed_reason`) and the `## Why proposed`
102
+ line / first non-blank line of `## Session context` from the body. If
103
+ either is missing on a pre-rc.7 pending entry, render `Proposed reason:
104
+ <legacy entry, no reason recorded>` and `Session context: <not recorded>`
105
+ so the reviewer can still proceed.
106
+
107
+ 5. Surface a per-item AskUserQuestion:
108
+
109
+ ```ts
110
+ AskUserQuestion({
111
+ header: "Review pending entry",
112
+ question: "What action for '单 .cjs hook 跨客户端'?",
113
+ options: ["approve", "reject", "modify", "defer", "skip"]
114
+ })
115
+ ```
116
+
117
+ 6. Route the user's choice:
118
+ - `approve` → accumulate pending_path into a batch; flush via single `fab_review action="approve"` with `pending_paths=[…]` after the loop ends.
119
+ - `reject` → ask the user for a one-line reason via free-text follow-up; call `fab_review action="reject"` with `pending_paths=[path]` and `reason`.
120
+ - `modify` → see Modify Sub-Flow below.
121
+ - `defer` → call `fab_review action="defer"` with `pending_paths=[path]`; optional `until` ISO datetime if the user supplies one ("defer 2 weeks" → compute and set).
122
+ - `skip` → no MCP call; move to next item.
123
+ 7. After the loop, display a roll-up: counts by action, list of newly-allocated `stable_id`s (from approve output), and tail of `.fabric/events.jsonl` showing the appended events.
124
+
125
+ ### Mode: topic — Search & Surface Findings
126
+
127
+ 1. Extract the topic keyword(s) from the user's message (e.g. "find about deepMerge" → query="deepMerge").
128
+ 2. Call `fab_review action="search"` with `query` and any obvious filters (if user said "team-only" → `filters.layer="team"`).
129
+ 3. Server returns `items[]` ranked by relevance — these are entries already in `.fabric/knowledge/{layer}/{type}/` (NOT pending), unless `filters` says otherwise.
130
+ 4. Render top-N (cap at 8) results with title / summary / pending_path.
131
+ 5. If the user follow-up indicates intent to act ("approve all", "modify the second one"), pivot into the corresponding pending mode action — the search result already gives the `pending_path` needed for the action.
132
+ 6. NEVER surface a per-item AskUserQuestion just for browsing — only when the user signals an action verb.
133
+
134
+ ### Mode: health — Corpus Health & Stale Detection
135
+
136
+ 1. Call `fab_review action="list"` with `filters.maturity="draft"` (or no filter for full corpus inspection).
137
+ 2. Tail `.fabric/events.jsonl` for layer_changed / demoted / rejected counts in the trailing 30 days.
138
+ 3. Compute stale candidates: pending entries with mtime `>14 days` OR maturity=draft entries with no recent evidence-append events.
139
+ 4. Render a corpus dashboard:
140
+
141
+ ```md
142
+ ## Health Overview
143
+ - Pending: 12 entries (oldest 18d) — recommend `defer` or `reject`
144
+ - Drafts: 8 (3 are stale candidates: KP-G-3, KP-G-5, KT-P-9)
145
+ - Layer flips (30d): 2
146
+ - Rejections (30d): 1
147
+ ```
148
+
149
+ 5. For each stale candidate, surface AskUserQuestion `{options: ["defer", "demote", "skip"]}`; route `defer` → `fab_review action="defer"`, `demote` → `fab_review action="modify"` with `changes.maturity` lowered (or `reject` if the user wants outright removal of a pending entry).
150
+
151
+ ### Mode: revisit — Specific Entry Deep Dive
152
+
153
+ 1. The user referenced a specific entry (by id `KT-D-7` or by slug `single-cjs-hook`).
154
+ 2. Call `fab_review action="list"` with `filters` narrowed by best-guess fields; if the entry is canonical (has stable_id), `Read` the file directly at `.fabric/knowledge/{layer}/{type}/<id>--<slug>.md`.
155
+ 3. Display the full body (frontmatter + content). Tail the events.jsonl for any history events tagged with this stable_id.
156
+ 4. Surface AskUserQuestion `{options: ["approve", "modify", "reject", "skip"]}` only if the entry is still pending; for canonical entries the only mutation path is `modify` (incl. layer flip).
157
+
158
+ ## Semantic Check Guidance (LLM-Assisted Duplicate / Contradiction Detection)
159
+
160
+ > Boundary B (locked): "extraction classification / layer inference / slug naming / mode inference / **semantic dedup** → Skill (LLM); pending file write / frontmatter assembly / idempotency check / counter mgmt / layer-flip transaction / atomic promote → MCP (deterministic)"
161
+
162
+ Semantic check is the LLM's job — the MCP tool does NOT compare meaning. Run this check during `pending` mode (and on demand during `topic` mode):
163
+
164
+ For each pending entry to be presented:
165
+
166
+ 1. Call `fab_review action="search"` with `query=<title or summary keywords>` and `filters.type=<same type>` to fetch already-canonical entries of the same type.
167
+ 2. Compare semantically (LLM judgment, not string match):
168
+ - **Duplicate** — same essential claim. Heuristics: title keyword overlap >60%, summary asserts the same outcome with no novel context. Flag: `⚠ Possible duplicate of <stable_id>`.
169
+ - **Contradiction** — opposing claims about the same subject. Heuristics: one entry says "use X" while pending says "avoid X" on identical scope. Flag: `⚠ Contradicts <stable_id>`.
170
+ - **Subsumption** — pending fully covered by an existing entry plus extras. Flag: `⚠ Subsumed by <stable_id>; consider modify-to-merge`.
171
+ 3. Surface the flag in the per-item display block (see pending mode step 4).
172
+ 4. The user decides:
173
+ - Still approve → flag is informational; pending becomes canonical alongside the existing entry.
174
+ - Modify-to-harmonize → user supplies edits via `modify` action; consider merging language with the existing entry.
175
+ - Reject as duplicate → reason field MUST cite the existing stable_id (e.g. `reason="duplicate of KT-D-7"`).
176
+
177
+ DO NOT call `AskUserQuestion` to ask "is this a duplicate?" — the LLM has already judged. The user only chooses among approve / reject / modify, which is a genuine choice.
178
+
179
+ ## Modify Sub-Flow & Layer-Flip Rules
180
+
181
+ `modify` is the only action that mutates frontmatter or stable_id. It accepts `changes` of shape `{title?, summary?, layer?, maturity?, tags?}`. Server semantics:
182
+
183
+ - **title / summary / tags / maturity changes** → in-place rewrite; stable_id PRESERVED; emits `knowledge_slug_renamed` only when slug derives from title.
184
+ - **layer change** → the ONLY legal stable_id mutation in the system.
185
+
186
+ ### Layer-Flip Rules (the only legal stable_id mutation)
187
+
188
+ Triggered when `changes.layer` differs from current entry layer. Server-side transaction:
189
+
190
+ 1. Allocate new id under target layer via `KnowledgeIdAllocator.allocate(new_layer, type)` (e.g. KT-D-7 in `team/decisions/` flips to KP-D-3 in `personal/decisions/`).
191
+ 2. `git mv <old-layer>/<type>/<old-id>--<slug>.md <new-layer>/<type>/<new-id>--<slug>.md`.
192
+ 3. Append `knowledge_layer_changed` event with `{from_layer, to_layer, prior_stable_id, new_stable_id}`.
193
+ 4. Server response includes `prior_stable_id` and `new_stable_id` — surface BOTH to the user in the roll-up.
194
+
195
+ Skill responsibilities for layer flip:
196
+
197
+ - BEFORE calling fab_review, surface `AskUserQuestion {options: ["team", "personal"]}` to confirm target layer. The default in the question header should reflect the verbatim layer heuristic (default team unless 强 personal signals dominate). This IS a genuine choice — the user must pick.
198
+ - AFTER server returns, render: `Layer flipped: <prior_stable_id> → <new_stable_id>`. Do NOT silently swallow the id change — downstream agents may have cached the prior id.
199
+
200
+ ### Modify Examples
201
+
202
+ ```ts
203
+ // Maturity bump only (no id change)
204
+ mcp__fabric__fab_review({
205
+ action: "modify",
206
+ pending_path: "knowledge/team/decisions/KT-D-0007--single-cjs-hook.md",
207
+ changes: { maturity: "verified" }
208
+ })
209
+
210
+ // Layer flip team → personal (id WILL change)
211
+ mcp__fabric__fab_review({
212
+ action: "modify",
213
+ pending_path: "knowledge/team/guidelines/KT-G-0003--indent-style.md",
214
+ changes: { layer: "personal" }
215
+ })
216
+ ```
217
+
218
+ ## AskUserQuestion Policy
219
+
220
+ ### DO ask (genuine choices that require human judgment)
221
+
222
+ - Per-pending-item action: `["approve", "reject", "modify", "defer", "skip"]`
223
+ - Per-stale-item action (health mode): `["defer", "demote", "skip"]`
224
+ - Layer-flip target when modify path includes layer change: `["team", "personal"]`
225
+ - Reject reason follow-up (free-text, may use AskUserQuestion's free-form variant if available, otherwise plain prompt)
226
+
227
+ ### DO NOT ask (system must infer or operate deterministically)
228
+
229
+ - Mode picking (pending / topic / health / revisit) — INFERRED per the 3-step algorithm
230
+ - Whether to invoke this skill at all — Stop-hook signal or explicit user request decides
231
+ - Whether an entry is a duplicate — LLM semantic check answers
232
+ - Frontmatter parsing — deterministic, never asked
233
+ - Allocate next id — deterministic via KnowledgeIdAllocator, never asked
234
+
235
+ ### Per-Item Question Phrasing Template
236
+
237
+ ```ts
238
+ AskUserQuestion({
239
+ header: "Review pending entry",
240
+ question: "What action for '{title}'? ({pending_path})",
241
+ options: ["approve", "reject", "modify", "defer", "skip"]
242
+ })
243
+ ```
244
+
245
+ For layer-flip target:
246
+
247
+ ```ts
248
+ AskUserQuestion({
249
+ header: "Layer-flip target",
250
+ question: "Move '{title}' to which layer? (current: {current_layer})",
251
+ options: ["team", "personal"]
252
+ })
253
+ ```
254
+
255
+ ## Decision Tree — Is This Entry Approvable?
256
+
257
+ ```
258
+ Pending entry presented for review
259
+ ├─ Has clear stable scope (not too narrow / not one-off)?
260
+ │ ├─ NO → reject (reason: "too narrow / not generalizable")
261
+ │ └─ YES ↓
262
+ ├─ Duplicates an existing canonical entry (semantic check flagged)?
263
+ │ ├─ YES → reject (reason: "duplicate of <stable_id>") OR modify-to-merge
264
+ │ └─ NO ↓
265
+ ├─ Wrong layer (e.g. personal preference shipped as team)?
266
+ │ ├─ YES → modify with changes.layer = correct layer (triggers id flip)
267
+ │ └─ NO ↓
268
+ └─ Approvable → approve (single via pending_paths=[path], or batch via pending_paths=[…])
269
+ ```
270
+
271
+ ## Hard Rules — DISPLAY / WRITE Split
272
+
273
+ ### DISPLAY Rules
274
+
275
+ - MUST infer mode before any user-facing output; NEVER ask the user which mode to use.
276
+ - MUST present every pending item with explicit `[type=...]`, `[layer=...]`, and `pending_path=...` fields.
277
+ - MUST run semantic check during `pending` mode and surface `⚠` flags for possible duplicates / contradictions.
278
+ - MUST display zh-CN body for entry summaries (M3 style consistent with fabric-archive).
279
+ - MUST display EN section headings.
280
+ - MUST surface BOTH `prior_stable_id` and `new_stable_id` after a layer flip so callers can update caches.
281
+ - NEVER show raw `idempotency_key` to the user (internal server-side concern).
282
+ - NEVER skip the AskUserQuestion for per-item action — every pending entry MUST receive an explicit user judgment OR a `skip`.
283
+
284
+ ### WRITE Rules
285
+
286
+ - NEVER write a knowledge file directly via Edit/Write/Bash; the only legal mutation path is `mcp__fabric__fab_review`.
287
+ - NEVER call `git mv` from this skill — layer flip and slug rename are server-side transactions.
288
+ - NEVER invent an `action` value — `action` MUST be one of {`list`, `approve`, `reject`, `modify`, `search`, `defer`}.
289
+ - NEVER batch heterogeneous decisions into a single MCP call. Approve and reject MAY be batched within their own action; modify MUST be one call per entry.
290
+ - NEVER invoke `fab_review action="approve"` without at least one `pending_paths` entry.
291
+ - NEVER infer a layer-flip target — the user MUST choose via AskUserQuestion.
292
+ - MUST preserve protected tokens exactly: `stable_id`, `pending_path`, `layer`, `team`, `personal`, `knowledge_promoted`, `knowledge_layer_changed`, `knowledge_proposed`, `fab_review`, `MUST`, `NEVER`.
293
+
294
+ ## Output Contract
295
+
296
+ After each invocation, the skill MUST produce a brief roll-up to the user:
297
+
298
+ ```md
299
+ # Review Summary — mode={pending|topic|health|revisit}
300
+ - Listed: N entries
301
+ - Approved: M (new stable_ids: KT-D-12, KT-G-4, KP-P-2)
302
+ - Rejected: R
303
+ - Modified: U (incl. K layer flips)
304
+ - Deferred: D
305
+ - Skipped: S
306
+
307
+ ## Events appended (.fabric/events.jsonl tail)
308
+ - knowledge_promote_started ×M
309
+ - knowledge_promoted ×M
310
+ - knowledge_layer_changed ×K
311
+ - knowledge_rejected ×R
312
+ - knowledge_deferred ×D
313
+ ```
314
+
315
+ Also surface a one-line `git status` of `.fabric/knowledge/` so the user sees the file moves caused by approve / layer-flip.
316
+
317
+ ## Worked Examples
318
+
319
+ ### Example A — pending mode with semantic check flagging a duplicate (user chooses reject)
320
+
321
+ User: "review the pending knowledge".
322
+
323
+ Inferred mode: `pending` (Step 1 keyword "review … pending").
324
+
325
+ Skill flow:
326
+
327
+ 1. `fab_review action="list"` → returns 3 pending items.
328
+ 2. Semantic check on item 2 (`pending/decisions/single-cjs-hook.md`) — `fab_review action="search"` with `query="single cjs hook"` filter `type=decisions` returns canonical `KT-D-0007--single-cjs-hook-across-clients.md` (similarity high).
329
+ 3. Display block:
330
+
331
+ ```md
332
+ ## [type=decisions] [layer=team] pending_path=knowledge/pending/decisions/single-cjs-hook.md
333
+ Title: 单 .cjs hook 跨客户端
334
+ Summary: 三客户端 stdout JSON 格式一致,单脚本即可。
335
+ ⚠ Possible duplicate of KT-D-0007 (similarity 0.84 on title + summary)
336
+ ```
337
+
338
+ 4. AskUserQuestion fires; user picks `reject`.
339
+ 5. Free-text follow-up: user types `duplicate of KT-D-7`.
340
+ 6. `fab_review action="reject"` with `pending_paths=["knowledge/pending/decisions/single-cjs-hook.md"]` and `reason="duplicate of KT-D-7"`.
341
+ 7. Roll-up reports: 1 rejected, 0 approved, events appended.
342
+
343
+ ### Example B — revisit mode with layer flip (KT → KP)
344
+
345
+ User: "look at KT-G-3, that's actually personal not team".
346
+
347
+ Inferred mode: `revisit` (Step 1 keyword "look at <id>").
348
+
349
+ Skill flow:
350
+
351
+ 1. Read `.fabric/knowledge/team/guidelines/KT-G-0003--indent-style.md`. Display body to user.
352
+ 2. AskUserQuestion `{options: ["approve", "modify", "reject", "skip"]}` — user picks `modify`.
353
+ 3. Skill detects user-stated intent "actually personal not team" — surface AskUserQuestion `{options: ["team", "personal"]}` with current layer=team noted; user confirms `personal`.
354
+ 4. Call:
355
+
356
+ ```ts
357
+ mcp__fabric__fab_review({
358
+ action: "modify",
359
+ pending_path: "knowledge/team/guidelines/KT-G-0003--indent-style.md",
360
+ changes: { layer: "personal" }
361
+ })
362
+ ```
363
+
364
+ 5. Server returns `{prior_stable_id: "KT-G-0003", new_stable_id: "KP-G-0001"}`.
365
+ 6. Roll-up: `Layer flipped: KT-G-0003 → KP-G-0001`. `git status` shows the rename across layer roots.
366
+
367
+ ### Example C — health mode finding stale entries (defer 2, demote 1)
368
+
369
+ User: "anything stale in our knowledge base?"
370
+
371
+ Inferred mode: `health` (Step 1 keyword "stale").
372
+
373
+ Skill flow:
374
+
375
+ 1. `fab_review action="list"` (no filter) + tail events.jsonl for trailing-30d demoted/layer_changed counts.
376
+ 2. Compute stale candidates: 3 pending entries with mtime >14d (KP-G-5 candidate-pending, KT-P-9 candidate-pending, KP-G-3 canonical draft with no evidence-append in 21d).
377
+ 3. Render dashboard then loop per stale item.
378
+ 4. Per-item AskUserQuestion fires:
379
+ - KP-G-5 → user picks `defer` (until="2026-06-01") → `fab_review action="defer"` with `until` set.
380
+ - KT-P-9 → user picks `defer` (no until) → `fab_review action="defer"` with no `until`.
381
+ - KP-G-3 → user picks `demote` → `fab_review action="modify"` with `changes.maturity="draft"` (already draft; equivalently demote means reject if pending — skill chooses correct action by inspecting current state).
382
+ 5. Roll-up: 2 deferred, 1 modified, events appended (`knowledge_deferred ×2`, `knowledge_promote_started/promoted` not relevant; `knowledge_layer_changed` not relevant).
@@ -1,216 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- createDebugLogger,
4
- displayWidth,
5
- padEnd,
6
- paint,
7
- readFabricConfig,
8
- resolveDevMode,
9
- symbol,
10
- t
11
- } from "./chunk-QPCRBQ5Y.js";
12
-
13
- // src/commands/scan.ts
14
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
15
- import { isAbsolute, join, relative, resolve, sep } from "path";
16
- import { defineCommand } from "citty";
17
-
18
- // src/scanner/detector.ts
19
- import { detectFramework } from "@fenglimg/fabric-shared/node";
20
-
21
- // src/scanner/ignores.ts
22
- var DEFAULT_IGNORES = [
23
- "**/*.meta",
24
- "library/**",
25
- "temp/**",
26
- "build/**",
27
- "settings/**",
28
- "profiles/**",
29
- "node_modules/**",
30
- "dist/**",
31
- ".git/**",
32
- ".fabric/**"
33
- ];
34
- function resolveIgnores(fabricConfig) {
35
- return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
36
- }
37
-
38
- // src/commands/scan.ts
39
- async function createScanReport(targetInput = process.cwd(), fabricConfig) {
40
- const target = normalizeTarget(targetInput);
41
- const framework = detectFramework(target);
42
- const readmeQuality = getReadmeQuality(target);
43
- const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
44
- const hasExistingFabric = existsSync(join(target, ".fabric", "bootstrap", "README.md")) || existsSync(join(target, ".fabric"));
45
- const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
46
- return {
47
- target,
48
- framework,
49
- readmeQuality,
50
- hasContributing,
51
- fileCount: walkResult.fileCount,
52
- ignoredCount: walkResult.ignoredCount,
53
- hasExistingFabric,
54
- recommendations: buildRecommendations({
55
- framework,
56
- readmeQuality,
57
- hasContributing,
58
- hasExistingFabric
59
- })
60
- };
61
- }
62
- var scanCommand = defineCommand({
63
- meta: {
64
- name: "scan",
65
- description: t("cli.scan.description")
66
- },
67
- args: {
68
- target: {
69
- type: "string",
70
- description: t("cli.scan.args.target.description")
71
- },
72
- debug: {
73
- type: "boolean",
74
- description: t("cli.scan.args.debug.description"),
75
- default: false
76
- },
77
- json: {
78
- type: "boolean",
79
- description: t("cli.scan.args.json.description"),
80
- default: false
81
- }
82
- },
83
- async run({ args }) {
84
- const workspaceRoot = process.cwd();
85
- const logger = createDebugLogger(args.debug);
86
- const resolution = resolveDevMode(args.target, workspaceRoot);
87
- const fabricConfig = readFabricConfig(workspaceRoot);
88
- logger(`scan target source: ${resolution.source}`);
89
- for (const step of resolution.chain) {
90
- logger(step);
91
- }
92
- const report = await createScanReport(resolution.target, fabricConfig);
93
- if (args.json) {
94
- console.log(JSON.stringify(report, null, 2));
95
- return;
96
- }
97
- printPrettyReport(report, Boolean(args.debug));
98
- }
99
- });
100
- var scan_default = scanCommand;
101
- function normalizeTarget(targetInput) {
102
- return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
103
- }
104
- function getReadmeQuality(target) {
105
- const readmePath = join(target, "README.md");
106
- if (!existsSync(readmePath)) {
107
- return "stub";
108
- }
109
- const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
110
- return wordCount >= 200 ? "ok" : "stub";
111
- }
112
- function walkFiles(root, ignorePatterns) {
113
- if (!existsSync(root) || !statSync(root).isDirectory()) {
114
- throw new Error(t("cli.shared.target-invalid", { target: root }));
115
- }
116
- let fileCount = 0;
117
- let ignoredCount = 0;
118
- const stack = [root];
119
- while (stack.length > 0) {
120
- const current = stack.pop();
121
- if (current === void 0) {
122
- continue;
123
- }
124
- for (const entry of readdirSync(current, { withFileTypes: true })) {
125
- const absolutePath = join(current, entry.name);
126
- const relativePath = toPosixPath(relative(root, absolutePath));
127
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
128
- ignoredCount += 1;
129
- continue;
130
- }
131
- if (entry.isDirectory()) {
132
- stack.push(absolutePath);
133
- } else if (entry.isFile()) {
134
- fileCount += 1;
135
- }
136
- }
137
- }
138
- return { fileCount, ignoredCount };
139
- }
140
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
141
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
142
- }
143
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
144
- const normalizedPattern = toPosixPath(pattern);
145
- if (normalizedPattern === "**/*.meta") {
146
- return relativePath.endsWith(".meta");
147
- }
148
- if (normalizedPattern.endsWith("/**")) {
149
- const directoryPrefix = normalizedPattern.slice(0, -3);
150
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
151
- }
152
- return relativePath === normalizedPattern;
153
- }
154
- function toPosixPath(path) {
155
- return path.split(sep).join("/");
156
- }
157
- function buildRecommendations(input) {
158
- const recommendations = [];
159
- if (!input.hasExistingFabric) {
160
- recommendations.push(t("cli.scan.recommendation.init"));
161
- }
162
- if (input.readmeQuality === "stub") {
163
- recommendations.push(t("cli.scan.recommendation.readme"));
164
- }
165
- if (!input.hasContributing) {
166
- recommendations.push(t("cli.scan.recommendation.contributing"));
167
- }
168
- if (input.framework.kind === "unknown") {
169
- recommendations.push(t("cli.scan.recommendation.unknown-framework"));
170
- } else {
171
- recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
172
- }
173
- return recommendations;
174
- }
175
- function printPrettyReport(report, debug) {
176
- console.log(paint.ai(t("cli.scan.report.title")));
177
- const rows = [
178
- [t("cli.scan.report.target"), paint.human(report.target)],
179
- [t("cli.scan.report.framework"), paint.ai(report.framework.kind)],
180
- [
181
- t("cli.scan.report.readme-quality"),
182
- report.readmeQuality === "ok" ? paint.success(t("cli.scan.readme-quality.ok")) : paint.warn(t("cli.scan.readme-quality.stub"))
183
- ],
184
- [
185
- t("cli.scan.report.contributing"),
186
- report.hasContributing ? paint.success(t("cli.shared.present")) : paint.warn(t("cli.shared.absent"))
187
- ],
188
- [t("cli.scan.report.files-counted"), String(report.fileCount)],
189
- [t("cli.scan.report.ignored-entries"), report.ignoredCount > 0 ? paint.muted(String(report.ignoredCount)) : "0"],
190
- [
191
- t("cli.scan.report.existing-fabric"),
192
- report.hasExistingFabric ? paint.warn(t("cli.shared.yes")) : paint.success(t("cli.shared.no"))
193
- ]
194
- ];
195
- if (debug) {
196
- rows.splice(2, 0, [
197
- t("cli.scan.report.evidence"),
198
- report.framework.evidence.length > 0 ? paint.muted(report.framework.evidence.join(", ")) : paint.muted(t("cli.shared.none"))
199
- ]);
200
- }
201
- const labelWidth = Math.max(...rows.map(([key]) => displayWidth(key)));
202
- for (const [key, value] of rows) {
203
- console.log(`${paint.muted(padEnd(key, labelWidth))} ${value}`);
204
- }
205
- console.log(paint.muted(t("cli.scan.report.recommendations")));
206
- for (const recommendation of report.recommendations) {
207
- console.log(`${symbol.warn} ${paint.drift(recommendation)}`);
208
- }
209
- }
210
-
211
- export {
212
- detectFramework,
213
- createScanReport,
214
- scanCommand,
215
- scan_default
216
- };