@fenglimg/fabric-cli 2.2.0-rc.1 → 2.2.0-rc.3
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/dist/chunk-5LQIHYFC.js +64 -0
- package/dist/chunk-5ZUMLCD5.js +248 -0
- package/dist/chunk-EOT63RDH.js +36 -0
- package/dist/{chunk-AOE6AYI7.js → chunk-F6ITRM7T.js} +2 -2
- package/dist/{chunk-WU6GAPKH.js → chunk-H3FE6VIK.js} +3 -5
- package/dist/chunk-XCBVSGCS.js +25 -0
- package/dist/{chunk-2R55HNVD.js → chunk-XHHCRDIR.js} +71 -6
- package/dist/{config-XYRBZJDU.js → config-VJMXCLXW.js} +1 -1
- package/dist/{doctor-YONYXDX6.js → doctor-J4O3X54I.js} +118 -7
- package/dist/index.js +13 -12
- package/dist/{install-74ANPCCP.js → install-BULNDUIM.js} +159 -80
- package/dist/{plan-context-hint-FC6P3WFE.js → plan-context-hint-CHVZGOZ5.js} +21 -8
- package/dist/{scope-explain-CDIZESP5.js → scope-explain-BWRWBCCP.js} +14 -4
- package/dist/{status-GLQWLWH6.js → status-PANEGKU2.js} +17 -6
- package/dist/store-66NK2FTQ.js +443 -0
- package/dist/{sync-UJ4BBCZJ.js → sync-EA5HZMXM.js} +165 -21
- package/dist/{uninstall-C3QXKOO6.js → uninstall-F75MPKQC.js} +27 -1
- package/dist/{whoami-2MLO4Y37.js → whoami-66YKY5DZ.js} +16 -5
- package/package.json +3 -3
- package/templates/hooks/cite-policy-evict.cjs +412 -160
- package/templates/hooks/configs/claude-code.json +17 -2
- package/templates/hooks/configs/codex-hooks.json +14 -2
- package/templates/hooks/configs/cursor-hooks.json +14 -2
- package/templates/hooks/fabric-hint.cjs +151 -15
- package/templates/hooks/knowledge-hint-broad.cjs +12 -1
- package/templates/hooks/knowledge-hint-narrow.cjs +54 -1
- package/templates/hooks/post-tooluse-mutation.cjs +285 -0
- package/templates/hooks/session-end-marker.cjs +140 -0
- package/templates/skills/fabric-archive/SKILL.md +7 -1
- package/dist/chunk-4R2CYEA4.js +0 -116
- package/dist/chunk-L4Q55UC4.js +0 -52
- package/dist/chunk-LFIKMVY7.js +0 -27
- package/dist/chunk-RYAFBNES.js +0 -33
- package/dist/chunk-T5RPGCCM.js +0 -40
- package/dist/store-XB3ADT65.js +0 -144
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lifecycle-refactor W2-T3 — PostToolUse mutation marker hook (previously
|
|
4
|
+
* dormant). Closes the mutation env opened by the PreToolUse narrow hint.
|
|
5
|
+
*
|
|
6
|
+
* PostToolUse fires AFTER an Edit/Write/MultiEdit tool call completes. This
|
|
7
|
+
* hook appends one `file_mutated` event per edited path to
|
|
8
|
+
* `.fabric/events.jsonl`, carrying the `tool_call_id` so doctor can pair the
|
|
9
|
+
* Pre (intent) and Post (mutation) halves of a single tool call — the per-call
|
|
10
|
+
* key also guards against parallel-fire races (two concurrent edits never
|
|
11
|
+
* collapse to one ledger key).
|
|
12
|
+
*
|
|
13
|
+
* Design (lifecycle-concept-final.md §1 FROZEN invariants + §5 row7):
|
|
14
|
+
* - LOW compute: extract paths + tool_call_id, append; the hook never reads
|
|
15
|
+
* or aggregates the ledger, never runs `git diff`. ALL mutation_pool /
|
|
16
|
+
* attribution work is doctor-side (offline, §5 row7).
|
|
17
|
+
* - hook = nudge/marker, never a gate (KT-DEC-0007): every error path ends
|
|
18
|
+
* in a silent exit 0; we never throw upward.
|
|
19
|
+
* - Front-stage O(1) per path: advisory-locked append, no traversal.
|
|
20
|
+
* - Per-event session_id: threaded from the REAL payload when present
|
|
21
|
+
* (omitted when the client omits it — the marker is still useful for the
|
|
22
|
+
* tool_call_id pairing even without a session).
|
|
23
|
+
* - Hooks never require() the server package — only co-located lib/*.cjs.
|
|
24
|
+
*
|
|
25
|
+
* Each emitted line matches `fileMutatedEventSchema`
|
|
26
|
+
* (packages/shared/src/schemas/event-ledger.ts):
|
|
27
|
+
* { kind:"fabric-event", id, ts, schema_version:1, session_id?,
|
|
28
|
+
* event_type:"file_mutated", path, tool_call_id, tool_name? }
|
|
29
|
+
*
|
|
30
|
+
* Stdout/stderr are intentionally empty — PostToolUse is observation-only and
|
|
31
|
+
* never blocks the host's tool pipeline.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const { randomUUID } = require("node:crypto");
|
|
35
|
+
const { existsSync } = require("node:fs");
|
|
36
|
+
const { isAbsolute, join, relative } = require("node:path");
|
|
37
|
+
|
|
38
|
+
// W1-01 (ISS-011) parity: route every shared-ledger append through the
|
|
39
|
+
// advisory-lock primitive so concurrent PostToolUse fires (multi-window /
|
|
40
|
+
// parallel edits) never interleave a partial line. Best-effort, drop-on-
|
|
41
|
+
// contention — same primitive the narrow/broad hooks use.
|
|
42
|
+
const { appendLockedLine } = require("./lib/injection-log.cjs");
|
|
43
|
+
|
|
44
|
+
const FABRIC_DIR_REL = ".fabric";
|
|
45
|
+
const EVENTS_LEDGER_FILE = "events.jsonl";
|
|
46
|
+
|
|
47
|
+
// Tool names that trigger the mutation marker. PostToolUse fires on many tool
|
|
48
|
+
// names across clients; we only react to the file-edit tools (matches the
|
|
49
|
+
// PreToolUse narrow hint's EDIT_TOOL_NAMES so Pre/Post pair on the same set).
|
|
50
|
+
const EDIT_TOOL_NAMES = new Set(["Edit", "Write", "MultiEdit"]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read stdin (or a test-supplied raw string) as JSON. Returns null on any
|
|
54
|
+
* parse failure — the hook stays silent rather than crashing the tool pipeline.
|
|
55
|
+
*/
|
|
56
|
+
function readPayload(rawStdin) {
|
|
57
|
+
if (typeof rawStdin !== "string" || rawStdin.length === 0) return null;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(rawStdin);
|
|
60
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract the tool name. Mirrors the narrow hint's probe:
|
|
71
|
+
* - Claude Code / Codex: { tool_name, ... }
|
|
72
|
+
* - Cursor (legacy): { tool, ... }
|
|
73
|
+
*/
|
|
74
|
+
function extractToolName(payload) {
|
|
75
|
+
if (!payload || typeof payload !== "object") return null;
|
|
76
|
+
if (typeof payload.tool_name === "string") return payload.tool_name;
|
|
77
|
+
if (typeof payload.tool === "string") return payload.tool;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract the tool_input object, accepting both the `tool_input`
|
|
83
|
+
* (Claude/Codex) and `input` (Cursor) conventions.
|
|
84
|
+
*/
|
|
85
|
+
function extractToolInput(payload) {
|
|
86
|
+
if (!payload || typeof payload !== "object") return null;
|
|
87
|
+
if (payload.tool_input && typeof payload.tool_input === "object") {
|
|
88
|
+
return payload.tool_input;
|
|
89
|
+
}
|
|
90
|
+
if (payload.input && typeof payload.input === "object") {
|
|
91
|
+
return payload.input;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pull file paths out of a tool_input object. Same three shapes the narrow
|
|
98
|
+
* hint handles (single file_path / array file_paths / MultiEdit edits[]).
|
|
99
|
+
* Returns a deduped array of strings — empty when none recognizable.
|
|
100
|
+
*/
|
|
101
|
+
function extractPaths(toolInput) {
|
|
102
|
+
if (!toolInput || typeof toolInput !== "object") return [];
|
|
103
|
+
const collected = [];
|
|
104
|
+
|
|
105
|
+
if (typeof toolInput.file_path === "string" && toolInput.file_path.length > 0) {
|
|
106
|
+
collected.push(toolInput.file_path);
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(toolInput.file_paths)) {
|
|
109
|
+
for (const p of toolInput.file_paths) {
|
|
110
|
+
if (typeof p === "string" && p.length > 0) collected.push(p);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(toolInput.edits)) {
|
|
114
|
+
for (const edit of toolInput.edits) {
|
|
115
|
+
if (
|
|
116
|
+
edit &&
|
|
117
|
+
typeof edit === "object" &&
|
|
118
|
+
typeof edit.file_path === "string" &&
|
|
119
|
+
edit.file_path.length > 0
|
|
120
|
+
) {
|
|
121
|
+
collected.push(edit.file_path);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const p of collected) {
|
|
129
|
+
if (seen.has(p)) continue;
|
|
130
|
+
seen.add(p);
|
|
131
|
+
out.push(p);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Extract the per-call id. Claude Code's PostToolUse payload carries the id of
|
|
138
|
+
* the originating tool_use block as `tool_use_id`; older drafts / other clients
|
|
139
|
+
* variously used `tool_call_id` / `call_id` / `id`. We probe in that order.
|
|
140
|
+
*
|
|
141
|
+
* Returns null when none is present — the caller then synthesizes a best-effort
|
|
142
|
+
* fallback key so the marker still lands (per W2-T3: "缺失则用 best-effort
|
|
143
|
+
* fallback key 但仍 append"). A fallback key cannot pair with the Pre half but
|
|
144
|
+
* still records the mutation, which is strictly better than dropping it.
|
|
145
|
+
*/
|
|
146
|
+
function extractToolCallId(payload) {
|
|
147
|
+
if (!payload || typeof payload !== "object") return null;
|
|
148
|
+
const candidates = ["tool_use_id", "tool_call_id", "call_id", "id"];
|
|
149
|
+
for (const key of candidates) {
|
|
150
|
+
const v = payload[key];
|
|
151
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Normalize a path to a project-relative, forward-slash form. Drops paths that
|
|
158
|
+
* escape the project tree (mirrors appendEditIntentToLedger in the narrow
|
|
159
|
+
* hook). Returns null for out-of-tree / empty.
|
|
160
|
+
*/
|
|
161
|
+
function normalizePath(projectRoot, p) {
|
|
162
|
+
if (typeof p !== "string" || p.length === 0) return null;
|
|
163
|
+
let rel;
|
|
164
|
+
if (isAbsolute(p)) {
|
|
165
|
+
rel = relative(projectRoot, p);
|
|
166
|
+
if (rel.startsWith("..")) return null;
|
|
167
|
+
} else {
|
|
168
|
+
if (p.startsWith("..")) return null;
|
|
169
|
+
rel = p;
|
|
170
|
+
}
|
|
171
|
+
const slashed = rel.split(/[\\/]/).join("/");
|
|
172
|
+
return slashed.length > 0 ? slashed : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Append one `file_mutated` marker per edited path to `.fabric/events.jsonl`.
|
|
177
|
+
* Best-effort:
|
|
178
|
+
* - Skips silently when `.fabric/` does not exist (project not init'd).
|
|
179
|
+
* - Skips silently when there are no in-tree paths.
|
|
180
|
+
* - ANY error (append, JSON throw) is swallowed — never blocks the pipeline.
|
|
181
|
+
*
|
|
182
|
+
* One JSON line per path (PIPE_BUF-atomic). The per-path lines share a single
|
|
183
|
+
* tool_call_id so doctor can group them as one tool call.
|
|
184
|
+
*/
|
|
185
|
+
function appendFileMutated(projectRoot, now, paths, toolCallId, toolName, sessionId) {
|
|
186
|
+
try {
|
|
187
|
+
const fabricDir = join(projectRoot, FABRIC_DIR_REL);
|
|
188
|
+
if (!existsSync(fabricDir)) return;
|
|
189
|
+
const pathList = Array.isArray(paths)
|
|
190
|
+
? paths.map((p) => normalizePath(projectRoot, p)).filter((p) => p !== null)
|
|
191
|
+
: [];
|
|
192
|
+
if (pathList.length === 0) return;
|
|
193
|
+
const tsMs = now instanceof Date ? now.getTime() : Number(now);
|
|
194
|
+
// Best-effort fallback key when the client omits the tool-call id: the
|
|
195
|
+
// mutation is still recorded (can't pair with the Pre half, but the path
|
|
196
|
+
// signal is preserved). Per-fire UUID keeps parallel fires distinct.
|
|
197
|
+
const callId =
|
|
198
|
+
typeof toolCallId === "string" && toolCallId.length > 0
|
|
199
|
+
? toolCallId
|
|
200
|
+
: `fallback:${randomUUID()}`;
|
|
201
|
+
const validSessionId =
|
|
202
|
+
typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
203
|
+
const validToolName =
|
|
204
|
+
typeof toolName === "string" && toolName.length > 0 ? toolName : null;
|
|
205
|
+
const lines =
|
|
206
|
+
pathList
|
|
207
|
+
.map((p) =>
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
kind: "fabric-event",
|
|
210
|
+
id: `event:${randomUUID()}`,
|
|
211
|
+
ts: tsMs,
|
|
212
|
+
schema_version: 1,
|
|
213
|
+
...(validSessionId ? { session_id: validSessionId } : {}),
|
|
214
|
+
event_type: "file_mutated",
|
|
215
|
+
path: p,
|
|
216
|
+
tool_call_id: callId,
|
|
217
|
+
...(validToolName ? { tool_name: validToolName } : {}),
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
.join("\n") + "\n";
|
|
221
|
+
appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), lines);
|
|
222
|
+
} catch {
|
|
223
|
+
// Silent — marker failure must never block the tool pipeline.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -----------------------------------------------------------------------------
|
|
228
|
+
// Main — invoked as a CLI (require.main === module) and in-process by tests.
|
|
229
|
+
// Wraps the entire flow in try/catch: ANY error → silent exit 0.
|
|
230
|
+
// -----------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
function main(env) {
|
|
233
|
+
try {
|
|
234
|
+
const cwd = (env && env.cwd) || process.cwd();
|
|
235
|
+
const now = (env && env.now) || new Date();
|
|
236
|
+
const payload =
|
|
237
|
+
env && env.payload !== undefined ? env.payload : readPayload(env && env.stdin);
|
|
238
|
+
if (payload === null || payload === undefined) return;
|
|
239
|
+
|
|
240
|
+
const toolName = extractToolName(payload);
|
|
241
|
+
if (!toolName || !EDIT_TOOL_NAMES.has(toolName)) return;
|
|
242
|
+
|
|
243
|
+
const toolInput = extractToolInput(payload);
|
|
244
|
+
const paths = extractPaths(toolInput);
|
|
245
|
+
if (paths.length === 0) return;
|
|
246
|
+
|
|
247
|
+
const toolCallId = extractToolCallId(payload);
|
|
248
|
+
const sessionId =
|
|
249
|
+
payload && typeof payload === "object" && typeof payload.session_id === "string"
|
|
250
|
+
? payload.session_id
|
|
251
|
+
: null;
|
|
252
|
+
|
|
253
|
+
appendFileMutated(cwd, now, paths, toolCallId, toolName, sessionId);
|
|
254
|
+
} catch {
|
|
255
|
+
// Silent — never block the tool pipeline on hook failure.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
main,
|
|
261
|
+
readPayload,
|
|
262
|
+
extractToolName,
|
|
263
|
+
extractToolInput,
|
|
264
|
+
extractPaths,
|
|
265
|
+
extractToolCallId,
|
|
266
|
+
normalizePath,
|
|
267
|
+
appendFileMutated,
|
|
268
|
+
CONSTANTS: {
|
|
269
|
+
FABRIC_DIR_REL,
|
|
270
|
+
EVENTS_LEDGER_FILE,
|
|
271
|
+
EDIT_TOOL_NAMES,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (require.main === module) {
|
|
276
|
+
// Read stdin synchronously (small hook payloads, no concurrency concerns).
|
|
277
|
+
let stdinRaw = "";
|
|
278
|
+
try {
|
|
279
|
+
stdinRaw = require("node:fs").readFileSync(0, "utf8");
|
|
280
|
+
} catch {
|
|
281
|
+
// No stdin — proceed with empty payload (E*: no append without paths).
|
|
282
|
+
}
|
|
283
|
+
main({ cwd: process.cwd(), now: new Date(), stdin: stdinRaw });
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lifecycle-refactor W2-T2 — SessionEnd marker hook (previously dormant).
|
|
4
|
+
*
|
|
5
|
+
* SessionEnd fires once when a client session boots down. This hook is a
|
|
6
|
+
* PURE MARKER: it appends a single `session_ended` event (session_id + ts,
|
|
7
|
+
* via the shared envelope) to `.fabric/events.jsonl` and does NOTHING else.
|
|
8
|
+
*
|
|
9
|
+
* Design (lifecycle-concept-final.md §1 FROZEN invariants + §5 row2):
|
|
10
|
+
* - ZERO compute: the hook never reads/aggregates the ledger. ALL
|
|
11
|
+
* surfaced→cited→edited funnel reconciliation is doctor-side (offline).
|
|
12
|
+
* - hook = nudge/marker, never a gate (KT-DEC-0007): every error path ends
|
|
13
|
+
* in a silent exit 0; we never throw upward.
|
|
14
|
+
* - Front-stage O(1): one advisory-locked append, no traversal.
|
|
15
|
+
* - Per-event session_id: when the client omits session_id we DEGRADE by
|
|
16
|
+
* skipping the append entirely (a marker with no session is useless for
|
|
17
|
+
* the per-session funnel doctor reconstructs).
|
|
18
|
+
* - Hooks never require() the server package — only the co-located lib/*.cjs.
|
|
19
|
+
*
|
|
20
|
+
* The emitted line matches `sessionEndedEventSchema`
|
|
21
|
+
* (packages/shared/src/schemas/event-ledger.ts):
|
|
22
|
+
* { kind:"fabric-event", id, ts, schema_version:1, session_id?, event_type:"session_ended" }
|
|
23
|
+
*
|
|
24
|
+
* Stdout/stderr are intentionally empty — SessionEnd is observation-only.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { randomUUID } = require("node:crypto");
|
|
28
|
+
const { existsSync } = require("node:fs");
|
|
29
|
+
const { join } = require("node:path");
|
|
30
|
+
|
|
31
|
+
// W1-01 (ISS-011) parity: route the shared-ledger append through the
|
|
32
|
+
// advisory-lock primitive so concurrent SessionEnd fires (multi-window) never
|
|
33
|
+
// interleave a partial line. Drop-on-contention, best-effort — same primitive
|
|
34
|
+
// the narrow/broad hooks use.
|
|
35
|
+
const { appendLockedLine } = require("./lib/injection-log.cjs");
|
|
36
|
+
|
|
37
|
+
const FABRIC_DIR_REL = ".fabric";
|
|
38
|
+
const EVENTS_LEDGER_FILE = "events.jsonl";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read stdin (or a test-supplied raw string) as JSON. Returns null on any
|
|
42
|
+
* parse failure — the hook stays silent rather than crashing session teardown.
|
|
43
|
+
*/
|
|
44
|
+
function readPayload(rawStdin) {
|
|
45
|
+
if (typeof rawStdin !== "string" || rawStdin.length === 0) return null;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(rawStdin);
|
|
48
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract the REAL payload session_id (never a synthetic fallback). The
|
|
59
|
+
* funnel reconciliation in doctor keys on the client's own session id, so a
|
|
60
|
+
* fabricated id would silently corrupt the per-session join. Returns null when
|
|
61
|
+
* absent — the caller then skips the append (degraded, per §1).
|
|
62
|
+
*/
|
|
63
|
+
function extractSessionId(payload) {
|
|
64
|
+
if (
|
|
65
|
+
payload &&
|
|
66
|
+
typeof payload === "object" &&
|
|
67
|
+
typeof payload.session_id === "string" &&
|
|
68
|
+
payload.session_id.length > 0
|
|
69
|
+
) {
|
|
70
|
+
return payload.session_id;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Append one `session_ended` marker to `.fabric/events.jsonl`. Best-effort:
|
|
77
|
+
* - Skips silently when `.fabric/` does not exist (project not init'd).
|
|
78
|
+
* - Skips silently when sessionId is null (degraded — no session to mark).
|
|
79
|
+
* - ANY error (append, JSON throw) is swallowed — never blocks teardown.
|
|
80
|
+
*/
|
|
81
|
+
function appendSessionEnded(projectRoot, now, sessionId) {
|
|
82
|
+
try {
|
|
83
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return;
|
|
84
|
+
const fabricDir = join(projectRoot, FABRIC_DIR_REL);
|
|
85
|
+
if (!existsSync(fabricDir)) return;
|
|
86
|
+
const tsMs = now instanceof Date ? now.getTime() : Number(now);
|
|
87
|
+
const event = {
|
|
88
|
+
kind: "fabric-event",
|
|
89
|
+
id: `event:${randomUUID()}`,
|
|
90
|
+
ts: tsMs,
|
|
91
|
+
schema_version: 1,
|
|
92
|
+
session_id: sessionId,
|
|
93
|
+
event_type: "session_ended",
|
|
94
|
+
};
|
|
95
|
+
appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), JSON.stringify(event) + "\n");
|
|
96
|
+
} catch {
|
|
97
|
+
// Silent — marker failure must never block session teardown.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -----------------------------------------------------------------------------
|
|
102
|
+
// Main — invoked as a CLI (require.main === module) and in-process by tests.
|
|
103
|
+
// Wraps the entire flow in try/catch: ANY error → silent exit 0.
|
|
104
|
+
// -----------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function main(env) {
|
|
107
|
+
try {
|
|
108
|
+
const cwd = (env && env.cwd) || process.cwd();
|
|
109
|
+
const now = (env && env.now) || new Date();
|
|
110
|
+
const payload =
|
|
111
|
+
env && env.payload !== undefined ? env.payload : readPayload(env && env.stdin);
|
|
112
|
+
const sessionId = extractSessionId(payload);
|
|
113
|
+
appendSessionEnded(cwd, now, sessionId);
|
|
114
|
+
} catch {
|
|
115
|
+
// Silent — never block session teardown on hook failure.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
main,
|
|
121
|
+
readPayload,
|
|
122
|
+
extractSessionId,
|
|
123
|
+
appendSessionEnded,
|
|
124
|
+
CONSTANTS: {
|
|
125
|
+
FABRIC_DIR_REL,
|
|
126
|
+
EVENTS_LEDGER_FILE,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (require.main === module) {
|
|
131
|
+
// Read stdin synchronously (small hook payloads, no concurrency concerns).
|
|
132
|
+
let stdinRaw = "";
|
|
133
|
+
try {
|
|
134
|
+
stdinRaw = require("node:fs").readFileSync(0, "utf8");
|
|
135
|
+
} catch {
|
|
136
|
+
// No stdin — proceed with empty payload (degrades to a no-op append).
|
|
137
|
+
}
|
|
138
|
+
main({ cwd: process.cwd(), now: new Date(), stdin: stdinRaw });
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
@@ -113,6 +113,12 @@ Assign `relevance_scope` ∈ {narrow, broad} + derive `relevance_paths` BEFORE b
|
|
|
113
113
|
|
|
114
114
|
`Read ref/phase-3-5-scope.md` for the 6-step relevance_paths derivation pseudocode (COLLECT → DEDUPE → BLACKLIST → PUBLIC-PREFIX GENERALIZE → SCOPE GATE → ATTACH READ-ONLY EVIDENCE), worked example (5 sample paths → glob + literal output), and narrow↔broad inline-edit re-derivation rules.
|
|
115
115
|
|
|
116
|
+
### Phase 3.6 — Related-edge Extraction (§7 graph generation)
|
|
117
|
+
|
|
118
|
+
For each candidate, identify the **`related`** graph edges to other KB entries — the store-qualified `stable_id`s this entry semantically links to (the decision it supersedes, the pitfall it explains, the model it instantiates). You discovered these ids during the session via `fab_recall` / plan-context, so cite the ones you actually saw, NEVER invent stable_ids. Because `fab_extract_knowledge` has no dedicated `related` input, record the candidate edges as one line inside `session_context` (e.g. `related: team:KT-DEC-0007, team:KT-PIT-0011`) so they survive to approve-time frontmatter authoring (`fabric-review` writes the canonical `related: [...]` frontmatter).
|
|
119
|
+
|
|
120
|
+
**§4 privacy iron law — KT→KP is FORBIDDEN.** A **team** (`KT-*`) entry's `related` MUST NOT point at a **personal** (`KP-*`) id: that would write a personal-knowledge topology pointer into the project's shared physical ledger (`./.fabric/agents.meta.json`). Allowed: `KT→KT`, `KP→KP`, `KP→KT`. Forbidden: `KT→KP` only. When unsure whether a target is personal, OMIT the edge. (The meta-builder also strips any KT→KP edge that slips through, but do not rely on that — author clean edges.)
|
|
121
|
+
|
|
116
122
|
### Phase 4 — Persist via MCP
|
|
117
123
|
|
|
118
124
|
For each user-confirmed candidate, call `fab_extract_knowledge` ONCE (NEVER batch). Required: `source_sessions[]`, `recent_paths[]` (cap 20), `user_messages_summary`, `type` (plural form: decisions/pitfalls/guidelines/models/processes), `slug`, `layer`, `relevance_scope`, `relevance_paths[]`, `proposed_reason` (enum), `session_context` (3-5 line narrative). Four OPTIONAL rc.23 triage fields (`intent_clues`, `tech_stack`, `impact`, `must_read_if`) — populate when clean, **omit rather than guess**.
|
|
@@ -155,7 +161,7 @@ MANDATORY closing step on EVERY invocation (Phase 4 success path + every early-e
|
|
|
155
161
|
- 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+.
|
|
156
162
|
- NEVER batch multiple candidates into a single fab_extract_knowledge call; one call per candidate.
|
|
157
163
|
- NEVER paraphrase the verbatim layer heuristic block above — the Chinese text is contract-locked.
|
|
158
|
-
- MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `.fabric/knowledge/pending/`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`.
|
|
164
|
+
- MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `.fabric/knowledge/pending/`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`, `related`, `KT→KP`.
|
|
159
165
|
|
|
160
166
|
## Worked Examples / E5 Cron / Dry-run (ref-only)
|
|
161
167
|
|
package/dist/chunk-4R2CYEA4.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
loadProjectConfig,
|
|
4
|
-
saveProjectConfig
|
|
5
|
-
} from "./chunk-LFIKMVY7.js";
|
|
6
|
-
import {
|
|
7
|
-
loadGlobalConfig,
|
|
8
|
-
resolveGlobalRoot,
|
|
9
|
-
saveGlobalConfig
|
|
10
|
-
} from "./chunk-RYAFBNES.js";
|
|
11
|
-
|
|
12
|
-
// src/store/store-ops.ts
|
|
13
|
-
import { randomUUID } from "crypto";
|
|
14
|
-
import { existsSync } from "fs";
|
|
15
|
-
import { join } from "path";
|
|
16
|
-
import {
|
|
17
|
-
addMountedStore,
|
|
18
|
-
bindRequiredStore,
|
|
19
|
-
detachMountedStore,
|
|
20
|
-
explainStore,
|
|
21
|
-
initStore,
|
|
22
|
-
storeRelativePath
|
|
23
|
-
} from "@fenglimg/fabric-shared";
|
|
24
|
-
var NO_GLOBAL_CONFIG = "no global Fabric config found \u2014 run `fabric install --global <url>` first";
|
|
25
|
-
function requireConfig(globalRoot) {
|
|
26
|
-
const config = loadGlobalConfig(globalRoot);
|
|
27
|
-
if (config === null) {
|
|
28
|
-
throw new Error(NO_GLOBAL_CONFIG);
|
|
29
|
-
}
|
|
30
|
-
return config;
|
|
31
|
-
}
|
|
32
|
-
function storeList(globalRoot = resolveGlobalRoot()) {
|
|
33
|
-
return requireConfig(globalRoot).stores;
|
|
34
|
-
}
|
|
35
|
-
function storeAdd(store, globalRoot = resolveGlobalRoot()) {
|
|
36
|
-
const next = addMountedStore(requireConfig(globalRoot), store);
|
|
37
|
-
saveGlobalConfig(next, globalRoot);
|
|
38
|
-
return next;
|
|
39
|
-
}
|
|
40
|
-
function storeCreate(alias, now, options = {}) {
|
|
41
|
-
const globalRoot = options.globalRoot ?? resolveGlobalRoot();
|
|
42
|
-
const config = requireConfig(globalRoot);
|
|
43
|
-
const uuid = options.uuid ?? randomUUID();
|
|
44
|
-
const storeDir = join(globalRoot, storeRelativePath(uuid));
|
|
45
|
-
initStore(
|
|
46
|
-
storeDir,
|
|
47
|
-
{ store_uuid: uuid, created_at: now, canonical_alias: alias },
|
|
48
|
-
{ git: options.git }
|
|
49
|
-
);
|
|
50
|
-
const mounted = options.remote === void 0 ? { store_uuid: uuid, alias } : { store_uuid: uuid, alias, remote: options.remote };
|
|
51
|
-
const next = addMountedStore(config, mounted);
|
|
52
|
-
saveGlobalConfig(next, globalRoot);
|
|
53
|
-
return { config: next, store_uuid: uuid, storeDir };
|
|
54
|
-
}
|
|
55
|
-
function assertStoreMountable(uuid, globalRoot = resolveGlobalRoot()) {
|
|
56
|
-
const storeDir = join(globalRoot, storeRelativePath(uuid));
|
|
57
|
-
if (!existsSync(join(storeDir, "store.json"))) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`cannot mount store ${uuid}: no store tree at ${storeDir} \u2014 clone it first (\`fabric install --global --url <remote>\`) or create it locally, then re-run \`fabric store add\`. Refusing to register a phantom store.`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function storeRemove(alias, globalRoot = resolveGlobalRoot()) {
|
|
64
|
-
const result = detachMountedStore(requireConfig(globalRoot), alias);
|
|
65
|
-
saveGlobalConfig(result.config, globalRoot);
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
function storeExplain(alias, globalRoot = resolveGlobalRoot()) {
|
|
69
|
-
return explainStore(requireConfig(globalRoot), alias);
|
|
70
|
-
}
|
|
71
|
-
var NO_PROJECT_CONFIG = "no project Fabric config \u2014 run `fabric install` in this repo first";
|
|
72
|
-
function requireProjectConfig(projectRoot) {
|
|
73
|
-
const config = loadProjectConfig(projectRoot);
|
|
74
|
-
if (config === null) {
|
|
75
|
-
throw new Error(NO_PROJECT_CONFIG);
|
|
76
|
-
}
|
|
77
|
-
return config;
|
|
78
|
-
}
|
|
79
|
-
function storeBind(projectRoot, entry) {
|
|
80
|
-
const config = requireProjectConfig(projectRoot);
|
|
81
|
-
const next = {
|
|
82
|
-
...config,
|
|
83
|
-
required_stores: bindRequiredStore(config.required_stores ?? [], entry)
|
|
84
|
-
};
|
|
85
|
-
saveProjectConfig(next, projectRoot);
|
|
86
|
-
return next;
|
|
87
|
-
}
|
|
88
|
-
function storeSwitchWrite(projectRoot, alias) {
|
|
89
|
-
const config = requireProjectConfig(projectRoot);
|
|
90
|
-
const next = { ...config, active_write_store: alias };
|
|
91
|
-
saveProjectConfig(next, projectRoot);
|
|
92
|
-
return next;
|
|
93
|
-
}
|
|
94
|
-
function missingRequiredStores(projectRoot, globalRoot = resolveGlobalRoot()) {
|
|
95
|
-
const project = loadProjectConfig(projectRoot);
|
|
96
|
-
if (project === null || project.required_stores === void 0) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
const global = loadGlobalConfig(globalRoot);
|
|
100
|
-
const mounted = new Set(
|
|
101
|
-
(global?.stores ?? []).flatMap((s) => [s.alias, s.store_uuid])
|
|
102
|
-
);
|
|
103
|
-
return project.required_stores.filter((r) => !mounted.has(r.id));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export {
|
|
107
|
-
storeList,
|
|
108
|
-
storeAdd,
|
|
109
|
-
storeCreate,
|
|
110
|
-
assertStoreMountable,
|
|
111
|
-
storeRemove,
|
|
112
|
-
storeExplain,
|
|
113
|
-
storeBind,
|
|
114
|
-
storeSwitchWrite,
|
|
115
|
-
missingRequiredStores
|
|
116
|
-
};
|
package/dist/chunk-L4Q55UC4.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
loadProjectConfig
|
|
4
|
-
} from "./chunk-LFIKMVY7.js";
|
|
5
|
-
import {
|
|
6
|
-
loadGlobalConfig,
|
|
7
|
-
resolveGlobalRoot
|
|
8
|
-
} from "./chunk-RYAFBNES.js";
|
|
9
|
-
|
|
10
|
-
// src/store/scope-explain.ts
|
|
11
|
-
import {
|
|
12
|
-
createStoreResolver
|
|
13
|
-
} from "@fenglimg/fabric-shared";
|
|
14
|
-
function buildResolveInput(projectRoot, globalRoot = resolveGlobalRoot()) {
|
|
15
|
-
const global = loadGlobalConfig(globalRoot);
|
|
16
|
-
if (global === null) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
const project = loadProjectConfig(projectRoot);
|
|
20
|
-
return {
|
|
21
|
-
uid: global.uid,
|
|
22
|
-
mountedStores: global.stores.map((s) => ({
|
|
23
|
-
store_uuid: s.store_uuid,
|
|
24
|
-
alias: s.alias,
|
|
25
|
-
...s.remote === void 0 ? {} : { remote: s.remote },
|
|
26
|
-
writable: s.writable ?? true,
|
|
27
|
-
personal: s.personal ?? false
|
|
28
|
-
})),
|
|
29
|
-
requiredStores: (project?.required_stores ?? []).map((r) => ({
|
|
30
|
-
id: r.id,
|
|
31
|
-
...r.suggested_remote === void 0 ? {} : { suggested_remote: r.suggested_remote }
|
|
32
|
-
})),
|
|
33
|
-
...project?.active_write_store === void 0 ? {} : { activeWriteAlias: project.active_write_store }
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function scopeExplain(projectRoot, scope, globalRoot = resolveGlobalRoot()) {
|
|
37
|
-
const input = buildResolveInput(projectRoot, globalRoot);
|
|
38
|
-
if (input === null) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
const resolver = createStoreResolver();
|
|
42
|
-
return {
|
|
43
|
-
scope,
|
|
44
|
-
readSet: resolver.resolveReadSet(input),
|
|
45
|
-
writeTarget: resolver.resolveWriteTarget(input, scope).target
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
buildResolveInput,
|
|
51
|
-
scopeExplain
|
|
52
|
-
};
|
package/dist/chunk-LFIKMVY7.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/store/project-config-io.ts
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { fabricConfigSchema } from "@fenglimg/fabric-shared";
|
|
7
|
-
function projectConfigPath(projectRoot) {
|
|
8
|
-
return join(projectRoot, ".fabric", "fabric-config.json");
|
|
9
|
-
}
|
|
10
|
-
function loadProjectConfig(projectRoot) {
|
|
11
|
-
const path = projectConfigPath(projectRoot);
|
|
12
|
-
if (!existsSync(path)) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
return fabricConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
16
|
-
}
|
|
17
|
-
function saveProjectConfig(config, projectRoot) {
|
|
18
|
-
const validated = fabricConfigSchema.parse(config);
|
|
19
|
-
mkdirSync(join(projectRoot, ".fabric"), { recursive: true });
|
|
20
|
-
writeFileSync(projectConfigPath(projectRoot), `${JSON.stringify(validated, null, 2)}
|
|
21
|
-
`, "utf8");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
loadProjectConfig,
|
|
26
|
-
saveProjectConfig
|
|
27
|
-
};
|
package/dist/chunk-RYAFBNES.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/store/global-config-io.ts
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
import { homedir } from "os";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { globalConfigSchema } from "@fenglimg/fabric-shared";
|
|
8
|
-
function resolveGlobalRoot() {
|
|
9
|
-
return join(process.env.FABRIC_HOME ?? homedir(), ".fabric");
|
|
10
|
-
}
|
|
11
|
-
function globalConfigPath(globalRoot = resolveGlobalRoot()) {
|
|
12
|
-
return join(globalRoot, "fabric-global.json");
|
|
13
|
-
}
|
|
14
|
-
function loadGlobalConfig(globalRoot = resolveGlobalRoot()) {
|
|
15
|
-
const path = globalConfigPath(globalRoot);
|
|
16
|
-
if (!existsSync(path)) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
return globalConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
20
|
-
}
|
|
21
|
-
function saveGlobalConfig(config, globalRoot = resolveGlobalRoot()) {
|
|
22
|
-
const validated = globalConfigSchema.parse(config);
|
|
23
|
-
mkdirSync(globalRoot, { recursive: true });
|
|
24
|
-
writeFileSync(globalConfigPath(globalRoot), `${JSON.stringify(validated, null, 2)}
|
|
25
|
-
`, "utf8");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export {
|
|
29
|
-
resolveGlobalRoot,
|
|
30
|
-
globalConfigPath,
|
|
31
|
-
loadGlobalConfig,
|
|
32
|
-
saveGlobalConfig
|
|
33
|
-
};
|