@fenglimg/fabric-cli 2.0.0-rc.34 → 2.0.0-rc.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-SRX7WZUG.js → chunk-BATF4PEJ.js} +2 -2
- package/dist/{chunk-5N3KXIVI.js → chunk-XVS4F3P6.js} +32 -0
- package/dist/{config-5CH4EJQ2.js → config-XJIPZNUP.js} +1 -1
- package/dist/{doctor-E26YO67D.js → doctor-2FCRAWDZ.js} +23 -8
- package/dist/index.js +7 -7
- package/dist/{install-XCRX34CX.js → install-XSUIX6AD.js} +59 -4
- package/dist/{onboard-coverage-6MN3CYHT.js → onboard-coverage-MFCAEBDO.js} +4 -4
- package/dist/{plan-context-hint-CXTLNVSV.js → plan-context-hint-UQLRKGBZ.js} +2 -2
- package/dist/{uninstall-Q7V55BXH.js → uninstall-BIJ5GLEU.js} +1 -1
- package/package.json +3 -4
- package/templates/hooks/cite-policy-evict.cjs +1 -1
- package/templates/hooks/knowledge-hint-broad.cjs +26 -6
- package/templates/hooks/knowledge-hint-narrow.cjs +106 -1
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +1 -1
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/e5-cron-recap.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 +10 -10
- package/templates/skills/fabric-import/SKILL.md +75 -163
- package/templates/skills/fabric-import/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-review/ref/i18n-policy.md +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rc.35 TASK-06 (P0-10.b) — summary-fallback library.
|
|
3
|
+
*
|
|
4
|
+
* Resolves opaque hint entries (where `entry.summary === entry.id` so the
|
|
5
|
+
* AI sees no information beyond the id) by reading the entry's markdown
|
|
6
|
+
* file at `.fabric/knowledge/<type>/<id>--<slug>.md`, extracting the first
|
|
7
|
+
* paragraph under `## Summary`, and substituting that text into the entry
|
|
8
|
+
* before the hook renders it.
|
|
9
|
+
*
|
|
10
|
+
* Caching: results are stored in `.fabric/.cache/summary-fallback.json`
|
|
11
|
+
* keyed by the current `revision_hash` returned by plan-context-hint. The
|
|
12
|
+
* cache is wiped wholesale when the revision changes (cheap invariant —
|
|
13
|
+
* any meta rev bump implies entry text MAY have moved). Per-process call
|
|
14
|
+
* also benefits from in-memory dedup since the same opaque id may appear
|
|
15
|
+
* across narrow + broad paths.
|
|
16
|
+
*
|
|
17
|
+
* Design contract:
|
|
18
|
+
* - Never throw. ANY failure (cache read, fs scan, file read) degrades
|
|
19
|
+
* to a no-op — the original opaque summary is left untouched. Hooks
|
|
20
|
+
* must remain best-effort.
|
|
21
|
+
* - Idempotent over identical inputs. Two calls in succession with the
|
|
22
|
+
* same revision_hash + entries set produce zero disk reads on the
|
|
23
|
+
* second call.
|
|
24
|
+
*
|
|
25
|
+
* Public API (module.exports):
|
|
26
|
+
* resolveOpaqueSummaries(entries, projectRoot, revisionHash) — returns
|
|
27
|
+
* a NEW array of entries with `summary` substituted for opaque cases.
|
|
28
|
+
* Original `entry.id` is preserved verbatim.
|
|
29
|
+
*
|
|
30
|
+
* _extractFirstSummaryParagraph(md) — pure helper, exposed for testing.
|
|
31
|
+
*
|
|
32
|
+
* _readCache / _writeCache — exposed for testing.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } = require("node:fs");
|
|
36
|
+
const { join } = require("node:path");
|
|
37
|
+
|
|
38
|
+
const CACHE_DIR_REL = ".fabric/.cache";
|
|
39
|
+
const CACHE_FILE_REL = ".fabric/.cache/summary-fallback.json";
|
|
40
|
+
const KNOWLEDGE_DIR_REL = ".fabric/knowledge";
|
|
41
|
+
const SUMMARY_MAX_LEN = 80;
|
|
42
|
+
const KNOWLEDGE_TYPE_DIRS = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
43
|
+
|
|
44
|
+
function _isOpaque(entry) {
|
|
45
|
+
if (!entry || typeof entry.id !== "string" || typeof entry.summary !== "string") {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return entry.summary.trim() === entry.id.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pure helper: extract the first paragraph under a `## Summary` heading.
|
|
53
|
+
*
|
|
54
|
+
* - `## Summary` is case-insensitive but level-sensitive (only H2).
|
|
55
|
+
* - First paragraph = lines until blank line or next heading.
|
|
56
|
+
* - Collapses whitespace + trims; returns `""` if no summary section or
|
|
57
|
+
* the section is empty.
|
|
58
|
+
*/
|
|
59
|
+
function _extractFirstSummaryParagraph(md) {
|
|
60
|
+
if (typeof md !== "string" || md.length === 0) return "";
|
|
61
|
+
const lines = md.split(/\r?\n/);
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
if (/^##\s+summary\s*$/i.test(lines[i].trim())) {
|
|
65
|
+
i += 1;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
if (i >= lines.length) return "";
|
|
71
|
+
// Skip blank lines after the heading
|
|
72
|
+
while (i < lines.length && lines[i].trim().length === 0) i += 1;
|
|
73
|
+
// Collect until the next blank line or next heading
|
|
74
|
+
const buf = [];
|
|
75
|
+
while (i < lines.length) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
if (line.trim().length === 0) break;
|
|
78
|
+
if (/^#{1,6}\s/.test(line.trim())) break;
|
|
79
|
+
buf.push(line.trim());
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
const flat = buf.join(" ").replace(/\s+/g, " ").trim();
|
|
83
|
+
if (flat.length === 0) return "";
|
|
84
|
+
if (flat.length <= SUMMARY_MAX_LEN) return flat;
|
|
85
|
+
return `${flat.slice(0, SUMMARY_MAX_LEN - 1)}…`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _readCache(projectRoot) {
|
|
89
|
+
const cachePath = join(projectRoot, CACHE_FILE_REL);
|
|
90
|
+
if (!existsSync(cachePath)) return null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = readFileSync(cachePath, "utf8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (parsed && typeof parsed === "object" && typeof parsed.revision === "string" && parsed.summaries && typeof parsed.summaries === "object") {
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore — caller treats null as no-cache
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _writeCache(projectRoot, payload) {
|
|
104
|
+
try {
|
|
105
|
+
const cacheDir = join(projectRoot, CACHE_DIR_REL);
|
|
106
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
107
|
+
const cachePath = join(projectRoot, CACHE_FILE_REL);
|
|
108
|
+
writeFileSync(cachePath, JSON.stringify(payload), "utf8");
|
|
109
|
+
} catch {
|
|
110
|
+
// Best-effort — failing to persist cache is not an error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Scan `.fabric/knowledge/<type>/` for the canonical `<id>--<slug>.md`
|
|
116
|
+
* matching `stableId`. Tries the most likely type-dir first based on the
|
|
117
|
+
* entry's `type` hint, then falls back to scanning all canonical type
|
|
118
|
+
* directories. Returns the absolute path or null.
|
|
119
|
+
*
|
|
120
|
+
* The id→file mapping is unique by construction (stable_id is allocated
|
|
121
|
+
* once per file), so the first match wins.
|
|
122
|
+
*/
|
|
123
|
+
function _findEntryFile(projectRoot, stableId, typeHint) {
|
|
124
|
+
const baseDir = join(projectRoot, KNOWLEDGE_DIR_REL);
|
|
125
|
+
if (!existsSync(baseDir)) return null;
|
|
126
|
+
const tryOrder = [];
|
|
127
|
+
if (typeof typeHint === "string" && typeHint.length > 0) {
|
|
128
|
+
// Accept both singular and plural hints — find the plural form.
|
|
129
|
+
const lower = typeHint.toLowerCase();
|
|
130
|
+
const plural = KNOWLEDGE_TYPE_DIRS.find((d) => d === lower || d.startsWith(lower));
|
|
131
|
+
if (plural) tryOrder.push(plural);
|
|
132
|
+
}
|
|
133
|
+
for (const t of KNOWLEDGE_TYPE_DIRS) {
|
|
134
|
+
if (!tryOrder.includes(t)) tryOrder.push(t);
|
|
135
|
+
}
|
|
136
|
+
const prefix = `${stableId}--`;
|
|
137
|
+
for (const t of tryOrder) {
|
|
138
|
+
const typeDir = join(baseDir, t);
|
|
139
|
+
if (!existsSync(typeDir)) continue;
|
|
140
|
+
let files;
|
|
141
|
+
try {
|
|
142
|
+
files = readdirSync(typeDir);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const f of files) {
|
|
147
|
+
if (f.startsWith(prefix) && f.endsWith(".md")) {
|
|
148
|
+
return join(typeDir, f);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _resolveOne(projectRoot, entry) {
|
|
156
|
+
const filePath = _findEntryFile(projectRoot, entry.id, entry.type);
|
|
157
|
+
if (filePath === null) return "";
|
|
158
|
+
let md;
|
|
159
|
+
try {
|
|
160
|
+
md = readFileSync(filePath, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
return _extractFirstSummaryParagraph(md);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Main API. Returns a new array of entries with `summary` swapped for
|
|
169
|
+
* the extracted fallback wherever the original summary was opaque AND
|
|
170
|
+
* the fallback extraction yielded a non-empty string. Non-opaque entries
|
|
171
|
+
* pass through unchanged.
|
|
172
|
+
*/
|
|
173
|
+
function resolveOpaqueSummaries(entries, projectRoot, revisionHash) {
|
|
174
|
+
if (!Array.isArray(entries) || entries.length === 0) return entries;
|
|
175
|
+
const cache = _readCache(projectRoot);
|
|
176
|
+
const cachedSummaries = cache && cache.revision === revisionHash && cache.summaries ? cache.summaries : {};
|
|
177
|
+
const nextCacheSummaries = { ...cachedSummaries };
|
|
178
|
+
let cacheChanged = cache === null || cache.revision !== revisionHash;
|
|
179
|
+
const result = entries.map((entry) => {
|
|
180
|
+
if (!_isOpaque(entry)) return entry;
|
|
181
|
+
const id = entry.id;
|
|
182
|
+
let fallback;
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(cachedSummaries, id)) {
|
|
184
|
+
fallback = cachedSummaries[id];
|
|
185
|
+
} else {
|
|
186
|
+
fallback = _resolveOne(projectRoot, entry);
|
|
187
|
+
nextCacheSummaries[id] = fallback;
|
|
188
|
+
cacheChanged = true;
|
|
189
|
+
}
|
|
190
|
+
if (typeof fallback === "string" && fallback.length > 0) {
|
|
191
|
+
return { ...entry, summary: fallback };
|
|
192
|
+
}
|
|
193
|
+
return entry;
|
|
194
|
+
});
|
|
195
|
+
if (cacheChanged) {
|
|
196
|
+
_writeCache(projectRoot, { revision: revisionHash, summaries: nextCacheSummaries });
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
resolveOpaqueSummaries,
|
|
203
|
+
_extractFirstSummaryParagraph,
|
|
204
|
+
_readCache,
|
|
205
|
+
_writeCache,
|
|
206
|
+
_findEntryFile,
|
|
207
|
+
_isOpaque,
|
|
208
|
+
SUMMARY_MAX_LEN,
|
|
209
|
+
KNOWLEDGE_TYPE_DIRS,
|
|
210
|
+
};
|
|
@@ -53,7 +53,7 @@ Graceful degradation: missing digest cache → single-session fallback. Missing
|
|
|
53
53
|
|
|
54
54
|
### Phase 1.5 — First-run Onboard (ref-only)
|
|
55
55
|
|
|
56
|
-
**SKIP this phase entirely unless** entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `
|
|
56
|
+
**SKIP this phase entirely unless** entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `fabric onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5, silently fall through to Phase 0.
|
|
57
57
|
|
|
58
58
|
`Read ref/phase-1-5-onboard.md` for the Step 1-4 coverage check → user prompt → tour-and-propose procedure.
|
|
59
59
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| `fab_extract_knowledge` MCP call (Phase 4) | One call per confirmed candidate, writes to `.fabric/knowledge/pending/<slug>.md` | 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. |
|
|
11
11
|
| `.fabric/.cache/session-digests/<session_id>.md` reads | Read freely (read-side, safe) | Read freely — same as normal. |
|
|
12
12
|
| Stop-hook / archive-hint stdin/stdout | Read-only inspection of `.fabric/events.jsonl` | Same — no change. |
|
|
13
13
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
`今日复盘` = E5 entry point. Default scope = today. Falls back to historical scan if today yields no candidates (silent-skip per Phase 4.5).
|
|
10
10
|
|
|
11
|
-
E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。
|
|
11
|
+
E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。fabric 端**零代码**——不提供 `fabric schedule` 子命令,亦不内嵌 daemon。用户基于自己的执行环境二选一接入: `/loop`(Claude Code 原生,推荐) 或 OS cron(跨平台 fallback)。
|
|
12
12
|
|
|
13
13
|
### /loop sample (primary path for Claude Code)
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ Rendering rule:
|
|
|
34
34
|
|
|
35
35
|
- `fabric_language === "zh-CN"` → emit the zh-CN variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
36
36
|
- `fabric_language === "en"` → emit the en variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
37
|
-
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `
|
|
37
|
+
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
|
|
38
38
|
- `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
|
|
39
39
|
|
|
40
40
|
Protected tokens (`fab_extract_knowledge`, `relevance_scope`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Phase 1.5 — First-run Onboard Phase (ref)
|
|
2
2
|
|
|
3
|
-
> **Loaded on demand.** SKILL.md hot path only runs this when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `
|
|
3
|
+
> **Loaded on demand.** SKILL.md hot path only runs this when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `fabric onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5 entries OR fully-covered workspaces, this entire phase is skipped — no reason to load.
|
|
4
4
|
|
|
5
5
|
## Phase 1.5 — First-run Onboard Phase
|
|
6
6
|
|
|
@@ -55,7 +55,7 @@ tone.
|
|
|
55
55
|
A first-time user whose ONLY invocations ever come via hook (never an
|
|
56
56
|
explicit `/fabric-archive`) will not see the onboard prompt; the 5
|
|
57
57
|
onboard slots remain empty. Mitigation: documentation tells users to
|
|
58
|
-
run an explicit `
|
|
58
|
+
run an explicit `fabric archive` at least once to populate the onboard
|
|
59
59
|
baseline.
|
|
60
60
|
|
|
61
61
|
##### Worked example
|
|
@@ -82,7 +82,7 @@ $ /fabric-archive
|
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
85
|
-
After F8a removed the auto-`
|
|
85
|
+
After F8a removed the auto-`fabric scan` baseline pipeline, a freshly installed
|
|
86
86
|
Fabric workspace ships with an EMPTY `.fabric/knowledge/` tree. Five fixed
|
|
87
87
|
**S5 onboard slots** capture the "project tone" baseline that the AI needs
|
|
88
88
|
for high-quality plan_context retrieval from day one:
|
|
@@ -98,10 +98,10 @@ gathering, so coverage state is fresh for the session.
|
|
|
98
98
|
|
|
99
99
|
#### Step 1 — Check coverage
|
|
100
100
|
|
|
101
|
-
Invoke `
|
|
101
|
+
Invoke `fabric onboard-coverage --json` and parse the JSON payload:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
|
|
104
|
+
fabric onboard-coverage --json
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
Expected shape:
|
|
@@ -147,8 +147,8 @@ proposed entry counts toward coverage once approved via fab_review.
|
|
|
147
147
|
| User choice | Action |
|
|
148
148
|
|----------------|--------|
|
|
149
149
|
| `fill-all` | For EACH slot in `missing`, run Step 4 (Tour-and-propose). All proposals share session_id; one batch review at the end (Phase 3). |
|
|
150
|
-
| `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `
|
|
151
|
-
| `dismiss-all` | For EACH slot in `missing`, invoke `Bash("
|
|
150
|
+
| `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `fabric config dismiss-slot <slot>`; `skip` → leave for next archive run. |
|
|
151
|
+
| `dismiss-all` | For EACH slot in `missing`, invoke `Bash("fabric config dismiss-slot <slot>")`. Print a one-line confirmation each. Skip to Phase 0. |
|
|
152
152
|
| `skip` | No-op. Slots remain in `missing` for the next archive run. Skip to Phase 0. |
|
|
153
153
|
|
|
154
154
|
#### Step 4 — Tour-and-propose (per-slot)
|
|
@@ -174,7 +174,7 @@ After Read-ing the slot-specific sources, classify the observation:
|
|
|
174
174
|
|
|
175
175
|
Call `fab_extract_knowledge` with the inferred fields PLUS `onboard_slot:
|
|
176
176
|
<slot>`. The pending file's frontmatter will carry the slot label, and the
|
|
177
|
-
next `
|
|
177
|
+
next `fabric onboard-coverage` run will see the slot as filled (once approved
|
|
178
178
|
via fab_review).
|
|
179
179
|
|
|
180
180
|
Example:
|
|
@@ -201,9 +201,9 @@ mcp__fabric__fab_extract_knowledge({
|
|
|
201
201
|
|
|
202
202
|
- MUST run BEFORE Phase 2 evidence gathering — onboard is a separate flow,
|
|
203
203
|
not interleaved with session-archive candidates.
|
|
204
|
-
- MUST call `
|
|
204
|
+
- MUST call `fabric onboard-coverage --json` before deciding; never assume
|
|
205
205
|
coverage state.
|
|
206
|
-
- NEVER fill a slot that is in `opted_out` — `
|
|
206
|
+
- NEVER fill a slot that is in `opted_out` — `fabric onboard-coverage` already
|
|
207
207
|
excludes those from `missing`, but the Skill MUST NOT re-propose them
|
|
208
208
|
even if the user asks "fill all of them" — the dismiss is intentional.
|
|
209
209
|
- NEVER prompt the user when `missing.length === 0` — silent skip.
|