@fenglimg/fabric-cli 2.0.0-rc.29 → 2.0.0-rc.33
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/{doctor-TTDTKOFJ.js → doctor-E26YO67D.js} +8 -2
- package/dist/index.js +3 -3
- package/dist/{install-ODEKSJDS.js → install-YSFVNY3T.js} +1 -1
- package/package.json +3 -3
- package/templates/hooks/knowledge-hint-broad.cjs +268 -21
- package/templates/hooks/knowledge-hint-narrow.cjs +466 -14
- package/templates/skills/fabric-archive/SKILL.md +144 -738
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +5 -5
- package/templates/skills/fabric-archive/ref/i18n-policy.md +3 -3
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/{phase-0-4-onboard.md → phase-1-5-onboard.md} +21 -21
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +60 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +54 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +80 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +6 -6
- package/templates/skills/fabric-archive/ref/worked-examples.md +1 -1
- package/templates/skills/fabric-import/SKILL.md +29 -556
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +1 -1
|
@@ -261,8 +261,12 @@ var doctorCommand = defineCommand({
|
|
|
261
261
|
fixKnowledgeReport = await runDoctorFixKnowledge(resolution.target);
|
|
262
262
|
report = fixKnowledgeReport.report;
|
|
263
263
|
} else if (fix) {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
if (args["dry-run"] === true) {
|
|
265
|
+
report = await runDoctorReport(resolution.target);
|
|
266
|
+
} else {
|
|
267
|
+
fixReport = await runDoctorFix(resolution.target);
|
|
268
|
+
report = fixReport.report;
|
|
269
|
+
}
|
|
266
270
|
} else {
|
|
267
271
|
report = await runDoctorReport(resolution.target);
|
|
268
272
|
}
|
|
@@ -277,6 +281,8 @@ var doctorCommand = defineCommand({
|
|
|
277
281
|
renderFixKnowledgeMutations(fixKnowledgeReport, dt);
|
|
278
282
|
} else if (fixReport !== null) {
|
|
279
283
|
writeStdout(fixReport.message);
|
|
284
|
+
} else if (fix && args["dry-run"] === true) {
|
|
285
|
+
writeStdout(dt("cli.doctor.fix-dry-run-banner"));
|
|
280
286
|
}
|
|
281
287
|
renderHumanReport(report, dt);
|
|
282
288
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import { defineCommand, runMain } from "citty";
|
|
|
11
11
|
|
|
12
12
|
// src/commands/index.ts
|
|
13
13
|
var allCommands = {
|
|
14
|
-
install: () => import("./install-
|
|
15
|
-
doctor: () => import("./doctor-
|
|
14
|
+
install: () => import("./install-YSFVNY3T.js").then((module) => module.default),
|
|
15
|
+
doctor: () => import("./doctor-E26YO67D.js").then((module) => module.default),
|
|
16
16
|
serve: () => import("./serve-43JTEM3U.js").then((module) => module.default),
|
|
17
17
|
uninstall: () => import("./uninstall-VLLJG7JT.js").then((module) => module.default),
|
|
18
18
|
config: () => import("./config-5CH4EJQ2.js").then((module) => module.default),
|
|
@@ -26,7 +26,7 @@ var allCommands = {
|
|
|
26
26
|
var main = defineCommand({
|
|
27
27
|
meta: {
|
|
28
28
|
name: "fabric",
|
|
29
|
-
version: "2.0.0-rc.
|
|
29
|
+
version: "2.0.0-rc.33",
|
|
30
30
|
description: t("cli.main.description")
|
|
31
31
|
},
|
|
32
32
|
subCommands: allCommands
|
|
@@ -1348,7 +1348,7 @@ function readProjectName(target) {
|
|
|
1348
1348
|
return basename(target);
|
|
1349
1349
|
}
|
|
1350
1350
|
function getCliVersion() {
|
|
1351
|
-
return true ? "2.0.0-rc.
|
|
1351
|
+
return true ? "2.0.0-rc.33" : "unknown";
|
|
1352
1352
|
}
|
|
1353
1353
|
function sortRecord(record) {
|
|
1354
1354
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fab": "dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"tree-sitter-javascript": "^0.25.0",
|
|
21
21
|
"tree-sitter-typescript": "^0.23.2",
|
|
22
22
|
"web-tree-sitter": "^0.26.8",
|
|
23
|
-
"@fenglimg/fabric-server": "2.0.0-rc.
|
|
24
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
23
|
+
"@fenglimg/fabric-server": "2.0.0-rc.33",
|
|
24
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.33"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.15.0",
|
|
@@ -49,10 +49,12 @@
|
|
|
49
49
|
const { spawnSync } = require("node:child_process");
|
|
50
50
|
const {
|
|
51
51
|
existsSync,
|
|
52
|
+
mkdirSync,
|
|
52
53
|
readdirSync,
|
|
53
54
|
readFileSync,
|
|
55
|
+
writeFileSync,
|
|
54
56
|
} = require("node:fs");
|
|
55
|
-
const { join } = require("node:path");
|
|
57
|
+
const { dirname, join } = require("node:path");
|
|
56
58
|
|
|
57
59
|
// rc.16 TASK-003: shared banner-i18n lib (resolves fabric_language config and
|
|
58
60
|
// renders localized banner text). Mirror of the wiring in fabric-hint.cjs
|
|
@@ -89,6 +91,30 @@ const KNOWLEDGE_CANONICAL_TYPES = [
|
|
|
89
91
|
];
|
|
90
92
|
const DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
91
93
|
|
|
94
|
+
// v2.0.0-rc.33 W2-1 (P0-9): TopK upper bound on broad-scoped entries surfaced
|
|
95
|
+
// per SessionStart fire. Keeps the banner inside ~1 screenful so the agent
|
|
96
|
+
// actually reads the top-priority entries instead of triaging a wall of text.
|
|
97
|
+
// Overridable via fabric-config.json#hint_broad_top_k (range 1..50).
|
|
98
|
+
const DEFAULT_HINT_BROAD_TOP_K = 8;
|
|
99
|
+
|
|
100
|
+
// v2.0.0-rc.33 W2-5 (P1-8): cooldown (in hours) between broad-hint re-emits.
|
|
101
|
+
// Default 0 preserves rc.32 behavior — every SessionStart re-fires the banner.
|
|
102
|
+
// Cache key uses a separate sidecar from the fabric-hint Signal A/B/C cache
|
|
103
|
+
// so the two cooldowns don't interfere.
|
|
104
|
+
const DEFAULT_HINT_BROAD_COOLDOWN_HOURS = 0;
|
|
105
|
+
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
106
|
+
const HINT_BROAD_LAST_EMIT_FILE = join(
|
|
107
|
+
".fabric",
|
|
108
|
+
".cache",
|
|
109
|
+
"knowledge-hint-broad-last-emit",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// v2.0.0-rc.33 W2-6 (P0-7): when true, emit banner as
|
|
113
|
+
// hookSpecificOutput.additionalContext JSON on stdout (Claude Code PreToolUse
|
|
114
|
+
// contract) so the model receives the reminder in-context. Stderr remains the
|
|
115
|
+
// human-facing channel for logs / breadcrumbs.
|
|
116
|
+
const DEFAULT_HINT_REMINDER_TO_CONTEXT = true;
|
|
117
|
+
|
|
92
118
|
// -----------------------------------------------------------------------------
|
|
93
119
|
// rc.8 underseed self-check helpers.
|
|
94
120
|
//
|
|
@@ -153,6 +179,97 @@ function readUnderseedThreshold(projectRoot) {
|
|
|
153
179
|
return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
154
180
|
}
|
|
155
181
|
|
|
182
|
+
/**
|
|
183
|
+
* v2.0.0-rc.33 W2-1: resolve hint_broad_top_k from fabric-config.json. Slices
|
|
184
|
+
* the broad entry list to TopK before group/truncation render. Validates the
|
|
185
|
+
* schema's 1..50 range inline so a malformed config silently falls back.
|
|
186
|
+
*/
|
|
187
|
+
function readBroadTopK(projectRoot) {
|
|
188
|
+
const configPath = join(projectRoot, FABRIC_DIR_REL, FABRIC_CONFIG_FILE);
|
|
189
|
+
if (!existsSync(configPath)) return DEFAULT_HINT_BROAD_TOP_K;
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
192
|
+
const v = parsed && parsed.hint_broad_top_k;
|
|
193
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 1 && v <= 50) {
|
|
194
|
+
return Math.floor(v);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// fall through to default
|
|
198
|
+
}
|
|
199
|
+
return DEFAULT_HINT_BROAD_TOP_K;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* v2.0.0-rc.33 W2-5: resolve hint_broad_cooldown_hours. Schema clamps 0..168;
|
|
204
|
+
* 0 means "no cooldown" (re-emit on every SessionStart, rc.32 behavior).
|
|
205
|
+
*/
|
|
206
|
+
function readBroadCooldownHours(projectRoot) {
|
|
207
|
+
const configPath = join(projectRoot, FABRIC_DIR_REL, FABRIC_CONFIG_FILE);
|
|
208
|
+
if (!existsSync(configPath)) return DEFAULT_HINT_BROAD_COOLDOWN_HOURS;
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
211
|
+
const v = parsed && parsed.hint_broad_cooldown_hours;
|
|
212
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 168) {
|
|
213
|
+
return v;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// fall through to default
|
|
217
|
+
}
|
|
218
|
+
return DEFAULT_HINT_BROAD_COOLDOWN_HOURS;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* v2.0.0-rc.33 W2-6: resolve hint_reminder_to_context. Boolean flag — when
|
|
223
|
+
* true (default) the hook writes a Claude-Code-shaped JSON envelope to stdout
|
|
224
|
+
* carrying the banner under hookSpecificOutput.additionalContext so the model
|
|
225
|
+
* receives the reminder in-context. Stderr stays informational either way.
|
|
226
|
+
*/
|
|
227
|
+
function readReminderToContext(projectRoot) {
|
|
228
|
+
const configPath = join(projectRoot, FABRIC_DIR_REL, FABRIC_CONFIG_FILE);
|
|
229
|
+
if (!existsSync(configPath)) return DEFAULT_HINT_REMINDER_TO_CONTEXT;
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
232
|
+
const v = parsed && parsed.hint_reminder_to_context;
|
|
233
|
+
if (typeof v === "boolean") return v;
|
|
234
|
+
} catch {
|
|
235
|
+
// fall through to default
|
|
236
|
+
}
|
|
237
|
+
return DEFAULT_HINT_REMINDER_TO_CONTEXT;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* v2.0.0-rc.33 W2-5: read/write the broad-hint last-emit timestamp sidecar.
|
|
242
|
+
* Distinct from fabric-hint's shown-cache so signal cooldowns stay isolated.
|
|
243
|
+
* Returns epoch ms or null when missing/unreadable.
|
|
244
|
+
*/
|
|
245
|
+
function readBroadLastEmit(projectRoot) {
|
|
246
|
+
const p = join(projectRoot, HINT_BROAD_LAST_EMIT_FILE);
|
|
247
|
+
if (!existsSync(p)) return null;
|
|
248
|
+
try {
|
|
249
|
+
const raw = readFileSync(p, "utf8").trim();
|
|
250
|
+
if (raw.length === 0) return null;
|
|
251
|
+
const asNum = Number(raw);
|
|
252
|
+
if (Number.isFinite(asNum) && asNum > 0) return asNum;
|
|
253
|
+
const ms = Date.parse(raw);
|
|
254
|
+
if (Number.isFinite(ms)) return ms;
|
|
255
|
+
} catch {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function writeBroadLastEmit(projectRoot, nowMs) {
|
|
262
|
+
const p = join(projectRoot, HINT_BROAD_LAST_EMIT_FILE);
|
|
263
|
+
try {
|
|
264
|
+
if (!existsSync(dirname(p))) {
|
|
265
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
writeFileSync(p, String(nowMs));
|
|
268
|
+
} catch {
|
|
269
|
+
// Silent — sidecar failure must never block session start.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
156
273
|
/**
|
|
157
274
|
* Classify the on-disk import lifecycle by reading
|
|
158
275
|
* `.fabric/.import-state.json`. Returns one of:
|
|
@@ -245,8 +362,24 @@ const CLI_TIMEOUT_MS = 2000;
|
|
|
245
362
|
|
|
246
363
|
// Maximum summary length per entry. Keeps each line bounded so stderr does
|
|
247
364
|
// not blow up terminal width with multi-paragraph summaries from sloppy
|
|
248
|
-
// pending entries. Truncation appends an ellipsis.
|
|
249
|
-
|
|
365
|
+
// pending entries. Truncation appends an ellipsis. v2.0.0-rc.33 W4-A3:
|
|
366
|
+
// `hint_summary_max_len` in fabric-config overrides this default (range 40..240).
|
|
367
|
+
const DEFAULT_SUMMARY_MAX_LEN = 80;
|
|
368
|
+
|
|
369
|
+
function readSummaryMaxLen(projectRoot) {
|
|
370
|
+
const configPath = join(projectRoot, FABRIC_DIR_REL, FABRIC_CONFIG_FILE);
|
|
371
|
+
if (!existsSync(configPath)) return DEFAULT_SUMMARY_MAX_LEN;
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
374
|
+
const v = parsed && parsed.hint_summary_max_len;
|
|
375
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 40 && v <= 240) {
|
|
376
|
+
return Math.floor(v);
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// fall through to default
|
|
380
|
+
}
|
|
381
|
+
return DEFAULT_SUMMARY_MAX_LEN;
|
|
382
|
+
}
|
|
250
383
|
|
|
251
384
|
// Canonical type order — render groups in this sequence so output is stable
|
|
252
385
|
// across runs (Object.keys iteration order is insertion order, but the JSON
|
|
@@ -288,6 +421,11 @@ const MATURITY_DRAFT = "draft";
|
|
|
288
421
|
*/
|
|
289
422
|
function invokePlanContextHint(cwd) {
|
|
290
423
|
const candidates = ["fabric", "fab"];
|
|
424
|
+
// rc.31 NEW-6: capture the last meaningful failure so we can surface it on
|
|
425
|
+
// stderr before fail-open. Without this, hook silently swallows backend
|
|
426
|
+
// crashes (e.g. agents_meta_invalid → plan-context-hint exits with stderr
|
|
427
|
+
// payload and the AI / user never sees KB chain is dead).
|
|
428
|
+
let lastFailure = null;
|
|
291
429
|
for (const bin of candidates) {
|
|
292
430
|
let res;
|
|
293
431
|
try {
|
|
@@ -300,17 +438,37 @@ function invokePlanContextHint(cwd) {
|
|
|
300
438
|
} catch {
|
|
301
439
|
continue; // spawn throw (extremely rare) — try next candidate
|
|
302
440
|
}
|
|
303
|
-
// ENOENT surfaces as error on the result object.
|
|
304
|
-
|
|
441
|
+
// ENOENT surfaces as error on the result object. Skip silently for ENOENT
|
|
442
|
+
// (bin not installed is expected for `fabric` when only `fab` is shipped).
|
|
443
|
+
if (res.error) {
|
|
444
|
+
if (res.error.code !== "ENOENT") {
|
|
445
|
+
lastFailure = { bin, reason: String(res.error.message || res.error.code || res.error) };
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (res.status === null || res.status !== 0) {
|
|
450
|
+
const stderrSnip = (res.stderr || "").trim().slice(0, 240);
|
|
451
|
+
if (stderrSnip.length > 0) {
|
|
452
|
+
lastFailure = { bin, reason: stderrSnip };
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
305
456
|
const raw = (res.stdout || "").trim();
|
|
306
457
|
if (raw.length === 0) continue;
|
|
307
458
|
try {
|
|
308
459
|
const parsed = JSON.parse(raw);
|
|
309
460
|
if (parsed && typeof parsed === "object") return parsed;
|
|
310
|
-
} catch {
|
|
311
|
-
|
|
461
|
+
} catch (err) {
|
|
462
|
+
lastFailure = { bin, reason: `malformed JSON from plan-context-hint: ${String(err && err.message || err)}` };
|
|
312
463
|
}
|
|
313
464
|
}
|
|
465
|
+
if (lastFailure !== null) {
|
|
466
|
+
// Single warning line — never throws, never blocks the hook. Lets users /
|
|
467
|
+
// AI notice that the KB chain is degraded instead of being silently empty.
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
`[fabric-hint] plan-context-hint (${lastFailure.bin}) failed: ${lastFailure.reason.replace(/\n/g, " ")}\n`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
314
472
|
return null;
|
|
315
473
|
}
|
|
316
474
|
|
|
@@ -351,17 +509,21 @@ function groupEntries(narrow) {
|
|
|
351
509
|
return { typeOrder, byType };
|
|
352
510
|
}
|
|
353
511
|
|
|
354
|
-
|
|
512
|
+
// v2.0.0-rc.33 W4-A3: maxLen is now caller-supplied (sourced from
|
|
513
|
+
// fabric-config#hint_summary_max_len in main; tests + ad-hoc callers may
|
|
514
|
+
// omit to fall back to DEFAULT_SUMMARY_MAX_LEN).
|
|
515
|
+
function truncateSummary(raw, maxLen) {
|
|
355
516
|
const s = typeof raw === "string" ? raw : "";
|
|
356
517
|
// Collapse newlines / runs of whitespace so each entry fits one line.
|
|
357
518
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
358
|
-
|
|
359
|
-
|
|
519
|
+
const cap = typeof maxLen === "number" && maxLen > 0 ? maxLen : DEFAULT_SUMMARY_MAX_LEN;
|
|
520
|
+
if (flat.length <= cap) return flat;
|
|
521
|
+
return `${flat.slice(0, cap - 1)}…`;
|
|
360
522
|
}
|
|
361
523
|
|
|
362
|
-
function formatEntryLine(entry) {
|
|
524
|
+
function formatEntryLine(entry, maxLen) {
|
|
363
525
|
const id = entry.id || "(no-id)";
|
|
364
|
-
const summary = truncateSummary(entry.summary);
|
|
526
|
+
const summary = truncateSummary(entry.summary, maxLen);
|
|
365
527
|
return summary.length > 0 ? ` - ${id} · ${summary}` : ` - ${id}`;
|
|
366
528
|
}
|
|
367
529
|
|
|
@@ -370,7 +532,7 @@ function formatEntryLine(entry) {
|
|
|
370
532
|
* Each entry gets one line: ` - <id> · <summary>`. Type/maturity headers
|
|
371
533
|
* group the listing.
|
|
372
534
|
*/
|
|
373
|
-
function renderFull(narrow) {
|
|
535
|
+
function renderFull(narrow, maxLen) {
|
|
374
536
|
const { typeOrder, byType } = groupEntries(narrow);
|
|
375
537
|
const lines = [];
|
|
376
538
|
for (const type of typeOrder) {
|
|
@@ -389,7 +551,7 @@ function renderFull(narrow) {
|
|
|
389
551
|
for (const maturity of maturities) {
|
|
390
552
|
lines.push(` [${type}] (${maturity}):`);
|
|
391
553
|
for (const entry of maturityMap.get(maturity)) {
|
|
392
|
-
lines.push(formatEntryLine(entry));
|
|
554
|
+
lines.push(formatEntryLine(entry, maxLen));
|
|
393
555
|
}
|
|
394
556
|
}
|
|
395
557
|
}
|
|
@@ -402,7 +564,7 @@ function renderFull(narrow) {
|
|
|
402
564
|
* an inline id list (no summary); draft (and unknown) buckets collapse to a
|
|
403
565
|
* count.
|
|
404
566
|
*/
|
|
405
|
-
function renderTruncated(narrow) {
|
|
567
|
+
function renderTruncated(narrow, maxLen) {
|
|
406
568
|
const { typeOrder, byType } = groupEntries(narrow);
|
|
407
569
|
const lines = [];
|
|
408
570
|
for (const type of typeOrder) {
|
|
@@ -413,7 +575,7 @@ function renderTruncated(narrow) {
|
|
|
413
575
|
if (proven && proven.length > 0) {
|
|
414
576
|
lines.push(` [${type}] proven (${proven.length}):`);
|
|
415
577
|
for (const entry of proven) {
|
|
416
|
-
lines.push(formatEntryLine(entry));
|
|
578
|
+
lines.push(formatEntryLine(entry, maxLen));
|
|
417
579
|
}
|
|
418
580
|
}
|
|
419
581
|
|
|
@@ -450,7 +612,7 @@ function renderTruncated(narrow) {
|
|
|
450
612
|
* after writing exactly one stderr breadcrumb so operators grepping a stuck-
|
|
451
613
|
* banner report can diagnose the version drift without source-diving.
|
|
452
614
|
*/
|
|
453
|
-
function renderSummary(payload) {
|
|
615
|
+
function renderSummary(payload, maxLen) {
|
|
454
616
|
if (!payload || payload.version !== 2) {
|
|
455
617
|
if (payload && payload.version !== undefined) {
|
|
456
618
|
try {
|
|
@@ -473,7 +635,7 @@ function renderSummary(payload) {
|
|
|
473
635
|
? `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available (truncated):`
|
|
474
636
|
: `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available:`;
|
|
475
637
|
|
|
476
|
-
const body = truncated ? renderTruncated(entries) : renderFull(entries);
|
|
638
|
+
const body = truncated ? renderTruncated(entries, maxLen) : renderFull(entries, maxLen);
|
|
477
639
|
|
|
478
640
|
const lines = [banner, ...body];
|
|
479
641
|
const revHash = typeof payload.revision_hash === "string" ? payload.revision_hash : null;
|
|
@@ -532,7 +694,25 @@ function renderSummary(payload) {
|
|
|
532
694
|
function main(env, stdio) {
|
|
533
695
|
try {
|
|
534
696
|
const cwd = (env && env.cwd) || process.cwd();
|
|
697
|
+
const now = (env && env.now) || new Date();
|
|
698
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now);
|
|
535
699
|
const err = (stdio && stdio.stderr) || process.stderr;
|
|
700
|
+
const out = (stdio && stdio.stdout) || process.stdout;
|
|
701
|
+
|
|
702
|
+
// v2.0.0-rc.33 W2-5 (P1-8): cooldown gate. When configured > 0 hours, the
|
|
703
|
+
// broad banner stays silent for that many hours after a successful emit.
|
|
704
|
+
// 0 (default) preserves rc.32 behavior — every SessionStart re-fires the
|
|
705
|
+
// banner. Test seam env.skipCooldown bypasses for unit tests.
|
|
706
|
+
const cooldownHours = readBroadCooldownHours(cwd);
|
|
707
|
+
if (cooldownHours > 0 && !(env && env.skipCooldown === true)) {
|
|
708
|
+
const lastEmitMs = readBroadLastEmit(cwd);
|
|
709
|
+
if (
|
|
710
|
+
typeof lastEmitMs === "number" &&
|
|
711
|
+
nowMs - lastEmitMs < cooldownHours * MS_PER_HOUR
|
|
712
|
+
) {
|
|
713
|
+
return; // still in cooldown — silent
|
|
714
|
+
}
|
|
715
|
+
}
|
|
536
716
|
|
|
537
717
|
// Test seam: env.payload short-circuits the CLI spawn so unit tests can
|
|
538
718
|
// feed canned plan-context-hint JSON without depending on a built CLI.
|
|
@@ -540,6 +720,16 @@ function main(env, stdio) {
|
|
|
540
720
|
env && env.payload !== undefined ? env.payload : invokePlanContextHint(cwd);
|
|
541
721
|
if (payload === null || payload === undefined) return; // silent
|
|
542
722
|
|
|
723
|
+
// v2.0.0-rc.33 W2-1 (P0-9): apply TopK slice BEFORE renderSummary so the
|
|
724
|
+
// grouped/truncation rendering operates on the bounded set. Slicing here
|
|
725
|
+
// (not inside renderSummary) keeps the formatter pure — it never has to
|
|
726
|
+
// know about the cap.
|
|
727
|
+
const topK = readBroadTopK(cwd);
|
|
728
|
+
const slicedPayload =
|
|
729
|
+
payload && Array.isArray(payload.entries) && payload.entries.length > topK
|
|
730
|
+
? { ...payload, entries: payload.entries.slice(0, topK) }
|
|
731
|
+
: payload;
|
|
732
|
+
|
|
543
733
|
// rc.8 underseed self-check: decide whether to surface the one-line
|
|
544
734
|
// `/fabric-import` recommendation banner alongside the broad summary.
|
|
545
735
|
const recommendImport = shouldRecommendImport(cwd);
|
|
@@ -548,8 +738,10 @@ function main(env, stdio) {
|
|
|
548
738
|
// SessionStart fire (Skill-style progressive disclosure). The prior
|
|
549
739
|
// revision_hash cooldown gate (rc.7 T8 — rc.11) was removed because
|
|
550
740
|
// compact/clear-triggered SessionStart re-fires must re-inject the menu
|
|
551
|
-
// for the agent's working memory.
|
|
552
|
-
|
|
741
|
+
// for the agent's working memory. rc.33 W2-5 reintroduces an opt-in
|
|
742
|
+
// hours-based cooldown via fabric-config (see gate above).
|
|
743
|
+
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
744
|
+
const lines = renderSummary(slicedPayload, summaryMaxLen);
|
|
553
745
|
|
|
554
746
|
if (recommendImport) {
|
|
555
747
|
// rc.16 TASK-003: resolve fabric_language ONCE per invocation (only when
|
|
@@ -562,9 +754,52 @@ function main(env, stdio) {
|
|
|
562
754
|
|
|
563
755
|
if (lines.length === 0) return; // nothing to say — silent exit
|
|
564
756
|
|
|
757
|
+
// Stderr: always emit (human-facing breadcrumb + legacy contract).
|
|
565
758
|
for (const line of lines) {
|
|
566
759
|
err.write(`${line}\n`);
|
|
567
760
|
}
|
|
761
|
+
|
|
762
|
+
// v2.0.0-rc.33 W2-6 (P0-7): stdout JSON envelope. When
|
|
763
|
+
// hint_reminder_to_context is true (default), serialize the same banner
|
|
764
|
+
// body as Claude Code's SessionStart hookSpecificOutput shape so the model
|
|
765
|
+
// receives the reminder IN-CONTEXT (rc.32 baseline cite-coverage 3.1%
|
|
766
|
+
// root cause: reminders never entered model context). Stderr stays the
|
|
767
|
+
// host-facing channel.
|
|
768
|
+
//
|
|
769
|
+
// Failure to write JSON envelope must NOT crash the hook — stderr already
|
|
770
|
+
// delivered, the stdout layer is best-effort.
|
|
771
|
+
// v2.0.0-rc.33 W4 review-fix (gemini High-1): the stdout JSON envelope
|
|
772
|
+
// is Claude Code-specific (hookSpecificOutput.additionalContext contract).
|
|
773
|
+
// Codex CLI / Cursor don't parse it — leaking it to their stdout risks
|
|
774
|
+
// either polluting the terminal or crashing the host's hook-parsing
|
|
775
|
+
// pipeline. CLAUDE_PROJECT_DIR is set by CC when invoking hooks (see
|
|
776
|
+
// packages/cli/templates/hooks/configs/claude-code.json sigil paths);
|
|
777
|
+
// its presence is the single-bit "this is Claude Code" signal.
|
|
778
|
+
const isClaudeCode =
|
|
779
|
+
typeof process.env.CLAUDE_PROJECT_DIR === "string" &&
|
|
780
|
+
process.env.CLAUDE_PROJECT_DIR.length > 0;
|
|
781
|
+
const reminderToContext = readReminderToContext(cwd) && isClaudeCode;
|
|
782
|
+
if (reminderToContext && !(env && env.skipStdout === true)) {
|
|
783
|
+
try {
|
|
784
|
+
const envelope = {
|
|
785
|
+
hookSpecificOutput: {
|
|
786
|
+
hookEventName: "SessionStart",
|
|
787
|
+
additionalContext: lines.join("\n"),
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
out.write(`${JSON.stringify(envelope)}\n`);
|
|
791
|
+
} catch {
|
|
792
|
+
// Best-effort — stderr is the durable contract
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// v2.0.0-rc.33 W2-5 (P1-8): record successful emit timestamp for the
|
|
797
|
+
// cooldown gate's next-invocation check. Skip when cooldown is disabled
|
|
798
|
+
// (cooldownHours === 0) to avoid polluting the FS with a never-read
|
|
799
|
+
// sidecar on rc.32-style "no cooldown" workspaces.
|
|
800
|
+
if (cooldownHours > 0 && !(env && env.skipCooldownWrite === true)) {
|
|
801
|
+
writeBroadLastEmit(cwd, nowMs);
|
|
802
|
+
}
|
|
568
803
|
} catch {
|
|
569
804
|
// Silent — never block session start on hook failure.
|
|
570
805
|
}
|
|
@@ -583,16 +818,28 @@ module.exports = {
|
|
|
583
818
|
readUnderseedThreshold,
|
|
584
819
|
isImportTouched,
|
|
585
820
|
shouldRecommendImport,
|
|
821
|
+
// v2.0.0-rc.33 W2-1 / W2-5 / W2-6 helpers.
|
|
822
|
+
readBroadTopK,
|
|
823
|
+
readBroadCooldownHours,
|
|
824
|
+
readReminderToContext,
|
|
825
|
+
readBroadLastEmit,
|
|
826
|
+
writeBroadLastEmit,
|
|
827
|
+
readSummaryMaxLen,
|
|
586
828
|
CONSTANTS: {
|
|
587
829
|
TRUNCATION_THRESHOLD,
|
|
588
830
|
CLI_TIMEOUT_MS,
|
|
589
|
-
SUMMARY_MAX_LEN,
|
|
831
|
+
SUMMARY_MAX_LEN: DEFAULT_SUMMARY_MAX_LEN,
|
|
832
|
+
DEFAULT_SUMMARY_MAX_LEN,
|
|
590
833
|
CANONICAL_TYPE_ORDER,
|
|
591
834
|
MATURITY_PROVEN,
|
|
592
835
|
MATURITY_VERIFIED,
|
|
593
836
|
MATURITY_DRAFT,
|
|
594
837
|
DEFAULT_UNDERSEED_NODE_THRESHOLD,
|
|
595
838
|
KNOWLEDGE_CANONICAL_TYPES,
|
|
839
|
+
DEFAULT_HINT_BROAD_TOP_K,
|
|
840
|
+
DEFAULT_HINT_BROAD_COOLDOWN_HOURS,
|
|
841
|
+
DEFAULT_HINT_REMINDER_TO_CONTEXT,
|
|
842
|
+
HINT_BROAD_LAST_EMIT_FILE,
|
|
596
843
|
},
|
|
597
844
|
};
|
|
598
845
|
|