@fenglimg/fabric-shared 2.2.0 → 2.3.0-rc.2

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.
@@ -18,6 +18,7 @@ import { z as z2 } from "zod";
18
18
  var PERSONAL_SCOPE = "personal";
19
19
  var KNOWN_SCOPE_PREFIXES = ["personal", "team", "project", "org"];
20
20
  var SCOPE_COORDINATE_PATTERN = /^[a-z0-9_-]+(:[a-z0-9_-]+)*$/u;
21
+ var SCOPE_COORDINATE_HINT = 'scope coordinate must look like "project:fabric-v2", "team", or "personal" \u2014 lowercase [a-z0-9_-] segments joined by ":"';
21
22
  var scopeCoordinateSchema = z2.string().min(1).regex(
22
23
  SCOPE_COORDINATE_PATTERN,
23
24
  "scope coordinate must be ':'-joined lowercase [a-z0-9_-] segments"
@@ -129,6 +130,7 @@ var _preflightDiagnosticSchema = z3.object({
129
130
  stable_ids: z3.array(z3.string()).optional(),
130
131
  path: z3.string().optional()
131
132
  });
133
+ var _recallDropReasonSchema = z3.enum(["retrieval_budget", "payload_budget"]);
132
134
  var planContextOutputSchema = z3.object({
133
135
  revision_hash: z3.string(),
134
136
  stale: z3.boolean(),
@@ -143,12 +145,16 @@ var planContextOutputSchema = z3.object({
143
145
  // duplicated into every entry's requirement_profile). Omitted when no intent.
144
146
  intent: z3.string().optional(),
145
147
  candidates: z3.array(_descriptionIndexItemSchema),
146
- // v2.2 A-INFRA-3 (W1-T3-TOPK) / MC4-payload-budget (W1-T4): number of
147
- // lower-ranked candidates dropped by the unified truncation chain (top_k cap
148
- // + payload-budget trim). Present and > 0 ONLY when truncation fired, so the
149
- // steady-state wire shape is unchanged. Lets the LLM know the returned set is
150
- // not exhaustive ("N more exist; narrow your intent").
151
- omitted_candidate_count: z3.number().int().nonnegative().optional(),
148
+ // v2.2 A-INFRA-3 (W1-T3-TOPK) / MC4-payload-budget (W1-T4) / K6 (W3-K):
149
+ // structured list of lower-ranked candidates dropped by the unified truncation
150
+ // chain, each tagged with WHY it was dropped (`retrieval_budget` = top_k cap +
151
+ // ratio-to-top floor; `payload_budget` = MCP payload-byte trim). Present and
152
+ // non-empty ONLY when truncation fired, so the steady-state wire shape is
153
+ // unchanged. Replaces the bare numeric omission count so the LLM sees WHICH
154
+ // candidates were dropped and can act ("these N exist; narrow your intent").
155
+ // Reuses the archive-scan {key,reason} omission convention
156
+ // (_recallDropReasonSchema, keyed on id here).
157
+ dropped: z3.array(z3.object({ id: z3.string(), reason: _recallDropReasonSchema })).optional(),
152
158
  preflight_diagnostics: z3.array(_preflightDiagnosticSchema),
153
159
  warnings: z3.array(structuredWarningSchema).optional(),
154
160
  // v2.0.0-rc.22 Scope D T-D2: optional auto-heal banner fields. Surfaced
@@ -293,39 +299,59 @@ var recallInputSchema = z3.object({
293
299
  "When true, also surface the one-hop `related` graph neighbours (of the surfaced entries) that are present in the candidate set \u2014 their descriptions and read paths, NOT their bodies."
294
300
  )
295
301
  });
302
+ var _recallEntrySchema = z3.object({
303
+ stable_id: z3.string(),
304
+ // 1-based relevance rank (entries are returned best-first). The surfaced
305
+ // ranking signal — entries are already sorted, rank makes the order explicit.
306
+ rank: z3.number().int().positive(),
307
+ // The DESCRIPTION (summary / intent_clues / must_read_if / related ...). No body.
308
+ description: _ruleDescriptionSchema,
309
+ // on-disk knowledge file to Read for the full body. Omitted when the entry has
310
+ // no resolvable file (description-only discovery) or was scoped out by `ids`.
311
+ read_path: z3.string().optional(),
312
+ // originating store alias (omitted for unqualified / single-store entries).
313
+ store: z3.object({ alias: z3.string() }).optional(),
314
+ // true when this entry's body is ALSO injected at SessionStart (broad
315
+ // model/guideline "ALWAYS-ACTIVE") — skip the Read, it is already in context.
316
+ body_in_context: z3.boolean().optional(),
317
+ // P1 recall-observability: the fused relevance score this entry scored during
318
+ // the plan-context sort (was computed internally but dropped before this wave).
319
+ // Optional + additive — backward-compatible. MUST be declared here or zod
320
+ // .strip() silently drops it at the MCP boundary (KT-PIT-0005).
321
+ score: z3.number().optional(),
322
+ // P1 recall-observability: numbers-only decomposition of `score` into its
323
+ // weighted signal contributions. NEVER carries body/description text — preserves
324
+ // the lean read_path contract (KT-DEC-0019 / KT-GLD-0005). bm25_rank/vector_rank
325
+ // are reserved for a later RRF wave (declared so the wire never strips them).
326
+ score_breakdown: z3.object({
327
+ final: z3.number(),
328
+ bm25: z3.number().optional(),
329
+ bm25_rank: z3.number().optional(),
330
+ vector: z3.number().optional(),
331
+ vector_rank: z3.number().optional(),
332
+ salience: z3.number(),
333
+ recency: z3.number(),
334
+ locality: z3.number()
335
+ }).optional()
336
+ });
296
337
  var recallOutputSchema = z3.object({
297
338
  revision_hash: z3.string(),
298
339
  stale: z3.boolean(),
299
- // v2.0.0-rc.38 UX-1/UX-4: mirrors planContextOutputSchema fold per-path
300
- // description_index collapsed into a single top-level `candidates`, and
301
- // `preflight_diagnostics` lifted out of the removed `shared` wrapper.
302
- entries: z3.array(
303
- z3.object({
304
- path: z3.string(),
305
- requirement_profile: _requirementProfileSchema
306
- })
307
- ),
308
- // v2.2 payload de-dup: single top-level echo of the caller's `intent` (was
309
- // duplicated into every entry's requirement_profile). Omitted when no intent.
340
+ // ux-w2-4: single unified entry list (was candidates[] + paths[] + per-path
341
+ // requirement-profile entries[]). Each item carries description + read_path +
342
+ // rank + body_in_context, so the agent never joins two arrays on stable_id.
343
+ entries: z3.array(_recallEntrySchema),
344
+ // v2.2 payload de-dup: single top-level echo of the caller's `intent`.
345
+ // Omitted when no intent.
310
346
  intent: z3.string().optional(),
311
- // W1 (KT-DEC-0026): the discovery index every surfaced candidate's
312
- // DESCRIPTION (summary / intent_clues / must_read_if / related ...). No body.
313
- candidates: z3.array(_descriptionIndexItemSchema),
314
- // W1 (KT-DEC-0026): the read-path indexone entry per surfaced candidate
315
- // (scoped by `ids` when provided), in ranked order. `path` is the on-disk
316
- // knowledge file the agent Reads to load the body on demand; `store` is the
317
- // originating store alias (omitted for unqualified entries).
318
- paths: z3.array(
319
- z3.object({
320
- stable_id: z3.string(),
321
- path: z3.string(),
322
- store: z3.object({ alias: z3.string() }).optional()
323
- })
324
- ),
325
- // Number of lower-ranked candidates dropped by the retrieval budget. Present
326
- // (and > 0) ONLY when truncation fired — keeps the steady-state wire shape
327
- // unchanged while signalling "more exist; narrow your intent".
328
- omitted_candidate_count: z3.number().int().nonnegative().optional(),
347
+ // K6 (W3-K): structured list of lower-ranked candidates dropped by the
348
+ // retrieval pipeline, each tagged with WHY (`retrieval_budget` = top_k cap +
349
+ // ratio-to-top floor; `payload_budget` = MCP payload-byte trim). Present (and
350
+ // non-empty) ONLY when truncation fired keeps the steady-state wire shape
351
+ // unchanged while signalling which entries exist ("narrow your intent"), now
352
+ // with a controlled reason instead of a bare count. Reuses the archive-scan
353
+ // {key,reason} omission convention (_recallDropReasonSchema, keyed on id).
354
+ dropped: z3.array(z3.object({ id: z3.string(), reason: _recallDropReasonSchema })).optional(),
329
355
  preflight_diagnostics: z3.array(_preflightDiagnosticSchema),
330
356
  warnings: z3.array(structuredWarningSchema).optional(),
331
357
  auto_healed: z3.boolean().optional(),
@@ -366,6 +392,8 @@ var archiveScanOutputSchema = z3.object({
366
392
  // in first-seen order — ready for the Skill to load digests + stitch.
367
393
  session_ids: z3.array(z3.string()),
368
394
  // Sessions dropped by the filter, with the rule that fired (audit/debug).
395
+ // Shares the {key,reason} omission convention with recall's dropped[] above
396
+ // (keys on session_id here, on id in recall).
369
397
  dropped: z3.array(
370
398
  z3.object({
371
399
  session_id: z3.string(),
@@ -428,15 +456,19 @@ var _FabExtractKnowledgeInputBaseSchema = z3.object({
428
456
  user_messages_summary: z3.string().describe("Skill-side summary of the user's intent/messages, kept compact"),
429
457
  type: z3.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).describe("Knowledge type bucket (plural form, mirrors directory layout)"),
430
458
  slug: z3.string().describe("URL-safe short identifier proposed by the Skill; server may sanitize"),
431
- // Store-only cutover: layer is a compatibility audience hint. The server
432
- // resolves the actual write target through semantic_scope/write_routes and
433
- // writes to the selected mounted store's knowledge/pending/<type>/ tree.
434
- // Defaults to 'team' to preserve existing call sites (Skill bumps as needed).
435
- layer: z3.enum(["team", "personal"]).optional().describe(
436
- "Compatibility storage audience. 'personal' writes to the personal store; non-personal writes resolve by semantic_scope/write_routes. Defaults to 'team'."
437
- ),
438
- semantic_scope: z3.string().regex(SCOPE_COORDINATE_PATTERN).optional().describe(
439
- "Logical audience/write route coordinate for this pending entry, e.g. personal, team, project:fabric-v2, org:acme:team:platform. Server validates and resolves it through write_routes."
459
+ // v2.2 C1 (W1) — author-facing scope is now TWO fields only: `audience` +
460
+ // `paths`. Everything else (layer / visibility_store / relevance_scope /
461
+ // store) is engine-derived, never author input (C1 §一.1). The physical write
462
+ // store is the hard privacy boundary (cross-store-write R5#3); `audience` only
463
+ // subdivides WHO within that boundary.
464
+ //
465
+ // audience — the open scope coordinate describing WHO the entry is for
466
+ // (personal | team | project:x | org:y...). Replaces the old
467
+ // `layer` + `semantic_scope` pair: a `personal` coordinate routes
468
+ // to the personal store; everything else resolves via write_routes.
469
+ // Omit → engine defaults to project:<active> (bound repo) or team.
470
+ audience: z3.string().regex(SCOPE_COORDINATE_PATTERN, { message: SCOPE_COORDINATE_HINT }).optional().describe(
471
+ "WHO this entry is for \u2014 an open scope coordinate (personal | team | project:x | org:y...). The sole author-facing audience field; the engine derives layer/visibility_store/store from it + the physical write store. Omit to default to project:<active> (bound repo) or team."
440
472
  ),
441
473
  // v2.0.0-rc.7 T6: proposed_reason — required enum that drives `## Why
442
474
  // proposed` rendering. Skills (archive / import / review) infer the
@@ -462,11 +494,16 @@ var _FabExtractKnowledgeInputBaseSchema = z3.object({
462
494
  // a `knowledge_scope_degraded` event keyed by `pending:<idempotency_key>`.
463
495
  // NOTE: these fields MUST NOT be part of the idempotency hash inputs at
464
496
  // extract-knowledge.ts:78 — preserves rc.5→rc.7 collision detection.
465
- relevance_scope: z3.enum(["narrow", "broad"]).optional().describe(
466
- "Optional relevance scope. 'narrow' restricts plan-context-hint surfacing to relevance_paths; 'broad' always surfaces. Omit to let the meta-builder default to 'broad'. Personal + narrow is silently degraded to broad + []."
467
- ),
468
- relevance_paths: z3.array(z3.string()).optional().describe(
469
- "Optional path anchors for narrow scope. Workspace-relative globs or paths. Omit to let the meta-builder default to []. Ignored when scope is broad (server preserves the array for audit)."
497
+ // paths — relevance anchors (workspace-relative globs/paths). The engine
498
+ // DERIVES relevance_scope from this field's presence: non-empty
499
+ // narrow (surface only when an edit matches an anchor); empty/omitted
500
+ // → broad (always surface). This eliminates the old separate
501
+ // relevance_scope flag and its narrow+empty illegal state by
502
+ // construction (KT-MOD-0001). Glob syntax follows Copilot `applyTo`
503
+ // / Cursor `globs` (cross-client moat). Personal audience forces
504
+ // broad+[] (workspace-relative paths cross-project lose meaning).
505
+ paths: z3.array(z3.string()).optional().describe(
506
+ "Relevance anchors (workspace-relative globs/paths). Non-empty \u2192 narrow (surface only on matching edits); empty/omitted \u2192 broad (always surface). The engine derives relevance_scope from this \u2014 there is no separate scope flag."
470
507
  ),
471
508
  // v2.0.0-rc.23 TASK-006 (a-C1): four optional structured fields that the
472
509
  // skill-side LLM populates from raw observations. The same information
@@ -512,7 +549,7 @@ var _FabExtractKnowledgeInputBaseSchema = z3.object({
512
549
  // discover unclaimed slots, then propagates the chosen slot label here
513
550
  // so the resulting pending entry counts toward coverage.
514
551
  //
515
- // STRICT optionality: every non-onboard fab_extract_knowledge call MUST
552
+ // STRICT optionality: every non-onboard fab_propose call MUST
516
553
  // omit this field. The skill is the only producer; downstream consumers
517
554
  // (plan_context retrieval, doctor lints) treat missing as a steady-state
518
555
  // signal that the entry was NOT part of an onboard pass.
@@ -627,10 +664,6 @@ var _fabReviewModifyChangesSchema = z3.object({
627
664
  related: z3.array(z3.string()).optional()
628
665
  });
629
666
  var FabReviewInputSchema = z3.discriminatedUnion("action", [
630
- z3.object({
631
- action: z3.literal("list"),
632
- filters: _fabReviewFiltersSchema
633
- }),
634
667
  z3.object({
635
668
  action: z3.literal("approve"),
636
669
  pending_paths: z3.array(z3.string()).min(1)
@@ -663,11 +696,6 @@ var FabReviewInputSchema = z3.discriminatedUnion("action", [
663
696
  layer: z3.enum(["team", "personal"])
664
697
  })
665
698
  }),
666
- z3.object({
667
- action: z3.literal("search"),
668
- query: z3.string().min(1),
669
- filters: _fabReviewFiltersSchema
670
- }),
671
699
  z3.object({
672
700
  action: z3.literal("defer"),
673
701
  pending_paths: z3.array(z3.string()).min(1),
@@ -675,13 +703,32 @@ var FabReviewInputSchema = z3.discriminatedUnion("action", [
675
703
  reason: z3.string().optional()
676
704
  })
677
705
  ]);
678
- var FabReviewInputShape = {
679
- action: z3.enum(["list", "approve", "reject", "modify", "modify-content", "modify-layer", "search", "defer"]).describe(
680
- "Action selector. Discriminates the per-action fields below; required. modify-content edits scalars (no layer); modify-layer is the layer-flip path (changes.layer required); modify is the legacy combined alias."
706
+ var FabPendingInputSchema = z3.discriminatedUnion("action", [
707
+ z3.object({
708
+ action: z3.literal("list"),
709
+ filters: _fabReviewFiltersSchema
710
+ }),
711
+ z3.object({
712
+ action: z3.literal("search"),
713
+ query: z3.string().min(1),
714
+ filters: _fabReviewFiltersSchema
715
+ })
716
+ ]);
717
+ var FabPendingInputShape = {
718
+ action: z3.enum(["list", "search"]).describe(
719
+ "Action selector. Discriminates the per-action fields below; required. list browses pending entries; search ranges over pending + canonical knowledge."
681
720
  ),
682
721
  filters: _fabReviewFiltersSchema.describe(
683
722
  "Optional filters (type/layer/maturity/tags/created_after). Used by action=list and action=search."
684
723
  ),
724
+ query: z3.string().min(1).optional().describe(
725
+ "Substring query against title/summary/tags/path. Required (non-empty) when action=search."
726
+ )
727
+ };
728
+ var FabReviewInputShape = {
729
+ action: z3.enum(["approve", "reject", "modify", "modify-content", "modify-layer", "defer"]).describe(
730
+ "Action selector. Discriminates the per-action fields below; required. modify-content edits scalars (no layer); modify-layer is the layer-flip path (changes.layer required); modify is the legacy combined alias. (list/search moved to the read-only fab_pending tool.)"
731
+ ),
685
732
  pending_paths: z3.array(z3.string()).min(1).optional().describe(
686
733
  "Workspace-relative pending entry paths. Required when action=approve|reject|defer (non-empty array)."
687
734
  ),
@@ -694,9 +741,6 @@ var FabReviewInputShape = {
694
741
  changes: _fabReviewModifyChangesSchema.optional().describe(
695
742
  "Frontmatter scalar patches (title/summary/layer/maturity/tags/relevance_*/semantic_scope/related). Required when action=modify. semantic_scope re-scopes the entry's resolution coordinate in place (e.g. team \u2192 project:<id>) without moving stores; personal-root coordinates are rejected (use modify-layer)."
696
743
  ),
697
- query: z3.string().min(1).optional().describe(
698
- "Substring query against title/summary/tags/path. Required (non-empty) when action=search."
699
- ),
700
744
  until: z3.string().datetime().optional().describe(
701
745
  "ISO-8601 datetime upper bound for the deferral. Optional; used only when action=defer."
702
746
  )
@@ -757,11 +801,6 @@ var _fabReviewSearchItemSchema = z3.object({
757
801
  stable_id: z3.string().optional()
758
802
  });
759
803
  var FabReviewOutputSchema = z3.discriminatedUnion("action", [
760
- z3.object({
761
- action: z3.literal("list"),
762
- items: z3.array(_fabReviewListItemSchema),
763
- warnings: z3.array(structuredWarningSchema).optional()
764
- }),
765
804
  z3.object({
766
805
  action: z3.literal("approve"),
767
806
  approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })),
@@ -781,25 +820,36 @@ var FabReviewOutputSchema = z3.discriminatedUnion("action", [
781
820
  warnings: z3.array(structuredWarningSchema).optional()
782
821
  }),
783
822
  z3.object({
784
- action: z3.literal("search"),
785
- // v2.0.0-rc.29 TASK-007 (BUG-M4): search returns the new search-item
786
- // shape with `area` discriminator + neutrally-named `path` field.
787
- items: z3.array(_fabReviewSearchItemSchema),
823
+ action: z3.literal("defer"),
824
+ deferred: z3.array(z3.string()),
825
+ warnings: z3.array(structuredWarningSchema).optional()
826
+ })
827
+ ]);
828
+ var FabPendingOutputSchema = z3.discriminatedUnion("action", [
829
+ z3.object({
830
+ action: z3.literal("list"),
831
+ items: z3.array(_fabReviewListItemSchema),
788
832
  warnings: z3.array(structuredWarningSchema).optional()
789
833
  }),
790
834
  z3.object({
791
- action: z3.literal("defer"),
792
- deferred: z3.array(z3.string()),
835
+ action: z3.literal("search"),
836
+ items: z3.array(_fabReviewSearchItemSchema),
793
837
  warnings: z3.array(structuredWarningSchema).optional()
794
838
  })
795
839
  ]);
796
- var FabReviewOutputShape = {
797
- action: z3.enum(["list", "approve", "reject", "modify", "search", "defer"]).describe(
840
+ var FabPendingOutputShape = {
841
+ action: z3.enum(["list", "search"]).describe(
798
842
  "Echoes the input action; clients can switch on it for per-variant fields below."
799
843
  ),
800
844
  items: z3.array(z3.union([_fabReviewListItemSchema, _fabReviewSearchItemSchema])).optional().describe(
801
845
  "Pending entries (action=list, `pending_path` shape) or pending+canonical entries (action=search, `area`+`path` shape)."
802
846
  ),
847
+ warnings: z3.array(structuredWarningSchema).optional()
848
+ };
849
+ var FabReviewOutputShape = {
850
+ action: z3.enum(["approve", "reject", "modify", "defer"]).describe(
851
+ "Echoes the input action; clients can switch on it for per-variant fields below. (list/search results moved to the read-only fab_pending tool.)"
852
+ ),
803
853
  approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })).optional().describe(
804
854
  "Allocated stable ids paired with their original pending paths. Present when action=approve."
805
855
  ),
@@ -829,6 +879,13 @@ var fabReviewAnnotations = {
829
879
  openWorldHint: false,
830
880
  title: "Review pending knowledge entries"
831
881
  };
882
+ var fabPendingAnnotations = {
883
+ readOnlyHint: true,
884
+ idempotentHint: true,
885
+ destructiveHint: false,
886
+ openWorldHint: false,
887
+ title: "Browse and search pending knowledge entries"
888
+ };
832
889
  var citeContractMetricsSchema = z3.object({
833
890
  decisions_cited: z3.number().int().nonnegative(),
834
891
  pitfalls_cited: z3.number().int().nonnegative(),
@@ -1071,6 +1128,7 @@ export {
1071
1128
  PERSONAL_SCOPE,
1072
1129
  KNOWN_SCOPE_PREFIXES,
1073
1130
  SCOPE_COORDINATE_PATTERN,
1131
+ SCOPE_COORDINATE_HINT,
1074
1132
  scopeCoordinateSchema,
1075
1133
  scopeRoot,
1076
1134
  isPersonalScope,
@@ -1097,10 +1155,15 @@ export {
1097
1155
  FabExtractKnowledgeOutputSchema,
1098
1156
  fabExtractKnowledgeAnnotations,
1099
1157
  FabReviewInputSchema,
1158
+ FabPendingInputSchema,
1159
+ FabPendingInputShape,
1100
1160
  FabReviewInputShape,
1101
1161
  FabReviewOutputSchema,
1162
+ FabPendingOutputSchema,
1163
+ FabPendingOutputShape,
1102
1164
  FabReviewOutputShape,
1103
1165
  fabReviewAnnotations,
1166
+ fabPendingAnnotations,
1104
1167
  citeContractMetricsSchema,
1105
1168
  citeLayerTypeBreakdownSchema,
1106
1169
  citeCoverageReportSchema,
@@ -151,11 +151,15 @@ var mountedStoreSchema = z.object({
151
151
  // Git remote locator for this clone, if any. Absent = local-only store
152
152
  // (valid; doctor nudges to add a remote for backup — R5#5, P6).
153
153
  remote: z.string().min(1).optional(),
154
- // v2.1.0-rc.1 P3: marks the implicit personal store (the one minted by
155
- // `install --global`). Exactly one mounted store carries personal=true; it
156
- // is the write target for personal-scope entries (R5#3) and always in the
157
- // read-set (S11). Optional (no default) so the output type stays a plain
158
- // optional consumers coalesce `?? false` when building resolver input.
154
+ // v2.1.0-rc.1 P3: marks a personal store (the kind minted by
155
+ // `install --global`). 语义 A (multi-personal): MULTIPLE mounted stores may
156
+ // carry personal=true a machine can mount several personal stores and
157
+ // switch which is ACTIVE via globalConfig.active_personal_store. The ACTIVE
158
+ // personal is the write target for personal-scope entries (R5#3) and the one
159
+ // in the read-set (S11); non-active personal stores stay mounted but out of
160
+ // the read-set. Absent active pointer ⇒ resolver falls back to the first
161
+ // mounted personal (back-compat). Optional (no default) so the output type
162
+ // stays a plain optional — consumers coalesce `?? false`.
159
163
  personal: z.boolean().optional(),
160
164
  // Whether writes are accepted into this store from this machine. Optional;
161
165
  // consumers coalesce `?? true`. Shared stores cloned read-only set false.
@@ -174,7 +178,14 @@ var globalConfigSchema = z.object({
174
178
  // All stores mounted on this machine. The implicit personal store is
175
179
  // included here once initialized. Default empty so a fresh global config
176
180
  // (before `install --global`) parses cleanly.
177
- stores: z.array(mountedStoreSchema).optional().default([])
181
+ stores: z.array(mountedStoreSchema).optional().default([]),
182
+ // 语义 A (multi-personal): alias/UUID of the ACTIVE personal store among the
183
+ // possibly-many `personal:true` stores in `stores[]`. Machine-wide (personal
184
+ // is uid-scoped identity, KT-DEC-0020) — switching it in any repo takes
185
+ // effect everywhere. Set by `fabric store switch-personal <alias>` and the
186
+ // install personal slot. Absent ⇒ the resolver falls back to the first
187
+ // mounted personal, so legacy single-personal configs are unchanged.
188
+ active_personal_store: z.string().min(1).optional()
178
189
  }).passthrough();
179
190
 
180
191
  // src/store/global-config-io.ts
@@ -0,0 +1,152 @@
1
+ import {
2
+ resolveGlobalLocale
3
+ } from "./chunk-ANUDBQBK.js";
4
+
5
+ // src/templates/bootstrap-canonical.ts
6
+ var BOOTSTRAP_MARKER_BEGIN = "<!-- fabric:bootstrap:begin -->";
7
+ var BOOTSTRAP_MARKER_END = "<!-- fabric:bootstrap:end -->";
8
+ var BOOTSTRAP_REGEX = /(?:\r?\n){0,2}<!-- fabric:bootstrap:begin -->[\s\S]*?<!-- fabric:bootstrap:end -->/;
9
+ var BOOTSTRAP_CANONICAL_ZH = `# Fabric Bootstrap
10
+
11
+ \u672C\u9879\u76EE\u4F7F\u7528 Fabric \u7BA1\u7406\u8DE8\u5BA2\u6237\u7AEF AI \u77E5\u8BC6\u4E0E\u884C\u4E3A\u89C4\u5219\u3002\u672C\u6587\u4EF6\u7531 \`fabric install\` \u540C\u6B65\u5230\u4E24\u7AEF managed block,**\u4E0D\u8981\u624B\u52A8\u7F16\u8F91\u4E24\u7AEF\u7684 block**,\u53EA\u6539\u8FD9\u91CC + \u91CD\u8DD1 \`fabric install\`\u3002
12
+
13
+ ## For Developers
14
+
15
+ \u8FD9\u4E2A\u6587\u4EF6\u662F **AI \u5BA2\u6237\u7AEF\u7684\u7B56\u7565\u4E0E\u89C4\u7EA6\u914D\u7F6E**,\u4E0D\u662F dev onboarding\u3002\u4F60\u4E0D\u9700\u8981\u8BFB Self-archive / Cite / Phase 0.4 \u7B49\u7EC6\u8282\u3002
16
+ \u4F5C\u4E3A dev \u4F60\u53EA\u9700\u8981:\u5728\u6BCF\u4E2A repo \u8DD1\u4E00\u6B21 \`fabric install\`,\u7528 \`fabric store bind <alias>\` / \`fabric store switch-write <alias>\` \u63A5\u5165\u5199\u5165 store,\u51FA\u95EE\u9898\u8DD1 \`fabric doctor\`\u3002
17
+ **\u4E25\u7981\u624B\u52A8\u7F16\u8F91 \`.fabric/agents.meta.json\`** \u2014 \u6D3E\u751F\u72B6\u6001\u7531 engine \u91CD\u5EFA\u3002
18
+
19
+ ## 5 \u5206\u949F\u4E0A\u624B (Dev Quickstart)
20
+
21
+ **Fabric \u662F\u4EC0\u4E48**:\u8DE8\u5BA2\u6237\u7AEF(Claude Code / Codex CLI)\u7684 AI \u77E5\u8BC6\u5C42\u3002\u628A\u56E2\u961F/\u9879\u76EE\u7684 **decisions / pitfalls / guidelines / models / processes** \u5B58\u4E3A markdown,hook \u81EA\u52A8 surface \u7ED9 AI,\u8BA9 AI \u4E0D\u7528\u6BCF\u6B21\u91CD\u5B66\u3002
22
+
23
+ **\u4F60\u8981\u505A\u7684 (DO)** vs **engine \u81EA\u52A8\u7684 (DON'T \u624B\u52A8)**:
24
+
25
+ | \u4F60 DO | \u4F60 DON'T |
26
+ | --- | --- |
27
+ | \u6BCF\u4E2A repo \u8DD1\u4E00\u6B21 \`fabric install\` | \u624B\u7F16 \`.fabric/agents.meta.json\` |
28
+ | \u5F02\u5E38\u65F6\u8DD1 \`fabric doctor\` (--fix \u81EA\u6108) | \u624B\u7F16 \`.claude/hooks/\` \u4E0B \`.cjs\` |
29
+ | \u7528 \`fabric-archive\` / \`fabric-review\` / \`fabric store ...\` \u7BA1\u7406 store-backed knowledge | \u624B\u5199\u4EFB\u4F55\u975E store knowledge \u6839 |
30
+ | \`npm install -g @fenglimg/fabric-cli@latest\` \u5347\u7EA7 | \u80CC 35 \u6761 doctor lint \u4EE3\u7801 |
31
+
32
+ **4 \u6B65\u5FAA\u73AF**: \`fabric install\` (\u4E00\u6B21) \u2192 \u7ED1\u5B9A\u5E76\u9009\u62E9\u5199\u5165 store \u2192 AI \u6B63\u5E38\u5DE5\u4F5C (hook on session start + edit) \u2192 AI \u901A\u8FC7 MCP \u5199\u5165\u5F53\u524D write store \u7684 pending \u6761\u76EE\u5E76\u8FD4\u56DE \`pending_path\` \u2192 \u7528 \`fabric-review\` skill \u5BA1\u6838\u3002
33
+
34
+ **\u771F\u4F8B**:\u67D0 sprite \u9ED1\u8FB9 root cause \u662F \`atlas.premultiplyAlpha\` flag \u53CD\u5411 \u2014 \u5F52\u6863\u8FDB store \u7684 \`knowledge/pitfalls/\` \u540E,\u4E0B\u6B21\u540C\u7C7B\u95EE\u9898 AI \u81EA\u52A8 reference\u3002
35
+
36
+ \u5B8C\u6574 maintainer \u7248\u89C1 \`docs/USER-QUICKSTART.md\`\u3002
37
+
38
+ ## \u884C\u4E3A\u89C4\u5219
39
+ - **\u4FEE\u6539\u4EFB\u4F55\u6587\u4EF6\u524D**:\u5148 \`fab_recall(paths=[<\u88AB\u6539\u6587\u4EF6>])\` \u2014\u2014 \u4E00\u6B21\u8C03\u7528\u62FF\u56DE\u76F8\u5173 KB \u7684\u63CF\u8FF0 + \u539F\u751F\u8BFB\u53D6\u8DEF\u5F84(\`entries[].read_path\`)\u3002\`fab_recall\` \u4E0D\u518D\u6295\u9012\u6B63\u6587;\u9700\u8981\u67D0\u6761\u6B63\u6587\u65F6\u76F4\u63A5\u5BF9\u5176 \`entries[].read_path\` \u505A\u539F\u751F Read(\`Read <store>/knowledge/<type>/<id>--*.md\`),\u8FD9\u4F1A\u88AB PostToolUse hook \u8BB0\u4E3A \`knowledge_body_read\`\u3002lean \u9ED8\u8BA4:\u63CF\u8FF0+\u7D22\u5F15\u5DF2\u591F\u53D1\u73B0\u6761\u76EE,\u6B63\u6587\u6309\u9700\u8BFB\u4E00\u6B21,\u4E0D\u6BCF\u8F6E\u91CD\u704C(KT-GLD-0005)\u3002
40
+ - **\`.fabric/agents.meta.json\` \u4E25\u7981\u624B\u52A8\u7F16\u8F91**;engine \u4F1A\u81EA\u52A8\u540C\u6B65\u6D3E\u751F\u72B6\u6001,\u663E\u5F0F reconcile \u8DD1 \`fabric doctor --fix\`\u3002
41
+
42
+ ## \u77E5\u8BC6\u5E93(KB)
43
+ - **Discovery**:SessionStart hook \u5217 broad-scoped \u6761\u76EE(\u6761\u76EE\u6309 \`semantic_scope\` \u5206\u4E09\u5C42:\`team\` \u56E2\u961F\u901A\u7528 / \`project:<id>\` \u672C\u9879\u76EE\u4E13\u5C5E(\u4EC5\u5728\u7ED1\u5B9A\u8BE5\u9879\u76EE\u7684\u4ED3\u5E93\u6D6E\u73B0)/ \`personal\` \u4E2A\u4EBA \`KP-*\`,\u4E09\u8005\u5F15\u7528\u65B9\u5F0F\u76F8\u540C);edit \u6587\u4EF6\u65F6 PreToolUse hook \u53EF\u80FD\u89E6\u53D1 narrow hint\u3002
44
+ - **Scope \u4E09\u8F74(\u4E3A\u4EC0\u4E48\u6CA1\u6D6E\u73B0)** (KT-MOD-0001):\u4E00\u6761\u77E5\u8BC6\u662F\u5426\u6D6E\u73B0\u7531\u4E09\u4E2A**\u6B63\u4EA4**\u8F74\u51B3\u5B9A \u2014\u2014 \u2460 \`semantic_scope\` \u53D7\u4F17(\`team\` / \`project:<id>\` / \`personal\`;\u7ED1\u9519\u9879\u76EE\u5219\u4E0D\u663E)\u2461 \`relevance_scope\` \u65F6\u673A(\`broad\` \u5E38\u9A7B / \`narrow\` \u4EC5\u7F16\u8F91\u5339\u914D\u6587\u4EF6\u65F6\u6D6E\u73B0)\u2462 \`store\` \u7269\u7406\u5E93(\u6CA1 \`fabric store bind\` \u5C31\u4E0D\u8BFB)\u3002\u4E09\u8F74\u540D\u5B57\u4F1A\u649E("team" \u65E2\u662F\u53D7\u4F17\u503C\u4E5F\u53EF\u662F store \u522B\u540D),\u6240\u4EE5\u56F0\u60D1"\u4E3A\u4EC0\u4E48\u8FD9\u6761\u6CA1\u6D6E\u73B0"\u65F6\u8DD1 \`fabric audit why-not-surfaced <id>\` \u9010\u56E0\u8BCA\u65AD(store \u6CA1\u7ED1 / scope \u4E0D\u5339\u914D / narrow \u65F6\u673A)\u3002
45
+ - **Usage**:\u8D70\u5355\u6B65 \`fab_recall(paths=[...])\` \u4E00\u6B21\u62FF\u56DE\u76F8\u5173 KB \u7684\u63CF\u8FF0 + \u8BFB\u53D6\u8DEF\u5F84;\u9700\u8981\u67D0\u6761\u6B63\u6587\u65F6\u5BF9\u5176 \`entries[].read_path\` \u505A\u539F\u751F Read \u53D6\u56DE(\u4E0D\u518D\u8D70 MCP \u4E8C\u6B21\u53D6\u6B63\u6587)\u3002
46
+ - **session_id**: \u8C03\u7528 \`fab_recall\` \u65F6, \u52A1\u5FC5\u628A\u5F53\u524D client session id \u4F5C\u4E3A \`session_id\` \u53C2\u6570\u4F20\u5165(Claude Code \u7684 session id \u5728 stdin payload \u4E2D, Codex \u7684\u5BF9\u5E94 identifier \u540C\u7406)\u3002\u8FD9\u80FD\u8BA9 \`fabric doctor --archive-history\` \u4E0E \`fabric-hint.cjs\` Stop hook \u51C6\u786E\u8BC6\u522B\u8DE8\u4F1A\u8BDD debt \u72B6\u6001\u3002
47
+ - **Skills (4)**:\u5199\u6D41\u7A0B \`fabric-archive\`(\u542B source mode \u51B7\u542F\u52A8\u4ECE git/docs \u56DE\u704C)/ \`fabric-review\`(\u542B retire \u8BED\u4E49\u6DD8\u6C70 + relate \u5173\u8054\u5EFA\u8FB9 \u5B50\u6D41\u7A0B);store \u8FD0\u7EF4 \`fabric-store\` / \`fabric-sync\`\u3002
48
+ - **Language**:\u6E32\u67D3\u6309 \`~/.fabric/fabric-global.json\` \u7684 \`language\` \u5B57\u6BB5(machine-wide tone)\u3002
49
+ - **Archive cadence nudge** (rc.36): \u6BCF\u5B8C\u6210\u4E00\u6279 Edit(\u9ED8\u8BA4 ~20 \u6B21, \u4E0E Stop hook \u9608\u503C config \`archive_edit_threshold\` \u4E00\u81F4)/ \u663E\u8457 decision \u540E,\u5728\u5408\u9002\u56DE\u5408\u4E3B\u52A8 propose \u8C03 \`fabric-archive\` skill \u2014 archive \u6CA1\u5EFA\u7ACB\u9891\u7387\u4F1A\u8BA9 KB \u6162\u901F\u6B7B\u6389\u3002
50
+ - **Review backlog nudge** (rc.36): \u9700\u8981\u5224\u65AD pending backlog \u65F6\u8D70 \`fab_pending action="list"\` \u6216 \`fabric-review\` \u8FD4\u56DE\u7684 \`pending_path\`;\u4E0D\u8981 glob \u9879\u76EE\u672C\u5730 \`.fabric/knowledge/pending\`\u3002\u5F53\u53EF\u89C1 pending \u7D2F\u79EF >10 \u6761\u65F6,\u5728\u5408\u9002\u56DE\u5408\u4E3B\u52A8 propose \u8C03 \`fabric-review\` skill \u6279\u91CF\u5BA1,\u907F\u514D draft \u5361\u6B7B\u3002
51
+
52
+ ## Self-archive policy (v2.2 C1: \u7CBE\u7B80\u8BF4\u660E\u4E66)
53
+
54
+ \u51FA\u73B0\u660E\u786E\u5F52\u6863\u4FE1\u53F7\u65F6, \u4E8E\u8BE5 turn \u672B\u5C3E\u81EA\u4E3B\u8C03\u7528 \`fabric-archive\` skill(AI \u81EA\u89E6\u53D1\u5165\u53E3 E3):
55
+
56
+ - **\u89E6\u53D1** (\u4E8C\u9009\u4E00): \u2460 **User-driven normative** \u2014 \u7528\u6237\u8BF4 \`\u4EE5\u540E\` / \`always\` / \`never\` / \`\u4E0B\u6B21\` / \`\u8BB0\u4E00\u4E0B\`, \u6216\u5728 \u22652 \u5019\u9009\u95F4\u6743\u8861\u540E\u9501\u5B9A\u65B9\u5411, \u6216\u62D2\u4E86\u5EFA\u8BAE\u5E76\u7ED9\u4E86\u7406\u7531(\u7406\u7531\u5373\u77E5\u8BC6); \u2461 **Wrong-turn-and-revert** \u2014 \u4F60\u8BD5\u4E86 path X \u53CD\u601D\u540E\u6539\u8D70 path Y("\u5426\u5B9A+\u66FF\u4EE3"\u4E24\u6B65\u7ED3\u6784, \u975E\u5355\u7EAF\u63A2\u7D22\u5931\u8D25)\u3002
57
+ - **\u4E0D\u89E6\u53D1**: \u7528\u6237\u7EAF\u8BE2\u95EE / \u7B80\u5355 refactor\xB7typo / \u51ED\u7A7A"\u6211\u5B66\u5230\u4E86"\u7684\u6D1E\u5BDF\u3002
58
+ - **\u9632 loop**: \u540C turn \u6700\u591A\u81EA\u8C03 1 \u6B21; \u540C session \u540C outcome \u4E0D\u91CD\u590D; skill \u5185 Phase 2.5 viability gate \u515C\u5E95\u3002
59
+ - **\u56DE\u6267 (marker-free)**: \u76F4\u63A5\u81EA\u8C03 \`fabric-archive\` skill \u5373\u53EF, \u65E0\u9700\u6253\u5370\u4EFB\u4F55\u6697\u53F7\u5B57\u7B26\u4E32 \u2014\u2014 skill \u9ED8\u8BA4\u628A AI \u81EA\u8C03\u8BC6\u522B\u4E3A E3(\u786E\u5B9A\u6027 else \u8DEF\u7531, \u4E0D\u518D\u4F9D\u8D56 AI \u8F93\u51FA\u7CBE\u786E\u5B57\u7B26\u4E32)\u3002skill \u843D pending \u540E\u8FD4\u56DE \`pending_path\`, \u4E0D\u8BE5\u8BB0\u5C31\u56DE \`undo\`(\u6211\u8C03 fab_review reject)\u3002
60
+
61
+ ## Cite policy (v2.2 C1: recall \u81EA\u52A8\u8BB0\u8D26, \u96F6\u9996\u884C\u8D1F\u62C5)
62
+
63
+ - **\u6838\u5FC3 (recall-first \u81EA\u52A8\u8BB0\u8D26)**: \u6539\u4EFB\u4F55\u6587\u4EF6\u524D\u5148 \`fab_recall(paths=[<\u88AB\u6539\u6587\u4EF6>])\`\u3002\u7CFB\u7EDF\u6309"\u672C session recall \u547D\u4E2D\u7684 path \u4E0E\u7F16\u8F91\u76EE\u6807\u91CD\u53E0"\u81EA\u52A8\u628A\u53EC\u56DE\u7684 KB \u8BB0\u4E3A\u8BE5\u6B21 edit \u7684\u5F15\u7528 \u2014\u2014 **\u65E0\u9700\u624B\u5199\u4EFB\u4F55\u56DE\u590D\u9996\u884C**(C1 \u5220\u9664\u9996\u884C \`KB:\` contract \u516B\u80A1:\u5148\u60F3\u540E\u8BF4,recall \u624D\u662F\u5F15\u7528\u53D1\u751F\u7684\u771F\u5B9E\u4FE1\u53F7)\u3002PreToolUse \u68C0\u6D4B\u4E0D\u5230\u76F8\u5173 recall \u65F6\u7ED9\u4E00\u6761\u8F6F nudge(nudge \u975E gate,\u5B88 KT-DEC-0007)\u3002
64
+ - **\u552F\u4E00\u8981\u5F00\u53E3\u7684\u65F6\u5019 (dismissed / override)**: \u4F60\u5224\u65AD\u67D0\u53EC\u56DE KB \u4E0D\u8BE5\u5E94\u7528\u65F6,\u8BF4\u4E00\u53E5 \`dismissed: <id> (<reason>)\`;reason \u679A\u4E3E \`scope-mismatch | outdated | not-applicable | other:<text>\`\u3002\u9700\u7CBE\u786E\u6807\u6CE8\u4ECD\u53EF\u7528\u9996\u884C \`KB: <id> [applied|dismissed]\`(\u89E3\u6790\u5668\u4FDD\u7559,\u5411\u540E\u517C\u5BB9)\u3002
65
+ - **\`[applied]\` \u9A8C\u8BC1\u4E49\u52A1**: \u5F15\u7528\u4EFB\u4F55 id(\u81EA\u52A8\u6216\u624B\u5199)\u524D\u5FC5\u987B\u5148 fab_recall \u5B9E\u9645\u6293\u56DE KB(\u6309\u9700\u5BF9\u6B63\u6587\u8DEF\u5F84\u505A\u539F\u751F Read),\u9632\u6B62\u7F16\u9020 id\u3002\u9A8C\u8BC1\u4E0D\u901A\u8FC7 = \u4E0D\u80FD cite\u3002
66
+ - **\u7A3D\u6838\u4E0E\u5B8C\u6574\u89C4\u7EA6**: \`fabric audit cite\` \u8F93\u51FA\u8986\u76D6\u7387(\u4E0D\u963B\u65AD\u5DE5\u4F5C,\u53EA\u8BB0\u5F55);contract operator / store \u524D\u7F00 / skip\xB7dismissed \u8BCD\u5178 / \u7C7B\u578B\u8DEF\u7531 / \u88C1\u51B3\u9636\u68AF\u7B49\u5B8C\u6574\u89C4\u7EA6\u6743\u5A01\u8BE6\u53C2 \`fabric-review\` skill \u7684 \`ref/cite-contract.md\` \u2014\u2014 bootstrap \u53EA\u7559\u53EF\u6267\u884C core\u3002
67
+ `;
68
+ var BOOTSTRAP_CANONICAL_EN = `# Fabric Bootstrap
69
+
70
+ This project uses Fabric to manage cross-client AI knowledge and behavior rules. This file is synced into the managed block on both clients by \`fabric install\` \u2014 **do not hand-edit the block on any client**; edit here + re-run \`fabric install\`.
71
+
72
+ ## For Developers
73
+
74
+ This file is the **AI client's policy & convention config**, not dev onboarding. You don't need to read the Self-archive / Cite / Phase 0.4 details.
75
+ As a dev you only need to: run \`fabric install\` once per repo, use \`fabric store bind <alias>\` / \`fabric store switch-write <alias>\` to wire up a write store, and run \`fabric doctor\` when something breaks.
76
+ **Never hand-edit \`.fabric/agents.meta.json\`** \u2014 derived state is rebuilt by the engine.
77
+
78
+ ## Dev Quickstart
79
+
80
+ **What Fabric is**: a cross-client (Claude Code / Codex CLI) AI knowledge layer. Store the team/project **decisions / pitfalls / guidelines / models / processes** as markdown; hooks surface them to the AI automatically so it doesn't re-learn every time.
81
+
82
+ **What you DO** vs **what the engine does (DON'T hand-edit)**:
83
+
84
+ | You DO | You DON'T |
85
+ | --- | --- |
86
+ | Run \`fabric install\` once per repo | Hand-edit \`.fabric/agents.meta.json\` |
87
+ | Run \`fabric doctor\` (--fix self-heals) on trouble | Hand-edit \`.cjs\` under \`.claude/hooks/\` |
88
+ | Use \`fabric-archive\` / \`fabric-review\` / \`fabric store ...\` to manage store-backed knowledge | Hand-write any non-store knowledge root |
89
+ | \`npm install -g @fenglimg/fabric-cli@latest\` to upgrade | Memorize the 35 doctor lint codes |
90
+
91
+ **4-step loop**: \`fabric install\` (once) \u2192 bind and pick a write store \u2192 AI works normally (hook on session start + edit) \u2192 the AI writes pending entries into the current write store via MCP and returns a \`pending_path\` \u2192 review with the \`fabric-review\` skill.
92
+
93
+ **Real example**: a sprite black-edge root cause was the \`atlas.premultiplyAlpha\` flag being inverted \u2014 once archived into the store's \`knowledge/pitfalls/\`, the AI auto-references it next time a similar issue shows up.
94
+
95
+ See \`docs/USER-QUICKSTART.md\` for the full maintainer version.
96
+
97
+ ## Behavior Rules
98
+ - **Before modifying any file**: first \`fab_recall(paths=[<file-being-edited>])\` \u2014\u2014 a single call returns the relevant KB descriptions + native read paths (\`entries[].read_path\`). \`fab_recall\` no longer delivers bodies; when you need a body, do a native Read of its \`entries[].read_path\` (\`Read <store>/knowledge/<type>/<id>--*.md\`), which the PostToolUse hook records as \`knowledge_body_read\`. Lean default: descriptions + index already suffice to discover entries; read a body once on demand, don't re-inject it every turn (KT-GLD-0005).
99
+ - **Never hand-edit \`.fabric/agents.meta.json\`**; the engine syncs derived state automatically \u2014 run \`fabric doctor --fix\` for an explicit reconcile.
100
+
101
+ ## Knowledge Base (KB)
102
+ - **Discovery**: the SessionStart hook lists broad-scoped entries (scoped by \`semantic_scope\` across three tiers: \`team\` team-wide / \`project:<id>\` this-project-only (surfaces only in repos bound to that project) / \`personal\` \`KP-*\`, all three referenced the same way); editing a file may trigger a narrow hint via the PreToolUse hook.
103
+ - **Scope's 3 axes (why something isn't surfacing)** (KT-MOD-0001): whether an entry surfaces is decided by three **orthogonal** axes \u2014 \u2460 \`semantic_scope\` audience (\`team\` / \`project:<id>\` / \`personal\`; the wrong project binding hides it) \u2461 \`relevance_scope\` timing (\`broad\` always-on / \`narrow\` only when you edit a matching file) \u2462 \`store\` physical lib (not read without \`fabric store bind\`). The axis names collide ("team" is both an audience value and a possible store alias), so when puzzled about "why isn't this surfacing" run \`fabric audit why-not-surfaced <id>\` for a per-cause diagnosis (store unbound / scope mismatch / narrow timing).
104
+ - **Usage**: go one-step \`fab_recall(paths=[...])\` to fetch the relevant KB descriptions + read paths in one call; when you need a body, do a native Read of its \`entries[].read_path\` (no second MCP round-trip for the body).
105
+ - **session_id**: when calling \`fab_recall\`, always pass the current client session id as the \`session_id\` argument (Claude Code's session id is in the stdin payload; Codex's corresponding identifier likewise). This lets \`fabric doctor --archive-history\` and the \`fabric-hint.cjs\` Stop hook track cross-session debt accurately.
106
+ - **Skills (4)**: write flow \`fabric-archive\` (with source-mode cold-start backfill from git/docs) / \`fabric-review\` (with retire-deprecation + relate-edge sub-flows); store ops \`fabric-store\` / \`fabric-sync\`.
107
+ - **Language**: rendered per the \`language\` field in \`~/.fabric/fabric-global.json\` (machine-wide tone).
108
+ - **Archive cadence nudge** (rc.36): after each batch of edits (default ~20, matching the Stop hook threshold config \`archive_edit_threshold\`) / a significant decision, proactively propose the \`fabric-archive\` skill at a suitable turn \u2014 without an archive cadence the KB slowly dies.
109
+ - **Review backlog nudge** (rc.36): to judge the pending backlog, go through \`fab_pending action="list"\` or the \`pending_path\` returned by \`fabric-review\`; don't glob the project-local \`.fabric/knowledge/pending\`. When the visible pending count exceeds 10, proactively propose the \`fabric-review\` skill at a suitable turn to batch-review and avoid draft deadlock.
110
+
111
+ ## Self-archive policy (v2.2 C1: lean spec)
112
+
113
+ When a clear archival signal appears, autonomously invoke the \`fabric-archive\` skill at the end of that turn (AI self-trigger entry E3):
114
+
115
+ - **Trigger** (either): \u2460 **User-driven normative** \u2014 the user says \`\u4EE5\u540E\` / \`always\` / \`never\` / \`\u4E0B\u6B21\` / \`\u8BB0\u4E00\u4E0B\`, or locks a direction with rationale after weighing \u22652 candidates, or rejects a suggestion and states a reason (the reason is knowledge); \u2461 **Wrong-turn-and-revert** \u2014 you tried path X, then after reflection switched to path Y (a two-step "negate + replace" structure, not mere exploratory failure).
116
+ - **Does NOT trigger**: pure user questions / simple refactor\xB7typo / a baseless "I learned something" insight.
117
+ - **Anti-loop**: at most 1 self-invocation per turn; no repeat for the same outcome in the same session; the skill's Phase 2.5 viability gate is the backstop.
118
+ - **Receipt (marker-free)**: just invoke the \`fabric-archive\` skill directly \u2014 no marker string to print: the skill routes an AI self-invocation to E3 by default (deterministic else-branch, no longer dependent on the AI emitting an exact string). The skill returns \`pending_path\` after writing pending \u2014 reply \`undo\` if it shouldn't be recorded (I'll call fab_review reject).
119
+
120
+ ## Cite policy (v2.2 C1: recall auto-accounting, zero first-line burden)
121
+
122
+ - **Core (recall-first auto-accounting)**: before changing any file, run \`fab_recall(paths=[<file-being-edited>])\` first. The system auto-accounts the recalled KB as that edit's citation by "paths recall-hit this session overlap the edit target" \u2014\u2014 **no hand-written first reply line needed** (C1 removes the first-line \`KB:\` contract boilerplate: think-then-speak, recall is the real signal that a citation happened). The PreToolUse hook gives a soft nudge when it detects no relevant recall (nudge, not a gate, per KT-DEC-0007).
123
+ - **The only time to speak up (dismissed / override)**: when you judge a recalled KB should NOT apply, say one line \`dismissed: <id> (<reason>)\`; reason enum \`scope-mismatch | outdated | not-applicable | other:<text>\`. For precise annotation you may still use a first-line \`KB: <id> [applied|dismissed]\` (parser retained, backward compatible).
124
+ - **\`[applied]\` verification duty**: citing any id (auto or hand-written) requires first fetching the KB via fab_recall (a native Read of its body path when needed) to prevent fabricated ids. Verification failing = you cannot cite.
125
+ - **Audit & full spec**: \`fabric audit cite\` reports coverage (does not block your work, only records); the full spec (contract operators / store prefix / skip\xB7dismissed dictionaries / type routing / adjudication ladder) lives authoritatively in the \`fabric-review\` skill's \`ref/cite-contract.md\` \u2014\u2014 bootstrap keeps only the executable core.
126
+ `;
127
+ var BOOTSTRAP_CANONICAL_BY_LOCALE = {
128
+ "zh-CN": BOOTSTRAP_CANONICAL_ZH,
129
+ en: BOOTSTRAP_CANONICAL_EN
130
+ };
131
+ function resolveBootstrapCanonical(locale) {
132
+ return BOOTSTRAP_CANONICAL_BY_LOCALE[locale ?? resolveGlobalLocale()];
133
+ }
134
+ function matchBootstrapCanonicalLocale(body) {
135
+ for (const locale of Object.keys(BOOTSTRAP_CANONICAL_BY_LOCALE)) {
136
+ if (BOOTSTRAP_CANONICAL_BY_LOCALE[locale] === body) {
137
+ return locale;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+
143
+ export {
144
+ BOOTSTRAP_MARKER_BEGIN,
145
+ BOOTSTRAP_MARKER_END,
146
+ BOOTSTRAP_REGEX,
147
+ BOOTSTRAP_CANONICAL_ZH,
148
+ BOOTSTRAP_CANONICAL_EN,
149
+ BOOTSTRAP_CANONICAL_BY_LOCALE,
150
+ resolveBootstrapCanonical,
151
+ matchBootstrapCanonicalLocale
152
+ };