@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
|
@@ -29,17 +29,32 @@
|
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
31
|
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-narrow.cjs"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/cite-policy-evict.cjs"
|
|
32
36
|
}
|
|
33
37
|
]
|
|
34
38
|
}
|
|
35
39
|
],
|
|
36
|
-
"
|
|
40
|
+
"PostToolUse": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/post-tooluse-mutation.cjs"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"SessionEnd": [
|
|
37
52
|
{
|
|
38
53
|
"matcher": "*",
|
|
39
54
|
"hooks": [
|
|
40
55
|
{
|
|
41
56
|
"type": "command",
|
|
42
|
-
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/
|
|
57
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-end-marker.cjs"
|
|
43
58
|
}
|
|
44
59
|
]
|
|
45
60
|
}
|
|
@@ -8,15 +8,27 @@
|
|
|
8
8
|
"SessionStart": [
|
|
9
9
|
{
|
|
10
10
|
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-broad.cjs\""
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
16
|
+
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-narrow.cjs\""
|
|
11
17
|
},
|
|
12
18
|
{
|
|
19
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
13
20
|
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/cite-policy-evict.cjs\""
|
|
14
21
|
}
|
|
15
22
|
],
|
|
16
|
-
"
|
|
23
|
+
"PostToolUse": [
|
|
17
24
|
{
|
|
18
25
|
"matcher": "Edit|Write|MultiEdit",
|
|
19
|
-
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/
|
|
26
|
+
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/post-tooluse-mutation.cjs\""
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"SessionEnd": [
|
|
30
|
+
{
|
|
31
|
+
"command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/session-end-marker.cjs\""
|
|
20
32
|
}
|
|
21
33
|
]
|
|
22
34
|
}
|
|
@@ -5,14 +5,26 @@
|
|
|
5
5
|
{ "command": ".cursor/hooks/fabric-hint.cjs" }
|
|
6
6
|
],
|
|
7
7
|
"sessionStart": [
|
|
8
|
-
{ "command": ".cursor/hooks/knowledge-hint-broad.cjs" }
|
|
9
|
-
{ "command": ".cursor/hooks/cite-policy-evict.cjs" }
|
|
8
|
+
{ "command": ".cursor/hooks/knowledge-hint-broad.cjs" }
|
|
10
9
|
],
|
|
11
10
|
"preToolUse": [
|
|
12
11
|
{
|
|
13
12
|
"matcher": "Edit|Write|MultiEdit",
|
|
14
13
|
"command": ".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
17
|
+
"command": ".cursor/hooks/cite-policy-evict.cjs"
|
|
15
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" }
|
|
16
28
|
]
|
|
17
29
|
}
|
|
18
30
|
}
|
|
@@ -984,8 +984,34 @@ function readUnderseedThreshold(projectRoot) {
|
|
|
984
984
|
return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
985
985
|
}
|
|
986
986
|
|
|
987
|
-
|
|
988
|
-
|
|
987
|
+
// F13 (ISS-20260531-038): the reminder cooldown sidecars were process-global
|
|
988
|
+
// (one file per project, no session key), so in concurrent multi-window sessions
|
|
989
|
+
// one window firing a nudge wrote the cooldown and silenced that nudge in EVERY
|
|
990
|
+
// other window. Scope the sidecar filename by sessionId — mirrors the already-
|
|
991
|
+
// session-scoped dismiss sidecar (sessionDismissFileName). Backward-compatible:
|
|
992
|
+
// a null/absent sessionId falls back to the legacy non-scoped path (upgrade +
|
|
993
|
+
// pre-session-id callers), so existing on-disk state and tests are unaffected;
|
|
994
|
+
// the Stop hook always passes the real session_id from its stdin payload.
|
|
995
|
+
function resolveHookSessionId(payload) {
|
|
996
|
+
return payload && typeof payload.session_id === "string" && payload.session_id.length > 0
|
|
997
|
+
? payload.session_id
|
|
998
|
+
: null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function sessionScopedCacheFile(baseRelPath, sessionId) {
|
|
1002
|
+
if (sessionId === undefined || sessionId === null || String(sessionId).length === 0) {
|
|
1003
|
+
return baseRelPath;
|
|
1004
|
+
}
|
|
1005
|
+
const safe = String(sessionId).replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
1006
|
+
const lastSlash = baseRelPath.lastIndexOf("/");
|
|
1007
|
+
const dot = baseRelPath.lastIndexOf(".");
|
|
1008
|
+
return dot > lastSlash
|
|
1009
|
+
? `${baseRelPath.slice(0, dot)}-${safe}${baseRelPath.slice(dot)}`
|
|
1010
|
+
: `${baseRelPath}-${safe}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function readShownCache(projectRoot, sessionId) {
|
|
1014
|
+
const cachePath = join(projectRoot, sessionScopedCacheFile(SHOWN_CACHE_FILE, sessionId));
|
|
989
1015
|
if (!existsSync(cachePath)) return {};
|
|
990
1016
|
try {
|
|
991
1017
|
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
@@ -995,12 +1021,11 @@ function readShownCache(projectRoot) {
|
|
|
995
1021
|
}
|
|
996
1022
|
}
|
|
997
1023
|
|
|
998
|
-
function writeShownCache(projectRoot, cache) {
|
|
999
|
-
const cachePath = join(projectRoot, SHOWN_CACHE_FILE);
|
|
1024
|
+
function writeShownCache(projectRoot, cache, sessionId) {
|
|
1025
|
+
const cachePath = join(projectRoot, sessionScopedCacheFile(SHOWN_CACHE_FILE, sessionId));
|
|
1000
1026
|
try {
|
|
1001
|
-
// ISS-016: atomic tmp+rename so
|
|
1002
|
-
//
|
|
1003
|
-
// plain write only if the shared lib failed to load.
|
|
1027
|
+
// ISS-016: atomic tmp+rename so a crash never leaves a truncated shown-cache.
|
|
1028
|
+
// Falls back to a plain write only if the shared lib failed to load.
|
|
1004
1029
|
if (stateStore && typeof stateStore.atomicWrite === "function") {
|
|
1005
1030
|
stateStore.atomicWrite(cachePath, JSON.stringify(cache));
|
|
1006
1031
|
} else {
|
|
@@ -1124,8 +1149,8 @@ function findLastDoctorRunTs(events) {
|
|
|
1124
1149
|
* v2.0.0-rc.7 T10: read the Signal-D cooldown sidecar timestamp (epoch ms).
|
|
1125
1150
|
* Missing file / parse failure → null (allow signal to fire).
|
|
1126
1151
|
*/
|
|
1127
|
-
function readMaintenanceLastEmit(projectRoot) {
|
|
1128
|
-
const p = join(projectRoot, MAINTENANCE_HINT_LAST_EMIT_FILE);
|
|
1152
|
+
function readMaintenanceLastEmit(projectRoot, sessionId) {
|
|
1153
|
+
const p = join(projectRoot, sessionScopedCacheFile(MAINTENANCE_HINT_LAST_EMIT_FILE, sessionId));
|
|
1129
1154
|
if (!existsSync(p)) return null;
|
|
1130
1155
|
try {
|
|
1131
1156
|
const raw = readFileSync(p, "utf8").trim();
|
|
@@ -1140,8 +1165,8 @@ function readMaintenanceLastEmit(projectRoot) {
|
|
|
1140
1165
|
return null;
|
|
1141
1166
|
}
|
|
1142
1167
|
|
|
1143
|
-
function writeMaintenanceLastEmit(projectRoot, nowMs) {
|
|
1144
|
-
const p = join(projectRoot, MAINTENANCE_HINT_LAST_EMIT_FILE);
|
|
1168
|
+
function writeMaintenanceLastEmit(projectRoot, nowMs, sessionId) {
|
|
1169
|
+
const p = join(projectRoot, sessionScopedCacheFile(MAINTENANCE_HINT_LAST_EMIT_FILE, sessionId));
|
|
1145
1170
|
try {
|
|
1146
1171
|
// ISS-016: atomic tmp+rename (see writeShownCache).
|
|
1147
1172
|
if (stateStore && typeof stateStore.atomicWrite === "function") {
|
|
@@ -1246,6 +1271,101 @@ function evaluateMaintenanceSignal(events, now, canonicalCount, lastEmitMs, thre
|
|
|
1246
1271
|
};
|
|
1247
1272
|
}
|
|
1248
1273
|
|
|
1274
|
+
// lifecycle-refactor W3-A2 (§7 graph generation signal): after a successful
|
|
1275
|
+
// archive the Stop hook REQUESTS edge extraction by emitting one
|
|
1276
|
+
// graph_edge_candidate_requested{stable_id, store?}. The hook never PRODUCES
|
|
1277
|
+
// edges (that is the archive/import skill's or doctor co-occurrence's job,
|
|
1278
|
+
// KT-DEC-0007) — it only flags "this entry just landed; someone should extract
|
|
1279
|
+
// its `related` edges". FROZEN-safe: O(1) tail scan, best-effort silent, single
|
|
1280
|
+
// advisory-locked appendLockedLine (same primitive the rest of this hook uses).
|
|
1281
|
+
//
|
|
1282
|
+
// HONEST stable_id sourcing — the deliberate limitation: pending entries (the
|
|
1283
|
+
// fabric-archive → extractKnowledge path) carry NO canonical stable_id (id is
|
|
1284
|
+
// late-bound at fab_review approve), so their knowledge_proposed event omits
|
|
1285
|
+
// stable_id (or sets the `pending:<key>` sentinel). A graph edge between
|
|
1286
|
+
// id-less pending drafts is meaningless, so we DO NOT fabricate one. We emit
|
|
1287
|
+
// ONLY when the most-recent knowledge_proposed event carries a real
|
|
1288
|
+
// K[TP]-XXX-NNNN stable_id (the approve/promote path) — i.e. an entry that
|
|
1289
|
+
// actually has a canonical node to attach edges to. When the latest proposed
|
|
1290
|
+
// is id-less we honestly skip; the request will fire on the approve event that
|
|
1291
|
+
// allocates the id. A session-scoped sidecar de-dupes so repeated Stop fires in
|
|
1292
|
+
// one session don't re-request the same id.
|
|
1293
|
+
const STABLE_ID_RE = /^K[TP]-[A-Z]{3}-\d{4}$/;
|
|
1294
|
+
const GRAPH_EDGE_REQUESTED_SIDECAR = ".fabric/.cache/graph-edge-requested";
|
|
1295
|
+
|
|
1296
|
+
function emitGraphEdgeCandidateBestEffort(cwd, events, sessionId) {
|
|
1297
|
+
try {
|
|
1298
|
+
if (!Array.isArray(events) || events.length === 0) return;
|
|
1299
|
+
const fabricDir = join(cwd, FABRIC_DIR);
|
|
1300
|
+
if (!existsSync(fabricDir)) return;
|
|
1301
|
+
|
|
1302
|
+
// O(1)-amortized tail scan for the newest knowledge_proposed carrying a
|
|
1303
|
+
// real (non-sentinel) stable_id. Stop at the first knowledge_proposed we
|
|
1304
|
+
// see — if the latest archive is id-less, we honestly skip rather than
|
|
1305
|
+
// reaching back to an older approved entry (that older entry's edges were
|
|
1306
|
+
// already requested when IT landed).
|
|
1307
|
+
let stableId = null;
|
|
1308
|
+
let store;
|
|
1309
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
1310
|
+
const ev = events[i];
|
|
1311
|
+
if (!ev || ev.event_type !== EVENT_TYPE_PROPOSED) continue;
|
|
1312
|
+
const candidate = typeof ev.stable_id === "string" ? ev.stable_id : null;
|
|
1313
|
+
if (candidate && STABLE_ID_RE.test(candidate)) {
|
|
1314
|
+
stableId = candidate;
|
|
1315
|
+
if (typeof ev.store === "string" && ev.store.length > 0) store = ev.store;
|
|
1316
|
+
}
|
|
1317
|
+
// First knowledge_proposed encountered (newest) decides; do not walk past
|
|
1318
|
+
// it to an older one.
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
if (stableId === null) return;
|
|
1322
|
+
|
|
1323
|
+
// Session-scoped de-dup: skip if we already requested edges for this exact
|
|
1324
|
+
// stable_id this session. Sidecar is a single line holding the last id.
|
|
1325
|
+
const sidecarPath = join(cwd, sessionScopedCacheFile(GRAPH_EDGE_REQUESTED_SIDECAR, sessionId));
|
|
1326
|
+
try {
|
|
1327
|
+
if (existsSync(sidecarPath)) {
|
|
1328
|
+
const prev = readFileSync(sidecarPath, "utf8").trim();
|
|
1329
|
+
if (prev === stableId) return;
|
|
1330
|
+
}
|
|
1331
|
+
} catch {
|
|
1332
|
+
// unreadable sidecar → fall through and (re)emit; de-dup is best-effort.
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
let idSuffix;
|
|
1336
|
+
try {
|
|
1337
|
+
idSuffix = require("node:crypto").randomUUID();
|
|
1338
|
+
} catch {
|
|
1339
|
+
idSuffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1340
|
+
}
|
|
1341
|
+
const event = {
|
|
1342
|
+
kind: "fabric-event",
|
|
1343
|
+
id: `event:${idSuffix}`,
|
|
1344
|
+
ts: Date.now(),
|
|
1345
|
+
schema_version: 1,
|
|
1346
|
+
event_type: "graph_edge_candidate_requested",
|
|
1347
|
+
stable_id: stableId,
|
|
1348
|
+
};
|
|
1349
|
+
if (store !== undefined) event.store = store;
|
|
1350
|
+
if (typeof sessionId === "string" && sessionId.length > 0) event.session_id = sessionId;
|
|
1351
|
+
appendLockedLine(join(fabricDir, EVENT_LEDGER_FILE), JSON.stringify(event) + "\n");
|
|
1352
|
+
|
|
1353
|
+
// Record the de-dup marker (best-effort; atomic when state-store lib loaded).
|
|
1354
|
+
try {
|
|
1355
|
+
if (stateStore && typeof stateStore.atomicWrite === "function") {
|
|
1356
|
+
stateStore.atomicWrite(sidecarPath, stableId);
|
|
1357
|
+
} else {
|
|
1358
|
+
mkdirSync(dirname(sidecarPath), { recursive: true });
|
|
1359
|
+
writeFileSync(sidecarPath, stableId);
|
|
1360
|
+
}
|
|
1361
|
+
} catch {
|
|
1362
|
+
// de-dup marker write failed — at worst we re-request next Stop; harmless.
|
|
1363
|
+
}
|
|
1364
|
+
} catch {
|
|
1365
|
+
// best-effort §7 signal — never block the Stop hook (KT-DEC-0007).
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1249
1369
|
// v2.1 NEW-N-3 (ADJ-NEWN-3): hook_signal_emitted instrumentation. Writes ONE
|
|
1250
1370
|
// best-effort ledger row at the point a nudge is actually delivered (post-
|
|
1251
1371
|
// cooldown), so the join key measures nudge-trigger logic (which signal fired,
|
|
@@ -1852,6 +1972,17 @@ function main(env, stdio) {
|
|
|
1852
1972
|
);
|
|
1853
1973
|
|
|
1854
1974
|
const events = readLedger(cwd);
|
|
1975
|
+
|
|
1976
|
+
// lifecycle-refactor W3-A2 (§7): request graph-edge extraction for a freshly
|
|
1977
|
+
// archived canonical entry. Runs UNCONDITIONALLY here (before the nudge
|
|
1978
|
+
// cooldown/dismiss early-returns) so the §7 signal is independent of whether
|
|
1979
|
+
// a reminder banner is shown this Stop. Best-effort, never throws.
|
|
1980
|
+
try {
|
|
1981
|
+
emitGraphEdgeCandidateBestEffort(cwd, events, resolveHookSessionId(stdinPayload));
|
|
1982
|
+
} catch {
|
|
1983
|
+
// never block the Stop hook
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1855
1986
|
let pendingStats;
|
|
1856
1987
|
try {
|
|
1857
1988
|
pendingStats = readPendingStats(cwd, now);
|
|
@@ -1984,7 +2115,7 @@ function main(env, stdio) {
|
|
|
1984
2115
|
// for the prompt to be actionable.
|
|
1985
2116
|
if (result === null) {
|
|
1986
2117
|
try {
|
|
1987
|
-
const lastEmit = readMaintenanceLastEmit(cwd);
|
|
2118
|
+
const lastEmit = readMaintenanceLastEmit(cwd, resolveHookSessionId(stdinPayload));
|
|
1988
2119
|
result = evaluateMaintenanceSignal(
|
|
1989
2120
|
events,
|
|
1990
2121
|
now,
|
|
@@ -2041,7 +2172,7 @@ function main(env, stdio) {
|
|
|
2041
2172
|
delete result.threshold;
|
|
2042
2173
|
delete result.actual_value;
|
|
2043
2174
|
out.write(JSON.stringify(result));
|
|
2044
|
-
writeMaintenanceLastEmit(cwd, nowMs);
|
|
2175
|
+
writeMaintenanceLastEmit(cwd, nowMs, resolveHookSessionId(stdinPayload));
|
|
2045
2176
|
return;
|
|
2046
2177
|
}
|
|
2047
2178
|
|
|
@@ -2049,7 +2180,7 @@ function main(env, stdio) {
|
|
|
2049
2180
|
// archive_hint_cooldown_hours (default 12h) regardless of state drift.
|
|
2050
2181
|
// Pure reminder-noise reduction; the underlying trigger logic is unchanged.
|
|
2051
2182
|
const cooldownMs = readCooldownHours(cwd) * MS_PER_HOUR;
|
|
2052
|
-
const cache = readShownCache(cwd);
|
|
2183
|
+
const cache = readShownCache(cwd, resolveHookSessionId(stdinPayload));
|
|
2053
2184
|
const lastShown = cache[result.signal];
|
|
2054
2185
|
// rc.34 TASK-01 + review-fix (Gemini P1): future-stamped lastShown
|
|
2055
2186
|
// (backward clock skew) bypasses cooldown — sidecar treated as expired.
|
|
@@ -2066,7 +2197,7 @@ function main(env, stdio) {
|
|
|
2066
2197
|
delete result.actual_value;
|
|
2067
2198
|
out.write(JSON.stringify(result));
|
|
2068
2199
|
cache[result.signal] = nowMs;
|
|
2069
|
-
writeShownCache(cwd, cache);
|
|
2200
|
+
writeShownCache(cwd, cache, resolveHookSessionId(stdinPayload));
|
|
2070
2201
|
} catch {
|
|
2071
2202
|
// Silent — never block on hook failure.
|
|
2072
2203
|
}
|
|
@@ -2121,6 +2252,9 @@ module.exports = {
|
|
|
2121
2252
|
// of the contract-missing emission contract). The lib module itself is
|
|
2122
2253
|
// also exported indirectly via the reminder helper.
|
|
2123
2254
|
emitCiteContractRemindersBestEffort,
|
|
2255
|
+
// lifecycle-refactor W3-A2 (§7): graph-edge-candidate request emitter
|
|
2256
|
+
// (exported for unit testing of the honest stable_id-gating + de-dup).
|
|
2257
|
+
emitGraphEdgeCandidateBestEffort,
|
|
2124
2258
|
CONSTANTS: {
|
|
2125
2259
|
FABRIC_DIR,
|
|
2126
2260
|
EVENT_LEDGER_FILE,
|
|
@@ -2158,6 +2292,8 @@ module.exports = {
|
|
|
2158
2292
|
// v2.0.0-rc.8 (TASK-002): in-flight import gate for Signal B.
|
|
2159
2293
|
IMPORT_STATE_FILE_REL,
|
|
2160
2294
|
IMPORT_IN_FLIGHT_MAX_AGE_HOURS,
|
|
2295
|
+
// lifecycle-refactor W3-A2 (§7): graph-edge-request de-dup sidecar.
|
|
2296
|
+
GRAPH_EDGE_REQUESTED_SIDECAR,
|
|
2161
2297
|
},
|
|
2162
2298
|
};
|
|
2163
2299
|
|
|
@@ -574,7 +574,18 @@ function truncateSummary(raw, maxLen) {
|
|
|
574
574
|
function formatEntryLine(entry, maxLen) {
|
|
575
575
|
const id = entry.id || "(no-id)";
|
|
576
576
|
const summary = truncateSummary(entry.summary, maxLen);
|
|
577
|
-
|
|
577
|
+
// lifecycle-refactor W3-T2 (§7 图谱消费 / §5 hook 沿 related 二阶召回): when this
|
|
578
|
+
// entry was pulled in by following a surfaced entry's `related` graph edge,
|
|
579
|
+
// tag the line with its provenance so the agent knows it arrived via the graph,
|
|
580
|
+
// not its own ranking. Omitted entirely for ordinarily-ranked entries — no fake
|
|
581
|
+
// "related" annotation is ever synthesized (graph-empty honesty).
|
|
582
|
+
const provenance =
|
|
583
|
+
typeof entry.related_to === "string" && entry.related_to.length > 0
|
|
584
|
+
? ` (related-to-${entry.related_to})`
|
|
585
|
+
: "";
|
|
586
|
+
return summary.length > 0
|
|
587
|
+
? ` - ${id} · ${summary}${provenance}`
|
|
588
|
+
: ` - ${id}${provenance}`;
|
|
578
589
|
}
|
|
579
590
|
|
|
580
591
|
/**
|
|
@@ -91,6 +91,9 @@ const { readJsonState, writeJsonState } = require("./lib/state-store.cjs");
|
|
|
91
91
|
// and corrupt a line. Route every shared-file append through the advisory-lock
|
|
92
92
|
// primitive (drop-on-contention, best-effort — matches injection-log).
|
|
93
93
|
const { appendLockedLine } = require("./lib/injection-log.cjs");
|
|
94
|
+
// lifecycle-refactor W1-T2: client discriminator for the hook_surface_emitted
|
|
95
|
+
// event (schema requires the `client` enum). Mirrors the broad hook's import.
|
|
96
|
+
const { detectClient } = require("./lib/client-adapter.cjs");
|
|
94
97
|
// v2.1.0-rc.1 P4 (F4/S63): hook-side reader for the CLI pre-generated
|
|
95
98
|
// resolved-bindings snapshot. Store-aware hint surfaces the write-target store
|
|
96
99
|
// for the edited file WITHOUT re-resolving or walking store trees. Best-effort.
|
|
@@ -1169,7 +1172,15 @@ function formatEntryLine(entry, maxLen) {
|
|
|
1169
1172
|
const maturity = entry.maturity || "unknown";
|
|
1170
1173
|
const summary = truncateSummary(entry.summary, maxLen);
|
|
1171
1174
|
const tail = summary.length > 0 ? ` ${summary}` : "";
|
|
1172
|
-
|
|
1175
|
+
// lifecycle-refactor W3-T2 (§7 图谱消费 / §5 hook 沿 related 二阶召回): mark entries
|
|
1176
|
+
// pulled in by a surfaced entry's one-hop `related` graph edge with their source
|
|
1177
|
+
// provenance. Omitted for ordinarily-ranked entries — no fake graph annotation
|
|
1178
|
+
// is ever synthesized (graph-empty honesty).
|
|
1179
|
+
const provenance =
|
|
1180
|
+
typeof entry.related_to === "string" && entry.related_to.length > 0
|
|
1181
|
+
? ` (related-to-${entry.related_to})`
|
|
1182
|
+
: "";
|
|
1183
|
+
return ` [${id}] (${type}/${maturity})${tail}${provenance}`;
|
|
1173
1184
|
}
|
|
1174
1185
|
|
|
1175
1186
|
function readSummaryMaxLen(projectRoot) {
|
|
@@ -1516,6 +1527,48 @@ function main(env, stdio) {
|
|
|
1516
1527
|
err.write(`${line}\n`);
|
|
1517
1528
|
}
|
|
1518
1529
|
|
|
1530
|
+
// lifecycle-refactor W1-T2: hook_surface_emitted — record WHICH narrow-scoped
|
|
1531
|
+
// stable_ids this PreToolUse fire surfaced into the edit, so doctor can join
|
|
1532
|
+
// surfaced→edited (this is the join's LEFT half; the edit_intent_checked event
|
|
1533
|
+
// appended above supplies the edited path / RIGHT half, keyed on the same
|
|
1534
|
+
// real payload session_id). Fires only after all gates passed and lines were
|
|
1535
|
+
// rendered (so it tracks genuinely-surfaced hints, never bloat). Best-effort,
|
|
1536
|
+
// never blocks the edit (KT-DEC-0007); the schema's `client` is a required
|
|
1537
|
+
// enum, so skip when the client is undetectable rather than emit an invalid
|
|
1538
|
+
// row. Mirrors the broad SessionStart emit (knowledge-hint-broad.cjs).
|
|
1539
|
+
try {
|
|
1540
|
+
const surfaceClient = detectClient();
|
|
1541
|
+
const fabricDir = join(cwd, FABRIC_DIR_REL);
|
|
1542
|
+
if (surfaceClient !== undefined && existsSync(fabricDir)) {
|
|
1543
|
+
const renderedIds = resolvedEntries
|
|
1544
|
+
.map((e) => (e && typeof e.id === "string" ? e.id : null))
|
|
1545
|
+
.filter((x) => x !== null);
|
|
1546
|
+
const realSessionId =
|
|
1547
|
+
payload &&
|
|
1548
|
+
typeof payload === "object" &&
|
|
1549
|
+
typeof payload.session_id === "string" &&
|
|
1550
|
+
payload.session_id.length > 0
|
|
1551
|
+
? payload.session_id
|
|
1552
|
+
: null;
|
|
1553
|
+
const surfaceEvent = {
|
|
1554
|
+
kind: "fabric-event",
|
|
1555
|
+
id: `event:${randomUUID()}`,
|
|
1556
|
+
ts: nowMs,
|
|
1557
|
+
schema_version: 1,
|
|
1558
|
+
...(realSessionId ? { session_id: realSessionId } : {}),
|
|
1559
|
+
event_type: "hook_surface_emitted",
|
|
1560
|
+
hook_name: "knowledge-hint-narrow",
|
|
1561
|
+
client: surfaceClient,
|
|
1562
|
+
target_channel: "stderr",
|
|
1563
|
+
rendered_ids: renderedIds,
|
|
1564
|
+
delivery_status: "delivered",
|
|
1565
|
+
};
|
|
1566
|
+
appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), JSON.stringify(surfaceEvent) + "\n");
|
|
1567
|
+
}
|
|
1568
|
+
} catch {
|
|
1569
|
+
// best-effort telemetry — never block the edit
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1519
1572
|
// v2.0.0-rc.33 W2-6 (P0-7): stdout JSON envelope. When
|
|
1520
1573
|
// hint_reminder_to_context is true (default), serialize the same banner
|
|
1521
1574
|
// body as Claude Code's PreToolUse hookSpecificOutput shape so the model
|