<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CosmoCode Blog</title><description>Posts from the CosmoCode team.</description><link>https://www.cosmocode.de/</link><item><title>Ein KI-Agent, der Ihre Software von innen kennt</title><link>https://www.cosmocode.de/en/blog/agoh/20260930-james-backend-butler/</link><guid isPermaLink="true">https://www.cosmocode.de/en/blog/agoh/20260930-james-backend-butler/</guid><description>James, der KI-Butler fürs Backend: ein Chat-Assistent für TYPO3 und Individualanwendungen, der Fragen beantwortet, Daten auswertet und Abläufe erklärt.</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;ein-ki-agent-der-ihre-software-von-innen-kennt&quot;&gt;Ein KI-Agent, der Ihre Software von innen kennt&lt;/h1&gt;
&lt;p&gt;In der Softwareentwicklung arbeiten wir täglich mit KI-Agenten und wissen, wie nützlich diese Werkzeuge sein können.
Doch auch in anderen Bereichen sind diese Systeme nützlich. Unser Wiki zum Beispiel hat einen eingebauten
Agenten. Er beantwortet Fragen mit Wissen aus dem Wiki und mit Daten aus unserer übrigen internen Software. Für einen
Kunden haben wir ein ähnliches System gebaut, das komplexe Auswertungen aus der Vertragsdatenbank erstellt.&lt;/p&gt;
&lt;p&gt;Da lag die Idee nahe, einen solchen Agenten in jede Webanwendung einzubauen, die wir für unsere Kunden betreuen: ins
TYPO3-CMS genauso wie in individuell entwickelte Fachanwendungen. Überall dort, wo Mitarbeitende im Backend Inhalte und
Daten pflegen.&lt;/p&gt;
&lt;p&gt;Das Ergebnis ist James - Ihr Butler fürs Backend.&lt;/p&gt;
&lt;video src=&quot;https://www.cosmocode.de/_astro/james_de_web.C9N5VykH.mp4&quot; poster=&quot;https://www.cosmocode.de/_astro/james_de_poster.Dm0Kvjep.jpg&quot; width=&quot;1920&quot; height=&quot;1080&quot; controls preload=&quot;metadata&quot;&gt;&lt;p&gt;Ein kurzes Video, das James vorstellt.&lt;/p&gt;&lt;/video&gt;
&lt;h2 id=&quot;was-ist-james&quot;&gt;Was ist James?&lt;/h2&gt;
&lt;p&gt;Wir schreiben nicht nur Software für unsere Kunden, sondern unterstützen sie auch im laufenden Betrieb der Anwendung.
Denn egal, wie gut die Software ist: Im Betrieb ergeben sich Fragen. Hier setzt James an. James ist ein
Chat-Assistent, der direkt in die Anwendung eingebaut ist und dort hilft, wo gearbeitet wird: im Backend. Er ist für
die Menschen gedacht, die sich in der Anwendung anmelden, um Inhalte und Daten zu pflegen, zum Beispiel den
Redakteur:innen in einem CMS.&lt;/p&gt;
&lt;p&gt;James ist also kein Chatbot für die Besucher:innen der Website. Wer die Seiten liest oder im Shop einkauft, bekommt ihn
nie zu sehen. Er ist für die Menschen da, die mit der Anwendung arbeiten, nicht für die, die sie von außen nutzen.&lt;/p&gt;
&lt;p&gt;James hat Werkzeuge, um den Quellcode der Applikation zu lesen, und kann in die Live-Datenbank schauen. Zudem kann er
über den Browser der angemeldeten Nutzer:in “sehen”, was dargestellt wird. Zudem lassen sich weitere
Informationen wie das Nutzerhandbuch oder Fachwissen hinzufügen. Damit hat James dieselben Werkzeuge zur
Verfügung, die auch wir nutzen würden, um eine Anfrage zu beantworten.&lt;/p&gt;
&lt;p&gt;Wichtig ist dabei, dass der Agent nichts “kaputt” machen kann. Er darf zwar den Quellcode und die Datenbank lesen, hat
aber keine Werkzeuge, um Änderungen durchzuführen. Gegebenenfalls kann er natürlich erklären, wie eine Anpassung
durch die Nutzer:in selbst vorgenommen werden kann.&lt;/p&gt;
&lt;h2 id=&quot;wofür-ist-das-gut&quot;&gt;Wofür ist das gut?&lt;/h2&gt;
&lt;p&gt;Das Tolle an Agentensystemen ist, wie flexibel sie mit nur einer Handvoll Werkzeugen einsetzbar sind. Für James ergeben
sich einige Anwendungsfelder.&lt;/p&gt;
&lt;h3 id=&quot;direktes-hilfesystem&quot;&gt;Direktes Hilfesystem&lt;/h3&gt;
&lt;p&gt;Fragen zur Bedienung und zum Zustand der Applikation sind die naheliegendsten Aufgaben.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Wie lege ich eine neue Veranstaltung an, die nur für angemeldete Mitglieder sichtbar ist?”&lt;/li&gt;
&lt;li&gt;“Warum erscheint mein Artikel von gestern nicht auf der Startseite?”&lt;/li&gt;
&lt;li&gt;“Wie kann ich einer Kollegin Schreibrechte für den Pressebereich geben?”&lt;/li&gt;
&lt;li&gt;“Warum hat Bestellung 4711 noch keine Rechnung bekommen?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;James bietet hier eine direkte Zeitersparnis. Statt sich auf der Suche nach der richtigen Stelle durch die Anwendung zu
klicken oder am Ende sogar bei uns nachfragen zu müssen, kann James die Antwort direkt liefern.&lt;/p&gt;
&lt;h3 id=&quot;spontane-auswertungen&quot;&gt;Spontane Auswertungen&lt;/h3&gt;
&lt;p&gt;Oftmals möchte man etwas über die Daten in der eigenen Software wissen, das über die Oberfläche in dieser Form nicht
ausgegeben wird.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Welche Kunden haben im letzten Jahr mehr als dreimal bestellt, aber seit sechs Monaten nicht mehr?”&lt;/li&gt;
&lt;li&gt;“Wie hat sich die Zahl der Neuanmeldungen pro Monat seit Januar entwickelt, getrennt nach Region?”&lt;/li&gt;
&lt;li&gt;“Welche Seiten wurden seit über zwei Jahren nicht mehr bearbeitet und von wem stammen sie?”&lt;/li&gt;
&lt;li&gt;“Wie viele Verträge laufen im nächsten Quartal aus und welchen Umsatz machen sie zusammen aus?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;James kann die Daten dabei nicht nur zusammensuchen, sondern über eine eingebaute Bibliothek für Balken- und
Liniendiagramme auch gleich grafisch darstellen.&lt;/p&gt;
&lt;div class=&quot;CenterAlign&quot;&gt; &amp;lt;p&amp;gt;&amp;lt;img src=&amp;quot;/_astro/james-chart.CG5XxqFf_1umKgd.webp&amp;quot; alt=&amp;quot;James zeigt die Neuanmeldungen pro Monat und Region als Balkendiagramm&amp;quot; loading=&amp;quot;lazy&amp;quot; decoding=&amp;quot;async&amp;quot; width=&amp;quot;560&amp;quot; height=&amp;quot;820&amp;quot;&amp;gt;&amp;lt;/p&amp;gt; &lt;/div&gt;
&lt;p&gt;James ist dabei für die schnelle Frage zwischendurch gedacht, nicht als Ersatz für ein richtiges Auswertungswerkzeug.
Zeigt sich, dass eine Auswertung immer wieder gebraucht wird, ist es sinnvoller, die Anwendung entsprechend zu
erweitern.&lt;/p&gt;
&lt;h3 id=&quot;der-große-überblick&quot;&gt;Der große Überblick&lt;/h3&gt;
&lt;p&gt;Eine eingebaute Bibliothek für Schaubilder (Mermaid) erlaubt es James, auch mal eben Datenflüsse oder Zusammenhänge
grafisch zu visualisieren.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Zeig mir, welche Schritte eine Bestellung vom Warenkorb bis zum Versand durchläuft.”&lt;/li&gt;
&lt;li&gt;“Welche Rollen gibt es im System und wer darf was?”&lt;/li&gt;
&lt;li&gt;“Wie hängen Produkte, Kategorien und Angebote zusammen?”&lt;/li&gt;
&lt;li&gt;“Was passiert alles, wenn sich jemand für den Newsletter anmeldet?”&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;CenterAlign&quot;&gt; &amp;lt;p&amp;gt;&amp;lt;img src=&amp;quot;/_astro/james-diagram.CNp1yQmG_28R5wv.webp&amp;quot; alt=&amp;quot;James zeigt den Ablauf einer Bestellung vom Warenkorb bis zum Versand als Diagramm&amp;quot; loading=&amp;quot;lazy&amp;quot; decoding=&amp;quot;async&amp;quot; width=&amp;quot;560&amp;quot; height=&amp;quot;1180&amp;quot;&amp;gt;&amp;lt;/p&amp;gt; &lt;/div&gt;
&lt;h3 id=&quot;fehlermeldungen-und-änderungswünsche&quot;&gt;Fehlermeldungen und Änderungswünsche&lt;/h3&gt;
&lt;p&gt;Keine Software ist perfekt und manchmal stoßen unsere Kunden auf unvorhergesehene Probleme. James kann das Problem
analysieren und einen technischen Bericht für CosmoCode erstellen - egal, ob es sich um einen Fehler oder eine fehlende
Funktion handelt.&lt;/p&gt;
&lt;p&gt;Wir sparen Zeit bei der Analyse des Problems und unsere Kunden Geld, weil die Fehlerbehebung schneller geht.&lt;/p&gt;
&lt;h2 id=&quot;software&quot;&gt;Software&lt;/h2&gt;
&lt;p&gt;James ist in Go geschrieben und enthält in einem einzigen Programm alles, was nötig ist: einen eigenen Webserver, das
Backend, welches die Werkzeuge bereitstellt und mit dem Sprachmodell (LLM) spricht, sowie das Frontend für die Ausgabe.
Dazu gehört eine Konfigurationsdatei, die festlegt, wo der Quellcode und andere Informationsquellen zu finden sind und wie
die Datenbank anzusprechen ist.&lt;/p&gt;
&lt;p&gt;Dieser Aufbau macht James unabhängig von der Programmiersprache und dem Framework der eigentlichen Anwendung, die es zu
unterstützen gilt. Egal, ob ein PHP-basiertes TYPO3 oder eine individuelle Django-App: Am Ende muss nur ein kleiner
JavaScript-Schnipsel integriert werden, und James steht allen authentifizierten Nutzer:innen zur Verfügung.&lt;/p&gt;
&lt;p&gt;Die Konfiguration und Integration übernehmen wir natürlich.&lt;/p&gt;
&lt;h2 id=&quot;was-noch-kommt&quot;&gt;Was noch kommt&lt;/h2&gt;
&lt;p&gt;Hat man James erst einmal im Einsatz, ergeben sich schnell neue Ideen. Mit weiteren Datenquellen könnte James zum
Beispiel auch solche Fragen beantworten:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Welche Seiten werden am häufigsten besucht, sind aber seit Jahren nicht aktualisiert worden?” (mit Daten aus Matomo
oder Google Analytics)&lt;/li&gt;
&lt;li&gt;“Bei welchen Suchbegriffen verlieren wir gerade Plätze bei Google?” (mit Daten aus der Google Search Console)&lt;/li&gt;
&lt;li&gt;“Welche Kunden aus dem Shop haben ein offenes Ticket im Support-System?” (mit Anbindung an weitere Systeme)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Welche Erweiterungen wir zuerst umsetzen, entscheiden die Rückmeldungen unserer Kunden. Sie haben eine Idee oder
möchten James in Ihrer Anwendung ausprobieren? Sprechen Sie uns an!&lt;/p&gt;</content:encoded><author>gohr@cosmocode.de (Andreas Gohr)</author></item><item><title>KI klaut unseren Traffic, und wir holen ihn uns mit KI zurück</title><link>https://www.cosmocode.de/en/blog/dhue/20260924-ki-traffic-zurueckholen/</link><guid isPermaLink="true">https://www.cosmocode.de/en/blog/dhue/20260924-ki-traffic-zurueckholen/</guid><description>Weniger Besuche durch KI-Antworten: Wie wir KI mit Search-Console-Daten, Inhalten und Änderungshistorie verknüpfen, um gezielt gegenzusteuern.</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Wir spüren den Traffic-Rückgang deutlich. Dreißig bis vierzig Prozent weniger Besuche sind kein Ausschlag, das ist ein
Bruch. KI-basierte Antworten nutzen unsere Inhalte, ohne dass jemand unsere Seiten besucht. Verhindern können wir das
nicht. Aber wir haben uns gefragt, ob wir genau diese Technologie nutzen können, um gegenzusteuern.&lt;/p&gt;
&lt;p&gt;Was wir aktuell ausprobieren, ist kein schnellerer Reporting-Zyklus, sondern etwas anderes: Wir verknüpfen KI mit den
echten Suchdaten, Inhalten und der Änderungshistorie einer Website, um Einsichten zu bekommen, deren Fragen wir früher gar nicht
gestellt hätten. So können wir nicht nur Änderungen dokumentieren, sondern rückwirkend bewerten, ob sie wirklich etwas
gebracht haben.&lt;/p&gt;
&lt;h2 id=&quot;fragen-stellen-wenn-sie-entstehen&quot;&gt;Fragen stellen, wenn sie entstehen&lt;/h2&gt;
&lt;p&gt;Suchmaschinenoptimierung war immer ein iterativer Prozess. Man analysiert die Sichtbarkeit einer Website, identifiziert
Schwächen, nimmt Änderungen vor und schaut einige Wochen oder Monate später, was daraus geworden ist.&lt;/p&gt;
&lt;p&gt;Das Problem dabei ist, dass solche Analysen vorbereitet werden müssen. Daten müssen gesammelt, Berichte definiert und
Auswertungen programmiert werden. Neue Fragen bedeuten neuen Aufwand. Und häufig merkt man erst im laufenden Prozess,
dass man eigentlich etwas ganz anderes wissen möchte.&lt;/p&gt;
&lt;p&gt;Genau an dieser Stelle verändert KI für uns die Arbeitsweise. Wir müssen Analysefragen nicht mehr im Voraus festzurren.
Wir können sie dann stellen, wenn sie sich aus einer konkreten Beobachtung heraus ergeben und können sie dann auf die vorhandene
Historie anwenden.&lt;/p&gt;
&lt;p&gt;Dafür verbinden wir die Daten der Google Search Console mit den Inhalten und Strukturen einer Website. Bei
dateibasierten Systemen können zusätzlich Änderungen aus der Git-Historie einfließen, bei datenbankbasierten Systemen
die jeweiligen Änderungsprotokolle. Das LLM einer KI-Anwendung bekommt so einen holistischen Blick auf die Inhalte und kann Zusammenhänge erkennen, die wir nicht oder nur mit viel Rechercheaufwand sehen.&lt;/p&gt;
&lt;h2 id=&quot;vom-befund-zum-to-do&quot;&gt;Vom Befund zum To-do&lt;/h2&gt;
&lt;p&gt;Analysen helfen uns allerdings nur, wenn wir daraus etwas tun können. Das System sucht deshalb gezielt nach
Verbesserungspotenzialen: Offensichtliche Schwächen, aber auch weniger sichtbare Chancen. Seiten, die knapp vor den vorderen Positionen stehen,
Themen, die noch nicht gut genug beantwortet sind, Kannibalisierungen oder veraltete Inhalte.&lt;/p&gt;
&lt;p&gt;Daraus entstehen To-dos, die nach der Umsetzung wieder in die Historie einfließen. Diese Spur ist nicht in erster Linie
für uns gedacht, sondern für das System selbst. Es kann später nachvollziehen, warum etwas geändert wurde, was danach
passiert ist und wie sich Kennzahlen entwickelt haben. So entsteht eine Erfahrungsbasis, auf die das
System bei künftigen Analysen zurückgreifen kann.&lt;/p&gt;
&lt;h2 id=&quot;wann-funktioniert-ein-neuer-artikel&quot;&gt;Wann funktioniert ein neuer Artikel?&lt;/h2&gt;
&lt;p&gt;Ein Beispiel hat uns besonders deutlich gezeigt, was dadurch möglich wird. Wir wollten eine Frage beantworten, die uns
eigentlich schon immer begleitet: Wann kann man überhaupt beurteilen, ob ein neuer Artikel funktioniert? Wann ist er im
Index? Wann kommen die ersten Impressionen und Klicks? Ab wann sind Daten belastbar genug, um Entscheidungen zu treffen?
Die Search Console liefert einzelne Datenpunkte, aber keine direkte Antwort darauf.&lt;/p&gt;
&lt;p&gt;Das LLM hat selbstständig eine Kohortenanalyse erstellt. Alle neuen Webseiten wurden automatisch mit dem Median der gleich alten Seiten verglichen – so konnten wir sehen, ob sich ein Artikel besser, schlechter oder im Rahmen entwickelt.&lt;/p&gt;
&lt;p&gt;Neu war für uns dabei weniger das statistische Verfahren an sich, sondern wie schnell die KI die passende Analyse für unsere Fragestellung ausgewählt und umgesetzt hat … und wie einfach sich diese Analyse anpassen lässt, wenn wir andere Schwerpunkte setzen wollen.&lt;/p&gt;
&lt;h2 id=&quot;wie-es-weitergeht&quot;&gt;Wie es weitergeht&lt;/h2&gt;
&lt;p&gt;Die nächste Herausforderung liegt für uns darin, die Analyse mit der Struktur der Website zu verbinden. Gute Websites
bilden fachliche Zusammenhänge ab und verwenden unterschiedliche Dokumenttypen und semantische Modelle. Diese Semantik
muss verstanden und technisch angebunden werden. Nicht nur zum Lesen, sondern perspektivisch auch, um freigegebene
Änderungen gezielt zurückzuschreiben.&lt;/p&gt;
&lt;p&gt;Daran arbeiten wir gerade mit unserem Werkzeug Trailmarks. Aktuell setzen wir es vor allem auf eigenen Websites ein. Im
nächsten Schritt wollen wir die gewonnenen Erfahrungen auf unsere &lt;a href=&quot;https://www.cosmocode.de/de/leistungen/cms/typo3/&quot;&gt;TYPO3-Projekte&lt;/a&gt; übertragen.&lt;/p&gt;</content:encoded><author>huettemann@cosmocode.de (Detlef Hüttemann)</author></item><item><title>Tarifrechner testen mit PICT</title><link>https://www.cosmocode.de/en/blog/dhue/20260615-pict-einfuehrung/</link><guid isPermaLink="true">https://www.cosmocode.de/en/blog/dhue/20260615-pict-einfuehrung/</guid><description>Tarifrechner testen mit PICT: Kombinatorische Testdatengenerierung für Versicherungen</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Finanzprodukte wie beispielsweise Versicherungen berechnen Beiträge mit komplexen mathematischen Verfahren. Je nach
Produkt fließen mehr oder weniger viele Einflussgrößen in die Berechnung ein, um unterschiedliche Risiken angemessen zu
berücksichtigen.
Wenn diese Berechnungsverfahren in Softwarelösungen implementiert werden, müssen die Algorithmen gegen die fachlichen
Vorgaben getestet werden. Das stellt Softwareingenieure vor eine besondere Herausforderung, denn Berechnungsfunktionen
verhalten sich häufig nicht überall stetig im mathematischen Sinne. Schon geringe Änderungen an den Eingabewerten können
größere Änderungen an den Prämien hervorrufen.&lt;/p&gt;
&lt;h2 id=&quot;funktionen-mit-sprüngen&quot;&gt;Funktionen mit Sprüngen&lt;/h2&gt;
&lt;p&gt;Die Ursache liegt häufig im Modell der Produktentwicklung. Versicherungsprodukte werden von Aktuaren oft zunächst mit
Excel entwickelt. In Excel ist es vergleichsweise einfach, das Verhalten von Parametern über Lookup-Tabellen zu
definieren. Diese Lookups sind mathematisch betrachtet Stützstellen einer Funktion. Der Lookup in Excel entspricht dann
häufig einer Treppenfunktion.
Warum ist das wichtig? Als Softwaretester liegt es nahe, ein konsistentes Verhalten einer Anwendung zu erwarten. Wenn
ich das Verhalten der Anwendung an den Extremwerten und in der Mitte teste, könnte ich erwarten, dass sie sich auch an
den Zwischenwerten entsprechend gutartig verhält.
Genau das ist bei einem stützstellenbasierten Ansatz mit Excel-Lookup-Algorithmen aber nicht zwingend der Fall.
Zusätzlich wird es komplexer, wenn solche Variablen im Excel-Algorithmus mit Bedingungen verknüpft werden, wie zum
Beispiel:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Wenn Tarifgruppe &amp;gt; 100 und Schadenquote &amp;gt; 1 dann Tarifgruppe -= 10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mit Tests nach Gefühl kommt man hier nicht weiter. Das macht man ja sowieso nie — oder man nennt es exploratives Testen.
Ohne Kenntnis des tatsächlichen Algorithmus lassen sich die neuralgischen Parameterkombinationen, bei denen die
Berechnungsfunktion ihr Verhalten ändert, nicht zuverlässig vorhersagen.
&lt;strong&gt;Die Testabdeckung muss entsprechend hoch angesetzt werden.&lt;/strong&gt;
Testen ist allerdings teuer. Besonders bei Ende-zu-Ende-Tests ist die Durchführungsdauer hoch. Eine Automatisierung der
Testdurchführung kann zwar den Personaleinsatz beim Testen reduzieren, erhöht aber die Last auf dem System. Auch das
verursacht Kosten. Da das Testen in einen Projektrahmen eingebettet ist, führt eine längere Testdauer außerdem zu einer
verlängerten Projektdauer und damit zu Zusatzkosten durch die längere Bereithaltung des Projektteams.
Während sich die Werte einer einzelnen Variablen noch vergleichsweise gut bestimmen lassen, zum Beispiel durch
Extraktion der Wertetabellen aus den Lookups, ist das Zusammenspiel mehrerer Variablen deutlich schwieriger zu
überschauen. Eine Analyse des Referenzrechners in Bezug auf abhängige Variablen ist mit KI zwar gut machbar, löst das
Problem aber nicht vollständig. Denn im Soll-Ist-Vergleich zwischen Referenzsystem und dem zu testenden System bleibt
das Testsystem eine Blackbox, die durch bewusste oder unbewusste Programmierentscheidungen ein anderes Verhalten
aufweisen kann.&lt;/p&gt;
&lt;h2 id=&quot;paarweise-unabhängige-kombinationen--pict&quot;&gt;Paarweise unabhängige Kombinationen – PICT&lt;/h2&gt;
&lt;p&gt;Ein ökonomischer Weg, eine sinnvolle Testabdeckung in den Parameterkombinationen zu erreichen, ist das PICT-Verfahren.
PICT steht für Pairwise Independent Combinatorial Testing. Der Begriff beschreibt den Ansatz recht genau: Ausgehend von
den Testwerten der Einzelparameter sorgt das PICT-Verfahren dafür, dass für je zwei Parameter alle Wertekombinationen
dieser beiden Parameter abgedeckt sind.
Da ein Testdatensatz nicht nur die beiden jeweils betrachteten Parameter enthalten muss, sondern auch die übrigen
Parameter, entstehen automatisch auch bestimmte 3er- und 4er-Kombinationen.
PICT ersetzt damit nicht die fachliche Teststrategie. Es hilft aber, eine große Menge möglicher Eingabekombinationen
systematisch auf eine kleinere, besser handhabbare Menge von Testfällen zu reduzieren.&lt;/p&gt;
&lt;h2 id=&quot;beispiel&quot;&gt;Beispiel&lt;/h2&gt;
&lt;p&gt;Ein Beispiel soll das PICT-Verfahren verdeutlichen.
Nehmen wir ein Versicherungsprodukt, beispielsweise eine Hausratversicherung, mit den prämienrelevanten Einflussgrößen
Laufzeit, Wohnfläche, Schmuck und einem Bonusrabatt.
Wir halten die Problemgröße bewusst klein, um die kombinatorische Entwicklung intuitiv einschätzen zu können. Für jeden
dieser Parameter legen wir die zu testenden Werte fest.&lt;/p&gt;
&lt;h3 id=&quot;parameter-und-werte-des-beispiels&quot;&gt;Parameter und Werte des Beispiels&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Laufzeit&lt;/strong&gt;: 1, 3, 5 Jahre&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wohnfläche&lt;/strong&gt;: 30 qm, 50 qm, 100 qm, 200 qm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Schmuck mitversichert im Wert von&lt;/strong&gt;: 0 €, 100 €, 1.000 €&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bonusrabatt&lt;/strong&gt;: ja, nein
Rechnerisch ergeben sich 72 Kombinationen. Eine vollständige Abdeckung hätte also 72 Testdatensätze.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/pict-beispielprodukt.DpW9LMen_ZRhNSJ.webp&quot; alt=&quot;Beispiel Versicherungsprodukt&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;426&quot; height=&quot;160&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;parameterpaare-und-deren-kombinationen&quot;&gt;Parameterpaare und deren Kombinationen&lt;/h3&gt;
&lt;p&gt;Das PICT-Verfahren betrachtet nun alle Parameterpaare und sorgt dafür, dass in diesen Paaren jeweils alle
Wertekombinationen abgedeckt sind.
Laufzeit hat 3 Werte, Wohnfläche 4 Werte. Für die Kombination Laufzeit × Wohnfläche ergeben sich also 12 Kombinationen.
Bricht man dies für alle Parameterpaare herunter, kommt man auf 53 Paar-Kombinationen. Das ist allerdings noch naiv
gerechnet, weil die übrigen Parameter in realen Testdatensätzen bereits mitverteilt werden und dadurch mehrere
Paar-Kombinationen gleichzeitig abgedeckt werden können.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/pict-kombinationen.KkcUSdcb_10yUFg.webp&quot; alt=&quot;Parameterpaare&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1326&quot; height=&quot;356&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;datenpacking-im-pict-verfahren&quot;&gt;Datenpacking im PICT-Verfahren&lt;/h3&gt;
&lt;p&gt;Das PICT-Verfahren versucht nun, die Kombinationen in gemeinsame Testdatensätze zu packen.
Das Ergebnis sind in diesem Beispiel 14 dicht gepackte Testdatensätze.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/pict-alle-testfaelle.COA3T6OK_1hEDPS.webp&quot; alt=&quot;Alle Testfälle&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;425&quot; height=&quot;437&quot;&gt;&lt;/p&gt;
&lt;p&gt;14 PICT-Testfälle bei 72 rechnerischen Kombinationen: In diesem kleinen Beispiel hat das noch keine große praktische
Bedeutung. Interessant wird es, wenn wir mehr Werte oder mehr Parameter zulassen.
Der Einfachheit halber nehmen wir an, dass alle Parameter genau 10 Werteausprägungen haben.
Ein Testlauf des PICT-Verfahrens zeigt, dass die Zahl der PICT-Testdatensätze bei der weiteren Hinzunahme von Parametern
nur moderat anwächst.&lt;/p&gt;
&lt;p&gt;In der Praxis wächst die Zahl der Testfälle bei paarweiser Abdeckung meist deutlich langsamer als die vollständige
kartesische Produktmenge. Das ist der entscheidende wirtschaftliche Vorteil des Verfahrens: Jeder zusätzliche Parameter
erhöht den Testumfang, aber nicht in dem Maße, wie es bei einer vollständigen Kombination aller Werte der Fall wäre.&lt;/p&gt;
&lt;p&gt;Die grüne Linie in der folgenden Grafik zeigt die Summe aller 2-er Kombinationen. die blaue Linie zeigt die Anzahl der
pairwise Kombinationen, die rote Linie die Anzahl der 3-wise.&lt;/p&gt;
&lt;p&gt;Auf der horizontalen Achse ist die Anzahl der Parameter eingetragen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/pict-verlauf-2-3-wise.ZlyWW9mh_Z2dd1Tw.webp&quot; alt=&quot;Asymptotisches Verhalten&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;2888&quot; height=&quot;1500&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;3-wise-und-sub-models&quot;&gt;3-wise und Sub-Models&lt;/h2&gt;
&lt;p&gt;Wenn bekannt ist, dass die Parameterabhängigkeiten im Wesentlichen von mehr als zwei Parametern abhängen, kann das
Verfahren entsprechend erweitert werden — mit dem Preis steigender Testfallzahlen.
Anstatt hierbei alles auf 3-wise umzustellen, kann man über Sub-Models das 3-wise-Verhalten für einzelne
Parametergruppen erzwingen:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# PICT-Beispiel: Versicherungsprodukt mit 7 Parametern&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Standard: paarweise Kombinationen über alle Parameter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Aufruf z. B.: pict versicherung.txt /o:2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Laufzeit: 1 Jahr, 3 Jahre, 5 Jahre&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Wohnfläche: 30 qm, 50 qm, 100 qm, 200 qm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Schmuck: 0 EUR, 100 EUR, 1000 EUR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Bonusrabatt: ja, nein&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Zahlweise: monatlich, vierteljährlich, jährlich&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Selbstbeteiligung: 0 EUR, 150 EUR, 300 EUR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tarifvariante: Basis, Komfort, Premium&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Für die beitragsrelevanten Kernparameter wird zusätzlich 3-wise Coverage erzwungen.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{ Laufzeit, Wohnfläche, Schmuck, Selbstbeteiligung } @ 3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Damit werden weiterhin alle Parameter grundsätzlich paarweise kombiniert. Für die definierte Gruppe aus Laufzeit,
Wohnfläche, Schmuck und Selbstbeteiligung wird zusätzlich sichergestellt, dass alle 3er-Kombinationen innerhalb dieser
Gruppe abgedeckt sind.
Das ist besonders hilfreich, wenn fachlich bekannt ist, dass bestimmte Parameter gemeinsam auf die Prämie wirken, man
aber nicht den gesamten Testraum auf 3-wise anheben möchte.&lt;/p&gt;
&lt;h2 id=&quot;negative-tests-mit-ungültigen-werten&quot;&gt;Negative Tests mit ungültigen Werten&lt;/h2&gt;
&lt;p&gt;PICT eignet sich nicht nur für gültige Kombinationen, sondern kann auch bei negativen Tests helfen. Damit sind Testfälle
gemeint, bei denen bewusst ungültige oder fachlich fehlerhafte Werte verwendet werden, um die Validierung des Systems zu
prüfen. Bei einem Versicherungsprodukt könnte das zum Beispiel eine negative Wohnfläche, eine Laufzeit von 0 Jahren oder
ein nicht erlaubter Schmuckwert sein.
Solche Werte können in PICT mit einer Tilde &lt;code&gt;~&lt;/code&gt; gekennzeichnet werden:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Laufzeit: 1 Jahr, 3 Jahre, 5 Jahre, ~0 Jahre&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Wohnfläche: 30 qm, 50 qm, 100 qm, 200 qm, ~-10 qm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Schmuck: 0 EUR, 100 EUR, 1000 EUR, ~-500 EUR&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der Vorteil dieser Kennzeichnung ist, dass PICT ungültige Werte kontrolliert behandelt. &lt;strong&gt;In der Regel wird pro Testfall
nur ein negativer Wert verwendet&lt;/strong&gt;. Das ist wichtig, weil ein Testfall mit mehreren ungültigen Eingaben oft schwer
auszuwerten ist: Wenn gleichzeitig die Laufzeit, die Wohnfläche und der Schmuckwert falsch sind, ist nicht mehr
eindeutig erkennbar, welche Validierung genau fehlgeschlagen ist.
Negative Tests sollten deshalb bewusst sparsam eingesetzt werden. Sie ersetzen keine vollständige Validierungsstrategie,
können aber sehr gut helfen, typische Fehleingaben systematisch in die generierten Testfälle aufzunehmen.&lt;/p&gt;
&lt;h2 id=&quot;gewichtung-von-werten&quot;&gt;Gewichtung von Werten&lt;/h2&gt;
&lt;p&gt;Nicht alle Werte sind in der Praxis gleich wichtig. Manche Tarifvarianten werden besonders häufig verkauft, bestimmte
Laufzeiten kommen öfter vor, und einzelne Kombinationen sind geschäftlich relevanter als andere. PICT ermöglicht es,
solche Werte zu gewichten.
Eine Gewichtung wird direkt hinter dem Wert angegeben:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tarifvariante: Basis, Komfort, Premium (3)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Zahlweise: monatlich (2), vierteljährlich, jährlich&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In diesem Beispiel wird &lt;code&gt;Premium&lt;/code&gt; stärker bevorzugt als &lt;code&gt;Basis&lt;/code&gt; oder &lt;code&gt;Komfort&lt;/code&gt;. Ebenso wird die monatliche Zahlweise
häufiger berücksichtigt als die anderen Zahlweisen. Das bedeutet allerdings nicht, dass PICT daraus eine exakte
statistische Verteilung erzeugt. Die Gewichtung beeinflusst die Auswahl der Werte, aber das eigentliche Ziel bleibt
weiterhin die kombinatorische Abdeckung.
Gewichtung ist besonders dann sinnvoll, wenn bestimmte Werte im Test stärker sichtbar sein sollen, ohne dass man dafür
eigene Sondertestfälle schreiben möchte. Sie kann helfen, realistischere Testdaten zu erzeugen, etwa wenn ein Produkt in
der Praxis überwiegend mit bestimmten Optionen abgeschlossen wird. Gleichzeitig sollte man Gewichtung nicht mit
fachlicher Priorisierung verwechseln: Kritische Kombinationen sollten weiterhin explizit modelliert werden, zum Beispiel
über Sub-Models mit höherer Teststärke oder über zusätzliche manuelle Testfälle.&lt;/p&gt;
&lt;h2 id=&quot;ausschluss-ungültiger-kombinationen&quot;&gt;Ausschluss ungültiger Kombinationen&lt;/h2&gt;
&lt;p&gt;Ein Testmodell enthält oft Kombinationen, die theoretisch möglich wären, fachlich aber keinen Sinn ergeben. Solche
Kombinationen sollten nicht als Testfälle erzeugt werden, weil sie das Ergebnis verfälschen und unnötigen Aufwand
verursachen. PICT erlaubt deshalb den Ausschluss ungültiger Kombinationen über Constraints.
Ein Beispiel: Ein hoher Schmuckwert ist möglicherweise nur in der Premium-Variante erlaubt.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IF [Schmuck] = &amp;quot;1000 EUR&amp;quot; THEN [Tarifvariante] = &amp;quot;Premium&amp;quot;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oder: Ein Bonusrabatt soll erst ab einer längeren Laufzeit möglich sein.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IF [Bonusrabatt] = &amp;quot;ja&amp;quot; THEN [Laufzeit] &amp;lt;&amp;gt; &amp;quot;1 Jahr&amp;quot;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Solche Regeln sorgen dafür, dass PICT keine fachlich unzulässigen Testfälle erzeugt. Das ist besonders wichtig, wenn
Testfälle später automatisiert ausgeführt oder an Fachbereiche zur Abnahme gegeben werden. Ein Testfall, der in der
Realität gar nicht vorkommen darf, ist sonst schwer zu bewerten: Scheitert er wegen eines Fehlers im System oder weil
der Testfall selbst ungültig ist?
Wichtig ist aber die Unterscheidung zwischen „ungültig“ und „ungewöhnlich“. Eine kleine Wohnfläche mit einem
Premiumtarif mag selten sein, kann aber fachlich durchaus erlaubt sein. Eine solche Kombination sollte nicht
ausgeschlossen werden. Constraints sollten nur dort verwendet werden, wo eine Kombination tatsächlich fachlich verboten
ist.
Ein vollständiger Ausschnitt aus einer PICT-Datei könnte dann so aussehen:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Laufzeit: 1 Jahr, 3 Jahre, 5 Jahre&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Wohnfläche: 30 qm, 50 qm, 100 qm, 200 qm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Schmuck: 0 EUR, 100 EUR, 1000 EUR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Bonusrabatt: ja, nein&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Zahlweise: monatlich, vierteljährlich, jährlich&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Selbstbeteiligung: 0 EUR, 150 EUR, 300 EUR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tarifvariante: Basis, Komfort, Premium&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IF [Schmuck] = &amp;quot;1000 EUR&amp;quot; THEN [Tarifvariante] = &amp;quot;Premium&amp;quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IF [Bonusrabatt] = &amp;quot;ja&amp;quot; THEN [Laufzeit] &amp;lt;&amp;gt; &amp;quot;1 Jahr&amp;quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IF [Tarifvariante] = &amp;quot;Basis&amp;quot; THEN [Selbstbeteiligung] &amp;lt;&amp;gt; &amp;quot;0 EUR&amp;quot;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Durch solche Ausschlüsse wird das Modell fachlich genauer. PICT erzeugt dann nicht einfach beliebige Kombinationen,
sondern Kombinationen, die innerhalb der definierten Geschäftsregeln gültig sind.&lt;/p&gt;
&lt;h2 id=&quot;typischer-ablauf-in-einem-testprojekt&quot;&gt;Typischer Ablauf in einem Testprojekt&lt;/h2&gt;
&lt;p&gt;In einem konkreten Testprojekt beginnt die Arbeit nicht mit PICT selbst, sondern mit der fachlichen Analyse des
Tarifrechners. Zunächst müssen die relevanten Eingabeparameter bestimmt werden. Danach werden für jeden Parameter
sinnvolle Werteklassen gebildet: Grenzwerte, typische Standardwerte, fachliche Sonderfälle und Werte unmittelbar vor
oder nach einem Sprung in der Berechnungslogik.&lt;/p&gt;
&lt;p&gt;Im nächsten Schritt werden fachliche Einschränkungen ergänzt. Nicht jede theoretisch mögliche Kombination darf auch im
Testmodell vorkommen. Ungültige Kombinationen werden daher über Constraints ausgeschlossen. Für besonders kritische
Parametergruppen kann zusätzlich eine höhere Kombinationsstärke definiert werden.&lt;/p&gt;
&lt;p&gt;Erst danach werden mit PICT die eigentlichen Testdaten erzeugt. Diese Testdaten können anschließend gegen den
Referenzrechner, zum Beispiel einen Excel-Rechner, ausgeführt werden. Die dort berechneten Sollwerte werden dann mit den
Ergebnissen des Zielsystems verglichen. Auf diese Weise entsteht ein nachvollziehbarer Soll-Ist-Vergleich zwischen
fachlicher Referenz und technischer Implementierung.&lt;/p&gt;
&lt;h2 id=&quot;grenzen-des-verfahrens&quot;&gt;Grenzen des Verfahrens&lt;/h2&gt;
&lt;p&gt;PICT reduziert die Anzahl der Testfälle, aber nicht die Verantwortung für ein gutes Testmodell. Die Qualität der
erzeugten Testfälle hängt wesentlich davon ab, ob die richtigen Parameter ausgewählt, passende Werteklassen gebildet und
fachliche Abhängigkeiten korrekt modelliert wurden.&lt;/p&gt;
&lt;p&gt;Das Verfahren ersetzt außerdem keine Grenzwertanalyse, keine fachlichen End-to-End-Szenarien und keine gezielten
Regressionstests für bereits bekannte Fehler. Gerade bei Tarifrechnern bleiben zusätzliche Tests an fachlich relevanten
Schwellenwerten wichtig, weil dort die größten Sprünge in der Berechnung auftreten können.&lt;/p&gt;
&lt;p&gt;PICT ist deshalb am stärksten, wenn es als Baustein einer umfassenden Teststrategie eingesetzt wird: Es sorgt für eine
systematische kombinatorische Abdeckung, während fachliche Analyse, Grenzwerttests und Regressionstests die inhaltliche
Tiefe ergänzen.&lt;/p&gt;
&lt;h2 id=&quot;fazit&quot;&gt;Fazit&lt;/h2&gt;
&lt;p&gt;PICT ist kein Ersatz für fachliches Testdesign, aber ein sehr nützliches Werkzeug, um aus vielen Eingabeparametern eine
überschaubare und dennoch systematische Menge von Testfällen zu erzeugen.&lt;/p&gt;
&lt;p&gt;Gerade bei Tarifrechnern, deren Verhalten
durch Lookup-Tabellen, Bedingungen und Sonderregeln geprägt ist, hilft das Verfahren dabei, relevante
Parameterkombinationen nicht nur zufällig, sondern nachvollziehbar abzudecken.&lt;/p&gt;
&lt;p&gt;Wichtig ist dabei, dass das PICT-Modell sorgfältig erstellt wird. Die Auswahl der Parameter, die Bildung sinnvoller
Werteklassen, der Ausschluss fachlich ungültiger Kombinationen und die gezielte Erhöhung der Teststärke für kritische
Parametergruppen entscheiden darüber, wie aussagekräftig die generierten Testfälle am Ende sind.&lt;/p&gt;
&lt;p&gt;In einem Folgeartikel zeige ich an einem konkreten Beispiel, wie PICT in der Praxis zusammen mit einem Excel-Rechner
eingesetzt werden kann: von der Modellierung der Eingabeparameter über die Generierung der Testdaten bis zum Vergleich
der berechneten Ergebnisse mit dem Zielsystem.&lt;/p&gt;</content:encoded><author>huettemann@cosmocode.de (Detlef Hüttemann)</author></item><item><title>Vibe Engineering a Personal Tool</title><link>https://www.cosmocode.de/en/blog/agoh/20260609-vibe-engineering/</link><guid isPermaLink="true">https://www.cosmocode.de/en/blog/agoh/20260609-vibe-engineering/</guid><description>How to use vibe engineering to create personal tools</description><pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;style&gt;
  .chat {
    padding: 0.25rem 1rem;
    margin: 1rem 0;
    border-radius: 4px;
    border-left: 4px solid;
    font-style: normal;
    color: #333;
    
    &amp;.chat-me {
        border-color: #104e7d;
        background: #e5f2ff; 
    }
    &amp;.chat-claude {
        border-color: #e07a0f;
        background: #fff6ec;
        
        h1, h2, h3, h4, h5, h6 {
            font-size: 1rem;
            font-weight: bold;
            padding: 0.25rem 0;
            margin: 0.5rem 0;
        }
    }
    
    &amp; &gt; :first-child { 
        margin-top: 0; 
    }
    
    &amp; &gt; :last-child {
        margin-bottom: 0;
    }
    
    details &gt; div{
        max-height: 60vh;
        overflow: auto;
        margin-right: -1rem;
    }
    
    summary {
        cursor: pointer;
        font-weight: normal;
    }
  }
  
&lt;/style&gt;
&lt;p&gt;There are two approaches when it comes to using modern coding agents. On one end of the spectrum is pure vibe coding: you let the agent build and never look at the code. The other end has been dubbed agentic engineering: you treat the agent as a co-developer, and you discuss and review all changes in detail.&lt;/p&gt;
&lt;p&gt;At CosmoCode, we usually use the latter approach. We want to own and understand the code we deliver. &lt;strong&gt;Coding agents can help us deliver better code, not necessarily more code or code built faster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But if the approach to coding agents is a spectrum, then there should be a middle ground as well. I like to call it “vibe engineering”. Vibe engineering is a more relaxed approach to coding agents: you still review the code it generates, but you don’t scrutinize every line of it.&lt;/p&gt;
&lt;p&gt;I like this approach for personal tools that aren’t purely one-off scripts, but also aren’t production code. These are tools that I want to use for myself, but that I don’t necessarily want to turn into real “projects”.&lt;/p&gt;
&lt;p&gt;One of my colleagues asked me if I could describe how I approach &lt;strong&gt;vibe engineering&lt;/strong&gt; for such &lt;strong&gt;a personal tool&lt;/strong&gt;, using a real example. This is easier said than done, but I’ll try anyway. I will give some examples of the kind of interactions I have with the agent without reproducing the entire conversation. I’ll hide the full Claude answers behind a &lt;code&gt;details&lt;/code&gt; tag, so you can drill into them if you want to see the full context. I will still omit the tool calls.&lt;/p&gt;
&lt;h2 id=&quot;step-1-brainstorming&quot;&gt;Step 1: Brainstorming&lt;/h2&gt;
&lt;p&gt;We’re running most of our services in a &lt;strong&gt;Kubernetes cluster&lt;/strong&gt;, and sometimes you need to move files to or from a storage volume mounted to a specific pod. This can be done with &lt;code&gt;kubectl cp&lt;/code&gt;. However, &lt;code&gt;kubectl cp&lt;/code&gt; does not take care of copying last modified dates. The workaround is to use &lt;code&gt;kubectl exec&lt;/code&gt; and tar to stream to stdout, then use a second tar to read stdin on the other side: classic Unix tooling. But constructing the right &lt;code&gt;kubectl exec&lt;/code&gt; call is non-trivial.&lt;/p&gt;
&lt;p&gt;I wanted a simple, Midnight Commander-like, &lt;strong&gt;two-panel TUI interface for copying files&lt;/strong&gt;. One side shows my local computer and the pod’s filesystem is shown on the other side. Files can simply be selected and copied by the press of a button.&lt;/p&gt;
&lt;p&gt;To figure out how feasible this is, I actually started in Claude chat. I find that the chat interface is a better way to do initial brainstorming than jumping right into Claude Code - especially when I am not even sure I want to pursue the project in the end. I can ask questions, get suggestions, and iterate on the idea without constantly having to tell Claude not to start coding. But yes, I could also have used Claude Code’s plan mode.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;question: how complicated would it be to create a simple TUI tool that uses kubectl and tar to allow for easy file transfer between the local machine and a kubernetes pod using a two panel layout? (tar to preserve timestamps and file permissions)&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude explains how it could work and offers three approaches&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Not very complicated. A working version is a weekend project; a polished one is maybe a week.&lt;/p&gt;&lt;p&gt;Here’s the breakdown:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The core mechanics are easy.&lt;/strong&gt; kubectl already does the heavy lifting:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;List remote files: &lt;code&gt;kubectl exec &amp;lt;pod&amp;gt; -- ls -la --time-style=full-iso /path&lt;/code&gt; (parse the output)&lt;/li&gt;
&lt;li&gt;Pull a file/dir preserving metadata: &lt;code&gt;kubectl exec &amp;lt;pod&amp;gt; -- tar cf - -C /parent target | tar xf - -C /local/dest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Push: &lt;code&gt;tar cf - -C /local target | kubectl exec -i &amp;lt;pod&amp;gt; -- tar xf - -C /remote/dest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This is essentially what &lt;code&gt;kubectl cp&lt;/code&gt; does internally (it’s tar-based), but you get to control flags like &lt;code&gt;--numeric-owner&lt;/code&gt;, &lt;code&gt;-p&lt;/code&gt;, etc. yourself.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The TUI is the bulk of the work.&lt;/strong&gt; A two-panel layout (local on left, remote on right, Tab to switch focus, arrows to navigate, Enter to descend, F5/F6 to copy — basically Midnight Commander) is a well-trodden pattern. The real effort goes into directory navigation state, scrolling, selection, and async transfers so the UI doesn’t freeze.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended stack:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt; — &lt;code&gt;bubbletea&lt;/code&gt;/&lt;code&gt;bubbles&lt;/code&gt; for the TUI, and you can use &lt;code&gt;client-go&lt;/code&gt; to talk to the API directly (including the exec/SPDY streaming) instead of shelling out. Natural fit since kubectl is Go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt; — &lt;code&gt;textual&lt;/code&gt; for the TUI is very fast to build in; shell out to &lt;code&gt;kubectl&lt;/code&gt; or use the &lt;code&gt;kubernetes&lt;/code&gt; client library. Lowest effort to a prototype.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust&lt;/strong&gt; — &lt;code&gt;ratatui&lt;/code&gt; if you want a single static binary and don’t mind more work.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The fiddly parts&lt;/strong&gt; (where the time actually goes):&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Pods often lack &lt;code&gt;tar&lt;/code&gt; (especially distroless/scratch images) — you need a fallback or a clear error.&lt;/li&gt;
&lt;li&gt;Multi-container pods need &lt;code&gt;-c &amp;lt;container&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Async progress reporting for large transfers without blocking the event loop.&lt;/li&gt;
&lt;li&gt;Error handling for permissions, broken pipes, missing paths.&lt;/li&gt;
&lt;li&gt;Context/namespace selection.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;If you shell out to &lt;code&gt;kubectl&lt;/code&gt; and use Textual or Bubbletea, a usable single-pod-context prototype is genuinely a 1–2 day effort. Going to API-direct streaming, progress bars, multi-namespace browsing, and the tar-missing fallbacks is what stretches it toward a week.&lt;/p&gt;&lt;p&gt;Want me to sketch out a starter implementation in one of these stacks?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Hmm I would tend towards go because I like the single binary and I know a bit of go. But you say it’s the most difficult option?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude suggests to keep things simple&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Not the most difficult — I’d put it in the middle. The ordering I had in mind:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python/Textual&lt;/strong&gt; — fastest to a prototype, but you don’t get the single binary you want (you’re shipping a Python env or doing PyInstaller gymnastics).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go/Bubbletea&lt;/strong&gt; — slightly more upfront ceremony than Python, but you get the single static binary, and you already know some Go. This is the sweet spot for &lt;em&gt;your&lt;/em&gt; constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust/ratatui&lt;/strong&gt; — most work (borrow checker + more manual TUI plumbing), only worth it if you specifically want it.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So Go isn’t the hard option — it’s the right one for what you care about. Python only “wins” on raw speed-to-prototype, and it loses the thing you explicitly said you like.&lt;/p&gt;&lt;p&gt;One more decision inside the Go path that affects difficulty:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Shell out to &lt;code&gt;kubectl&lt;/code&gt;&lt;/strong&gt; (run &lt;code&gt;exec.Command(&amp;quot;kubectl&amp;quot;, ...)&lt;/code&gt;) — much simpler. You let the user’s existing kubeconfig/context do its thing, and you just pipe tar streams around. This keeps you firmly in the “1–2 day prototype” zone.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;client-go&lt;/code&gt; directly&lt;/strong&gt; — no kubectl dependency, cleaner streaming, but &lt;code&gt;client-go&lt;/code&gt; has a real learning curve (the exec/SPDY/remotecommand setup is notoriously fiddly, and kubeconfig loading has its own boilerplate). This is what pushes things toward the week.&lt;/p&gt;&lt;p&gt;My suggestion: start by shelling out to &lt;code&gt;kubectl&lt;/code&gt;. You get a working tool fast, and you can swap the transfer layer for &lt;code&gt;client-go&lt;/code&gt; later without touching the TUI — if you keep a clean interface like:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Transfer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ([]&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;FileInfo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Pull&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;remotePath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;localPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;localPath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;remotePath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the kubectl-shelling implementation and a future client-go implementation are just two structs satisfying the same interface.&lt;/p&gt;&lt;p&gt;Want me to put together a starter Bubbletea skeleton with the two-panel layout and a kubectl-backed &lt;code&gt;Transfer&lt;/code&gt; implementation?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;can you create a PLAN file that I can pass to a coding agent?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;At that point, Claude created a plan artifact, which I read through and addressed my concerns about certain details.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;one question about the numeric UIDs. when the pod container runs rootless, you probably can not change UIDs?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;After Claude clarified, I asked it to update the plan, and then I was ready to start coding.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;PLAN.md:&lt;/summary&gt;&lt;div&gt;&lt;h1 id=&quot;plan-k8tc--a-two-panel-tui-for-kubernetes-pod-file-transfer&quot;&gt;PLAN: &lt;code&gt;k8tc&lt;/code&gt; — A Two-Panel TUI for Kubernetes Pod File Transfer&lt;/h1&gt;&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;/h2&gt;&lt;p&gt;Build a terminal UI tool, written in &lt;strong&gt;Go&lt;/strong&gt;, that lets a user browse the local
filesystem and a Kubernetes pod’s filesystem side-by-side in a two-panel
(Midnight Commander style) layout, and transfer files/directories between them.
Transfers use &lt;strong&gt;&lt;code&gt;tar&lt;/code&gt; streamed over &lt;code&gt;kubectl exec&lt;/code&gt;&lt;/strong&gt; so that timestamps and file
permissions are preserved.&lt;/p&gt;&lt;p&gt;Ship as a &lt;strong&gt;single static binary&lt;/strong&gt;.&lt;/p&gt;&lt;h2 id=&quot;non-goals-v1&quot;&gt;Non-Goals (v1)&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;client-go&lt;/code&gt; integration. v1 shells out to the user’s existing &lt;code&gt;kubectl&lt;/code&gt;.
(Keep the transfer layer behind an interface so &lt;code&gt;client-go&lt;/code&gt; can be added later
without touching the TUI.)&lt;/li&gt;
&lt;li&gt;No editing of remote files in-place.&lt;/li&gt;
&lt;li&gt;No multi-pod parallel transfers.&lt;/li&gt;
&lt;li&gt;No Windows-specific polish (target Linux/macOS; it may work on Windows but
that is not a v1 requirement).&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language:&lt;/strong&gt; Go (1.22+)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUI framework:&lt;/strong&gt; &lt;a href=&quot;https://github.com/charmbracelet/bubbletea&quot;&gt;Bubble Tea&lt;/a&gt;
(&lt;code&gt;github.com/charmbracelet/bubbletea&lt;/code&gt;) with
&lt;a href=&quot;https://github.com/charmbracelet/bubbles&quot;&gt;Bubbles&lt;/a&gt; components and
&lt;a href=&quot;https://github.com/charmbracelet/lipgloss&quot;&gt;Lip Gloss&lt;/a&gt; for styling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External dependency at runtime:&lt;/strong&gt; &lt;code&gt;kubectl&lt;/code&gt; must be on the user’s &lt;code&gt;PATH&lt;/code&gt;
and configured (valid kubeconfig / current context). The target pod must have
&lt;code&gt;tar&lt;/code&gt; available in the chosen container.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;&lt;h3 id=&quot;package-layout&quot;&gt;Package layout&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cmd/k8tc/main.go        # entrypoint, flag parsing, bubbletea program start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/transfer/      # the Transfer interface + kubectl implementation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;transfer.go           # interface + shared types (FileInfo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl.go            # kubectl-backed implementation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/local/         # local filesystem browsing (List/Stat helpers)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/ui/            # bubbletea model, panels, key handling, rendering&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;model.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;panel.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;keys.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;styles.go&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;core-interface&quot;&gt;Core interface&lt;/h3&gt;&lt;p&gt;The transfer layer is abstracted so the kubectl implementation can later be
swapped for a &lt;code&gt;client-go&lt;/code&gt; one:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;package&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;time&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; FileInfo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Name    &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Size    &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Mode    &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // e.g. &amp;quot;drwxr-xr-x&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IsDir   &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;bool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ModTime &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;time&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Transfer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // List returns directory contents at path inside the pod.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;container&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ([]&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;FileInfo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Pull copies remotePath (file or dir) from the pod to localPath, preserving metadata.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Pull&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;container&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;remotePath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;localPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;progress&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Push copies localPath (file or dir) into the pod at remotePath, preserving metadata.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;pod&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;container&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;localPath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;remotePath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;progress&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The local filesystem panel does &lt;strong&gt;not&lt;/strong&gt; go through &lt;code&gt;Transfer&lt;/code&gt;; it uses the
&lt;code&gt;internal/local&lt;/code&gt; helpers directly. Only the remote panel uses &lt;code&gt;Transfer&lt;/code&gt;.&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;transfer-mechanics-the-important-bit&quot;&gt;Transfer Mechanics (the important bit)&lt;/h2&gt;&lt;p&gt;All remote operations shell out to &lt;code&gt;kubectl&lt;/code&gt;. Build commands with
&lt;code&gt;os/exec.CommandContext&lt;/code&gt; and stream stdin/stdout — &lt;strong&gt;never&lt;/strong&gt; buffer whole files
in memory.&lt;/p&gt;&lt;h3 id=&quot;listing-remote-files&quot;&gt;Listing remote files&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl exec &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- ls -la --full-time &amp;lt;path&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Parse the output into &lt;code&gt;[]FileInfo&lt;/code&gt;. Notes:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;--full-time&lt;/code&gt; (GNU coreutils) for a parseable ISO timestamp. If that fails
(BusyBox), fall back to &lt;code&gt;ls -la&lt;/code&gt; and accept coarser/absent mtimes rather than
erroring out.&lt;/li&gt;
&lt;li&gt;Skip the &lt;code&gt;total N&lt;/code&gt; first line.&lt;/li&gt;
&lt;li&gt;Always synthesize a &lt;code&gt;..&lt;/code&gt; entry for navigation (unless at &lt;code&gt;/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Detect directories from the leading &lt;code&gt;d&lt;/code&gt; in the mode string.&lt;/li&gt;
&lt;/ul&gt;&lt;h3 id=&quot;pull-pod--local-metadata-preserving&quot;&gt;Pull (pod → local), metadata-preserving&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl exec &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- tar cf - -C &amp;lt;remoteParent&amp;gt; &amp;lt;remoteBase&amp;gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;| tar xpf - --no-same-owner -C &amp;lt;localDest&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tar c&lt;/code&gt; on the remote side, piped to &lt;code&gt;tar xp&lt;/code&gt; locally (&lt;code&gt;-p&lt;/code&gt; preserves mode +
mtime). &lt;code&gt;--no-same-owner&lt;/code&gt; is the default for pulling — see “tar flags &amp;amp;
ownership” below for why.&lt;/li&gt;
&lt;li&gt;Run the local &lt;code&gt;tar&lt;/code&gt; via &lt;code&gt;exec.Command&lt;/code&gt; and connect the kubectl stdout to its
stdin with an &lt;code&gt;io.Pipe&lt;/code&gt; (or &lt;code&gt;cmd.StdoutPipe()&lt;/code&gt; → &lt;code&gt;cmd2.Stdin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Wrap the pipe in a counting &lt;code&gt;io.Reader&lt;/code&gt; to drive the &lt;code&gt;progress&lt;/code&gt; callback.&lt;/li&gt;
&lt;/ul&gt;&lt;h3 id=&quot;push-local--pod-metadata-preserving&quot;&gt;Push (local → pod), metadata-preserving&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;tar cf - -C &amp;lt;localParent&amp;gt; &amp;lt;localBase&amp;gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;| kubectl exec -i &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- tar xpf - --no-same-owner -C &amp;lt;remoteDest&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Note the &lt;code&gt;-i&lt;/code&gt; on &lt;code&gt;kubectl exec&lt;/code&gt; so stdin is forwarded.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-same-owner&lt;/code&gt; again by default — in a rootless container the extract
cannot chown anyway (see below); this makes the intent explicit and avoids
warnings.&lt;/li&gt;
&lt;li&gt;Same counting-reader trick for progress.&lt;/li&gt;
&lt;/ul&gt;&lt;h3 id=&quot;tar-flags--ownership&quot;&gt;tar flags &amp;amp; ownership&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Mode bits and mtime are reliably preserved without privilege. Owner UID/GID
is not — treat it as best-effort.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;When &lt;code&gt;tar x&lt;/code&gt; runs without &lt;code&gt;CAP_CHOWN&lt;/code&gt; (extracting on your local machine as a
normal user, or inside a rootless pod), the &lt;code&gt;chown()&lt;/code&gt; calls fail with &lt;code&gt;EPERM&lt;/code&gt;.
GNU tar’s default for a non-root extract is to &lt;em&gt;silently drop&lt;/em&gt; ownership restore
and create files owned by the extracting user — it does &lt;strong&gt;not&lt;/strong&gt; hard-fail. So a
blanket &lt;code&gt;--numeric-owner&lt;/code&gt; on extract buys nothing in the common case: it only
controls &lt;em&gt;how&lt;/em&gt; a UID is chosen (by number vs. name lookup), not whether tar is
&lt;em&gt;allowed&lt;/em&gt; to apply it.&lt;/p&gt;&lt;p&gt;Defaults, therefore:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create side:&lt;/strong&gt; &lt;code&gt;tar --numeric-owner -cf - ...&lt;/code&gt;
Numeric is harmless here and avoids name-lookup surprises when packing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract side (default):&lt;/strong&gt; &lt;code&gt;tar -xpf - --no-same-owner ...&lt;/code&gt;
Preserves mode + mtime, and explicitly tells tar not to attempt chown. This is
the right default for both directions:&lt;/li&gt;
&lt;li&gt;Pulling to local: you almost never want the pod’s UIDs applied on your
machine anyway (UID 1000 in the pod ≠ you).&lt;/li&gt;
&lt;li&gt;Pushing to a rootless pod: the chown would no-op regardless, so don’t pretend
otherwise.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Opt-in ownership preservation:&lt;/strong&gt; add a &lt;code&gt;--preserve-ownership&lt;/code&gt; flag to &lt;code&gt;k8tc&lt;/code&gt;.
When set, use &lt;code&gt;tar --same-owner --numeric-owner -xpf - ...&lt;/code&gt; on the extract side.
This only does anything useful when the extracting end is privileged (root in
the container, or root locally); otherwise it degrades to the same best-effort
behavior. Document this clearly so users aren’t surprised when UIDs don’t carry
across into a rootless target.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What actually hard-fails&lt;/strong&gt; is unrelated to ownership: writing into a directory
you lack write permission for, or a restored directory mode that locks tar out
mid-extract. Those surface as &lt;code&gt;EPERM&lt;/code&gt;/&lt;code&gt;EACCES&lt;/code&gt; on the file ops themselves and
should be reported per-transfer (see Error Handling).&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;tui-behavior&quot;&gt;TUI Behavior&lt;/h2&gt;&lt;h3 id=&quot;layout&quot;&gt;Layout&lt;/h3&gt;&lt;p&gt;Two equal-width panels filling the terminal, a header line, and a footer/status
line.&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;┌─ LOCAL: /home/user/project ──┐┌─ POD nginx-abc:/var/www ──────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│ ..                           ││ ..                             │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│ &amp;gt; src/                       ││   index.html                   │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   README.md                  ││   assets/                      │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   go.mod                     ││                                │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│                              ││                                │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└──────────────────────────────┘└────────────────────────────────┘&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tab: switch  ↑↓: move  ⏎: open  F5: copy  q: quit      [status...]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;focused&lt;/strong&gt; panel has a highlighted border; the cursor row is highlighted.&lt;/li&gt;
&lt;li&gt;Each panel maintains its own &lt;code&gt;cwd&lt;/code&gt;, file list, cursor index, and scroll
offset.&lt;/li&gt;
&lt;/ul&gt;&lt;h3 id=&quot;keybindings&quot;&gt;Keybindings&lt;/h3&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Key&lt;/th&gt;&lt;th&gt;Action&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Switch focus between local and remote panel&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;↑&lt;/code&gt; / &lt;code&gt;↓&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Move cursor&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PgUp/PgDn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Page cursor&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;&lt;td&gt;If dir: descend; if &lt;code&gt;..&lt;/code&gt;: go up; if file: no-op (v1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;F5&lt;/code&gt; / &lt;code&gt;c&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Copy highlighted entry from focused panel → other panel’s cwd&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Refresh focused panel&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;q&lt;/code&gt; / &lt;code&gt;Ctrl+C&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Quit&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id=&quot;async-transfers&quot;&gt;Async transfers&lt;/h3&gt;&lt;p&gt;Transfers must not block the event loop. Use the Bubble Tea pattern:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;On &lt;code&gt;F5&lt;/code&gt;, dispatch a &lt;code&gt;tea.Cmd&lt;/code&gt; that runs the &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; in a goroutine and
returns a &lt;code&gt;transferDoneMsg{err}&lt;/code&gt; (and intermediate &lt;code&gt;transferProgressMsg{n}&lt;/code&gt;
via a channel + &lt;code&gt;tea.Tick&lt;/code&gt; or a custom message pump).&lt;/li&gt;
&lt;li&gt;While in flight, show progress/byte-count in the status line and disable
further copy actions.&lt;/li&gt;
&lt;li&gt;On completion, refresh the destination panel and clear status.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2 id=&quot;cli&quot;&gt;CLI&lt;/h2&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;k8tc --pod &amp;lt;name&amp;gt; [--namespace &amp;lt;ns&amp;gt;] [--container &amp;lt;name&amp;gt;] [--remote-path &amp;lt;path&amp;gt;] [--local-path &amp;lt;path&amp;gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--pod&lt;/code&gt; (required for v1)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--namespace&lt;/code&gt; / &lt;code&gt;-n&lt;/code&gt; → passed through as &lt;code&gt;kubectl -n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--container&lt;/code&gt; / &lt;code&gt;-c&lt;/code&gt; → passed through as &lt;code&gt;kubectl exec -c&lt;/code&gt;; if omitted, let
kubectl pick the default container&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--remote-path&lt;/code&gt; initial remote dir (default &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--local-path&lt;/code&gt; initial local dir (default &lt;code&gt;.&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--preserve-ownership&lt;/code&gt; attempt to restore owner UID/GID on extract
(&lt;code&gt;--same-owner --numeric-owner&lt;/code&gt;). Off by default; only effective when the
extracting end is privileged. See “tar flags &amp;amp; ownership.”&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;(Stretch: a pod picker if &lt;code&gt;--pod&lt;/code&gt; is omitted, via &lt;code&gt;kubectl get pods -o json&lt;/code&gt;.)&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;error-handling--edge-cases&quot;&gt;Error Handling &amp;amp; Edge Cases&lt;/h2&gt;&lt;p&gt;The agent must handle these explicitly, surfacing errors in the status line
rather than crashing:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kubectl&lt;/code&gt; not found on PATH&lt;/strong&gt; → fail fast at startup with a clear message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tar&lt;/code&gt; missing in the pod&lt;/strong&gt; (distroless/scratch images) → detect the exec
failure and show: “pod has no &lt;code&gt;tar&lt;/code&gt;; cannot transfer.” Do &lt;strong&gt;not&lt;/strong&gt; hang.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-container pod with no &lt;code&gt;--container&lt;/code&gt;&lt;/strong&gt; → kubectl will error; surface
its message and hint to pass &lt;code&gt;-c&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission denied&lt;/strong&gt; on read (local or remote) → show per-transfer error,
keep the UI alive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BusyBox &lt;code&gt;ls&lt;/code&gt;&lt;/strong&gt; lacking &lt;code&gt;--full-time&lt;/code&gt; → fall back gracefully (see Listing).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broken pipe / context cancel&lt;/strong&gt; mid-transfer → clean up both processes
(&lt;code&gt;CommandContext&lt;/code&gt; + &lt;code&gt;cmd.Wait()&lt;/code&gt; on both ends; kill the partner on failure).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty directories&lt;/strong&gt; and the root &lt;code&gt;/&lt;/code&gt; (no &lt;code&gt;..&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large files&lt;/strong&gt; → never read fully into memory; always stream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spaces / special chars in paths&lt;/strong&gt; → pass paths as separate &lt;code&gt;exec.Command&lt;/code&gt;
args (no shell string interpolation); when piping two &lt;code&gt;exec.Cmd&lt;/code&gt;s, do it in
Go via pipes, not via a &lt;code&gt;sh -c &amp;quot;... | ...&amp;quot;&lt;/code&gt; string.&lt;/li&gt;
&lt;/ol&gt;&lt;hr&gt;&lt;h2 id=&quot;suggested-build-order-milestones&quot;&gt;Suggested Build Order (milestones)&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Transfer interface + kubectl &lt;code&gt;List&lt;/code&gt;.&lt;/strong&gt; CLI prints a remote &lt;code&gt;ls&lt;/code&gt;. Verify
parsing against a real pod.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local &lt;code&gt;List&lt;/code&gt;.&lt;/strong&gt; Mirror the same &lt;code&gt;FileInfo&lt;/code&gt; for the local FS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static two-panel render&lt;/strong&gt; (Lip Gloss) with both panels populated, no
interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigation:&lt;/strong&gt; focus switching, cursor movement, &lt;code&gt;Enter&lt;/code&gt; to descend/ascend,
scroll offset, refresh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Pull&lt;/code&gt; (pod → local)&lt;/strong&gt; synchronous first, then move it onto the async
&lt;code&gt;tea.Cmd&lt;/code&gt; pattern with a status line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Push&lt;/code&gt; (local → pod)&lt;/strong&gt; same shape as Pull.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress reporting&lt;/strong&gt; via counting reader → status line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge-case hardening&lt;/strong&gt; from the list above (tar-missing, busybox ls,
cancellation).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polish:&lt;/strong&gt; styling, help footer, &lt;code&gt;--namespace&lt;/code&gt;/&lt;code&gt;--container&lt;/code&gt; plumbing.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Milestones 1–6 are the usable prototype. 7–9 are the path to “done.”&lt;/p&gt;&lt;hr&gt;&lt;h2 id=&quot;acceptance-criteria&quot;&gt;Acceptance Criteria&lt;/h2&gt;&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Launches with &lt;code&gt;k8tc --pod &amp;lt;p&amp;gt;&lt;/code&gt; and shows local + remote panels.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Tab switches focus; arrows + Enter navigate both filesystems.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; F5 copies the highlighted file &lt;strong&gt;or directory&lt;/strong&gt; in the focused panel into
the other panel’s current directory.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Transferred files retain original mtime and permission (mode) bits
(verify with &lt;code&gt;stat&lt;/code&gt; on both ends). Owner UID/GID is best-effort: preserved
only with &lt;code&gt;--preserve-ownership&lt;/code&gt; against a privileged extract target.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Directory transfers are recursive and also preserve metadata.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; A transfer of a large file does not freeze the UI and shows progress.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Missing &lt;code&gt;tar&lt;/code&gt; in the pod produces a clear error, not a hang or panic.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Builds to a single static binary: &lt;code&gt;CGO_ENABLED=0 go build&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2 id=&quot;future-post-v1-do-not-build-now&quot;&gt;Future (post-v1, do not build now)&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Swap the kubectl-backed &lt;code&gt;Transfer&lt;/code&gt; for a &lt;code&gt;client-go&lt;/code&gt; implementation
(exec via &lt;code&gt;remotecommand&lt;/code&gt; SPDY) to drop the &lt;code&gt;kubectl&lt;/code&gt; runtime dependency.&lt;/li&gt;
&lt;li&gt;Pod/namespace picker UI.&lt;/li&gt;
&lt;li&gt;Multi-select and queued transfers.&lt;/li&gt;
&lt;li&gt;File preview / view pane.&lt;/li&gt;
&lt;li&gt;Delete / rename / mkdir operations.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;p&gt;The main takeaway here is not to start the implementation in the chat. That’s the entirely wrong environment. Get a &lt;strong&gt;PLAN artifact&lt;/strong&gt; and take it to the code environment.&lt;/p&gt;
&lt;h2 id=&quot;step-2-initial-implementation&quot;&gt;Step 2: Initial Implementation&lt;/h2&gt;
&lt;p&gt;It was time to move to Claude Code. I run Claude in a &lt;a href=&quot;https://gist.github.com/splitbrain/5b20b82abfcd874c0b5b6ed22cb40443&quot;&gt;custom sandbox&lt;/a&gt; using bubblewrap on Linux. This allows me to &lt;strong&gt;safely run in &lt;code&gt;auto&lt;/code&gt; mode&lt;/strong&gt;, which means Claude is running mostly autonomously with an LLM classifier as the only safeguard.&lt;/p&gt;
&lt;p&gt;In this concrete example, the sandbox prevented Claude from accessing my kubectl config and thus from running kubectl commands against our cluster unchecked.&lt;/p&gt;
&lt;p&gt;I added the &lt;code&gt;PLAN.md&lt;/code&gt; to a new repository and let Claude one-shot the initial implementation.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;read @PLAN.md and implement it&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;About 20 minutes later, Claude reported that it was done. Time to give the tool a quick test.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/k8tc-initial.CWqxJFxk_Z22WC5H.webp&quot; alt=&quot;Initial implementation&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1145&quot; height=&quot;674&quot;&gt;&lt;/p&gt;
&lt;p&gt;It worked just as specified ✨️. I could have stopped here if this were an one-off tool.&lt;/p&gt;
&lt;h2 id=&quot;step-n-review&quot;&gt;Step n: Review&lt;/h2&gt;
&lt;p&gt;Before I decided to add more features, I wanted to review the code.&lt;/p&gt;
&lt;p&gt;Another reason why I decided to use Go is that the code is usually more straightforward and easier to review than Python or Rust. Go tends to have only one way to do things, which makes it easier for the agent to generate code that is correct and idiomatic. And that in turn makes it easier for me to review the code and understand what it does.&lt;/p&gt;
&lt;p&gt;When vibe engineering, I mostly &lt;strong&gt;look at architectural decisions and code structure&lt;/strong&gt;. I don’t want to nitpick every line of code, but I want to make sure that the overall design makes sense and that the code is organized in a way that is maintainable.&lt;/p&gt;
&lt;p&gt;As an example, here is the review session I had with Claude right after the initial implementation.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Let’s review the current state. I see that we have a FileInfo struct and a Transfer interface. The comment says that this only covers the remote side and that the local side does not use the interface. But then looking at local.go it uses transfer.FileInot and transfer.Sort so it seems the two sides are not that different?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude gives an explanation that doesn’t fully satisfy me&lt;/summary&gt;&lt;div&gt;&lt;p&gt;I read all four files. Here’s what’s actually going on — the tension you spotted is real but it’s conflating two different things that live in the &lt;code&gt;transf er&lt;/code&gt; package.&lt;/p&gt;&lt;h2 id=&quot;there-are-two-separate-things-in-transfergo&quot;&gt;There are two separate things in &lt;code&gt;transfer.go&lt;/code&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A shared data layer&lt;/strong&gt; — the &lt;code&gt;FileInfo&lt;/code&gt; struct (transfer.go:15) and the &lt;code&gt;Sort&lt;/code&gt; helper (transfer.go:40). Both panels use these, by design. That’s why &lt;code&gt; FileInfo&lt;/code&gt;’s own comment says “on either the local or the remote side.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;Transfer&lt;/code&gt; interface&lt;/strong&gt; — &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; (transfer.go:25). &lt;em&gt;This&lt;/em&gt; is what the local side doesn’t use.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;The comment at transfer.go:23-24 is talking strictly about #2. And it’s literally accurate: the local panel calls &lt;code&gt;local.List(p)&lt;/code&gt; — a plain package functi
on (model.go:109) — whereas the remote panel dispatches through the interface, &lt;code&gt;t.List(...)&lt;/code&gt; (model.go:111). So when the comment says “does not go through
this interface,” it means local doesn’t implement the &lt;code&gt;List/Pull/Push&lt;/code&gt; &lt;em&gt;method set&lt;/em&gt;, not that local is walled off from the package.&lt;/p&gt;&lt;p&gt;So your instinct is &lt;strong&gt;half right&lt;/strong&gt;: the &lt;em&gt;display&lt;/em&gt; side is barely different (shared types + shared sort), but the &lt;em&gt;transfer&lt;/em&gt; side is fundamentally asymmetr
ic.&lt;/p&gt;&lt;h2 id=&quot;why-local-isnt-behind-the-interface&quot;&gt;Why local isn’t behind the interface&lt;/h2&gt;&lt;p&gt;Two reasons, and the second is the deeper one:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing to swap.&lt;/strong&gt; The package doc says the interface exists “so the kubectl implementation can later be swapped for a client-go one.” The local files
ystem has no such alternative — it’s always &lt;code&gt;os.ReadDir&lt;/code&gt;. An interface with exactly one possible implementation is just ceremony.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The local side has no &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; of its own.&lt;/strong&gt; Look at the signatures: &lt;code&gt;Pull(pod, container, remotePath, localPath, ...)&lt;/code&gt; and &lt;code&gt;Push(pod, container,  localPath, remotePath, ...)&lt;/code&gt; — the &lt;code&gt;Kubectl&lt;/code&gt; value &lt;em&gt;already straddles both ends&lt;/em&gt;; it reads/writes local paths itself (it’s the local &lt;code&gt;tar&lt;/code&gt; process in &lt;code&gt;pi pe&lt;/code&gt;). So &lt;code&gt;Transfer&lt;/code&gt; isn’t really “the remote filesystem” — it’s “the thing that bridges local↔remote.” The only purely-local operation is &lt;em&gt;listing a direc
tory&lt;/em&gt;, and that’s a one-liner over &lt;code&gt;os.ReadDir&lt;/code&gt;. There’s no “local Transfer” concept to even abstract.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That’s the real answer to “are the two sides that different?”: for browsing, no; for transferring, there is no local side — it’s subsumed into &lt;code&gt;Kubectl&lt;/code&gt;.&lt;/p&gt;&lt;h2 id=&quot;two-minor-warts-worth-noting-in-the-review&quot;&gt;Two minor warts worth noting in the review&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;FileInfo&lt;/code&gt;/&lt;code&gt;Sort&lt;/code&gt; live in the package named after the remote side&lt;/strong&gt;, yet are used by both. It’s pragmatic (local imports transfer, not vice versa), but it’s why the comment has to keep clarifying “this is also used locally.” If you wanted stricter cohesion you’d hoist &lt;code&gt;FileInfo&lt;/code&gt;+&lt;code&gt;Sort&lt;/code&gt; into a small shared package (e.g. &lt;code&gt;fileinfo&lt;/code&gt;) that both &lt;code&gt;transfer&lt;/code&gt; and &lt;code&gt;local&lt;/code&gt; import — but that’s arguably over-engineering for two callers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;..&lt;/code&gt;/root-synthesis logic is duplicated&lt;/strong&gt; — local.go:36-38 vs kubectl.go:139-141. The duplication is &lt;em&gt;justified&lt;/em&gt;: local uses &lt;code&gt;filepath&lt;/code&gt; (OS separators) and remote uses &lt;code&gt;path&lt;/code&gt; (always &lt;code&gt;/&lt;/code&gt;, inside a Linux pod), so they can’t trivially share it. But it’s worth a one-line comment on each noting the deliberate split, since they’ll need to stay in sync.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;If you want, I can tighten the interface comment to say “the &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; &lt;em&gt;method set&lt;/em&gt;” instead of “this interface” (to kill the ambiguity you hit), or sketch the &lt;code&gt;fileinfo&lt;/code&gt; extraction so you can judge whether it’s worth it. Which direction do you want to take?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I feel like if there are two concerns (transfer and file representation) then the two should be separated. It would make it less weird that local reuses the file representation then&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude refactors and mentions another issue&lt;/summary&gt;&lt;div&gt;&lt;h2 id=&quot;decisions-i-made&quot;&gt;Decisions I made&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Renamed &lt;code&gt;FileInfo&lt;/code&gt; → &lt;code&gt;file.Info&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;file.FileInfo&lt;/code&gt; would stutter; &lt;code&gt;file.Info&lt;/code&gt; is the idiomatic Go form. &lt;code&gt;Sort&lt;/code&gt; keeps its name (&lt;code&gt;file.Sort&lt;/code&gt;). If you’d
rather preserve the &lt;code&gt;FileInfo&lt;/code&gt; name, say so and I’ll rename — it’s a mechanical change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reworded the &lt;code&gt;Transfer&lt;/code&gt; doc comment&lt;/strong&gt; to describe it as a local↔remote &lt;em&gt;bridge&lt;/em&gt; (List reads the pod; Pull/Push move trees between pod and disk), which
is the accurate framing we landed on in the previous discussion — and it no longer needs to awkwardly disclaim the shared type, since the type now visibl
y lives in its own package.&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id=&quot;still-untouched-deliberately&quot;&gt;Still untouched (deliberately)&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;..&lt;/code&gt;/root-synthesis duplication between &lt;code&gt;local.List&lt;/code&gt; and &lt;code&gt;parseLS&lt;/code&gt; is still there — that’s the &lt;code&gt;filepath&lt;/code&gt;-vs-&lt;code&gt;path&lt;/code&gt; split we discussed, and it’s ortho
gonal to this refactor. &lt;code&gt;file&lt;/code&gt; would be the natural home for a shared helper if you ever want to collapse it, but I left it alone since it’d need the sepa
rator parameterized. Want me to tackle that too?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I don’t remember discussing filepath-vs-path - what’s that about?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude explains the issue and recommends to ignore it&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Fair — I overstated it as “discussed.” It was a one-line aside in my earlier review, not something we actually worked through. Here’s what I meant.&lt;/p&gt;&lt;p&gt;It’s about two different standard-library packages in Go:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;path&lt;/code&gt;&lt;/strong&gt; — manipulates always-slash-separated paths (&lt;code&gt;/&lt;/code&gt;). It’s what you use for URLs and anything that is &lt;em&gt;defined&lt;/em&gt; to use forward slashes regardless of the host OS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;path/filepath&lt;/code&gt;&lt;/strong&gt; — manipulates paths using the &lt;em&gt;host OS’s&lt;/em&gt; rules: &lt;code&gt;/&lt;/code&gt; on Linux/macOS, &lt;code&gt;\&lt;/code&gt; on Windows.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;In k8tc the two panels live on different sides of that line:&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;local&lt;/strong&gt; panel walks the filesystem of whatever machine k8tc runs on, so it correctly uses &lt;code&gt;filepath&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;That last grep is the clearest illustration: &lt;code&gt;kubectl.go&lt;/code&gt; uses &lt;strong&gt;both&lt;/strong&gt; packages, deliberately. The remote (pod) side uses &lt;code&gt;path&lt;/code&gt; (lines 141, 149–150) and the local side of the very same transfer uses &lt;code&gt;filepath&lt;/code&gt; (lines 158–159).&lt;/p&gt;&lt;p&gt;The reason: a pod is a Linux container, so its filesystem paths are &lt;em&gt;always&lt;/em&gt; &lt;code&gt;/&lt;/code&gt;-separated, no matter what OS k8tc itself is running on. If you used &lt;code&gt;filepath&lt;/code&gt; on a pod path while running k8tc on Windows, you’d get backslash semantics applied to a path that is genuinely slash-based — wrong. So pod paths must go through &lt;code&gt;path&lt;/code&gt;; host paths must go through &lt;code&gt;filepath&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Now, the &lt;code&gt;..&lt;/code&gt;-synthesis duplication I flagged. The two snippets are:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// local.go:36 — host filesystem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; filepath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Clean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(dir) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(filepath.Separator) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// kubectl.go:141 — pod filesystem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; path.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Clean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(dir) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;/&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;They’re structurally identical (“if this isn’t the root, prepend a &lt;code&gt;..&lt;/code&gt; entry”), but they differ in exactly the two spots where the path-flavor matters:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;which &lt;code&gt;Clean&lt;/code&gt;&lt;/strong&gt; — OS-aware vs. always-slash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what counts as root&lt;/strong&gt; — &lt;code&gt;string(filepath.Separator)&lt;/code&gt; (the host’s root) vs. the literal &lt;code&gt;&amp;quot;/&amp;quot;&lt;/code&gt; (the pod’s root)&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So you &lt;em&gt;can’t&lt;/em&gt; just lift the snippet into one shared function and call it from both sides — you’d have to parameterize it with “which path semantics,” e.g. pass in the clean-func and the root sentinel. That’s why I called the duplication “justified”: collapsing it would trade two obvious 3-line blocks for one indirection-laden helper, and arguably make it &lt;em&gt;less&lt;/em&gt; clear that one side is host-pathed and the other is pod-pathed.&lt;/p&gt;&lt;p&gt;So nothing for you to do here — I just wanted to flag that the duplication is intentional, not an oversight, in case it caught your eye during review. It’s unrelated to the &lt;code&gt;file&lt;/code&gt; extraction we just did.&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Hmm to me it sound like local and remote should implement at least Transfer.List (don’t implement, discuss)&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude suggests different ways to address the issue&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Good instinct — and it points at a real seam. Let me lay out the case for and against, because there’s a genuine obstacle that’s worth seeing clearly. First let me confirm one thing the argument hinges on:&lt;/p&gt;&lt;p&gt;Confirmed what I needed: &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; are set once in &lt;code&gt;New&lt;/code&gt; (model.go:66–67) and only ever read after that. They’re &lt;strong&gt;session-constant&lt;/strong&gt;, yet they’re threaded as arguments into every single &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; call. That detail is the whole crux of your suggestion. Here’s the discussion.&lt;/p&gt;&lt;h2 id=&quot;your-instinct-is-right--for-list-specifically&quot;&gt;Your instinct is right — for &lt;code&gt;List&lt;/code&gt; specifically&lt;/h2&gt;&lt;p&gt;The two operations in &lt;code&gt;Transfer&lt;/code&gt; have fundamentally different &lt;em&gt;shapes&lt;/em&gt;:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;List&lt;/code&gt;&lt;/strong&gt; is &lt;strong&gt;symmetric and one-sided&lt;/strong&gt;: &lt;code&gt;(path) → []file.Info&lt;/code&gt;. Both panels do &lt;em&gt;exactly&lt;/em&gt; this. There’s nothing pod-specific about the operation itself — “read a directory, give me the entries.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt;&lt;/strong&gt; are &lt;strong&gt;asymmetric and two-sided&lt;/strong&gt;: they move a tree &lt;em&gt;across&lt;/em&gt; the local↔pod boundary. There is no “local Pull”; the kubectl process is inherently both ends at once. These genuinely belong only to the bridge.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So &lt;code&gt;Transfer&lt;/code&gt; today actually bundles two responsibilities — &lt;em&gt;browsing&lt;/em&gt; and &lt;em&gt;moving&lt;/em&gt; — and you’ve spotted that browsing is the half that’s symmetric. This is the same kind of cut we just made pulling &lt;code&gt;file&lt;/code&gt; out: separate the thing that’s shared from the thing that isn’t.&lt;/p&gt;&lt;h2 id=&quot;the-obstacle-the-signature-carries-podcontainer&quot;&gt;The obstacle: the signature carries &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;Why local &lt;em&gt;can’t&lt;/em&gt; satisfy &lt;code&gt;Transfer.List&lt;/code&gt; as written:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Kubectl: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pod, container, path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ([]file.Info, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 3 path-ish args&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;local:   &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)                 ([]file.Info, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The local side has no pod or container, so it can’t match. But — as the grep above shows — &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; are session-constant. They don’t &lt;em&gt;need&lt;/em&gt; to be per-call args; they’re only per-call because the interface made them so. Bind them once and the signatures converge:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Lister&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ([]&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;file&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;local&lt;/strong&gt;: a zero-field &lt;code&gt;local.FS{}&lt;/code&gt; whose &lt;code&gt;List(path)&lt;/code&gt; wraps today’s &lt;code&gt;local.List&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;remote&lt;/strong&gt;: a tiny adapter holding &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt;, whose &lt;code&gt;List(path)&lt;/code&gt; calls &lt;code&gt;kubectl.List(pod, container, path)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id=&quot;what-it-buys-concretely&quot;&gt;What it buys, concretely&lt;/h2&gt;&lt;p&gt;&lt;code&gt;loadPanel&lt;/code&gt; loses its &lt;code&gt;which == focusLocal&lt;/code&gt; fork (model.go:108–112):&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; which &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; focusLocal {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    files, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; local.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    files, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; t.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pod, container, p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// after — each Panel owns its own lister&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;files, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; m.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;panelPtr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(which).lister.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;List&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(p)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each &lt;code&gt;Panel&lt;/code&gt; becomes self-sufficient: it knows how to load &lt;em&gt;itself&lt;/em&gt;, and the Model stops caring whether a panel is local or remote when listing. That’s a real improvement in the model’s coherence, and it makes test fakes trivial — a &lt;code&gt;Lister&lt;/code&gt; fake is one method, versus today’s &lt;code&gt;fakeTransfer&lt;/code&gt; that must stub &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; even for a listing test.&lt;/p&gt;&lt;h2 id=&quot;where-id-put-the-interface&quot;&gt;Where I’d put the interface&lt;/h2&gt;&lt;p&gt;The idiomatic Go move: &lt;strong&gt;define &lt;code&gt;Lister&lt;/code&gt; in the consumer (&lt;code&gt;ui&lt;/code&gt;), not the producer.&lt;/strong&gt; Then &lt;code&gt;local.FS&lt;/code&gt; satisfies it structurally without importing anything, and &lt;code&gt;local&lt;/code&gt; stays fully decoupled (no &lt;code&gt;transfer&lt;/code&gt; import — which we just worked to remove). The remote adapter can live in &lt;code&gt;ui&lt;/code&gt; too. &lt;code&gt;transfer.Transfer&lt;/code&gt; keeps &lt;code&gt;List&lt;/code&gt; as the &lt;em&gt;backend&lt;/em&gt; capability (the future client-go backend must still list), and the adapter just curries &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; onto it.&lt;/p&gt;&lt;h2 id=&quot;two-honest-caveats&quot;&gt;Two honest caveats&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It doesn’t unify everything.&lt;/strong&gt; &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; stay asymmetric and the push-vs-pull decision stays in the UI (model.go:289/292). The &lt;code&gt;Lister&lt;/code&gt; cut only simplifies &lt;em&gt;browsing&lt;/em&gt;. That’s fine — but don’t expect it to collapse the transfer logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It doesn’t touch the &lt;code&gt;..&lt;/code&gt;/&lt;code&gt;filepath&lt;/code&gt;-vs-&lt;code&gt;path&lt;/code&gt; duplication.&lt;/strong&gt; Each side’s &lt;code&gt;List&lt;/code&gt; still synthesizes &lt;code&gt;..&lt;/code&gt; its own way. Orthogonal to this.&lt;/li&gt;
&lt;/ol&gt;&lt;h2 id=&quot;my-recommendation&quot;&gt;My recommendation&lt;/h2&gt;&lt;p&gt;Worth doing, at the &lt;strong&gt;lightweight&lt;/strong&gt; end: introduce a consumer-side &lt;code&gt;Lister&lt;/code&gt;, adapt both sides, give each &lt;code&gt;Panel&lt;/code&gt; its own. It’s contained, reversible, and improves the conceptual model.&lt;/p&gt;&lt;p&gt;I’d &lt;em&gt;stop short&lt;/em&gt; of the maximal version (ripping &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; out of the &lt;code&gt;Transfer&lt;/code&gt; interface entirely and binding them at &lt;code&gt;Kubectl&lt;/code&gt; construction). That’s arguably “more correct” — it’d kill the per-call threading on &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; too — but it turns &lt;code&gt;Kubectl&lt;/code&gt; from a reusable engine into a session-scoped object, and the blast radius is much larger for a payoff that’s mostly aesthetic.&lt;/p&gt;&lt;p&gt;The deciding question is really: do you want each &lt;code&gt;Panel&lt;/code&gt; to “own how it lists itself”? If yes, the &lt;code&gt;Lister&lt;/code&gt; is the clean expression of that. If the 6-line branch in &lt;code&gt;loadPanel&lt;/code&gt; doesn’t bother you, the status quo is defensible too — the asymmetry it encodes (only the remote side is swappable) is real.&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;TBH your “maximal version” sounds more correct. I feel like there are distinct responsibilities. A kubernetes package which wraps around kubectl and handling the tar commands. A ls parser (used probably remotely only). A Transfer manager that implements the push/pull mechanism. And remote/local implementations of the same “virtual” file system list. Would you agree?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude comes up with a detailed proposal for what I suggested&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Yes — broadly I agree. Those are real, separable responsibilities, and the fact that you can name them this cleanly is a good sign the seams are real and
not invented. But I’d refine &lt;em&gt;one&lt;/em&gt; of your four boundaries, and in doing so I think the decomposition gets sharper than what you sketched. Let me give you the version I’d actually defend.&lt;/p&gt;&lt;h2 id=&quot;the-dependency-graph-id-aim-for&quot;&gt;The dependency graph I’d aim for&lt;/h2&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;file        Info + Sort                                    (done; depends on nothing)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ▲   ▲&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  │   └──────────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;parser              local Lister        kube      ← the swap seam&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;(ls -la → []Info)   (os.ReadDir)         ▲  ▲&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ▲                                      │  │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └────── remote Lister ─────────────────┘  │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          (ls via kube + parse)             │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  transfer (Pull/Push)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  (tar streaming over kube)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;file&lt;/code&gt;&lt;/strong&gt; — representation. Done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kube&lt;/code&gt;&lt;/strong&gt; — the exec/stream primitive: “run this command in this pod/container, give me stdin/stdout/stderr.” Holds the bound &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; (killing the per-call threading you spotted). Knows nothing about tar, ls, or &lt;code&gt;file.Info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;parser&lt;/strong&gt; — pure &lt;code&gt;[]byte → []file.Info&lt;/code&gt;. Remote-only, as you said (locally &lt;code&gt;os.ReadDir&lt;/code&gt; already hands back structured entries; nothing to parse).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;two Listers&lt;/strong&gt; — local (&lt;code&gt;os.ReadDir&lt;/code&gt;) and remote (&lt;code&gt;kube&lt;/code&gt; + parser), both satisfying one &lt;code&gt;List(path) → []file.Info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/strong&gt; — the Pull/Push manager: tar create/extract + the streaming pipe, using &lt;code&gt;kube&lt;/code&gt; for the remote endpoint and local &lt;code&gt;exec&lt;/code&gt; for the local one.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;No cycles, clean DAG. Each node has an independent test story — and notably the tests you &lt;em&gt;already have&lt;/em&gt; fall out naturally: &lt;code&gt;parse_test.go&lt;/code&gt; (pure parser), and &lt;code&gt;fakeKubectl&lt;/code&gt; (which makes the “pod” just the local fs) is really a test of the transfer streaming independent of real k8s.&lt;/p&gt;&lt;h2 id=&quot;where-id-push-back-tar-does-not-belong-in-the-kube-package&quot;&gt;Where I’d push back: tar does &lt;em&gt;not&lt;/em&gt; belong in the &lt;code&gt;kube&lt;/code&gt; package&lt;/h2&gt;&lt;p&gt;You said the kubernetes package wraps kubectl &lt;em&gt;and&lt;/em&gt; the tar commands. I’d split those. Put &lt;strong&gt;only the exec/stream primitive&lt;/strong&gt; in &lt;code&gt;kube&lt;/code&gt;; keep &lt;strong&gt;tar&lt;/strong&gt; (createArgs/extractArgs/&lt;code&gt;pipe&lt;/code&gt;/classify) in the &lt;code&gt;transfer&lt;/code&gt; manager.&lt;/p&gt;&lt;p&gt;The reason is your own swappability rationale. The package doc promises “swap the kubectl implementation for a client-go one without touching the TUI.” The thing that differs between kubectl and client-go is &lt;em&gt;how you exec into a pod and get a byte stream&lt;/em&gt; — not tar. tar runs identically either way. So the swap seam is the exec primitive, and it must be tar-agnostic, or every transfer detail leaks across the seam. If &lt;code&gt;kube&lt;/code&gt; owns tar, you haven’t isolated the swappable part; you’ve just renamed &lt;code&gt;Kubectl&lt;/code&gt;.&lt;/p&gt;&lt;h2 id=&quot;the-non-obvious-consequence--and-why-this-is-the-version-that-makes-the-doc-true&quot;&gt;The non-obvious consequence — and why this is the version that makes the doc &lt;em&gt;true&lt;/em&gt;&lt;/h2&gt;&lt;p&gt;Here’s the part I think is the real payoff, and it’s hidden in &lt;code&gt;pipe&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; pipe&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;dst&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;exec&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Cmd&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;progress&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;pipe&lt;/code&gt; wires together &lt;strong&gt;two &lt;code&gt;*exec.Cmd&lt;/code&gt;&lt;/strong&gt;. That’s fine for kubectl (both endpoints are local processes — &lt;code&gt;kubectl exec …&lt;/code&gt; is itself a local process). But &lt;strong&gt;client-go does not give you an &lt;code&gt;*exec.Cmd&lt;/code&gt;&lt;/strong&gt; — it gives you a &lt;code&gt;remotecommand&lt;/code&gt; executor you hand &lt;code&gt;Stdin&lt;/code&gt;/&lt;code&gt;Stdout&lt;/code&gt;/&lt;code&gt;Stderr&lt;/code&gt; io streams. So today’s &lt;code&gt;Transfer&lt;/code&gt; interface &lt;em&gt;claims&lt;/em&gt; client-go swappability while its internals (two &lt;code&gt;*exec.Cmd&lt;/code&gt;) would actively fight it. The swappability is currently aspirational, not real.&lt;/p&gt;&lt;p&gt;The decomposition you’re proposing fixes exactly that — if the &lt;code&gt;kube&lt;/code&gt; seam is expressed as streams (&lt;code&gt;Run(cmd, stdin io.Reader, stdout, stderr io.Writer) error&lt;/code&gt;) rather than &lt;code&gt;*exec.Cmd&lt;/code&gt;, then &lt;code&gt;pipe&lt;/code&gt; connects two &lt;em&gt;stream endpoints&lt;/em&gt;, the local tar side is one impl, the pod side is another, and kubectl-vs-client-go becomes a true drop-in. So this isn’t just tidying — it’s the refactor that would make the package doc’s promise honest. That’s the strongest argument &lt;em&gt;for&lt;/em&gt; going maximal.&lt;/p&gt;&lt;h2 id=&quot;two-cautions&quot;&gt;Two cautions&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Granularity.&lt;/strong&gt; Four responsibilities ≠ four packages. The parser is ~45 lines of pure code — I’d keep it as &lt;code&gt;parse.go&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; the remote-listing package with its existing test, not a package of its own. A package per function is its own smell. The boundaries that earn a package are &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;kube&lt;/code&gt;, &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;transfer&lt;/code&gt;, and the remote side; whether remote-List and Pull/Push are one “pod” package or two is a coin-flip I’d decide on cohesion (I lean: one &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;remote&lt;/code&gt; package exposing both, since they share the &lt;code&gt;kube&lt;/code&gt; client and the “pod-ness” — but your capability-split is defensible).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost is front-loaded in &lt;code&gt;pipe&lt;/code&gt;.&lt;/strong&gt; Everything else (extract &lt;code&gt;kube&lt;/code&gt;, bind pod/container, add Listers) is mechanical and low-risk. Re-expressing &lt;code&gt;pipe&lt;/code&gt; around streams instead of &lt;code&gt;*exec.Cmd&lt;/code&gt; is the one genuinely fiddly change, and it only pays off the day someone actually writes the client-go backend. If that day is hypothetical, you can stop short: extract &lt;code&gt;kube&lt;/code&gt; as an &lt;code&gt;*exec.Cmd&lt;/code&gt; factory (still removes the threading, still separates concerns) and leave &lt;code&gt;pipe&lt;/code&gt; alone. You lose nothing you have today and can tighten the seam later.&lt;/li&gt;
&lt;/ol&gt;&lt;h2 id=&quot;so-sequencing&quot;&gt;So, sequencing&lt;/h2&gt;&lt;p&gt;If you want to do this, I’d go in dependency order, each step independently shippable and green:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Extract &lt;strong&gt;&lt;code&gt;kube&lt;/code&gt;&lt;/strong&gt; (exec primitive, pod/container bound) — removes the per-call threading.&lt;/li&gt;
&lt;li&gt;Introduce the &lt;strong&gt;&lt;code&gt;Lister&lt;/code&gt;&lt;/strong&gt; interface + local/remote impls — collapses the &lt;code&gt;loadPanel&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Move &lt;strong&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/strong&gt; to Pull/Push-only on top of &lt;code&gt;kube&lt;/code&gt;; isolate the &lt;strong&gt;parser&lt;/strong&gt; into the remote lister.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Optional, later:&lt;/em&gt; re-express &lt;code&gt;pipe&lt;/code&gt;/&lt;code&gt;kube&lt;/code&gt; around streams when (if) client-go becomes real.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Net: I agree with your four-way split, with the single amendment that tar lives with &lt;code&gt;transfer&lt;/code&gt;, not &lt;code&gt;kube&lt;/code&gt; — and the headline is that doing this is what would finally make the “swap to client-go” sentence in the package doc actually true rather than aspirational.&lt;/p&gt;&lt;p&gt;Want me to start at step 1?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I like your analysis and corrections. It seems like a sound plan. Implement the changes fully&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The final result was a lot cleaner than the original first version. At the same time, I had gotten a rough overview of the codebase.&lt;/p&gt;
&lt;p&gt;As you can see, I often try to give Claude &lt;strong&gt;open-ended questions&lt;/strong&gt; to coax it to “think” through the problem and come up with its own analysis, rather than just asking for a specific change. Only when I am confident that I have understood the issue and solution well enough do I come in with specific guidance. A &lt;code&gt;question:&lt;/code&gt; prefix or an explicit &lt;code&gt;discuss, don&amp;#39;t code&lt;/code&gt; can keep it from jumping to code before the design is clear without having to switch back to planning mode.&lt;/p&gt;
&lt;p&gt;The example above also shows the importance of &lt;strong&gt;pushing back&lt;/strong&gt; on the agent’s suggestions when they don’t feel right. Claude often tries to take the lazy way out ;-)&lt;/p&gt;
&lt;h2 id=&quot;step-n1-add-features&quot;&gt;Step n+1: Add Features&lt;/h2&gt;
&lt;p&gt;With a first prototype and a clearer understanding of the codebase, I can now start adding features. I won’t go through all the details here, but the process is similar to what we did before: identify a feature, discuss it with Claude, and then ask it to implement it.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Currently only a single item can be copied. I would like to be able to mark multiple files or directories and then copy them over. Before the copy starts, a confirmation dialog should be shown. While the copy is running a progress dialog (with abort button) should be shown instead of using the status line for progress tracking. Can you come up with a plan first?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Once a feature is planned and implemented, it’s a good time to review the code again and make more architectural improvements if needed. Repeat until you’re happy.&lt;/p&gt;
&lt;p&gt;I usually start a new session for each feature and for each review.&lt;/p&gt;
&lt;h2 id=&quot;final-result&quot;&gt;Final Result&lt;/h2&gt;
&lt;p&gt;The final tool is available at its &lt;a href=&quot;https://github.com/cosmocode/k8tc&quot;&gt;GitHub repository&lt;/a&gt;. Following the commits, you can see the sequence of adding new features and refactorings in between.&lt;/p&gt;
&lt;p&gt;The final tool has the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;selecting multiple files and directories&lt;/li&gt;
&lt;li&gt;copying files and directories&lt;/li&gt;
&lt;li&gt;deleting files and directories&lt;/li&gt;
&lt;li&gt;creating new directories&lt;/li&gt;
&lt;li&gt;confirmation dialogs for copy and delete operations&lt;/li&gt;
&lt;li&gt;abortable progress dialog for copy and delete operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I might add a pod/container selector on startup in the future, but for now it’s doing exactly what I wanted it to do, and the whole process took about four hours of work. Incidentally, writing this blog post took nearly the same amount of time ;-)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/_astro/k8tc-final.DYVF5lhV_K7k0t.webp&quot; alt=&quot;Final result&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1145&quot; height=&quot;674&quot;&gt;&lt;/p&gt;</content:encoded><author>gohr@cosmocode.de (Andreas Gohr)</author></item><item><title>Hello again, World</title><link>https://www.cosmocode.de/en/blog/dhue/20260526-hello-again-world/</link><guid isPermaLink="true">https://www.cosmocode.de/en/blog/dhue/20260526-hello-again-world/</guid><description>Reaktivierung unseres Blogs: Warum wir wieder einen eigenen Ort für unsere Gedanken brauchen.</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Wenn man die Geburt eines Blogs üblicherweise mit einem „Hello World“-Post begrüßt, wie sollte dann der Titel für einen wieder ins Leben gerufenen Blog lauten? Vielleicht: „Hello again, World“?&lt;/p&gt;
&lt;p&gt;Viele Jahre lang hatten wir auf unserer Website einen Blog. Er brachte auch durchaus guten Traffic. Als wir vor einigen Jahren unsere Homepage relauncht haben, haben wir uns allerdings entschieden, den Blog nicht zu übernehmen.&lt;/p&gt;
&lt;p&gt;Der Hintergrund war, dass viele der alten Artikel zwar in hohem Maße SEO-relevant waren, zugleich aber auch alt und inhaltlich nicht mehr auf dem Stand, den wir heute vertreten würden.&lt;/p&gt;
&lt;p&gt;Das ist nicht unbedingt das beste Aushängeschild: Wenn ein möglicher Interessent oder Neukunde ausgerechnet über einen veralteten Artikel auf uns aufmerksam wird, entsteht womöglich ein Eindruck, zu dem wir heute nicht mehr stehen würden.&lt;/p&gt;
&lt;h2 id=&quot;was-uns-ohne-blog-gefehlt-hat&quot;&gt;Was uns ohne Blog gefehlt hat&lt;/h2&gt;
&lt;p&gt;Bei unserem kürzlich stattgefundenen Seminar in Lübbenow haben wir beschlossen, den Blog wieder zu reaktivieren.&lt;/p&gt;
&lt;p&gt;Denn uns fehlt ein wichtiger Aspekt der Öffentlichkeitsarbeit: ein Ort, an dem wir nicht ganz so formell auftreten müssen wie in Referenzen oder Case Studies. Ein Ort, an dem wir freier erzählen können — über Entwicklungen in unserer Firma, über unser Team oder über Technologien, die wir gerade einsetzen.&lt;/p&gt;
&lt;p&gt;Natürlich ist damit auch die Hoffnung verbunden, den einen oder anderen Leser zu erreichen — und vielleicht auch den einen oder anderen Interessenten.&lt;/p&gt;
&lt;p&gt;Vielleicht stehen wir in ein paar Jahren wieder vor dem Problem veralteter Inhalte. Der Unterschied ist: Inhalte zu identifizieren, zu prüfen und entsprechend zu markieren, wird inzwischen deutlich einfacher. Schon heute lassen sich solche Aufgaben mit KI-Agenten unterstützen. In fünf Jahren werden vermutlich noch einmal ganz andere Werkzeuge zur Verfügung stehen, um Inhalte zu auditieren, einzuordnen und bei Bedarf zu aktualisieren.&lt;/p&gt;
&lt;h2 id=&quot;warum-nicht-einfach-linkedin&quot;&gt;Warum nicht einfach LinkedIn?&lt;/h2&gt;
&lt;p&gt;Als wir den Blog seinerzeit gestrichen hatten, gab es auch die Idee, stattdessen stärker in anderen Medien über uns zu berichten: auf sozialen Plattformen, über Medium oder über LinkedIn.&lt;/p&gt;
&lt;p&gt;LinkedIn ist eine Plattform mit Relevanz. Aber es ist eben auch ein durchoptimiertes System, das — wie alle sozialen Plattformen — die Aufmerksamkeit der Akteure vermarktet.&lt;/p&gt;
&lt;p&gt;Auf LinkedIn spielen wir alle nach denselben Regeln. Wir strampeln uns ab, um in möglichst vielen Feeds zu erscheinen. Und dazu gehört auch, immer mehr und immer kräftiger strampeln zu müssen, wenn die anderen auf LinkedIn dasselbe tun — notfalls mit automatisierten Posts, die von KI geschrieben werden.&lt;/p&gt;
&lt;p&gt;LinkedIn ist als Nutzer- oder Konsumentenplattform durchaus brauchbar. Als Produzent geht es dort aber irgendwann nicht mehr nur um kluge Inhalte, sondern auch um Dauerfeuer.&lt;/p&gt;
&lt;h2 id=&quot;ein-eigener-ort-für-eigene-gedanken&quot;&gt;Ein eigener Ort für eigene Gedanken&lt;/h2&gt;
&lt;p&gt;Wenn ich mir schon viele Gedanken über ein Thema mache und an der schriftlichen Ausarbeitung feile, dann empfinde ich es als angemessener, diesen Text in einem Rahmen zu veröffentlichen, der uns gehört: auf unserer eigenen Bühne.&lt;/p&gt;
&lt;p&gt;Natürlich stehen auch Blogposts in einem Aufmerksamkeitswettbewerb — nämlich dem der Suchmaschinen und KI-Content-Aggregatoren. Aber in meinem privaten Blog habe ich den Eindruck gewonnen, dass sich guter Content immer noch lohnt und dass er auch gelesen wird. Meine anfängliche Skepsis, dass LLMs die Inhalte lediglich als Knowledge scrapen, um daraus eigene Inhalte zu formen, und nur noch Bots, aber keine Menschen mehr auf die Blogs gehen, hat sich nicht bestätigt. Noch nicht.&lt;/p&gt;
&lt;h2 id=&quot;happy-rebirth-cosmoblog&quot;&gt;Happy Rebirth, CosmoBlog&lt;/h2&gt;
&lt;p&gt;Also versuchen wir es mit einem neuen Blog auf CosmoCode.&lt;/p&gt;
&lt;p&gt;Happy Rebirth, CosmoBlog. Hereinspaziert, liebe Welt.&lt;/p&gt;</content:encoded><author>huettemann@cosmocode.de (Detlef Hüttemann)</author></item></channel></rss>