@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0
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 +6 -6
- package/dist/chunk-6ICJICVU.js +10 -0
- package/dist/chunk-74SZWYPH.js +658 -0
- package/dist/{chunk-UHNP7T7W.js → chunk-EYIDD2YS.js} +346 -86
- package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
- package/dist/chunk-WWNXR34K.js +49 -0
- package/dist/doctor-T7JWODKG.js +282 -0
- package/dist/hooks-Y74Y5LQS.js +12 -0
- package/dist/index.js +7 -5
- package/dist/{init-DRHUYHYA.js → init-BIRSIOXO.js} +212 -271
- package/dist/plan-context-hint-QMUPAXIB.js +98 -0
- package/dist/{scan-HU2EGITF.js → scan-LMK3UCWL.js} +4 -2
- package/dist/{serve-3LXXSBFR.js → serve-H554BHLG.js} +8 -4
- package/package.json +3 -3
- package/templates/bootstrap/CLAUDE.md +1 -1
- package/templates/bootstrap/codex-AGENTS-header.md +1 -1
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
- package/templates/hooks/configs/README.md +73 -0
- package/templates/hooks/configs/claude-code.json +37 -0
- package/templates/hooks/configs/codex-hooks.json +20 -0
- package/templates/hooks/configs/cursor-hooks.json +20 -0
- package/templates/hooks/fabric-hint.cjs +1307 -0
- package/templates/hooks/knowledge-hint-broad.cjs +464 -0
- package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
- package/templates/hooks/lib/session-digest-writer.cjs +172 -0
- package/templates/skills/fabric-archive/SKILL.md +486 -0
- package/templates/skills/fabric-import/SKILL.md +588 -0
- package/templates/skills/fabric-review/SKILL.md +382 -0
- package/dist/doctor-DUHWLAYD.js +0 -98
|
@@ -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).
|
package/dist/doctor-DUHWLAYD.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
paint,
|
|
4
|
-
resolveDevMode,
|
|
5
|
-
symbol,
|
|
6
|
-
t
|
|
7
|
-
} from "./chunk-5LOYBXWD.js";
|
|
8
|
-
|
|
9
|
-
// src/commands/doctor.ts
|
|
10
|
-
import { defineCommand } from "citty";
|
|
11
|
-
import { checkLockOrThrow, runDoctorFix, runDoctorReport } from "@fenglimg/fabric-server";
|
|
12
|
-
var doctorCommand = defineCommand({
|
|
13
|
-
meta: {
|
|
14
|
-
name: "doctor",
|
|
15
|
-
description: t("cli.doctor.description")
|
|
16
|
-
},
|
|
17
|
-
args: {
|
|
18
|
-
target: {
|
|
19
|
-
type: "string",
|
|
20
|
-
description: t("cli.doctor.args.target.description")
|
|
21
|
-
},
|
|
22
|
-
fix: {
|
|
23
|
-
type: "boolean",
|
|
24
|
-
description: t("cli.doctor.args.fix.description"),
|
|
25
|
-
default: false
|
|
26
|
-
},
|
|
27
|
-
json: {
|
|
28
|
-
type: "boolean",
|
|
29
|
-
description: t("cli.doctor.args.json.description"),
|
|
30
|
-
default: false
|
|
31
|
-
},
|
|
32
|
-
strict: {
|
|
33
|
-
type: "boolean",
|
|
34
|
-
description: t("cli.doctor.args.strict.description"),
|
|
35
|
-
default: false
|
|
36
|
-
},
|
|
37
|
-
force: {
|
|
38
|
-
type: "boolean",
|
|
39
|
-
description: t("cli.doctor.args.force.description"),
|
|
40
|
-
default: false
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
async run({ args }) {
|
|
44
|
-
const workspaceRoot = process.cwd();
|
|
45
|
-
const resolution = resolveDevMode(args.target, workspaceRoot);
|
|
46
|
-
checkLockOrThrow(resolution.target, { force: args.force });
|
|
47
|
-
const fixReport = args.fix === true ? await runDoctorFix(resolution.target) : null;
|
|
48
|
-
const report = fixReport?.report ?? await runDoctorReport(resolution.target);
|
|
49
|
-
if (args.json === true) {
|
|
50
|
-
writeStdout(JSON.stringify(fixReport ?? report, null, 2));
|
|
51
|
-
} else {
|
|
52
|
-
if (fixReport !== null) {
|
|
53
|
-
writeStdout(fixReport.message);
|
|
54
|
-
}
|
|
55
|
-
renderHumanReport(report);
|
|
56
|
-
}
|
|
57
|
-
if (report.status === "error" || args.strict === true && (report.status === "warn" || report.warnings.length > 0)) {
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
var doctor_default = doctorCommand;
|
|
63
|
-
function renderHumanReport(report) {
|
|
64
|
-
writeStdout(`${renderStatus(report.status)} ${paint.ai("fabric doctor")} ${paint.human(report.summary.target)}`);
|
|
65
|
-
for (const check of report.checks) {
|
|
66
|
-
writeStdout(`${renderStatus(check.status)} ${check.name}: ${check.message}`);
|
|
67
|
-
}
|
|
68
|
-
writeIssueSection(t("doctor.section.fixable"), report.fixable_errors);
|
|
69
|
-
writeIssueSection(t("doctor.section.manual"), report.manual_errors);
|
|
70
|
-
writeIssueSection(t("doctor.section.warnings"), report.warnings);
|
|
71
|
-
}
|
|
72
|
-
function writeIssueSection(title, issues) {
|
|
73
|
-
if (issues.length === 0) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
writeStdout("");
|
|
77
|
-
writeStdout(title);
|
|
78
|
-
for (const issue of issues) {
|
|
79
|
-
writeStdout(`- ${issue.code}: ${issue.message}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function renderStatus(status) {
|
|
83
|
-
if (status === "ok") {
|
|
84
|
-
return symbol.ok;
|
|
85
|
-
}
|
|
86
|
-
if (status === "warn") {
|
|
87
|
-
return symbol.warn;
|
|
88
|
-
}
|
|
89
|
-
return symbol.error;
|
|
90
|
-
}
|
|
91
|
-
function writeStdout(message) {
|
|
92
|
-
process.stdout.write(`${message}
|
|
93
|
-
`);
|
|
94
|
-
}
|
|
95
|
-
export {
|
|
96
|
-
doctor_default as default,
|
|
97
|
-
doctorCommand
|
|
98
|
-
};
|