hmgr 0.0.51 → 0.0.53

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.
@@ -51,6 +51,9 @@ export class Agents {
51
51
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
52
52
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
53
53
  }
54
+ quellOrdner() {
55
+ return `${AGENTS_VERZEICHNIS}/${SKILLS_ORDNER}`;
56
+ }
54
57
  folgtLinks() {
55
58
  return true;
56
59
  }
@@ -681,6 +681,9 @@ export class ClaudeCode {
681
681
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
682
682
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
683
683
  }
684
+ quellOrdner() {
685
+ return `${CLAUDE_VERZEICHNIS}/${SKILLS_ORDNER}`;
686
+ }
684
687
  folgtLinks() {
685
688
  return true;
686
689
  }
@@ -289,6 +289,9 @@ export class Hermes {
289
289
  await mkdir(hermesHome, { recursive: true });
290
290
  await writeFile(pfad, doc.toString());
291
291
  }
292
+ quellOrdner() {
293
+ return `${HERMES_VERZEICHNIS}/${EIGENER_ORDNER}`;
294
+ }
292
295
  folgtLinks() {
293
296
  return true;
294
297
  }
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.53": [
5
+ "MCP-Server stehen an der Ordnerkarte jetzt in derselben Tabelle wie die Skills, mit dem Zeichen des Harness davor. Ein Tooltip erklärt, warum ein Server für Hermes fehlt: Hermes liest MCP-Server nur im Home oder in einem Profil, nicht pro Ordner. `hmgr mcp list` sagt das am Ende ebenfalls."
6
+ ],
7
+ "0.0.52": [
8
+ "Liegt ein Skill in einem Repo in mehreren Fassungen, je Werkzeug eine (etwa `.claude/skills` und `.hermes/skills`), verlinkt hmgr jetzt für jeden Harness die passende Fassung. Gibt es keine passende, nennt die Meldung die Pfade, und `hmgr link … --pfad` wählt einen aus."
9
+ ],
4
10
  "0.0.51": [
5
11
  "Ein gelöschter Projektordner, an dem noch ein Bundle hing, wird beim Aktualisieren nicht mehr neu angelegt. `hmgr doctor` findet solche Ordner jetzt und bietet an, sie samt ihren Bundles zu vergessen."
6
12
  ],
package/dist/cli/hilfe.js CHANGED
@@ -147,6 +147,7 @@ Schalter:
147
147
  --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
148
148
  kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
149
149
  --quelle <teil> Teilstring der Quelle, wenn der Skill-Name in mehreren Quellen vorkommt
150
+ --pfad <skill-pfad> Skill-Ordner in der Quelle, wenn der Name dort mehrfach vorkommt (E121)
150
151
  --version <spanne> Versionseinschränkung nach Composer-Regeln (etwa ^1.2) oder ein Tag- oder
151
152
  Branchname. Nur mit Zeiger, also dir:<pfad> ohne --local (E33)
152
153
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
@@ -155,11 +156,15 @@ Schalter:
155
156
 
156
157
  Beispiele:
157
158
  hmgr link netzwerk
159
+ hmgr link impeccable --pfad plugin/skills/impeccable
158
160
  hmgr link netzwerk --harness hermes --ort profil:researcher
159
161
  hmgr link tq-schema --ort dir:. --version ^2.0
160
162
 
161
163
  Häufige Fälle:
162
164
  „liegt in mehreren Quellen": mit --quelle <teilstring der URL oder des Pfads> entscheiden.
165
+ „liegt in der Quelle … mehrfach": mit --pfad einen der genannten Pfade wählen. Liegt genau
166
+ eine Fassung im Skill-Ordner des Harness (claude-code .claude/skills, hermes .hermes/skills,
167
+ agents .agents/skills), nimmt hmgr sie von selbst und fragt nicht nach.
163
168
  „liegt bereits ein fremder Eintrag": --ersetzen setzen oder vorher hmgr adopt <name>.
164
169
  „gibt es keinen Zeiger": --version braucht ein Arbeitsverzeichnis mit composer.json und kein
165
170
  --local. Ohne Zeiger den Link ohne --version legen.
@@ -419,6 +424,10 @@ global (Scope user). Hermes kennt nur global, mcp_servers in seiner config.yaml
419
424
  mit --ort profil:<name>); ein Projekt-Ziel ist bei Hermes ein Fehler, es kennt keine Projektebene für
420
425
  MCPs. agents kennt MCP-Server gar nicht.
421
426
 
427
+ Hängt ein Eintrag an einem Arbeitsverzeichnis und ist ein aktiver Harness ohne Projektebene (Hermes) da,
428
+ endet mcp list mit einer Zeile, die sagt, dass dieser Harness den Eintrag dort nicht liest, und auf
429
+ --ort home oder --ort profil:<name> verweist.
430
+
422
431
  Bei claude-code führt der Scope local die Server je Git-Repo, nicht je Unterordner (E91). Ein
423
432
  Arbeitsverzeichnis, das in einem Repo liegt, ohne dessen Wurzel zu sein, bekommt deshalb kein mcp add,
424
433
  mcp remove und mcp migrate: die Meldung nennt den Wurzelordner, dort gehören die Server hin (ist er
package/dist/cli/main.js CHANGED
@@ -121,7 +121,7 @@ const SCHALTER = {
121
121
  source: { werte: ['name'], flaggen: [] },
122
122
  dir: { werte: ['composer'], flaggen: [] },
123
123
  link: {
124
- werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
124
+ werte: ['harness', 'ort', 'composer', 'quelle', 'version', 'pfad'],
125
125
  flaggen: ['local', 'lokal', 'ersetzen'],
126
126
  },
127
127
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
@@ -507,10 +507,12 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
507
507
  }
508
508
  const quelle = werte.get('quelle');
509
509
  const version = werte.get('version');
510
+ const pfad = werte.get('pfad');
510
511
  const link = await anwendungsfaelle.linkLegen({
511
512
  skill,
512
513
  ziel,
513
514
  ...(quelle === undefined ? {} : { quelle }),
515
+ ...(pfad === undefined ? {} : { pfad }),
514
516
  ...(version === undefined ? {} : { version }),
515
517
  ersetzen: flaggen.has('ersetzen'),
516
518
  lokal,
@@ -739,6 +741,28 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
739
741
  for (const f of [...uebersicht.unbekannteGeteilteFundorte].sort((a, b) => nachName(a.name, b.name)))
740
742
  console.log(` ${f.name} ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope}`);
741
743
  }
744
+ // E120: hängt etwas an einem Arbeitsverzeichnis, erklärt eine Zeile, warum ein aktiver Harness
745
+ // ohne Ordner-MCP (Hermes) es dort nicht hat. Abgeleitet aus den Ebenen der Fähigkeit (E16).
746
+ const anOrdner = [
747
+ ...uebersicht.katalog.flatMap((k) => [...k.eingehaengt, ...k.geteilteFundorte.map((g) => g.ziel)]),
748
+ ...uebersicht.unbekannt.map((u) => u.ziel),
749
+ ...uebersicht.unbekannteGeteilteFundorte.map((u) => u.ziel),
750
+ ].some((z) => z.ort.art === 'arbeitsverzeichnis');
751
+ if (anOrdner) {
752
+ const aktive = await anwendungsfaelle.aktiveHarnesses();
753
+ const anzeigeNamen = { 'claude-code': 'Claude Code', hermes: 'Hermes', agents: 'Agents' };
754
+ for (const { harness, ebenen } of anwendungsfaelle.mcpEbenen()) {
755
+ if (!aktive.includes(harness) || ebenen.includes('arbeitsverzeichnis'))
756
+ continue;
757
+ const home = ebenen.includes('home');
758
+ const profil = ebenen.includes('profil');
759
+ if (!home && !profil)
760
+ continue;
761
+ const wo = home && profil ? 'im Home oder in einem Profil' : home ? 'im Home' : 'in einem Profil';
762
+ const orte = [...(home ? ['--ort home'] : []), ...(profil ? ['--ort profil:<name>'] : [])].join(' oder ');
763
+ console.log(`${anzeigeNamen[harness] ?? harness} liest MCP-Server nicht pro Ordner, nur ${wo} (${orte}).`);
764
+ }
765
+ }
742
766
  return 0;
743
767
  }
744
768
  if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'migrate')
@@ -1320,7 +1344,9 @@ function istModellFehler(fehler) {
1320
1344
  */
1321
1345
  function naechsterSchritt(fehler) {
1322
1346
  if (fehler instanceof SkillMehrdeutig)
1323
- return `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
1347
+ return fehler.nurEineQuelle
1348
+ ? `Fassung wählen: hmgr link ${fehler.name} --pfad <einer der genannten Pfade>`
1349
+ : `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
1324
1350
  if (fehler instanceof SkillUnbekannt)
1325
1351
  return 'Erst die Quelle hinzufügen: hmgr source add <url|pfad>. hmgr source list zeigt die bekannten Quellen.';
1326
1352
  if (fehler instanceof NameBelegt)
package/dist/gui/main.js CHANGED
@@ -228,6 +228,7 @@ function registriereIpc() {
228
228
  ...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
229
229
  ...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
230
230
  ...(skill.geaendert === undefined ? {} : { geaendert: skill.geaendert }),
231
+ ...(skill.jeHarness === undefined ? {} : { jeHarness: skill.jeHarness }),
231
232
  quelleText: quelleAlsText(eintrag.quelle),
232
233
  quelleName: quelleName(eintrag.quelle),
233
234
  quelleSchluessel: quelleSchluessel(eintrag.quelle),
@@ -463,6 +464,7 @@ function registriereIpc() {
463
464
  }));
464
465
  // Plätze für den Einhängen-Dialog (F42): die Fähigkeit hängt am Adapter, nicht am Namen (E16),
465
466
  // deshalb kommt die Liste unverändert aus dem Kern statt aus einer Harness-Fallunterscheidung hier.
467
+ ipcMain.handle('hmgr:mcpEbenen', async () => auffangen(async () => anwendungsfaelle.mcpEbenen()));
466
468
  ipcMain.handle('hmgr:mcpPlaetze', async () => auffangen(async () => anwendungsfaelle.mcpPlaetze()));
467
469
  ipcMain.handle('hmgr:mcpEinhaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpEinhaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
468
470
  ipcMain.handle('hmgr:mcpAushaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpAushaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
@@ -886,6 +888,8 @@ function fensterOeffnen() {
886
888
  var dskBundleBoxSichtbar = false;
887
889
  var dskBundleText = null;
888
890
  var dskBundleZuVorher = null, dskBundleKoerperText = null;
891
+ // E121: ein Name mit mehreren Fassungen in der Quelle steht einmal, mit Vermerk statt Pfad.
892
+ var dskMehrfachAnzahl = null, dskMehrfachText = null;
889
893
  // Aufklappbereich je Zeile (E92): zu vor dem Klick, danach sichtbar mit der ungekürzten
890
894
  // Beschreibung; die Checkbox bleibt unberührt, der Dialog offen.
891
895
  var dskKoerperVorher = null;
@@ -920,6 +924,10 @@ function fensterOeffnen() {
920
924
  dskAufklapp?.click();
921
925
  dskKoerper?.querySelector('p')?.click();
922
926
  dskAufklappFreiAngehakt = dskEingabe?.checked ?? null;
927
+ var dskMehrfach = Array.from(document.querySelectorAll('#dsk-liste .wahlzeile'))
928
+ .filter((z) => z.querySelector('b')?.textContent === 'zmehrfach');
929
+ dskMehrfachAnzahl = dskMehrfach.length;
930
+ dskMehrfachText = dskMehrfach[0]?.querySelector('.mono')?.textContent ?? null;
923
931
  dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
924
932
  dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
925
933
  ?.replace(/\\s+/g, ' ').trim() ?? null;
@@ -990,7 +998,7 @@ function fensterOeffnen() {
990
998
  streifenHarnesse = streifen[0]?.querySelector('.zeilenzeichen')?.getAttribute('aria-label') ?? null;
991
999
  streifenText = streifen[0]?.querySelector('.bundlekopf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
992
1000
  alleGruppeZeilen = Array.from(streifen[0]?.querySelectorAll('.linkliste li .name') ?? []).map(nurName).join(',');
993
- alleTabelleZeilen = Array.from(streifenKarte?.querySelectorAll('.linkliste li') ?? [])
1001
+ alleTabelleZeilen = Array.from(streifenKarte?.querySelectorAll('.linkliste:not(.mcpzeilen) li') ?? [])
994
1002
  .filter((li) => li.closest('.bundlegruppe') === null && li.querySelector('.name'))
995
1003
  .map((li) => nurName(li.querySelector('.name'))).join(',');
996
1004
  Array.from(streifen[0]?.querySelectorAll('.bundlekopf button') ?? [])
@@ -1067,7 +1075,7 @@ function fensterOeffnen() {
1067
1075
  // Seit E64 stehen im MCP-Abschnitt zwei Knopfarten nebeneinander (Entfernen und
1068
1076
  // Übernehmen), deshalb wird hier nicht mehr der erste Knopf genommen, sondern der
1069
1077
  // Entfernen-Knopf an seinem aria-label erkannt.
1070
- var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpkartezeilen button'));
1078
+ var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpzeilen button'));
1071
1079
  try {
1072
1080
  var mcpAusKnopf = mcpZeilenKnoepfe.find(function (b) {
1073
1081
  return (b.getAttribute('aria-label') || '').indexOf('aushängen') > -1;
@@ -1119,13 +1127,20 @@ function fensterOeffnen() {
1119
1127
  var hermesKarte = karteMitKopftext('Hermes · Home');
1120
1128
  var hermesMcpText = hermesKarte?.querySelector('.mcpkarteabschnitt')?.textContent
1121
1129
  ?.replace(/\\s+/g, ' ').trim() ?? null;
1122
- var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
1130
+ var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpzeilen button').length ?? 0;
1123
1131
  var hermesMcpKnopfVorhanden = mcpHinzuKnopfAn(hermesKarte) !== undefined;
1124
1132
  var kundeXKarte = karteMitKopftext('kunde-x');
1125
1133
  var kundeXMcpText = kundeXKarte?.querySelector('.mcpkarteabschnitt')?.textContent
1126
1134
  ?.replace(/\\s+/g, ' ').trim() ?? null;
1127
- var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
1135
+ var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpzeilen button').length ?? 0;
1128
1136
  var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
1137
+ // E120: Tabellenkopf des MCP-Abschnitts und die Zeile mail (Katalog, unter „Alle").
1138
+ var kundeXMcpKopf = Array.from(kundeXKarte?.querySelectorAll('.mcpkarteabschnitt .linkkopf .etikett') ?? [])
1139
+ .map((x) => x.textContent).join('|');
1140
+ var kundeXMailZeile = Array.from(kundeXKarte?.querySelectorAll('.mcpzeilen li') ?? [])
1141
+ .find((li) => (li.querySelector('.name')?.textContent || '') === 'mail');
1142
+ var kundeXMailQuelle = kundeXMailZeile?.querySelector('.quelle')?.textContent ?? null;
1143
+ var kundeXMailZeichenTitel = kundeXMailZeile?.querySelector('.zeilenzeichen')?.getAttribute('title') ?? null;
1129
1144
  // Kartenkopf-Zahl (E71): der Kopf zählt Skills, gebündelt wie der Körper zeilt. An der
1130
1145
  // Verzeichnis-Karte kunde-x hängen dieselben zwei Skills an zwei Harnesses, eine Summe
1131
1146
  // über die Abschnitte ergäbe hier 4.
@@ -1283,6 +1298,8 @@ function fensterOeffnen() {
1283
1298
  katalogDetails,
1284
1299
  katalogMarkenWarnend,
1285
1300
  dskMarken,
1301
+ dskMehrfachAnzahl,
1302
+ dskMehrfachText,
1286
1303
  dskKoerperVorher,
1287
1304
  dskKoerperNachher,
1288
1305
  dskKoerperText,
@@ -1364,6 +1381,9 @@ function fensterOeffnen() {
1364
1381
  kundeXMcpText,
1365
1382
  kundeXMcpZeilenKnopfZahl,
1366
1383
  kundeXMcpKnopfVorhanden,
1384
+ kundeXMcpKopf,
1385
+ kundeXMailQuelle,
1386
+ kundeXMailZeichenTitel,
1367
1387
  kundeXKopfZahl,
1368
1388
  hermesKopfZahl,
1369
1389
  hermesWarnzahl,
@@ -100,6 +100,7 @@ contextBridge.exposeInMainWorld('hmgr', {
100
100
  mcpUebersicht: () => ipcRenderer.invoke('hmgr:mcpUebersicht'),
101
101
  /** Plätze für den Einhängen-Dialog (F42): kommt aus dem Kern, hängt an der Fähigkeit (E16). */
102
102
  mcpPlaetze: () => ipcRenderer.invoke('hmgr:mcpPlaetze'),
103
+ mcpEbenen: () => ipcRenderer.invoke('hmgr:mcpEbenen'),
103
104
  mcpEinhaengen: (args) => ipcRenderer.invoke('hmgr:mcpEinhaengen', args),
104
105
  mcpAushaengen: (args) => ipcRenderer.invoke('hmgr:mcpAushaengen', args),
105
106
  /** Übernehmen aus der geteilten .mcp.json in den privaten Scope (E64), nach Bestätigung im Renderer. */
@@ -339,6 +339,8 @@
339
339
  // Kern (F42, E16): Grundlage für den MCP-Abschnitt und den „MCP hinzufügen …"-Knopf an den
340
340
  // Ziel-Karten — nie am Harness-Namen entschieden, sondern an dieser Liste.
341
341
  var MCP_PLAETZE = [];
342
+ // MCP-Ebenen je Harness (E120), aus der Fähigkeit am Adapter: Grundlage der Tooltip-Erklärung.
343
+ var MCP_EBENEN = [];
342
344
  var quellenFilter = 'alle';
343
345
  // Sortierung der Skills-Rubrik (E79, Richtung E81): hält für die Sitzung wie quellenFilter,
344
346
  // übersteht also ladeAlles. SKILLS selbst bleibt unsortiert nach Datum, damit li.dataset.index
@@ -473,6 +475,7 @@
473
475
  rufe(function () { return window.hmgr.mcpPlaetze(); }),
474
476
  rufe(function () { return window.hmgr.bundles(); }),
475
477
  rufe(function () { return window.hmgr.harnessHeime(); }),
478
+ rufe(function () { return window.hmgr.mcpEbenen(); }),
476
479
  ]);
477
480
  // Alter Stand der Verzeichnis-Pfade, vor dem Überschreiben unten (E84 Teil 1): zielKarten()
478
481
  // liest hier noch ZIELE/AKTIVE_HARNESSE von vor diesem Lauf. null heißt „kein Vergleich", der
@@ -495,6 +498,7 @@
495
498
  BUNDLES.sort(function (a, b) { return a.name.localeCompare(b.name, 'de') || a.quelleText.localeCompare(b.quelleText, 'de'); });
496
499
  BUNDLES_KAPUTT = ergebnisse[9].kaputte;
497
500
  HARNESS_HEIME = ergebnisse[10] || [];
501
+ MCP_EBENEN = ergebnisse[11] || [];
498
502
  STANDARD_REF = {};
499
503
  SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
500
504
 
@@ -813,7 +817,7 @@
813
817
  /** Skill zu einem Mitglied einer Definition aus dem Skill-Katalog (E114 L1): gleiche Quelle, gleicher
814
818
  Pfad. null, wenn die Quelle (noch) nicht eingerichtet oder nicht lesbar ist. */
815
819
  function bundleMitgliedSkill(m) {
816
- return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && s.pfad === m.pfad; })[0] || null;
820
+ return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && (s.pfad === m.pfad || (s.jeHarness || []).indexOf(m.pfad) > -1); })[0] || null;
817
821
  }
818
822
 
819
823
  /** Skill eines nicht verlinkten Mitglieds über seinen Namen, gesucht unter den Mitgliedern der
@@ -1112,54 +1116,115 @@
1112
1116
  return zeilen;
1113
1117
  }
1114
1118
 
1115
- /** Der MCP-Abschnitt unter der Skill-Link-Tabelle einer Ziel-Karte (E61-GUI-Nachtrag): nur, wenn
1116
- dort etwas eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). */
1117
- function mcpAbschnitt(ziel) {
1118
- var wurzel = mcpGitWurzel(ziel);
1119
- if (wurzel !== null) {
1120
- var gesperrt = e('div', 'mcpkarteabschnitt');
1121
- gesperrt.appendChild(e('span', 'etikett', 'MCP'));
1122
- gesperrt.appendChild(e('p', 'ohne mcpkarteleer', mcpWurzelNotiz(wurzel)));
1123
- return gesperrt;
1124
- }
1125
- var zeilen = mcpZeilenFuerZiel(ziel);
1126
- if (zeilen.length === 0 && !mcpKannAn(ziel)) return null;
1119
+ /** Quelle-Spalte und Beschreibung einer MCP-Zeile (E120, F142). */
1120
+ function mcpQuelleText(row) {
1121
+ if (row.art === 'katalog') return 'Katalog';
1122
+ if (row.art === 'fremd') return 'nicht im Katalog';
1123
+ return '.mcp.json des Projekts';
1124
+ }
1125
+
1126
+ /** Tooltip an den Harness-Zeichen einer MCP-Zeile unter „Alle" (E120): wo der Server hier hängt und,
1127
+ je weiterem aktivem Harness des Orts, warum nicht. Der Grund kommt aus den MCP-Ebenen der
1128
+ Fähigkeit (MCP_EBENEN, E16), nie aus dem Harness-Namen. Ein Harness, der den Server hier haben
1129
+ könnte (Ebene arbeitsverzeichnis), bekommt keinen Satz: ein Grund wäre geraten (etwa fehlende
1130
+ CLI, E103). */
1131
+ function mcpZeichenTitel(name, hatHarnesse, teile) {
1132
+ var saetze = ['Hier hängt ' + name + ' für ' + hatHarnesse.map(harnessName).join(', ') + '.'];
1133
+ var gesehen = {};
1134
+ teile.forEach(function (t) {
1135
+ var h = t.ziel.harness;
1136
+ if (gesehen[h] || hatHarnesse.indexOf(h) > -1 || !harnessAktiv(h)) return;
1137
+ gesehen[h] = true;
1138
+ var f = MCP_EBENEN.find(function (x) { return x.harness === h; });
1139
+ if (!f) return;
1140
+ if (f.ebenen.indexOf('arbeitsverzeichnis') > -1) return;
1141
+ if (f.ebenen.length === 0) {
1142
+ saetze.push(harnessName(h) + ' kennt keine MCP-Server.');
1143
+ } else {
1144
+ saetze.push(harnessName(h) + ' liest MCP-Server nicht pro Ordner, nur im Home'
1145
+ + (f.ebenen.indexOf('profil') > -1 ? ' oder in einem Profil' : '') + '.');
1146
+ }
1147
+ });
1148
+ return saetze.join(' ');
1149
+ }
1150
+
1151
+ /** Der MCP-Abschnitt einer Ziel-Karte (E61-GUI-Nachtrag, E120): eine Tabelle mit denselben Spalten
1152
+ wie die Skill-Tabelle darüber. `ziele` ist ein Ziel (Harness-Chip) oder mehrere (Chip „Alle",
1153
+ `teile` gesetzt: dann stehen die Harness-Zeichen vor dem Namen). Nur, wenn dort etwas
1154
+ eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). Sperrnotiz (E91)
1155
+ und „Noch nichts eingehängt." bleiben eine Zeile über die Breite. */
1156
+ function mcpAbschnitt(ziele, teile) {
1157
+ var alle = teile !== undefined;
1158
+ if (!Array.isArray(ziele)) ziele = [ziele];
1159
+ var notizen = [];
1160
+ var zeilen = [];
1161
+ var kannAn = false;
1162
+ ziele.forEach(function (ziel) {
1163
+ var wurzel = mcpGitWurzel(ziel);
1164
+ if (wurzel !== null) { notizen.push(mcpWurzelNotiz(wurzel)); return; }
1165
+ if (mcpKannAn(ziel)) kannAn = true;
1166
+ mcpZeilenFuerZiel(ziel).forEach(function (row) { row.ziel = ziel; zeilen.push(row); });
1167
+ });
1168
+ if (zeilen.length === 0 && notizen.length === 0 && !kannAn) return null;
1169
+ zeilen.sort(function (a, b) {
1170
+ return a.name.localeCompare(b.name, 'de') || harnessName(a.ziel.harness).localeCompare(harnessName(b.ziel.harness), 'de');
1171
+ });
1127
1172
  var abschnitt = e('div', 'mcpkarteabschnitt');
1128
- abschnitt.appendChild(e('span', 'etikett', 'MCP'));
1129
1173
  if (zeilen.length === 0) {
1130
- abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
1174
+ abschnitt.appendChild(e('span', 'etikett', 'MCP'));
1175
+ if (notizen.length === 0) abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
1176
+ notizen.forEach(function (n) { abschnitt.appendChild(e('p', 'ohne mcpkarteleer', n)); });
1131
1177
  return abschnitt;
1132
1178
  }
1133
- var ul = e('ul', 'mcpkartezeilen');
1179
+ var kopf = e('div', 'linkkopf');
1180
+ kopf.appendChild(e('span'));
1181
+ kopf.appendChild(e('span', 'etikett', 'MCP'));
1182
+ kopf.appendChild(e('span', 'etikett', 'Beschreibung'));
1183
+ kopf.appendChild(e('span', 'etikett', 'Quelle'));
1184
+ kopf.appendChild(e('span'));
1185
+ abschnitt.appendChild(kopf);
1186
+ var ul = e('ul', 'linkliste mcpzeilen');
1134
1187
  zeilen.forEach(function (row) {
1188
+ var ziel = row.ziel;
1135
1189
  var li = e('li');
1136
- li.appendChild(e('span', 'name', row.name));
1190
+ li.appendChild(e('span', 'repo'));
1191
+ var nameSpan = e('span', 'name');
1192
+ if (alle) {
1193
+ var hat = [];
1194
+ zeilen.forEach(function (r) {
1195
+ if (r.name === row.name && hat.indexOf(r.ziel.harness) === -1) hat.push(r.ziel.harness);
1196
+ });
1197
+ hat.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
1198
+ var zeichen = harnesseZeichenSammel([ziel.harness]);
1199
+ zeichen.title = mcpZeichenTitel(row.name, hat, teile);
1200
+ nameSpan.appendChild(zeichen);
1201
+ }
1202
+ nameSpan.appendChild(document.createTextNode(row.name));
1203
+ li.appendChild(nameSpan);
1204
+ var wasFeld = e('span', 'wasfeld');
1205
+ if (row.art === 'katalog' || row.art === 'geteilt') wasFeld.appendChild(e('span', 'was', row.beschreibung || ''));
1206
+ else if (row.art === 'fremd-geteilt') wasFeld.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
1207
+ li.appendChild(wasFeld);
1208
+ li.appendChild(e('span', 'quelle', mcpQuelleText(row)));
1137
1209
  if (row.art === 'katalog') {
1138
- li.appendChild(e('span', 'was', row.beschreibung || ''));
1139
1210
  var weg = e('button', 'knopf leise klein', 'Entfernen');
1140
1211
  weg.type = 'button';
1141
1212
  weg.setAttribute('aria-label', row.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen');
1142
1213
  weg.addEventListener('click', function () { frageMcpAus(row.eintrag, ziel); });
1143
1214
  li.appendChild(weg);
1144
1215
  } else if (row.art === 'geteilt' || row.art === 'fremd-geteilt') {
1145
- // E64: der Vermerk nennt die Bedeutung („steht im Projekt, nicht bei dir"), nicht mehr bloß
1146
- // den Dateinamen — und daneben steht der Weg, das zu ändern.
1147
- li.appendChild(row.art === 'geteilt'
1148
- ? e('span', 'was', row.beschreibung || '')
1149
- : e('span', 'was fehlt', 'nicht im Katalog'));
1150
- li.appendChild(e('span', 'geteiltvermerk', 'in der .mcp.json des Projekts'));
1151
1216
  var her = e('button', 'knopf leise klein', 'Übernehmen …');
1152
1217
  her.type = 'button';
1153
1218
  her.setAttribute('aria-label', row.name + ' aus der .mcp.json des Projekts in deine private Konfiguration übernehmen');
1154
1219
  her.addEventListener('click', function () { frageMcpUebernahme(row.name, ziel, row.datei); });
1155
1220
  li.appendChild(her);
1156
1221
  } else {
1157
- li.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
1158
1222
  li.appendChild(e('span'));
1159
1223
  }
1160
1224
  ul.appendChild(li);
1161
1225
  });
1162
1226
  abschnitt.appendChild(ul);
1227
+ notizen.forEach(function (n) { abschnitt.appendChild(e('p', 'ohne mcpkarteleer', n)); });
1163
1228
  return abschnitt;
1164
1229
  }
1165
1230
 
@@ -1357,10 +1422,8 @@
1357
1422
  if (mcpKnopfLeer) leer.appendChild(mcpKnopfLeer);
1358
1423
  });
1359
1424
  teil.appendChild(leer);
1360
- verzMcpZiele.forEach(function (mz) {
1361
- var mcpAbschnLeer = mcpAbschnitt(mz);
1362
- if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1363
- });
1425
+ var mcpAbschnLeer = mcpAbschnitt(verzMcpZiele, teile);
1426
+ if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1364
1427
  return teil;
1365
1428
  }
1366
1429
 
@@ -1494,10 +1557,8 @@
1494
1557
 
1495
1558
  // MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
1496
1559
  // der an diesem Pfad MCP kann.
1497
- verzMcpZiele.forEach(function (mz) {
1498
- var mcpAbschnAlle = mcpAbschnitt(mz);
1499
- if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1500
- });
1560
+ var mcpAbschnAlle = mcpAbschnitt(verzMcpZiele, teile);
1561
+ if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1501
1562
 
1502
1563
  var fuss = e('div', 'zielfuss');
1503
1564
  fuss.appendChild(alleSkillsKnopf(pfad, teile));
@@ -2274,7 +2335,8 @@
2274
2335
  // Skill-Ordner berührt. Nicht der Pull-Zeitpunkt — der steht als „Zuletzt geprüft" in der
2275
2336
  // Update-Ansicht und wäre für alle Skills einer Quelle derselbe.
2276
2337
  var geaendert = datumDeutsch(skill.geaendert);
2277
- var nachRef = ' · Ref ' + skill.ref + (geaendert ? ' · geändert ' + geaendert : '');
2338
+ var nachRef = ' · Ref ' + skill.ref + (geaendert ? ' · geändert ' + geaendert : '')
2339
+ + (skill.jeHarness ? ' · ' + JE_HARNESS_VERMERK : '');
2278
2340
  if (herkunftQuelle) {
2279
2341
  herkunft.appendChild(quelleLinkElement(herkunftQuelle));
2280
2342
  herkunft.appendChild(e('b', null, nachRef));
@@ -2341,7 +2403,8 @@
2341
2403
  var sichtbar = 0;
2342
2404
  document.querySelectorAll('#katalog .zeile').forEach(function (li) {
2343
2405
  var skill = SKILLS[+li.dataset.index];
2344
- var passtSuche = !suche || suchform(skill.name).indexOf(suche) > -1 || suchform(skill.pfad).indexOf(suche) > -1;
2406
+ var passtSuche = !suche || suchform(skill.name).indexOf(suche) > -1 || suchform(skill.pfad).indexOf(suche) > -1
2407
+ || (skill.jeHarness || []).some(function (p) { return suchform(p).indexOf(suche) > -1; });
2345
2408
  var passtFilter = quellenFilter === 'alle' || skill.quelleSchluessel === quellenFilter;
2346
2409
  var an = passtSuche && passtFilter;
2347
2410
  li.hidden = !an;
@@ -2581,9 +2644,13 @@
2581
2644
  /** Quelle · [Pfad ·] Ref für eine Skill-Karte im „Skills hinzufügen"-Dialog (F106): der Pfad
2582
2645
  unterscheidet Skills, die unter demselben Namen mehrfach im selben Repo liegen (z. B. je
2583
2646
  Harness ein eigener Ordner). Ein Wurzel-Skill hat pfad === '' und zeigt kein Pfad-Segment. */
2647
+ var JE_HARNESS_VERMERK = 'je Harness eigene Fassung';
2648
+
2584
2649
  function skillQuelleBeschriftung(skill) {
2585
2650
  var teile = [skill.quelleName];
2586
- if (skill.pfad) teile.push(skill.pfad);
2651
+ // Mehrfach in der Quelle, je Harness eine eigene Fassung (E121): der Kern löst beim Verlinken auf.
2652
+ if (skill.jeHarness) teile.push(JE_HARNESS_VERMERK);
2653
+ else if (skill.pfad) teile.push(skill.pfad);
2587
2654
  teile.push('Ref ' + skill.ref);
2588
2655
  return teile.join(' · ');
2589
2656
  }
@@ -771,21 +771,11 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
771
771
  }
772
772
 
773
773
  /* MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht (F46: die einzige Stelle,
774
- die noch zeigt, wo ein Server hängt): eigener Abschnitt unter der Skill-Link-Tabelle, Name,
775
- Beschreibung/Vermerk, Aktion rechtsbündig, flach in der Karte statt in einem eigenen Kasten. */
774
+ die noch zeigt, wo ein Server hängt): eigener Abschnitt unter der Skill-Link-Tabelle, seit E120 mit
775
+ derselben Tabelle (linkkopf/linkliste) wie die Skills; flach in der Karte statt in einem eigenen Kasten. */
776
776
  .mcpkarteabschnitt{margin-top:16px;padding-top:14px;border-top:1px solid var(--strich)}
777
777
  .mcpkarteabschnitt > .etikett{display:block;margin-bottom:10px}
778
778
  .mcpkarteleer{margin:0;color:var(--tinte-leise);font-style:italic}
779
- .mcpkartezeilen{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
780
- .mcpkartezeilen li{display:flex;flex-wrap:wrap;align-items:center;gap:5px 14px}
781
- .mcpkartezeilen .name{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte)}
782
- .mcpkartezeilen .was{font-size:16px;color:var(--tinte-weich)}
783
- .mcpkartezeilen .was.fehlt{color:var(--tinte-leise);font-style:italic}
784
- .mcpkartezeilen .knopf{margin-left:auto}
785
- .mcpkartezeilen .kein-weg{margin-left:auto;font-size:16px;color:var(--tinte-leise);text-align:right}
786
- /* Wo ein Eintrag steht (E64): kein margin-left:auto, sonst teilte er sich den freien Platz mit dem
787
- Übernehmen-Knopf und stünde in der Mitte statt beim Text. */
788
- .mcpkartezeilen .geteiltvermerk{font-size:16px;color:var(--tinte-leise)}
789
779
  /* Bundle-Gruppe an der Ziel-Karte (E69, F68): eine Einheit über ihren Skill-Zeilen, sichtbar
790
780
  zusammengefasst durch eine linke Kante statt durch einen eigenen Kasten — die Zeilen darin sind
791
781
  dieselbe Tabelle wie darunter und sollen mit ihr fluchten. */
@@ -578,6 +578,15 @@ export class Anwendungsfaelle {
578
578
  }
579
579
  return ziele;
580
580
  }
581
+ /**
582
+ * Die MCP-Ebenen je Harness-Typ (E120), aus der Fähigkeit am Adapter, nie am Namen (E16): eine
583
+ * leere Liste heißt, der Harness kennt MCP-Server gar nicht (agents). Grundlage für die Erklärung,
584
+ * warum ein Server an einem Ort für einen Harness fehlt (GUI-Tooltip, Hinweis von `mcp list`).
585
+ * Unabhängig von Installation und Aktivierung; das entscheidet der Aufrufer.
586
+ */
587
+ mcpEbenen() {
588
+ return this.p.harnesses.map((h) => ({ harness: h.name, ebenen: kannMcp(h) ? h.mcp.ebenen() : [] }));
589
+ }
581
590
  /**
582
591
  * Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
583
592
  * `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
@@ -861,10 +870,10 @@ export class Anwendungsfaelle {
861
870
  ergebnis.push({
862
871
  quelle,
863
872
  ref,
864
- skills: skills.map((skill) => {
873
+ skills: this.fassungenZusammenfassen(skills.map((skill) => {
865
874
  const geaendert = daten.get(skill.pfad);
866
875
  return geaendert === undefined ? skill : { ...skill, geaendert };
867
- }),
876
+ })),
868
877
  });
869
878
  }
870
879
  catch (fehler) {
@@ -873,6 +882,39 @@ export class Anwendungsfaelle {
873
882
  }
874
883
  return ergebnis;
875
884
  }
885
+ /**
886
+ * Katalog-Zusammenfassung (E121): Ein Name, der in einer Quelle mehrfach vorkommt und mindestens
887
+ * einmal unmittelbar in einem Harness-Skill-Ordner liegt, erscheint einmal, mit `jeHarness`. Als
888
+ * Vertreter dient die Fassung im ersten passenden Harness-Ordner (Reihenfolge der Harnesses),
889
+ * sonst die erste. Alle anderen Namen bleiben unverändert, ebenso die Reihenfolge.
890
+ */
891
+ fassungenZusammenfassen(skills) {
892
+ const ordner = this.p.harnesses.map((h) => `${h.quellOrdner()}/`);
893
+ const direkt = (s, o) => s.pfad.startsWith(o) && !s.pfad.slice(o.length).includes('/');
894
+ const gruppen = new Map();
895
+ for (const s of skills)
896
+ gruppen.set(s.name, [...(gruppen.get(s.name) ?? []), s]);
897
+ const ergebnis = [];
898
+ for (const s of skills) {
899
+ const gruppe = gruppen.get(s.name);
900
+ if (gruppe.length < 2 || !gruppe.some((g) => ordner.some((o) => direkt(g, o)))) {
901
+ ergebnis.push(s);
902
+ continue;
903
+ }
904
+ if (gruppe[0] !== s)
905
+ continue; // Vertreter steht an der Stelle der ersten Fassung
906
+ let vertreter = gruppe[0];
907
+ for (const o of ordner) {
908
+ const treffer = gruppe.find((g) => direkt(g, o));
909
+ if (treffer !== undefined) {
910
+ vertreter = treffer;
911
+ break;
912
+ }
913
+ }
914
+ ergebnis.push({ ...vertreter, jeHarness: gruppe.map((g) => g.pfad) });
915
+ }
916
+ return ergebnis;
917
+ }
876
918
  /** Alle Marketplaces der bekannten Quellen, für `hmgr plugin list`. */
877
919
  async marketplaces() {
878
920
  const zustand = await this.p.zustand.lesen();
@@ -1566,7 +1608,7 @@ export class Anwendungsfaelle {
1566
1608
  throw new Error(`Mehrere Quellen passen auf „${hinweis}", bitte eingrenzen:\n${zeilen(treffer)}`);
1567
1609
  }
1568
1610
  /** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
1569
- async skillFinden(name, quelleHinweis) {
1611
+ async skillFinden(name, quelleHinweis, wahl = {}) {
1570
1612
  const zustand = await this.p.zustand.lesen();
1571
1613
  const funde = [];
1572
1614
  for (const quelle of zustand.quellen) {
@@ -1574,16 +1616,33 @@ export class Anwendungsfaelle {
1574
1616
  continue;
1575
1617
  const ref = await this.p.quellen.standardRef(quelle);
1576
1618
  const skills = await this.p.quellen.skills(quelle, ref);
1577
- for (const skill of skills)
1578
- if (skill.name === name)
1579
- funde.push({ quelle, ref, skill });
1619
+ for (const skill of Anwendungsfaelle.skillTreffer(skills, name, wahl))
1620
+ funde.push({ quelle, ref, skill });
1580
1621
  }
1581
1622
  if (funde.length === 0)
1582
1623
  throw new SkillUnbekannt(name);
1583
1624
  if (funde.length > 1)
1584
- throw new SkillMehrdeutig(name, funde.map((f) => f.quelle));
1625
+ throw new SkillMehrdeutig(name, funde.map((f) => ({ quelle: f.quelle, pfad: f.skill.pfad })));
1585
1626
  return funde[0];
1586
1627
  }
1628
+ /**
1629
+ * Die Skills einer Quelle zu einem Namen (E121). Mit `pfad` zählt nur der Skill mit genau diesem
1630
+ * Pfad. Sonst gewinnt bei mehreren Fassungen die eine, die unmittelbar im Skill-Ordner des
1631
+ * Harness liegt (`<quellOrdner>/<ordner>`, eine Ebene); gibt es keine solche oder mehrere,
1632
+ * bleiben alle, und der Aufrufer meldet die Mehrdeutigkeit.
1633
+ */
1634
+ static skillTreffer(skills, name, wahl) {
1635
+ const gleichnamig = skills.filter((s) => s.name === name);
1636
+ if (wahl.pfad !== undefined) {
1637
+ const pfad = wahl.pfad.replace(/^\.?\/+|\/+$/g, '');
1638
+ return gleichnamig.filter((s) => s.pfad === pfad);
1639
+ }
1640
+ if (gleichnamig.length < 2 || wahl.quellOrdner === undefined)
1641
+ return gleichnamig;
1642
+ const praefix = `${wahl.quellOrdner}/`;
1643
+ const direkt = gleichnamig.filter((s) => s.pfad.startsWith(praefix) && !s.pfad.slice(praefix.length).includes('/'));
1644
+ return direkt.length === 1 ? direkt : gleichnamig;
1645
+ }
1587
1646
  /** Checkout anlegen oder erweitern, Ziel vorbereiten, Link setzen. Ohne Zeiger-Arbeit. */
1588
1647
  async linkAnlegen(harness, ziel, fund, ersetzen) {
1589
1648
  if (!harness.orte().includes(ziel.ort.art))
@@ -1627,8 +1686,12 @@ export class Anwendungsfaelle {
1627
1686
  if (!harness.orte().includes(args.ziel.ort.art))
1628
1687
  throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
1629
1688
  const version = args.version;
1689
+ const wahl = {
1690
+ quellOrdner: harness.quellOrdner(),
1691
+ ...(args.pfad === undefined ? {} : { pfad: args.pfad }),
1692
+ };
1630
1693
  if (version === undefined) {
1631
- const fund = await this.skillFinden(args.skill, args.quelle);
1694
+ const fund = await this.skillFinden(args.skill, args.quelle, wahl);
1632
1695
  const ergebnis = await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
1633
1696
  return Anwendungsfaelle.hinweisAnhaengen(ergebnis, await this.bundleMitgliedZurueckholen(args.ziel, fund));
1634
1697
  }
@@ -1640,7 +1703,7 @@ export class Anwendungsfaelle {
1640
1703
  // Eine Versionseinschränkung schreibt immer geteilt (siehe EinschraenkungOhneZeiger oben), das
1641
1704
  // trifft die Sperre also ausnahmslos, solange sie steht (E57).
1642
1705
  await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
1643
- const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
1706
+ const fund = await this.fundAusZeiger(args.skill, version, args.quelle, wahl);
1644
1707
  const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
1645
1708
  await this.zeigerErgaenzen(zeigerPfad, fund, version);
1646
1709
  const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
@@ -1957,9 +2020,9 @@ export class Anwendungsfaelle {
1957
2020
  * gilt der Text als genauer Ref-Name gegen alle Refs, damit Tags und Branches ohne
1958
2021
  * Versions-Schema erreichbar bleiben. Nie auf den Standard-Ref ausweichen (B8).
1959
2022
  */
1960
- async fundAusZeiger(name, einschraenkung, quelleHinweis) {
2023
+ async fundAusZeiger(name, einschraenkung, quelleHinweis, wahl = {}) {
1961
2024
  if (einschraenkung === null)
1962
- return await this.skillFinden(name, quelleHinweis);
2025
+ return await this.skillFinden(name, quelleHinweis, wahl);
1963
2026
  const zustand = await this.p.zustand.lesen();
1964
2027
  const quellen = zustand.quellen.filter((q) => quellePasst(q, quelleHinweis));
1965
2028
  for (const quelle of quellen) {
@@ -1968,7 +2031,7 @@ export class Anwendungsfaelle {
1968
2031
  const ref = passenderRef(tags, einschraenkung);
1969
2032
  if (ref === null)
1970
2033
  continue;
1971
- const skill = (await this.p.quellen.skills(quelle, ref)).find((s) => s.name === name);
2034
+ const skill = Anwendungsfaelle.skillTreffer(await this.p.quellen.skills(quelle, ref), name, wahl)[0];
1972
2035
  if (skill)
1973
2036
  return { quelle, ref, skill };
1974
2037
  }
@@ -1976,7 +2039,7 @@ export class Anwendungsfaelle {
1976
2039
  const refs = await this.p.quellen.refs(quelle).catch(() => []);
1977
2040
  if (!refs.includes(einschraenkung))
1978
2041
  continue;
1979
- const skill = (await this.p.quellen.skills(quelle, einschraenkung)).find((s) => s.name === name);
2042
+ const skill = Anwendungsfaelle.skillTreffer(await this.p.quellen.skills(quelle, einschraenkung), name, wahl)[0];
1980
2043
  if (skill)
1981
2044
  return { quelle, ref: einschraenkung, skill };
1982
2045
  }
@@ -2053,7 +2116,7 @@ export class Anwendungsfaelle {
2053
2116
  }
2054
2117
  for (const [name, einschraenkung] of zeiger.skills) {
2055
2118
  try {
2056
- const fund = await this.fundAusZeiger(name, einschraenkung);
2119
+ const fund = await this.fundAusZeiger(name, einschraenkung, undefined, { quellOrdner: harness.quellOrdner() });
2057
2120
  const gefunden = eintraege.find((e) => e.art === 'link' && e.link.name === name);
2058
2121
  // Ein eigener Link auf einen anderen Ref weicht ab und wird umgelegt (E33). Das trifft
2059
2122
  // die Einschränkung, die inzwischen einen höheren Tag findet.
@@ -338,10 +338,24 @@ export class LinkAnlegenFehlgeschlagen extends Error {
338
338
  export class SkillMehrdeutig extends Error {
339
339
  name;
340
340
  quellen;
341
- constructor(name, quellen) {
342
- super(`Skill „${name}" liegt in mehreren Quellen: ${quellen.map(quelleSchluessel).join(', ')}`);
341
+ funde;
342
+ /** Die Mehrdeutigkeit kommt aus einer einzigen Quelle: `--pfad` hilft, nicht `--quelle` (E121). */
343
+ nurEineQuelle;
344
+ constructor(name, funde) {
345
+ const quellen = [];
346
+ for (const f of funde)
347
+ if (!quellen.some((q) => quelleSchluessel(q) === quelleSchluessel(f.quelle)))
348
+ quellen.push(f.quelle);
349
+ const zeilen = funde
350
+ .map((f) => ` ${quelleSchluessel(f.quelle)}: ${f.pfad === '' ? '(Repo-Wurzel)' : f.pfad}`)
351
+ .join('\n');
352
+ super(quellen.length === 1
353
+ ? `Skill „${name}" liegt in der Quelle ${quelleSchluessel(quellen[0])} mehrfach:\n${zeilen}`
354
+ : `Skill „${name}" liegt in mehreren Quellen:\n${zeilen}`);
343
355
  this.name = name;
344
356
  this.quellen = quellen;
357
+ this.funde = funde;
358
+ this.nurEineQuelle = quellen.length === 1;
345
359
  }
346
360
  }
347
361
  export class SkillUnbekannt extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",