@fenglimg/fabric-cli 2.0.0 → 2.0.1

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -5
  3. package/dist/chunk-BATF4PEJ.js +361 -0
  4. package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
  5. package/dist/chunk-D25XJ4BC.js +880 -0
  6. package/dist/chunk-MF3OTILQ.js +544 -0
  7. package/dist/chunk-PWLW3B57.js +18 -0
  8. package/dist/config-XJIPZNUP.js +13 -0
  9. package/dist/doctor-EJDSEJSS.js +810 -0
  10. package/dist/index.js +15 -8
  11. package/dist/{init-BIRSIOXO.js → install-EKWMFLUU.js} +622 -711
  12. package/dist/metrics-ACEQFPDU.js +122 -0
  13. package/dist/onboard-coverage-MFCAEBDO.js +220 -0
  14. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
  15. package/dist/uninstall-MH7ZIB6M.js +1064 -0
  16. package/package.json +30 -5
  17. package/templates/hooks/cite-policy-evict.cjs +231 -0
  18. package/templates/hooks/configs/README.md +29 -6
  19. package/templates/hooks/configs/claude-code.json +14 -3
  20. package/templates/hooks/configs/codex-hooks.json +6 -3
  21. package/templates/hooks/configs/cursor-hooks.json +8 -10
  22. package/templates/hooks/fabric-hint.cjs +833 -105
  23. package/templates/hooks/knowledge-hint-broad.cjs +509 -135
  24. package/templates/hooks/knowledge-hint-narrow.cjs +791 -26
  25. package/templates/hooks/lib/banner-i18n.cjs +309 -0
  26. package/templates/hooks/lib/cite-contract-reminder.cjs +173 -0
  27. package/templates/hooks/lib/cite-line-parser.cjs +158 -0
  28. package/templates/hooks/lib/client-adapter.cjs +106 -0
  29. package/templates/hooks/lib/config-cache.cjs +107 -0
  30. package/templates/hooks/lib/state-store.cjs +84 -0
  31. package/templates/hooks/lib/summary-fallback.cjs +210 -0
  32. package/templates/skills/fabric-archive/SKILL.md +93 -419
  33. package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
  34. package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
  35. package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
  36. package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
  37. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
  38. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
  39. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
  40. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
  41. package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
  42. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
  43. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
  44. package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
  45. package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
  46. package/templates/skills/fabric-import/SKILL.md +75 -516
  47. package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
  48. package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
  49. package/templates/skills/fabric-import/ref/output-contract.md +61 -0
  50. package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
  51. package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
  52. package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
  53. package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
  54. package/templates/skills/fabric-review/SKILL.md +86 -284
  55. package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
  56. package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
  57. package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
  58. package/templates/skills/fabric-review/ref/output-contract.md +58 -0
  59. package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
  60. package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
  61. package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
  62. package/templates/skills/lib/shared-policy.md +69 -0
  63. package/dist/chunk-6ICJICVU.js +0 -10
  64. package/dist/chunk-74SZWYPH.js +0 -658
  65. package/dist/chunk-EYIDD2YS.js +0 -1000
  66. package/dist/doctor-T7JWODKG.js +0 -282
  67. package/dist/hooks-Y74Y5LQS.js +0 -12
  68. package/dist/scan-LMK3UCWL.js +0 -22
  69. package/dist/serve-H554BHLG.js +0 -124
  70. package/templates/agents-md/AGENTS.md.template +0 -59
  71. package/templates/bootstrap/CLAUDE.md +0 -8
  72. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  73. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -47,86 +47,255 @@
47
47
  */
48
48
 
49
49
  const { spawnSync } = require("node:child_process");
50
- const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("node:fs");
51
- const { dirname, join } = require("node:path");
50
+ const { existsSync, readdirSync, readFileSync } = require("node:fs");
51
+ const { join } = require("node:path");
52
+
53
+ // rc.16 TASK-003: shared banner-i18n lib (resolves fabric_language config and
54
+ // renders localized banner text). Mirror of the wiring in fabric-hint.cjs
55
+ // (TASK-002). Variant is resolved ONCE per main() invocation via
56
+ // readFabricLanguage(cwd) and threaded into renderBanner — no fs in render path.
57
+ const { renderBanner, readFabricLanguage } = require("./lib/banner-i18n.cjs");
58
+ const { resolveOpaqueSummaries } = require("./lib/summary-fallback.cjs");
59
+ // v2.0.0-rc.37 NEW-19: shared fabric-config reader + sidecar I/O. Replaces the
60
+ // five per-key readFileSync+parse config readers (one parse per fire now) and
61
+ // the bespoke last-emit sidecar helpers. The L78 "refactor into lib/ if a
62
+ // third hook needs it" note is now realised.
63
+ const {
64
+ readConfigNumber,
65
+ readConfigBoolean,
66
+ } = require("./lib/config-cache.cjs");
67
+ const { readTextState, writeTextState } = require("./lib/state-store.cjs");
68
+ // v2.0.0-rc.37 NEW-30: shared client detection (replaces the inline
69
+ // CLAUDE_PROJECT_DIR single-bit check below).
70
+ const { isClaudeCode } = require("./lib/client-adapter.cjs");
52
71
 
53
72
  // -----------------------------------------------------------------------------
54
- // rc.7 T8: SessionStart revision_hash gating.
55
- //
56
- // Q-14 problem: every SessionStart re-dumped the full broad knowledge list,
57
- // causing banner blindness. Solution: hash-of-canonical-graph gating — record
58
- // the last-emitted `payload.revision_hash` to a sidecar; on subsequent
59
- // SessionStart fires, compare. Match silent exit 0 (no re-dump). Mismatch
60
- // (canonical/ corpus changed planContext bumps revision_hash) emit AND
61
- // update sidecar.
62
- //
63
- // The revision_hash is supplied by `fabric plan-context-hint --all`'s JSON
64
- // payload (carried in payload.revision_hash since rc.5). Reusing the existing
65
- // hash primitive keeps the gating predicate exactly aligned with the "is the
66
- // knowledge graph different from last time?" question — no second hashing
67
- // scheme to maintain. computeRevisionHash() is not needed at this layer; we
68
- // compare the strings the CLI hands us.
69
- //
70
- // rc.7 T1 (sentinel hand-off) overrides this gate: a `.fabric/.import-requested`
71
- // sentinel forces emission regardless of revision_hash, because the user has
72
- // asked (via `fabric init` Y-confirm) for the import recommendation to surface
73
- // on next SessionStart. That branch is layered on top in main() — see T1
74
- // implementation.
73
+ // rc.12: SessionStart broad-menu is now unconditionally emitted on every
74
+ // SessionStart fire (matching Skill-style progressive disclosure). Prior
75
+ // versions (rc.5-rc.11) wrote `.fabric/.cache/sessionstart-last-hash` as a
76
+ // revision_hash cooldown sidecar to suppress re-emission on unchanged
77
+ // knowledge graphs; that gate was removed in rc.12. Orphaned sidecar files
78
+ // on existing dogfood repos are harmless dead state and are intentionally
79
+ // NOT cleaned up (zero-user clean-slate no migration logic needed).
75
80
  // -----------------------------------------------------------------------------
76
81
 
77
82
  const FABRIC_DIR_REL = ".fabric";
78
- const SESSIONSTART_HASH_CACHE_FILE = join(".fabric", ".cache", "sessionstart-last-hash");
83
+
84
+ // rc.8 underseed self-check constants (mirror fabric-hint.cjs ~line 76 / 83).
85
+ // Intentionally duplicated inline — hooks are independent .cjs files and
86
+ // cannot `require` each other. If a third hook ever needs the same logic,
87
+ // refactor into packages/cli/templates/hooks/lib/. Keep these values in sync
88
+ // with packages/cli/templates/hooks/fabric-hint.cjs.
89
+ const AGENTS_META_FILE = "agents.meta.json";
90
+ const IMPORT_STATE_FILE = ".import-state.json";
91
+ const KNOWLEDGE_CANONICAL_TYPES = [
92
+ "decisions",
93
+ "pitfalls",
94
+ "guidelines",
95
+ "models",
96
+ "processes",
97
+ ];
98
+ const DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
99
+
100
+ // v2.0.0-rc.33 W2-1 (P0-9): TopK upper bound on broad-scoped entries surfaced
101
+ // per SessionStart fire. Keeps the banner inside ~1 screenful so the agent
102
+ // actually reads the top-priority entries instead of triaging a wall of text.
103
+ // Overridable via fabric-config.json#hint_broad_top_k (range 1..50).
104
+ const DEFAULT_HINT_BROAD_TOP_K = 8;
105
+
106
+ // v2.0.0-rc.33 W2-5 (P1-8): cooldown (in hours) between broad-hint re-emits.
107
+ // Default 0 preserves rc.32 behavior — every SessionStart re-fires the banner.
108
+ // Cache key uses a separate sidecar from the fabric-hint Signal A/B/C cache
109
+ // so the two cooldowns don't interfere.
110
+ const DEFAULT_HINT_BROAD_COOLDOWN_HOURS = 0;
111
+ const MS_PER_HOUR = 60 * 60 * 1000;
112
+ // v2.0.0-rc.37 NEW-19: state-store resolves this basename under .fabric/.cache/.
113
+ const HINT_BROAD_LAST_EMIT_FILE_NAME = "knowledge-hint-broad-last-emit";
114
+
115
+ // v2.0.0-rc.33 W2-6 (P0-7): when true, emit banner as
116
+ // hookSpecificOutput.additionalContext JSON on stdout (Claude Code PreToolUse
117
+ // contract) so the model receives the reminder in-context. Stderr remains the
118
+ // human-facing channel for logs / breadcrumbs.
119
+ const DEFAULT_HINT_REMINDER_TO_CONTEXT = true;
120
+
121
+ // -----------------------------------------------------------------------------
122
+ // rc.8 underseed self-check helpers.
123
+ //
124
+ // These three helpers (countCanonicalNodes / readUnderseedThreshold /
125
+ // isImportTouched) are inline copies of the equivalent logic in
126
+ // packages/cli/templates/hooks/fabric-hint.cjs (~lines 218 / 749). Hooks
127
+ // cannot `require` each other (each .cjs is rendered as a standalone template
128
+ // at init time), so duplication is the documented convention. Cross-reference:
129
+ // keep both copies in sync; if a third hook needs the same logic, extract to
130
+ // packages/cli/templates/hooks/lib/.
131
+ // -----------------------------------------------------------------------------
79
132
 
80
133
  /**
81
- * Read the previously-emitted revision_hash from
82
- * `.fabric/.cache/sessionstart-last-hash`. Missing file / read failure /
83
- * empty file null (treat as "no prior emit", forces re-emit).
134
+ * Count canonical knowledge entries across the five canonical type subdirs
135
+ * (decisions / pitfalls / guidelines / models / processes). Pending entries
136
+ * are NOT counted they are proposals, not seeded knowledge.
84
137
  *
85
- * NEVER throws best-effort read.
138
+ * Returns the integer count. ENOENT / unreadable subdir → silently treated as
139
+ * zero (preserves never-block-on-failure invariant). Filters on `.md` suffix
140
+ * only; the more-precise canonical filename pattern check is owned by
141
+ * doctor.ts (the hook is a coarse signal, not a lint).
86
142
  */
87
- function readSessionStartLastHash(projectRoot) {
88
- try {
89
- const p = join(projectRoot, SESSIONSTART_HASH_CACHE_FILE);
90
- if (!existsSync(p)) return null;
91
- const raw = readFileSync(p, "utf8").trim();
92
- return raw.length > 0 ? raw : null;
93
- } catch {
94
- return null;
143
+ function countCanonicalNodes(projectRoot) {
144
+ const knowledgeRoot = join(projectRoot, FABRIC_DIR_REL, "knowledge");
145
+ if (!existsSync(knowledgeRoot)) {
146
+ return 0;
95
147
  }
148
+ let count = 0;
149
+ for (const type of KNOWLEDGE_CANONICAL_TYPES) {
150
+ const typeDir = join(knowledgeRoot, type);
151
+ if (!existsSync(typeDir)) continue;
152
+ let entries;
153
+ try {
154
+ entries = readdirSync(typeDir);
155
+ } catch {
156
+ continue;
157
+ }
158
+ for (const entry of entries) {
159
+ if (entry.endsWith(".md")) {
160
+ count += 1;
161
+ }
162
+ }
163
+ }
164
+ return count;
165
+ }
166
+
167
+ /**
168
+ * Resolve the underseed-node threshold from .fabric/fabric-config.json
169
+ * (underseed_node_threshold), falling back to DEFAULT_UNDERSEED_NODE_THRESHOLD.
170
+ * Any read/parse failure → default (never block on config errors).
171
+ */
172
+ function readUnderseedThreshold(projectRoot) {
173
+ // > 0 guard via min: Number.MIN_VALUE (any positive). config-cache returns
174
+ // the parsed number when finite & in-range, else the default.
175
+ return readConfigNumber(projectRoot, "underseed_node_threshold", DEFAULT_UNDERSEED_NODE_THRESHOLD, {
176
+ min: Number.MIN_VALUE,
177
+ });
178
+ }
179
+
180
+ /**
181
+ * v2.0.0-rc.33 W2-1: resolve hint_broad_top_k from fabric-config.json. Slices
182
+ * the broad entry list to TopK before group/truncation render. Validates the
183
+ * schema's 1..50 range inline so a malformed config silently falls back.
184
+ */
185
+ function readBroadTopK(projectRoot) {
186
+ return readConfigNumber(projectRoot, "hint_broad_top_k", DEFAULT_HINT_BROAD_TOP_K, {
187
+ min: 1,
188
+ max: 50,
189
+ floor: true,
190
+ });
191
+ }
192
+
193
+ /**
194
+ * v2.0.0-rc.33 W2-5: resolve hint_broad_cooldown_hours. Schema clamps 0..168;
195
+ * 0 means "no cooldown" (re-emit on every SessionStart, rc.32 behavior).
196
+ */
197
+ function readBroadCooldownHours(projectRoot) {
198
+ return readConfigNumber(projectRoot, "hint_broad_cooldown_hours", DEFAULT_HINT_BROAD_COOLDOWN_HOURS, {
199
+ min: 0,
200
+ max: 168,
201
+ });
202
+ }
203
+
204
+ /**
205
+ * v2.0.0-rc.33 W2-6: resolve hint_reminder_to_context. Boolean flag — when
206
+ * true (default) the hook writes a Claude-Code-shaped JSON envelope to stdout
207
+ * carrying the banner under hookSpecificOutput.additionalContext so the model
208
+ * receives the reminder in-context. Stderr stays informational either way.
209
+ */
210
+ function readReminderToContext(projectRoot) {
211
+ return readConfigBoolean(projectRoot, "hint_reminder_to_context", DEFAULT_HINT_REMINDER_TO_CONTEXT);
212
+ }
213
+
214
+ /**
215
+ * v2.0.0-rc.33 W2-5: read/write the broad-hint last-emit timestamp sidecar.
216
+ * Distinct from fabric-hint's shown-cache so signal cooldowns stay isolated.
217
+ * Returns epoch ms or null when missing/unreadable.
218
+ */
219
+ function readBroadLastEmit(projectRoot) {
220
+ const raw = readTextState(projectRoot, HINT_BROAD_LAST_EMIT_FILE_NAME);
221
+ if (raw === null || raw.length === 0) return null;
222
+ const asNum = Number(raw);
223
+ if (Number.isFinite(asNum) && asNum > 0) return asNum;
224
+ const ms = Date.parse(raw);
225
+ if (Number.isFinite(ms)) return ms;
226
+ return null;
227
+ }
228
+
229
+ function writeBroadLastEmit(projectRoot, nowMs) {
230
+ // Silent — sidecar failure must never block session start.
231
+ writeTextState(projectRoot, HINT_BROAD_LAST_EMIT_FILE_NAME, String(nowMs));
96
232
  }
97
233
 
98
234
  /**
99
- * Write `hash` to `.fabric/.cache/sessionstart-last-hash` so subsequent
100
- * SessionStart fires can compare. Creates the directory if missing.
101
- * Best-effort: any write failure is swallowed so a read-only .fabric/
102
- * never blocks session start.
235
+ * Classify the on-disk import lifecycle by reading
236
+ * `.fabric/.import-state.json`. Returns one of:
237
+ * - 'absent' — state file missing user has NEVER started import
238
+ * - 'in_progress' file present, phase is anything that is not 'complete'
239
+ * (covers 'P1-done', 'P2-done', 'phase 1', 'in_progress',
240
+ * '1', and any other live-import marker)
241
+ * - 'complete' — file present and phase === 'complete'
242
+ * - 'error' — file present but unreadable / unparseable JSON
243
+ *
244
+ * Recommendation rule (see shouldRecommendImport): only 'absent' triggers a
245
+ * banner — both 'in_progress' (user is actively importing) and 'complete'
246
+ * (user already imported) suppress the banner. 'error' also suppresses
247
+ * (defensive: do not nag when state is unreadable, the user has clearly
248
+ * touched the file).
103
249
  */
104
- function writeSessionStartLastHash(projectRoot, hash) {
250
+ function isImportTouched(projectRoot) {
251
+ const statePath = join(projectRoot, FABRIC_DIR_REL, IMPORT_STATE_FILE);
252
+ if (!existsSync(statePath)) return "absent";
253
+ let raw;
254
+ try {
255
+ raw = readFileSync(statePath, "utf8");
256
+ } catch {
257
+ return "error";
258
+ }
259
+ let parsed;
105
260
  try {
106
- if (typeof hash !== "string" || hash.length === 0) return;
107
- const p = join(projectRoot, SESSIONSTART_HASH_CACHE_FILE);
108
- mkdirSync(dirname(p), { recursive: true });
109
- writeFileSync(p, hash, "utf8");
261
+ parsed = JSON.parse(raw);
110
262
  } catch {
111
- // Silent — sidecar failure must never block session start.
263
+ return "error";
112
264
  }
265
+ if (!parsed || typeof parsed !== "object") return "error";
266
+ return parsed.phase === "complete" ? "complete" : "in_progress";
113
267
  }
114
268
 
115
269
  /**
116
- * rc.7 T1 sentinel pickup: `.fabric/.import-requested` is an empty marker
117
- * file written by `fabric init` (clack.confirm Y answer) signalling that
118
- * the user wants the next SessionStart to recommend `fabric-import`.
270
+ * rc.8 underseed self-check: determine whether the SessionStart hook should
271
+ * surface the one-line `/fabric-import` recommendation banner.
119
272
  *
120
- * When the sentinel is present, the gate is overridden — the broad-injection
121
- * banner is appended with the import recommendation line and the
122
- * revision_hash gate is bypassed entirely (we always want to surface the
123
- * recommendation until the import Skill clears the sentinel).
273
+ * Three-condition truth table (ALL must hold to return true):
274
+ * 1. `.fabric/agents.meta.json` exists
275
+ * (workspace has been `fabric init`-ed; otherwise the recommendation
276
+ * is meaningless `fabric-import` requires init's baseline scan).
277
+ * 2. countCanonicalNodes(cwd) < readUnderseedThreshold(cwd)
278
+ * (knowledge graph is sparse — import would meaningfully enrich it).
279
+ * 3. isImportTouched(cwd) === 'absent'
280
+ * (.import-state.json is missing entirely; user has neither started
281
+ * nor completed an import. ANY phase value — including 'in_progress'
282
+ * and 'complete' — returns false because the user has either started
283
+ * or finished.)
124
284
  *
125
- * Best-effort presence check. NEVER throws.
285
+ * Best-effort: any unexpected error → return false (do not nag on faults).
126
286
  */
127
- function isImportRequestedSentinelPresent(projectRoot) {
287
+ function shouldRecommendImport(projectRoot) {
128
288
  try {
129
- return existsSync(join(projectRoot, FABRIC_DIR_REL, ".import-requested"));
289
+ const metaPath = join(projectRoot, FABRIC_DIR_REL, AGENTS_META_FILE);
290
+ if (!existsSync(metaPath)) return false;
291
+
292
+ const threshold = readUnderseedThreshold(projectRoot);
293
+ const nodeCount = countCanonicalNodes(projectRoot);
294
+ if (nodeCount >= threshold) return false;
295
+
296
+ if (isImportTouched(projectRoot) !== "absent") return false;
297
+
298
+ return true;
130
299
  } catch {
131
300
  return false;
132
301
  }
@@ -136,12 +305,16 @@ function isImportRequestedSentinelPresent(projectRoot) {
136
305
  // CONSTANTS
137
306
  // -----------------------------------------------------------------------------
138
307
 
139
- // Per-type truncation triggers when total narrow entries > 30. The threshold
140
- // was originally aligned with the rc.5 plan-context degenerate-mode cutoff,
141
- // which is now retired (rc.7 T9 see docs/decisions/rc5-a3-superseded.md).
142
- // We keep 30 here as a stable rendering boundary independent of that protocol
143
- // change: it's a UI-density choice, not a wire-shape one.
144
- const TRUNCATION_THRESHOLD = 30;
308
+ // Per-type truncation triggers when total broad-scope entries > N.
309
+ // v2.0.0-rc.29 TASK-007 (BUG-F1): lowered from 30 → 12. SessionStart hint
310
+ // should bias toward "is there anything relevant?" rather than "exhaustive
311
+ // index" at 30, the banner consumed several terminal screens on
312
+ // well-seeded repos and operators reported scroll fatigue. 12 keeps a
313
+ // dense-enough scan (still fits "top hits per type" in 1-2 screenfuls)
314
+ // without prompting the user to mentally truncate themselves. The constant
315
+ // stays a stable rendering boundary; downstream consumers (banner-i18n.cjs,
316
+ // truncation summary lines) consume it as a single source of truth.
317
+ const TRUNCATION_THRESHOLD = 12;
145
318
 
146
319
  // `fabric plan-context-hint` is a thin wrapper over planContext(); on a
147
320
  // well-seeded repo it returns in ~100ms. Two-second cap is defensive — any
@@ -150,8 +323,17 @@ const CLI_TIMEOUT_MS = 2000;
150
323
 
151
324
  // Maximum summary length per entry. Keeps each line bounded so stderr does
152
325
  // not blow up terminal width with multi-paragraph summaries from sloppy
153
- // pending entries. Truncation appends an ellipsis.
154
- const SUMMARY_MAX_LEN = 80;
326
+ // pending entries. Truncation appends an ellipsis. v2.0.0-rc.33 W4-A3:
327
+ // `hint_summary_max_len` in fabric-config overrides this default (range 40..240).
328
+ const DEFAULT_SUMMARY_MAX_LEN = 80;
329
+
330
+ function readSummaryMaxLen(projectRoot) {
331
+ return readConfigNumber(projectRoot, "hint_summary_max_len", DEFAULT_SUMMARY_MAX_LEN, {
332
+ min: 40,
333
+ max: 240,
334
+ floor: true,
335
+ });
336
+ }
155
337
 
156
338
  // Canonical type order — render groups in this sequence so output is stable
157
339
  // across runs (Object.keys iteration order is insertion order, but the JSON
@@ -172,6 +354,13 @@ const MATURITY_PROVEN = "proven";
172
354
  const MATURITY_VERIFIED = "verified";
173
355
  const MATURITY_DRAFT = "draft";
174
356
 
357
+ // rc.8 underseed self-check banner: single line, emoji-prefixed (cf.
358
+ // fabric-hint.cjs Signal C `📋 Fabric:`). rc.16 TASK-003 routed the literal
359
+ // through the banner-i18n lib (key: 'broadImportBanner') — see main() below
360
+ // for the renderBanner call site. Substring contracts preserved across all
361
+ // variants: leading two-space indent, `📋 Fabric:` prefix, `/fabric-import`
362
+ // verbatim token (asserted by knowledge-hint-broad.test.ts).
363
+
175
364
  // -----------------------------------------------------------------------------
176
365
  // CLI invocation
177
366
  // -----------------------------------------------------------------------------
@@ -180,12 +369,16 @@ const MATURITY_DRAFT = "draft";
180
369
  * Spawn `fabric plan-context-hint --all` and return parsed JSON. Returns
181
370
  * null on any failure (ENOENT, non-zero exit, malformed JSON). Never throws.
182
371
  *
183
- * spawn strategy: try `fabric` first (user-PATH install) then `fab` (the
184
- * alternate bin name shipped by @fenglimg/fabric-cli). If neither is on PATH,
185
- * return null — the hook stays silent rather than nagging about install state.
372
+ * If `fabric` is not on PATH, return null the hook stays silent rather
373
+ * than nagging about install state.
186
374
  */
187
375
  function invokePlanContextHint(cwd) {
188
- const candidates = ["fabric", "fab"];
376
+ const candidates = ["fabric"];
377
+ // rc.31 NEW-6: capture the last meaningful failure so we can surface it on
378
+ // stderr before fail-open. Without this, hook silently swallows backend
379
+ // crashes (e.g. agents_meta_invalid → plan-context-hint exits with stderr
380
+ // payload and the AI / user never sees KB chain is dead).
381
+ let lastFailure = null;
189
382
  for (const bin of candidates) {
190
383
  let res;
191
384
  try {
@@ -198,17 +391,37 @@ function invokePlanContextHint(cwd) {
198
391
  } catch {
199
392
  continue; // spawn throw (extremely rare) — try next candidate
200
393
  }
201
- // ENOENT surfaces as error on the result object.
202
- if (res.error || res.status === null || res.status !== 0) continue;
394
+ // ENOENT surfaces as error on the result object. Skip silently for ENOENT
395
+ // (bin not installed is the only legitimate reason to bail).
396
+ if (res.error) {
397
+ if (res.error.code !== "ENOENT") {
398
+ lastFailure = { bin, reason: String(res.error.message || res.error.code || res.error) };
399
+ }
400
+ continue;
401
+ }
402
+ if (res.status === null || res.status !== 0) {
403
+ const stderrSnip = (res.stderr || "").trim().slice(0, 240);
404
+ if (stderrSnip.length > 0) {
405
+ lastFailure = { bin, reason: stderrSnip };
406
+ }
407
+ continue;
408
+ }
203
409
  const raw = (res.stdout || "").trim();
204
410
  if (raw.length === 0) continue;
205
411
  try {
206
412
  const parsed = JSON.parse(raw);
207
413
  if (parsed && typeof parsed === "object") return parsed;
208
- } catch {
209
- // malformed JSON try next bin (unlikely to differ, but no harm)
414
+ } catch (err) {
415
+ lastFailure = { bin, reason: `malformed JSON from plan-context-hint: ${String(err && err.message || err)}` };
210
416
  }
211
417
  }
418
+ if (lastFailure !== null) {
419
+ // Single warning line — never throws, never blocks the hook. Lets users /
420
+ // AI notice that the KB chain is degraded instead of being silently empty.
421
+ process.stderr.write(
422
+ `[fabric-hint] plan-context-hint (${lastFailure.bin}) failed: ${lastFailure.reason.replace(/\n/g, " ")}\n`,
423
+ );
424
+ }
212
425
  return null;
213
426
  }
214
427
 
@@ -249,17 +462,21 @@ function groupEntries(narrow) {
249
462
  return { typeOrder, byType };
250
463
  }
251
464
 
252
- function truncateSummary(raw) {
465
+ // v2.0.0-rc.33 W4-A3: maxLen is now caller-supplied (sourced from
466
+ // fabric-config#hint_summary_max_len in main; tests + ad-hoc callers may
467
+ // omit to fall back to DEFAULT_SUMMARY_MAX_LEN).
468
+ function truncateSummary(raw, maxLen) {
253
469
  const s = typeof raw === "string" ? raw : "";
254
470
  // Collapse newlines / runs of whitespace so each entry fits one line.
255
471
  const flat = s.replace(/\s+/g, " ").trim();
256
- if (flat.length <= SUMMARY_MAX_LEN) return flat;
257
- return `${flat.slice(0, SUMMARY_MAX_LEN - 1)}…`;
472
+ const cap = typeof maxLen === "number" && maxLen > 0 ? maxLen : DEFAULT_SUMMARY_MAX_LEN;
473
+ if (flat.length <= cap) return flat;
474
+ return `${flat.slice(0, cap - 1)}…`;
258
475
  }
259
476
 
260
- function formatEntryLine(entry) {
477
+ function formatEntryLine(entry, maxLen) {
261
478
  const id = entry.id || "(no-id)";
262
- const summary = truncateSummary(entry.summary);
479
+ const summary = truncateSummary(entry.summary, maxLen);
263
480
  return summary.length > 0 ? ` - ${id} · ${summary}` : ` - ${id}`;
264
481
  }
265
482
 
@@ -268,7 +485,7 @@ function formatEntryLine(entry) {
268
485
  * Each entry gets one line: ` - <id> · <summary>`. Type/maturity headers
269
486
  * group the listing.
270
487
  */
271
- function renderFull(narrow) {
488
+ function renderFull(narrow, maxLen) {
272
489
  const { typeOrder, byType } = groupEntries(narrow);
273
490
  const lines = [];
274
491
  for (const type of typeOrder) {
@@ -287,7 +504,7 @@ function renderFull(narrow) {
287
504
  for (const maturity of maturities) {
288
505
  lines.push(` [${type}] (${maturity}):`);
289
506
  for (const entry of maturityMap.get(maturity)) {
290
- lines.push(formatEntryLine(entry));
507
+ lines.push(formatEntryLine(entry, maxLen));
291
508
  }
292
509
  }
293
510
  }
@@ -300,7 +517,7 @@ function renderFull(narrow) {
300
517
  * an inline id list (no summary); draft (and unknown) buckets collapse to a
301
518
  * count.
302
519
  */
303
- function renderTruncated(narrow) {
520
+ function renderTruncated(narrow, maxLen) {
304
521
  const { typeOrder, byType } = groupEntries(narrow);
305
522
  const lines = [];
306
523
  for (const type of typeOrder) {
@@ -311,7 +528,7 @@ function renderTruncated(narrow) {
311
528
  if (proven && proven.length > 0) {
312
529
  lines.push(` [${type}] proven (${proven.length}):`);
313
530
  for (const entry of proven) {
314
- lines.push(formatEntryLine(entry));
531
+ lines.push(formatEntryLine(entry, maxLen));
315
532
  }
316
533
  }
317
534
 
@@ -341,24 +558,83 @@ function renderTruncated(narrow) {
341
558
  *
342
559
  * Returns an array of lines (one stderr write per line keeps the formatter
343
560
  * trivial and testable). Returns [] when there is nothing meaningful to say
344
- * (empty narrow set) so callers know to stay silent.
561
+ * (empty entries set) so callers know to stay silent.
562
+ *
563
+ * Protocol v2 gate (rc.18): payloads must carry `version: 2`. A null/missing
564
+ * payload returns [] silently; a payload with a mismatched `version` returns []
565
+ * after writing exactly one stderr breadcrumb so operators grepping a stuck-
566
+ * banner report can diagnose the version drift without source-diving.
345
567
  */
346
- function renderSummary(payload) {
347
- const narrow = Array.isArray(payload && payload.narrow) ? payload.narrow : [];
348
- if (narrow.length === 0) return [];
349
-
350
- const truncated = narrow.length > TRUNCATION_THRESHOLD;
568
+ function renderSummary(payload, maxLen) {
569
+ if (!payload || payload.version !== 2) {
570
+ if (payload && payload.version !== undefined) {
571
+ try {
572
+ process.stderr.write(
573
+ `[fabric] hint payload version=${payload.version} unsupported (expected 2), skipping\n`,
574
+ );
575
+ } catch {}
576
+ }
577
+ return [];
578
+ }
579
+ // Protocol v2 (rc.18): the wire field is now `payload.entries`, matching what
580
+ // this renderer always called it locally. The historical `narrow` name (which
581
+ // degenerated in --all mode) has been retired without a compat shim per
582
+ // pre-user clean-slate policy.
583
+ const entries = Array.isArray(payload.entries) ? payload.entries : [];
584
+ if (entries.length === 0) return [];
585
+
586
+ const truncated = entries.length > TRUNCATION_THRESHOLD;
351
587
  const banner = truncated
352
- ? `[fabric] Session start — ${narrow.length} broad-scoped knowledge entries available (truncated):`
353
- : `[fabric] Session start — ${narrow.length} broad-scoped knowledge entries available:`;
588
+ ? `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available (truncated):`
589
+ : `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available:`;
354
590
 
355
- const body = truncated ? renderTruncated(narrow) : renderFull(narrow);
591
+ const body = truncated ? renderTruncated(entries, maxLen) : renderFull(entries, maxLen);
356
592
 
357
593
  const lines = [banner, ...body];
358
594
  const revHash = typeof payload.revision_hash === "string" ? payload.revision_hash : null;
359
595
  if (revHash !== null && revHash.length > 0) {
360
596
  lines.push(` revision_hash: ${revHash}`);
361
597
  }
598
+
599
+ // rc.22 Scope D T-D4 (TASK-011): meta auto-refresh breadcrumb. Emitted ONLY
600
+ // when the server's planContext() detected meta drift and rebuilt the meta
601
+ // in-place (auto_healed === true). One informational line — operators need
602
+ // a paper trail when revision_hash flips mid-session.
603
+ //
604
+ // Variant resolution:
605
+ // - Both hashes present → full transition line (`sha PREV → CUR`) with
606
+ // 8-char hex prefixes stripped of the `sha256:` scheme prefix.
607
+ // - auto_healed:true but previous_revision_hash missing → generic line
608
+ // (T10 noted the server may emit `auto_healed:true` alone if it lost
609
+ // the prior hash for any reason). Stays informational.
610
+ //
611
+ // i18n: routed through renderBanner so zh-CN / en / zh-CN-hybrid variants
612
+ // share one call site. fabric_language is resolved via readFabricLanguage()
613
+ // ONLY when the line is actually emitted (keeps the no-banner path free of
614
+ // the extra config read, matching the broadImportBanner site below).
615
+ if (payload.auto_healed === true) {
616
+ const variant = readFabricLanguage(process.cwd());
617
+ const prevRaw =
618
+ typeof payload.previous_revision_hash === "string"
619
+ ? payload.previous_revision_hash
620
+ : null;
621
+ const curRaw =
622
+ typeof payload.revision_hash === "string" ? payload.revision_hash : null;
623
+ if (prevRaw && curRaw) {
624
+ // Strip optional `sha256:` scheme prefix, then take first 8 hex chars.
625
+ const stripScheme = (h) =>
626
+ h.startsWith("sha256:") ? h.slice("sha256:".length) : h;
627
+ const prev = stripScheme(prevRaw).slice(0, 8);
628
+ const cur = stripScheme(curRaw).slice(0, 8);
629
+ lines.push(
630
+ renderBanner("metaAutoRefreshedBanner", variant, { prev, cur }),
631
+ );
632
+ } else {
633
+ // Defensive: auto_healed:true but no usable previous hash → generic line.
634
+ lines.push(renderBanner("metaAutoRefreshedBannerGeneric", variant, {}));
635
+ }
636
+ }
637
+
362
638
  lines.push(" Use `fab_get_knowledge_sections` to fetch full content.");
363
639
  return lines;
364
640
  }
@@ -371,7 +647,33 @@ function renderSummary(payload) {
371
647
  function main(env, stdio) {
372
648
  try {
373
649
  const cwd = (env && env.cwd) || process.cwd();
650
+ const now = (env && env.now) || new Date();
651
+ const nowMs = now instanceof Date ? now.getTime() : Number(now);
374
652
  const err = (stdio && stdio.stderr) || process.stderr;
653
+ const out = (stdio && stdio.stdout) || process.stdout;
654
+
655
+ // v2.0.0-rc.33 W2-5 (P1-8): cooldown gate. When configured > 0 hours, the
656
+ // broad banner stays silent for that many hours after a successful emit.
657
+ // 0 (default) preserves rc.32 behavior — every SessionStart re-fires the
658
+ // banner. Test seam env.skipCooldown bypasses for unit tests.
659
+ const cooldownHours = readBroadCooldownHours(cwd);
660
+ if (cooldownHours > 0 && !(env && env.skipCooldown === true)) {
661
+ const lastEmitMs = readBroadLastEmit(cwd);
662
+ if (
663
+ typeof lastEmitMs === "number" &&
664
+ // rc.34 TASK-01 + review-fix (Gemini P1): when lastEmit is in the
665
+ // FUTURE relative to now (backward clock skew — NTP sync /
666
+ // suspend-wake / TZ change), the gate fires immediately. Otherwise
667
+ // standard cooldown check. Math.max(0, …) was a no-op (silent for
668
+ // cooldown + |skew| under both formulations); this guard actually
669
+ // heals the skew on the next invocation by treating future-stamped
670
+ // sidecar as "expired."
671
+ nowMs >= lastEmitMs &&
672
+ nowMs - lastEmitMs < cooldownHours * MS_PER_HOUR
673
+ ) {
674
+ return; // still in cooldown — silent
675
+ }
676
+ }
375
677
 
376
678
  // Test seam: env.payload short-circuits the CLI spawn so unit tests can
377
679
  // feed canned plan-context-hint JSON without depending on a built CLI.
@@ -379,54 +681,113 @@ function main(env, stdio) {
379
681
  env && env.payload !== undefined ? env.payload : invokePlanContextHint(cwd);
380
682
  if (payload === null || payload === undefined) return; // silent
381
683
 
382
- // rc.7 T1: sentinel-override gate. When `.fabric/.import-requested` is
383
- // present, the import-recommendation banner ALWAYS surfaces regardless
384
- // of revision_hash equality the user asked for it on init Y-confirm
385
- // and the fabric-import Skill is responsible for clearing the sentinel
386
- // when its Phase 3 completes. The override sits BEFORE the gate so the
387
- // revision_hash cache is not updated either (we want the
388
- // recommendation to keep surfacing on subsequent boots until the user
389
- // actually runs import).
390
- const sentinelPresent = isImportRequestedSentinelPresent(cwd);
391
-
392
- // rc.7 T8: revision_hash gate. If the CLI payload carries a stable
393
- // revision_hash and it matches the previously-emitted hash recorded in
394
- // the sidecar, the knowledge graph is unchanged since last session
395
- // silent exit 0 (no re-dump). The sentinel override above takes
396
- // precedence and bypasses this gate.
397
- const currentHash =
398
- typeof payload.revision_hash === "string" ? payload.revision_hash : "";
399
- if (!sentinelPresent && currentHash.length > 0) {
400
- const lastHash = readSessionStartLastHash(cwd);
401
- if (lastHash !== null && lastHash === currentHash) {
402
- // Same canonical graph as last session — banner blindness mitigation.
403
- return;
684
+ // v2.0.0-rc.33 W2-1 (P0-9): apply TopK slice BEFORE renderSummary so the
685
+ // grouped/truncation rendering operates on the bounded set. Slicing here
686
+ // (not inside renderSummary) keeps the formatter pure it never has to
687
+ // know about the cap.
688
+ const topK = readBroadTopK(cwd);
689
+ const slicedPayload =
690
+ payload && Array.isArray(payload.entries) && payload.entries.length > topK
691
+ ? { ...payload, entries: payload.entries.slice(0, topK) }
692
+ : payload;
693
+
694
+ // rc.35 TASK-06 (P0-10.b): summary-fallback substitution. Entries whose
695
+ // description.summary equals stable_id render as "<id> · <id>" and the
696
+ // AI skips fetching them; the fallback reads `## Summary` from the
697
+ // entry's .md file and swaps in the first paragraph. Best-effort —
698
+ // failure leaves the original opaque summary untouched.
699
+ let resolvedPayload = slicedPayload;
700
+ try {
701
+ if (slicedPayload && Array.isArray(slicedPayload.entries)) {
702
+ const resolvedEntries = resolveOpaqueSummaries(
703
+ slicedPayload.entries,
704
+ cwd,
705
+ typeof slicedPayload.revision_hash === "string" ? slicedPayload.revision_hash : "",
706
+ );
707
+ resolvedPayload = { ...slicedPayload, entries: resolvedEntries };
404
708
  }
709
+ } catch {
710
+ // resolveOpaqueSummaries swallows its own errors; this catch is belt
711
+ // + suspenders for any unexpected exception from the lib layer.
405
712
  }
406
713
 
407
- const lines = renderSummary(payload);
408
-
409
- // rc.7 T1: when the sentinel is present, append the import-recommendation
410
- // banner. This line is appended whether or not the broad summary had
411
- // entries even an empty knowledge graph benefits from the prompt.
412
- if (sentinelPresent) {
413
- lines.push(
414
- " 📋 Fabric: 检测到 fabric init 提示要回灌知识 — 是否调 /fabric-import git 历史和现有文档抽取?",
415
- );
714
+ // rc.8 underseed self-check: decide whether to surface the one-line
715
+ // `/fabric-import` recommendation banner alongside the broad summary.
716
+ const recommendImport = shouldRecommendImport(cwd);
717
+
718
+ // rc.12: broad-summary body is unconditionally rendered on every
719
+ // SessionStart fire (Skill-style progressive disclosure). The prior
720
+ // revision_hash cooldown gate (rc.7 T8 — rc.11) was removed because
721
+ // compact/clear-triggered SessionStart re-fires must re-inject the menu
722
+ // for the agent's working memory. rc.33 W2-5 reintroduces an opt-in
723
+ // hours-based cooldown via fabric-config (see gate above).
724
+ const summaryMaxLen = readSummaryMaxLen(cwd);
725
+ const lines = renderSummary(resolvedPayload, summaryMaxLen);
726
+
727
+ // v2.0.0-rc.37 NEW-23: resolve fabric_language ONCE per emit path —
728
+ // shared between the (existing) broadImportBanner branch and the new
729
+ // 'next step' nudge tail added below. 'match-existing' / unknown variants
730
+ // fold to 'en' inside renderBanner per UX i18n Policy class 1.
731
+ const fabricLanguageForEmit = lines.length > 0 || recommendImport ? readFabricLanguage(cwd) : null;
732
+ if (recommendImport && fabricLanguageForEmit !== null) {
733
+ lines.push(renderBanner("broadImportBanner", fabricLanguageForEmit, {}));
416
734
  }
417
735
 
418
- if (lines.length === 0) return; // empty narrow set + no sentinel — silent
736
+ if (lines.length === 0) return; // nothing to say — silent exit
419
737
 
738
+ // v2.0.0-rc.37 NEW-23: SessionStart 索引末尾"下一步"引导。Tail line that
739
+ // tells the AI what to do with the broad index it just received. Without
740
+ // this, the model often parses the index and moves on without ever calling
741
+ // fab_recall / fab_plan_context. One-line nudge, bilingual.
742
+ const nextStepNudge =
743
+ fabricLanguageForEmit === "zh-CN"
744
+ ? "下一步: 调 fab_recall(paths) 拿 KB 相关条目;或调 fab_plan_context 先看候选 description_index。"
745
+ : "Next: call fab_recall(paths) to fetch related KB entries, or fab_plan_context to preview the description_index first.";
746
+ lines.push(nextStepNudge);
747
+
748
+ // Stderr: always emit (human-facing breadcrumb + legacy contract).
420
749
  for (const line of lines) {
421
750
  err.write(`${line}\n`);
422
751
  }
423
752
 
424
- // Update sidecar AFTER successful emit. We only persist the hash when
425
- // the gate actually let the dump through (i.e. when not sentinel-only).
426
- // Sentinel-only emits don't bump the cache so the next non-sentinel
427
- // SessionStart still gets to compare the prior session's true hash.
428
- if (!sentinelPresent && currentHash.length > 0) {
429
- writeSessionStartLastHash(cwd, currentHash);
753
+ // v2.0.0-rc.33 W2-6 (P0-7): stdout JSON envelope. When
754
+ // hint_reminder_to_context is true (default), serialize the same banner
755
+ // body as Claude Code's SessionStart hookSpecificOutput shape so the model
756
+ // receives the reminder IN-CONTEXT (rc.32 baseline cite-coverage 3.1%
757
+ // root cause: reminders never entered model context). Stderr stays the
758
+ // host-facing channel.
759
+ //
760
+ // Failure to write JSON envelope must NOT crash the hook — stderr already
761
+ // delivered, the stdout layer is best-effort.
762
+ // v2.0.0-rc.33 W4 review-fix (gemini High-1): the stdout JSON envelope
763
+ // is Claude Code-specific (hookSpecificOutput.additionalContext contract).
764
+ // Codex CLI / Cursor don't parse it — leaking it to their stdout risks
765
+ // either polluting the terminal or crashing the host's hook-parsing
766
+ // pipeline. CLAUDE_PROJECT_DIR is set by CC when invoking hooks (see
767
+ // packages/cli/templates/hooks/configs/claude-code.json sigil paths);
768
+ // its presence is the single-bit "this is Claude Code" signal (now via
769
+ // the shared client-adapter, rc.37 NEW-30).
770
+ const reminderToContext = readReminderToContext(cwd) && isClaudeCode();
771
+ if (reminderToContext && !(env && env.skipStdout === true)) {
772
+ try {
773
+ const envelope = {
774
+ hookSpecificOutput: {
775
+ hookEventName: "SessionStart",
776
+ additionalContext: lines.join("\n"),
777
+ },
778
+ };
779
+ out.write(`${JSON.stringify(envelope)}\n`);
780
+ } catch {
781
+ // Best-effort — stderr is the durable contract
782
+ }
783
+ }
784
+
785
+ // v2.0.0-rc.33 W2-5 (P1-8): record successful emit timestamp for the
786
+ // cooldown gate's next-invocation check. Skip when cooldown is disabled
787
+ // (cooldownHours === 0) to avoid polluting the FS with a never-read
788
+ // sidecar on rc.32-style "no cooldown" workspaces.
789
+ if (cooldownHours > 0 && !(env && env.skipCooldownWrite === true)) {
790
+ writeBroadLastEmit(cwd, nowMs);
430
791
  }
431
792
  } catch {
432
793
  // Silent — never block session start on hook failure.
@@ -441,20 +802,33 @@ module.exports = {
441
802
  renderTruncated,
442
803
  renderSummary,
443
804
  truncateSummary,
444
- // rc.7 T8: revision_hash gating sidecar helpers (exported for unit testing).
445
- readSessionStartLastHash,
446
- writeSessionStartLastHash,
447
- // rc.7 T1: sentinel-override pickup (exported for unit testing).
448
- isImportRequestedSentinelPresent,
805
+ // rc.8 underseed self-check helpers (exported for unit testing).
806
+ countCanonicalNodes,
807
+ readUnderseedThreshold,
808
+ isImportTouched,
809
+ shouldRecommendImport,
810
+ // v2.0.0-rc.33 W2-1 / W2-5 / W2-6 helpers.
811
+ readBroadTopK,
812
+ readBroadCooldownHours,
813
+ readReminderToContext,
814
+ readBroadLastEmit,
815
+ writeBroadLastEmit,
816
+ readSummaryMaxLen,
449
817
  CONSTANTS: {
450
818
  TRUNCATION_THRESHOLD,
451
819
  CLI_TIMEOUT_MS,
452
- SUMMARY_MAX_LEN,
820
+ SUMMARY_MAX_LEN: DEFAULT_SUMMARY_MAX_LEN,
821
+ DEFAULT_SUMMARY_MAX_LEN,
453
822
  CANONICAL_TYPE_ORDER,
454
823
  MATURITY_PROVEN,
455
824
  MATURITY_VERIFIED,
456
825
  MATURITY_DRAFT,
457
- SESSIONSTART_HASH_CACHE_FILE,
826
+ DEFAULT_UNDERSEED_NODE_THRESHOLD,
827
+ KNOWLEDGE_CANONICAL_TYPES,
828
+ DEFAULT_HINT_BROAD_TOP_K,
829
+ DEFAULT_HINT_BROAD_COOLDOWN_HOURS,
830
+ DEFAULT_HINT_REMINDER_TO_CONTEXT,
831
+ HINT_BROAD_LAST_EMIT_FILE_NAME,
458
832
  },
459
833
  };
460
834