hmgr 0.0.22 → 0.0.24

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.
@@ -241,6 +241,7 @@ class ClaudeMcp {
241
241
  this.ausfuehrer = ausfuehrer;
242
242
  this.home = home;
243
243
  }
244
+ anGitWurzelGebunden = true;
244
245
  ebenen() {
245
246
  return ['home', 'arbeitsverzeichnis'];
246
247
  }
@@ -359,7 +360,10 @@ class ClaudeMcp {
359
360
  /**
360
361
  * Die MCP-Server, die `~/.claude.json` für diesen Ort führt. Ein Arbeitsverzeichnis steht dort
361
362
  * unter dem aufgelösten Pfad, weil Claude Code den cwd auflöst (belegt an 2.1.261); hmgr merkt
362
- * sich Arbeitsverzeichnisse aber so, wie der Nutzer sie genannt hat, also zählen beide.
363
+ * sich Arbeitsverzeichnisse aber so, wie der Nutzer sie genannt hat, also zählen beide. Liegt der
364
+ * Ordner in einem Git-Repo, ist der Schlüssel die Git-Wurzel, nicht der Ordner (E91, belegt an
365
+ * 2.1.285): für einen Unterordner liest diese Methode deshalb nichts, und der Kern sperrt dort
366
+ * die MCP-Operationen, bevor sie den Adapter erreichen.
363
367
  */
364
368
  async mcpServersAm(ort) {
365
369
  const heim = zusaetzlichesHeim(ort);
package/dist/cli/hilfe.js CHANGED
@@ -383,6 +383,12 @@ global (Scope user). Hermes kennt nur global, mcp_servers in seiner config.yaml
383
383
  mit --ort profil:<name>); ein Projekt-Ziel ist bei Hermes ein Fehler, es kennt keine Projektebene für
384
384
  MCPs. agents kennt MCP-Server gar nicht.
385
385
 
386
+ Bei claude-code führt der Scope local die Server je Git-Repo, nicht je Unterordner (E91). Ein
387
+ Arbeitsverzeichnis, das in einem Repo liegt, ohne dessen Wurzel zu sein, bekommt deshalb kein mcp add,
388
+ mcp remove und mcp migrate: die Meldung nennt den Wurzelordner, dort gehören die Server hin (ist er
389
+ noch kein Arbeitsverzeichnis: hmgr dir add <wurzel>). Die Wurzel selbst und Ordner ohne Repo gehen
390
+ wie gewohnt.
391
+
386
392
  mcp migrate ist der einzige Weg, auf dem hmgr die geteilte .mcp.json anfasst (E64): der Eintrag
387
393
  wandert 1:1 aus der Datei in deinen privaten Scope am selben Ort und wird dort gelöscht. Auch ein
388
394
  Eintrag, der nicht im Katalog steht, lässt sich so übernehmen — der Konfigblock kommt aus der Datei,
package/dist/cli/main.js CHANGED
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { Anwendungsfaelle, } from "../kern/anwendungsfaelle.js";
8
- import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimOhneCliZugriff, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
8
+ import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimOhneCliZugriff, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
9
  import { Agents } from "../adapter/agents.js";
10
10
  import { ClaudeCode, claudeAusfuehrer } from "../adapter/claude-code.js";
11
11
  import { Hermes } from "../adapter/hermes.js";
@@ -1142,6 +1142,7 @@ function istModellFehler(fehler) {
1142
1142
  fehler instanceof McpAbweichenderEintrag ||
1143
1143
  fehler instanceof McpKatalogFehler ||
1144
1144
  fehler instanceof McpNurGeteilterFundort ||
1145
+ fehler instanceof McpOrtUnterGitWurzel ||
1145
1146
  fehler instanceof McpUebernahmeNichtMoeglich ||
1146
1147
  fehler instanceof McpNichtInGeteilterDatei ||
1147
1148
  fehler instanceof McpUebernahmeUnvollstaendig ||
package/dist/gui/main.js CHANGED
@@ -819,10 +819,40 @@ function fensterOeffnen() {
819
819
  // fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
820
820
  var dskBundleBoxSichtbar = false;
821
821
  var dskBundleText = null;
822
+ // Aufklappbereich je Zeile (E92): zu vor dem Klick, danach sichtbar mit der ungekürzten
823
+ // Beschreibung; die Checkbox bleibt unberührt, der Dialog offen.
824
+ var dskKoerperVorher = null;
825
+ var dskKoerperNachher = null;
826
+ var dskKoerperText = null;
827
+ var dskAufklappZeileAngehakt = null;
828
+ var dskAufklappZeileGesperrt = null;
829
+ var dskAufklappAriaNachher = null;
830
+ var dskDialogOffenNachAufklappen = null;
831
+ var dskAufklappFreiAngehakt = null;
822
832
  if (dskKnopf) {
823
833
  dskKnopf.click();
824
834
  dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
825
835
  ?.replace(/\\s+/g, ' ').trim() ?? null;
836
+ var dskZeile = document.querySelector('#dsk-liste .wahlzeile:has(.aufklappknopf)');
837
+ var dskAufklapp = dskZeile?.querySelector('.aufklappknopf');
838
+ var dskKoerper = dskZeile?.querySelector('.wahlkoerper');
839
+ dskKoerperVorher = dskKoerper?.hidden ?? null;
840
+ dskAufklapp?.click();
841
+ dskKoerperNachher = dskKoerper?.hidden ?? null;
842
+ dskAufklappAriaNachher = dskAufklapp?.getAttribute('aria-expanded') ?? null;
843
+ dskKoerperText = dskKoerper?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
844
+ dskKoerper?.querySelector('p')?.click();
845
+ dskAufklappZeileAngehakt = dskZeile?.querySelector('input')?.checked ?? null;
846
+ dskAufklappZeileGesperrt = dskZeile?.querySelector('input')?.disabled ?? null;
847
+ dskDialogOffenNachAufklappen = document.getElementById('d-skills')?.open ?? null;
848
+ // Die Fixture-Zeile ist gesperrt (Link liegt schon), das beweist nichts über das Label:
849
+ // darum dieselbe Zeile entsperren, zu- und wieder aufklappen und in den Text klicken.
850
+ var dskEingabe = dskZeile?.querySelector('input');
851
+ if (dskEingabe) dskEingabe.disabled = false;
852
+ dskAufklapp?.click();
853
+ dskAufklapp?.click();
854
+ dskKoerper?.querySelector('p')?.click();
855
+ dskAufklappFreiAngehakt = dskEingabe?.checked ?? null;
826
856
  dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
827
857
  dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
828
858
  ?.replace(/\\s+/g, ' ').trim() ?? null;
@@ -879,6 +909,8 @@ function fensterOeffnen() {
879
909
  // schon (gesperrte Zeile), „teamwork" nirgends (die Notizen je Ort).
880
910
  var mcpWahlText = null;
881
911
  var mcpWahlTextFrei = null;
912
+ // E91: die Wahlzeile des Unterordners im Repo ist deaktiviert und trägt die Notiz.
913
+ var mcpUnterordnerGesperrt = null;
882
914
  try {
883
915
  var mcpEinKnoepfe = Array.from(document.querySelectorAll('#mcpliste .mcpkopf button'))
884
916
  .filter((b) => (b.textContent || '').indexOf('Einhängen') === 0);
@@ -893,6 +925,9 @@ function fensterOeffnen() {
893
925
  document.querySelector('#d-mcp button[value="abbruch"]')?.click();
894
926
  dMcpOffenNachAbbrechen = document.getElementById('d-mcp')?.open === true;
895
927
  mcpWahlTextFrei = await mcpWahlOeffnen(mcpEinKnoepfe[1]);
928
+ mcpUnterordnerGesperrt = Array.from(document.querySelectorAll('#dm-orte .wahlzeile'))
929
+ .filter((z) => (z.textContent || '').indexOf('unterordner') > -1)
930
+ .map((z) => z.querySelector('input')?.disabled === true);
896
931
  document.querySelector('#d-mcp button[value="abbruch"]')?.click();
897
932
  } catch (fehler) {
898
933
  mcpFehler = String(fehler);
@@ -1121,6 +1156,14 @@ function fensterOeffnen() {
1121
1156
  katalogDetails,
1122
1157
  katalogMarkenWarnend,
1123
1158
  dskMarken,
1159
+ dskKoerperVorher,
1160
+ dskKoerperNachher,
1161
+ dskKoerperText,
1162
+ dskAufklappZeileAngehakt,
1163
+ dskAufklappZeileGesperrt,
1164
+ dskAufklappAriaNachher,
1165
+ dskDialogOffenNachAufklappen,
1166
+ dskAufklappFreiAngehakt,
1124
1167
  dskBundleBoxSichtbar,
1125
1168
  dskBundleText,
1126
1169
  dzZieleText,
@@ -1162,6 +1205,7 @@ function fensterOeffnen() {
1162
1205
  dMcpOffenNachAbbrechen,
1163
1206
  mcpWahlText,
1164
1207
  mcpWahlTextFrei,
1208
+ mcpUnterordnerGesperrt,
1165
1209
  mcpFehler,
1166
1210
  dMcpAusOffenNachOeffnen,
1167
1211
  dMcpAusOffenNachAbbrechen,
@@ -960,6 +960,18 @@
960
960
  return MCP_PLAETZE.some(function (p) { return zielKey(p) === zk; });
961
961
  }
962
962
 
963
+ /* E91: Claude Code führt MCP-Server je Git-Wurzel. Liegt ein Arbeitsverzeichnis in einem Repo,
964
+ ohne dessen Wurzel zu sein, liefert der Kern den Platz mit gitWurzel, und hier gibt es dort
965
+ kein Hinzufügen, Entfernen oder Übernehmen. Ergebnis: die Wurzel oder null. */
966
+ function mcpGitWurzel(ziel) {
967
+ var zk = zielKey(ziel);
968
+ var platz = MCP_PLAETZE.find(function (p) { return zielKey(p) === zk; });
969
+ return platz && platz.gitWurzel ? platz.gitWurzel : null;
970
+ }
971
+ function mcpWurzelNotiz(wurzel) {
972
+ return 'Liegt im Git-Repo ' + wurzel + '. Claude Code führt MCP-Server je Repo — dort einhängen.';
973
+ }
974
+
963
975
  /** Die MCP-Zeilen eines Ziels, aus den drei Quellen der MCP-Ansicht zusammengesucht: eingehängte
964
976
  Katalog-Einträge (mit Entfernen-Knopf), ihre Fundorte in der geteilten .mcp.json des Projekts
965
977
  (E61-Nachtrag, seit E64 mit Übernehmen-Knopf) und Einträge, die dort hängen, aber nicht im
@@ -991,6 +1003,13 @@
991
1003
  /** Der MCP-Abschnitt unter der Skill-Link-Tabelle einer Ziel-Karte (E61-GUI-Nachtrag): nur, wenn
992
1004
  dort etwas eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). */
993
1005
  function mcpAbschnitt(ziel) {
1006
+ var wurzel = mcpGitWurzel(ziel);
1007
+ if (wurzel !== null) {
1008
+ var gesperrt = e('div', 'mcpkarteabschnitt');
1009
+ gesperrt.appendChild(e('span', 'etikett', 'MCP'));
1010
+ gesperrt.appendChild(e('p', 'ohne mcpkarteleer', mcpWurzelNotiz(wurzel)));
1011
+ return gesperrt;
1012
+ }
994
1013
  var zeilen = mcpZeilenFuerZiel(ziel);
995
1014
  if (zeilen.length === 0 && !mcpKannAn(ziel)) return null;
996
1015
  var abschnitt = e('div', 'mcpkarteabschnitt');
@@ -1035,7 +1054,7 @@
1035
1054
  /** Knopf „MCP hinzufügen …" im Kartenfuß neben „Skills hinzufügen" (E61-GUI-Nachtrag, Gegenrichtung
1036
1055
  zur MCP-Ansicht): nur, wo der Harness an diesem Ort MCP kann (mcpKannAn, E16). */
1037
1056
  function mcpHinzufuegenKnopf(ziel) {
1038
- if (!mcpKannAn(ziel)) return null;
1057
+ if (!mcpKannAn(ziel) || mcpGitWurzel(ziel) !== null) return null;
1039
1058
  var knopf = e('button', 'knopf leise klein mitsymbol', 'MCP hinzufügen …');
1040
1059
  knopf.type = 'button';
1041
1060
  knopf.insertBefore(harnessZeichen(ziel.harness), knopf.firstChild);
@@ -2281,6 +2300,53 @@
2281
2300
  return teile.join(' · ');
2282
2301
  }
2283
2302
 
2303
+ /** Aufklappbereich einer Wahlzeile im Skills-hinzufügen-Dialog (E92): ungekürzte description und
2304
+ die Frontmatter-Angaben aus E55. Der Knopf hängt am Label (absolut oben rechts, `txt` bekommt
2305
+ Platz dafür), der Körper als drittes Kind des Labels, damit er als Grid-Zeile über die volle
2306
+ Breite läuft. Der Zustand ist der Zustand von aria-expanded: er lebt nur, solange die Zeile
2307
+ lebt (je Dialog-Öffnung neu, immer zugeklappt), nicht in SKILL_OFFEN. Das Label schaltet
2308
+ sonst die Checkbox um, deshalb preventDefault auf Knopf und Körper. Ohne Beschreibung und
2309
+ ohne Details entsteht nichts. */
2310
+ function skillAufklappbereich(skill, label, txt, index) {
2311
+ var details = skillDetails(skill);
2312
+ if (!skill.beschreibung && details.length === 0) return;
2313
+ var koerperId = 'dsk-koerper-' + index;
2314
+ var knopf = e('button', 'aufklappknopf');
2315
+ knopf.type = 'button';
2316
+ knopf.setAttribute('aria-expanded', 'false');
2317
+ knopf.setAttribute('aria-controls', koerperId);
2318
+ knopf.setAttribute('aria-label', 'Beschreibung und Details zu ' + skill.name + ' ein- oder ausklappen');
2319
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2320
+ pfeil.setAttribute('viewBox', '0 0 16 16');
2321
+ pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
2322
+ pfeil.setAttribute('aria-hidden', 'true');
2323
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
2324
+ knopf.appendChild(pfeil);
2325
+
2326
+ var koerper = e('div', 'wahlkoerper');
2327
+ koerper.id = koerperId;
2328
+ koerper.hidden = true;
2329
+ if (skill.beschreibung) koerper.appendChild(e('p', 'skilldetail skillbeschreibung', skill.beschreibung));
2330
+ details.forEach(function (d) {
2331
+ var zeile = e('p', 'skilldetail');
2332
+ zeile.appendChild(e('b', null, d.label + ': '));
2333
+ zeile.appendChild(document.createTextNode(d.text));
2334
+ koerper.appendChild(zeile);
2335
+ });
2336
+
2337
+ knopf.addEventListener('click', function (ev) {
2338
+ ev.preventDefault();
2339
+ var war = knopf.getAttribute('aria-expanded') === 'true';
2340
+ knopf.setAttribute('aria-expanded', war ? 'false' : 'true');
2341
+ koerper.hidden = war;
2342
+ });
2343
+ koerper.addEventListener('click', function (ev) { ev.preventDefault(); });
2344
+
2345
+ txt.classList.add('mitknopf');
2346
+ label.appendChild(knopf);
2347
+ label.appendChild(koerper);
2348
+ }
2349
+
2284
2350
  function oeffneSkillsHinzufuegen(z) {
2285
2351
  skillsKontext = { modus: 'einzeln', z: z };
2286
2352
  $('dsk-ziel').textContent = zielTitel(z);
@@ -2310,6 +2376,7 @@
2310
2376
  txt.appendChild(quelleSpanEl);
2311
2377
  if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
2312
2378
  label.appendChild(txt);
2379
+ skillAufklappbereich(skill, label, txt, i);
2313
2380
  box.appendChild(label);
2314
2381
  });
2315
2382
  fuelleBundleSektion(function (bundle) { return bundleHaengtIn([z], bundle); }, 1);
@@ -2355,6 +2422,7 @@
2355
2422
  txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + (teile.length - fehltIn.length) + ' von ' + teile.length + ' Harnesses.'));
2356
2423
  }
2357
2424
  label.appendChild(txt);
2425
+ skillAufklappbereich(skill, label, txt, i);
2358
2426
  box.appendChild(label);
2359
2427
  });
2360
2428
  fuelleBundleSektion(function (bundle) { return bundleHaengtIn(teile, bundle); }, teile.length);
@@ -3071,14 +3139,17 @@
3071
3139
  input.type = 'radio';
3072
3140
  input.name = 'mcport';
3073
3141
  input.value = String(i);
3074
- input.disabled = schon || !aktiv;
3142
+ input.disabled = schon || !aktiv || !!ziel.gitWurzel;
3075
3143
  label.appendChild(input);
3076
3144
 
3077
3145
  var txt = e('span');
3078
3146
  txt.appendChild(beschriftet(e('b', null, zielTitel({ ziel: ziel })), ziel.harness));
3079
3147
  txt.appendChild(e('span', 'mono', mcpOrtSpur(ziel)));
3080
3148
 
3081
- if (schon) {
3149
+ if (ziel.gitWurzel) {
3150
+ // E91: der Kern sperrt diesen Platz, Einhängen ginge an die Wurzel.
3151
+ txt.appendChild(e('span', 'notiz', mcpWurzelNotiz(ziel.gitWurzel)));
3152
+ } else if (schon) {
3082
3153
  // F46: die Orte-Liste am Katalog-Eintrag gibt es nicht mehr, der Weg zum Aushängen führt
3083
3154
  // über den Entfernen-Knopf im MCP-Abschnitt der Ziel-Karte.
3084
3155
  txt.appendChild(e('span', 'notiz', 'Hängt hier schon. Zum Aushängen den Entfernen-Knopf am MCP-Abschnitt der Ziel-Karte benutzen.'));
@@ -539,6 +539,15 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
539
539
  .wahlzeile b{font-family:var(--schrift);font-weight:700;font-size:17px;letter-spacing:-.012em}
540
540
  .wahlzeile .mono{display:block;color:var(--tinte-weich);margin-top:3px}
541
541
  .wahlzeile .notiz{display:block;margin-top:8px;font-size:16px;color:var(--messing-satt);background:var(--messing-blass);border-left:3px solid var(--messing);padding:7px 10px;border-radius:0 2px 2px 0}
542
+ /* Aufklappbereich einer Wahlzeile im Skills-Dialog (E92). Der Knopf sitzt absolut oben rechts im Label
543
+ (die Zeile ist position:relative), die Textspalte lässt ihm Platz; der Körper ist ein eigenes
544
+ Grid-Kind über beide Spalten. Wie bei .zeile .skillkoerper braucht hidden eine eigene Regel. */
545
+ .wahlzeile .aufklappknopf{position:absolute;top:10px;right:10px}
546
+ .wahlzeile .mitknopf{padding-right:40px}
547
+ .wahlzeile .wahlkoerper{grid-column:1/-1;margin-top:2px;padding-top:12px;border-top:1px solid var(--strich-zart);cursor:text}
548
+ .wahlzeile .wahlkoerper[hidden]{display:none}
549
+ .wahlzeile .wahlkoerper b{font-size:inherit;font-weight:600;letter-spacing:normal}
550
+ .skillbeschreibung{white-space:pre-wrap}
542
551
  .nummer{
543
552
  font-family:var(--mono);font-size:16px;font-weight:500;width:28px;height:28px;flex:none;display:grid;
544
553
  place-items:center;border:1.5px solid var(--tinte);border-radius:50%;margin-top:1px;
@@ -1,6 +1,6 @@
1
1
  // Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
2
2
  import path from 'node:path';
3
- import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
3
+ import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
4
4
  import { passenderRef } from "./version.js";
5
5
  import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
6
6
  /**
@@ -345,6 +345,25 @@ export class Anwendungsfaelle {
345
345
  throw new McpFaehigkeitFehlt(name);
346
346
  return harness;
347
347
  }
348
+ /**
349
+ * Die Git-Wurzel, an der ein Arbeitsverzeichnis in Wahrheit hängt (E91), wenn der Harness seinen
350
+ * local-Scope je Git-Wurzel führt und der Ordner in einem Repo liegt, aber nicht dessen Wurzel
351
+ * ist. Sonst null: kein Repo, Ordner fehlt, der Ordner ist die Wurzel, oder die Fähigkeit hat das
352
+ * Merkmal nicht.
353
+ */
354
+ async mcpGitWurzelFuer(faehig, ziel) {
355
+ if (faehig.anGitWurzelGebunden !== true || ziel.ort.art !== 'arbeitsverzeichnis')
356
+ return null;
357
+ const kopie = await this.p.quellen.arbeitskopie(ziel.ort.pfad).catch(() => null);
358
+ if (kopie === null || kopie.relativerPfad === '')
359
+ return null;
360
+ return kopie.wurzel;
361
+ }
362
+ async mcpGitWurzelPruefen(faehig, ziel) {
363
+ const wurzel = await this.mcpGitWurzelFuer(faehig, ziel);
364
+ if (wurzel !== null)
365
+ throw new McpOrtUnterGitWurzel(ziel, wurzel);
366
+ }
348
367
  /** MCP-Katalog, unverändert wie in ~/.hmgr/mcp.yaml gepflegt (E61). hmgr schreibt ihn nie. */
349
368
  async mcpKatalog() {
350
369
  return await this.p.mcpKatalog.lesen();
@@ -385,6 +404,7 @@ export class Anwendungsfaelle {
385
404
  const faehig = this.harnessMitMcp(harness.name);
386
405
  if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
387
406
  throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
407
+ await this.mcpGitWurzelPruefen(faehig.mcp, args.ziel);
388
408
  await faehig.mcp.einhaengen(eintrag.name, eintrag.konfig, args.ziel.ort);
389
409
  const warnung = await this.mcpVariablenWarnung(faehig, eintrag, args.ziel.ort);
390
410
  const hinweis = faehig.mcp.hinweisNachAenderung?.();
@@ -402,6 +422,7 @@ export class Anwendungsfaelle {
402
422
  const faehig = this.harnessMitMcp(harness.name);
403
423
  if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
404
424
  throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
425
+ await this.mcpGitWurzelPruefen(faehig.mcp, args.ziel);
405
426
  await faehig.mcp.aushaengen(eintrag.name, args.ziel.ort);
406
427
  const hinweis = faehig.mcp.hinweisNachAenderung?.();
407
428
  return { eintrag, ziel: args.ziel, ...(hinweis === undefined ? {} : { hinweis }) };
@@ -420,6 +441,7 @@ export class Anwendungsfaelle {
420
441
  const uebernehmen = faehig.mcp.geteiltenUebernehmen;
421
442
  if (uebernehmen === undefined)
422
443
  throw new McpUebernahmeNichtMoeglich(args.ziel);
444
+ await this.mcpGitWurzelPruefen(faehig.mcp, args.ziel);
423
445
  const { datei } = await uebernehmen.call(faehig.mcp, args.name, args.ziel.ort);
424
446
  const hinweis = faehig.mcp.hinweisNachAenderung?.();
425
447
  return { name: args.name, ziel: args.ziel, datei, ...(hinweis === undefined ? {} : { hinweis }) };
@@ -458,7 +480,12 @@ export class Anwendungsfaelle {
458
480
  * Adapter, nicht am Harness-Namen (E16): agents kommt nie vor, weil es `mcp` nicht implementiert.
459
481
  */
460
482
  async mcpPlaetze() {
461
- return this.mcpZiele();
483
+ const plaetze = [];
484
+ for (const ziel of await this.mcpZiele()) {
485
+ const wurzel = await this.mcpGitWurzelFuer(this.harnessMitMcp(ziel.harness).mcp, ziel);
486
+ plaetze.push(wurzel === null ? ziel : { ...ziel, gitWurzel: wurzel });
487
+ }
488
+ return plaetze;
462
489
  }
463
490
  /**
464
491
  * Übersicht für `hmgr mcp list` (E61): je Katalog-Eintrag, wo er eingehängt ist, dazu Einträge in
@@ -524,6 +524,23 @@ export class McpNurGeteilterFundort extends Error {
524
524
  this.datei = datei;
525
525
  }
526
526
  }
527
+ /**
528
+ * MCP-Operation an einem Arbeitsverzeichnis, das in einem Git-Repo liegt, aber nicht dessen Wurzel
529
+ * ist (E91): Claude Code führt den local-Scope je Repo, nicht je Unterordner. Einhängen, Aushängen
530
+ * und Übernehmen sind dort gesperrt; ein Aushängen würde sonst den Eintrag der Wurzel entfernen.
531
+ */
532
+ export class McpOrtUnterGitWurzel extends Error {
533
+ ziel;
534
+ wurzel;
535
+ constructor(ziel, wurzel) {
536
+ const pfad = ziel.ort.art === 'arbeitsverzeichnis' ? ziel.ort.pfad : '';
537
+ super(`${pfad} liegt im Git-Repo ${wurzel}. Claude Code führt MCP-Server je Repo, nicht je Unterordner: `
538
+ + `was dort hängt, gilt auch hier. Häng MCP-Server an ${wurzel} ein; ist das noch kein Arbeitsverzeichnis: `
539
+ + `hmgr dir add ${wurzel}.`);
540
+ this.ziel = ziel;
541
+ this.wurzel = wurzel;
542
+ }
543
+ }
527
544
  /**
528
545
  * Übernehmen (E64) an einem Ort, der keine geteilte Konfigurationsdatei kennt: ein Harness ohne
529
546
  * diese Fähigkeit (Hermes kennt keine geteilte Datei) oder eine Ebene ohne sie (bei Claude Code hat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",