@fenglimg/fabric-cli 2.2.0-rc.3 → 2.2.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +8 -5
  2. package/dist/{chunk-5LQIHYFC.js → chunk-27HK6H5Y.js} +10 -5
  3. package/dist/{chunk-F6ITRM7T.js → chunk-2KBCTMID.js} +29 -6
  4. package/dist/{chunk-XC5RUHLK.js → chunk-3IOLS5EK.js} +23 -38
  5. package/dist/{chunk-XHHCRDIR.js → chunk-CMDW3PYK.js} +105 -220
  6. package/dist/chunk-FEOPLBGA.js +150 -0
  7. package/dist/{chunk-XCBVSGCS.js → chunk-FNHDQTPC.js} +1 -10
  8. package/dist/{chunk-2CY4BMTH.js → chunk-HORSMSZL.js} +9 -5
  9. package/dist/{doctor-J4O3X54I.js → chunk-JTHWLUD3.js} +103 -51
  10. package/dist/{chunk-BO4XIZWZ.js → chunk-NLNH64A3.js} +5 -18
  11. package/dist/{chunk-H3FE6VIK.js → chunk-PTGQAZEW.js} +13 -3
  12. package/dist/chunk-QFIVFZRH.js +13 -0
  13. package/dist/chunk-QPAW6IYT.js +387 -0
  14. package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
  15. package/dist/{plan-context-hint-CHVZGOZ5.js → chunk-YM4XATJF.js} +29 -4
  16. package/dist/{config-VJMXCLXW.js → config-A3LTECAY.js} +4 -3
  17. package/dist/context-7NUKXDB6.js +117 -0
  18. package/dist/doctor-REZDNH4A.js +24 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +131 -21
  21. package/dist/info-7FKBTMVO.js +139 -0
  22. package/dist/install-v2-2COC3DO3.js +3277 -0
  23. package/dist/{metrics-RER6NLFC.js → metrics-HMFH4YHK.js} +1 -1
  24. package/dist/{onboard-coverage-JWQWDZW7.js → onboard-coverage-XSG77LL3.js} +48 -27
  25. package/dist/plan-context-hint-G75R4P4J.js +12 -0
  26. package/dist/{scope-explain-BWRWBCCP.js → scope-explain-HLJZ2M33.js} +3 -2
  27. package/dist/{status-PANEGKU2.js → status-4R3TM4FJ.js} +8 -5
  28. package/dist/store-HOCORVL3.js +563 -0
  29. package/dist/{sync-EA5HZMXM.js → sync-DT5UJMMR.js} +36 -13
  30. package/dist/{uninstall-F75MPKQC.js → uninstall-62F4LNKI.js} +62 -140
  31. package/dist/{whoami-66YKY5DZ.js → whoami-ITGEFWH4.js} +9 -7
  32. package/package.json +7 -5
  33. package/templates/hooks/cite-policy-evict.cjs +5 -5
  34. package/templates/hooks/configs/README.md +14 -27
  35. package/templates/hooks/configs/claude-code.json +1 -1
  36. package/templates/hooks/configs/codex-hooks.json +3 -3
  37. package/templates/hooks/fabric-hint.cjs +301 -161
  38. package/templates/hooks/knowledge-hint-broad.cjs +426 -207
  39. package/templates/hooks/knowledge-hint-narrow.cjs +56 -56
  40. package/templates/hooks/lib/banner-i18n.cjs +31 -0
  41. package/templates/hooks/lib/bindings-snapshot-reader.cjs +117 -7
  42. package/templates/hooks/lib/cite-line-parser.cjs +12 -20
  43. package/templates/hooks/lib/client-adapter.cjs +66 -7
  44. package/templates/hooks/lib/nudge-policy.cjs +117 -0
  45. package/templates/hooks/lib/state-store.cjs +60 -0
  46. package/templates/hooks/lib/summary-fallback.cjs +82 -19
  47. package/templates/hooks/post-tooluse-mutation.cjs +112 -11
  48. package/templates/skills/fabric/SKILL.md +94 -0
  49. package/templates/skills/fabric-archive/SKILL.md +29 -26
  50. package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
  51. package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
  52. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
  53. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
  54. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
  55. package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
  56. package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
  57. package/templates/skills/fabric-audit/SKILL.md +13 -3
  58. package/templates/skills/fabric-connect/SKILL.md +3 -3
  59. package/templates/skills/fabric-import/SKILL.md +7 -7
  60. package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
  61. package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
  62. package/templates/skills/fabric-review/SKILL.md +5 -5
  63. package/templates/skills/fabric-review/ref/cite-contract.md +1 -1
  64. package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
  65. package/templates/skills/fabric-review/ref/output-contract.md +1 -1
  66. package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
  67. package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
  68. package/templates/skills/fabric-store/SKILL.md +1 -1
  69. package/templates/skills/fabric-sync/SKILL.md +1 -1
  70. package/templates/skills/lib/shared-policy.md +2 -2
  71. package/dist/chunk-5ZUMLCD5.js +0 -248
  72. package/dist/install-BULNDUIM.js +0 -2816
  73. package/dist/store-66NK2FTQ.js +0 -443
  74. package/templates/hooks/configs/cursor-hooks.json +0 -30
  75. package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
@@ -1,443 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- regenerateBindingsSnapshot
4
- } from "./chunk-H3FE6VIK.js";
5
- import "./chunk-EOT63RDH.js";
6
- import {
7
- getProjectTranslator
8
- } from "./chunk-2CY4BMTH.js";
9
- import {
10
- assertStoreMountable,
11
- storeAdd,
12
- storeBind,
13
- storeCreate,
14
- storeExplain,
15
- storeGitRemote,
16
- storeList,
17
- storeRemove,
18
- storeSwitchWrite
19
- } from "./chunk-5ZUMLCD5.js";
20
- import {
21
- loadGlobalConfig
22
- } from "./chunk-XCBVSGCS.js";
23
-
24
- // src/commands/store.ts
25
- import { defineCommand } from "citty";
26
-
27
- // src/store/store-migrate.ts
28
- import { execFileSync } from "child_process";
29
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
30
- import { basename, join } from "path";
31
- import {
32
- STORE_KNOWLEDGE_TYPE_DIRS,
33
- STORE_LAYOUT,
34
- STORE_PENDING_DIR,
35
- buildStoreResolveInput,
36
- createStoreResolver,
37
- formatKnowledgeId,
38
- parseKnowledgeId,
39
- resolveGlobalRoot,
40
- storeRelativePath
41
- } from "@fenglimg/fabric-shared";
42
- function resolveTargetStore(layer, projectRoot, globalRoot) {
43
- const input = buildStoreResolveInput(projectRoot, globalRoot);
44
- if (input === null) {
45
- return null;
46
- }
47
- const scope = layer === "personal" ? "personal" : "team";
48
- const { target } = createStoreResolver().resolveWriteTarget(input, scope);
49
- if (target === null) {
50
- return null;
51
- }
52
- const alias = loadGlobalConfig(globalRoot)?.stores.find((s) => s.store_uuid === target.store_uuid)?.alias ?? target.store_uuid;
53
- return {
54
- uuid: target.store_uuid,
55
- alias,
56
- dir: join(globalRoot, storeRelativePath(target.store_uuid))
57
- };
58
- }
59
- function listMd(dir) {
60
- if (!existsSync(dir)) {
61
- return [];
62
- }
63
- return readdirSync(dir).filter((name) => name.endsWith(".md")).sort();
64
- }
65
- function readId(content) {
66
- const match = content.match(/^id:\s*(\S+)\s*$/mu);
67
- return match ? match[1] : null;
68
- }
69
- function slugSuffix(fileName, oldId) {
70
- const stem = fileName.replace(/\.md$/u, "");
71
- if (oldId !== null && stem.startsWith(`${oldId}--`)) {
72
- return stem.slice(oldId.length);
73
- }
74
- return "";
75
- }
76
- function buildStoreIdIndex(storeDir) {
77
- const existing = /* @__PURE__ */ new Set();
78
- const maxCounter = /* @__PURE__ */ new Map();
79
- for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
80
- const dir = join(storeDir, STORE_LAYOUT.knowledgeDir, type);
81
- for (const file of listMd(dir)) {
82
- const content = readFileSync(join(dir, file), "utf8");
83
- const id = readId(content) ?? file.replace(/\.md$/u, "").split("--")[0];
84
- const parsed = parseKnowledgeId(id);
85
- if (parsed === null) {
86
- continue;
87
- }
88
- existing.add(id);
89
- const key = id.slice(0, id.lastIndexOf("-"));
90
- maxCounter.set(key, Math.max(maxCounter.get(key) ?? 0, parsed.counter));
91
- }
92
- }
93
- return { existing, maxCounter };
94
- }
95
- function nextId(index, layer, type) {
96
- const probe = formatKnowledgeId(layer, type, 1);
97
- const key = probe.slice(0, probe.lastIndexOf("-"));
98
- let counter = (index.maxCounter.get(key) ?? 0) + 1;
99
- let id = formatKnowledgeId(layer, type, counter);
100
- while (index.existing.has(id)) {
101
- counter += 1;
102
- id = formatKnowledgeId(layer, type, counter);
103
- }
104
- index.existing.add(id);
105
- index.maxCounter.set(key, counter);
106
- return id;
107
- }
108
- function typeDirToKnowledgeType(typeDir) {
109
- return STORE_KNOWLEDGE_TYPE_DIRS.includes(typeDir) ? typeDir : null;
110
- }
111
- function migrateProjectKnowledge(projectRoot, options = {}) {
112
- const dryRun = options.dryRun ?? false;
113
- const globalRoot = options.globalRoot ?? resolveGlobalRoot();
114
- const runGit = options.git ?? true;
115
- const items = [];
116
- const skips = [];
117
- const remap = {};
118
- const targets = {};
119
- const sourceRoots = {
120
- team: join(projectRoot, ".fabric", "knowledge"),
121
- personal: join(globalRoot, "knowledge")
122
- };
123
- const layerState = {};
124
- for (const layer of ["team", "personal"]) {
125
- if (!existsSync(sourceRoots[layer])) {
126
- continue;
127
- }
128
- const target = resolveTargetStore(layer, projectRoot, globalRoot);
129
- if (target === null) {
130
- continue;
131
- }
132
- targets[layer] = { uuid: target.uuid, dir: target.dir };
133
- layerState[layer] = { target, index: buildStoreIdIndex(target.dir) };
134
- }
135
- for (const layer of ["team", "personal"]) {
136
- const root = sourceRoots[layer];
137
- if (!existsSync(root)) {
138
- continue;
139
- }
140
- const state = layerState[layer];
141
- for (const typeDir of STORE_KNOWLEDGE_TYPE_DIRS) {
142
- const dir = join(root, typeDir);
143
- for (const file of listMd(dir)) {
144
- const source = join(dir, file);
145
- if (state === void 0) {
146
- skips.push({
147
- source,
148
- reason: `no ${layer} write-target store \u2014 run \`fabric install --global\` then \`fabric store bind <alias>\`${layer === "team" ? " + `fabric store switch-write <alias>`" : ""}`
149
- });
150
- continue;
151
- }
152
- const content = readFileSync(source, "utf8");
153
- const oldId = readId(content);
154
- const knowledgeType = typeDirToKnowledgeType(typeDir);
155
- let newId = null;
156
- if (oldId !== null && state.index.existing.has(oldId) && knowledgeType !== null) {
157
- const parsed = parseKnowledgeId(oldId);
158
- const idLayer = parsed?.layer ?? layer;
159
- newId = nextId(state.index, idLayer, knowledgeType);
160
- remap[oldId] = newId;
161
- } else if (oldId !== null) {
162
- state.index.existing.add(oldId);
163
- const parsed = parseKnowledgeId(oldId);
164
- if (parsed !== null) {
165
- const key = oldId.slice(0, oldId.lastIndexOf("-"));
166
- state.index.maxCounter.set(
167
- key,
168
- Math.max(state.index.maxCounter.get(key) ?? 0, parsed.counter)
169
- );
170
- }
171
- }
172
- const effectiveId = newId ?? oldId;
173
- const targetName = newId !== null && effectiveId !== null ? `${effectiveId}${slugSuffix(file, oldId)}.md` : file;
174
- const targetFile = join(state.target.dir, STORE_LAYOUT.knowledgeDir, typeDir, targetName);
175
- items.push({
176
- source,
177
- layer,
178
- type: typeDir,
179
- oldId,
180
- newId,
181
- target: targetFile,
182
- storeUuid: state.target.uuid,
183
- alias: state.target.alias
184
- });
185
- }
186
- }
187
- const pendingRoot = join(root, STORE_PENDING_DIR);
188
- for (const sub of [".", "decisions", "guidelines", "pitfalls", "models", "processes"]) {
189
- const dir = sub === "." ? pendingRoot : join(pendingRoot, sub);
190
- for (const file of listMd(dir)) {
191
- const source = join(dir, file);
192
- if (state === void 0) {
193
- skips.push({ source, reason: `no ${layer} write-target store` });
194
- continue;
195
- }
196
- const rel = sub === "." ? file : join(sub, file);
197
- const targetFile = join(
198
- state.target.dir,
199
- STORE_LAYOUT.knowledgeDir,
200
- STORE_PENDING_DIR,
201
- rel
202
- );
203
- if (existsSync(targetFile)) {
204
- skips.push({ source, reason: `pending already present in store: ${basename(targetFile)}` });
205
- continue;
206
- }
207
- items.push({
208
- source,
209
- layer,
210
- type: STORE_PENDING_DIR,
211
- oldId: null,
212
- newId: null,
213
- target: targetFile,
214
- storeUuid: state.target.uuid,
215
- alias: state.target.alias
216
- });
217
- }
218
- }
219
- }
220
- if (dryRun || items.length === 0) {
221
- return { dryRun, committed: false, items, skips, remap, targets };
222
- }
223
- for (const item of items) {
224
- let content = readFileSync(item.source, "utf8");
225
- if (item.newId !== null && item.oldId !== null) {
226
- content = content.replace(/^id:\s*\S+\s*$/mu, `id: ${item.newId}`);
227
- }
228
- content = rewriteRelated(content, remap);
229
- mkdirSync(join(item.target, ".."), { recursive: true });
230
- writeFileSync(item.target, content, "utf8");
231
- }
232
- for (const item of items) {
233
- rmSync(item.source, { force: true });
234
- }
235
- let committed = false;
236
- if (runGit) {
237
- for (const [layer, info] of Object.entries(targets)) {
238
- const moved = items.filter((i) => i.layer === layer).length;
239
- if (moved === 0) {
240
- continue;
241
- }
242
- committed = gitCommitStore(info.dir, moved) || committed;
243
- }
244
- }
245
- return { dryRun, committed, items, skips, remap, targets };
246
- }
247
- function rewriteRelated(content, remap) {
248
- if (Object.keys(remap).length === 0) {
249
- return content;
250
- }
251
- return content.replace(/^related:\s*\[(.*)\]\s*$/mu, (line, inner) => {
252
- const rewritten = inner.split(",").map((token) => {
253
- const trimmed = token.trim();
254
- return remap[trimmed] ?? trimmed;
255
- }).join(", ");
256
- return `related: [${rewritten}]`;
257
- });
258
- }
259
- function gitCommitStore(storeDir, count) {
260
- if (!existsSync(join(storeDir, ".git"))) {
261
- return false;
262
- }
263
- try {
264
- execFileSync("git", ["add", "-A"], { cwd: storeDir, stdio: ["ignore", "ignore", "pipe"] });
265
- execFileSync(
266
- "git",
267
- ["commit", "-m", `chore(migrate): import ${count} entries from project dual-root`],
268
- { cwd: storeDir, stdio: ["ignore", "ignore", "pipe"] }
269
- );
270
- return true;
271
- } catch {
272
- return false;
273
- }
274
- }
275
-
276
- // src/commands/store.ts
277
- var listCommand = defineCommand({
278
- meta: { name: "list", description: "List mounted knowledge stores" },
279
- run() {
280
- const t = getProjectTranslator();
281
- const stores = storeList();
282
- if (stores.length === 0) {
283
- console.log(t("cli.store.none-mounted"));
284
- return;
285
- }
286
- const localOnly = t("cli.shared.local-only");
287
- for (const store of stores) {
288
- const realRemote = storeGitRemote(store.store_uuid);
289
- console.log(`${store.alias} ${store.store_uuid} ${realRemote ?? localOnly}`);
290
- }
291
- }
292
- });
293
- var addCommand = defineCommand({
294
- meta: { name: "add", description: "Mount a knowledge store into the global registry" },
295
- args: {
296
- uuid: { type: "string", required: true, description: "Intrinsic store UUID" },
297
- alias: { type: "string", required: true, description: "Local alias for this store" },
298
- remote: { type: "string", description: "Git remote locator (omit for local-only)" }
299
- },
300
- run({ args }) {
301
- assertStoreMountable(args.uuid);
302
- const store = args.remote === void 0 ? { store_uuid: args.uuid, alias: args.alias } : { store_uuid: args.uuid, alias: args.alias, remote: args.remote };
303
- const next = storeAdd(store);
304
- const t = getProjectTranslator();
305
- console.log(
306
- t("cli.store.mounted", {
307
- alias: args.alias,
308
- count: String(next.stores.length)
309
- })
310
- );
311
- }
312
- });
313
- var createCommand = defineCommand({
314
- meta: { name: "create", description: "Create a brand-new local knowledge store and mount it" },
315
- args: {
316
- alias: { type: "string", required: true, description: "Local alias for the new store" },
317
- remote: { type: "string", description: "Git remote to associate (push target; optional)" }
318
- },
319
- run({ args }) {
320
- const result = storeCreate(args.alias, (/* @__PURE__ */ new Date()).toISOString(), {
321
- ...args.remote === void 0 ? {} : { remote: args.remote }
322
- });
323
- const t = getProjectTranslator();
324
- console.log(
325
- t("cli.store.created", { alias: args.alias, uuid: result.store_uuid, dir: result.storeDir }) + (args.remote === void 0 ? `
326
- ${t("cli.store.created-local-hint")}` : "")
327
- );
328
- }
329
- });
330
- var removeCommand = defineCommand({
331
- meta: { name: "remove", description: "Detach a store from the registry (does NOT delete it)" },
332
- args: {
333
- alias: { type: "positional", required: true, description: "Alias to detach" }
334
- },
335
- run({ args }) {
336
- const { detached } = storeRemove(args.alias);
337
- const t = getProjectTranslator();
338
- console.log(
339
- detached === null ? t("cli.store.no-alias", { alias: args.alias }) : t("cli.store.detached", { alias: args.alias })
340
- );
341
- }
342
- });
343
- var explainCommand = defineCommand({
344
- meta: { name: "explain", description: "Explain how a store alias resolves" },
345
- args: {
346
- alias: { type: "positional", required: true, description: "Alias to explain" }
347
- },
348
- run({ args }) {
349
- const explanation = storeExplain(args.alias);
350
- console.log(
351
- explanation === null ? getProjectTranslator()("cli.store.no-alias", { alias: args.alias }) : JSON.stringify(explanation, null, 2)
352
- );
353
- }
354
- });
355
- var bindCommand = defineCommand({
356
- meta: { name: "bind", description: "Declare a required store on this project's config" },
357
- args: {
358
- id: { type: "positional", required: true, description: "Store alias/UUID to require" },
359
- remote: { type: "string", description: "Suggested remote for clone onboarding" }
360
- },
361
- run({ args }) {
362
- const entry = args.remote === void 0 ? { id: args.id } : { id: args.id, suggested_remote: args.remote };
363
- const projectRoot = process.cwd();
364
- const next = storeBind(projectRoot, entry);
365
- console.log(
366
- getProjectTranslator(projectRoot)("cli.store.bound", {
367
- id: args.id,
368
- count: String(next.required_stores?.length ?? 0)
369
- })
370
- );
371
- regenerateBindingsSnapshot(projectRoot, { now: (/* @__PURE__ */ new Date()).toISOString() });
372
- }
373
- });
374
- var switchWriteCommand = defineCommand({
375
- meta: { name: "switch-write", description: "Set the active write store for non-personal scopes" },
376
- args: {
377
- alias: { type: "positional", required: true, description: "Alias of the store to write to" }
378
- },
379
- run({ args }) {
380
- const projectRoot = process.cwd();
381
- storeSwitchWrite(projectRoot, args.alias);
382
- console.log(getProjectTranslator(projectRoot)("cli.store.switch-write", { alias: args.alias }));
383
- }
384
- });
385
- var migrateCommand = defineCommand({
386
- meta: {
387
- name: "migrate",
388
- description: "Move project-local (dual-root) knowledge into the resolved write-target stores"
389
- },
390
- args: {
391
- "dry-run": {
392
- type: "boolean",
393
- description: "Preview the move without writing anything"
394
- }
395
- },
396
- run({ args }) {
397
- const projectRoot = process.cwd();
398
- const t = getProjectTranslator(projectRoot);
399
- const dryRun = args["dry-run"] === true;
400
- const report = migrateProjectKnowledge(projectRoot, { dryRun });
401
- if (report.items.length === 0 && report.skips.length === 0) {
402
- console.log(t("cli.store.migrate.none"));
403
- return;
404
- }
405
- console.log(
406
- dryRun ? t("cli.store.migrate.dry-run-header") : t("cli.store.migrate.applied-header", { count: String(report.items.length) })
407
- );
408
- for (const item of report.items) {
409
- const id = item.newId ?? item.oldId ?? "(draft)";
410
- console.log(` ${item.layer}/${item.type} ${id} \u2192 ${item.alias}`);
411
- if (item.newId !== null && item.oldId !== null) {
412
- console.log(
413
- t("cli.store.migrate.remap-note", { oldId: item.oldId, newId: item.newId })
414
- );
415
- }
416
- }
417
- if (report.skips.length > 0) {
418
- console.log(t("cli.store.migrate.skips-header", { count: String(report.skips.length) }));
419
- for (const skip of report.skips) {
420
- console.log(` ${skip.source}: ${skip.reason}`);
421
- }
422
- }
423
- if (report.committed) {
424
- console.log(t("cli.store.migrate.committed"));
425
- }
426
- }
427
- });
428
- var store_default = defineCommand({
429
- meta: { name: "store", description: "Manage mounted Fabric knowledge stores" },
430
- subCommands: {
431
- list: listCommand,
432
- create: createCommand,
433
- add: addCommand,
434
- remove: removeCommand,
435
- explain: explainCommand,
436
- bind: bindCommand,
437
- "switch-write": switchWriteCommand,
438
- migrate: migrateCommand
439
- }
440
- });
441
- export {
442
- store_default as default
443
- };
@@ -1,30 +0,0 @@
1
- {
2
- "version": 1,
3
- "hooks": {
4
- "stop": [
5
- { "command": ".cursor/hooks/fabric-hint.cjs" }
6
- ],
7
- "sessionStart": [
8
- { "command": ".cursor/hooks/knowledge-hint-broad.cjs" }
9
- ],
10
- "preToolUse": [
11
- {
12
- "matcher": "Edit|Write|MultiEdit",
13
- "command": ".cursor/hooks/knowledge-hint-narrow.cjs"
14
- },
15
- {
16
- "matcher": "Edit|Write|MultiEdit",
17
- "command": ".cursor/hooks/cite-policy-evict.cjs"
18
- }
19
- ],
20
- "postToolUse": [
21
- {
22
- "matcher": "Edit|Write|MultiEdit",
23
- "command": ".cursor/hooks/post-tooluse-mutation.cjs"
24
- }
25
- ],
26
- "sessionEnd": [
27
- { "command": ".cursor/hooks/session-end-marker.cjs" }
28
- ]
29
- }
30
- }
@@ -1,179 +0,0 @@
1
- // v2.0.0-rc.24 TASK-05: L1 Stop hook soft reminder for missing cite contract.
2
- //
3
- // Reads `.fabric/agents.meta.json` to build a stable_id → knowledge_type lookup
4
- // map, then scans summarised assistant turns (cite_ids + cite_tags +
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 [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.)
9
- //
10
- // Emits one reminder line per offending id (deduplicated across the turn
11
- // summary). Non-blocking — caller writes the lines to stderr; failure to
12
- // load the meta file or absence of offenders means zero output.
13
- //
14
- // Reminder template (rc.24 lock B2 / L1 enforcement layer):
15
- // ⚠ KB: <id> cited as [applied] but missing contract; add → edit:<glob>
16
- // or → skip:<reason> next turn
17
- //
18
- // Type filter rationale: only `decision` and `pitfall` types are contract-
19
- // required per rc.24 design lock B6 (idTypeMap routing). `model`,
20
- // `guideline`, `process` use reference-cite or LLM-judge (deferred to rc.25+)
21
- // and are intentionally skipped here to avoid false-positive nudges.
22
- //
23
- // agents.meta.json schema note: `description.knowledge_type` values are
24
- // SINGULAR (`decision`, `pitfall`, `model`, `guideline`, `process`) per
25
- // packages/shared/src/schemas/agents-meta.ts. The reminder filter normalises
26
- // any plural input defensively but the canonical contract is singular.
27
- //
28
- // Reading happens once per hook invocation (caller passes the projectRoot;
29
- // the lib does the fs read internally). The map is small (<200 entries in
30
- // typical corpora) so caching beyond the per-invocation scope is unnecessary.
31
-
32
- const { existsSync, readFileSync } = require("node:fs");
33
- const { join } = require("node:path");
34
-
35
- const FABRIC_DIR = ".fabric";
36
- const AGENTS_META_FILE = "agents.meta.json";
37
-
38
- // Knowledge types that require contract commitments on [applied] cites.
39
- // Matches the singular form persisted by `withDerivedAgentsMetaNodeDefaults`
40
- // in packages/shared/src/schemas/agents-meta.ts. We accept both singular
41
- // and plural defensively so a future schema change to plurals doesn't
42
- // silently break the filter.
43
- const CONTRACT_REQUIRED_TYPES = new Set([
44
- "decision",
45
- "decisions",
46
- "pitfall",
47
- "pitfalls",
48
- ]);
49
-
50
- /**
51
- * Build a Map<stable_id, knowledge_type> from <projectRoot>/.fabric/agents.meta.json.
52
- *
53
- * Never throws — missing file, malformed JSON, missing nodes key, etc. all
54
- * yield an empty Map. The caller's downstream filter then becomes a no-op
55
- * (no id resolves → no reminders).
56
- *
57
- * @param {string} projectRoot - workspace root
58
- * @returns {Map<string, string>} stable_id → knowledge_type (singular)
59
- */
60
- function readKnowledgeTypeMap(projectRoot) {
61
- const out = new Map();
62
- if (typeof projectRoot !== "string" || projectRoot.length === 0) return out;
63
-
64
- const metaPath = join(projectRoot, FABRIC_DIR, AGENTS_META_FILE);
65
- if (!existsSync(metaPath)) return out;
66
-
67
- let raw;
68
- try {
69
- raw = readFileSync(metaPath, "utf8");
70
- } catch {
71
- return out;
72
- }
73
-
74
- let parsed;
75
- try {
76
- parsed = JSON.parse(raw);
77
- } catch {
78
- return out;
79
- }
80
-
81
- if (parsed === null || typeof parsed !== "object") return out;
82
- const nodes = parsed.nodes;
83
- if (nodes === null || typeof nodes !== "object") return out;
84
-
85
- for (const [id, node] of Object.entries(nodes)) {
86
- if (node === null || typeof node !== "object") continue;
87
- const description = node.description;
88
- if (description === null || typeof description !== "object") continue;
89
- const kt = description.knowledge_type;
90
- if (typeof kt !== "string" || kt.length === 0) continue;
91
- out.set(id, kt);
92
- }
93
-
94
- return out;
95
- }
96
-
97
- /**
98
- * Scan parsed assistant turns for cites that should have a contract but
99
- * don't, returning the reminder lines to emit.
100
- *
101
- * Filter (all must hold for a given index i within a turn):
102
- * 1. cite_tags includes "applied" (turn-level — applies to the cited id)
103
- * 2. cite_commitments[i].operators is empty AND cite_commitments[i].skip_reason is null
104
- * 3. idTypeMap.get(cite_ids[i]) is in {decision, pitfall}
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
- *
110
- * Tag-level filter clarification: rc.20 cite_tags is parallel to ALL parsed
111
- * lines (including sentinels), but for the contract-missing reminder we use
112
- * the turn-level semantic — if the assistant tagged the cite as [applied],
113
- * the operator-or-skip contract applies. Per TASK-04 invariant, cite_ids and
114
- * cite_commitments are parallel index-aligned arrays (length-N each).
115
- *
116
- * Sentinel turns (cite_ids=[], cite_tags=["none"]) contribute no offenders
117
- * because the cite_ids loop has zero iterations.
118
- *
119
- * Offenders are deduplicated by id across the entire turn array; multiple
120
- * turns citing the same id yield ONE reminder line.
121
- *
122
- * @param {Object} args
123
- * @param {Array<{cite_ids: string[], cite_tags: string[], cite_commitments: Array<{operators: Array<unknown>, skip_reason: string|null}>}>} args.assistant_turns
124
- * @param {Map<string, string>} args.idTypeMap
125
- * @returns {string[]} reminder lines (empty when no offenders)
126
- */
127
- function formatContractMissingReminders({ assistant_turns, idTypeMap }) {
128
- if (!Array.isArray(assistant_turns) || assistant_turns.length === 0) return [];
129
- if (!(idTypeMap instanceof Map) || idTypeMap.size === 0) return [];
130
-
131
- const offenders = new Set();
132
-
133
- for (const turn of assistant_turns) {
134
- if (turn === null || typeof turn !== "object") continue;
135
- const citeIds = Array.isArray(turn.cite_ids) ? turn.cite_ids : [];
136
- const citeTags = Array.isArray(turn.cite_tags) ? turn.cite_tags : [];
137
- const commitments = Array.isArray(turn.cite_commitments) ? turn.cite_commitments : [];
138
-
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;
142
-
143
- // Iterate by cite_ids.length — sentinel entries don't have ids so they
144
- // contribute zero iterations even if cite_tags carries "none".
145
- for (let i = 0; i < citeIds.length; i += 1) {
146
- const id = citeIds[i];
147
- if (typeof id !== "string" || id.length === 0) continue;
148
-
149
- const type = idTypeMap.get(id);
150
- if (!CONTRACT_REQUIRED_TYPES.has(type)) continue;
151
-
152
- const commitment = commitments[i];
153
- if (commitment === null || typeof commitment !== "object") continue;
154
- const operators = Array.isArray(commitment.operators) ? commitment.operators : [];
155
- const skipReason = commitment.skip_reason;
156
- const hasContract = operators.length > 0 || (typeof skipReason === "string" && skipReason.length > 0);
157
- if (hasContract) continue;
158
-
159
- offenders.add(id);
160
- }
161
- }
162
-
163
- if (offenders.size === 0) return [];
164
-
165
- // Stable order: insertion order is the order ids first appeared across turns.
166
- const reminders = [];
167
- for (const id of offenders) {
168
- reminders.push(
169
- `⚠ KB: ${id} cited as [applied] but missing contract; add \`→ edit:<glob>\` or \`→ skip:<reason>\` next turn`,
170
- );
171
- }
172
- return reminders;
173
- }
174
-
175
- module.exports = {
176
- readKnowledgeTypeMap,
177
- formatContractMissingReminders,
178
- CONTRACT_REQUIRED_TYPES,
179
- };