mind-cli 0.44.0

A manager for agent tooling (skills, agents, rules, tools) that melds arbitrary git repos and links items into your agent directories.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
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
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
# Discovery

How a source's installable items are found and described. The catalog is source
truth: it holds bare names; prefixing and token expansion are install-time
transforms (see namespacing.md).

## Precedence

Three layers, in order:

- `DSC-1` Convention discovery is the zero-config default: any melded repo is
  scanned with no manifest required.
- `DSC-2` Per-item frontmatter is always read for an item's description.
- `DSC-3` A `mind.toml` at the repo root is optional. `[source]` metadata is read
  whenever the file is present. If it declares `[[items]]` or `[discover]` item
  globs it becomes authoritative and convention discovery is skipped for that
  source. A bare `[discover].sources` list (no item globs) does not disable
  convention.

A fourth, external-manifest layer reads Claude Code's native plugin manifests
(`.claude-plugin/marketplace.json`, `.claude-plugin/plugin.json`) as a discovery
input for repos published to the built-in plugin system. It feeds the same catalog
and leaves the install model unchanged; an authoritative `mind.toml` (DSC-3) still
wins over it. A co-present `mind.toml` composes at a finer grain: own-item
directives (`[[items]]`, `[discover]` item globs, `roots`, `flat-skills`) suppress
the manifest's own-item layer, while a `[discover].sources` list layers a curated
super-source on top of the manifest (MKT-15, MKT-16). See
[marketplace.md](marketplace.md) (MKT-1..16).

## Convention

- `DSC-10` A skill is a directory `skills/<name>/` containing `SKILL.md`; its name
  is the directory name.
- `DSC-11` An agent is a file `agents/<name>.md`; its name is the file stem.
- `DSC-12` A rule is a file `rules/<name>.md`; its name is the file stem.
- `DSC-14` A command is a file `commands/<name>.md`; its name is the file stem.
  The scan is flat, as for agents and rules (commands.md CMD-1, CMD-2).
- `DSC-13` A missing `skills/`, `agents/`, `rules/`, `commands/`, `workflows/`,
  or `tools/` directory yields no items (not an error).
- `DSC-108` The scan classifies every entry by symlink metadata, not by
  following the link, so a symlink cannot make an absent file appear present, an
  unrelated file's size be reported, or an unrelated file's metadata be
  republished. A symlinked entry is not discovered as an item, whatever it points
  at. The rule covers every classification the scan makes, not one kind's:

  - a single-file kind's entry (`agents/<name>.md`, `rules/<name>.md`,
    `commands/<name>.md`, `workflows/<name>.js`);
  - both halves of the skill shape: the `skills/<name>/` directory AND its
    `SKILL.md` anchor;
  - a `tools/<name>/` directory, which has no anchor file, so its own
    classification is the whole test of whether the item exists;
  - the kind container itself (`skills/`, `agents/`, `workflows/`, ...): a
    container that is a symlink is not listed, so the scan never walks a
    directory outside the source;
  - the same components read from a Claude plugin or a marketplace entry's
    in-repo plugin (marketplace.md MKT-3, MKT-14, MKT-18, workflows.md WF-40),
    including a leaf skill directory an entry's `skills` array names directly;
  - a `[discover]` glob match (DSC-80), which matches by name and so would
    otherwise be a second route to the same answers. Such a match is dropped
    silently, as the convention scan drops such an entry; a match whose link
    resolves outside the repo root remains DSC-81's hard error, which takes
    precedence.

  A symlinked entry that is therefore not an item is also not counted as a
  mapped plugin component: it is reported among the unmapped entries (MKT-4),
  since what the scan leaves behind must not be described as installed.

  This matters because the scan runs over a tree the source controls entirely.
  Following links would let a repo of `workflows/<probe>.js` symlinks aimed at
  absolute paths on the consumer's machine answer "does this path exist" per
  entry through the item listing, and would let `review`'s workflow size report
  print the byte count of a file outside the repo. A symlinked `SKILL.md` is
  worse than an existence bit: the anchor is the file whose frontmatter becomes
  the item's description, so following it republishes a linked-to file's
  `description:` through `recall` and `probe`. It also matches what install would
  do with such an entry: the copy walk rejects a symlink at or inside an item
  tree outright (lifecycle.md LIFE-42), so an item the scan declines to offer
  here was never installable, and discovering one could only ever produce an
  offer `mind` cannot honor.

  The rule is about DISCOVERY. An explicit `[[items]]` entry names its own path,
  and a declared path that is a symlink is not refused at scan time (the author
  wrote it); it fails at install, where LIFE-42 names the symlink.

## Frontmatter

- `DSC-20` An item's description is the top-level `description` from the YAML
  frontmatter of its `SKILL.md` (skill) or its `.md` (agent, rule, command).
- `DSC-21` The frontmatter reader handles a leading `--- ... ---` block with
  top-level scalar keys and surrounding quotes, including block scalars (DSC-22).
  Flow collections and nested mappings are not interpreted. No frontmatter yields
  no description.
- `DSC-22` The frontmatter reader interprets a top-level block scalar value: a
  folded scalar (`>`, `>-`, `>+`) or a literal scalar (`|`, `|-`, `|+`) introduced
  by the indicator after the colon. The value is the run of more-indented lines
  that follow, ending at the first line that dedents to or below the key's
  indentation or at the closing `---`. A folded scalar joins its lines with single
  spaces (a blank line is a paragraph break); a literal scalar preserves its line
  breaks; the chomping indicator (`-` strip, `+` keep, none clip) governs trailing
  newlines. The result is trimmed for display in `recall`/`probe`. Other nested
  structures and flow collections remain uninterpreted.
- `DSC-23` The frontmatter reader strips a leading UTF-8 BOM (`\xEF\xBB\xBF`,
  U+FEFF) from the file text before checking for the `---` opening delimiter.
  A file authored on Windows or by an editor that emits a BOM still has its
  frontmatter read correctly.
- `DSC-94` An item's description (DSC-20, from frontmatter, or a `[[items]]`
  override, DSC-32) has ANSI escape sequences, control characters, and
  security-blocked Unicode code points (bidi overrides, directional marks,
  zero-width characters) stripped at the single catalog capture point
  (`build_item`), the same class of sanitization the plugin-manifest discovery
  path (marketplace.md) already applies to its own description fields. This
  makes every display surface (`recall`, `probe --no-tui`, `--json`, which
  otherwise only ANSI-escapes but does not strip a bidi override) safe by
  construction rather than depending on each call site remembering to sanitize
  before printing. A source `mind.toml`'s `[source].description` (below) is
  sanitized the same way, at every point it is read into the registry (meld,
  sync, pin/freeze).

## mind.toml

```toml
[source]
description = "..."          # optional; shown by recall --sources
prefix = "jk"                # optional; namespace (see namespacing.md)
min-mind-version = "0.2"     # minimum mind version; enforced at scan/meld (DSC-40)
roots = ["packages/tools"]   # optional; convention scan roots (DSC-50)
flat-skills = true           # optional; skills are bare dirs at a root, no
                             #   skills/ container (DSC-74)
follow-branch = "main"       # optional; pin directive, one of
                             #   follow-branch / pin-tag / pin-ref (DSC-41)

[[items]]                     # explicit inventory (authoritative)
kind = "skill"               # skill | agent | rule | command | workflow | tool
name = "review"
path = "skills/review"       # relative to repo root; a dir for skills
link = "rules/x.md"          # optional; link target relative to ~/.claude
description = "..."          # optional; overrides frontmatter

[discover]                    # glob discovery (authoritative for items)
skills = { include = ["packages/*/SKILL.md"], exclude = ["packages/internal-*/SKILL.md"] }
agents = { include = ["agents/*.md"] }
rules  = { include = ["rules/*.md"] }
# A curated super-source: list other repos to meld recursively.
sources = [
  { source = "owner/repo" },
  { source = "github:foo/bar", as = "fb" },        # impose a namespace on the nested source
  { source = "owner/recommended", install = true } # offer this one for install on meld
]
# Adopt an un-onboarded source: supply config it lacks (applied only when it has
# no mind.toml of its own). The array-of-tables form carries hooks:
[[discover.sources]]
source = "owner/unonboarded"
on-auth-failure = { action = "skip", message = "..." } # optional; skip if auth fails (DSC-68)
follow-branch = "main"           # pin directive for the nested source (DSC-41)
roots = ["packages/agents"]      # scan roots for the nested source (DSC-50)
flat-skills = true               # flat skill layout for the nested source (DSC-77)
[[discover.sources.hooks]]       # build hooks for the nested source (HOOK-50)
run = "make build"
```

- `DSC-30` Unknown top-level or table fields are rejected (the file is strict).
- `DSC-31` A `[[items]]` entry with an unknown `kind` is an error (`MindToml`).
- `DSC-109` A declared `workflow` item's path must name a file, not a directory;
  a directory-shaped workflow entry is refused at scan time. The refusal is a
  `MindToml` error naming the item and its path. A workflow is exactly one `.js`
  file (workflows.md WF-1), and that file is also the metadata the scan reads
  (WF-4), so a directory has nothing to read, would install a whole tree at a
  link target the harness tries to load as a script, and would report a
  directory's size in `review`. The author wrote the path, so this is a hard
  error rather than a skipped item, matching DSC-31's treatment of a declared
  entry that cannot be honored.
- `DSC-71` A `[[items]]` `name` must be a single safe path component: it is
  rejected (`MindToml`) when empty, equal to `.` or `..`, or containing a path
  separator (`/` or `\`) or a NUL byte. The name indexes the store
  (`store/<kind>/<name>`) and the per-home link (`<home>/<dir>/<name>`), so this
  stops a melded source from steering either path outside its kind directory.
  (Convention and `[discover]`-glob names are derived from a single filesystem
  component, so they cannot carry a separator and are inherently safe.)
- `DSC-95` An item's bare NAME is source-controlled (a filename or a `[[items]]`
  value) and keys identity, so -- unlike a description (DSC-94) -- it cannot be
  sanitized at the catalog capture point without changing what the item is.
  Instead every human/`--json` print site renders the name through a sanitizing
  display accessor (`display_key`/`display_name`/`display_effective_name`, and
  `InstalledItem::sanitized_for_display` for a whole serialized item), so an
  ANSI/control/bidi code point in a name is stripped before it reaches a
  terminal, exactly as the TUI already sanitizes at its model boundary (TUI-60)
  and `review` sanitizes a finding message (CLI-224); the raw `key()`/`name`
  stays intact for identity, store, link, and manifest matching. Separately,
  every source-controlled string a scan-time error message echoes back (a
  `[[items]]` name/link/path, a `[discover]` glob pattern, a matched path in a
  confinement error, and the annotated snippet a TOML parse error quotes from
  the file it failed to read, root manifest or item manifest alike) is sanitized
  (DSC-94 rule) before it reaches stderr, so a rejection cannot itself carry an
  injection payload. The parse-error snippet is not an edge case: a raw ESC byte
  anywhere in a TOML file is itself a parse error, so quoting the offending line
  is the guaranteed path for that byte, and with DSC-98 it is reachable from
  `recall`/`probe`, verbs the user did not point at any particular source. A
  path is sanitized on the same terms as the text: a clone directory carries
  source-controlled components.

  Caveat: sanitizing only at display means a sanitized name may not round-trip
  as an item ref -- but only for a name captured by a binary predating
  DSC-96 (a legacy manifest entry). `mind recall`/`probe` show the
  display-sanitized `skill:review`, but `mind forget skill:review` matches on
  the raw name, so if that item's raw name actually carries a stripped code
  point (e.g. `revie<ZWSP>w`), the ref a user copies from `recall`'s output
  does not resolve and `forget` reports "not installed". DSC-96 closes this
  for anything installed by a current binary: it refuses a control/bidi/
  zero-width name at capture, so such a name can no longer exist to diverge
  between display and identity, and the ref a user copies round-trips.

  Mechanism: this guarantee is enforced by the type system for the DEFAULT
  formatting path, not by call-site discipline alone. `InstalledItem::key()`,
  `CatalogItem::key()`, and `UnmanagedItem::key()` return an `ItemKey`
  newtype (`sanitize::ItemKey`, `pub(crate)` in a crate with no lib target),
  not a plain `String`. `ItemKey` carries no `Display` impl, so
  `println!("{}", key)` / `format!("{key}")` / `{}` inside a `--json` struct
  via `#[derive(Serialize)]` on a `String` field is a compile error, not a
  lint or a review-time habit a new print site can silently skip. That is the
  only thing withholding `Display` buys: `ItemKey::as_str()` (the raw
  identity reading -- map/set lookups, comparisons, a manifest key, a path
  component) and `impl From<ItemKey> for String` (also raw, e.g. to key a
  `BTreeMap<String, _>`) remain as explicit, named escape hatches, and each
  still needs the same call-site discipline this type otherwise removes from
  every default-formatting print site -- neither is "the" accessor that
  produces a printable string; `ItemKey::display()` is the one accessor that
  sanitizes, and reaching for it instead of `as_str()`/`.into()` at a given
  print site is a choice the compiler does not check. `ItemKey` still derives
  `Debug` (safe: `Debug for str` escapes a control character, so `{:?}`
  cannot forge a terminal escape sequence the way `Display` could) and has no
  `Serialize`/`Deserialize` impl of its own -- every persisted or `--json`
  site goes through `.as_str()` or `Into<String>` first -- so the on-disk
  manifest format (a bare `kind:name` object key) is unchanged by this type.
- `DSC-96` An item's bare NAME (DSC-71's single-path-component rule) is also
  rejected when it contains a control character or a security-blocked Unicode
  code point (a bidi override, a directional mark, or a zero-width character --
  the same class `namespace::is_safe_prefix_component` rejects for a prefix,
  NS-72). Name identity is raw (it keys the store path, the symlink, and the
  manifest, and the cross-source collision check compares it raw, NS-43), while
  DSC-95 sanitizes only at display; without this rule a hostile source could
  ship a name (e.g. a skill directory `revie<ZWSP>w/`, or a declared
  `[[items]] name = "revie<ZWSP>w"`) that renders identically to a trusted
  item's name in `recall`, `probe`, the TUI, and the `learn` confirmation while
  installing as a distinct item, defeating both the visual check a user makes
  before approving an install and the DSC-95/NS-43 collision warning (neither
  of which would fire, since the raw names differ only by an invisible or
  direction-altering code point). Two capture points, two consequences for the
  same rule:
  - A `[[items]]` declared `name` failing this check is a hard `MindToml`
    error (the author wrote the name explicitly, so it is treated like any
    other DSC-71 violation).
  - A convention- or `[discover]`-glob-derived name (an arbitrary filename in
    the source tree, not an author-declared value) failing this check is
    skipped with a stderr warning instead of failing the whole scan, so one
    hostile filename does not make an otherwise-good repo un-meldable; every
    other item in the source is still discovered.
- `DSC-98` An item directory's own `mind.toml` (install-hooks.md HOOK-131) that
  cannot be read as an item manifest -- it does not parse, or it carries a key
  outside `[[hooks]]`, or one of its hooks names an unknown event -- drops THAT
  ITEM from the catalog with a sanitized stderr warning naming the file and the
  one-table rule. It does not fail the scan. The item is dropped rather than
  treated as hook-free, which preserves HOOK-131's guarantee that a malformed
  manifest never degrades to "this item declares no hooks": an item that is
  never offered can run no hook, so this degradation is safe on the verbs that
  execute hooks (`meld`, `learn`, `upgrade`, `hooks run`) as well as on the
  read-only ones. The scan-wide alternative is what makes this necessary: item
  manifests are read on every catalog walk, so a single stray file (a vendored
  sub-source, a skill that ships a sample manifest, a monorepo matched by a
  `[discover].skills` glob) would otherwise take down `recall`, `probe`,
  `learn`, and `upgrade` for EVERY melded source, on verbs where no hook could
  ever run and with no opt-out. Same shape as DSC-96's name skip and CLI-212's
  dead-source skip: the narrowest thing that is broken is what stops working.
  It applies to a `[[items]]`-declared item too, since the manifest it cannot
  read is the item's own, not the one that declared it. A root `[[items]]` entry
  that declares hooks is authoritative (HOOK-132) and the item manifest is never
  read at all, so it cannot be dropped for one.
- `DSC-99` An item whose source has NO effective namespace prefix
  (namespacing.md) but whose bare name contains `:` is warned about, naming the
  item and its source. The name is still installed as written: `:` is
  legal in an item name and commands.md CMD-2/CMD-6 recommend it for a command
  group (`commands/frontend:build.md`), so it cannot be rejected. But a prefix
  component may not contain `:` (NS-72), which is what makes an unprefixed
  source shipping `commands/acme:deploy.md` a forgery risk: it installs
  `acme:deploy`, the exact spelling a source prefixed `acme` would produce for
  its `deploy`, and no later surface can tell the two apart by name alone. The
  warning is the detection the identity cannot provide; the source column in
  `recall`/`probe` and the cross-source collision check (NS-43) are what a user
  reads after it.
- `DSC-101` The DSC-99 warning is emitted by the verbs at which the user decides
  to trust a source -- `meld` (over the offered inventory) and `learn` (over the
  install closure, `--dry-run` included) -- and by `review` as a
  `forged-namespace` advisory. The catalog scan itself only computes the
  predicate and never writes it: `catalog::scan` runs on every read-only surface,
  and the `probe` TUI re-scans on its ~1s poll tick (TUI-15), so a warning
  emitted from the scan repeats forever and is written straight into the
  alternate screen, corrupting the display. Read-only surfaces (`recall`,
  `probe`, `introspect`) therefore emit nothing; the source column they already
  show is what distinguishes the two spellings there.
- `DSC-102` The scan's remaining degraded-state warnings -- a linked source whose
  directory is gone (CLI-212), an item manifest that will not parse (DSC-98), an
  unsafe item name skipped by the convention scan (DSC-96), and a registry entry
  dropped by revalidation on `Registry::load` (STO-68) -- are muted for as long
  as the TUI owns the terminal, and restored when it gives it back. Each reports
  a persistent condition, so on the ~1s poll tick (TUI-15) it is not a one-time
  notice but a line written into the alternate screen every second. Muting is a
  live process-wide toggle the TUI sets, not a property of the output context
  (which resolves once from the global flags): the same process runs the TUI and,
  through it, the verbs. Outside the TUI every one of these still prints.
- `DSC-72` A `[[items]]` `link` override (the link target relative to an agent
  home) must be a safe relative path: it is rejected (`MindToml`) when empty,
  absolute, beginning with `~`, containing a `..` (parent) component, or
  containing a NUL byte. So a melded source cannot place a symlink outside the
  agent home (e.g. `link = "../../.bashrc"`). DSC-97 narrows this further: the
  target must also stay inside a kind directory.
- `DSC-97` A `[[items]]` `link` override must, beyond DSC-72's escape-safety
  rule, have its first path component name one of the kind directories
  (`skills/`, `agents/`, `rules/`, `commands/`, `workflows/`, `tools/`);
  anything else is rejected (`MindToml`) at catalog scan, before install ever
  runs. `commands/` and `workflows/` carry an extra condition: only an item of
  that kind may name the directory, since a file in either is not content the
  harness merely offers, it is the slash command `/<name>` (commands.md CMD-1)
  or a workflow the harness runs (workflows.md WF-8). Without that condition a
  source could declare `kind = "rule", path = "rules/style.md", link =
  "commands/deploy.md"` and have its prose become `/deploy` in a consumer's
  agent home, including for a consumer who filtered their install to rules. Without this, DSC-72
  alone accepts any relative path inside the agent home, including its root:
  a source declaring `link = "settings.json"` (or a harness's own hooks/config
  path) and shipping matching content installs, via an ordinary `learn`, as a
  symlink at that root path. On the Claude harness that turns attacker-chosen
  item content into `~/.claude/settings.json` (or the equivalent inside a
  `--local`/`link-project` project lobe), which the harness reads as its own
  settings on the very next tool use, including a `hooks` key that runs an
  arbitrary command -- with no `mind` hook-consent prompt involved, since this
  path never touches the hooks machinery (hook.rs). `ensure_unoccupied`
  (install.rs) only refuses when the target already exists, so a fresh
  `~/.claude`/project lobe is not protected by that guard, and `--force`
  (LIFE-41) stashing a pre-existing file aside does not help either; the fix
  has to happen before either of those, at catalog validation. Confining the
  first component to a kind directory closes both the fresh-lobe and the
  `--local`/snapshot-lobe (which copies a real file rather than symlinking)
  variants at once, without narrowing TOOL-4's documented use case (a tool
  surfaced under a *different* kind directory than its own, e.g.
  `link = "agents/detect"` on a `kind = "tool"` item): TOOL-4 only ever needed
  a different kind directory, never an arbitrary path, so nothing legitimate
  is lost. There is no consent-based escape hatch (contrast the hook
  disclosure/prompt machinery): the risk (planting a harness's own config) is
  judged severe enough, and the confined form flexible enough (any name,
  subdirectory nesting, and any of the four non-command kind directories, not
  just the item's own kind), that a bypass is not offered. `mind review`
  (CLI-130..133)
  discloses every item's custom `link` target as an advisory finding
  (`custom-link`) alongside the hooks and risky references it already reports,
  so a consumer sees where each item will symlink before melding; a link that
  fails this confinement surfaces instead as a hard `scan-error` finding (the
  meld would already refuse it), not a `custom-link` advisory.
- `DSC-73` A `[[items]]` `path` must be a safe repo-root-relative path: it is
  rejected (`MindToml`) when empty, absolute, beginning with `~`, containing a
  `..` (parent) component, or containing a NUL byte. A relative value free of
  those components may contain `/` for subdirectories (e.g.
  `guidelines/style.md`). This stops a melded source from reading host files
  outside its clone via the copy-to-store step (e.g.
  `path = "../../../../etc/passwd"` or `path = "/home/victim/.ssh/id_rsa"`).
  The same safety rule (`is_safe_link_rel`) that validates `link` (DSC-72)
  applies to `path`.
- `DSC-32` An item's description is its `mind.toml` `description` if given, else
  its frontmatter description.
- `DSC-33` Each `[discover]` kind (`skills`, `agents`, `rules`, `commands`,
  `workflows`, `tools`) is a table with
  `include` and optional `exclude` glob lists, relative to the repo root. A skill
  glob matches a `SKILL.md` (the item is its parent directory); agent, rule,
  command, and workflow globs match the item FILE directly; a tool glob matches
  the tool directory (TOOL-7). Every kind's globs count equally: any non-empty
  `include` list makes the file authoritative (DSC-3) and every pattern is
  confinement-checked at load (DSC-81).
- `DSC-34` `[[items]]` and `[discover]` may both appear; their results are unioned.
- `DSC-35` A source with only `[source]` metadata, or only `[discover].sources`
  (no item globs), still uses convention discovery for its own items.
- `DSC-36` A repo with no `mind.toml` is unaffected by all of the above.
- `DSC-37` Within a kind, `include` globs are matched first, then any path also
  matched by an `exclude` glob is dropped from the result.
- `DSC-38` `[discover].sources` lists other repo specs (each parsed like a `meld`
  argument). Melding a source recursively melds each listed source, skipping any
  already registered, so a `mind.toml` can act as a curated registry /
  super-source. Each curated source is registered independently and tracks its
  own upstream commit. Recursion always terminates, even when a nested source is
  itself a super-source and the chain forms a cycle (`A -> B -> C -> A`): a source
  is registered before its own nested sources are processed, and a source already
  seen is skipped, matched both by URL within the run and by `host/owner/repo`
  identity against the registry (so two spellings of the same repo do not slip
  past). Each source in the transitive set is therefore processed at most once.
- `DSC-39` A `[discover].sources` entry may set `as = "<prefix>"` to impose a
  namespace on that nested source (equivalent to `meld --as`).
- `DSC-100` A `[discover].sources` entry whose `source` is an item link
  (item-link.md LNK-2) may set `kind = "<agent|rule|command>"` to declare what
  the linked file is (equivalent to `meld --kind`, CLI-239). It is the first
  step of the LNK-21 resolution order, so it outranks the containing directory
  and the file's frontmatter. A `kind` on an entry whose `source` is not an item
  link, or one naming a kind the link's shape cannot be (`skill`/`tool` on a
  file link, anything but `skill` on a directory link), is `LinkKindMismatch`
  (LNK-21).
- `DSC-92` A `[discover].sources` entry's `source` spec is parsed exactly like a
  `meld` argument (DSC-38), so it may itself be a local-path spec (`./rel/path`
  or `../rel/path`). When it is, `MindToml::load` rewrites that relative path to
  an absolute one against the DIRECTORY IT READ THE `mind.toml` FROM, not
  against whatever directory the consumer running `meld`/`sync` happens to be
  in. Without this, a curated entry like `source = "../explicit"` (the shape
  `examples/super-source/mind.toml` uses) resolves relative to the consumer's
  cwd - almost never the curator's intended sibling directory - so the same
  super-source melds a different (or no) nested source depending on where the
  consumer happens to be standing. Fixed at the read site (`MindToml::load`
  itself knows the directory it read from) rather than at each use site, so it
  covers both callers that read a curator's entries uniformly: `meld`'s initial
  curated-source walk and `sync`'s DSC-57 re-walk resolve nested local sources
  against the same, correct base with no per-caller change needed. (`dump` is
  not one of them: it never reads a curator's `[discover].sources`, it rebuilds
  the list from the registry, where every local source already carries its
  absolute path, STO-72.) An absolute nested `source` (or a remote spec) is
  unaffected.
- `DSC-93` The absolute entry wins. A `[discover].sources` entry whose DSC-92
  resolution lands inside mind's own managed sources tree
  (`<mind_home>/sources`) at a path that does not exist is skipped with a stderr
  warning and a `SkippedEntry` (`reason = "unresolvable_local_path"`), instead of
  being attempted, failing to clone, and counting toward the DSC-80 curator
  guard. That combination only arises for a relative entry in a CLONED curator:
  DSC-92 resolves it against the directory the `mind.toml` was read from, which
  for a clone is `<mind_home>/sources/<host>/<owner>/<repo>`, so `../nested`
  names a sibling clone dir mind never created. The reference meant something in
  the curator's own working tree and nothing here, and under DSC-80 a curator
  with no items of its own would turn that dead reference into a hard failure of
  the entire meld. It is specifically a `dump` reproduction that suffers: the
  dump carries a correct absolute entry for that same nested source (DUMP-1), so
  the source IS reachable, but the curator's own re-walk reaches the broken
  relative reading first and aborts before the absolute entry is reached. A
  LINKED curator is unaffected (its `mind.toml` is read from the user's working
  tree, so its relative entries resolve there), as is a mistyped relative path in
  any curator whose resolution lands outside the sources tree: that still fails
  as DSC-79/DSC-80 describe. The predicate is structural, not name-based: mind is
  the sole writer of the sources tree and never creates the sibling such an entry
  needs, so a non-existent path there cannot be anything but a misresolved
  relative reference. Identity (`host/owner/repo`) is deliberately not consulted,
  because the two readings of the same nested source resolve to different owners.
- `DSC-58` A `[discover].sources` entry may set `install = true` (default false)
  to recommend that nested source for install: melding the super-source offers its
  items via the same preview-and-prompt as the top-level source (CLI-23, honoring
  `--yes` and skipped under `--link-only`), rather than leaving them only
  registered and available (DSC-54). The flag is per entry, so a curator chooses
  which nested sources install by default and which stay available. It applies on
  a fresh meld and a re-meld, and the whole chain is still traversed so a deeper
  `install = true` is reached even under an unflagged parent. `meld --recursive`
  (DSC-55) is the superset: it installs every nested source regardless of the flag.
- `DSC-62` A `[discover].sources` entry may set `install-items = ["skill:name",
  ...]`, a list of bare `kind:name` refs that scopes install to exactly those of
  the nested source's items, instead of `install = true` (offer all, DSC-58) or
  `install = false` (offer none). When the super-source is melded and the entry is
  reached by the install flow (DSC-54, DSC-55), only the listed items are offered
  via the same preview-and-prompt as the top-level source (CLI-23, honoring
  `--yes`, skipped under `--link-only`); the source's other items are registered
  and left available. `install-items = []` is equivalent to `install = false`.
  When `install-items` is present it governs the entry's install behavior; when it
  is absent the `install` boolean governs as before (DSC-58).
- `DSC-63` Refs in `install-items` (DSC-62) are bare `kind:name` in source truth;
  a prefix in effect for the entry (`as`, DSC-39) is applied at install. A ref
  naming an item the nested source does not offer is an error (`BadReference`) at
  meld, not a silent skip.
- `DSC-64` Setting `install = true` together with a non-empty `install-items` on
  the same entry is a `MindToml` error: offering all and offering a named subset
  are mutually exclusive. The subset form is expressed by `install-items` alone
  (with `install` left unset or false).
- `DSC-59` A `[discover].sources` entry may carry configuration for the
  nested source that the source itself would normally declare in its own
  `mind.toml`: a pin directive (exactly one of `follow-branch = "<branch>"`,
  `pin-tag = "<tag>"`, `pin-ref = "<commit>"`, or `pin-version = "<range>"`,
  per DSC-41 and VER-110), `roots = [...]`
  (convention scan roots, DSC-50), `add-roots = [...]` (additive convention
  scan roots, DSC-84; gated like `roots`, see DSC-88), `flat-skills = true`
  (flat skill layout, DSC-74; see DSC-77), and one or more hooks as a
  `[[discover.sources.hooks]]` array-of-tables (the `[[hooks]]` shape, HOOK-50).
  Declaring more than one pin directive on an entry is a `MindToml` error, the
  same one-of rule a `[source]` section follows (DSC-41). This lets a curator add
  support for a source that has not onboarded itself (no `mind.toml`), including
  one with custom build requirements or a monorepo layout, without forking it,
  and lets a generated super-source pin each source to a reproducible revision
  (DSC-65, DUMP-1).
- `DSC-60` The curator-supplied `roots`, `add-roots` (DSC-88), `flat-skills`, and
  `hooks` (DSC-59) apply only when the nested source ships no `mind.toml` of its
  own. When the nested source has a `mind.toml`, that file is authoritative for
  its roots, add-roots, flat-skills, and hooks and the curator-supplied values
  are ignored (a warning is emitted, since the source has onboarded). The gate is
  whole-file: a nested `mind.toml`, even one that does not declare
  roots/add-roots/flat-skills/hooks, suppresses all four. The curator-supplied
  pin is NOT gated: it is authoritative regardless of the nested `mind.toml`
  (DSC-65). `as` (DSC-39) and `install` (DSC-58) are registry/consumer concerns
  and are likewise unaffected by this gate; they always apply.
- `DSC-61` A curator-supplied entry behaves as if the source had declared the
  same in its own `mind.toml`: when applied (the DSC-60 gate permits, i.e. the
  nested source ships no `mind.toml`), `roots` governs convention discovery
  (DSC-50) and the supplied hooks run under the same disclosure and safety prompt
  as a source's own hooks (HOOK-50..60), including the non-TTY skip and
  `--accept-hooks`. The curator-supplied pin always
  applies (DSC-65): it resolves and is recorded as the source's pin directive
  (DSC-41), so `sync` tracks it. A consumer's explicit `meld` pin flag still
  overrides a curator-supplied pin (DSC-41 precedence).
- `DSC-65` A pin directive on a `[discover].sources` entry (DSC-59) is
  authoritative: it sets the nested source's pin whether or not the source ships
  its own `mind.toml`, overriding the source's own `[source]` pin directive
  (DSC-41). It is exempt from the DSC-60 fallback gate, which governs only
  `roots`, `add-roots`, `flat-skills`, and `hooks`; a nested entry that supplies
  only a pin (no gated roots/add-roots/flat-skills/hooks) does not trigger the
  DSC-60 "ignored" warning. The precedence is: a consumer's direct top-level
  `meld` pin flag wins (DSC-41), then the entry's pin directive, then the
  source's own `[source]` pin directive, then the default branch. `dump` relies on
  this to reproduce each melded source at its recorded commit by emitting a
  per-entry `pin-ref` (DUMP-1, DUMP-4).
- `DSC-88` A `[discover].sources` entry's `add-roots` (DSC-59, mirroring the
  consumer `--add-root` flag, DSC-84) is gated by DSC-60 exactly like `roots`,
  `flat-skills`, and hooks: it applies only when the nested source ships no
  `mind.toml` of its own, and is otherwise ignored with the DSC-60 warning. A
  direct consumer `--add-root` (DSC-84) is never gated, and the asymmetry is
  deliberate: a consumer melding a source directly makes their own
  export-control decision for their own install, while a curator's entry acts on
  a nested source on the consumer's behalf. Ungated, a curator could set
  `add-roots = ["."]` on an entry for a source whose author shipped an
  authoritative `mind.toml` exporting a subset of its items (DSC-3), reaching
  every item the author did not export, and installing all of them when the
  entry is also `install = true` (DSC-58). `dump` emits a nested entry's
  `add-roots` as it emits `roots` (DUMP-11), so re-melding a generated
  super-source applies the same gate.
- `DSC-54` Melding a super-source (one whose `mind.toml` lists `[discover].sources`)
  registers the whole nested chain (DSC-38), but the post-meld auto-install flow
  (CLI-23) runs only over the super-source's OWN items (`<source>#*`) plus any
  nested source the curator marked `install = true` (DSC-58): the remaining nested
  discovered sources are registered and their items are left available, not
  installed. A super-source that ships its own items still offers them for install
  like any source; a purely curated registry installs nothing by default unless it
  flags an entry `install = true`.
- `DSC-55` `meld --recursive` (`-r`) extends the auto-install flow to EVERY nested
  discovered source, beyond the curator's `install = true` defaults: each source in
  the curated chain has its items offered for install via the same
  preview-and-prompt as the top-level source (honoring `--yes`). It applies both on
  a fresh meld and on a re-meld of an already-registered super-source: on a re-meld
  the chain is already registered, so its items are installed without
  re-registering. Without the flag only the top-level source's items and the
  `install = true` entries are offered (DSC-54, DSC-58). `--link-only` (register,
  install nothing) takes precedence: combined with `--recursive` it still installs
  nothing.
- `DSC-56` After a successful `meld` of a source that declares `[discover].sources`,
  `mind` prints a one-time advisory note pointing the user to `mind probe` to
  browse and search what the newly registered sources offer, so a curated registry
  is discoverable right after melding. The note prints after the install step.
- `DSC-57` `sync` re-walks each registered source's `[discover].sources` from its
  refreshed `mind.toml` and melds any newly-listed nested source not already
  registered, register-only (the DSC-54 default, never auto-installing nested
  items) and cycle-safe by the DSC-38 guards, so a curated registry picks up
  sources added upstream without a re-meld. A nested source removed from the list
  is left registered (removal stays an explicit `unmeld`): `sync` only adds.
- `DSC-66` Pin/ref values supplied in a `mind.toml` `[source]` or
  `[[discover.sources]]` pin directive (`follow-branch`, `pin-tag`, `pin-ref`)
  are validated at parse time before any git subprocess is invoked. A value that
  is empty, begins with `-`, contains ASCII whitespace, contains control
  characters, or contains `..` is rejected with `MindError::InvalidRef`. This
  prevents argument injection: a malicious or misconfigured `mind.toml` shipped
  by a melded super-source cannot inject git options (e.g.
  `--upload-pack=touch /tmp/pwned`) into a child `git` process. At the git call
  layer, a `--` end-of-options terminator is inserted before every positional
  ref/branch/tag/sha argument and before the repository URL in `git clone`
  invocations, so that git cannot interpret a value as an option even if
  validation were bypassed. The `prefer_ssh` rewrite applies to both `https://`
  and `http://` remotes so that a plain-HTTP clone URL is not left unrewritten
  when SSH is preferred.
- `DSC-40` When a source's `[source].min-mind-version` is greater than the
  running `mind` version, melding or scanning that source is an error
  (`IncompatibleVersion`) rather than proceeding against a format it predates.
  Versions compare by dotted numeric component (a missing component is 0, so
  `0.2` == `0.2.0`). The `min-mind-version` field value is validated at parse
  time (`MindToml::load`): it must be a non-empty string of one or more
  dot-separated components where every component is non-empty and consists
  solely of ASCII decimal digits (e.g. `"1"`, `"0.7"`, `"2.3.1"`). A value
  with an empty component, a non-digit character, or an empty string is
  rejected with a `MindToml` error naming the field and the bad value (e.g.
  `"0.3-beta"`, `"abc"`, `""`, `"1.x"` are all rejected). The running
  binary's own version string (from the build environment) may carry a
  pre-release segment (e.g. `0.2.0-rc1`); such a segment compares as 0 in
  the version comparison only. A scan of a source whose declared version the
  binary does not meet proceeds with a warning, printed once per source per
  run, that names both versions, says the source may not work as expected, and
  points at `mind evolve`. `sync`, `recall`, and `probe` therefore keep working
  on a source the user chose to meld anyway; whether to meld it at all is
  decided at meld time (DSC-116). `review` still reports it as a hard
  `incompatible-version` finding (CLI-132).
- `DSC-41` `[source]` may declare a pin: exactly one of `follow-branch = "<branch>"`,
  `pin-tag = "<tag>"`, `pin-ref = "<commit>"`, or `pin-version = "<range>"`
  (VER-51). It is read from the source's
  default-branch `mind.toml` and supplies the default pin when the consumer gives
  no `--pin` (or deprecated alias) flag at meld (CLI-17); a
  consumer flag overrides it. Declaring more than one is a `MindToml` error. (See
  CLI-18 for clone behavior and CLI-55 for how `sync` treats each pin kind.)

## Clone depth

A source's clone holds only what installing from it needs: the checked-out
tree, not the repo's history.

- `DSC-118` Every clone `mind` makes of a source is shallow (`--depth 1`). A
  default-branch, `follow-branch`, tag, or version pin clones just that tip. A
  `ref` pin clones the default branch's tip at depth 1 and then fetches the
  pinned commit by itself at depth 1. When the remote refuses a fetch by commit
  sha (a server that does not serve unadvertised commits), `mind` falls back to
  fetching the full history of every branch and tag (a depth-1 clone is
  single-branch, so the default branch alone could miss the commit) and checks
  the commit out from that.
- `DSC-119` A fetch into an existing clone (`sync`, version resolution, the
  release metadata reads of VER-3/VER-41/VER-64) is depth 1 while the clone is
  shallow. A clone that holds its complete history is fetched without
  `--depth`, so no fetch truncates history already there. A `ref`-pinned clone
  that already holds the pinned commit is not fetched at all: a commit sha is
  immutable.
- `DSC-120` `[source] history = "full"` declares that a source needs its
  complete git history (e.g. a hook or tool that reads `git log`). The values
  are `"shallow"` (the default, DSC-118) and `"full"`; any other value fails
  the `mind.toml` parse. After `meld` or `sync` checks out a tree whose root
  `mind.toml` declares `"full"`, a shallow clone is deepened (`git fetch
  --unshallow`) and stays complete from then on (DSC-119), including after
  the declaration is dropped. The throwaway clones `review` and the TUI preview
  make stay shallow regardless.
- `DSC-121` `meld --full-history` is the consumer's equivalent of DSC-120: the
  named source's clone is deepened to its complete history before any of its
  hooks run, and the setting is persisted on the source (`full_history` in
  `sources.json`, omitted when off) so every later `sync`, re-pin, or re-clone
  deepens it again. It applies to the named source only, not to the sources it
  curates. On an already-melded source it records the setting and deepens the
  existing clone, unlike the discovery flags CLI-206 lists. A linked local
  source is the user's own working tree, which `mind` never fetches into, so
  the flag does not apply to it (a note says so).

## Scan roots

By default convention discovery (DSC-10..12) scans the repo root. A monorepo, or
a repo whose agent tooling lives in a subtree, can point the scan at one or more
subdirectories instead.

- `DSC-50` `[source].roots` is an optional list of repo-root-relative directories.
  When set, convention discovery scans for `skills/`, `agents/`, `rules/` under
  *each* listed root rather than at the repo root. Unset means a single implicit
  root of the repo root (the DSC-10..13 behavior, unchanged). An explicitly empty
  list (`roots = []`) is distinct from unset: it scans zero roots and so
  discovers nothing.
- `DSC-51` `meld --root <dir>` (repeatable) overrides `[source].roots` entirely:
  convention discovery scans only the consumer-specified roots, letting a consumer
  narrow a broad source to exactly the subtree they want. The override is persisted
  on the source (STO-17) and applied by later scans and `sync`.
- `DSC-52` Scan roots affect convention discovery only. An authoritative `mind.toml`
  (one declaring `[[items]]` or `[discover]` item globs, DSC-3) keeps its
  repo-root-relative paths and ignores `roots`; if `--root` is passed for such a
  source, `meld` prints a note that it is ignored. A `--root` or `[source].roots`
  path that is not a directory in the clone is an error (`InvalidRoot`).
- `DSC-53` When scanning multiple roots, results are unioned. Two roots that yield
  the same kind and bare name within one source is an error (`DuplicateItem`),
  since an item's identity is `(source, kind, bare_name)` and the collision could
  not be installed unambiguously. The same uniqueness check applies to explicit
  `[[items]]` declarations: two entries with the same kind and name in a
  `mind.toml` are a `DuplicateItem` error.

## Additive roots

`--root` replaces a source's discovery layout (DSC-51) and, on a manifest
source, suppresses the manifest (MKT-15). A consumer who wants the declared
inventory AND more - a marketplace catalog that lists only a subset of the
repo's skills, an authoritative `mind.toml` that exports only some items -
needs roots that compose instead of replace.

- `DSC-84` `meld --add-root <dir>` (repeatable) adds convention scan roots that
  compose with whatever discovery layer is authoritative for the source instead
  of replacing or suppressing it. Each added root is convention-scanned in both
  skill layouts at once - the `skills/` container (DSC-10) and flat bare-name
  directories (DSC-74) - since the `SKILL.md` anchor disambiguates them and a
  `--flat-skills` override would suppress a manifest (MKT-15); agent, rule, and
  tool discovery under the root is the DSC-11..13 convention. The results union
  with the layer's items: a `.claude-plugin/` manifest's items
  (MKT-2/MKT-14, see MKT-17), an authoritative `mind.toml`'s declared items
  (DSC-3), or the ordinary convention scan (where the added roots simply extend
  the effective root set, DSC-50/DSC-51). Unlike `--root`, `--add-root` is not
  an own-item layout override: it does not suppress a manifest (it is not in
  the MKT-15 directive set) and it is not ignored for an authoritative
  `mind.toml` (contrast DSC-52). The roots are persisted on the source (STO-55)
  and applied by later scans and `sync`. Validation matches `--root`: a value
  that is absolute, escapes the clone, or is not a directory in the clone is
  `InvalidRoot`.
- `DSC-85` De-duplication and collisions for added roots: an add-root item
  whose on-disk path is already contributed by the authoritative layer (e.g. a
  marketplace in-repo plugin's skill directory that also sits under an added
  root) is skipped - the authoritative entry wins, keeping its namespace and
  metadata. After the union, the uniqueness rule applies to the source's whole
  offering by (kind, effective name): a distinct-path collision between an
  add-root item and an authoritative-layer item, or between two add-root items
  at DISTINCT on-disk paths (DSC-53), is a `DuplicateItem` error. A same-path
  overlap between two add-root items de-duplicates instead (DSC-87).
- `DSC-86` Add-root items are ordinary convention items: bare names derived
  from the layout, with the layer's effective prefix applied at install. For a
  single-plugin source (`plugin.json`) with no explicit override, that is the
  plugin-name default prefix (MKT-5), so the plugin's items and the added
  roots' items share one namespace. A marketplace's per-entry namespaces
  (MKT-8) do not reach add-root items: there is no single entry to attribute
  an added root to, so they carry only the source's own prefix (alias or
  `[source].namespace`).
- `DSC-87` Two added roots can surface the SAME on-disk item AS THE SAME KIND
  through different scan mechanisms (e.g. `--add-root . --add-root skills`,
  where the `.` root finds `skills/foo` as a Skill via the `skills/` container
  pass and the `skills` root finds the same directory as a Skill via the
  flat-skills pass). This is a same-path overlap, not a name collision: among
  the items an add-root scan itself contributes, only the first occurrence of a
  given (kind, canonical on-disk path) pair is kept and later duplicates are
  silently dropped, before the DSC-85 (kind, effective-name) uniqueness check
  runs. `DuplicateItem` is reserved for a same-name collision between items at
  distinct on-disk paths.
- `DSC-89` The DSC-87 dedup key is `(kind, canonical path)`, not the path alone:
  a single on-disk path can contribute up to one item PER KIND. This matters for
  a directory that qualifies as more than one kind at once -- a `tools/<name>/`
  directory needs no anchor file (DSC-13), so one that also carries a
  `SKILL.md` is both a valid Tool and a valid Skill. With `--add-root .
  --add-root tools`, the `.` root's tools/ pass finds it as a Tool and the
  `tools` root's flat-skills pass finds the same directory as a Skill: two
  DIFFERENT kinds at the same path, so DSC-87 does not apply to this pair and
  both survive (each still subject to the DSC-85 (kind, effective-name)
  uniqueness check independently). Keying DSC-87 on path alone (pre-DSC-89)
  silently dropped whichever kind's scan pass ran second.

## Flat skill layout

By default a skill is a directory under a `skills/` container (DSC-10). A source
whose skill directories sit directly at a scan root, with no `skills/` container,
can opt into flat skill discovery instead of having to spell out a
`[discover].skills` glob.

- `DSC-74` `[source].flat-skills` is an optional boolean (default false). When
  true, convention discovery finds a skill as a bare-name directory containing a
  `SKILL.md` directly under a scan root (`<root>/<name>/SKILL.md`), taking the
  directory name as the bare skill name, rather than requiring the `skills/`
  container (DSC-10). The scan is shallow: only the immediate child directories of
  each root are checked for a direct `SKILL.md`. It composes with `roots` (DSC-50):
  with `roots` unset the single implicit root is the repo root, so flat discovery
  scans `<repo>/*/SKILL.md`; with `roots` set it scans each listed root. Flat
  discovery applies to the skill kind only: the `SKILL.md` anchor disambiguates a
  skill directory from an arbitrary one, whereas agent and rule items are bare
  `.md` files and tool directories carry no required anchor, so a bare directory at
  a root cannot be classified for those kinds. Agent (`agents/`), rule (`rules/`),
  and tool (`tools/`) discovery under each root is unchanged. False or unset is the
  DSC-10 container behavior. Flat discovery changes only the discovered on-disk
  path and the derived bare name; an item's stable identity, store path, and link
  target are unchanged (a flat skill `<root>/foo/` installs and links identically
  to a containered `skills/foo/`), so install, link, uninstall, drift, and the
  `recall`/`probe` kind categorization (the item's `kind` is assigned at scan, not
  inferred from its source path) need no layout-specific handling.
- `DSC-75` `meld --flat-skills` force-enables flat skill discovery for the source:
  it turns the flat layout on even for a source that did not declare
  `[source].flat-skills`. The flag is one-directional (there is no
  `--no-flat-skills`): the effective setting is the consumer override OR the
  source's own `[source].flat-skills` (STO-44, DSC-74), so a consumer cannot
  disable a source's declared flat layout (which would make its skills
  undiscoverable). It is persisted on the source (STO-44) and applied by later
  scans and `sync`. Like `--root` (DSC-51), it is set at the initial meld; a
  re-meld of an already-registered source does not change the persisted value
  (unmeld and meld again to change it). Like `roots`, it affects convention
  discovery only.
- `DSC-76` Flat skill discovery affects convention discovery only. An authoritative
  `mind.toml` (one declaring `[[items]]` or `[discover]` item globs, DSC-3) keeps
  its explicit paths and ignores `flat-skills`; if `--flat-skills` is passed for
  such a source, `meld` prints a note that it is ignored (mirroring the DSC-52
  treatment of `--root`).
- `DSC-77` A `[discover].sources` entry may set `flat-skills = true` to declare
  that a nested source uses the flat skill layout, alongside the other
  curator-supplied configuration a source would normally declare in its own
  `mind.toml` (`roots`, hooks, a pin; DSC-59). Like `roots` and hooks, it is
  applied only when the nested source ships no `mind.toml` of its own; when the
  nested source has a `mind.toml`, that file is authoritative and the
  curator-supplied `flat-skills` is ignored with a warning (the DSC-60 gate). This
  lets a curator adopt a flat-layout source that has not onboarded itself without
  forking it.

- `DSC-78` A `[discover].sources` entry uses the key `namespace` (not `as`) to
  declare the alias to impose on the nested source; `as` is retained as a
  backward-compatible alias for `namespace` but `namespace` is the canonical form.
  `dump` always emits `namespace =` (never `as =`) so round-tripped manifests use
  the canonical key. `effective_alias()` returns the `namespace` value when present,
  falling back to the legacy `as` value, so both keys work at meld and sync time.

## Authentication failure handling for nested sources

A curator may list sources that require authentication (private GitHub repos,
self-hosted forges). By default a git auth failure during meld or sync is a hard
error indistinguishable from any other network error. The `on-auth-failure` entry
field lets the curator opt in to named handling.

- `DSC-67` *(removed: `private = true` flag was dropped before implementation in
  favor of the inline-table form in DSC-68)*

- `DSC-68` A `[discover].sources` entry may set `on-auth-failure` as an inline
  table with a required `action` key and an optional `message` key.
  `action` must be `"error"` or `"skip"`. `message`, when present, is a plain
  string shown to the user alongside the standard auth-failure line (DSC-69).
  Without `on-auth-failure`, an auth failure is a generic git error (hard
  error, non-zero exit). Authentication
  failure is detected by matching the following credential-denial patterns in
  the git subprocess stderr (case-insensitive): `authentication failed`,
  `permission denied (publickey)`, `could not read username`,
  `could not read password`,
  `the requested url returned error: 401`,
  `the requested url returned error: 403`, `invalid username or password`,
  `invalid credentials`, `http basic: access denied`,
  `fatal: unable to authenticate`. Detection depends on English-language git
  stderr; under a non-English locale (`LANG`/`LC_ALL` not `en`), patterns may
  not match and the entry degrades to the generic hard-error path. Patterns
  that conflate access denial with a missing repository (e.g. `repository not
  found`) are intentionally excluded. The same handling applies during `sync`,
  which re-walks `[discover].sources` through the same path (DSC-57); a
  skipped nested source is warned and left unregistered, and
  `action = "error"` fails the sync. Setting `action = "error"` vs. omitting
  `on-auth-failure` entirely: both exit non-zero on auth failure, but
  `"error"` uses the standardized DSC-69 message format and supports the
  optional `message` field; omitting `on-auth-failure` produces a raw generic
  git error with no custom message. With it, the action governs: `"error"`
  still exits non-zero; `"skip"` emits a warning and continues. The source is
  not registered and any transitive chain reachable only through it is also
  skipped.

  ```toml
  [[discover.sources]]
  source = "owner/private-repo"
  on-auth-failure = { action = "skip", message = "Configure credentials: https://example.com/auth" }
  ```

- `DSC-69` When `on-auth-failure` is set and auth fails, `mind` always prints
  `"unable to meld source <source> due to authentication failure"` (where
  `<source>` is the parsed short name, the `name` field derived from
  `parse_spec`, not the full URL or spec string the curator wrote), with
  `" (skipping)"` appended when `action` is `"skip"`. If `message` is set, it is
  printed on the line immediately following. Both the source name and the
  curator `message` have ANSI escape sequences and non-printable control
  characters stripped before display, preventing terminal injection from
  curator-controlled content. Under `"error"`, the message (if any)
  is printed before the process exits non-zero. Under `--json`, skipped entries
  appear as objects in a `"skipped"` array on the outer mutation result, each
  object carrying `"source"` and `"reason": "auth_failure"`. No separate JSON
  object is emitted per skipped source; the outer result is one JSON object
  total, consistent with CLI-153.

- `DSC-70` `on-auth-failure` governs only the direct clone failure of the entry
  itself; auth failures from transitive descendants (sources nested within the
  entry's own `mind.toml`) propagate as hard errors regardless of the entry's
  policy. The implementation detects descendant failures by checking whether the
  entry's source is already present in the registry at the point the auth failure
  arrives: a registered entry cloned successfully, so the failure originates from
  a deeper level. The same scoping applies during `sync` re-walk.

- `DSC-79` — A `[discover].sources` entry whose clone fails for a non-auth reason (network
  error, not-found, etc.) is skipped with a stderr warning rather than hard-failing the meld;
  the primary source and every successfully-cloned nested source remain registered. The skip is
  surfaced in the `SkippedEntry` list (`--json` `skipped[]`) with `reason = "clone_failure"`.
  Scoping follows DSC-70: a failure arriving after the entry is already in the registry
  originates from a descendant and propagates unchanged. The same skip applies during `sync`
  re-walk.
- `DSC-80` — When the primary source is exclusively a curator (a catalog scan of its own
  directory yields zero items) and every nested `[discover].sources` entry fails to register,
  the meld hard-fails: registering a source with zero discoverable items is not useful. A
  primary source with at least one of its own items, or with at least one nested source that
  registered, succeeds (exit 0). The primary/top-level source's own clone failure is a hard
  error regardless (unchanged).

## Glob confinement and name safety

- `DSC-81` A `[discover]` glob pattern must be a relative path: a pattern that
  is absolute or contains a `..` path component is rejected at `mind.toml` parse
  time with a `MindToml` error. Every filesystem path produced by expanding a
  `[discover]` glob is canonicalized and checked to lie within the clone root; a
  match outside the root is an error (mirrors the DSC-73 treatment of explicit
  `[[items]]` paths). Glob-discovered item names are validated with the same
  safe-component rule as `[[items]]` names (DSC-71): an empty, `.`, `..`, or
  separator-bearing name is rejected with a `MindToml` error.

- `DSC-83` For skill globs in `[discover]`, the bare skill name is the parent
  directory name (not the `SKILL.md` file stem). The safe-component check
  (DSC-81) is applied to the parent directory name so that a skill directory
  whose name contains a path separator or NUL is rejected at discovery time,
  consistent with how `[[items]]` names are validated (DSC-71).

## Metadata size cap

- `DSC-91` Every read of a source-controlled **metadata** file is size-capped at
  a ceiling that defaults to 8 MiB (`METADATA_SIZE_LIMIT` in `src/error.rs`) and
  is configurable per invocation (DSC-103),
  shared by every metadata reader through one helper
  (`error::read_capped_metadata`) so the limit and the refusal are defined
  exactly once. "Metadata" here means the hand-authored files a maintainer
  edits directly to describe a source or an item, not the item's own content:
  `mind.toml` (`mindfile::MindToml::load`), an item's frontmatter block read
  from `SKILL.md`/`TOOL.md`/an agent or rule `.md` file
  (`frontmatter::description_capped`/`file_field_capped`, used by
  `catalog::build_item` for every field it derives from an item's meta file:
  description, `install`/`uninstall`, `bin`/`build`, `requires`), and a Claude
  plugin manifest (`.claude-plugin/plugin.json` /
  `.claude-plugin/marketplace.json`, `plugin_manifest::load_plugin_manifest` /
  `load_marketplace_manifest`).

  The read is refused, with `MindError::MetadataTooLarge` naming the file and
  the limit, before the file is read in full: the underlying helper reads at
  most `METADATA_SIZE_LIMIT + 1` bytes via `Read::take`, so an oversized file's
  cost to `mind` is bounded by the cap rather than by the file's actual size. A
  file at or below the cap is read and behaves exactly as before; the cap does
  not otherwise change parsing or validation. An absent file is unaffected (a
  metadata reader that already treats "file not found" as "absent", e.g. an
  optional `mind.toml` or an optional `TOOL.md`, keeps that behavior; only the
  new size-cap failure is a hard error).

  8 MiB is chosen because these are hand-authored text files: the largest
  `mind.toml`/`SKILL.md` in this repo's own examples is a few KB, and any
  legitimate metadata file is expected to stay well under 100 KB. 8 MiB sits
  orders of magnitude above that while still bounding how much memory a single
  melded source can force `mind` to allocate while scanning or installing it:
  narrow enough to matter, wide enough that no real-world metadata file is
  expected to ever approach it.

## Configuring the metadata cap

- `DSC-103` The DSC-91 cap is a default, not a fixed property of the format. An
  operator sets the ceiling for an invocation with the global
  `--max-metadata-size <SIZE>` flag (CLI-240), for an environment with
  `MIND_MAX_METADATA_SIZE`, or persistently with the `max-metadata-size` key in
  `~/.mind/config.toml`. The resolved value is installed once, before any verb
  dispatches, and every metadata read in that run uses it: the knob is the same
  one helper DSC-91 names, so no reader can be capped differently from another.
  Absent all three, the default applies and behavior is exactly as before.

  Raising it exists for a source with a legitimately large metadata file, which
  DSC-91 does not deny is possible (a generated `mind.toml`, a marketplace
  catalog of hundreds of plugins, a workflow whose whole `.js` body is its
  metadata, WF-55). Lowering it is equally supported, and is the reason the knob
  is not simply an `--allow-large-metadata` boolean: an operator melding sources
  they do not trust can bound the allocation further than the default does.

- `DSC-104` Precedence, highest first: `--max-metadata-size`, then
  `MIND_MAX_METADATA_SIZE`, then the config key, then the DSC-91 default. Only
  the highest-precedence value present is parsed; a lower one is not consulted,
  and never silently repairs a higher one that is invalid (DSC-105). An empty or
  whitespace-only `MIND_MAX_METADATA_SIZE` reads as unset rather than as an
  invalid value, matching the shell convention that `VAR=` clears a variable.

  The config file is read only when neither the flag nor the environment
  supplied a value, and a `config.toml` that will not parse falls back to the
  default cap rather than failing the run: commands that never read the config
  (`evolve`, `hooks`, `completions`, `man`) must not start failing on a
  malformed one, and the commands that do read it report the parse error
  themselves, in their own context. The exception is a malformed file that
  names the `max-metadata-size` key: config would have been the cap's origin,
  so the parse error is reported rather than the default applied silently.

- `DSC-105` An accepted size is a whole number of bytes (`16777216`), a
  binary-suffixed size (`32MiB`, `512KiB`, `2GiB`, and the bare `32M`/`512K`/
  `2G` spellings, which are binary), a decimal-suffixed size (`16MB` =
  16000000), an explicit `512B`, or one of `unlimited`, `none`, or a zero value
  for no ceiling (DSC-110). Case and internal whitespace are not significant. A
  fraction (`1.5MiB`) is refused rather than rounded, so a value that cannot be
  represented exactly is never silently changed to a different one. The refusal
  says the value is fractional and names a whole-unit equivalent, rather than
  reporting the text after the decimal point as an unknown unit.

  A value that is present but unparseable is a hard `MindError::BadMetadataSize`
  before any other work, naming which of the three origins supplied it. It is
  never a fallback to the default: an operator who typed a cap and silently got
  the default instead would have no way to tell.

- `DSC-110` Every zero-valued spelling of the cap means "unlimited", whatever
  unit it carries: `0`, `00`, `0B`, `0KiB`, `0MiB`, `0GB`, and the same with
  internal whitespace or different case all resolve to no ceiling, identically
  to `unlimited` and `none`. A cap of literally zero bytes would refuse every
  metadata file, including the source's own `mind.toml`, so it is not a value
  an operator can mean.

  The rule lives in the one size parser, so every origin (the flag,
  `MIND_MAX_METADATA_SIZE`, the config key) and every caller of it agree on
  what a zero means. It is not a rescue applied by one caller after the fact:
  that split gave a direct caller "refuse everything" for `0B` while the CLI
  read the same text as "unlimited". The accepted-spellings list in
  `--max-metadata-size --help` and in the configuration docs names this same
  set.

- `DSC-111` `MIND_MAX_METADATA_SIZE` is distinguished from unset by presence,
  not by decodability. A variable that is set but is not valid UTF-8 is a
  present-and-unparseable value: it is a hard `MindError::BadMetadataSize`
  naming `MIND_MAX_METADATA_SIZE` as the origin, per DSC-105, not a silent
  fall-through to the config key or the default. Only an absent variable, or
  one whose value is empty or whitespace-only (DSC-104), reads as unset.

  The refusal is subject to precedence (DSC-104), not ahead of it: it applies
  when the environment is the origin the run would have used. An explicit
  `--max-metadata-size` outranks the variable, and the undecodable value is then
  dropped without being examined, exactly as a junk-but-decodable one is (only
  the winning origin is ever parsed). Otherwise the flag would override the
  variable or not depending on which bytes it happened to hold.

  Under `--json` the refusal is the CLI-181 error envelope on stdout
  (`"kind": "bad-metadata-size"`), like any other error: it is raised before
  dispatch, which is where a bare text line on stdout would otherwise break
  CLI-217's one-document contract.

- `DSC-106` The cap in effect appears in `MindError::MetadataTooLarge` (so the
  refusal names the ceiling the operator can change, and its remedy names the
  flag) and in `config show` (CLI-241). Both report the *effective* value, not
  the config key, since the flag and the environment outrank it.

- `DSC-107` `unlimited` removes the bound, which re-enters the DSC-90 accepted
  risk for metadata files: a source can then make `mind` allocate as much as its
  largest metadata file. That is the operator's decision to make for a source
  they chose to meld, and it is the reason the default stays 8 MiB rather than
  being raised to accommodate the rare large file. The bounded-read guarantee
  itself is unchanged at every other setting: the reader still takes at most
  cap-plus-one bytes, so the cost of an oversized file is bounded by the cap and
  not by the file.

## Accepted risks

- `DSC-90` Beyond the metadata cap (DSC-91), no other read of source-controlled
  content is size-capped, and TOML nesting depth is not bounded. This covers
  every read of an item's own *content* (as opposed to the metadata describing
  it): every file in an item tree during `{{ns:}}` expansion at install; the
  unguarded-reference scan; `review`; the TUI preview; and content hashing. A
  source author could ship a multi-gigabyte file inside a skill's directory, or
  a deeply nested `mind.toml` table structure, to make `mind` allocate heavily
  while installing or reviewing that source.

  This is a self-inflicted denial of service: the operator chose to meld that
  source, no privilege boundary is crossed, and the exposure is a crash of the
  current invocation only, not a persistent or remote compromise. The cost of a
  fixed cap on arbitrary item content, a false ceiling that could someday
  reject a legitimately large file inside a skill, is judged higher than the
  benefit at the current adoption scale. The decision is to accept the residual
  risk and revisit if melding untrusted third-party sources at scale becomes
  common.

  Normative content: `mind` does not cap these reads, and does not bound TOML
  nesting depth. A change that adds a cap to any of them is a behavior change
  and must update this entry (and, if it narrows the scope further, split out
  another `DSC-9x` the way DSC-91 was split out of this one). Recorded with an
  ID, indexed from the feature table, and referenced from `SECURITY.md`,
  because the prior unlabeled version of this note was missed by a reviewer
  looking for exactly it.

## `[source].namespace`

- `DSC-82` The `[source]` table accepts both `namespace` and `prefix` for the
  namespace prefix field. Both keys name the same field; `namespace` is the
  canonical key (written by `init-source` per INIT-11, matching the nested-entry
  key per DSC-78 and the `--namespace` flag per CLI-159) and `prefix` stays
  accepted indefinitely as a deprecated parse alias. When a `mind.toml` carries
  `prefix =` and is rewritten by `init-source`, the `prefix =` line is replaced
  with `namespace =` so only one key is present after the update.

- `DSC-112` The reserved-prefix list (namespacing.md NS-25, NS-29) is
  append-only and grows when a word becomes an item kind, so a source can be
  melded under a prefix that is legal at meld time and reserved afterwards
  (`workflow`, once the workflow kind shipped). What the melded source then
  meets depends on which of the two ingresses set the prefix:

  - A consumer-set prefix (`meld --namespace`, recorded as the registry entry's
    alias) is never re-validated, so it stays in effect. The scan warns once
    per source that the prefix is now a reserved word and that `<prefix>:<name>`
    reads as a kind-qualified ref, and names re-melding with `--namespace
    <other>` as the fix. It is advisory: the items are already installed under
    that prefix, and failing every scanning verb over a naming problem would
    take the whole lobe down.
  - A source-declared `[source].prefix` is validated at every `mind.toml` load
    (NS-25), so the refusal is a hard error on any verb that scans the source
    (`recall`, `probe`, `learn`, `upgrade`, `introspect`). For a source that is
    already melded, that error names the source and `mind unmeld <source>` as
    the remedy, rather than the pre-meld wording that the prefix "cannot be
    used": the value is the source's own declaration, so no consumer-side flag
    overrides it and the only action available is to drop the source.

- `DSC-113` `completions` and `man` read no source metadata, so they skip
  metadata-cap resolution entirely (DSC-104). A malformed `--max-metadata-size`,
  `MIND_MAX_METADATA_SIZE`, or `max-metadata-size` config value therefore never
  stops a shell from loading its completion script or a user from reading the
  manual. Every other verb resolves the cap and refuses an undecodable value.

- `DSC-114` A `[discover].workflows` glob matches only a regular, non-symlink
  `.js` file, as the convention scan and a declared item (DSC-108, DSC-109)
  do. A match that is a directory, a non-`.js` file, or a symlink is dropped
  silently, not offered; a symlink resolving outside the clone stays the
  DSC-81 hard error.

- `DSC-115` A skill or tool anchor is classified no-follow wherever it is
  discovered. An item link (item-link.md) to a skill directory requires the
  directory and its `SKILL.md` to be a real directory and a regular file, not
  links. A tool's `TOOL.md` is optional, but when present (convention scan or
  `[discover].tools` glob) it must be a regular non-symlink file; a tool whose
  `TOOL.md` is a symlink or other non-file is not offered.

- `DSC-116` Before registering a source whose `[source].min-mind-version` the
  running binary does not meet (DSC-40), `meld` prints the DSC-40 warning and
  asks `continue melding <source> anyway? [y/N]`. Yes melds it as usual; no
  (the default) removes any clone and fails with `IncompatibleVersion`, which
  names both versions, `mind evolve`, and `--yes`. `meld --yes` skips the
  question and melds with the warning, and the answer carries to the sources
  that meld walks (`[discover].sources`, a marketplace's entries). With no
  terminal to ask on, or under `--json` (LIFE-45), and no `--yes`, the meld
  fails as if declined, and so do
  the melds that `curate`, `sync`, and a policy auto-meld run on their own.
  The check reads the `mind.toml` at the resolved pin.

- `DSC-117` When `mind.toml` fails to parse and a lenient read finds a
  well-formed `[source].min-mind-version` the running binary does not meet,
  the error is `MindTomlTooNew`: it names the file, both versions, and the
  parse error, and points at `mind evolve`, since the likely cause is a key
  added after this release. A parse failure in a file whose declared version
  is met, or that declares none, stays the plain `Toml` error.