@fenglimg/fabric-shared 2.3.0-rc.9 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,16 +49,24 @@ var _knowledgeTypeEnum = z3.enum(["models", "decisions", "guidelines", "pitfalls
49
49
  var _maturityEnum = z3.enum(["draft", "verified", "proven"]);
50
50
  var _ruleDescriptionSchema = z3.object({
51
51
  summary: z3.string(),
52
- intent_clues: z3.array(z3.string()),
52
+ // TASK-005 wire thinning: intent_clues dropped from recall wire (0 hook
53
+ // consumers grep-verified). Selection signal is summary + must_read_if
54
+ // (when distinct) + knowledge_type; intent_clues is preserved in the on-disk
55
+ // .md frontmatter (KB source-of-truth) and reachable via read_path.
56
+ intent_clues: z3.array(z3.string()).optional(),
53
57
  // wire-slim (payload): fab_recall projects a LEAN description (summary +
54
- // must_read_if + intent_clues + knowledge_type — the selection signal), leaving
55
- // tech_stack/impact to be Read on demand via read_path (KT-DEC-0026 lean
58
+ // must_read_if + knowledge_type — the selection signal), leaving tech_stack/
59
+ // impact/intent_clues to be Read on demand via read_path (KT-DEC-0026 lean
56
60
  // contract at the field level). So they are optional on the wire. plan-context
57
- // still returns them in full — zod keeps optional-present values, only ABSENT is
58
- // now allowed, so no plan-context consumer regresses.
61
+ // still returns them in full — zod keeps optional-present values, only ABSENT
62
+ // is now allowed, so no plan-context consumer regresses.
59
63
  tech_stack: z3.array(z3.string()).optional(),
60
64
  impact: z3.array(z3.string()).optional(),
61
- must_read_if: z3.string(),
65
+ // TASK-002 wire dedup: omitted when identical to `summary` (~40% of KB entries
66
+ // — knowledge-meta-builder.ts:212/:248 `?? summary` fallback). Consumers may
67
+ // fall back to `summary` when absent. KB source-of-truth (the .md frontmatter)
68
+ // stays unchanged; this optionality is a WIRE-only projection.
69
+ must_read_if: z3.string().optional(),
62
70
  // v2.0: optional knowledge-entry fields. Absent for v1.x rules; present for
63
71
  // entries that declare frontmatter `id/type/maturity`. W4/Track1: the redundant
64
72
  // `knowledge_layer` field was removed — a candidate's layer is derived from its
@@ -310,28 +318,40 @@ var recallInputSchema = z3.object({
310
318
  // W1-3 / KT-DEC-0031: graph expansion (surface related read paths, no body).
311
319
  include_related: z3.boolean().optional().describe(
312
320
  "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."
321
+ ),
322
+ // TASK-006 (KT-PIT-0036 observability opt-in): score_breakdown carries the
323
+ // numbers-only signal decomposition (bm25/vector/salience/recency/locality/
324
+ // proximity/credibility → final). Emitted per-entry ONLY when this flag is
325
+ // true — the debug/tuning surface. Steady-state recall omits it to stay lean
326
+ // (~4.8KB saved on a 24-entry sample). final===score invariant still enforced
327
+ // at the plan-context service layer (candidate_scores Map) regardless.
328
+ include_score_breakdown: z3.boolean().optional().describe(
329
+ "When true, populate `entry.score_breakdown` (numbers-only signal decomposition \u2014 bm25/vector/salience/recency/locality/proximity/credibility \u2192 final). Off by default for wire efficiency. Enable when debugging ranking or tuning scoring weights."
313
330
  )
314
331
  });
332
+ var _recallEntryDescriptionSchema = z3.object({
333
+ summary: z3.string(),
334
+ // TASK-002: omitted when identical to summary (~40% dedup).
335
+ must_read_if: z3.string().optional(),
336
+ // Semantic-preservation (PLN-002 F1 restored): knowledge-hint-narrow.cjs
337
+ // consumes this for the "⚠️ 后果" narrow-hint line.
338
+ impact: z3.array(z3.string()).optional(),
339
+ // Semantic-preservation (PLN-002): cite-contract-reminder.cjs consumes it.
340
+ knowledge_type: _knowledgeTypeEnum.optional()
341
+ });
315
342
  var _recallEntrySchema = z3.object({
316
343
  stable_id: z3.string(),
317
- // 1-based relevance rank (entries are returned best-first). The surfaced
318
- // ranking signal entries are already sorted, rank makes the order explicit.
319
- rank: z3.number().int().positive(),
320
- // The DESCRIPTION (summary / intent_clues / must_read_if / related ...). No body.
321
- description: _ruleDescriptionSchema,
344
+ // The projected DESCRIPTION (recall-specific slim contract, not the shared
345
+ // frontmatter shape). Codex review F6: dedicated schema locks the wire contract.
346
+ description: _recallEntryDescriptionSchema,
322
347
  // on-disk knowledge file to Read for the full body. Omitted when the entry has
323
348
  // no resolvable file (description-only discovery) or was scoped out by `ids`.
324
349
  read_path: z3.string().optional(),
325
350
  // originating store alias (omitted for unqualified / single-store entries).
326
- store: z3.object({ alias: z3.string() }).optional(),
351
+ store_alias: z3.string().optional(),
327
352
  // true when this entry's body is ALSO injected at SessionStart (broad
328
353
  // model/guideline "ALWAYS-ACTIVE") — skip the Read, it is already in context.
329
354
  body_in_context: z3.boolean().optional(),
330
- // P1 recall-observability: the fused relevance score this entry scored during
331
- // the plan-context sort (was computed internally but dropped before this wave).
332
- // Optional + additive — backward-compatible. MUST be declared here or zod
333
- // .strip() silently drops it at the MCP boundary (KT-PIT-0005).
334
- score: z3.number().optional(),
335
355
  // P1 recall-observability: numbers-only decomposition of `score` into its
336
356
  // weighted signal contributions. NEVER carries body/description text — preserves
337
357
  // the lean read_path contract (KT-DEC-0019 / KT-GLD-0005). bm25_rank/vector_rank
@@ -355,25 +375,24 @@ var _recallEntrySchema = z3.object({
355
375
  }).optional()
356
376
  });
357
377
  var recallOutputSchema = z3.object({
378
+ // Retained: client hook cache key (packages/cli/.claude/hooks/knowledge-hint-narrow.cjs)
358
379
  revision_hash: z3.string(),
359
- stale: z3.boolean(),
360
380
  // ux-w2-4: single unified entry list (was candidates[] + paths[] + per-path
361
381
  // requirement-profile entries[]). Each item carries description + read_path +
362
382
  // rank + body_in_context, so the agent never joins two arrays on stable_id.
363
383
  entries: z3.array(_recallEntrySchema),
364
- // v2.2 payload de-dup: single top-level echo of the caller's `intent`.
365
- // Omitted when no intent.
366
- intent: z3.string().optional(),
367
384
  // K6 (W3-K): structured list of lower-ranked candidates dropped by the
368
- // retrieval pipeline, each tagged with WHY (`retrieval_budget` = top_k cap +
369
- // ratio-to-top floor; `payload_budget` = MCP payload-byte trim). Present (and
370
- // non-empty) ONLY when truncation fired — keeps the steady-state wire shape
371
- // unchanged while signalling which entries exist ("narrow your intent"), now
372
- // with a controlled reason instead of a bare count. Reuses the archive-scan
373
- // {key,reason} omission convention (_recallDropReasonSchema, keyed on id).
374
- dropped: z3.array(z3.object({ id: z3.string(), reason: _recallDropReasonSchema })).optional(),
375
- preflight_diagnostics: z3.array(_preflightDiagnosticSchema),
385
+ // retrieval pipeline. dropped_ids preserves per-id transparency (KT-DEC-0028);
386
+ // dropped_reasons hoists the reason to a top-level count map (68/68 same-reason
387
+ // observation from ANL-002). Present ONLY when truncation fired.
388
+ dropped_ids: z3.array(z3.string()).optional(),
389
+ dropped_reasons: z3.object({
390
+ retrieval_budget: z3.number().int().nonnegative().optional(),
391
+ payload_budget: z3.number().int().nonnegative().optional()
392
+ }).optional(),
393
+ preflight_diagnostics: z3.array(_preflightDiagnosticSchema).optional(),
376
394
  warnings: z3.array(structuredWarningSchema).optional(),
395
+ // Auto-heal banner pair (consumed by knowledge-hint-broad.cjs:711-729).
377
396
  auto_healed: z3.boolean().optional(),
378
397
  previous_revision_hash: z3.string().optional(),
379
398
  // v2.0.0-rc.37 NEW-24: parallel to planContextOutputSchema.redirects — stale
@@ -384,9 +403,6 @@ var recallOutputSchema = z3.object({
384
403
  // (appended id → surfaced source id). Present only when include_related
385
404
  // appended an in-corpus neighbour. Omitted on the steady-state path.
386
405
  related_appended: z3.record(z3.string()).optional(),
387
- // v2.2 MC1-recall-pack: standing behavioral directive (cite-before-edit) +
388
- // dynamic discovery hints, so the one-call recall is self-describing.
389
- directive: z3.string(),
390
406
  next_steps: z3.array(z3.string()).optional()
391
407
  });
392
408
  var recallAnnotations = {
@@ -693,7 +709,18 @@ var _fabReviewModifyChangesSchema = z3.object({
693
709
  // impact are flow-arrays.
694
710
  must_read_if: z3.string().optional(),
695
711
  intent_clues: z3.array(z3.string()).optional(),
696
- impact: z3.array(z3.string()).optional()
712
+ impact: z3.array(z3.string()).optional(),
713
+ // ISS-20260711-180: propose writes these; without them here zod .strip()
714
+ // silently drops modify patches (KT-PIT-0005 recurrence).
715
+ tech_stack: z3.array(z3.string()).optional(),
716
+ evidence_paths: z3.array(z3.string()).optional(),
717
+ onboard_slot: z3.enum([
718
+ "tech-stack-decision",
719
+ "architecture-pattern",
720
+ "code-style-tone",
721
+ "build-system-idiom",
722
+ "domain-vocabulary"
723
+ ]).optional()
697
724
  });
698
725
  var FabReviewInputSchema = z3.discriminatedUnion("action", [
699
726
  z3.object({
@@ -728,11 +755,45 @@ var FabReviewInputSchema = z3.discriminatedUnion("action", [
728
755
  layer: z3.enum(["team", "personal"])
729
756
  })
730
757
  }),
758
+ // v2.3 batch content-modify: array-native flush for the fabric-review maintain
759
+ // loop. Each item is an INDEPENDENT content edit — unlike approve/reject/defer
760
+ // (which share one reason/until over pending_paths[]), modify needs its OWN
761
+ // changes per entry, so the shape is items[] not paths[]. Layer is stripped
762
+ // per item (content-only; layer-flips stay on the single interactive
763
+ // modify-layer path). Per-item failure is isolated: a bad item reports
764
+ // {ok:false, error} in modified[] without aborting its siblings.
765
+ z3.object({
766
+ action: z3.literal("modify-content-batch"),
767
+ items: z3.array(
768
+ z3.object({
769
+ pending_path: z3.string().min(1),
770
+ changes: _fabReviewModifyChangesSchema
771
+ })
772
+ ).min(1)
773
+ }),
731
774
  z3.object({
732
775
  action: z3.literal("defer"),
733
776
  pending_paths: z3.array(z3.string()).min(1),
734
777
  until: z3.string().datetime().optional(),
735
778
  reason: z3.string().optional()
779
+ }),
780
+ // retire (W3-C: fabric-review retire-mode landing surface). Semantically
781
+ // deprecates one or more CANONICAL knowledge entries so they stop surfacing in
782
+ // recall candidates / broad SessionStart indexes — WITHOUT deleting the file
783
+ // (red line: deprecate-over-delete). The service writes `deprecated: true`
784
+ // (+ `superseded_by: <id>` when the entry is replaced) into the entry's
785
+ // frontmatter via the same in-place merge path modify uses; body + stable_id
786
+ // are preserved so the "当时为什么这么决策" rationale stays inspectable.
787
+ z3.object({
788
+ action: z3.literal("retire"),
789
+ // Canonical entry paths (store-absolute, from fab_pending list/search).
790
+ pending_paths: z3.array(z3.string()).min(1),
791
+ // Optional stable_id of the entry that supersedes these (bare `KT-DEC-0001`
792
+ // or store-qualified `alias:KT-DEC-0001`). Written as `superseded_by`
793
+ // frontmatter so the supersession chain is recoverable.
794
+ superseded_by: z3.string().optional(),
795
+ // Optional human reason recorded on the knowledge_modified ledger event.
796
+ reason: z3.string().optional()
736
797
  })
737
798
  ]);
738
799
  var FabPendingInputSchema = z3.discriminatedUnion("action", [
@@ -758,11 +819,20 @@ var FabPendingInputShape = {
758
819
  )
759
820
  };
760
821
  var FabReviewInputShape = {
761
- action: z3.enum(["approve", "reject", "modify", "modify-content", "modify-layer", "defer"]).describe(
762
- "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.)"
822
+ action: z3.enum([
823
+ "approve",
824
+ "reject",
825
+ "modify",
826
+ "modify-content",
827
+ "modify-layer",
828
+ "modify-content-batch",
829
+ "defer",
830
+ "retire"
831
+ ]).describe(
832
+ "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; modify-content-batch flushes an array of independent content edits (items[]) in one call; retire marks canonical entries deprecated (deprecate-over-delete) so they stop surfacing. (list/search moved to the read-only fab_pending tool.)"
763
833
  ),
764
834
  pending_paths: z3.array(z3.string()).min(1).optional().describe(
765
- "Workspace-relative pending entry paths. Required when action=approve|reject|defer (non-empty array)."
835
+ "Workspace-relative pending entry paths (or canonical entry paths for action=retire). Required when action=approve|reject|defer|retire (non-empty array)."
766
836
  ),
767
837
  pending_path: z3.string().min(1).optional().describe(
768
838
  "Workspace-relative pending OR canonical entry path. Required when action=modify."
@@ -773,8 +843,19 @@ var FabReviewInputShape = {
773
843
  changes: _fabReviewModifyChangesSchema.optional().describe(
774
844
  "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)."
775
845
  ),
846
+ items: z3.array(
847
+ z3.object({
848
+ pending_path: z3.string().min(1),
849
+ changes: _fabReviewModifyChangesSchema
850
+ })
851
+ ).min(1).optional().describe(
852
+ "Batch of independent content edits \u2014 required (non-empty) when action=modify-content-batch. Each item {pending_path, changes} is applied content-only (layer stripped); per-item failures surface in modified[] without aborting siblings."
853
+ ),
776
854
  until: z3.string().datetime().optional().describe(
777
855
  "ISO-8601 datetime upper bound for the deferral. Optional; used only when action=defer."
856
+ ),
857
+ superseded_by: z3.string().optional().describe(
858
+ "Stable_id (bare or store-qualified) of the entry that supersedes the retired one, written as `superseded_by` frontmatter. Optional; used only when action=retire."
778
859
  )
779
860
  };
780
861
  var _fabReviewListItemSchema = z3.object({
@@ -793,6 +874,8 @@ var _fabReviewListItemSchema = z3.object({
793
874
  tags: z3.array(z3.string()).optional(),
794
875
  title: z3.string().optional(),
795
876
  summary: z3.string().optional(),
877
+ // ISS-20260712-011: triage UI needs proposed_reason without a second Read.
878
+ proposed_reason: z3.string().optional(),
796
879
  // Store-only cutover: origin reflects the resolved store audience where the
797
880
  // pending file lives; layer reflects the declared classification that will
798
881
  // drive approval semantics.
@@ -832,10 +915,16 @@ var _fabReviewSearchItemSchema = z3.object({
832
915
  // hits in the same array.
833
916
  stable_id: z3.string().optional()
834
917
  });
918
+ var _fabReviewFailedItemSchema = z3.object({
919
+ pending_path: z3.string(),
920
+ reason: z3.string()
921
+ });
835
922
  var FabReviewOutputSchema = z3.discriminatedUnion("action", [
836
923
  z3.object({
837
924
  action: z3.literal("approve"),
838
925
  approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })),
926
+ // ISS-20260712-012: partial-failure surface so empty approved[] is not false-success.
927
+ failed: z3.array(_fabReviewFailedItemSchema).optional(),
839
928
  warnings: z3.array(structuredWarningSchema).optional()
840
929
  }),
841
930
  z3.object({
@@ -851,10 +940,41 @@ var FabReviewOutputSchema = z3.discriminatedUnion("action", [
851
940
  new_stable_id: z3.string().optional(),
852
941
  warnings: z3.array(structuredWarningSchema).optional()
853
942
  }),
943
+ // v2.3 batch content-modify result: per-item {pending_path, ok, error?}. No
944
+ // prior/new_stable_id — content edits strip layer, so no id reallocation can
945
+ // occur. ok=false items carry the failure message; siblings still applied
946
+ // (partial failure is reported per-item, never thrown for the whole batch).
947
+ z3.object({
948
+ action: z3.literal("modify-content-batch"),
949
+ modified: z3.array(
950
+ z3.object({
951
+ pending_path: z3.string(),
952
+ ok: z3.boolean(),
953
+ error: z3.string().optional()
954
+ })
955
+ ),
956
+ warnings: z3.array(structuredWarningSchema).optional()
957
+ }),
854
958
  z3.object({
855
959
  action: z3.literal("defer"),
856
960
  deferred: z3.array(z3.string()),
857
961
  warnings: z3.array(structuredWarningSchema).optional()
962
+ }),
963
+ z3.object({
964
+ action: z3.literal("retire"),
965
+ // Each retired canonical entry: its echoed path + (when the frontmatter
966
+ // carried one) its stable_id, plus the superseded_by id when supplied. The
967
+ // file is NOT deleted — only marked `deprecated: true` in place.
968
+ retired: z3.array(
969
+ z3.object({
970
+ path: z3.string(),
971
+ stable_id: z3.string().optional(),
972
+ superseded_by: z3.string().optional()
973
+ })
974
+ ),
975
+ // ISS-20260712-012: partial-failure surface for skipped/unresolvable paths.
976
+ failed: z3.array(_fabReviewFailedItemSchema).optional(),
977
+ warnings: z3.array(structuredWarningSchema).optional()
858
978
  })
859
979
  ]);
860
980
  var FabPendingOutputSchema = z3.discriminatedUnion("action", [
@@ -879,12 +999,15 @@ var FabPendingOutputShape = {
879
999
  warnings: z3.array(structuredWarningSchema).optional()
880
1000
  };
881
1001
  var FabReviewOutputShape = {
882
- action: z3.enum(["approve", "reject", "modify", "defer"]).describe(
1002
+ action: z3.enum(["approve", "reject", "modify", "modify-content-batch", "defer", "retire"]).describe(
883
1003
  "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.)"
884
1004
  ),
885
1005
  approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })).optional().describe(
886
1006
  "Allocated stable ids paired with their original pending paths. Present when action=approve."
887
1007
  ),
1008
+ failed: z3.array(z3.object({ pending_path: z3.string(), reason: z3.string() })).optional().describe(
1009
+ "Paths that did not apply (approve/retire). Present when any path was skipped or failed."
1010
+ ),
888
1011
  rejected: z3.array(z3.string()).optional().describe(
889
1012
  "Pending paths that were rejected (files retained on disk; doctor owns vacuum). Present when action=reject."
890
1013
  ),
@@ -897,9 +1020,27 @@ var FabReviewOutputShape = {
897
1020
  new_stable_id: z3.string().optional().describe(
898
1021
  "New stable id after reallocation. Present when action=modify AND a layer-flip reallocated the id."
899
1022
  ),
1023
+ modified: z3.array(
1024
+ z3.object({
1025
+ pending_path: z3.string(),
1026
+ ok: z3.boolean(),
1027
+ error: z3.string().optional()
1028
+ })
1029
+ ).optional().describe(
1030
+ "Per-item results for action=modify-content-batch: {pending_path, ok, error?}. ok=false items carry the error; siblings still applied."
1031
+ ),
900
1032
  deferred: z3.array(z3.string()).optional().describe(
901
1033
  "Pending paths that were deferred (files retained on disk). Present when action=defer."
902
1034
  ),
1035
+ retired: z3.array(
1036
+ z3.object({
1037
+ path: z3.string(),
1038
+ stable_id: z3.string().optional(),
1039
+ superseded_by: z3.string().optional()
1040
+ })
1041
+ ).optional().describe(
1042
+ "Canonical entries marked deprecated in place (files retained \u2014 deprecate-over-delete). Present when action=retire."
1043
+ ),
903
1044
  // v2.0.0-rc.23 TASK-009 (d): optional warnings surface for the first-reconcile
904
1045
  // gate (`meta_stale` / `reconcile_failed`). Absent on the steady-state path.
905
1046
  warnings: z3.array(structuredWarningSchema).optional()
@@ -1,3 +1,8 @@
1
+ import {
2
+ atomicWriteJson,
3
+ withFileLock
4
+ } from "./chunk-C7WZPYZE.js";
5
+
1
6
  // src/i18n/normalize-locale.ts
2
7
  function normalizeLocale(raw) {
3
8
  if (typeof raw !== "string") {
@@ -189,7 +194,8 @@ var globalConfigSchema = z.object({
189
194
  }).passthrough();
190
195
 
191
196
  // src/store/global-config-io.ts
192
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
197
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
198
+ import { mkdir } from "fs/promises";
193
199
  import { homedir } from "os";
194
200
  import { join } from "path";
195
201
  function isTestRuntime() {
@@ -210,6 +216,9 @@ function resolveGlobalRoot() {
210
216
  function globalConfigPath(globalRoot = resolveGlobalRoot()) {
211
217
  return join(globalRoot, "fabric-global.json");
212
218
  }
219
+ function globalConfigLockPath(globalRoot) {
220
+ return `${globalConfigPath(globalRoot)}.lock`;
221
+ }
213
222
  function loadGlobalConfig(globalRoot = resolveGlobalRoot()) {
214
223
  const path = globalConfigPath(globalRoot);
215
224
  if (!existsSync(path)) {
@@ -217,11 +226,30 @@ function loadGlobalConfig(globalRoot = resolveGlobalRoot()) {
217
226
  }
218
227
  return globalConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
219
228
  }
229
+ async function saveGlobalConfigAsync(config, globalRoot = resolveGlobalRoot()) {
230
+ const validated = globalConfigSchema.parse(config);
231
+ await mkdir(globalRoot, { recursive: true });
232
+ const path = globalConfigPath(globalRoot);
233
+ await withFileLock(globalConfigLockPath(globalRoot), async () => {
234
+ await atomicWriteJson(path, validated, { indent: 2 });
235
+ });
236
+ }
220
237
  function saveGlobalConfig(config, globalRoot = resolveGlobalRoot()) {
221
238
  const validated = globalConfigSchema.parse(config);
222
239
  mkdirSync(globalRoot, { recursive: true });
223
- writeFileSync(globalConfigPath(globalRoot), `${JSON.stringify(validated, null, 2)}
240
+ const path = globalConfigPath(globalRoot);
241
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
242
+ try {
243
+ writeFileSync(tmpPath, `${JSON.stringify(validated, null, 2)}
224
244
  `, "utf8");
245
+ renameSync(tmpPath, path);
246
+ } catch (error) {
247
+ try {
248
+ unlinkSync(tmpPath);
249
+ } catch {
250
+ }
251
+ throw error;
252
+ }
225
253
  }
226
254
 
227
255
  // src/i18n/resolve-global-locale.ts
@@ -269,6 +297,7 @@ export {
269
297
  resolveGlobalRoot,
270
298
  globalConfigPath,
271
299
  loadGlobalConfig,
300
+ saveGlobalConfigAsync,
272
301
  saveGlobalConfig,
273
302
  resolveGlobalLocale
274
303
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveGlobalLocale
3
- } from "./chunk-ASS2KBB7.js";
3
+ } from "./chunk-OX6DWUK4.js";
4
4
 
5
5
  // src/templates/bootstrap-canonical.ts
6
6
  var BOOTSTRAP_MARKER_BEGIN = "<!-- fabric:bootstrap:begin -->";
@@ -36,8 +36,9 @@ var BOOTSTRAP_CANONICAL_ZH = `# Fabric Bootstrap
36
36
  \u5B8C\u6574 maintainer \u7248\u89C1 \`docs/USER-QUICKSTART.md\`\u3002
37
37
 
38
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
39
+ - **Pre-action gating (\u4FEE\u6539\u4EFB\u4F55\u6587\u4EF6\u524D MUST)**: \u2460 \u5148 \`fab_recall(paths=[<\u88AB\u6539\u6587\u4EF6>], session_id=<client-session-id>)\`\uFF1B\u2461 \u6309\u9700\u539F\u751F Read \u547D\u4E2D\u7684 \`entries[].read_path\` \u6B63\u6587(PostToolUse \u8BB0\u4E3A \`knowledge_body_read\`)\uFF1B\u2462 \u8DF3\u8FC7\u4E0D\u9002\u7528\u547D\u4E2D\u65F6\u8BF4 \`dismissed: <id> (<reason>)\`\u3002\`fab_recall\` \u4E0D\u518D\u6295\u9012\u6B63\u6587;lean \u9ED8\u8BA4\u63CF\u8FF0+\u7D22\u5F15\u5DF2\u591F\u53D1\u73B0\u6761\u76EE(KT-GLD-0005)\u3002PreToolUse \u4EC5\u8F6F nudge,\u5B88 KT-DEC-0007(\u975E permanent gate / \u65E0 \`decision:block\`)\u3002
40
+ - **Archive-as-truth**: \u8FDB canonical **\u552F\u4E00\u8DEF\u5F84** \u662F \`fab_propose\` \u2192 pending \u2192 \`fabric-review\`/\`fab_review\` \u5BA1\u6279\uFF1B\u7981\u6B62\u628A chat/log \u81EA\u52A8 promote \u8FDB canonical\u3002
41
+ - **\`.fabric/agents.meta.json\` \u4E25\u7981\u624B\u52A8\u7F16\u8F91**\uFF08co-location index \u5DF2\u9000\u5F79;\u77E5\u8BC6\u5728 mounted stores\uFF09\u3002\u5F02\u5E38\u65F6\u8DD1 \`fabric doctor\` / \`fabric doctor --fix\` \u81EA\u6108\u53EF\u4FEE\u590D\u9879\u3002
41
42
 
42
43
  ## \u77E5\u8BC6\u5E93(KB)
43
44
  - **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
@@ -46,7 +47,7 @@ var BOOTSTRAP_CANONICAL_ZH = `# Fabric Bootstrap
46
47
  - **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
48
  - **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
49
  - **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
+ - **Archive cadence nudge** (rc.36 / finish\u2192archive): \u663E\u8457 decision \u6536\u53E3\u6216\u4E00\u6279 Edit \u8FBE\u5230 config \`archive_edit_threshold\`(\u9ED8\u8BA4 20) \u540E,\u5728\u5408\u9002\u56DE\u5408\u8F7B\u91CF\u81EA\u8C03 \`fabric-archive\`(\u540C turn \u6700\u591A 1 \u6B21;\u975E task engine / \u975E Stop-hook flood)\u3002Stop hook \u4EC5 soft threshold nudge,\u5B88 KT-DEC-0007 \u2014 archive \u6CA1\u5EFA\u7ACB\u9891\u7387\u4F1A\u8BA9 KB \u6162\u901F\u6B7B\u6389\u3002
50
51
  - **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
 
52
53
  ## Self-archive policy (v2.2 C1: \u7CBE\u7B80\u8BF4\u660E\u4E66)
@@ -95,8 +96,9 @@ As a dev you only need to: run \`fabric install\` once per repo, use \`fabric st
95
96
  See \`docs/USER-QUICKSTART.md\` for the full maintainer version.
96
97
 
97
98
  ## 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.
99
+ - **Pre-action gating (MUST before Edit/Write)**: (1) call \`fab_recall(paths=[<file-being-edited>], session_id=<client-session-id>)\`; (2) optionally native-Read selected \`entries[].read_path\` bodies (PostToolUse records \`knowledge_body_read\`); (3) when skipping a hit, say \`dismissed: <id> (<reason>)\`. \`fab_recall\` no longer delivers bodies; lean default is description+index discovery (KT-GLD-0005). PreToolUse is soft nudge only per KT-DEC-0007 (never permanent gate / no \`decision:block\`).
100
+ - **Archive-as-truth**: the **only** path into canonical is \`fab_propose\` \u2192 pending \u2192 \`fabric-review\`/\`fab_review\` approve; never auto-promote chat/logs into canonical.
101
+ - **Never hand-edit \`.fabric/agents.meta.json\`** (co-location index retired; knowledge lives in mounted stores). For anomalies run \`fabric doctor\` / \`fabric doctor --fix\` on fixable state.
100
102
 
101
103
  ## Knowledge Base (KB)
102
104
  - **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.
@@ -105,7 +107,7 @@ See \`docs/USER-QUICKSTART.md\` for the full maintainer version.
105
107
  - **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
108
  - **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
109
  - **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.
110
+ - **Archive cadence nudge** (rc.36 / finish\u2192archive): after a significant decision lands or an edit batch reaches config \`archive_edit_threshold\` (default 20), lightly self-trigger \`fabric-archive\` at a suitable turn (max 1 per turn; not a task engine / not Stop-hook flood). Stop hook remains a soft threshold nudge per KT-DEC-0007 \u2014 without an archive cadence the KB slowly dies.
109
111
  - **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
112
 
111
113
  ## Self-archive policy (v2.2 C1: lean spec)