@fenglimg/fabric-cli 2.2.0-rc.9 → 2.3.0-rc.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.
- package/README.md +2 -2
- package/dist/audit-PURSJJFH.js +734 -0
- package/dist/{chunk-YM4XATJF.js → chunk-722JU5BP.js} +2 -0
- package/dist/{chunk-QPAW6IYT.js → chunk-7V4XMLQ2.js} +3 -3
- package/dist/{chunk-7ZDXBOOU.js → chunk-ACSMNX3V.js} +44 -128
- package/dist/{chunk-PTGQAZEW.js → chunk-GGDVZCD6.js} +2 -4
- package/dist/{chunk-EOT63RDH.js → chunk-I5F5BHWI.js} +9 -0
- package/dist/chunk-PP7QVRXH.js +565 -0
- package/dist/chunk-SL77FXX7.js +54 -0
- package/dist/{chunk-3D7B2UAZ.js → chunk-VQKXTMWH.js} +44 -4
- package/dist/doctor-S6KPGS35.js +27 -0
- package/dist/index.js +91 -81
- package/dist/{info-7FKBTMVO.js → info-NJEY26H6.js} +91 -46
- package/dist/{context-7NUKXDB6.js → inspect-5YZMJPFM.js} +11 -11
- package/dist/{install-v2-I6PJ6IFT.js → install-v2-KGIDII4H.js} +163 -364
- package/dist/{plan-context-hint-G75R4P4J.js → plan-context-hint-5TNGH3R4.js} +1 -1
- package/dist/{store-HOCORVL3.js → store-GF4SFBMJ.js} +155 -57
- package/dist/{sync-DT5UJMMR.js → sync-3XCIRDPK.js} +3 -4
- package/dist/{uninstall-IFN2KYBK.js → uninstall-BG4ML4FC.js} +39 -10
- package/package.json +3 -7
- package/templates/hooks/cite-policy-evict.cjs +1 -1
- package/templates/hooks/configs/claude-code.json +1 -5
- package/templates/hooks/configs/codex-hooks.json +1 -5
- package/templates/hooks/fabric-hint.cjs +346 -138
- package/templates/hooks/knowledge-hint-broad.cjs +265 -75
- package/templates/hooks/knowledge-hint-narrow.cjs +3 -3
- package/templates/hooks/knowledge-pretooluse.cjs +111 -0
- package/templates/hooks/lib/banner-i18n.cjs +31 -12
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +1 -1
- package/templates/hooks/lib/event-writer.cjs +79 -0
- package/templates/hooks/lib/nudge-policy.cjs +11 -0
- package/templates/hooks/lib/theme.cjs +62 -0
- package/templates/hooks/post-tooluse-mutation.cjs +28 -39
- package/templates/skills/fabric-archive/SKILL.md +43 -12
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +5 -5
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +2 -2
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +6 -5
- package/templates/skills/{fabric-import/ref/checkpoint-state.md → fabric-archive/ref/source-checkpoint.md} +3 -3
- package/templates/skills/{fabric-import/ref/phase-3-dedup.md → fabric-archive/ref/source-dedup.md} +4 -4
- package/templates/skills/{fabric-import/ref/phase-2-mining.md → fabric-archive/ref/source-mining.md} +20 -20
- package/templates/skills/{fabric-import/ref/output-contract.md → fabric-archive/ref/source-output-contract.md} +3 -3
- package/templates/skills/{fabric-import/ref/state-recovery.md → fabric-archive/ref/source-state-recovery.md} +2 -2
- package/templates/skills/{fabric-import/ref/worked-examples.md → fabric-archive/ref/source-worked-examples.md} +10 -10
- package/templates/skills/fabric-archive/ref/worked-examples.md +3 -3
- package/templates/skills/fabric-review/SKILL.md +28 -15
- package/templates/skills/fabric-review/ref/cite-contract.md +2 -2
- package/templates/skills/fabric-review/ref/modify-flow.md +13 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +5 -5
- package/templates/skills/fabric-review/ref/relate-mode.md +33 -0
- package/templates/skills/fabric-review/ref/retire-mode.md +47 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +1 -1
- package/templates/skills/fabric-review/ref/worked-examples.md +5 -5
- package/templates/skills/fabric-store/SKILL.md +12 -27
- package/templates/skills/fabric-sync/SKILL.md +16 -35
- package/templates/skills/lib/shared-policy.md +6 -4
- package/dist/chunk-27HK6H5Y.js +0 -69
- package/dist/chunk-E7HJUU34.js +0 -1096
- package/dist/chunk-NLNH64A3.js +0 -43
- package/dist/chunk-QFIVFZRH.js +0 -13
- package/dist/doctor-MDTZWKBK.js +0 -24
- package/dist/metrics-HMFH4YHK.js +0 -135
- package/dist/scope-explain-HLJZ2M33.js +0 -48
- package/dist/status-4R3TM4FJ.js +0 -37
- package/dist/whoami-ITGEFWH4.js +0 -49
- package/templates/skills/fabric/SKILL.md +0 -100
- package/templates/skills/fabric-audit/SKILL.md +0 -63
- package/templates/skills/fabric-connect/SKILL.md +0 -48
- package/templates/skills/fabric-import/SKILL.md +0 -151
- package/templates/skills/fabric-import/ref/i18n-policy.md +0 -78
|
@@ -16,11 +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 · <summary> # KT-DEC-0036
|
|
21
|
-
* REFERENCE (
|
|
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
|
|
22
22
|
* [decision] team:KT-DEC-0001 — <must_read_if>
|
|
23
|
-
* … N more folded (broad index > backstop 50;
|
|
23
|
+
* … N more folded (broad index > backstop 50; prune via fabric-audit)
|
|
24
24
|
* Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md
|
|
25
25
|
*
|
|
26
26
|
* Human sink (systemMessage) — broad-only census breadcrumb; SessionStart is
|
|
@@ -42,11 +42,21 @@ const { spawnSync } = require("node:child_process");
|
|
|
42
42
|
const { existsSync, readdirSync, readFileSync } = require("node:fs");
|
|
43
43
|
const { join } = require("node:path");
|
|
44
44
|
|
|
45
|
+
// W3-B F-005 (C-003 HUD-shared layer): the SessionStart AI sink reskin consumes
|
|
46
|
+
// the parity-trivial shared structural primitives — sectionBar (title row) +
|
|
47
|
+
// scopeBadge ([team]/[project]/[personal]) — from the .cjs theme mirror. This is
|
|
48
|
+
// the ONLY shared-theme dependency the hook may take: lib/theme.cjs is byte-locked
|
|
49
|
+
// to packages/shared/src/theme.ts by theme-parity.test.ts (G-THEME). The hook MUST
|
|
50
|
+
// NEVER reach for the CLI-only ESM/TS structure layer (the tree / grid primitives
|
|
51
|
+
// under packages/cli/src/tui) — it is unrequireable from a .cjs runtime; the HUD
|
|
52
|
+
// deliberately uses plain two-space indent instead of the complex tree() primitive.
|
|
53
|
+
const { sectionBar, scopeBadge } = require("./lib/theme.cjs");
|
|
54
|
+
|
|
45
55
|
// W1-01 (ISS-012): the SessionStart broad hook appends a hook_surface_emitted
|
|
46
|
-
// event to the shared events.jsonl.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
const {
|
|
56
|
+
// event to the shared events.jsonl. ux-w2-9: route through the single guarded
|
|
57
|
+
// event-writer (envelope stamp + event_type guard + advisory-lock append) so
|
|
58
|
+
// the row always satisfies the event-ledger schema the doctor reads.
|
|
59
|
+
const { appendEvent } = require("./lib/event-writer.cjs");
|
|
50
60
|
|
|
51
61
|
// rc.16 TASK-003: shared banner-i18n lib (resolves fabric_language config and
|
|
52
62
|
// renders localized banner text). Mirror of the wiring in fabric-hint.cjs
|
|
@@ -330,7 +340,7 @@ function isImportTouched(projectRoot) {
|
|
|
330
340
|
|
|
331
341
|
/**
|
|
332
342
|
* rc.8 underseed self-check: determine whether the SessionStart hook should
|
|
333
|
-
* surface the one-line `/fabric-
|
|
343
|
+
* surface the one-line `/fabric-archive` recommendation banner.
|
|
334
344
|
*
|
|
335
345
|
* Three-condition truth table (ALL must hold to return true):
|
|
336
346
|
* 1. the workspace is fabric-bound — readWorkspaceBindingId(cwd) !== null
|
|
@@ -348,12 +358,22 @@ function isImportTouched(projectRoot) {
|
|
|
348
358
|
*
|
|
349
359
|
* Best-effort: any unexpected error → return false (do not nag on faults).
|
|
350
360
|
*/
|
|
351
|
-
function shouldRecommendImport(projectRoot) {
|
|
361
|
+
function shouldRecommendImport(projectRoot, liveTotal) {
|
|
352
362
|
try {
|
|
353
363
|
if (readWorkspaceBindingId(projectRoot) === null) return false;
|
|
354
364
|
|
|
355
365
|
const threshold = readUnderseedThreshold(projectRoot);
|
|
356
|
-
|
|
366
|
+
// P0 (Goal H3 / KT-PIT-0017 + KT-PIT-0019): prefer the LIVE census total the
|
|
367
|
+
// HUD displays as the single count source. The census walks the read-set
|
|
368
|
+
// fresh every fire (never a frozen snapshot projection), so feeding it here
|
|
369
|
+
// makes the import nudge and the HUD agree by construction — killing the
|
|
370
|
+
// "HUD shows 61 entries but the nudge claims the KB is sparse" contradiction
|
|
371
|
+
// the stale-snapshot count produced. Fall back to the snapshot-derived count
|
|
372
|
+
// only when no live total is supplied (e.g. direct unit-test calls).
|
|
373
|
+
const nodeCount =
|
|
374
|
+
typeof liveTotal === "number" && Number.isFinite(liveTotal)
|
|
375
|
+
? liveTotal
|
|
376
|
+
: countCanonicalNodes(projectRoot);
|
|
357
377
|
// #3: undeterminable count (old snapshot predating knowledge_store_dirs) →
|
|
358
378
|
// skip. `null < threshold` coerces to true in JS, so an explicit guard is
|
|
359
379
|
// required — otherwise the stale-snapshot case would still false-fire.
|
|
@@ -383,6 +403,12 @@ function shouldRecommendImport(projectRoot) {
|
|
|
383
403
|
// truncation summary lines) consume it as a single source of truth.
|
|
384
404
|
const TRUNCATION_THRESHOLD = 12;
|
|
385
405
|
|
|
406
|
+
// Goal H4 action ladder — review rung: surface a single `/fabric-review` line when
|
|
407
|
+
// the LIVE pending backlog exceeds this. Mirrors fabric-hint.cjs's
|
|
408
|
+
// DEFAULT_REVIEW_HINT_PENDING_COUNT (the Stop hook's review threshold) so the two
|
|
409
|
+
// surfaces agree on "how much pending is too much". Strictly `> threshold`.
|
|
410
|
+
const REVIEW_PENDING_THRESHOLD = 10;
|
|
411
|
+
|
|
386
412
|
// `fabric plan-context-hint` is a thin wrapper over planContext(); on a
|
|
387
413
|
// well-seeded repo it returns in ~100ms. Two-second cap is defensive — any
|
|
388
414
|
// pathological hang must not stall session start.
|
|
@@ -425,7 +451,7 @@ const MATURITY_DRAFT = "draft";
|
|
|
425
451
|
// fabric-hint.cjs Signal C `📋 Fabric:`). rc.16 TASK-003 routed the literal
|
|
426
452
|
// through the banner-i18n lib (key: 'broadImportBanner') — see main() below
|
|
427
453
|
// for the renderBanner call site. Substring contracts preserved across all
|
|
428
|
-
// variants: leading two-space indent, `📋 Fabric:` prefix, `/fabric-
|
|
454
|
+
// variants: leading two-space indent, `📋 Fabric:` prefix, `/fabric-archive`
|
|
429
455
|
// verbatim token (asserted by knowledge-hint-broad.test.ts).
|
|
430
456
|
|
|
431
457
|
// -----------------------------------------------------------------------------
|
|
@@ -767,57 +793,126 @@ function toPluralType(type) {
|
|
|
767
793
|
// count the (possibly sliced) entries by knowledge_type so the human banner still
|
|
768
794
|
// has something to group. Production payloads always carry the unsliced census.
|
|
769
795
|
function deriveCensusFromEntries(entries) {
|
|
770
|
-
const census = {
|
|
796
|
+
const census = {
|
|
797
|
+
by_type: {},
|
|
798
|
+
by_layer: { team: 0, personal: 0, project: 0 },
|
|
799
|
+
broad_by_type: {},
|
|
800
|
+
narrow_total: 0,
|
|
801
|
+
dropped_other_project: 0,
|
|
802
|
+
total: 0,
|
|
803
|
+
};
|
|
771
804
|
if (!Array.isArray(entries)) return census;
|
|
772
805
|
for (const e of entries) {
|
|
773
806
|
const type = e && typeof e.type === "string" ? toPluralType(e.type) : null;
|
|
774
807
|
if (type === null) continue;
|
|
808
|
+
const isNarrow = e.relevance_scope === "narrow";
|
|
775
809
|
census.by_type[type] = (census.by_type[type] || 0) + 1;
|
|
810
|
+
if (isNarrow) census.narrow_total += 1;
|
|
811
|
+
else census.broad_by_type[type] = (census.broad_by_type[type] || 0) + 1;
|
|
776
812
|
census.total += 1;
|
|
777
813
|
}
|
|
778
814
|
return census;
|
|
779
815
|
}
|
|
780
816
|
|
|
781
|
-
// Render the human-facing
|
|
782
|
-
// Returns an array of lines (
|
|
817
|
+
// Render the human-facing scope-primary status HUD (Goal H2). `lang` is
|
|
818
|
+
// "zh-CN" | other (en). Returns an array of lines (empty when census is empty).
|
|
819
|
+
//
|
|
820
|
+
// Shape (KT-DEC-0029 — SessionStart is scope-primary; broad is the spine that's
|
|
821
|
+
// injected this session, narrow surfaces contextually via the PreToolUse hint):
|
|
822
|
+
// ▸ [fabric] 共 N 条 · 团队X · 项目Y · 个人Z
|
|
823
|
+
// broad B · 本会话注入
|
|
824
|
+
// ├ 常驻规则 G+M guideline G · model M (KT-DEC-0027 resident tier)
|
|
825
|
+
// └ 情境参考 D+P+Pr decision D · pitfall P · process Pr (reference tier)
|
|
826
|
+
// narrow M · 编辑对应文件时浮现 (合计 only, no per-type)
|
|
827
|
+
// Self-consistency invariant: broad (= 常驻 + 参考) + narrow == total.
|
|
783
828
|
function renderHumanCensus(census, opts) {
|
|
784
829
|
const { lang } = opts || {};
|
|
785
830
|
const c = census || {};
|
|
786
|
-
const byType = c.by_type || {};
|
|
787
831
|
const total = typeof c.total === "number" ? c.total : 0;
|
|
788
832
|
if (total === 0 && (c.dropped_other_project || 0) === 0) return [];
|
|
789
833
|
const zh = lang === "zh-CN";
|
|
790
834
|
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
835
|
+
const broadByType = c.broad_by_type || {};
|
|
836
|
+
const narrowTotal = typeof c.narrow_total === "number" ? c.narrow_total : 0;
|
|
837
|
+
// Per-tier broad counts. `broad_by_type` keys on the plural enum.
|
|
838
|
+
const g = broadByType.guidelines || 0;
|
|
839
|
+
const m = broadByType.models || 0;
|
|
840
|
+
const d = broadByType.decisions || 0;
|
|
841
|
+
const p = broadByType.pitfalls || 0;
|
|
842
|
+
const pr = broadByType.processes || 0;
|
|
843
|
+
const residentN = g + m; // 常驻规则 (always-active: guideline + model)
|
|
844
|
+
const referenceN = d + p + pr; // 情境参考 (decision + pitfall + process)
|
|
845
|
+
const broadN = residentN + referenceN;
|
|
796
846
|
|
|
797
|
-
const lines = [];
|
|
798
|
-
// `total` is the read-set ENTRY COUNT (not bytes) — label it as 条/entries.
|
|
799
|
-
lines.push(`▸ [fabric] SessionStart (${total} ${zh ? "条" : total === 1 ? "entry" : "entries"})`);
|
|
800
|
-
// W2-2/W2-3 (KT-DEC-0027/0029): the human breadcrumb shows only the
|
|
801
|
-
// always-loaded (guideline/model) census. The on-demand (decision/pitfall/
|
|
802
|
-
// process) count line and the dropped-other-project line are retired — the
|
|
803
|
-
// decision/pitfall/process REFERENCE lives in the AI sink (title + must_read_if),
|
|
804
|
-
// and SessionStart stays silent about narrow-scoped knowledge.
|
|
805
|
-
const alwaysCounts = typeCounts(ALWAYS_TYPES);
|
|
806
|
-
lines.push(zh ? " ─ always-loaded(AI 也收到正文)─" : " ─ always-loaded (AI also gets bodies) ─");
|
|
807
|
-
lines.push(` ${alwaysCounts.length > 0 ? alwaysCounts : zh ? "(无)" : "(none)"}`);
|
|
808
847
|
const layer = c.by_layer || {};
|
|
809
848
|
const teamCount = layer.team || 0;
|
|
810
849
|
const personalCount = layer.personal || 0;
|
|
811
850
|
const projectCount = layer.project || 0;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
851
|
+
|
|
852
|
+
const lines = [];
|
|
853
|
+
// Header: total entry count + semantic_scope breakdown (KT-MOD-0001 三轴).
|
|
854
|
+
const scopeSegs = [zh ? `团队 ${teamCount}` : `team ${teamCount}`];
|
|
855
|
+
if (projectCount > 0) scopeSegs.push(zh ? `项目 ${projectCount}` : `project ${projectCount}`);
|
|
856
|
+
scopeSegs.push(zh ? `个人 ${personalCount}` : `personal ${personalCount}`);
|
|
857
|
+
const totalLabel = zh ? `共 ${total} 条` : `${total} ${total === 1 ? "entry" : "entries"}`;
|
|
858
|
+
lines.push(`▸ [fabric] ${totalLabel} · ${scopeSegs.join(" · ")}`);
|
|
859
|
+
|
|
860
|
+
// broad spine — injected this session.
|
|
861
|
+
lines.push(zh ? ` broad ${broadN} · 本会话注入` : ` broad ${broadN} · injected this session`);
|
|
862
|
+
const residentDetail = [];
|
|
863
|
+
if (g > 0) residentDetail.push(`guideline ${g}`);
|
|
864
|
+
if (m > 0) residentDetail.push(`model ${m}`);
|
|
865
|
+
const refDetail = [];
|
|
866
|
+
if (d > 0) refDetail.push(`decision ${d}`);
|
|
867
|
+
if (p > 0) refDetail.push(`pitfall ${p}`);
|
|
868
|
+
if (pr > 0) refDetail.push(`process ${pr}`);
|
|
869
|
+
const dash = zh ? "—" : "—";
|
|
870
|
+
lines.push(
|
|
871
|
+
zh
|
|
872
|
+
? ` ├ 常驻规则 ${residentN} ${residentDetail.join(" · ") || dash}`
|
|
873
|
+
: ` ├ resident ${residentN} ${residentDetail.join(" · ") || dash}`,
|
|
874
|
+
);
|
|
875
|
+
lines.push(
|
|
876
|
+
zh
|
|
877
|
+
? ` └ 情境参考 ${referenceN} ${refDetail.join(" · ") || dash}`
|
|
878
|
+
: ` └ reference ${referenceN} ${refDetail.join(" · ") || dash}`,
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
// narrow remainder — 合计 only (no per-type; it's file-specific, surfaces on edit).
|
|
882
|
+
lines.push(
|
|
883
|
+
zh
|
|
884
|
+
? ` narrow ${narrowTotal} · 编辑对应文件时浮现`
|
|
885
|
+
: ` narrow ${narrowTotal} · surfaces when you edit matching files`,
|
|
886
|
+
);
|
|
818
887
|
return lines;
|
|
819
888
|
}
|
|
820
889
|
|
|
890
|
+
// Goal H2: the SessionStart store label, scope-primary wording — `写入 X · 只读 Y`
|
|
891
|
+
// (write target receives new knowledge; the rest are read-only sources). Replaces
|
|
892
|
+
// the legacy `read-set stores: a (write), b (ro)` jargon line inline (kept local
|
|
893
|
+
// to this hook so the shared lib formatStoreLabels — used by other hooks — is
|
|
894
|
+
// untouched). Empty string when there is nothing to show.
|
|
895
|
+
function renderScopeStoreLabel(snapshot, lang) {
|
|
896
|
+
if (!snapshot || !snapshot.read_set || !Array.isArray(snapshot.read_set.stores)) return "";
|
|
897
|
+
const stores = snapshot.read_set.stores;
|
|
898
|
+
if (stores.length === 0) return "";
|
|
899
|
+
const zh = lang === "zh-CN";
|
|
900
|
+
const writeAlias = snapshot.write_target && snapshot.write_target.alias;
|
|
901
|
+
const writeStores = [];
|
|
902
|
+
const readonlyStores = [];
|
|
903
|
+
for (const s of stores) {
|
|
904
|
+
const alias = s && typeof s.alias === "string" ? s.alias : null;
|
|
905
|
+
if (alias === null) continue;
|
|
906
|
+
if (alias === writeAlias) writeStores.push(alias);
|
|
907
|
+
else readonlyStores.push(alias);
|
|
908
|
+
}
|
|
909
|
+
const segs = [];
|
|
910
|
+
if (writeStores.length > 0) segs.push((zh ? "写入 " : "write ") + writeStores.join(", "));
|
|
911
|
+
if (readonlyStores.length > 0) segs.push((zh ? "只读 " : "readonly ") + readonlyStores.join(", "));
|
|
912
|
+
if (segs.length === 0) return "";
|
|
913
|
+
return " " + segs.join(" · ");
|
|
914
|
+
}
|
|
915
|
+
|
|
821
916
|
// W2 (KT-DEC-0027/0028/0029): render the AI-facing sink — the dynamically
|
|
822
917
|
// generated "MEMORY.md" spine injected into the SessionStart context. Two
|
|
823
918
|
// type-tiered sections over the BROAD knowledge (narrow stays silent — D0029):
|
|
@@ -836,6 +931,24 @@ function renderHumanCensus(census, opts) {
|
|
|
836
931
|
// must_read_if}); `alwaysBodies` is always_bodies[] ({id,type,layer,summary,body}).
|
|
837
932
|
const REFERENCE_TYPES = new Set(["decision", "pitfall", "process"]);
|
|
838
933
|
|
|
934
|
+
// W3-B F-005: map a knowledge entry's layer to a scopeBadge() scope key
|
|
935
|
+
// (team→'team' · project→'project' · personal→'personal'). Bodies carry an
|
|
936
|
+
// explicit `.layer`; plan-context reference entries do not, so the layer is
|
|
937
|
+
// recovered from the id prefix (`team:KT-…` / `project:…` / `personal:…`), with a
|
|
938
|
+
// `KP-` id always personal. Anything unrecognized falls back to 'team' — the
|
|
939
|
+
// broadest, default tier — so the badge never renders an undefined scope.
|
|
940
|
+
const VALID_SCOPES = new Set(["team", "project", "personal"]);
|
|
941
|
+
function layerToScope(entry) {
|
|
942
|
+
const e = entry || {};
|
|
943
|
+
if (typeof e.layer === "string" && VALID_SCOPES.has(e.layer)) return e.layer;
|
|
944
|
+
const id = typeof e.id === "string" ? e.id : "";
|
|
945
|
+
const prefix = id.includes(":") ? id.slice(0, id.indexOf(":")) : "";
|
|
946
|
+
if (VALID_SCOPES.has(prefix)) return prefix;
|
|
947
|
+
// `KP-*` ids are the personal convention even without a `personal:` prefix.
|
|
948
|
+
if (/(^|:)KP-/.test(id)) return "personal";
|
|
949
|
+
return "team";
|
|
950
|
+
}
|
|
951
|
+
|
|
839
952
|
function renderAiSink(opts) {
|
|
840
953
|
const { entries, alwaysBodies, storeLabel, broadIndexBackstop, summaryMaxLen, lang } =
|
|
841
954
|
opts || {};
|
|
@@ -854,34 +967,64 @@ function renderAiSink(opts) {
|
|
|
854
967
|
let indexCount = 0; // total rendered index lines (always + reference), for the backstop.
|
|
855
968
|
|
|
856
969
|
const lines = [];
|
|
857
|
-
|
|
970
|
+
// W3-B F-005 (mockup #3): a single sectionBar title carrying the active /
|
|
971
|
+
// reference counts, replacing the legacy `[fabric:SessionStart] <store>` +
|
|
972
|
+
// flat `ALWAYS-ACTIVE RULES (...)` header. The store label is preserved on the
|
|
973
|
+
// human sink (renderScopeStoreLabel); the AI sink leads with the count title.
|
|
974
|
+
// sectionBar degrades to `# <title>` when color is off (NO_COLOR / non-TTY),
|
|
975
|
+
// byte-locked to the TS source by theme-parity.
|
|
976
|
+
lines.push(
|
|
977
|
+
sectionBar(
|
|
978
|
+
zh
|
|
979
|
+
? `Fabric Knowledge · ${bodies.length} 常驻 · ${referenceEntries.length} 参考`
|
|
980
|
+
: `Fabric Knowledge · ${bodies.length} active · ${referenceEntries.length} reference`,
|
|
981
|
+
),
|
|
982
|
+
);
|
|
858
983
|
|
|
859
|
-
// ALWAYS-ACTIVE
|
|
860
|
-
|
|
984
|
+
// ALWAYS-ACTIVE — plain sub-section label (the `RULES (...)` qualifier is kept
|
|
985
|
+
// so the contract substring + framing survive the reskin). Index-only (title +
|
|
986
|
+
// summary), never the eager body.
|
|
987
|
+
lines.push(
|
|
988
|
+
zh
|
|
989
|
+
? " ALWAYS-ACTIVE RULES (无条件适用 · 照此行遵循,正文按需取):"
|
|
990
|
+
: " ALWAYS-ACTIVE RULES (unconditional · act on the line; body on demand):",
|
|
991
|
+
);
|
|
861
992
|
if (bodies.length === 0) {
|
|
862
993
|
lines.push(zh ? " (无 always-active 条目)" : " (none)");
|
|
863
994
|
} else {
|
|
864
995
|
// KT-DEC-0036: render each always-active entry as a single index line
|
|
865
|
-
// (title + summary). The body is one cheap on-demand fetch away
|
|
866
|
-
// so injecting it on every SessionStart is a permanent context
|
|
867
|
-
// (KT-GLD-0005) we no longer pay.
|
|
996
|
+
// (scope badge + title + summary). The body is one cheap on-demand fetch away
|
|
997
|
+
// (see footer), so injecting it on every SessionStart is a permanent context
|
|
998
|
+
// tax (KT-GLD-0005) we no longer pay.
|
|
868
999
|
for (const b of bodies) {
|
|
1000
|
+
const badge = scopeBadge(layerToScope(b));
|
|
869
1001
|
const label = `[${TYPE_SINGULAR[b.type] || b.type}] ${b.id}`;
|
|
870
|
-
|
|
871
|
-
|
|
1002
|
+
// ux-w1-3: bound the always-active summary by hint_summary_max_len, mirroring
|
|
1003
|
+
// the REFERENCE must_read_if hook below — one long entry must not blow up the
|
|
1004
|
+
// SessionStart sink.
|
|
1005
|
+
const raw = typeof b.summary === "string" ? b.summary.trim() : "";
|
|
1006
|
+
const summary = truncateSummary(raw, summaryMaxLen);
|
|
1007
|
+
lines.push(
|
|
1008
|
+
summary.length > 0 ? ` ${badge} ${label} · ${summary}` : ` ${badge} ${label}`,
|
|
1009
|
+
);
|
|
872
1010
|
indexCount += 1;
|
|
873
1011
|
}
|
|
874
1012
|
}
|
|
875
1013
|
|
|
876
|
-
// REFERENCE — broad decision/pitfall/process: title + must_read_if hook.
|
|
1014
|
+
// REFERENCE — broad decision/pitfall/process: scope badge + title + must_read_if hook.
|
|
877
1015
|
if (referenceEntries.length > 0) {
|
|
878
|
-
lines.push(
|
|
1016
|
+
lines.push(
|
|
1017
|
+
zh
|
|
1018
|
+
? " REFERENCE (情境触发 · 命中 must_read_if 时 Read / fab_recall):"
|
|
1019
|
+
: " REFERENCE (situational · Read when must_read_if fires / fab_recall):",
|
|
1020
|
+
);
|
|
879
1021
|
let folded = 0;
|
|
880
1022
|
for (const e of referenceEntries) {
|
|
881
1023
|
if (backstop > 0 && indexCount >= backstop) {
|
|
882
1024
|
folded += 1;
|
|
883
1025
|
continue;
|
|
884
1026
|
}
|
|
1027
|
+
const badge = scopeBadge(layerToScope(e));
|
|
885
1028
|
const type = TYPE_SINGULAR[toPluralType(e.type)] || e.type;
|
|
886
1029
|
const rawHook =
|
|
887
1030
|
typeof e.must_read_if === "string" && e.must_read_if.length > 0
|
|
@@ -890,15 +1033,19 @@ function renderAiSink(opts) {
|
|
|
890
1033
|
? e.summary
|
|
891
1034
|
: "";
|
|
892
1035
|
const hookText = truncateSummary(rawHook, summaryMaxLen);
|
|
893
|
-
lines.push(
|
|
1036
|
+
lines.push(
|
|
1037
|
+
hookText.length > 0
|
|
1038
|
+
? ` ${badge} [${type}] ${e.id} — ${hookText}`
|
|
1039
|
+
: ` ${badge} [${type}] ${e.id}`,
|
|
1040
|
+
);
|
|
894
1041
|
indexCount += 1;
|
|
895
1042
|
}
|
|
896
1043
|
// D0028 backstop: fold the overflow tail into one marker + drift signal.
|
|
897
1044
|
if (folded > 0) {
|
|
898
1045
|
lines.push(
|
|
899
1046
|
zh
|
|
900
|
-
? ` … 另 ${folded} 条 broad 条目折叠 (broad index > backstop ${backstop}
|
|
901
|
-
: ` … ${folded} more broad entr${folded === 1 ? "y" : "ies"} folded (broad index > backstop ${backstop};
|
|
1047
|
+
? ` … 另 ${folded} 条 broad 条目折叠 (broad index > backstop ${backstop})。先跑 fabric-audit 瘦身;确需全展示再调 .fabric/fabric-config.json#broad_index_backstop (20..500)`
|
|
1048
|
+
: ` … ${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`,
|
|
902
1049
|
);
|
|
903
1050
|
}
|
|
904
1051
|
}
|
|
@@ -909,6 +1056,15 @@ function renderAiSink(opts) {
|
|
|
909
1056
|
? "取正文: fab_recall(paths), 或 Read <store>/knowledge/<type>/<id>--*.md"
|
|
910
1057
|
: "Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md",
|
|
911
1058
|
);
|
|
1059
|
+
// H6 scope discipline: this sink carries ONLY broad (always-relevant) knowledge;
|
|
1060
|
+
// narrow (file-specific) entries surface contextually via the PreToolUse hint
|
|
1061
|
+
// when you edit a matching file (KT-DEC-0029). Stops the agent from assuming
|
|
1062
|
+
// SessionStart is the whole KB.
|
|
1063
|
+
lines.push(
|
|
1064
|
+
zh
|
|
1065
|
+
? "范围: 此处仅 broad(始终相关);narrow(文件专属)在你编辑对应文件时由 PreToolUse 浮现"
|
|
1066
|
+
: "Scope: broad only (always relevant) here; narrow (file-specific) surfaces via the PreToolUse hint when you edit a matching file",
|
|
1067
|
+
);
|
|
912
1068
|
return lines.join("\n");
|
|
913
1069
|
}
|
|
914
1070
|
|
|
@@ -920,7 +1076,7 @@ function renderAiSink(opts) {
|
|
|
920
1076
|
// Block 5 (Option X): build the two SessionStart sinks (human systemMessage +
|
|
921
1077
|
// AI additionalContext) from a plan-context-hint payload, WITHOUT emitting or
|
|
922
1078
|
// recording telemetry. This is the single shared renderer: main() calls it then
|
|
923
|
-
// emits + logs; `fabric
|
|
1079
|
+
// emits + logs; `fabric inspect` calls it then prints (byte-identical injection
|
|
924
1080
|
// by construction — same code, same config/FS reads). Pure-ish: it reads config
|
|
925
1081
|
// + snapshot + .md summaries for `cwd` but has no stdout/ledger side effects.
|
|
926
1082
|
//
|
|
@@ -939,7 +1095,6 @@ function buildSessionStartSinks(cwd, payload, env) {
|
|
|
939
1095
|
// review-time cold-eval audit pass.
|
|
940
1096
|
const resolvedPayload = payload;
|
|
941
1097
|
|
|
942
|
-
const recommendImport = shouldRecommendImport(cwd);
|
|
943
1098
|
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
944
1099
|
const fabricLanguageForEmit = readFabricLanguage(cwd);
|
|
945
1100
|
|
|
@@ -956,6 +1111,24 @@ function buildSessionStartSinks(cwd, payload, env) {
|
|
|
956
1111
|
? payload.always_bodies
|
|
957
1112
|
: [];
|
|
958
1113
|
|
|
1114
|
+
// H3: the LIVE census total is the single count source for the import gate —
|
|
1115
|
+
// computed AFTER census so the nudge and the HUD agree by construction.
|
|
1116
|
+
const censusTotal = census && typeof census.total === "number" ? census.total : undefined;
|
|
1117
|
+
const recommendImport = shouldRecommendImport(cwd, censusTotal);
|
|
1118
|
+
|
|
1119
|
+
// Read the resolved-bindings snapshot ONCE — reused for the scope store label
|
|
1120
|
+
// (写入/只读) and the H4 review-rung pending count. Best-effort/decorative: any
|
|
1121
|
+
// failure leaves snapshot null and the dependent lines simply don't render.
|
|
1122
|
+
let snapshot = null;
|
|
1123
|
+
if (bindingsSnapshotReader !== null) {
|
|
1124
|
+
try {
|
|
1125
|
+
const bindingId = readWorkspaceBindingId(cwd);
|
|
1126
|
+
if (bindingId) snapshot = bindingsSnapshotReader.readBindingsSnapshot(bindingId);
|
|
1127
|
+
} catch {
|
|
1128
|
+
snapshot = null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
959
1132
|
const humanGate =
|
|
960
1133
|
nudgePolicy !== null
|
|
961
1134
|
? nudgePolicy.resolveHumanSink(cwd, "session_start", {})
|
|
@@ -967,33 +1140,46 @@ function buildSessionStartSinks(cwd, payload, env) {
|
|
|
967
1140
|
const detail = renderSummary(resolvedPayload, summaryMaxLen);
|
|
968
1141
|
humanLines.push(...detail);
|
|
969
1142
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1143
|
+
// H2: scope store label — `写入 X · 只读 Y` (replaces the legacy read-set jargon).
|
|
1144
|
+
if (humanLines.length > 0 && snapshot !== null) {
|
|
1145
|
+
const storeLabel = renderScopeStoreLabel(snapshot, fabricLanguageForEmit);
|
|
1146
|
+
if (storeLabel) humanLines.push(storeLabel);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// H4 action ladder (KT-DEC-0007: nudge, never a gate). AT MOST ONE line, the
|
|
1150
|
+
// highest-priority rung wins, and steady state is fully silent:
|
|
1151
|
+
// 1. import — KB is sparse (recommendImport, off the live census total)
|
|
1152
|
+
// 2. review — pending backlog exceeds REVIEW_PENDING_THRESHOLD (live count)
|
|
1153
|
+
// 3. (silent)
|
|
1154
|
+
if (humanLines.length > 0 && fabricLanguageForEmit !== null) {
|
|
1155
|
+
if (recommendImport) {
|
|
1156
|
+
humanLines.push(renderBanner("broadImportBanner", fabricLanguageForEmit, {}));
|
|
1157
|
+
} else if (snapshot !== null && bindingsSnapshotReader !== null) {
|
|
1158
|
+
let pendingCount = 0;
|
|
1159
|
+
try {
|
|
1160
|
+
const live = bindingsSnapshotReader.liveKnowledgeStats(snapshot);
|
|
1161
|
+
if (live && Number.isFinite(live.pendingCount)) pendingCount = Math.floor(live.pendingCount);
|
|
1162
|
+
} catch {
|
|
1163
|
+
pendingCount = 0;
|
|
1164
|
+
}
|
|
1165
|
+
if (pendingCount > REVIEW_PENDING_THRESHOLD) {
|
|
1166
|
+
humanLines.push(
|
|
1167
|
+
fabricLanguageForEmit === "zh-CN"
|
|
1168
|
+
? ` 📋 Fabric: ${pendingCount} 条 pending 待审,是否调 /fabric-review?`
|
|
1169
|
+
: ` 📋 Fabric: ${pendingCount} pending entries — run /fabric-review?`,
|
|
976
1170
|
);
|
|
977
|
-
if (label) humanLines.push(label);
|
|
978
1171
|
}
|
|
979
|
-
} catch {
|
|
980
|
-
// store labels are decorative provenance — never crash the hook
|
|
981
1172
|
}
|
|
982
1173
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1174
|
+
|
|
1175
|
+
// H5: the `下一步: …fab_recall…` AI-plumbing line is retired from the human sink
|
|
1176
|
+
// (the AI gets it from its own footer + the MCP server directive). Keep only the
|
|
1177
|
+
// pointer to the byte-identical inspector for this injection.
|
|
986
1178
|
if (humanLines.length > 0) {
|
|
987
1179
|
humanLines.push(
|
|
988
1180
|
fabricLanguageForEmit === "zh-CN"
|
|
989
|
-
? "
|
|
990
|
-
: "
|
|
991
|
-
);
|
|
992
|
-
// Block 5 (Option X): point to the byte-identical inspector for this injection.
|
|
993
|
-
humanLines.push(
|
|
994
|
-
fabricLanguageForEmit === "zh-CN"
|
|
995
|
-
? "看具体注入: fabric context (--explain 看每条来源)"
|
|
996
|
-
: "Inspect this injection: fabric context (--explain for per-entry provenance)",
|
|
1181
|
+
? " 看具体注入: fabric inspect (--explain 看每条来源)"
|
|
1182
|
+
: " Inspect this injection: fabric inspect (--explain for per-entry provenance)",
|
|
997
1183
|
);
|
|
998
1184
|
}
|
|
999
1185
|
|
|
@@ -1058,7 +1244,7 @@ function main(env, stdio) {
|
|
|
1058
1244
|
// per-line char cap + broad_index_backstop fold, not by dropping entries.
|
|
1059
1245
|
|
|
1060
1246
|
// Block 5 (Option X): build both sinks via the shared renderer (same code
|
|
1061
|
-
// `fabric
|
|
1247
|
+
// `fabric inspect` uses → byte-identical injection). Side-effect-free; the
|
|
1062
1248
|
// emit + telemetry below stay in main().
|
|
1063
1249
|
const { human, ai, resolvedPayload, hasRenderedContent, reminderToContext } =
|
|
1064
1250
|
buildSessionStartSinks(cwd, payload, env);
|
|
@@ -1141,7 +1327,7 @@ function main(env, stdio) {
|
|
|
1141
1327
|
rendered_ids: renderedIds,
|
|
1142
1328
|
delivery_status: "delivered",
|
|
1143
1329
|
};
|
|
1144
|
-
|
|
1330
|
+
appendEvent(fabricDir, surfaceEvent);
|
|
1145
1331
|
}
|
|
1146
1332
|
} catch {
|
|
1147
1333
|
// best-effort telemetry — never block session start
|
|
@@ -1167,6 +1353,10 @@ module.exports = {
|
|
|
1167
1353
|
renderFull,
|
|
1168
1354
|
renderTruncated,
|
|
1169
1355
|
renderSummary,
|
|
1356
|
+
// W3-B F-005: the reskinned SessionStart AI sink + its layer→scope mapper,
|
|
1357
|
+
// exported for the NO_COLOR snapshot test (hud-reskin.test.ts).
|
|
1358
|
+
renderAiSink,
|
|
1359
|
+
layerToScope,
|
|
1170
1360
|
truncateSummary,
|
|
1171
1361
|
// rc.8 underseed self-check helpers (exported for unit testing).
|
|
1172
1362
|
countCanonicalNodes,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* [<id>] (<type>/<maturity>) <summary-line>
|
|
20
20
|
* [<id>] (<type>/<maturity>) <summary-line>
|
|
21
21
|
* ...
|
|
22
|
-
* (如需重读 broad
|
|
22
|
+
* (如需重读 broad 决策,跑 fabric plan-context-hint --all)
|
|
23
23
|
*
|
|
24
24
|
* When narrow.length === 0: complete silence (exit 0, no stderr).
|
|
25
25
|
*
|
|
@@ -1215,7 +1215,7 @@ function readSummaryMaxLen(projectRoot) {
|
|
|
1215
1215
|
* [fabric] N narrow-scoped knowledge entries match your edit targets:
|
|
1216
1216
|
* [<id>] (<type>/<maturity>) <summary>
|
|
1217
1217
|
* ...
|
|
1218
|
-
* (如需重读 broad
|
|
1218
|
+
* (如需重读 broad 决策,跑 fabric plan-context-hint --all)
|
|
1219
1219
|
*/
|
|
1220
1220
|
function renderSummary(payload, maxLen) {
|
|
1221
1221
|
if (!payload || payload.version !== 2) {
|
|
@@ -1242,7 +1242,7 @@ function renderSummary(payload, maxLen) {
|
|
|
1242
1242
|
for (const entry of entries) {
|
|
1243
1243
|
lines.push(formatEntryLine(entry, maxLen));
|
|
1244
1244
|
}
|
|
1245
|
-
lines.push(" (如需重读 broad
|
|
1245
|
+
lines.push(" (如需重读 broad 决策,跑 fabric plan-context-hint --all)");
|
|
1246
1246
|
return lines;
|
|
1247
1247
|
}
|
|
1248
1248
|
|