@fenglimg/fabric-cli 2.2.0-rc.4 → 2.2.0-rc.8
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 +8 -5
- package/dist/{chunk-5JG4QJLO.js → chunk-27HK6H5Y.js} +10 -5
- package/dist/{chunk-F6ITRM7T.js → chunk-2KBCTMID.js} +29 -6
- package/dist/{chunk-XC5RUHLK.js → chunk-3IOLS5EK.js} +23 -38
- package/dist/{chunk-XHHCRDIR.js → chunk-CMDW3PYK.js} +105 -220
- package/dist/chunk-FEOPLBGA.js +150 -0
- package/dist/{chunk-XCBVSGCS.js → chunk-FNHDQTPC.js} +1 -10
- package/dist/{chunk-2CY4BMTH.js → chunk-HORSMSZL.js} +9 -5
- package/dist/{doctor-U5W4CX5I.js → chunk-JTHWLUD3.js} +103 -51
- package/dist/{chunk-BO4XIZWZ.js → chunk-NLNH64A3.js} +5 -18
- package/dist/{chunk-H3FE6VIK.js → chunk-PTGQAZEW.js} +13 -3
- package/dist/chunk-QFIVFZRH.js +13 -0
- package/dist/{chunk-5SSNE5GM.js → chunk-QPAW6IYT.js} +125 -39
- package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
- package/dist/{plan-context-hint-CHVZGOZ5.js → chunk-YM4XATJF.js} +29 -4
- package/dist/{config-VJMXCLXW.js → config-A3LTECAY.js} +4 -3
- package/dist/context-7NUKXDB6.js +117 -0
- package/dist/doctor-REZDNH4A.js +24 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +131 -21
- package/dist/info-7FKBTMVO.js +139 -0
- package/dist/install-v2-2COC3DO3.js +3277 -0
- package/dist/{metrics-RER6NLFC.js → metrics-HMFH4YHK.js} +1 -1
- package/dist/{onboard-coverage-JWQWDZW7.js → onboard-coverage-XSG77LL3.js} +48 -27
- package/dist/plan-context-hint-G75R4P4J.js +12 -0
- package/dist/{scope-explain-BWRWBCCP.js → scope-explain-HLJZ2M33.js} +3 -2
- package/dist/{status-7UFLWRX7.js → status-4R3TM4FJ.js} +8 -5
- package/dist/{store-ZEZMQVG7.js → store-HOCORVL3.js} +96 -350
- package/dist/{sync-EA5HZMXM.js → sync-DT5UJMMR.js} +36 -13
- package/dist/{uninstall-F75MPKQC.js → uninstall-62F4LNKI.js} +62 -140
- package/dist/{whoami-3FRWYGML.js → whoami-ITGEFWH4.js} +9 -7
- package/package.json +7 -5
- package/templates/hooks/cite-policy-evict.cjs +5 -5
- package/templates/hooks/configs/README.md +14 -27
- package/templates/hooks/configs/claude-code.json +1 -1
- package/templates/hooks/configs/codex-hooks.json +3 -3
- package/templates/hooks/fabric-hint.cjs +301 -161
- package/templates/hooks/knowledge-hint-broad.cjs +426 -207
- package/templates/hooks/knowledge-hint-narrow.cjs +56 -56
- package/templates/hooks/lib/banner-i18n.cjs +31 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +117 -7
- package/templates/hooks/lib/cite-line-parser.cjs +12 -20
- package/templates/hooks/lib/client-adapter.cjs +66 -7
- package/templates/hooks/lib/nudge-policy.cjs +117 -0
- package/templates/hooks/lib/state-store.cjs +60 -0
- package/templates/hooks/lib/summary-fallback.cjs +82 -19
- package/templates/hooks/post-tooluse-mutation.cjs +112 -11
- package/templates/skills/fabric/SKILL.md +94 -0
- package/templates/skills/fabric-archive/SKILL.md +29 -26
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
- package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
- package/templates/skills/fabric-audit/SKILL.md +13 -3
- package/templates/skills/fabric-connect/SKILL.md +3 -3
- package/templates/skills/fabric-import/SKILL.md +7 -7
- package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
- package/templates/skills/fabric-review/SKILL.md +5 -5
- package/templates/skills/fabric-review/ref/cite-contract.md +1 -1
- package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-review/ref/output-contract.md +1 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
- package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
- package/templates/skills/fabric-store/SKILL.md +1 -1
- package/templates/skills/fabric-sync/SKILL.md +1 -1
- package/templates/skills/lib/shared-policy.md +2 -2
- package/dist/install-7XJ64WSC.js +0 -2743
- package/templates/hooks/configs/cursor-hooks.json +0 -30
- package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
|
@@ -12,28 +12,21 @@
|
|
|
12
12
|
* narrow-injection sibling (E2, knowledge-hint-narrow.cjs) handles
|
|
13
13
|
* per-Edit/Write hints with a session-hints cache.
|
|
14
14
|
*
|
|
15
|
-
* Output contract (
|
|
15
|
+
* Output contract (W2 / KT-DEC-0027/0028/0029 — the SessionStart spine):
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* [fabric]
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Load full content:
|
|
17
|
+
* AI sink (additionalContext) — the dynamically generated "MEMORY.md":
|
|
18
|
+
* [fabric:SessionStart] <store>
|
|
19
|
+
* ALWAYS-ACTIVE RULES (no recall needed): # guideline/model — full BODY
|
|
20
|
+
* [guideline] team:KT-GLD-0001
|
|
21
|
+
* <full body> # over budget → degrade to index line
|
|
22
|
+
* REFERENCE (read on demand / fab_recall): # decision/pitfall/process — title + hook
|
|
23
|
+
* [decision] team:KT-DEC-0001 — <must_read_if>
|
|
24
|
+
* … N more folded (broad index > backstop 50; run fabric-audit)
|
|
25
|
+
* Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - <id> · <summary>
|
|
31
|
-
* - ...
|
|
32
|
-
* [decision] verified (12): <id1>, <id2>, ...
|
|
33
|
-
* [decision] draft: 7 entries
|
|
34
|
-
* ...
|
|
35
|
-
* revision_hash: <hash>
|
|
36
|
-
* Load full content: `fab_recall(paths)`, or `fab_plan_context` -> `fab_get_knowledge_sections` to trim first.
|
|
27
|
+
* Human sink (systemMessage) — broad-only census breadcrumb; SessionStart is
|
|
28
|
+
* SILENT about narrow-scoped knowledge (no on-demand counts, no
|
|
29
|
+
* dropped-other-project line).
|
|
37
30
|
*
|
|
38
31
|
* When 0 entries / CLI unavailable / CLI error / parse failure:
|
|
39
32
|
* (no output — silent exit 0)
|
|
@@ -74,7 +67,18 @@ const {
|
|
|
74
67
|
const { readTextState, writeTextState } = require("./lib/state-store.cjs");
|
|
75
68
|
// v2.0.0-rc.37 NEW-30: shared client detection (replaces the inline
|
|
76
69
|
// CLAUDE_PROJECT_DIR single-bit check below).
|
|
77
|
-
|
|
70
|
+
// v2.2 dual-sink (Goal A): + emitDualSink (two-channel SessionStart emit).
|
|
71
|
+
const { isClaudeCode, detectClient, emitDualSink } = require("./lib/client-adapter.cjs");
|
|
72
|
+
// v2.2 dual-sink (Goal A / D4): human-output gate (nudge_mode + observe.*). Only
|
|
73
|
+
// governs the human systemMessage — the AI additionalContext is emitted
|
|
74
|
+
// regardless (flow ⊥ observation). Optional require so an old install lacking the
|
|
75
|
+
// lib degrades to "always emit human" (the pre-dual-sink default).
|
|
76
|
+
let nudgePolicy = null;
|
|
77
|
+
try {
|
|
78
|
+
nudgePolicy = require("./lib/nudge-policy.cjs");
|
|
79
|
+
} catch {
|
|
80
|
+
// Lib missing (old install) — human sink always emits (legacy behavior).
|
|
81
|
+
}
|
|
78
82
|
// v2.1.0-rc.1 P4 (F4/S63): hook-side reader for the CLI pre-generated
|
|
79
83
|
// resolved-bindings snapshot. The hook NEVER re-resolves stores or walks store
|
|
80
84
|
// trees — it only echoes the read-set the CLI already computed. Best-effort.
|
|
@@ -94,19 +98,46 @@ try {
|
|
|
94
98
|
// Lib missing (old install) — injection telemetry degrades to silent absence.
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
// Read the
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
function readProjectId(cwd) {
|
|
101
|
+
// Read the workspace binding id from `.fabric/fabric-config.json` (the snapshot
|
|
102
|
+
// key). Defaults to project_id when workspace_binding_id is absent.
|
|
103
|
+
function readWorkspaceBindingId(cwd) {
|
|
101
104
|
try {
|
|
102
105
|
const raw = readFileSync(join(cwd, ".fabric", "fabric-config.json"), "utf8");
|
|
103
106
|
const parsed = JSON.parse(raw);
|
|
107
|
+
if (typeof parsed.workspace_binding_id === "string") return parsed.workspace_binding_id;
|
|
104
108
|
return typeof parsed.project_id === "string" ? parsed.project_id : null;
|
|
105
109
|
} catch {
|
|
106
110
|
return null;
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
function readSnapshotCanonicalCount(projectRoot) {
|
|
115
|
+
if (bindingsSnapshotReader === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const bindingId = readWorkspaceBindingId(projectRoot);
|
|
119
|
+
if (bindingId === null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const snapshot = bindingsSnapshotReader.readBindingsSnapshot(bindingId);
|
|
124
|
+
// LIVE recount off the snapshot's resolved store dirs. The cached
|
|
125
|
+
// knowledge_stats.canonical_count is frozen at snapshot-write time and goes
|
|
126
|
+
// stale when store content syncs in out-of-band (e.g. the store grew from 1
|
|
127
|
+
// → 57 nodes via a `git pull`/cross-workspace sync that never regenerated
|
|
128
|
+
// THIS workspace's snapshot), which mis-fired the "knowledge sparse"
|
|
129
|
+
// underseed nudge (KT-PIT-0017, same stale-projection root cause).
|
|
130
|
+
const live = bindingsSnapshotReader.liveKnowledgeStats(snapshot);
|
|
131
|
+
if (live && Number.isFinite(live.canonicalCount) && live.canonicalCount > 0) {
|
|
132
|
+
return Math.floor(live.canonicalCount);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// best-effort hint stats only
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
110
141
|
// -----------------------------------------------------------------------------
|
|
111
142
|
// rc.12: SessionStart broad-menu is now unconditionally emitted on every
|
|
112
143
|
// SessionStart fire (matching Skill-style progressive disclosure). Prior
|
|
@@ -124,7 +155,6 @@ const FABRIC_DIR_REL = ".fabric";
|
|
|
124
155
|
// cannot `require` each other. If a third hook ever needs the same logic,
|
|
125
156
|
// refactor into packages/cli/templates/hooks/lib/. Keep these values in sync
|
|
126
157
|
// with packages/cli/templates/hooks/fabric-hint.cjs.
|
|
127
|
-
const AGENTS_META_FILE = "agents.meta.json";
|
|
128
158
|
const IMPORT_STATE_FILE = ".import-state.json";
|
|
129
159
|
const KNOWLEDGE_CANONICAL_TYPES = [
|
|
130
160
|
"decisions",
|
|
@@ -135,11 +165,12 @@ const KNOWLEDGE_CANONICAL_TYPES = [
|
|
|
135
165
|
];
|
|
136
166
|
const DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
137
167
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
168
|
+
// W2-1 (KT-DEC-0028): the broad index is shown in FULL (no top-K hard cap). The
|
|
169
|
+
// only scale guard is a backstop: when the rendered broad index exceeds this
|
|
170
|
+
// many lines, the overflow tail folds into one marker that doubles as a drift
|
|
171
|
+
// signal (the W4 doctor `broad-index-drift` lint is the authoritative detector).
|
|
172
|
+
// Overridable via fabric-config.json#broad_index_backstop (W4 schema range 20..500).
|
|
173
|
+
const DEFAULT_HINT_BROAD_INDEX_BACKSTOP = 50;
|
|
143
174
|
|
|
144
175
|
// v2.0.0-rc.33 W2-5 (P1-8): cooldown (in hours) between broad-hint re-emits.
|
|
145
176
|
// Default 0 preserves rc.32 behavior — every SessionStart re-fires the banner.
|
|
@@ -169,37 +200,13 @@ const DEFAULT_HINT_REMINDER_TO_CONTEXT = true;
|
|
|
169
200
|
// -----------------------------------------------------------------------------
|
|
170
201
|
|
|
171
202
|
/**
|
|
172
|
-
* Count canonical knowledge entries
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* Returns the integer count. ENOENT / unreadable subdir → silently treated as
|
|
177
|
-
* zero (preserves never-block-on-failure invariant). Filters on `.md` suffix
|
|
178
|
-
* only; the more-precise canonical filename pattern check is owned by
|
|
179
|
-
* doctor.ts (the hook is a coarse signal, not a lint).
|
|
203
|
+
* Count canonical knowledge entries from the CLI-generated resolved-bindings
|
|
204
|
+
* snapshot. Store-only: hooks never walk project-local knowledge or store
|
|
205
|
+
* trees — a missing snapshot degrades to zero (KT-DEC-0007).
|
|
180
206
|
*/
|
|
181
207
|
function countCanonicalNodes(projectRoot) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
return 0;
|
|
185
|
-
}
|
|
186
|
-
let count = 0;
|
|
187
|
-
for (const type of KNOWLEDGE_CANONICAL_TYPES) {
|
|
188
|
-
const typeDir = join(knowledgeRoot, type);
|
|
189
|
-
if (!existsSync(typeDir)) continue;
|
|
190
|
-
let entries;
|
|
191
|
-
try {
|
|
192
|
-
entries = readdirSync(typeDir);
|
|
193
|
-
} catch {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
for (const entry of entries) {
|
|
197
|
-
if (entry.endsWith(".md")) {
|
|
198
|
-
count += 1;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return count;
|
|
208
|
+
const snapshotCount = readSnapshotCanonicalCount(projectRoot);
|
|
209
|
+
return snapshotCount === null ? 0 : snapshotCount;
|
|
203
210
|
}
|
|
204
211
|
|
|
205
212
|
/**
|
|
@@ -216,14 +223,15 @@ function readUnderseedThreshold(projectRoot) {
|
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
/**
|
|
219
|
-
*
|
|
220
|
-
* the broad
|
|
221
|
-
* schema's
|
|
226
|
+
* W2-1 (KT-DEC-0028): resolve broad_index_backstop from fabric-config.json. Caps
|
|
227
|
+
* the rendered broad index line count; the overflow tail folds into a drift
|
|
228
|
+
* marker. Validates the W4 schema's 20..500 range inline so a malformed config
|
|
229
|
+
* silently falls back to the default.
|
|
222
230
|
*/
|
|
223
|
-
function
|
|
224
|
-
return readConfigNumber(projectRoot, "
|
|
225
|
-
min:
|
|
226
|
-
max:
|
|
231
|
+
function readBroadIndexBackstop(projectRoot) {
|
|
232
|
+
return readConfigNumber(projectRoot, "broad_index_backstop", DEFAULT_HINT_BROAD_INDEX_BACKSTOP, {
|
|
233
|
+
min: 20,
|
|
234
|
+
max: 500,
|
|
227
235
|
floor: true,
|
|
228
236
|
});
|
|
229
237
|
}
|
|
@@ -309,9 +317,11 @@ function isImportTouched(projectRoot) {
|
|
|
309
317
|
* surface the one-line `/fabric-import` recommendation banner.
|
|
310
318
|
*
|
|
311
319
|
* Three-condition truth table (ALL must hold to return true):
|
|
312
|
-
* 1.
|
|
313
|
-
* (
|
|
314
|
-
* is meaningless — `fabric-import` requires
|
|
320
|
+
* 1. the workspace is fabric-bound — readWorkspaceBindingId(cwd) !== null
|
|
321
|
+
* (a resolved binding id in fabric-config.json; otherwise the
|
|
322
|
+
* recommendation is meaningless — `fabric-import` requires a bound
|
|
323
|
+
* workspace. Store-only: replaces the legacy derived-index-file
|
|
324
|
+
* existence probe.)
|
|
315
325
|
* 2. countCanonicalNodes(cwd) < readUnderseedThreshold(cwd)
|
|
316
326
|
* (knowledge graph is sparse — import would meaningfully enrich it).
|
|
317
327
|
* 3. isImportTouched(cwd) === 'absent'
|
|
@@ -324,8 +334,7 @@ function isImportTouched(projectRoot) {
|
|
|
324
334
|
*/
|
|
325
335
|
function shouldRecommendImport(projectRoot) {
|
|
326
336
|
try {
|
|
327
|
-
|
|
328
|
-
if (!existsSync(metaPath)) return false;
|
|
337
|
+
if (readWorkspaceBindingId(projectRoot) === null) return false;
|
|
329
338
|
|
|
330
339
|
const threshold = readUnderseedThreshold(projectRoot);
|
|
331
340
|
const nodeCount = countCanonicalNodes(projectRoot);
|
|
@@ -745,26 +754,327 @@ function renderSummary(payload, maxLen, budgetChars) {
|
|
|
745
754
|
}
|
|
746
755
|
}
|
|
747
756
|
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
// does not yet have — directly contradicting the bilingual next-step nudge
|
|
752
|
-
// (and AGENTS.md) which leads with `fab_recall`. Footer now states the same
|
|
753
|
-
// two-path model: single-step `fab_recall`, or `fab_plan_context` →
|
|
754
|
-
// `fab_get_knowledge_sections` when the bodies must be trimmed first. Keeps
|
|
755
|
-
// the `fab_get_knowledge_sections` token (downstream substring contracts) but
|
|
756
|
-
// sequences it correctly behind the token-issuing `fab_plan_context`.
|
|
757
|
+
// W2-4 (KT-DEC-0026): single lean retrieval flow. The two-step
|
|
758
|
+
// fab_plan_context → fab_get_knowledge_sections footer is retired — fab_recall
|
|
759
|
+
// returns descriptions + read paths, and bodies load via a native Read.
|
|
757
760
|
lines.push(
|
|
758
|
-
" Load full content: `fab_recall(paths)
|
|
761
|
+
" Load full content: `fab_recall(paths)`, or Read `<store>/knowledge/<type>/<id>--*.md` directly.",
|
|
759
762
|
);
|
|
760
763
|
return lines;
|
|
761
764
|
}
|
|
762
765
|
|
|
766
|
+
// -----------------------------------------------------------------------------
|
|
767
|
+
// v2.2 dual-sink (Goal A): two-sink SessionStart rendering.
|
|
768
|
+
//
|
|
769
|
+
// HUMAN sink (systemMessage): a grouped census (§3 / D8) — always-loaded vs
|
|
770
|
+
// on-demand split + [team]/[personal] + ✗ dropped-other-project. Count-summary
|
|
771
|
+
// form (not a per-entry wall of text); the verbose nudge_mode appends the legacy
|
|
772
|
+
// per-entry renderSummary listing on top.
|
|
773
|
+
//
|
|
774
|
+
// AI sink (additionalContext): the always-active (guideline/model) BODIES (§3 /
|
|
775
|
+
// D9), bounded by the injection char budget with overflow degrading to summary +
|
|
776
|
+
// a recall pointer, followed by on-demand category counts. Replaces the legacy
|
|
777
|
+
// top_k=8 id-list that used to be the AI payload.
|
|
778
|
+
// -----------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
// Singular display label for a plural knowledge_type.
|
|
781
|
+
const TYPE_SINGULAR = {
|
|
782
|
+
decisions: "decision",
|
|
783
|
+
pitfalls: "pitfall",
|
|
784
|
+
guidelines: "guideline",
|
|
785
|
+
models: "model",
|
|
786
|
+
processes: "process",
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const ALWAYS_TYPES = ["guidelines", "models"];
|
|
790
|
+
|
|
791
|
+
// Normalize a knowledge_type to its canonical PLURAL form. Frontmatter / entries
|
|
792
|
+
// may carry the singular ("decision") while the census keys on the plural enum
|
|
793
|
+
// ("decisions"); fold both so counting + display stay consistent.
|
|
794
|
+
const TYPE_TO_PLURAL = {
|
|
795
|
+
decision: "decisions",
|
|
796
|
+
pitfall: "pitfalls",
|
|
797
|
+
guideline: "guidelines",
|
|
798
|
+
model: "models",
|
|
799
|
+
process: "processes",
|
|
800
|
+
};
|
|
801
|
+
function toPluralType(type) {
|
|
802
|
+
return TYPE_TO_PLURAL[type] || type;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Fallback census when payload.census is absent (old CLI / unit-test payloads):
|
|
806
|
+
// count the (possibly sliced) entries by knowledge_type so the human banner still
|
|
807
|
+
// has something to group. Production payloads always carry the unsliced census.
|
|
808
|
+
function deriveCensusFromEntries(entries) {
|
|
809
|
+
const census = { by_type: {}, by_layer: { team: 0, personal: 0, project: 0 }, dropped_other_project: 0, total: 0 };
|
|
810
|
+
if (!Array.isArray(entries)) return census;
|
|
811
|
+
for (const e of entries) {
|
|
812
|
+
const type = e && typeof e.type === "string" ? toPluralType(e.type) : null;
|
|
813
|
+
if (type === null) continue;
|
|
814
|
+
census.by_type[type] = (census.by_type[type] || 0) + 1;
|
|
815
|
+
census.total += 1;
|
|
816
|
+
}
|
|
817
|
+
return census;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Render the human-facing grouped census (§3). `lang` is "zh-CN" | other (en).
|
|
821
|
+
// Returns an array of lines (may be empty when the census is empty).
|
|
822
|
+
function renderHumanCensus(census, opts) {
|
|
823
|
+
const { lang } = opts || {};
|
|
824
|
+
const c = census || {};
|
|
825
|
+
const byType = c.by_type || {};
|
|
826
|
+
const total = typeof c.total === "number" ? c.total : 0;
|
|
827
|
+
if (total === 0 && (c.dropped_other_project || 0) === 0) return [];
|
|
828
|
+
const zh = lang === "zh-CN";
|
|
829
|
+
|
|
830
|
+
const typeCounts = (types) =>
|
|
831
|
+
types
|
|
832
|
+
.filter((t) => (byType[t] || 0) > 0)
|
|
833
|
+
.map((t) => `${TYPE_SINGULAR[t] || t} ${byType[t]}`)
|
|
834
|
+
.join(" · ");
|
|
835
|
+
|
|
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
|
+
const layer = c.by_layer || {};
|
|
848
|
+
const teamCount = layer.team || 0;
|
|
849
|
+
const personalCount = layer.personal || 0;
|
|
850
|
+
const projectCount = layer.project || 0;
|
|
851
|
+
if (teamCount > 0 || personalCount > 0 || projectCount > 0) {
|
|
852
|
+
const segs = [`[team] ${teamCount}`];
|
|
853
|
+
if (projectCount > 0) segs.push(`[project] ${projectCount}`);
|
|
854
|
+
segs.push(`[personal] ${personalCount}`);
|
|
855
|
+
lines.push(` ${segs.join(" · ")}`);
|
|
856
|
+
}
|
|
857
|
+
return lines;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// W2 (KT-DEC-0027/0028/0029): render the AI-facing sink — the dynamically
|
|
861
|
+
// generated "MEMORY.md" spine injected into the SessionStart context. Two
|
|
862
|
+
// type-tiered sections over the BROAD knowledge (narrow stays silent — D0029):
|
|
863
|
+
//
|
|
864
|
+
// ALWAYS-ACTIVE RULES (guideline/model): full BODIES injected, bounded by
|
|
865
|
+
// budgetChars. On overflow each entry degrades to an INDEX LINE (title +
|
|
866
|
+
// summary), NOT a folded count (D0028) — the entry stays individually
|
|
867
|
+
// visible and fetchable.
|
|
868
|
+
// REFERENCE (decision/pitfall/process): TITLE + must_read_if hook only
|
|
869
|
+
// (situational; the agent Reads the body on demand) — never the body.
|
|
870
|
+
//
|
|
871
|
+
// `broadIndexBackstop` (D0028) caps the total rendered index lines; the overflow
|
|
872
|
+
// tail folds into one marker that doubles as the drift signal (fabric-audit /
|
|
873
|
+
// the W4 doctor lint is the authoritative detector). `entries` is the broad
|
|
874
|
+
// plan-context-hint entry list ({id,type,maturity,summary,relevance_scope,
|
|
875
|
+
// must_read_if}); `alwaysBodies` is always_bodies[] ({id,type,layer,summary,body}).
|
|
876
|
+
const REFERENCE_TYPES = new Set(["decision", "pitfall", "process"]);
|
|
877
|
+
|
|
878
|
+
function renderAiSink(opts) {
|
|
879
|
+
const { entries, alwaysBodies, storeLabel, budgetChars, broadIndexBackstop, summaryMaxLen, lang } =
|
|
880
|
+
opts || {};
|
|
881
|
+
const zh = lang === "zh-CN";
|
|
882
|
+
const bodies = Array.isArray(alwaysBodies) ? alwaysBodies : [];
|
|
883
|
+
// REFERENCE = broad decision/pitfall/process. narrow entries stay silent (D0029).
|
|
884
|
+
const referenceEntries = (Array.isArray(entries) ? entries : []).filter((e) => {
|
|
885
|
+
if (!e || e.relevance_scope === "narrow") return false;
|
|
886
|
+
return REFERENCE_TYPES.has(TYPE_SINGULAR[toPluralType(e.type)] || e.type);
|
|
887
|
+
});
|
|
888
|
+
// Nothing to inject → empty so main() stays silent on an empty knowledge base.
|
|
889
|
+
if (bodies.length === 0 && referenceEntries.length === 0) return "";
|
|
890
|
+
|
|
891
|
+
const backstop =
|
|
892
|
+
typeof broadIndexBackstop === "number" && broadIndexBackstop > 0 ? broadIndexBackstop : 0;
|
|
893
|
+
let indexCount = 0; // total rendered index lines (always + reference), for the backstop.
|
|
894
|
+
|
|
895
|
+
const lines = [];
|
|
896
|
+
lines.push(`[fabric:SessionStart] ${storeLabel || "store"}`);
|
|
897
|
+
|
|
898
|
+
// ALWAYS-ACTIVE RULES — inject bodies up to the budget, degrade to index line.
|
|
899
|
+
lines.push(zh ? "ALWAYS-ACTIVE RULES (无需再 recall):" : "ALWAYS-ACTIVE RULES (no recall needed):");
|
|
900
|
+
if (bodies.length === 0) {
|
|
901
|
+
lines.push(zh ? " (无 always-active 条目)" : " (none)");
|
|
902
|
+
} else {
|
|
903
|
+
const budget = typeof budgetChars === "number" && budgetChars > 0 ? budgetChars : 0;
|
|
904
|
+
let used = 0;
|
|
905
|
+
let degraded = false;
|
|
906
|
+
for (const b of bodies) {
|
|
907
|
+
const label = `[${TYPE_SINGULAR[b.type] || b.type}] ${b.id}`;
|
|
908
|
+
const body = typeof b.body === "string" ? b.body.trim() : "";
|
|
909
|
+
const summary = typeof b.summary === "string" ? b.summary : "";
|
|
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
|
+
}
|
|
922
|
+
indexCount += 1;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// REFERENCE — broad decision/pitfall/process: title + must_read_if hook.
|
|
927
|
+
if (referenceEntries.length > 0) {
|
|
928
|
+
lines.push(zh ? "REFERENCE (按需 Read / fab_recall):" : "REFERENCE (read on demand / fab_recall):");
|
|
929
|
+
let folded = 0;
|
|
930
|
+
for (const e of referenceEntries) {
|
|
931
|
+
if (backstop > 0 && indexCount >= backstop) {
|
|
932
|
+
folded += 1;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const type = TYPE_SINGULAR[toPluralType(e.type)] || e.type;
|
|
936
|
+
const rawHook =
|
|
937
|
+
typeof e.must_read_if === "string" && e.must_read_if.length > 0
|
|
938
|
+
? e.must_read_if
|
|
939
|
+
: typeof e.summary === "string"
|
|
940
|
+
? e.summary
|
|
941
|
+
: "";
|
|
942
|
+
const hookText = truncateSummary(rawHook, summaryMaxLen);
|
|
943
|
+
lines.push(hookText.length > 0 ? ` [${type}] ${e.id} — ${hookText}` : ` [${type}] ${e.id}`);
|
|
944
|
+
indexCount += 1;
|
|
945
|
+
}
|
|
946
|
+
// D0028 backstop: fold the overflow tail into one marker + drift signal.
|
|
947
|
+
if (folded > 0) {
|
|
948
|
+
lines.push(
|
|
949
|
+
zh
|
|
950
|
+
? ` … 另 ${folded} 条 broad 条目折叠 (broad index > backstop ${backstop}; 跑 fabric-audit)`
|
|
951
|
+
: ` … ${folded} more broad entr${folded === 1 ? "y" : "ies"} folded (broad index > backstop ${backstop}; run fabric-audit)`,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// W2-4 footer: single lean retrieval flow — no two-step.
|
|
957
|
+
lines.push(
|
|
958
|
+
zh
|
|
959
|
+
? "取正文: fab_recall(paths), 或 Read <store>/knowledge/<type>/<id>--*.md"
|
|
960
|
+
: "Load full content: fab_recall(paths), or Read <store>/knowledge/<type>/<id>--*.md",
|
|
961
|
+
);
|
|
962
|
+
return lines.join("\n");
|
|
963
|
+
}
|
|
964
|
+
|
|
763
965
|
// -----------------------------------------------------------------------------
|
|
764
966
|
// Main entry — invoked both as a CLI (require.main === module) and in-process
|
|
765
967
|
// by tests. Wraps the entire flow in try/catch: ANY error → silent exit 0.
|
|
766
968
|
// -----------------------------------------------------------------------------
|
|
767
969
|
|
|
970
|
+
// Block 5 (Option X): build the two SessionStart sinks (human systemMessage +
|
|
971
|
+
// AI additionalContext) from a plan-context-hint payload, WITHOUT emitting or
|
|
972
|
+
// recording telemetry. This is the single shared renderer: main() calls it then
|
|
973
|
+
// emits + logs; `fabric context` calls it then prints (byte-identical injection
|
|
974
|
+
// by construction — same code, same config/FS reads). Pure-ish: it reads config
|
|
975
|
+
// + snapshot + .md summaries for `cwd` but has no stdout/ledger side effects.
|
|
976
|
+
//
|
|
977
|
+
// Returns:
|
|
978
|
+
// human — gated final human text (null when gated off / empty)
|
|
979
|
+
// ai — gated final AI text (null when reminder-to-context off / empty)
|
|
980
|
+
// resolvedPayload — payload with opaque summaries resolved (for telemetry / --explain)
|
|
981
|
+
// hasRenderedContent — true when ANY sink rendered content (main's silent-exit gate)
|
|
982
|
+
// reminderToContext — readReminderToContext(cwd) (telemetry target-channel)
|
|
983
|
+
function buildSessionStartSinks(cwd, payload, env) {
|
|
984
|
+
// rc.35 TASK-06: opaque-summary substitution (best-effort; failure leaves
|
|
985
|
+
// the original summary untouched).
|
|
986
|
+
let resolvedPayload = payload;
|
|
987
|
+
try {
|
|
988
|
+
if (payload && Array.isArray(payload.entries)) {
|
|
989
|
+
const resolvedEntries = resolveOpaqueSummaries(
|
|
990
|
+
payload.entries,
|
|
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
|
+
}
|
|
999
|
+
|
|
1000
|
+
const recommendImport = shouldRecommendImport(cwd);
|
|
1001
|
+
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
1002
|
+
const broadBudgetChars = readBroadBudgetChars(cwd);
|
|
1003
|
+
const fabricLanguageForEmit = readFabricLanguage(cwd);
|
|
1004
|
+
|
|
1005
|
+
const census =
|
|
1006
|
+
env && env.census !== undefined
|
|
1007
|
+
? env.census
|
|
1008
|
+
: payload && payload.census
|
|
1009
|
+
? payload.census
|
|
1010
|
+
: deriveCensusFromEntries(resolvedPayload && resolvedPayload.entries);
|
|
1011
|
+
const alwaysBodies =
|
|
1012
|
+
env && env.alwaysBodies !== undefined
|
|
1013
|
+
? env.alwaysBodies
|
|
1014
|
+
: payload && Array.isArray(payload.always_bodies)
|
|
1015
|
+
? payload.always_bodies
|
|
1016
|
+
: [];
|
|
1017
|
+
|
|
1018
|
+
const humanGate =
|
|
1019
|
+
nudgePolicy !== null
|
|
1020
|
+
? nudgePolicy.resolveHumanSink(cwd, "session_start", {})
|
|
1021
|
+
: { emitHuman: true, verbosity: "normal" };
|
|
1022
|
+
|
|
1023
|
+
// ---- HUMAN sink: §3 grouped census (+ verbose per-entry detail). ----
|
|
1024
|
+
const humanLines = renderHumanCensus(census, { lang: fabricLanguageForEmit });
|
|
1025
|
+
if (humanLines.length > 0 && humanGate.verbosity === "verbose") {
|
|
1026
|
+
const detail = renderSummary(resolvedPayload, summaryMaxLen, broadBudgetChars);
|
|
1027
|
+
humanLines.push(...detail);
|
|
1028
|
+
}
|
|
1029
|
+
if (bindingsSnapshotReader !== null && humanLines.length > 0) {
|
|
1030
|
+
try {
|
|
1031
|
+
const bindingId = readWorkspaceBindingId(cwd);
|
|
1032
|
+
if (bindingId) {
|
|
1033
|
+
const label = bindingsSnapshotReader.formatStoreLabels(
|
|
1034
|
+
bindingsSnapshotReader.readBindingsSnapshot(bindingId),
|
|
1035
|
+
);
|
|
1036
|
+
if (label) humanLines.push(label);
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
// store labels are decorative provenance — never crash the hook
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (recommendImport && humanLines.length > 0 && fabricLanguageForEmit !== null) {
|
|
1043
|
+
humanLines.push(renderBanner("broadImportBanner", fabricLanguageForEmit, {}));
|
|
1044
|
+
}
|
|
1045
|
+
if (humanLines.length > 0) {
|
|
1046
|
+
humanLines.push(
|
|
1047
|
+
fabricLanguageForEmit === "zh-CN"
|
|
1048
|
+
? "下一步: 改相关文件前调 fab_recall(paths) 拿 KB 条目的描述+读路径;按需 Read 路径取正文。"
|
|
1049
|
+
: "Next: before editing related files, call fab_recall(paths) for the KB entries' descriptions + read paths; Read a path on demand for the body.",
|
|
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)",
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ---- AI sink: spine — always-active bodies + reference, bounded. ----
|
|
1060
|
+
const broadIndexBackstop = readBroadIndexBackstop(cwd);
|
|
1061
|
+
const aiText = renderAiSink({
|
|
1062
|
+
entries: resolvedPayload && Array.isArray(resolvedPayload.entries) ? resolvedPayload.entries : [],
|
|
1063
|
+
alwaysBodies,
|
|
1064
|
+
budgetChars: broadBudgetChars,
|
|
1065
|
+
broadIndexBackstop,
|
|
1066
|
+
summaryMaxLen,
|
|
1067
|
+
lang: fabricLanguageForEmit,
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const hasRenderedContent = humanLines.length > 0 || (typeof aiText === "string" && aiText.length > 0);
|
|
1071
|
+
const human = humanGate.emitHuman && humanLines.length > 0 ? humanLines.join("\n") : null;
|
|
1072
|
+
const reminderToContext = readReminderToContext(cwd);
|
|
1073
|
+
const ai = reminderToContext && aiText && aiText.length > 0 ? aiText : null;
|
|
1074
|
+
|
|
1075
|
+
return { human, ai, resolvedPayload, hasRenderedContent, reminderToContext };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
768
1078
|
function main(env, stdio) {
|
|
769
1079
|
try {
|
|
770
1080
|
const cwd = (env && env.cwd) || process.cwd();
|
|
@@ -802,96 +1112,31 @@ function main(env, stdio) {
|
|
|
802
1112
|
env && env.payload !== undefined ? env.payload : invokePlanContextHint(cwd);
|
|
803
1113
|
if (payload === null || payload === undefined) return; // silent
|
|
804
1114
|
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
} catch {
|
|
831
|
-
// resolveOpaqueSummaries swallows its own errors; this catch is belt
|
|
832
|
-
// + suspenders for any unexpected exception from the lib layer.
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// rc.8 underseed self-check: decide whether to surface the one-line
|
|
836
|
-
// `/fabric-import` recommendation banner alongside the broad summary.
|
|
837
|
-
const recommendImport = shouldRecommendImport(cwd);
|
|
838
|
-
|
|
839
|
-
// rc.12: broad-summary body is unconditionally rendered on every
|
|
840
|
-
// SessionStart fire (Skill-style progressive disclosure). The prior
|
|
841
|
-
// revision_hash cooldown gate (rc.7 T8 — rc.11) was removed because
|
|
842
|
-
// compact/clear-triggered SessionStart re-fires must re-inject the menu
|
|
843
|
-
// for the agent's working memory. rc.33 W2-5 reintroduces an opt-in
|
|
844
|
-
// hours-based cooldown via fabric-config (see gate above).
|
|
845
|
-
const summaryMaxLen = readSummaryMaxLen(cwd);
|
|
846
|
-
// v2.2 HK2-degrade (W2-T2): thread the injection char-budget into the renderer.
|
|
847
|
-
const broadBudgetChars = readBroadBudgetChars(cwd);
|
|
848
|
-
const lines = renderSummary(resolvedPayload, summaryMaxLen, broadBudgetChars);
|
|
849
|
-
|
|
850
|
-
// v2.0.0-rc.37 NEW-23: resolve fabric_language ONCE per emit path —
|
|
851
|
-
// shared between the (existing) broadImportBanner branch and the new
|
|
852
|
-
// 'next step' nudge tail added below. 'match-existing' / unknown variants
|
|
853
|
-
// fold to 'en' inside renderBanner per UX i18n Policy class 1.
|
|
854
|
-
const fabricLanguageForEmit = lines.length > 0 || recommendImport ? readFabricLanguage(cwd) : null;
|
|
855
|
-
if (recommendImport && fabricLanguageForEmit !== null) {
|
|
856
|
-
lines.push(renderBanner("broadImportBanner", fabricLanguageForEmit, {}));
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (lines.length === 0) return; // nothing to say — silent exit
|
|
860
|
-
|
|
861
|
-
// v2.1.0-rc.1 P4 (F4/S63): append a per-store read-set label from the
|
|
862
|
-
// CLI-pre-generated bindings snapshot so the session opens aware of which
|
|
863
|
-
// stores it reads and where writes land. Best-effort, never blocks: a
|
|
864
|
-
// missing snapshot / single-store setup just omits the line.
|
|
865
|
-
if (bindingsSnapshotReader !== null) {
|
|
866
|
-
try {
|
|
867
|
-
const projectId = readProjectId(cwd);
|
|
868
|
-
if (projectId) {
|
|
869
|
-
const label = bindingsSnapshotReader.formatStoreLabels(
|
|
870
|
-
bindingsSnapshotReader.readBindingsSnapshot(projectId),
|
|
871
|
-
);
|
|
872
|
-
if (label) lines.push(label);
|
|
873
|
-
}
|
|
874
|
-
} catch {
|
|
875
|
-
// store labels are decorative provenance — never crash the hook
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// v2.0.0-rc.37 NEW-23: SessionStart 索引末尾"下一步"引导。Tail line that
|
|
880
|
-
// tells the AI what to do with the broad index it just received. Without
|
|
881
|
-
// this, the model often parses the index and moves on without ever calling
|
|
882
|
-
// fab_recall / fab_plan_context. One-line nudge, bilingual.
|
|
883
|
-
// v2.2 W1-REVIEW codex LOW-6: `description_index` was renamed to `candidates`
|
|
884
|
-
// in rc.38 UX-1; the nudge now uses the current field name so the guidance
|
|
885
|
-
// matches the actual MCP response shape.
|
|
886
|
-
const nextStepNudge =
|
|
887
|
-
fabricLanguageForEmit === "zh-CN"
|
|
888
|
-
? "下一步: 调 fab_recall(paths) 拿 KB 相关条目;或调 fab_plan_context 先看候选描述(candidates)。"
|
|
889
|
-
: "Next: call fab_recall(paths) to fetch related KB entries, or fab_plan_context to preview the candidate descriptions first.";
|
|
890
|
-
lines.push(nextStepNudge);
|
|
891
|
-
|
|
892
|
-
// Stderr: always emit (human-facing breadcrumb + legacy contract).
|
|
893
|
-
for (const line of lines) {
|
|
894
|
-
err.write(`${line}\n`);
|
|
1115
|
+
// W2-1 (KT-DEC-0028): broad 全显示 — the legacy hint_broad_top_k hard cap is
|
|
1116
|
+
// retired. SessionStart must SEE every broad entry; scale is bounded by the
|
|
1117
|
+
// per-line char cap + broad_index_backstop fold, not by dropping entries.
|
|
1118
|
+
|
|
1119
|
+
// Block 5 (Option X): build both sinks via the shared renderer (same code
|
|
1120
|
+
// `fabric context` uses → byte-identical injection). Side-effect-free; the
|
|
1121
|
+
// emit + telemetry below stay in main().
|
|
1122
|
+
const { human, ai, resolvedPayload, hasRenderedContent, reminderToContext } =
|
|
1123
|
+
buildSessionStartSinks(cwd, payload, env);
|
|
1124
|
+
|
|
1125
|
+
// Nothing to say at all → silent exit (preserves the empty-payload contract).
|
|
1126
|
+
if (!hasRenderedContent) return;
|
|
1127
|
+
|
|
1128
|
+
// v2.2 dual-sink (Goal A / D7): emit both channels in one render. The human
|
|
1129
|
+
// systemMessage is gated by nudge_mode (emitHuman); the AI additionalContext
|
|
1130
|
+
// is emitted regardless. emitDualSink shapes the protocol per client (CC/Codex
|
|
1131
|
+
// camelCase nested envelope; unknown → stderr).
|
|
1132
|
+
if (!(env && env.skipStdout === true)) {
|
|
1133
|
+
emitDualSink(
|
|
1134
|
+
{ human, ai },
|
|
1135
|
+
{ client: detectClient(), eventName: "SessionStart", streams: { stdout: out, stderr: err } },
|
|
1136
|
+
);
|
|
1137
|
+
} else if (human !== null) {
|
|
1138
|
+
// skipStdout test seam: still surface the human breadcrumb to stderr.
|
|
1139
|
+
err.write(`${human}\n`);
|
|
895
1140
|
}
|
|
896
1141
|
|
|
897
1142
|
// v2.2 HK3-telemetry (W3-T1): record the injection side. We just OFFERED the
|
|
@@ -914,37 +1159,10 @@ function main(env, stdio) {
|
|
|
914
1159
|
});
|
|
915
1160
|
}
|
|
916
1161
|
|
|
917
|
-
// v2.
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
// root cause: reminders never entered model context). Stderr stays the
|
|
922
|
-
// host-facing channel.
|
|
923
|
-
//
|
|
924
|
-
// Failure to write JSON envelope must NOT crash the hook — stderr already
|
|
925
|
-
// delivered, the stdout layer is best-effort.
|
|
926
|
-
// v2.0.0-rc.33 W4 review-fix (gemini High-1): the stdout JSON envelope
|
|
927
|
-
// is Claude Code-specific (hookSpecificOutput.additionalContext contract).
|
|
928
|
-
// Codex CLI / Cursor don't parse it — leaking it to their stdout risks
|
|
929
|
-
// either polluting the terminal or crashing the host's hook-parsing
|
|
930
|
-
// pipeline. CLAUDE_PROJECT_DIR is set by CC when invoking hooks (see
|
|
931
|
-
// packages/cli/templates/hooks/configs/claude-code.json sigil paths);
|
|
932
|
-
// its presence is the single-bit "this is Claude Code" signal (now via
|
|
933
|
-
// the shared client-adapter, rc.37 NEW-30).
|
|
934
|
-
const reminderToContext = readReminderToContext(cwd) && isClaudeCode();
|
|
935
|
-
if (reminderToContext && !(env && env.skipStdout === true)) {
|
|
936
|
-
try {
|
|
937
|
-
const envelope = {
|
|
938
|
-
hookSpecificOutput: {
|
|
939
|
-
hookEventName: "SessionStart",
|
|
940
|
-
additionalContext: lines.join("\n"),
|
|
941
|
-
},
|
|
942
|
-
};
|
|
943
|
-
out.write(`${JSON.stringify(envelope)}\n`);
|
|
944
|
-
} catch {
|
|
945
|
-
// Best-effort — stderr is the durable contract
|
|
946
|
-
}
|
|
947
|
-
}
|
|
1162
|
+
// v2.2 dual-sink (Goal A): the legacy rc.33 W2-6 stdout JSON envelope is
|
|
1163
|
+
// replaced by emitDualSink above (which carries BOTH the human systemMessage
|
|
1164
|
+
// and the AI additionalContext, shaped per client). hint_reminder_to_context
|
|
1165
|
+
// still gates whether the AI sink is populated (see `ai` above).
|
|
948
1166
|
|
|
949
1167
|
// v2.1 NEW-N-3 (ADJ-NEWN-3): hook_surface_emitted instrumentation. One
|
|
950
1168
|
// best-effort ledger row recording WHICH broad-scoped ids were surfaced
|
|
@@ -953,7 +1171,7 @@ function main(env, stdio) {
|
|
|
953
1171
|
// per session boot so this never bloats the ledger. Never blocks the hook
|
|
954
1172
|
// (KT-DEC-0007): any failure (no .fabric/, undetected client, write error)
|
|
955
1173
|
// degrades to silent skip. Client is omitted-by-skip when undetectable
|
|
956
|
-
// because the schema's `client` enum admits only cc/codex
|
|
1174
|
+
// because the schema's `client` enum admits only cc/codex.
|
|
957
1175
|
try {
|
|
958
1176
|
const surfaceClient = detectClient();
|
|
959
1177
|
const fabricDir = join(cwd, FABRIC_DIR_REL);
|
|
@@ -1002,6 +1220,7 @@ function main(env, stdio) {
|
|
|
1002
1220
|
|
|
1003
1221
|
module.exports = {
|
|
1004
1222
|
main,
|
|
1223
|
+
buildSessionStartSinks,
|
|
1005
1224
|
invokePlanContextHint,
|
|
1006
1225
|
groupEntries,
|
|
1007
1226
|
renderFull,
|
|
@@ -1013,8 +1232,8 @@ module.exports = {
|
|
|
1013
1232
|
readUnderseedThreshold,
|
|
1014
1233
|
isImportTouched,
|
|
1015
1234
|
shouldRecommendImport,
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1235
|
+
// W2-1 (KT-DEC-0028) + rc.33 W2-5 / W2-6 helpers.
|
|
1236
|
+
readBroadIndexBackstop,
|
|
1018
1237
|
readBroadCooldownHours,
|
|
1019
1238
|
readReminderToContext,
|
|
1020
1239
|
readBroadLastEmit,
|
|
@@ -1031,7 +1250,7 @@ module.exports = {
|
|
|
1031
1250
|
MATURITY_DRAFT,
|
|
1032
1251
|
DEFAULT_UNDERSEED_NODE_THRESHOLD,
|
|
1033
1252
|
KNOWLEDGE_CANONICAL_TYPES,
|
|
1034
|
-
|
|
1253
|
+
DEFAULT_HINT_BROAD_INDEX_BACKSTOP,
|
|
1035
1254
|
DEFAULT_HINT_BROAD_COOLDOWN_HOURS,
|
|
1036
1255
|
DEFAULT_HINT_REMINDER_TO_CONTEXT,
|
|
1037
1256
|
HINT_BROAD_LAST_EMIT_FILE_NAME,
|