@fenglimg/fabric-cli 2.0.1 → 2.1.0-rc.2
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-D25XJ4BC.js → chunk-F46ORPOA.js} +23 -0
- package/dist/chunk-HFQVXY6P.js +86 -0
- package/dist/chunk-L4Q55UC4.js +52 -0
- package/dist/chunk-LFIKMVY7.js +27 -0
- package/dist/chunk-RYAFBNES.js +33 -0
- package/dist/chunk-T5RPGCCM.js +40 -0
- package/dist/chunk-WU6GAPKH.js +36 -0
- package/dist/{doctor-EJDSEJSS.js → doctor-QVNPHLJK.js} +111 -1
- package/dist/index.js +12 -4
- package/dist/{install-EKWMFLUU.js → install-2HDO5FTQ.js} +242 -51
- package/dist/scope-explain-2F2R5URO.js +33 -0
- package/dist/status-GLQWLWH6.js +23 -0
- package/dist/store-XTSE5TY6.js +105 -0
- package/dist/sync-BJCWDPNC.js +245 -0
- package/dist/{uninstall-MH7ZIB6M.js → uninstall-TAXSUSKH.js} +14 -5
- package/dist/whoami-B6AEMSEV.js +31 -0
- package/package.json +3 -3
- package/templates/hooks/fabric-hint.cjs +40 -0
- package/templates/hooks/knowledge-hint-broad.cjs +40 -0
- package/templates/hooks/knowledge-hint-narrow.cjs +39 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +15 -9
- package/templates/hooks/lib/cite-line-parser.cjs +48 -26
- package/templates/skills/fabric-archive/SKILL.md +4 -0
- package/templates/skills/fabric-import/SKILL.md +4 -0
- package/templates/skills/fabric-review/SKILL.md +4 -0
- package/templates/skills/fabric-sync/SKILL.md +46 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
regenerateBindingsSnapshot
|
|
4
|
+
} from "./chunk-WU6GAPKH.js";
|
|
5
|
+
import "./chunk-L4Q55UC4.js";
|
|
6
|
+
import "./chunk-LFIKMVY7.js";
|
|
7
|
+
import {
|
|
8
|
+
loadGlobalConfig,
|
|
9
|
+
resolveGlobalRoot
|
|
10
|
+
} from "./chunk-RYAFBNES.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/sync.ts
|
|
13
|
+
import { defineCommand } from "citty";
|
|
14
|
+
|
|
15
|
+
// src/sync/run-sync.ts
|
|
16
|
+
import { execFileSync } from "child_process";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { GLOBAL_STATE_DIR, storeRelativePath } from "@fenglimg/fabric-shared";
|
|
20
|
+
|
|
21
|
+
// src/sync/state-machine.ts
|
|
22
|
+
function syncTransition(state, event) {
|
|
23
|
+
switch (state) {
|
|
24
|
+
case "pending":
|
|
25
|
+
if (event === "rebase_clean") return "synced";
|
|
26
|
+
if (event === "rebase_conflict") return "conflict";
|
|
27
|
+
if (event === "network_unavailable") return "offline";
|
|
28
|
+
break;
|
|
29
|
+
case "conflict":
|
|
30
|
+
if (event === "user_continue") return "synced";
|
|
31
|
+
if (event === "user_abort") return "aborted";
|
|
32
|
+
break;
|
|
33
|
+
case "offline":
|
|
34
|
+
if (event === "retry" || event === "rebase_clean") return "synced";
|
|
35
|
+
if (event === "rebase_conflict") return "conflict";
|
|
36
|
+
if (event === "network_unavailable") return "offline";
|
|
37
|
+
break;
|
|
38
|
+
case "synced":
|
|
39
|
+
case "aborted":
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`invalid sync transition: '${state}' --${event}-->`);
|
|
43
|
+
}
|
|
44
|
+
function planSync(stores) {
|
|
45
|
+
return { stores: stores.map((s) => ({ ...s, state: "pending" })) };
|
|
46
|
+
}
|
|
47
|
+
function applySyncEvent(session, alias, event) {
|
|
48
|
+
return {
|
|
49
|
+
stores: session.stores.map(
|
|
50
|
+
(s) => s.alias === alias ? { ...s, state: syncTransition(s.state, event) } : s
|
|
51
|
+
)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function continueSync(session) {
|
|
55
|
+
const conflicted = session.stores.find((s) => s.state === "conflict");
|
|
56
|
+
if (conflicted === void 0) {
|
|
57
|
+
throw new Error("`sync --continue` with no conflicted store to resume");
|
|
58
|
+
}
|
|
59
|
+
return applySyncEvent(session, conflicted.alias, "user_continue");
|
|
60
|
+
}
|
|
61
|
+
function abortSync(session) {
|
|
62
|
+
const conflicted = session.stores.find((s) => s.state === "conflict");
|
|
63
|
+
if (conflicted === void 0) {
|
|
64
|
+
throw new Error("`sync --abort` with no conflicted store to abort");
|
|
65
|
+
}
|
|
66
|
+
return applySyncEvent(session, conflicted.alias, "user_abort");
|
|
67
|
+
}
|
|
68
|
+
function isSyncSettled(session) {
|
|
69
|
+
return session.stores.every((s) => s.state !== "pending" && s.state !== "conflict");
|
|
70
|
+
}
|
|
71
|
+
function deferredPushStores(session) {
|
|
72
|
+
return session.stores.filter((s) => s.state === "offline");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/sync/run-sync.ts
|
|
76
|
+
var NO_GLOBAL_CONFIG = "no global Fabric config \u2014 run `fabric install --global <url>` first";
|
|
77
|
+
var NO_SESSION = "no sync in progress \u2014 run `fabric sync` first";
|
|
78
|
+
var NO_CONFLICT = "no conflicted store to resume \u2014 sync is not paused";
|
|
79
|
+
function syncSessionPath(globalRoot) {
|
|
80
|
+
return join(globalRoot, GLOBAL_STATE_DIR, "sync-session.json");
|
|
81
|
+
}
|
|
82
|
+
function loadSession(globalRoot) {
|
|
83
|
+
const path = syncSessionPath(globalRoot);
|
|
84
|
+
if (!existsSync(path)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
function saveSession(globalRoot, session) {
|
|
90
|
+
const path = syncSessionPath(globalRoot);
|
|
91
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
92
|
+
writeFileSync(path, `${JSON.stringify(session, null, 2)}
|
|
93
|
+
`, "utf8");
|
|
94
|
+
}
|
|
95
|
+
function clearSession(globalRoot) {
|
|
96
|
+
rmSync(syncSessionPath(globalRoot), { force: true });
|
|
97
|
+
}
|
|
98
|
+
function defaultPull(storeDir) {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync("git", ["pull", "--rebase"], {
|
|
101
|
+
cwd: storeDir,
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
103
|
+
});
|
|
104
|
+
return "clean";
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const detail = `${gitErrText(error, "stdout")}${gitErrText(error, "stderr")}`;
|
|
107
|
+
if (/CONFLICT|could not apply|needs merge|rebase --continue/i.test(detail)) {
|
|
108
|
+
return "conflict";
|
|
109
|
+
}
|
|
110
|
+
if (/could not resolve host|could not read from remote|unable to access|connection|network is unreachable|timed out/i.test(
|
|
111
|
+
detail
|
|
112
|
+
)) {
|
|
113
|
+
return "offline";
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function gitErrText(error, key) {
|
|
119
|
+
const value = error[key];
|
|
120
|
+
return typeof value === "string" || Buffer.isBuffer(value) ? String(value) : "";
|
|
121
|
+
}
|
|
122
|
+
function defaultRebaseContinue(storeDir) {
|
|
123
|
+
execFileSync("git", ["rebase", "--continue"], { cwd: storeDir, stdio: "ignore" });
|
|
124
|
+
}
|
|
125
|
+
function defaultRebaseAbort(storeDir) {
|
|
126
|
+
execFileSync("git", ["rebase", "--abort"], { cwd: storeDir, stdio: "ignore" });
|
|
127
|
+
}
|
|
128
|
+
var OUTCOME_EVENT = {
|
|
129
|
+
clean: "rebase_clean",
|
|
130
|
+
conflict: "rebase_conflict",
|
|
131
|
+
offline: "network_unavailable"
|
|
132
|
+
};
|
|
133
|
+
function walkPending(session, storeDirOf, pull) {
|
|
134
|
+
let next = session;
|
|
135
|
+
for (const store of session.stores) {
|
|
136
|
+
if (store.state !== "pending") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const outcome = pull(storeDirOf(store));
|
|
140
|
+
next = applySyncEvent(next, store.alias, OUTCOME_EVENT[outcome]);
|
|
141
|
+
if (outcome === "conflict") {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
function finalize(session, options, globalRoot) {
|
|
148
|
+
const settled = isSyncSettled(session);
|
|
149
|
+
let snapshotWritten = false;
|
|
150
|
+
if (settled) {
|
|
151
|
+
clearSession(globalRoot);
|
|
152
|
+
const snapshot = regenerateBindingsSnapshot(options.projectRoot, {
|
|
153
|
+
globalRoot,
|
|
154
|
+
now: options.now,
|
|
155
|
+
...options.writeScope === void 0 ? {} : { writeScope: options.writeScope }
|
|
156
|
+
});
|
|
157
|
+
snapshotWritten = snapshot !== null;
|
|
158
|
+
} else {
|
|
159
|
+
saveSession(globalRoot, session);
|
|
160
|
+
}
|
|
161
|
+
return { session, settled, deferred: deferredPushStores(session), snapshotWritten };
|
|
162
|
+
}
|
|
163
|
+
function runStartSync(options) {
|
|
164
|
+
const globalRoot = options.globalRoot ?? resolveGlobalRoot();
|
|
165
|
+
const config = loadGlobalConfig(globalRoot);
|
|
166
|
+
if (config === null) {
|
|
167
|
+
throw new Error(NO_GLOBAL_CONFIG);
|
|
168
|
+
}
|
|
169
|
+
const syncable = config.stores.filter((store) => store.remote !== void 0);
|
|
170
|
+
const session = planSync(
|
|
171
|
+
syncable.map((store) => ({ alias: store.alias, store_uuid: store.store_uuid }))
|
|
172
|
+
);
|
|
173
|
+
const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
|
|
174
|
+
const walked = walkPending(session, storeDirOf, options.pull ?? defaultPull);
|
|
175
|
+
return finalize(walked, options, globalRoot);
|
|
176
|
+
}
|
|
177
|
+
function runContinueSync(options) {
|
|
178
|
+
const globalRoot = options.globalRoot ?? resolveGlobalRoot();
|
|
179
|
+
const session = loadSession(globalRoot);
|
|
180
|
+
if (session === null) {
|
|
181
|
+
throw new Error(NO_SESSION);
|
|
182
|
+
}
|
|
183
|
+
const conflicted = session.stores.find((store) => store.state === "conflict");
|
|
184
|
+
if (conflicted === void 0) {
|
|
185
|
+
throw new Error(NO_CONFLICT);
|
|
186
|
+
}
|
|
187
|
+
const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
|
|
188
|
+
(options.rebaseContinue ?? defaultRebaseContinue)(storeDirOf(conflicted));
|
|
189
|
+
const resumed = walkPending(continueSync(session), storeDirOf, options.pull ?? defaultPull);
|
|
190
|
+
return finalize(resumed, options, globalRoot);
|
|
191
|
+
}
|
|
192
|
+
function runAbortSync(options) {
|
|
193
|
+
const globalRoot = options.globalRoot ?? resolveGlobalRoot();
|
|
194
|
+
const session = loadSession(globalRoot);
|
|
195
|
+
if (session === null) {
|
|
196
|
+
throw new Error(NO_SESSION);
|
|
197
|
+
}
|
|
198
|
+
const conflicted = session.stores.find((store) => store.state === "conflict");
|
|
199
|
+
if (conflicted === void 0) {
|
|
200
|
+
throw new Error(NO_CONFLICT);
|
|
201
|
+
}
|
|
202
|
+
const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
|
|
203
|
+
(options.rebaseAbort ?? defaultRebaseAbort)(storeDirOf(conflicted));
|
|
204
|
+
const resumed = walkPending(abortSync(session), storeDirOf, options.pull ?? defaultPull);
|
|
205
|
+
return finalize(resumed, options, globalRoot);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/commands/sync.ts
|
|
209
|
+
function report(result) {
|
|
210
|
+
for (const store of result.session.stores) {
|
|
211
|
+
console.log(`${store.alias} ${store.state}`);
|
|
212
|
+
}
|
|
213
|
+
if (result.deferred.length > 0) {
|
|
214
|
+
console.log(
|
|
215
|
+
`${result.deferred.length} store(s) offline \u2014 push deferred; re-run \`fabric sync\` when online`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!result.settled) {
|
|
219
|
+
console.log(
|
|
220
|
+
"sync paused on a conflict \u2014 resolve it, then run `fabric sync --continue` (or `--abort`)"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
var sync_default = defineCommand({
|
|
225
|
+
meta: { name: "sync", description: "Pull --rebase + push every mounted store; resume conflicts" },
|
|
226
|
+
args: {
|
|
227
|
+
continue: { type: "boolean", description: "Resume after resolving a rebase conflict" },
|
|
228
|
+
abort: { type: "boolean", description: "Abort the conflicted store's rebase" }
|
|
229
|
+
},
|
|
230
|
+
run({ args }) {
|
|
231
|
+
const options = { projectRoot: process.cwd(), now: (/* @__PURE__ */ new Date()).toISOString() };
|
|
232
|
+
if (args.continue === true) {
|
|
233
|
+
report(runContinueSync(options));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (args.abort === true) {
|
|
237
|
+
report(runAbortSync(options));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
report(runStartSync(options));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
export {
|
|
244
|
+
sync_default as default
|
|
245
|
+
};
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
7
7
|
HOOK_SCRIPT_DESTINATIONS,
|
|
8
8
|
SKILL_DESTINATIONS,
|
|
9
9
|
fabricAgentsSnapshotPath
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import {
|
|
12
|
-
detectClientSupports,
|
|
13
|
-
resolveClients
|
|
14
|
-
} from "./chunk-MF3OTILQ.js";
|
|
10
|
+
} from "./chunk-F46ORPOA.js";
|
|
15
11
|
import {
|
|
16
12
|
paint
|
|
17
13
|
} from "./chunk-WWNXR34K.js";
|
|
@@ -19,6 +15,10 @@ import {
|
|
|
19
15
|
createDebugLogger,
|
|
20
16
|
resolveDevMode
|
|
21
17
|
} from "./chunk-COI5VDFU.js";
|
|
18
|
+
import {
|
|
19
|
+
detectClientSupports,
|
|
20
|
+
resolveClients
|
|
21
|
+
} from "./chunk-MF3OTILQ.js";
|
|
22
22
|
import {
|
|
23
23
|
t
|
|
24
24
|
} from "./chunk-PWLW3B57.js";
|
|
@@ -46,6 +46,9 @@ async function uninstallFabricReviewSkill(projectRoot) {
|
|
|
46
46
|
async function uninstallFabricImportSkill(projectRoot) {
|
|
47
47
|
return removeSkill("skill-import", SKILL_DESTINATIONS.fabricImport, projectRoot);
|
|
48
48
|
}
|
|
49
|
+
async function uninstallFabricSyncSkill(projectRoot) {
|
|
50
|
+
return removeSkill("skill-sync", SKILL_DESTINATIONS.fabricSync, projectRoot);
|
|
51
|
+
}
|
|
49
52
|
async function removeSkill(step, rels, projectRoot) {
|
|
50
53
|
const results = [];
|
|
51
54
|
for (const rel of rels) {
|
|
@@ -301,6 +304,12 @@ async function uninstallBootstrapStage(projectRoot, _opts = {}) {
|
|
|
301
304
|
projectRoot,
|
|
302
305
|
() => removeArchiveHintHook(projectRoot)
|
|
303
306
|
);
|
|
307
|
+
await runAndCollect(
|
|
308
|
+
results,
|
|
309
|
+
"skill-sync",
|
|
310
|
+
projectRoot,
|
|
311
|
+
() => uninstallFabricSyncSkill(projectRoot)
|
|
312
|
+
);
|
|
304
313
|
await runAndCollect(
|
|
305
314
|
results,
|
|
306
315
|
"skill-import",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
whoami
|
|
4
|
+
} from "./chunk-T5RPGCCM.js";
|
|
5
|
+
import "./chunk-LFIKMVY7.js";
|
|
6
|
+
import "./chunk-RYAFBNES.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/whoami.ts
|
|
9
|
+
import { defineCommand } from "citty";
|
|
10
|
+
var whoami_default = defineCommand({
|
|
11
|
+
meta: { name: "whoami", description: "Show this machine's Fabric uid and mounted stores" },
|
|
12
|
+
run() {
|
|
13
|
+
const info = whoami();
|
|
14
|
+
if (info === null) {
|
|
15
|
+
console.log("no global Fabric config \u2014 run `fabric install --global <url>` first");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(`uid: ${info.uid}`);
|
|
19
|
+
if (info.stores.length === 0) {
|
|
20
|
+
console.log("stores: (none mounted)");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log("stores:");
|
|
24
|
+
for (const store of info.stores) {
|
|
25
|
+
console.log(` ${store.alias} ${store.store_uuid}${store.local_only ? " (local-only)" : ""}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export {
|
|
30
|
+
whoami_default as default
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.1.0-rc.2",
|
|
4
4
|
"description": "Fabric CLI — installs the MCP server + skills + hooks for Claude Code, Cursor, and Codex CLI; runs doctor / knowledge maintenance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wangzhichao <fenglimg90@gmail.com>",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"tree-sitter-javascript": "^0.25.0",
|
|
46
46
|
"tree-sitter-typescript": "^0.23.2",
|
|
47
47
|
"web-tree-sitter": "^0.26.8",
|
|
48
|
-
"@fenglimg/fabric-server": "2.0.
|
|
49
|
-
"@fenglimg/fabric-shared": "2.0.
|
|
48
|
+
"@fenglimg/fabric-server": "2.1.0-rc.2",
|
|
49
|
+
"@fenglimg/fabric-shared": "2.1.0-rc.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^22.15.0",
|
|
@@ -81,6 +81,27 @@ try {
|
|
|
81
81
|
stateStore = null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// v2.1.0-rc.1 P4 (F4/S63): hook-side reader for the CLI pre-generated
|
|
85
|
+
// resolved-bindings snapshot. The Stop hint surfaces the read-set stores
|
|
86
|
+
// (per-store, NOT aggregated into one pile) without re-resolving / walking
|
|
87
|
+
// store trees. Best-effort — a missing lib/snapshot omits the store line.
|
|
88
|
+
let bindingsSnapshotReader = null;
|
|
89
|
+
try {
|
|
90
|
+
bindingsSnapshotReader = require("./lib/bindings-snapshot-reader.cjs");
|
|
91
|
+
} catch {
|
|
92
|
+
bindingsSnapshotReader = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read the project's own `project_id` (the snapshot key) from its config.
|
|
96
|
+
function readProjectId(cwd) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(readFileSync(join(cwd, ".fabric", "fabric-config.json"), "utf8"));
|
|
99
|
+
return typeof parsed.project_id === "string" ? parsed.project_id : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
// CONSTANTS — duplicated from packages/server/src/services/_shared.ts.
|
|
85
106
|
// DRY violation accepted: this hook script runs in user repos WITHOUT
|
|
86
107
|
// node_modules access, so it cannot import from @fenglimg/fabric-server.
|
|
@@ -1907,6 +1928,25 @@ function main(env, stdio) {
|
|
|
1907
1928
|
result.reason = `${result.reason}\n${renderDismissOption(result.signal, variant)}`;
|
|
1908
1929
|
}
|
|
1909
1930
|
|
|
1931
|
+
// v2.1.0-rc.1 P4 (F4/S63): surface the read-set stores on the Stop hint so
|
|
1932
|
+
// backlog/maintenance nudges are read per-store, not as one undifferentiated
|
|
1933
|
+
// pile. Best-effort; missing snapshot / single-store omits the line.
|
|
1934
|
+
if (bindingsSnapshotReader !== null && typeof result.reason === "string") {
|
|
1935
|
+
try {
|
|
1936
|
+
const projectId = readProjectId(cwd);
|
|
1937
|
+
if (projectId) {
|
|
1938
|
+
const label = bindingsSnapshotReader.formatStoreLabels(
|
|
1939
|
+
bindingsSnapshotReader.readBindingsSnapshot(projectId),
|
|
1940
|
+
);
|
|
1941
|
+
if (label) {
|
|
1942
|
+
result.reason = `${result.reason}\n${label}`;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
} catch {
|
|
1946
|
+
// store label is decorative provenance — never crash the hook
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1910
1950
|
// v2.0.0-rc.7 T10: Signal D uses its own cooldown sidecar (day-based,
|
|
1911
1951
|
// see MAINTENANCE_HINT_LAST_EMIT_FILE). The A/B/C shared cooldown cache
|
|
1912
1952
|
// uses hours, so we branch here to avoid mixing semantics.
|
|
@@ -68,6 +68,28 @@ const { readTextState, writeTextState } = require("./lib/state-store.cjs");
|
|
|
68
68
|
// v2.0.0-rc.37 NEW-30: shared client detection (replaces the inline
|
|
69
69
|
// CLAUDE_PROJECT_DIR single-bit check below).
|
|
70
70
|
const { isClaudeCode } = require("./lib/client-adapter.cjs");
|
|
71
|
+
// v2.1.0-rc.1 P4 (F4/S63): hook-side reader for the CLI pre-generated
|
|
72
|
+
// resolved-bindings snapshot. The hook NEVER re-resolves stores or walks store
|
|
73
|
+
// trees — it only echoes the read-set the CLI already computed. Best-effort.
|
|
74
|
+
let bindingsSnapshotReader = null;
|
|
75
|
+
try {
|
|
76
|
+
bindingsSnapshotReader = require("./lib/bindings-snapshot-reader.cjs");
|
|
77
|
+
} catch {
|
|
78
|
+
// Lib missing (old install) — store labels degrade to silent absence.
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read the project's own `project_id` from `.fabric/fabric-config.json` (the
|
|
82
|
+
// snapshot key). Reading the PROJECT config is not a store-tree read — it is how
|
|
83
|
+
// the hook learns which snapshot to fetch. Returns null on any failure.
|
|
84
|
+
function readProjectId(cwd) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = readFileSync(join(cwd, ".fabric", "fabric-config.json"), "utf8");
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
return typeof parsed.project_id === "string" ? parsed.project_id : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
71
93
|
|
|
72
94
|
// -----------------------------------------------------------------------------
|
|
73
95
|
// rc.12: SessionStart broad-menu is now unconditionally emitted on every
|
|
@@ -735,6 +757,24 @@ function main(env, stdio) {
|
|
|
735
757
|
|
|
736
758
|
if (lines.length === 0) return; // nothing to say — silent exit
|
|
737
759
|
|
|
760
|
+
// v2.1.0-rc.1 P4 (F4/S63): append a per-store read-set label from the
|
|
761
|
+
// CLI-pre-generated bindings snapshot so the session opens aware of which
|
|
762
|
+
// stores it reads and where writes land. Best-effort, never blocks: a
|
|
763
|
+
// missing snapshot / single-store setup just omits the line.
|
|
764
|
+
if (bindingsSnapshotReader !== null) {
|
|
765
|
+
try {
|
|
766
|
+
const projectId = readProjectId(cwd);
|
|
767
|
+
if (projectId) {
|
|
768
|
+
const label = bindingsSnapshotReader.formatStoreLabels(
|
|
769
|
+
bindingsSnapshotReader.readBindingsSnapshot(projectId),
|
|
770
|
+
);
|
|
771
|
+
if (label) lines.push(label);
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
// store labels are decorative provenance — never crash the hook
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
738
778
|
// v2.0.0-rc.37 NEW-23: SessionStart 索引末尾"下一步"引导。Tail line that
|
|
739
779
|
// tells the AI what to do with the broad index it just received. Without
|
|
740
780
|
// this, the model often parses the index and moves on without ever calling
|
|
@@ -85,6 +85,26 @@ const { resolveOpaqueSummaries } = require("./lib/summary-fallback.cjs");
|
|
|
85
85
|
// cache (skips a redundant CLI cold-start spawn when the same path-set is
|
|
86
86
|
// re-edited within a session and the knowledge graph hasn't changed).
|
|
87
87
|
const { readJsonState, writeJsonState } = require("./lib/state-store.cjs");
|
|
88
|
+
// v2.1.0-rc.1 P4 (F4/S63): hook-side reader for the CLI pre-generated
|
|
89
|
+
// resolved-bindings snapshot. Store-aware hint surfaces the write-target store
|
|
90
|
+
// for the edited file WITHOUT re-resolving or walking store trees. Best-effort.
|
|
91
|
+
let bindingsSnapshotReader = null;
|
|
92
|
+
try {
|
|
93
|
+
bindingsSnapshotReader = require("./lib/bindings-snapshot-reader.cjs");
|
|
94
|
+
} catch {
|
|
95
|
+
// Lib missing (old install) — store labels degrade to silent absence.
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read the project's own `project_id` (the snapshot key) from its config. Not a
|
|
99
|
+
// store-tree read — it is how the hook learns which snapshot to fetch.
|
|
100
|
+
function readProjectId(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(join(cwd, ".fabric", "fabric-config.json"), "utf8"));
|
|
103
|
+
return typeof parsed.project_id === "string" ? parsed.project_id : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
88
108
|
|
|
89
109
|
// -----------------------------------------------------------------------------
|
|
90
110
|
// CONSTANTS
|
|
@@ -1466,6 +1486,25 @@ function main(env, stdio) {
|
|
|
1466
1486
|
const lines = renderSummary({ ...cliPayload, entries: resolvedEntries }, summaryMaxLen);
|
|
1467
1487
|
if (lines.length === 0) return;
|
|
1468
1488
|
|
|
1489
|
+
// v2.1.0-rc.1 P4 (F4/S63): store-aware hint — append the write-target store
|
|
1490
|
+
// so the edit-time hint says WHERE a derived knowledge entry would land.
|
|
1491
|
+
// Best-effort; missing snapshot / single-store setup omits the line.
|
|
1492
|
+
if (bindingsSnapshotReader !== null) {
|
|
1493
|
+
try {
|
|
1494
|
+
const projectId = readProjectId(cwd);
|
|
1495
|
+
if (projectId) {
|
|
1496
|
+
const snapshot = bindingsSnapshotReader.readBindingsSnapshot(projectId);
|
|
1497
|
+
const writeAlias =
|
|
1498
|
+
snapshot && snapshot.write_target && snapshot.write_target.alias;
|
|
1499
|
+
if (writeAlias) {
|
|
1500
|
+
lines.push(`[fabric] writes here land in store '${writeAlias}'`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
// store label is decorative provenance — never crash the hook
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1469
1508
|
// Stderr: human-facing breadcrumb + legacy contract.
|
|
1470
1509
|
for (const line of lines) {
|
|
1471
1510
|
err.write(`${line}\n`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// v2.1.0-rc.1 P4 — hook-side resolved-bindings snapshot reader (F4/S63/S65).
|
|
3
|
+
//
|
|
4
|
+
// Hooks are a REMINDER layer (KT-DEC-0007) and must never block. They are also
|
|
5
|
+
// FORBIDDEN from re-resolving stores or walking `.fabric` store trees directly
|
|
6
|
+
// — a hook reads ONLY the CLI-pre-generated snapshot at
|
|
7
|
+
// `~/.fabric/state/bindings/<project_id>_resolved.json` (written by P3
|
|
8
|
+
// install/sync/bind). This keeps the resolver logic in one place (the CLI) and
|
|
9
|
+
// keeps hooks a thin, store-unaware-by-construction projection. Missing /
|
|
10
|
+
// unreadable / malformed snapshot → null (harmless degrade; the hook proceeds
|
|
11
|
+
// without store labels). Zero-dep CJS so it inline-loads at hook runtime.
|
|
12
|
+
|
|
13
|
+
const { existsSync, readFileSync } = require("node:fs");
|
|
14
|
+
const { join } = require("node:path");
|
|
15
|
+
const { homedir } = require("node:os");
|
|
16
|
+
|
|
17
|
+
// `~/.fabric` (FABRIC_HOME override mirrors the CLI's resolveGlobalRoot).
|
|
18
|
+
function resolveGlobalRoot() {
|
|
19
|
+
return join(process.env.FABRIC_HOME || homedir(), ".fabric");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bindingsSnapshotPath(projectId, globalRoot) {
|
|
23
|
+
return join(
|
|
24
|
+
globalRoot || resolveGlobalRoot(),
|
|
25
|
+
"state",
|
|
26
|
+
"bindings",
|
|
27
|
+
projectId + "_resolved.json",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Read + shallow-validate the snapshot. Returns the parsed object, or null when
|
|
32
|
+
// absent / unreadable / not the expected shape. NEVER throws.
|
|
33
|
+
function readBindingsSnapshot(projectId, globalRoot) {
|
|
34
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const path = bindingsSnapshotPath(projectId, globalRoot);
|
|
38
|
+
if (!existsSync(path)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
43
|
+
if (
|
|
44
|
+
parsed &&
|
|
45
|
+
typeof parsed === "object" &&
|
|
46
|
+
parsed.read_set &&
|
|
47
|
+
Array.isArray(parsed.read_set.stores)
|
|
48
|
+
) {
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Render a compact, per-store label line for a SessionStart / Stop hook from a
|
|
58
|
+
// snapshot. Empty string when there is nothing to show (degrade silently). The
|
|
59
|
+
// label is provenance only — it never re-resolves; it just echoes the read-set
|
|
60
|
+
// the CLI already computed, with the write-target flagged (F4 store labels).
|
|
61
|
+
function formatStoreLabels(snapshot) {
|
|
62
|
+
if (!snapshot || !snapshot.read_set || !Array.isArray(snapshot.read_set.stores)) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
const writeAlias = snapshot.write_target && snapshot.write_target.alias;
|
|
66
|
+
const parts = snapshot.read_set.stores.map((store) => {
|
|
67
|
+
const tag = store.alias === writeAlias ? " (write)" : store.writable ? "" : " (ro)";
|
|
68
|
+
return store.alias + tag;
|
|
69
|
+
});
|
|
70
|
+
if (parts.length === 0) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
return "[fabric] read-set stores: " + parts.join(", ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
resolveGlobalRoot,
|
|
78
|
+
bindingsSnapshotPath,
|
|
79
|
+
readBindingsSnapshot,
|
|
80
|
+
formatStoreLabels,
|
|
81
|
+
};
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// Reads `.fabric/agents.meta.json` to build a stable_id → knowledge_type lookup
|
|
4
4
|
// map, then scans summarised assistant turns (cite_ids + cite_tags +
|
|
5
5
|
// cite_commitments parallel arrays produced by lib/cite-line-parser.cjs) for
|
|
6
|
-
// turns that cited a decision-class or pitfall-class id with [
|
|
7
|
-
// but no operator commitment and no skip:<reason>.
|
|
6
|
+
// turns that cited a decision-class or pitfall-class id with [applied] tag
|
|
7
|
+
// but no operator commitment and no skip:<reason>. (v2.1.0-rc.1 ADJ-P4-1:
|
|
8
|
+
// legacy [recalled] is remapped to [applied] by the parser upstream.)
|
|
8
9
|
//
|
|
9
10
|
// Emits one reminder line per offending id (deduplicated across the turn
|
|
10
11
|
// summary). Non-blocking — caller writes the lines to stderr; failure to
|
|
11
12
|
// load the meta file or absence of offenders means zero output.
|
|
12
13
|
//
|
|
13
14
|
// Reminder template (rc.24 lock B2 / L1 enforcement layer):
|
|
14
|
-
// ⚠ KB: <id> cited as [
|
|
15
|
+
// ⚠ KB: <id> cited as [applied] but missing contract; add → edit:<glob>
|
|
15
16
|
// or → skip:<reason> next turn
|
|
16
17
|
//
|
|
17
18
|
// Type filter rationale: only `decision` and `pitfall` types are contract-
|
|
@@ -34,7 +35,7 @@ const { join } = require("node:path");
|
|
|
34
35
|
const FABRIC_DIR = ".fabric";
|
|
35
36
|
const AGENTS_META_FILE = "agents.meta.json";
|
|
36
37
|
|
|
37
|
-
// Knowledge types that require contract commitments on [
|
|
38
|
+
// Knowledge types that require contract commitments on [applied] cites.
|
|
38
39
|
// Matches the singular form persisted by `withDerivedAgentsMetaNodeDefaults`
|
|
39
40
|
// in packages/shared/src/schemas/agents-meta.ts. We accept both singular
|
|
40
41
|
// and plural defensively so a future schema change to plurals doesn't
|
|
@@ -98,13 +99,17 @@ function readKnowledgeTypeMap(projectRoot) {
|
|
|
98
99
|
* don't, returning the reminder lines to emit.
|
|
99
100
|
*
|
|
100
101
|
* Filter (all must hold for a given index i within a turn):
|
|
101
|
-
* 1. cite_tags includes "
|
|
102
|
+
* 1. cite_tags includes "applied" (turn-level — applies to the cited id)
|
|
102
103
|
* 2. cite_commitments[i].operators is empty AND cite_commitments[i].skip_reason is null
|
|
103
104
|
* 3. idTypeMap.get(cite_ids[i]) is in {decision, pitfall}
|
|
104
105
|
*
|
|
106
|
+
* v2.1.0-rc.1 (ADJ-P4-1, full remap): the gate is the rc.37 NEW-1 `applied`
|
|
107
|
+
* tag. Legacy [recalled] cites are remapped to [applied] by the parser before
|
|
108
|
+
* they reach here, so gating on "applied" covers both old and new authoring.
|
|
109
|
+
*
|
|
105
110
|
* Tag-level filter clarification: rc.20 cite_tags is parallel to ALL parsed
|
|
106
111
|
* lines (including sentinels), but for the contract-missing reminder we use
|
|
107
|
-
* the turn-level semantic — if the assistant tagged the cite as [
|
|
112
|
+
* the turn-level semantic — if the assistant tagged the cite as [applied],
|
|
108
113
|
* the operator-or-skip contract applies. Per TASK-04 invariant, cite_ids and
|
|
109
114
|
* cite_commitments are parallel index-aligned arrays (length-N each).
|
|
110
115
|
*
|
|
@@ -131,8 +136,9 @@ function formatContractMissingReminders({ assistant_turns, idTypeMap }) {
|
|
|
131
136
|
const citeTags = Array.isArray(turn.cite_tags) ? turn.cite_tags : [];
|
|
132
137
|
const commitments = Array.isArray(turn.cite_commitments) ? turn.cite_commitments : [];
|
|
133
138
|
|
|
134
|
-
// Turn-level: the [
|
|
135
|
-
|
|
139
|
+
// Turn-level: the [applied] tag must appear in the turn's tag set
|
|
140
|
+
// (v2.1.0-rc.1 ADJ-P4-1: legacy [recalled] is remapped to [applied]).
|
|
141
|
+
if (!citeTags.includes("applied")) continue;
|
|
136
142
|
|
|
137
143
|
// Iterate by cite_ids.length — sentinel entries don't have ids so they
|
|
138
144
|
// contribute zero iterations even if cite_tags carries "none".
|
|
@@ -160,7 +166,7 @@ function formatContractMissingReminders({ assistant_turns, idTypeMap }) {
|
|
|
160
166
|
const reminders = [];
|
|
161
167
|
for (const id of offenders) {
|
|
162
168
|
reminders.push(
|
|
163
|
-
`⚠ KB: ${id} cited as [
|
|
169
|
+
`⚠ KB: ${id} cited as [applied] but missing contract; add \`→ edit:<glob>\` or \`→ skip:<reason>\` next turn`,
|
|
164
170
|
);
|
|
165
171
|
}
|
|
166
172
|
return reminders;
|