@fenglimg/fabric-cli 2.2.0-rc.8 → 2.2.0
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-FEOPLBGA.js → chunk-3D7B2UAZ.js} +1 -2
- package/dist/{chunk-YM4XATJF.js → chunk-722JU5BP.js} +2 -0
- package/dist/{chunk-CMDW3PYK.js → chunk-7ZDXBOOU.js} +78 -0
- package/dist/{chunk-JTHWLUD3.js → chunk-E7HJUU34.js} +1 -1
- package/dist/{context-7NUKXDB6.js → context-UJCGYOT6.js} +1 -1
- package/dist/{doctor-REZDNH4A.js → doctor-MDTZWKBK.js} +2 -2
- package/dist/index.js +9 -9
- package/dist/{install-v2-2COC3DO3.js → install-v2-3KJX3YRO.js} +5 -3
- package/dist/{plan-context-hint-G75R4P4J.js → plan-context-hint-5TNGH3R4.js} +1 -1
- package/dist/{uninstall-62F4LNKI.js → uninstall-IFN2KYBK.js} +10 -1
- package/package.json +3 -3
- package/templates/hooks/fabric-hint.cjs +309 -102
- package/templates/hooks/knowledge-hint-broad.cjs +246 -173
- package/templates/hooks/knowledge-hint-narrow.cjs +8 -21
- package/templates/hooks/lib/banner-i18n.cjs +19 -1
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +13 -12
- package/templates/skills/fabric/SKILL.md +15 -9
- package/templates/skills/fabric-archive/SKILL.md +14 -0
- package/templates/skills/fabric-review/SKILL.md +9 -0
- package/templates/hooks/lib/summary-fallback.cjs +0 -273
|
@@ -16,12 +16,11 @@
|
|
|
16
16
|
*
|
|
17
17
|
* AI sink (additionalContext) — the dynamically generated "MEMORY.md":
|
|
18
18
|
* [fabric:SessionStart] <store>
|
|
19
|
-
* ALWAYS-ACTIVE RULES (
|
|
20
|
-
* [guideline] team:KT-GLD-0001
|
|
21
|
-
*
|
|
22
|
-
* REFERENCE (read on demand / fab_recall): # decision/pitfall/process — title + hook
|
|
19
|
+
* ALWAYS-ACTIVE RULES (unconditional · act on the line): # guideline/model, BROAD only
|
|
20
|
+
* [guideline] team:KT-GLD-0001 · <summary> # INDEX line; body on demand (KT-DEC-0036)
|
|
21
|
+
* REFERENCE (situational · Read when must_read_if fires): # decision/pitfall/process, BROAD
|
|
23
22
|
* [decision] team:KT-DEC-0001 — <must_read_if>
|
|
24
|
-
* … N more folded (broad index > backstop 50;
|
|
23
|
+
* … N more folded (broad index > backstop 50; prune via fabric-audit)
|
|
25
24
|
* Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md
|
|
26
25
|
*
|
|
27
26
|
* Human sink (systemMessage) — broad-only census breadcrumb; SessionStart is
|
|
@@ -54,7 +53,6 @@ const { appendLockedLine } = require("./lib/injection-log.cjs");
|
|
|
54
53
|
// (TASK-002). Variant is resolved ONCE per main() invocation via
|
|
55
54
|
// readFabricLanguage(cwd) and threaded into renderBanner — no fs in render path.
|
|
56
55
|
const { renderBanner, readFabricLanguage } = require("./lib/banner-i18n.cjs");
|
|
57
|
-
const { resolveOpaqueSummaries } = require("./lib/summary-fallback.cjs");
|
|
58
56
|
// v2.0.0-rc.37 NEW-19: shared fabric-config reader + sidecar I/O. Replaces the
|
|
59
57
|
// five per-key readFileSync+parse config readers (one parse per fire now) and
|
|
60
58
|
// the bespoke last-emit sidecar helpers. The L78 "refactor into lib/ if a
|
|
@@ -112,15 +110,23 @@ function readWorkspaceBindingId(cwd) {
|
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
function readSnapshotCanonicalCount(projectRoot) {
|
|
113
|
+
// No reader / not bound → degrade to an empty corpus (0), the documented
|
|
114
|
+
// store-only behavior (KT-DEC-0007). Only the "snapshot EXISTS but predates
|
|
115
|
+
// knowledge_store_dirs" case below is undeterminable → null (skip).
|
|
115
116
|
if (bindingsSnapshotReader === null) {
|
|
116
|
-
return
|
|
117
|
+
return 0;
|
|
117
118
|
}
|
|
118
119
|
const bindingId = readWorkspaceBindingId(projectRoot);
|
|
119
120
|
if (bindingId === null) {
|
|
120
|
-
return
|
|
121
|
+
return 0;
|
|
121
122
|
}
|
|
122
123
|
try {
|
|
123
124
|
const snapshot = bindingsSnapshotReader.readBindingsSnapshot(bindingId);
|
|
125
|
+
// No snapshot file at all → treat as an empty corpus (KT-DEC-0007),
|
|
126
|
+
// preserving the fresh-project underseed nudge.
|
|
127
|
+
if (!snapshot) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
124
130
|
// LIVE recount off the snapshot's resolved store dirs. The cached
|
|
125
131
|
// knowledge_stats.canonical_count is frozen at snapshot-write time and goes
|
|
126
132
|
// stale when store content syncs in out-of-band (e.g. the store grew from 1
|
|
@@ -128,13 +134,21 @@ function readSnapshotCanonicalCount(projectRoot) {
|
|
|
128
134
|
// THIS workspace's snapshot), which mis-fired the "knowledge sparse"
|
|
129
135
|
// underseed nudge (KT-PIT-0017, same stale-projection root cause).
|
|
130
136
|
const live = bindingsSnapshotReader.liveKnowledgeStats(snapshot);
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
// #3: a snapshot that predates knowledge_store_dirs makes liveKnowledgeStats
|
|
138
|
+
// return null — the count is undeterminable and the cached projection is
|
|
139
|
+
// unreliable. Return null (not 0) so countCanonicalNodes / shouldRecommendImport
|
|
140
|
+
// SKIP the nudge instead of false-firing on stale data; the snapshot
|
|
141
|
+
// self-heals on the next install/sync. A genuine live 0 (dirs present, no
|
|
142
|
+
// *.md) still returns 0 and fires correctly.
|
|
143
|
+
if (live === null) {
|
|
144
|
+
return null;
|
|
133
145
|
}
|
|
146
|
+
return Number.isFinite(live.canonicalCount) ? Math.floor(live.canonicalCount) : 0;
|
|
134
147
|
} catch {
|
|
135
|
-
//
|
|
148
|
+
// Read/parse fault → degrade to empty (0), preserving prior behavior. The
|
|
149
|
+
// only undeterminable→skip path is the explicit live===null above.
|
|
150
|
+
return 0;
|
|
136
151
|
}
|
|
137
|
-
return 0;
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
|
|
@@ -205,8 +219,10 @@ const DEFAULT_HINT_REMINDER_TO_CONTEXT = true;
|
|
|
205
219
|
* trees — a missing snapshot degrades to zero (KT-DEC-0007).
|
|
206
220
|
*/
|
|
207
221
|
function countCanonicalNodes(projectRoot) {
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
// #3: null = undeterminable (old snapshot lacking store dirs, or no binding
|
|
223
|
+
// context). Propagate it — shouldRecommendImport SKIPS on null rather than
|
|
224
|
+
// treating it as zero and false-firing the underseed nudge on a stale corpus.
|
|
225
|
+
return readSnapshotCanonicalCount(projectRoot);
|
|
210
226
|
}
|
|
211
227
|
|
|
212
228
|
/**
|
|
@@ -332,12 +348,26 @@ function isImportTouched(projectRoot) {
|
|
|
332
348
|
*
|
|
333
349
|
* Best-effort: any unexpected error → return false (do not nag on faults).
|
|
334
350
|
*/
|
|
335
|
-
function shouldRecommendImport(projectRoot) {
|
|
351
|
+
function shouldRecommendImport(projectRoot, liveTotal) {
|
|
336
352
|
try {
|
|
337
353
|
if (readWorkspaceBindingId(projectRoot) === null) return false;
|
|
338
354
|
|
|
339
355
|
const threshold = readUnderseedThreshold(projectRoot);
|
|
340
|
-
|
|
356
|
+
// P0 (Goal H3 / KT-PIT-0017 + KT-PIT-0019): prefer the LIVE census total the
|
|
357
|
+
// HUD displays as the single count source. The census walks the read-set
|
|
358
|
+
// fresh every fire (never a frozen snapshot projection), so feeding it here
|
|
359
|
+
// makes the import nudge and the HUD agree by construction — killing the
|
|
360
|
+
// "HUD shows 61 entries but the nudge claims the KB is sparse" contradiction
|
|
361
|
+
// the stale-snapshot count produced. Fall back to the snapshot-derived count
|
|
362
|
+
// only when no live total is supplied (e.g. direct unit-test calls).
|
|
363
|
+
const nodeCount =
|
|
364
|
+
typeof liveTotal === "number" && Number.isFinite(liveTotal)
|
|
365
|
+
? liveTotal
|
|
366
|
+
: countCanonicalNodes(projectRoot);
|
|
367
|
+
// #3: undeterminable count (old snapshot predating knowledge_store_dirs) →
|
|
368
|
+
// skip. `null < threshold` coerces to true in JS, so an explicit guard is
|
|
369
|
+
// required — otherwise the stale-snapshot case would still false-fire.
|
|
370
|
+
if (nodeCount === null) return false;
|
|
341
371
|
if (nodeCount >= threshold) return false;
|
|
342
372
|
|
|
343
373
|
if (isImportTouched(projectRoot) !== "absent") return false;
|
|
@@ -363,6 +393,12 @@ function shouldRecommendImport(projectRoot) {
|
|
|
363
393
|
// truncation summary lines) consume it as a single source of truth.
|
|
364
394
|
const TRUNCATION_THRESHOLD = 12;
|
|
365
395
|
|
|
396
|
+
// Goal H4 action ladder — review rung: surface a single `/fabric-review` line when
|
|
397
|
+
// the LIVE pending backlog exceeds this. Mirrors fabric-hint.cjs's
|
|
398
|
+
// DEFAULT_REVIEW_HINT_PENDING_COUNT (the Stop hook's review threshold) so the two
|
|
399
|
+
// surfaces agree on "how much pending is too much". Strictly `> threshold`.
|
|
400
|
+
const REVIEW_PENDING_THRESHOLD = 10;
|
|
401
|
+
|
|
366
402
|
// `fabric plan-context-hint` is a thin wrapper over planContext(); on a
|
|
367
403
|
// well-seeded repo it returns in ~100ms. Two-second cap is defensive — any
|
|
368
404
|
// pathological hang must not stall session start.
|
|
@@ -374,65 +410,6 @@ const CLI_TIMEOUT_MS = 2000;
|
|
|
374
410
|
// `hint_summary_max_len` in fabric-config overrides this default (range 40..240).
|
|
375
411
|
const DEFAULT_SUMMARY_MAX_LEN = 80;
|
|
376
412
|
|
|
377
|
-
// v2.2 HK2-degrade (W2-T2): char budget for the rendered broad-menu BODY. The
|
|
378
|
-
// hook already degrades by COUNT (hint_broad_top_k slice + TRUNCATION_THRESHOLD
|
|
379
|
-
// grouped mode), but nothing bounded the total rendered SIZE — a corpus with
|
|
380
|
-
// many types or long (near-maxLen) summaries could still emit a wall of text
|
|
381
|
-
// that displaces the agent's working memory. Borrowing the maestro
|
|
382
|
-
// context-budget idea, this is the final rung of the degradation ladder: once
|
|
383
|
-
// the body exceeds the budget, the tail collapses to a single "N more omitted"
|
|
384
|
-
// marker. Default 2000 chars ≈ one screenful. Overridable via
|
|
385
|
-
// fabric-config.json#hint_broad_budget_chars (range 200..20000); 0 disables.
|
|
386
|
-
const DEFAULT_HINT_BROAD_BUDGET_CHARS = 2000;
|
|
387
|
-
|
|
388
|
-
// v2.2 C5-budget (W2-T3): bind the injection char budget to the layered retrieval
|
|
389
|
-
// budget profile. Mirrors the injectionChars column of shared/retrieval-budget.ts
|
|
390
|
-
// PROFILES (kept in sync — the hook cannot require the TS resolver). The explicit
|
|
391
|
-
// `hint_broad_budget_chars` knob still wins; the profile only supplies the
|
|
392
|
-
// default. `balanced` (and an absent/unknown profile) keeps the historical 2000.
|
|
393
|
-
const RETRIEVAL_BUDGET_INJECTION_CHARS = {
|
|
394
|
-
conservative: 1000,
|
|
395
|
-
balanced: 2000,
|
|
396
|
-
generous: 4000,
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
function readBroadBudgetChars(projectRoot) {
|
|
400
|
-
const profile = readConfigString(projectRoot, "retrieval_budget_profile", "balanced");
|
|
401
|
-
const profileDefault =
|
|
402
|
-
RETRIEVAL_BUDGET_INJECTION_CHARS[profile] ?? DEFAULT_HINT_BROAD_BUDGET_CHARS;
|
|
403
|
-
return readConfigNumber(projectRoot, "hint_broad_budget_chars", profileDefault, {
|
|
404
|
-
min: 0,
|
|
405
|
-
max: 20000,
|
|
406
|
-
floor: true,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// v2.2 HK2-degrade (W2-T2): cap the rendered body to `budgetChars`, collapsing
|
|
411
|
-
// the overflow tail into one marker line. Structural lines (banner, revision_hash,
|
|
412
|
-
// footer) are appended by renderSummary AFTER this pass, so they always survive —
|
|
413
|
-
// only entry/group body lines are subject to the budget. `budgetChars` of 0 or
|
|
414
|
-
// undefined is a no-op (preserves the pre-HK2 unbounded behavior and all
|
|
415
|
-
// existing snapshot tests).
|
|
416
|
-
function capBodyToBudget(body, budgetChars) {
|
|
417
|
-
if (!budgetChars || budgetChars <= 0) return body;
|
|
418
|
-
const kept = [];
|
|
419
|
-
let total = 0;
|
|
420
|
-
for (let i = 0; i < body.length; i += 1) {
|
|
421
|
-
const line = body[i];
|
|
422
|
-
// +1 for the newline each line costs once joined.
|
|
423
|
-
if (kept.length > 0 && total + line.length + 1 > budgetChars) {
|
|
424
|
-
const remaining = body.length - i;
|
|
425
|
-
kept.push(
|
|
426
|
-
` … ${remaining} more entr${remaining === 1 ? "y" : "ies"} omitted (injection budget ${budgetChars} chars; raise hint_broad_budget_chars or narrow scope)`,
|
|
427
|
-
);
|
|
428
|
-
return kept;
|
|
429
|
-
}
|
|
430
|
-
kept.push(line);
|
|
431
|
-
total += line.length + 1;
|
|
432
|
-
}
|
|
433
|
-
return kept;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
413
|
function readSummaryMaxLen(projectRoot) {
|
|
437
414
|
return readConfigNumber(projectRoot, "hint_summary_max_len", DEFAULT_SUMMARY_MAX_LEN, {
|
|
438
415
|
min: 40,
|
|
@@ -682,7 +659,7 @@ function renderTruncated(narrow, maxLen) {
|
|
|
682
659
|
* after writing exactly one stderr breadcrumb so operators grepping a stuck-
|
|
683
660
|
* banner report can diagnose the version drift without source-diving.
|
|
684
661
|
*/
|
|
685
|
-
function renderSummary(payload, maxLen
|
|
662
|
+
function renderSummary(payload, maxLen) {
|
|
686
663
|
if (!payload || payload.version !== 2) {
|
|
687
664
|
if (payload && payload.version !== undefined) {
|
|
688
665
|
try {
|
|
@@ -705,9 +682,9 @@ function renderSummary(payload, maxLen, budgetChars) {
|
|
|
705
682
|
? `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available (truncated):`
|
|
706
683
|
: `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available:`;
|
|
707
684
|
|
|
708
|
-
|
|
709
|
-
//
|
|
710
|
-
const body =
|
|
685
|
+
// KT-DEC-0028 completeness: the rendered census is bounded by the per-line
|
|
686
|
+
// maxLen + TRUNCATION_THRESHOLD grouped mode, not by a body char budget.
|
|
687
|
+
const body = truncated ? renderTruncated(entries, maxLen) : renderFull(entries, maxLen);
|
|
711
688
|
|
|
712
689
|
const lines = [banner, ...body];
|
|
713
690
|
const revHash = typeof payload.revision_hash === "string" ? payload.revision_hash : null;
|
|
@@ -806,65 +783,134 @@ function toPluralType(type) {
|
|
|
806
783
|
// count the (possibly sliced) entries by knowledge_type so the human banner still
|
|
807
784
|
// has something to group. Production payloads always carry the unsliced census.
|
|
808
785
|
function deriveCensusFromEntries(entries) {
|
|
809
|
-
const census = {
|
|
786
|
+
const census = {
|
|
787
|
+
by_type: {},
|
|
788
|
+
by_layer: { team: 0, personal: 0, project: 0 },
|
|
789
|
+
broad_by_type: {},
|
|
790
|
+
narrow_total: 0,
|
|
791
|
+
dropped_other_project: 0,
|
|
792
|
+
total: 0,
|
|
793
|
+
};
|
|
810
794
|
if (!Array.isArray(entries)) return census;
|
|
811
795
|
for (const e of entries) {
|
|
812
796
|
const type = e && typeof e.type === "string" ? toPluralType(e.type) : null;
|
|
813
797
|
if (type === null) continue;
|
|
798
|
+
const isNarrow = e.relevance_scope === "narrow";
|
|
814
799
|
census.by_type[type] = (census.by_type[type] || 0) + 1;
|
|
800
|
+
if (isNarrow) census.narrow_total += 1;
|
|
801
|
+
else census.broad_by_type[type] = (census.broad_by_type[type] || 0) + 1;
|
|
815
802
|
census.total += 1;
|
|
816
803
|
}
|
|
817
804
|
return census;
|
|
818
805
|
}
|
|
819
806
|
|
|
820
|
-
// Render the human-facing
|
|
821
|
-
// Returns an array of lines (
|
|
807
|
+
// Render the human-facing scope-primary status HUD (Goal H2). `lang` is
|
|
808
|
+
// "zh-CN" | other (en). Returns an array of lines (empty when census is empty).
|
|
809
|
+
//
|
|
810
|
+
// Shape (KT-DEC-0029 — SessionStart is scope-primary; broad is the spine that's
|
|
811
|
+
// injected this session, narrow surfaces contextually via the PreToolUse hint):
|
|
812
|
+
// ▸ [fabric] 共 N 条 · 团队X · 项目Y · 个人Z
|
|
813
|
+
// broad B · 本会话注入
|
|
814
|
+
// ├ 常驻规则 G+M guideline G · model M (KT-DEC-0027 resident tier)
|
|
815
|
+
// └ 情境参考 D+P+Pr decision D · pitfall P · process Pr (reference tier)
|
|
816
|
+
// narrow M · 编辑对应文件时浮现 (合计 only, no per-type)
|
|
817
|
+
// Self-consistency invariant: broad (= 常驻 + 参考) + narrow == total.
|
|
822
818
|
function renderHumanCensus(census, opts) {
|
|
823
819
|
const { lang } = opts || {};
|
|
824
820
|
const c = census || {};
|
|
825
|
-
const byType = c.by_type || {};
|
|
826
821
|
const total = typeof c.total === "number" ? c.total : 0;
|
|
827
822
|
if (total === 0 && (c.dropped_other_project || 0) === 0) return [];
|
|
828
823
|
const zh = lang === "zh-CN";
|
|
829
824
|
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
825
|
+
const broadByType = c.broad_by_type || {};
|
|
826
|
+
const narrowTotal = typeof c.narrow_total === "number" ? c.narrow_total : 0;
|
|
827
|
+
// Per-tier broad counts. `broad_by_type` keys on the plural enum.
|
|
828
|
+
const g = broadByType.guidelines || 0;
|
|
829
|
+
const m = broadByType.models || 0;
|
|
830
|
+
const d = broadByType.decisions || 0;
|
|
831
|
+
const p = broadByType.pitfalls || 0;
|
|
832
|
+
const pr = broadByType.processes || 0;
|
|
833
|
+
const residentN = g + m; // 常驻规则 (always-active: guideline + model)
|
|
834
|
+
const referenceN = d + p + pr; // 情境参考 (decision + pitfall + process)
|
|
835
|
+
const broadN = residentN + referenceN;
|
|
835
836
|
|
|
836
|
-
const lines = [];
|
|
837
|
-
// `total` is the read-set ENTRY COUNT (not bytes) — label it as 条/entries.
|
|
838
|
-
lines.push(`▸ [fabric] SessionStart (${total} ${zh ? "条" : total === 1 ? "entry" : "entries"})`);
|
|
839
|
-
// W2-2/W2-3 (KT-DEC-0027/0029): the human breadcrumb shows only the
|
|
840
|
-
// always-loaded (guideline/model) census. The on-demand (decision/pitfall/
|
|
841
|
-
// process) count line and the dropped-other-project line are retired — the
|
|
842
|
-
// decision/pitfall/process REFERENCE lives in the AI sink (title + must_read_if),
|
|
843
|
-
// and SessionStart stays silent about narrow-scoped knowledge.
|
|
844
|
-
const alwaysCounts = typeCounts(ALWAYS_TYPES);
|
|
845
|
-
lines.push(zh ? " ─ always-loaded(AI 也收到正文)─" : " ─ always-loaded (AI also gets bodies) ─");
|
|
846
|
-
lines.push(` ${alwaysCounts.length > 0 ? alwaysCounts : zh ? "(无)" : "(none)"}`);
|
|
847
837
|
const layer = c.by_layer || {};
|
|
848
838
|
const teamCount = layer.team || 0;
|
|
849
839
|
const personalCount = layer.personal || 0;
|
|
850
840
|
const projectCount = layer.project || 0;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
841
|
+
|
|
842
|
+
const lines = [];
|
|
843
|
+
// Header: total entry count + semantic_scope breakdown (KT-MOD-0001 三轴).
|
|
844
|
+
const scopeSegs = [zh ? `团队 ${teamCount}` : `team ${teamCount}`];
|
|
845
|
+
if (projectCount > 0) scopeSegs.push(zh ? `项目 ${projectCount}` : `project ${projectCount}`);
|
|
846
|
+
scopeSegs.push(zh ? `个人 ${personalCount}` : `personal ${personalCount}`);
|
|
847
|
+
const totalLabel = zh ? `共 ${total} 条` : `${total} ${total === 1 ? "entry" : "entries"}`;
|
|
848
|
+
lines.push(`▸ [fabric] ${totalLabel} · ${scopeSegs.join(" · ")}`);
|
|
849
|
+
|
|
850
|
+
// broad spine — injected this session.
|
|
851
|
+
lines.push(zh ? ` broad ${broadN} · 本会话注入` : ` broad ${broadN} · injected this session`);
|
|
852
|
+
const residentDetail = [];
|
|
853
|
+
if (g > 0) residentDetail.push(`guideline ${g}`);
|
|
854
|
+
if (m > 0) residentDetail.push(`model ${m}`);
|
|
855
|
+
const refDetail = [];
|
|
856
|
+
if (d > 0) refDetail.push(`decision ${d}`);
|
|
857
|
+
if (p > 0) refDetail.push(`pitfall ${p}`);
|
|
858
|
+
if (pr > 0) refDetail.push(`process ${pr}`);
|
|
859
|
+
const dash = zh ? "—" : "—";
|
|
860
|
+
lines.push(
|
|
861
|
+
zh
|
|
862
|
+
? ` ├ 常驻规则 ${residentN} ${residentDetail.join(" · ") || dash}`
|
|
863
|
+
: ` ├ resident ${residentN} ${residentDetail.join(" · ") || dash}`,
|
|
864
|
+
);
|
|
865
|
+
lines.push(
|
|
866
|
+
zh
|
|
867
|
+
? ` └ 情境参考 ${referenceN} ${refDetail.join(" · ") || dash}`
|
|
868
|
+
: ` └ reference ${referenceN} ${refDetail.join(" · ") || dash}`,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
// narrow remainder — 合计 only (no per-type; it's file-specific, surfaces on edit).
|
|
872
|
+
lines.push(
|
|
873
|
+
zh
|
|
874
|
+
? ` narrow ${narrowTotal} · 编辑对应文件时浮现`
|
|
875
|
+
: ` narrow ${narrowTotal} · surfaces when you edit matching files`,
|
|
876
|
+
);
|
|
857
877
|
return lines;
|
|
858
878
|
}
|
|
859
879
|
|
|
880
|
+
// Goal H2: the SessionStart store label, scope-primary wording — `写入 X · 只读 Y`
|
|
881
|
+
// (write target receives new knowledge; the rest are read-only sources). Replaces
|
|
882
|
+
// the legacy `read-set stores: a (write), b (ro)` jargon line inline (kept local
|
|
883
|
+
// to this hook so the shared lib formatStoreLabels — used by other hooks — is
|
|
884
|
+
// untouched). Empty string when there is nothing to show.
|
|
885
|
+
function renderScopeStoreLabel(snapshot, lang) {
|
|
886
|
+
if (!snapshot || !snapshot.read_set || !Array.isArray(snapshot.read_set.stores)) return "";
|
|
887
|
+
const stores = snapshot.read_set.stores;
|
|
888
|
+
if (stores.length === 0) return "";
|
|
889
|
+
const zh = lang === "zh-CN";
|
|
890
|
+
const writeAlias = snapshot.write_target && snapshot.write_target.alias;
|
|
891
|
+
const writeStores = [];
|
|
892
|
+
const readonlyStores = [];
|
|
893
|
+
for (const s of stores) {
|
|
894
|
+
const alias = s && typeof s.alias === "string" ? s.alias : null;
|
|
895
|
+
if (alias === null) continue;
|
|
896
|
+
if (alias === writeAlias) writeStores.push(alias);
|
|
897
|
+
else readonlyStores.push(alias);
|
|
898
|
+
}
|
|
899
|
+
const segs = [];
|
|
900
|
+
if (writeStores.length > 0) segs.push((zh ? "写入 " : "write ") + writeStores.join(", "));
|
|
901
|
+
if (readonlyStores.length > 0) segs.push((zh ? "只读 " : "readonly ") + readonlyStores.join(", "));
|
|
902
|
+
if (segs.length === 0) return "";
|
|
903
|
+
return " " + segs.join(" · ");
|
|
904
|
+
}
|
|
905
|
+
|
|
860
906
|
// W2 (KT-DEC-0027/0028/0029): render the AI-facing sink — the dynamically
|
|
861
907
|
// generated "MEMORY.md" spine injected into the SessionStart context. Two
|
|
862
908
|
// type-tiered sections over the BROAD knowledge (narrow stays silent — D0029):
|
|
863
909
|
//
|
|
864
|
-
// ALWAYS-ACTIVE RULES (guideline/model):
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
910
|
+
// ALWAYS-ACTIVE RULES (guideline/model): INDEX LINE only (title + summary) —
|
|
911
|
+
// never the eager body (KT-DEC-0036). The body is one cheap on-demand fetch
|
|
912
|
+
// away, so injecting it on every SessionStart is a permanent context tax
|
|
913
|
+
// (KT-GLD-0005) we no longer pay; each entry stays individually visible.
|
|
868
914
|
// REFERENCE (decision/pitfall/process): TITLE + must_read_if hook only
|
|
869
915
|
// (situational; the agent Reads the body on demand) — never the body.
|
|
870
916
|
//
|
|
@@ -876,7 +922,7 @@ function renderHumanCensus(census, opts) {
|
|
|
876
922
|
const REFERENCE_TYPES = new Set(["decision", "pitfall", "process"]);
|
|
877
923
|
|
|
878
924
|
function renderAiSink(opts) {
|
|
879
|
-
const { entries, alwaysBodies, storeLabel,
|
|
925
|
+
const { entries, alwaysBodies, storeLabel, broadIndexBackstop, summaryMaxLen, lang } =
|
|
880
926
|
opts || {};
|
|
881
927
|
const zh = lang === "zh-CN";
|
|
882
928
|
const bodies = Array.isArray(alwaysBodies) ? alwaysBodies : [];
|
|
@@ -895,37 +941,34 @@ function renderAiSink(opts) {
|
|
|
895
941
|
const lines = [];
|
|
896
942
|
lines.push(`[fabric:SessionStart] ${storeLabel || "store"}`);
|
|
897
943
|
|
|
898
|
-
// ALWAYS-ACTIVE RULES —
|
|
899
|
-
lines.push(
|
|
944
|
+
// ALWAYS-ACTIVE RULES — index-only (title + summary), never the eager body.
|
|
945
|
+
lines.push(
|
|
946
|
+
zh
|
|
947
|
+
? "ALWAYS-ACTIVE RULES (无条件适用 · 照此行遵循,正文按需取):"
|
|
948
|
+
: "ALWAYS-ACTIVE RULES (unconditional · act on the line; body on demand):",
|
|
949
|
+
);
|
|
900
950
|
if (bodies.length === 0) {
|
|
901
951
|
lines.push(zh ? " (无 always-active 条目)" : " (none)");
|
|
902
952
|
} else {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
953
|
+
// KT-DEC-0036: render each always-active entry as a single index line
|
|
954
|
+
// (title + summary). The body is one cheap on-demand fetch away (see footer),
|
|
955
|
+
// so injecting it on every SessionStart is a permanent context tax
|
|
956
|
+
// (KT-GLD-0005) we no longer pay.
|
|
906
957
|
for (const b of bodies) {
|
|
907
958
|
const label = `[${TYPE_SINGULAR[b.type] || b.type}] ${b.id}`;
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
const fullCost = label.length + body.length + 2;
|
|
911
|
-
if (!degraded && (budget === 0 || used + fullCost <= budget)) {
|
|
912
|
-
lines.push(` ${label}`);
|
|
913
|
-
if (body.length > 0) lines.push(body);
|
|
914
|
-
used += fullCost;
|
|
915
|
-
} else {
|
|
916
|
-
// D0028: degrade to an INDEX LINE (title + summary), never a folded count.
|
|
917
|
-
degraded = true;
|
|
918
|
-
lines.push(
|
|
919
|
-
` ${label} · ${summary}${zh ? " (超预算; fab_recall 取正文)" : " (over budget; fab_recall for body)"}`,
|
|
920
|
-
);
|
|
921
|
-
}
|
|
959
|
+
const summary = typeof b.summary === "string" ? b.summary.trim() : "";
|
|
960
|
+
lines.push(summary.length > 0 ? ` ${label} · ${summary}` : ` ${label}`);
|
|
922
961
|
indexCount += 1;
|
|
923
962
|
}
|
|
924
963
|
}
|
|
925
964
|
|
|
926
965
|
// REFERENCE — broad decision/pitfall/process: title + must_read_if hook.
|
|
927
966
|
if (referenceEntries.length > 0) {
|
|
928
|
-
lines.push(
|
|
967
|
+
lines.push(
|
|
968
|
+
zh
|
|
969
|
+
? "REFERENCE (情境触发 · 命中 must_read_if 时 Read / fab_recall):"
|
|
970
|
+
: "REFERENCE (situational · Read when must_read_if fires / fab_recall):",
|
|
971
|
+
);
|
|
929
972
|
let folded = 0;
|
|
930
973
|
for (const e of referenceEntries) {
|
|
931
974
|
if (backstop > 0 && indexCount >= backstop) {
|
|
@@ -947,8 +990,8 @@ function renderAiSink(opts) {
|
|
|
947
990
|
if (folded > 0) {
|
|
948
991
|
lines.push(
|
|
949
992
|
zh
|
|
950
|
-
? ` … 另 ${folded} 条 broad 条目折叠 (broad index > backstop ${backstop}
|
|
951
|
-
: ` … ${folded} more broad entr${folded === 1 ? "y" : "ies"} folded (broad index > backstop ${backstop};
|
|
993
|
+
? ` … 另 ${folded} 条 broad 条目折叠 (broad index > backstop ${backstop})。先跑 fabric-audit 瘦身;确需全展示再调 .fabric/fabric-config.json#broad_index_backstop (20..500)`
|
|
994
|
+
: ` … ${folded} more broad entr${folded === 1 ? "y" : "ies"} folded (broad index > backstop ${backstop}). Run fabric-audit to prune first; raise .fabric/fabric-config.json#broad_index_backstop (20..500) only if you truly need them all`,
|
|
952
995
|
);
|
|
953
996
|
}
|
|
954
997
|
}
|
|
@@ -959,6 +1002,15 @@ function renderAiSink(opts) {
|
|
|
959
1002
|
? "取正文: fab_recall(paths), 或 Read <store>/knowledge/<type>/<id>--*.md"
|
|
960
1003
|
: "Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md",
|
|
961
1004
|
);
|
|
1005
|
+
// H6 scope discipline: this sink carries ONLY broad (always-relevant) knowledge;
|
|
1006
|
+
// narrow (file-specific) entries surface contextually via the PreToolUse hint
|
|
1007
|
+
// when you edit a matching file (KT-DEC-0029). Stops the agent from assuming
|
|
1008
|
+
// SessionStart is the whole KB.
|
|
1009
|
+
lines.push(
|
|
1010
|
+
zh
|
|
1011
|
+
? "范围: 此处仅 broad(始终相关);narrow(文件专属)在你编辑对应文件时由 PreToolUse 浮现"
|
|
1012
|
+
: "Scope: broad only (always relevant) here; narrow (file-specific) surfaces via the PreToolUse hint when you edit a matching file",
|
|
1013
|
+
);
|
|
962
1014
|
return lines.join("\n");
|
|
963
1015
|
}
|
|
964
1016
|
|
|
@@ -977,29 +1029,19 @@ function renderAiSink(opts) {
|
|
|
977
1029
|
// Returns:
|
|
978
1030
|
// human — gated final human text (null when gated off / empty)
|
|
979
1031
|
// ai — gated final AI text (null when reminder-to-context off / empty)
|
|
980
|
-
// resolvedPayload — payload
|
|
1032
|
+
// resolvedPayload — the plan-context payload, passed through unchanged (for telemetry / --explain)
|
|
981
1033
|
// hasRenderedContent — true when ANY sink rendered content (main's silent-exit gate)
|
|
982
1034
|
// reminderToContext — readReminderToContext(cwd) (telemetry target-channel)
|
|
983
1035
|
function buildSessionStartSinks(cwd, payload, env) {
|
|
984
|
-
// rc.35
|
|
985
|
-
//
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
cwd,
|
|
992
|
-
typeof payload.revision_hash === "string" ? payload.revision_hash : "",
|
|
993
|
-
);
|
|
994
|
-
resolvedPayload = { ...payload, entries: resolvedEntries };
|
|
995
|
-
}
|
|
996
|
-
} catch {
|
|
997
|
-
// resolveOpaqueSummaries swallows its own errors; belt + suspenders.
|
|
998
|
-
}
|
|
1036
|
+
// KT-GLD-0006: the rc.35 opaque-summary runtime substitution
|
|
1037
|
+
// (resolveOpaqueSummaries) is retired. The write-time mechanical floor in
|
|
1038
|
+
// extractKnowledge (summary !== stable_id/slug + length floor) prevents
|
|
1039
|
+
// degenerate summaries at the source, so SessionStart no longer band-aids them
|
|
1040
|
+
// at render time; surviving legacy opaque summaries are fixed by the
|
|
1041
|
+
// review-time cold-eval audit pass.
|
|
1042
|
+
const resolvedPayload = payload;
|
|
999
1043
|
|
|
1000
|
-
const recommendImport = shouldRecommendImport(cwd);
|
|
1001
1044
|
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
1002
|
-
const broadBudgetChars = readBroadBudgetChars(cwd);
|
|
1003
1045
|
const fabricLanguageForEmit = readFabricLanguage(cwd);
|
|
1004
1046
|
|
|
1005
1047
|
const census =
|
|
@@ -1015,6 +1057,24 @@ function buildSessionStartSinks(cwd, payload, env) {
|
|
|
1015
1057
|
? payload.always_bodies
|
|
1016
1058
|
: [];
|
|
1017
1059
|
|
|
1060
|
+
// H3: the LIVE census total is the single count source for the import gate —
|
|
1061
|
+
// computed AFTER census so the nudge and the HUD agree by construction.
|
|
1062
|
+
const censusTotal = census && typeof census.total === "number" ? census.total : undefined;
|
|
1063
|
+
const recommendImport = shouldRecommendImport(cwd, censusTotal);
|
|
1064
|
+
|
|
1065
|
+
// Read the resolved-bindings snapshot ONCE — reused for the scope store label
|
|
1066
|
+
// (写入/只读) and the H4 review-rung pending count. Best-effort/decorative: any
|
|
1067
|
+
// failure leaves snapshot null and the dependent lines simply don't render.
|
|
1068
|
+
let snapshot = null;
|
|
1069
|
+
if (bindingsSnapshotReader !== null) {
|
|
1070
|
+
try {
|
|
1071
|
+
const bindingId = readWorkspaceBindingId(cwd);
|
|
1072
|
+
if (bindingId) snapshot = bindingsSnapshotReader.readBindingsSnapshot(bindingId);
|
|
1073
|
+
} catch {
|
|
1074
|
+
snapshot = null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1018
1078
|
const humanGate =
|
|
1019
1079
|
nudgePolicy !== null
|
|
1020
1080
|
? nudgePolicy.resolveHumanSink(cwd, "session_start", {})
|
|
@@ -1023,45 +1083,58 @@ function buildSessionStartSinks(cwd, payload, env) {
|
|
|
1023
1083
|
// ---- HUMAN sink: §3 grouped census (+ verbose per-entry detail). ----
|
|
1024
1084
|
const humanLines = renderHumanCensus(census, { lang: fabricLanguageForEmit });
|
|
1025
1085
|
if (humanLines.length > 0 && humanGate.verbosity === "verbose") {
|
|
1026
|
-
const detail = renderSummary(resolvedPayload, summaryMaxLen
|
|
1086
|
+
const detail = renderSummary(resolvedPayload, summaryMaxLen);
|
|
1027
1087
|
humanLines.push(...detail);
|
|
1028
1088
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1089
|
+
// H2: scope store label — `写入 X · 只读 Y` (replaces the legacy read-set jargon).
|
|
1090
|
+
if (humanLines.length > 0 && snapshot !== null) {
|
|
1091
|
+
const storeLabel = renderScopeStoreLabel(snapshot, fabricLanguageForEmit);
|
|
1092
|
+
if (storeLabel) humanLines.push(storeLabel);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// H4 action ladder (KT-DEC-0007: nudge, never a gate). AT MOST ONE line, the
|
|
1096
|
+
// highest-priority rung wins, and steady state is fully silent:
|
|
1097
|
+
// 1. import — KB is sparse (recommendImport, off the live census total)
|
|
1098
|
+
// 2. review — pending backlog exceeds REVIEW_PENDING_THRESHOLD (live count)
|
|
1099
|
+
// 3. (silent)
|
|
1100
|
+
if (humanLines.length > 0 && fabricLanguageForEmit !== null) {
|
|
1101
|
+
if (recommendImport) {
|
|
1102
|
+
humanLines.push(renderBanner("broadImportBanner", fabricLanguageForEmit, {}));
|
|
1103
|
+
} else if (snapshot !== null && bindingsSnapshotReader !== null) {
|
|
1104
|
+
let pendingCount = 0;
|
|
1105
|
+
try {
|
|
1106
|
+
const live = bindingsSnapshotReader.liveKnowledgeStats(snapshot);
|
|
1107
|
+
if (live && Number.isFinite(live.pendingCount)) pendingCount = Math.floor(live.pendingCount);
|
|
1108
|
+
} catch {
|
|
1109
|
+
pendingCount = 0;
|
|
1110
|
+
}
|
|
1111
|
+
if (pendingCount > REVIEW_PENDING_THRESHOLD) {
|
|
1112
|
+
humanLines.push(
|
|
1113
|
+
fabricLanguageForEmit === "zh-CN"
|
|
1114
|
+
? ` 📋 Fabric: ${pendingCount} 条 pending 待审,是否调 /fabric-review?`
|
|
1115
|
+
: ` 📋 Fabric: ${pendingCount} pending entries — run /fabric-review?`,
|
|
1035
1116
|
);
|
|
1036
|
-
if (label) humanLines.push(label);
|
|
1037
1117
|
}
|
|
1038
|
-
} catch {
|
|
1039
|
-
// store labels are decorative provenance — never crash the hook
|
|
1040
1118
|
}
|
|
1041
1119
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1120
|
+
|
|
1121
|
+
// H5: the `下一步: …fab_recall…` AI-plumbing line is retired from the human sink
|
|
1122
|
+
// (the AI gets it from its own footer + the MCP server directive). Keep only the
|
|
1123
|
+
// pointer to the byte-identical inspector for this injection.
|
|
1045
1124
|
if (humanLines.length > 0) {
|
|
1046
1125
|
humanLines.push(
|
|
1047
1126
|
fabricLanguageForEmit === "zh-CN"
|
|
1048
|
-
? "
|
|
1049
|
-
: "
|
|
1050
|
-
);
|
|
1051
|
-
// Block 5 (Option X): point to the byte-identical inspector for this injection.
|
|
1052
|
-
humanLines.push(
|
|
1053
|
-
fabricLanguageForEmit === "zh-CN"
|
|
1054
|
-
? "看具体注入: fabric context (--explain 看每条来源)"
|
|
1055
|
-
: "Inspect this injection: fabric context (--explain for per-entry provenance)",
|
|
1127
|
+
? " 看具体注入: fabric context (--explain 看每条来源)"
|
|
1128
|
+
: " Inspect this injection: fabric context (--explain for per-entry provenance)",
|
|
1056
1129
|
);
|
|
1057
1130
|
}
|
|
1058
1131
|
|
|
1059
|
-
// ---- AI sink: spine — always-active
|
|
1132
|
+
// ---- AI sink: spine — always-active INDEX lines (no eager body, KT-DEC-0036)
|
|
1133
|
+
// + reference, bounded by the broad_index_backstop fold. ----
|
|
1060
1134
|
const broadIndexBackstop = readBroadIndexBackstop(cwd);
|
|
1061
1135
|
const aiText = renderAiSink({
|
|
1062
1136
|
entries: resolvedPayload && Array.isArray(resolvedPayload.entries) ? resolvedPayload.entries : [],
|
|
1063
1137
|
alwaysBodies,
|
|
1064
|
-
budgetChars: broadBudgetChars,
|
|
1065
1138
|
broadIndexBackstop,
|
|
1066
1139
|
summaryMaxLen,
|
|
1067
1140
|
lang: fabricLanguageForEmit,
|