Moderner, schlanker Ersatz fuer Microsoft System Center Orchestrator. Agentless Workflow-Orchestrierung fuer Windows-Umgebungen via WinRM.
Diese Datei ist der Index, nicht die Doku. Sie enthält Verhaltensregeln, Nachschlage-Tabellen und Invarianten — die Tiefe liegt in docs/.
Pflege: Ein Feature-PR fügt hier höchstens eine Zeile hinzu. Wer einen Absatz schreiben will, schreibt ihn in docs/claude-reference.md und verlinkt ihn von hier. Keine Fehler-Rückblenden („vorher war es so…"), keine Messwerte als Beleg — dieselbe Regel wie für Code-Kommentare. Steht eine Erklärung schon in docs/, steht hier nur der Zeiger. Umfang ist per DocumentationCountsTests gedeckelt.
- Issue tracker: GitHub Issues für
Sev7eNup/NodePilot, siehedocs/agents/issue-tracker.md - Triage labels: Fünf-Label-Vokabular, siehe
docs/agents/triage-labels.md - Domain docs: Single-context repo → root
CONTEXT.md+docs/adr/, siehedocs/agents/domain.md
docs/roadmap.md— führendes Dokument für „was wird gebaut" (R1 gesetzt, R2 trigger-gated, E offen, Sperrvermerk-Anhang). Was dort nicht steht, ist kein Vorhaben.docs/claude-reference.md— Overflow-Referenz dieser Datei; die Sektionen hier zeigen auf ihre Tiefe dortdocs/alerting.md— Notification-Rules + System-Policies (ADR 0008), Dispatcher, Sinks, Ledgerdocs/custom-activities.md— Custom Activities (Plugin-System)docs/mcp-server.md— MCP-Server inkl. Tool-Katalog +.mcp.json-Beispieldocs/ai-features.md— KI-Features: Config-Keys, Modell-Empfehlungendocs/deployment-guide.md— (EN) Artefakt verifizieren, selbst bauen, Troubleshooting — nicht der Installationsweg, der steht einmal auf der Doku-Website (content/{de,en}/deployment/production.md)docs/av-exclusions.md— Antiviren-Ausschlüsse (Server + Desktop) als Übergabedokument für eine AV-Abteilungdocs/workflow-styleguide.md— Layout-Styleguide für Workflow-JSONs (vor jedem Workflow-Gen lesen)docs/workflow-tests.md— Test-Suite unterscripts/test-suite/: 50 generierte Workflows gegen die laufende Engine,suite-manifest.jsonals Abdeckungsquelle, Guard-TestTestSuiteCoverageTestsdocs/enterprise-features.md— HA, Secret-Provider, LDAP/SSO, SIEM, Folder-RBACdocs/ai-feature-ideas.md— Beschreibungstiefe zu den KI-Ideen, keine Spezifikation. Priorisierung und Status stehen indocs/roadmap.md.src/nodepilot-ui/e2e/README.md— E2E-Coverage-Map + Spec-Konventionensrc/nodepilot-ui/demo/— Browser-Demo der SPA auf www.nodepilot.run (/demo/), dieselbe App gegen ein In-Memory-Backend; Regeln insrc/nodepilot-ui/CLAUDE.mdsrc/nodepilot-docs-ui/src/site/— Projekt-Website (DE/EN) an der Website-Wurzel, Doku darunter unter/docs/; nie Teil vondist/. Build, Routen, Vorschau:src/nodepilot-docs-ui/README.md. Impressum/Datenschutz liefert der User.
- Backend: ASP.NET Core Web API, .NET 10, Windows-only (
net10.0-windows) - Datenbank: PostgreSQL (default) / SQL Server (
Database:Provider=postgres|sqlserver). SQLite nur als Test-In-Memory-Backend. - Remote Execution: PowerShell SDK / WinRM, agentless.
Remote:Provider:winrm(default) |noop(noopbrauchtRemote:AllowNoop=truebzw.NODEPILOT_ALLOW_NOOP_REMOTE=1, sonst Boot-Abbruch). Lokale Nutzerskripte (engine: auto) laufen als Windows-PowerShell-5.1-Prozess wie remote; der In-Proc-Pool (WinPS-Kompatibilität bewusst aus) bedient die eingebauten Activities undengine: runspace:docs/claude-reference.md - Real-time: SignalR (
/hubs/execution) - Logging: Serilog. Format via
Logging:Format:text|cmtrace|json|ecs-json(ECS 1.x für SIEM, siehedocs/siem-logging.md). Support-Log: File + DB-Projektion - MCP-Server (opt-in):
nodepilot-mcp(stdio) — AI-Agent steuert/editiert Workflows über 102 Tools, HTTP-only gegen die REST-API - Enterprise (opt-in): Active/Passive HA (
Cluster:Enabled), pluggable Secret-Provider (Secrets:Provider=Dpapi|AesGcm), LDAP/Windows-SSO, ECS-JSON-SIEM, Folder-RBAC
Projekt-Layout unter src/ + tests/ — nicht hier gespiegelt, direkt nachsehen. Bindend ist die Abhaengigkeitsrichtung:
Dep-Graph: Api -> Ai, Engine, Scheduler, Data, Remote, Core, Telemetry | Engine -> Ai, Data, Remote, Core, Telemetry | Scheduler -> Engine, Data, Core (Application-Tier: konsumiert Engine-Notifications/-Conditions/-Security) | Ai -> Core (LLM-Stack, sitzt unter Engine, damit Api+Engine ihn teilen) | Data -> Core | Remote -> Core | Telemetry -> Core | Cli -> Core (HTTP-only) | Mcp -> Core (HTTP-only, MCP-Server) | Switcher -> ∅ (lokale Windows-SCM-WPF-App). Maschinell erzwungen durch DependencyDirectionTests (Api.Tests/Architecture) — Graph-Änderung heißt: csproj + diese Zeile + der Test ändern sich gemeinsam.
& 'C:\NodePilot-Postgres\pgsql\bin\pg_ctl.exe' start -D 'C:\NodePilot-Postgres\data' -l 'C:\NodePilot-Postgres\data\postgres.log' -w # Postgres dieser Maschine — immer zuerst
cd src\NodePilot.Api; dotnet run # Backend, Port 5000 (launchSettings.json = Ziel des Vite-Proxys)
cd src\nodepilot-ui; npm run dev # Frontend, Port 5173
cd src\nodepilot-docs-ui; npm run dev # Doku-Website, Port 5174 — nur wenn /docs im Dev erreichbar sein sollErster Login braucht das Setup-Token aus src\NodePilot.Api\admin-setup.token (die Login-Maske zeigt das Feld beim ersten Versuch). Rolle/DB anlegen, Connection-String: CONTRIBUTING.md § Local setup.
/docs im Dev: Vite proxyt /docs auf 5174; läuft der Doku-Dev-Server nicht, führt der Doku-Button ins Leere — erwartet, kein Defekt. Der Doku-Dev-Server läuft selbst unter /docs/ (--base=/docs/ im dev-Skript, weil Vite 8 das base aus der Config im Dev ignoriert); ohne das landet man in der App statt in der Doku.
Für Claude: Dev-Mode verwenden. API-Neustarts (stop+rebuild+start) sind jederzeit ohne Rückfrage erlaubt — DLL-Locks sind normal. Vorab PID via Get-NetTCPConnection -LocalPort 5000 finden, dann Stop-Process + Rebuild + Start. npm run dev kaputt → npm install. Deploy-Skripte unter deploy/ laufen nur auf ausdrückliche Aufforderung — die Freigabe gilt jeweils nur für den einen Vorgang.
Langlaufende Prozesse (API + Vite): detached/als Background-Prozess starten (z. B. Start-Process mit umgeleiteten Logs), damit sie Tool-Call-Grenzen überleben. „Läuft" erst melden nach vollem Port-Check (Get-NetTCPConnection -LocalPort 5000 ohne -First N) und HTTP-Health-Probe. Vor jedem Kill verifizieren, dass es die Dev-Instanz ist — nie den installierten Windows-Dienst treffen.
- Nichts nach außen ohne ausdrückliche Ansage.
git commit,git push,gh pr create,gh pr merge,gh pr comment,gh issue create/comment,gh release create, Tags, Branch-Löschung — jeder dieser Schritte braucht eine eigene Aufforderung des Users. Lokal arbeiten (Branch anlegen, editieren, bauen, testen) ist frei; sobald etwas das Repository verlässt oder inmainlandet, wird gefragt.- „go", „mach das", „setz das um" heißt: implementieren. Es heißt nicht committen, pushen, PR öffnen oder mergen. Wenn der User Commit/PR/Merge will, sagt er es (z. B. „pr und merge bitte") — und diese Freigabe gilt nur für den einen Vorgang, nicht für die nächste Aufgabe.
- Nach getaner Arbeit den Stand im Arbeitsbaum liegen lassen und knapp berichten, was bereitliegt. Nicht vorgreifend committen, „damit nichts verlorengeht".
- Branching: Nicht-triviale Arbeit auf einem neuen Branch beginnen, bevor editiert wird; nachfragen nur, wenn der Branch-Name unklar ist. Triviale Einzeiler (z. B.
.gitignore) bekommen keinen eigenen Branch/PR — in die laufende Arbeit einfalten. - PR-Budget: maximal 5 PRs gleichzeitig für eigene Arbeits-Batches (größere Vorhaben in ≤5 PRs schneiden). Für Dependabot gilt diese Zahl nicht:
.github/dependabot.ymlbündelt Minor/Patch pro Ökosystem (open-pull-requests-limit: 1je Block → max. 5 Sammel-PRs), aber jeder offene Major fällt aus der Gruppe und bekommt einen eigenen PR. Majors bleiben bewusst ungruppiert, weil ein Bündel den Review verschlechtert. - Jede Änderung an
.github/dependabot.ymllöst sofort alle Blöcke neu aus (unabhängig vom Montags-Zeitplan) und erzeugt binnen Minuten neue PRs. Config-Edits deshalb bündeln, nicht nacheinander mergen. - Scope: Minimaler Root-Cause-Fix. Würde ein Fix deutlich mehr Dateien anfassen als das benannte Problem → stoppen und den geplanten Scope in 3 Bullets nennen, bevor editiert wird.
- Keine Abwärtskompatibilität: Keine Shims, Feature-Flags, optionale Defaults für sanfte Migration. Sauber durchziehen:
NOT NULL, Required-Properties, alte Code-Pfade ersatzlos löschen. Alte DB → Migrations fahren, fertig. - PowerShell 5.1 / Windows: Kein Inline-SQL durch PowerShell-Quoting — Query in eine
.sql-Datei schreiben und perpsql -fausführen. Dateien als UTF-8 ohne BOM schreiben. Keinesed/Regex-Zeilen-Edits auf Source-Dateien (CRLF bricht sie) — Edit-Tool verwenden. Kein$args-Splatting; explizite benannte Parameter. - Code-Kommentare: Sachlich und kurz, in einfachem Englisch. Sie sagen, was der Code tut und warum — nicht mehr. Keine Herleitung, keine Erzählung, keine Rückblende auf frühere Fehlversuche, keine Messwerte oder Beispielzahlen als Beleg, kein „X used to …, which meant …". Wer den Hintergrund braucht, findet ihn in Commit-Message, PR oder
docs/. Ein bis drei Zeilen reichen fast immer; ein Kommentar, der länger ist als der Code darunter, ist meist eine Erzählung. Die vorhandenen langen Kommentare im Repo sind kein Vorbild. - Reporting: Knapp berichten — was geändert, was verifiziert, was offen. Keine Per-File-Walkthroughs, kein Plan-Nacherzählen. Interaktive Rückfragen nur, wenn die Antwort wirklich blockiert.
| Provider | Database:Provider |
ConnectionString-Key |
|---|---|---|
| PostgreSQL (Default) | "postgres" |
ConnectionStrings:Postgres |
| SQL Server | "sqlserver" |
ConnectionStrings:DefaultConnection |
-
Ein gemeinsames Migration-Set, provider-agnostisch (ohne
type:-Strings). Bootstrap viadb.Database.Migrate(). Bestehende Datenbanken niemals automatisch löschen oder deren Migration-History umschreiben (Baseline und Upgrade-Pfad:CONTRIBUTING.md). -
Neue Migration:
dotnet ef migrations add <Name> --project src/NodePilot.Data --startup-project src/NodePilot.Api --context NodePilotDbContext. Pflicht-Postprocessing — zwei Schritte:- In der Migration (
<Name>.cs): alletype: "..."-Annotations entfernen. - In der Designer-Datei (
<Name>.Designer.cs):MigrationModelPortability.UseActiveProviderStoreTypes(modelBuilder);als letzte Zeile vor#pragma warning restore 612, 618inBuildTargetModelergänzen. DerModelSnapshotbekommt den Aufruf bewusst nicht (Diff-Basis, kein Migration-Target-Model).
Beide Schritte sind durch
MigrationDriftTestsabgesichert — laufen lassen statt sich erinnern. - In der Migration (
-
Schema-Änderungen IMMER per EF-Migration. Kein DDL-Hotpatching.
-
Credentials mit DPAPI verschlüsselt (
Credentials:DpapiScope). DB-TLS ist strikt (DatabaseTlsBootValidator); Escape-Bedingungen, Retention-Dienste und alle Background-Services:docs/claude-reference.md.
Prozessweiter In-Memory-Breaker (NodePilot.Data.Availability): fällt die DB zur Laufzeit aus, antwortet /api sofort 503 DATABASE_UNAVAILABLE statt zu hängen. Invarianten und Health-Endpunkte: docs/adr/0011-database-availability-breaker.md + docs/claude-reference.md. Zwei Invarianten stehen hier, weil man sie mit nur dieser Datei sonst „repariert":
- Nur die Sonde publiziert
Available(SELECT 1auf eigener ungepoolter Verbindung, nieCanConnectAsync); EF-Interceptors degradieren nur, ein Command-Timeout armt nur die Sonde. Hintergrunddienste parken viaWaitUntilServableAsync. Database:AuthReadTimeoutSecondsund die übrigen Availability-Budgets sind restart-pflichtige Boot-Config und bewusst keinSettingsSchema-Eintrag — der Connection-String gehört auf keine HTTP-Fläche.
Bekannte Falle (bewusst so): HostOptions.BackgroundServiceExceptionBehavior bleibt auf StopHost, und die sieben Retention-Dienste haben ihren breiten Catch eine Ebene unter der host-fatalen Grenze — Code, der in deren RunIterationAsync außerhalb des inneren try landet, kann den Host töten.
Routen + Rollen-Gating stehen an den Controllern in src/NodePilot.Api/Controllers/ ([Route]/[Authorize]) — dort nachsehen statt hier spiegeln. Rollen-Matrix unter ## Autorisierung.
Nicht getroffene /api-Pfade antworten 404 application/problem+json (code: NOT_FOUND), nicht mit dem SPA-Bundle — ein eigener MapFallback("/api/{**rest}") steht vor MapFallbackToFile("index.html"). Betrifft auch Routenparameter, die ihre Typ-Constraint verfehlen. Deep-Links außerhalb von /api gehen weiterhin an die SPA.
| Endpoint | Semantik |
|---|---|
POST /execute |
Startet Lauf, asynchron. Body: {"parameters": {}, "timeoutSeconds": N, "debug": bool}. 202 + ExecutionId, Fortschritt via SignalR. |
POST /enable / /disable |
Kill-Switch. enable verlangt einen lock-freien Workflow (jeder Lock, auch der eigene → 423), validiert die gespeicherte Definition und füllt PublishedByUserId, falls unbesetzt — ein vorhandener wird nie überschrieben. Bereits aktiv = No-Op (204) ohne Stempel. disable ignoriert Locks. |
POST /cancel-all |
Cancelt alle Running- und Pending-Executions des Workflows. |
PUT /concurrency-limit |
Setzt MaxConcurrentExecutions (1..1000, null = unbegrenzt). maxConcurrentExecutions ist Pflicht (fehlend → 400, sonst würde {} das Limit still löschen), 0 wird abgelehnt. Kein Edit-Lock, kein Version-Bump, kein History-Snapshot. |
POST /executions/{id}/cancel|retry|resume |
Einzelner Lauf. Resume-Body: {"stepId": "<node-id>", "mode": "continue"|"stepOver"|"stop", "overrides": {}} — stepId ist Pflicht. |
POST /{id}/rollback |
Snapshottet wie Update die vorherige Definition in die Version-History. |
Disable+cancel-all = Quarantäne.
Per-Workflow-Parallelität: Workflow.MaxConcurrentExecutions begrenzt gleichzeitige Läufe eines Workflows über alle Aufrufer hinweg; am Limit wird eingereiht statt abgelehnt (ein Zähler für beide Wege: IWorkflowConcurrencyGate), und der Dispatch-Claim überspringt Workflows am Limit. Nicht versioniert, nicht im Update/Publish-Body. Details: docs/claude-reference.md § Per-Workflow-Parallelität.
Workflows haben einen per-User-Edit-Lock (CheckedOutByUserId + CheckedOutAt). Mutierende Endpoints liefern 423 Locked wenn Caller nicht Lock-Owner. Disable ist nicht lock-gegated (Incident-Kill-Switch).
| Endpoint | Verhalten |
|---|---|
POST /lock |
Atomar IsEnabled=false + Lock-Fields setzen. 409 wenn schon gelockt. |
POST /unlock |
Lock-Fields auf null. IsEnabled bleibt unverändert. |
POST /publish |
Atomar: Save + IsEnabled=true + Unlock. Validiert den übergebenen Body vorher: schwaches Webhook-HMAC-Secret → 400 weak_webhook_hmac_secret, für Quartz ungültige Cron-Expression → 400 invalid_cron_expression. enable prüft dasselbe an der gespeicherten Definition. |
POST /force-unlock |
Admin-only. Bricht fremden Lock. |
UX-Flow und Button-State-Matrix: docs/claude-reference.md. Kurz: canWrite = role !== 'Viewer' && checkedOutByUserId === currentUserId.
"Remote" = targetMachineId/WinRM. "Engine-local" = im API-Prozess. (controlFlow) = Kategorie ControlFlow im backend ActivityCatalog (Palette-Achse, unabhängig vom Scope).
- Remote:
fileOperation,folderOperation,textFileEdit,serviceManagement,registryOperation,wmiQuery,startProgram,powerManagement,scheduledTask,fileHash,zipOperation - Engine-local:
restApi,sql,emailNotification,delay,xmlQuery,jsonQuery,log,generateText,llmQuery+ controlFlow:junction,forEach,decision,startWorkflow,returnData - Hybrid:
runScript,waitForCondition
Config-Keys & Output-Semantik pro Activity sowie Prozess-Isolation (config.isolated: true, nur lokal, No-Op auf dem WinRM-Pfad): docs/claude-reference.md.
- Retry pro Step:
config.retry(maxAttempts,backoff,initialDelayMs,maxDelayMs). Dauerhafte Remote-Fehler (abgelehnter WinRM-Logon, geblockte HTTP-Session, nicht entschlüsselbares Credential) werden nicht wiederholt — sonst sperrt ein Step ein Konto. - Execution-Timeout:
timeoutSecondsim Execute-Body + per-Stepconfig.timeoutSeconds.
User-authored, PowerShell-backed Activities (UI „Custom Nodes") als reine runScript-Presets — dieselbe Engine, Isolation, Marker-Capture und Redaction, keine zweite Script-Engine. activityType = custom:<key> → ein Sentinel-registrierter CustomActivityExecutor (__customDefinitionId authoritativ, __customKey Drift-Guard); captured werden nur die deklarierten Outputs (+ exitCode). Governance: Mutation nur solange disabled (Admin+Operator), ab enabled Admin-only; kein secret-Input-Typ. Geteilte Facts-Schicht NodePilot.Core.Activities.CustomActivityType, Frontend-Spiegel lib/customActivities.ts; activityCatalog.generated.ts bleibt unberührt. Volle Doku: docs/custom-activities.md.
Custom-Regeln (Kind=Custom, Execution-Events, Filter-AST = derselbe ConditionEvaluator wie Edge-Conditions) und System-Policies (Kind=System, ADR 0008, ISystemAlertSource-Katalog), zugestellt über SMTP / Webhook + HMAC; idle bis eine Regel existiert. Zustellung ist at-least-once: der NotificationDispatcher persistiert den Attempt vor jedem I/O, ein Crash danach kann erneut zustellen — Webhook-Empfänger deduplizieren über EventKey. Alle Mutationen + Test-Fire Admin-only, neue Regeln entstehen disabled. Volle Doku: docs/alerting.md.
- Neue Activity: Klasse in
Engine/Activities/,IActivityExecutorimplementieren — Auto-Discovery viaAddNodePilotActivities(), keine DI-Verdrahtung. Pflichtteil derselben Änderung: die UI-Seite (src/nodepilot-ui/CLAUDE.md) und ein Eintrag insrc/NodePilot.Core/Activities/Embedded/activity-config-reference.json, aus dem AI-Prompt-Katalog und MCP-Config-Tools gespeist werden. Guard-Tests: Tabelle unter Build & Test. - Neuer API Controller: In
Api/Controllers/, DTOs inApi/Dtos/; braucht CLI-Command und MCP-Tool (siehe Clients). - Frontend: Seiten, Nodes, i18n, State und Design-Tokens:
src/nodepilot-ui/CLAUDE.md. - Models/Interfaces: Immer in
NodePilot.Core - Doc-Sync: Feature-Änderungen halten alle Doku-Flächen synchron — README,
docs/*.md,docs/testing/E2ETests.md+e2e/README.mdund die Doku-Websitesrc/nodepilot-docs-ui/content/(eigener kuratierter Korpus, kein Render vondocs/). Zweisprachig: jede Seite brauchtcontent/de/…undcontent/en/…plus Titel-Eintrag insrc/i18n/locales/{de,en}.json; Querverweise ohne Sprach-Präfix. Der AI-Wissenskorpus zieht bewusst nurcontent/en/. Die Doku-index.htmldarf kein Inline-<script>enthalten (CSPscript-src 'self', Guard:document-head.test.ts).
{
"nodes": [{
"id": "step-123", "type": "activity",
"position": { "x": 100, "y": 200 },
"data": {
"label": "Check Disk", "activityType": "runScript",
"targetMachineId": "guid", "credentialId": null,
"outputVariable": "diskCheck",
"config": { "script": "Get-PSDrive C", "timeoutSeconds": 60 }
}
}],
"edges": [{
"id": "e1", "source": "step-123", "target": "step-456",
"type": "labeled",
"data": {
"label": "On Success", "condition": "step-123.success", "disabled": false,
"controlPoints": { "cp1x": 240, "cp1y": 200, "cp2x": 360, "cp2y": 200 }
}
}]
}data.controlPoints überschreibt Auto-Routing. Fehlt es → bestehendes Routing greift. Implementierungsdetails: docs/claude-reference.md.
Layout-Styleguide für Workflow-JSONs: zuerst docs/workflow-styleguide.md lesen. Referenz-Beispiel: scripts/test-master-all-activities.json.
{{varName.output}}— Stdout{{varName.error}}— Stderr{{varName.success}}— Step-Erfolg ("true"/"false"){{varName.param.xxx}}— OutputParameter{{globals.NAME}}— Globale Variable{{manual.NAME}}— Trigger-Input des Laufs (dieselben Keys liegen zusätzlich alsparam.*des Trigger-Nodes an). DeklariertemanualTrigger-Parameter werden beim Laufstart mit ihremdefaultgeseedet, wenn der Aufrufer sie weglässt. Ein deklarierter Parameter ohne Default bleibt abwesend, und die Referenz scheitert.- Kein
outputVariable→ Step-ID wird verwendet:{{step-123.output}}
Ein veröffentlichter Wert hat genau einen Besitzer (SCOrch-Modell). {{aktivität.param.name}} ist verbindlich und löst bei jedem Nachfahren auf; den unqualifizierten Kurznamen ($name im runScript) bindet der Resolver nur bei genau einem Publisher auf dem Vorgängerpfad, bei zweien wird nichts gebunden. Linter-Code dup-published-param, gemeldet nur für selbst vergebene Namen (WorkflowDataBusAnalyzer.AuthoredParameters vs. TypeDerivedParameters). Details: docs/workflow-designer-features.md.
Contract-Garantie: Drei Muster im VariableResolver — GlobalsPattern, ManualPattern und StepPattern mit genau vier Tails (output, error, success, param.X). Andere Tails bleiben Literal; unresolved → granulare Diagnostik je Namespace (StepRunner T-7.1). Das Globals-Muster gilt auch für runScript/Custom Activities, weil ein nicht existierendes Global nie legitimer Skripttext ist. Scheitert das Laden der Globals, endet ein Lauf, der Globals referenziert, vor dem ersten Step als Failed. Ein neuer Namespace braucht ein eigenes Muster — ein frei gewählter Tail kann von StepPattern prinzipiell nicht getroffen werden.
Sichtbarkeits-Scope (Ahnen-only): Ein Step sieht ausschließlich Ergebnisse seiner Graph-Vorgänger (AncestorIndex + AncestorScopedResults). Eine Referenz auf einen Knoten aus einem parallelen Zweig löst nie auf — auch nicht, wenn dieser Zweig zufällig schon fertig ist. Ein Ahne ohne Ergebnis bleibt unauflösbar; bei junction/waitAny und übersprungenen Knoten ist das korrekt so.
Out-of-Scope-Gate gilt auch für runScript/Custom Activities. Beide sind von der allgemeinen T-7.1-Prüfung ausgenommen (ein übriges {{...}} kann legitimer Skripttext sein) — nicht aber vom Cross-Branch-Fall. Maßgeblich ist die Graph-Zugehörigkeit, nicht ob der Knoten schon ein Ergebnis hat; sonst wird das Gate zum Rennen. Tippfehler/unbekannte Steps bleiben tolerant.
Strukturierter Output: runScript captured die Variablen, die das Skript selbst zuweist, als param.*. $hostName = ... → {{step.param.hostName}}. Nicht dabei: durchgereichte Upstream-Parameter und PowerShell-Automatiken/Preference-Variablen (Liste in NodePilot.Core.Activities.PowerShellReservedVariables). Umgesetzt über zwei geschachtelte Scopes im Wrapper: Injektion außen, User-Skript innen.
RunScript Auto-Quoting: {{step.output}} wird als Single-Quoted String eingesetzt. Im Script $x = {{step.output}} schreiben, NICHT $x = '{{step.output}}'.
RunScript Erfolg (fehler-basiert, lokal wie remote): Ein Step scheitert an jedem PowerShell-Fehlereintrag (!HadErrors; der 5.1-Prozess-Bootstrap spiegelt das über den ERROR-Marker), natives stderr beendet in 5.1 das Skript. Ein exit N macht den Step nicht rot; opt-in config.successExitCodes macht non-zero Codes wieder zum Fehlschlag. Der Exit-Code liegt als {{step.param.exitCode}} an und meint das letzte native Kommando dieses Skripts — der Wrapper setzt $LASTEXITCODE und $Error vorher zurück, weil beide sonst im prozesslang offenen Runspace-Pool aus einem fremden Lauf überleben. Engine-Asymmetrie: ein script-eigenes exit N ist nur im Prozess/isoliert-Pfad sichtbar (Runspace kann exit nicht beobachten → 0). Ein Parse-Fehler ist auf jeder Engine rot: das Fehlen des ###NODEPILOT_START###-Markers werten die Prozess-Engines als „Skript lief nie". Gating in RunScriptActivity.
stepId.success/stepId.failed— Shortcutnull/ leer — Immerdisabled: true— übersprungen; Target-Node wird dadurch nicht zum Root, und sind alle eingehenden Kanten disabled →SkippedconditionExpression— Typcomparison(==, !=, <, >, <=, >=, contains, startsWith, endsWith, matches, isEmpty, isNotEmpty, isTrue, isFalse),group(AND/OR),not. Operanden:variableoderliteral.
Conditions sind fail-closed (ADR 0015): Save/Publish/Import lehnen eine unbrauchbare Condition mit 400 invalid-edge-condition ab (EdgeConditionValidator in Core); zur Laufzeit bricht der Scheduler den Lauf mit Kantenbezug ab, statt die Kante still zu überspringen. Ein Variablen-Operand ohne Wert ist unentscheidbar und erfüllt die Condition nie, auch nicht durch not. Alerting-Filter (source: event) lesen fehlende Felder als leer. Details: docs/adr/0015-fail-closed-edge-conditions.md.
startWorkflow ruft jeden enabled Workflow auf (frischer DI-Scope), unabhängig vom Trigger-Typ; parameters landen als manual.* im Child-Run, bei waitForCompletion: true (default) blockiert der Parent und spiegelt das Child-returnData als param.*. Max Call-Depth: 10 — gilt auch für forEach. GET /{id}/contract leitet Inputs/Outputs ab; By-name-Lookup (API, Engine, Trigger/Webhook): exact-case gewinnt, sonst case-insensitive, mehrdeutig → 409 bzw. Step-Fehler (WorkflowNameResolver). Details: docs/claude-reference.md § Contract-Derivation.
| Trigger | Backing |
|---|---|
scheduleTrigger |
Quartz cron |
fileWatcherTrigger |
FileSystemWatcher |
databaseTrigger |
Timer + SELECT-Polling |
eventLogTrigger |
EventLog.EntryWritten |
webhookTrigger |
HTTP /api/webhooks/{name}/{path} |
manualTrigger |
UI / API |
TriggerOrchestrator scannt alle 5 s. Trigger-Daten landen als manual.*-Variablen im Run + als param.* des Trigger-Nodes — kein trigger.*-Namespace. Key-Namen, Config-Vertrag, Liveness und Zustellung: docs/claude-reference.md §§ Trigger. Invarianten:
- Ein Trigger-Key lebt einmal in
Core/Triggers/und wird von Node-Executor (Engine/Triggers/) und Hintergrundquelle (Scheduler/Sources/) gelesen. Neuer Key = Settings-Klasse +activity-config-reference.json+ Designer-Feld, sonst brichtTriggerContractParityTests.databaseTriggerfeuert bei Sentinel-Änderung, nicht pro Zeile. - Kein Nachholen nach Neustart oder Failover: der durable Cursor dedupliziert, er backfillt nicht — Signale aus einem Stillstandsfenster werden verworfen (
nodepilot.scheduler.triggers.fires_skipped). ITriggerSource.Healthist ein reiner In-Memory-Read;unhealthy→ Quelle wird evictet und mit Backoff neu aufgebaut, einFileSystemWatcherlässt sich nicht in-place re-armen. Buffer-Overflow ist bewusst kein Fault.- webhookTrigger:
signatureMode=header(default) odernodepilot-hmac-v2; Legacyhmac(Body-only) wird abgelehnt.fieldMappingsextrahiert Body-Felder per JSONPath alsmanual.*.
- Event-driven: Queue +
inFlight-Dict. Roots = ausschließlich Trigger-Nodes (ADR 0006): ohne aktiven Trigger 0 Roots → ExecutionFailed, keininDegree==0-Fallback. Activities ohne eingehende Edge und Nodes mitdata.disabled: true→Skipped, Downstream ohne andere Quellen auch. Leerer Workflow läuft mit 0 Steps durch (Succeeded). - Expliziter Fan-in (ADR 0013): Nur eine
junctiondarf mehrere eingehende Edges haben; Designer und SCOrch-Import fügen bei Bedarf einewaitAll-Junction ein, die Strukturvalidierung schützt Save/Publish/API. - Cancellation:
_runningExecutionsDict (Guid → CTS). Per-Step-DI-Scope: eigener Scope pro Step → scope-lokalerDbContext. - Startup-Reconciler (ADR 0014):
Running/PausedundPendingohne Dispatch Intent →Cancelled;Pendingmit durablem Outbox-Intent wird neu geleast. - Kein Step überlebt seine Execution: Jede terminale Execution-Schreibung setzt anschließend alle noch
Running/Paused-Steps aufCancelled(ExecutionStateLifecycle.CancelOrphanedStepsAsync). Sonst bliebe ein Step für immerRunning— sichtbar als endloser Spinner und als dauerhaft zu hohe „aktive Läufe"-Badge, weil beide nurStepExecution.Statuslesen. - Step-Debugger:
POST /executemitdebug: true→ Breakpoints, SignalRStepPaused, Resume viaPOST /executions/{id}/resume.
Standard-Invocations (dotnet build|test, in src/nodepilot-ui die package.json-Scripts). Backend nutzt Central Package Management (Directory.Packages.props).
Konventionen: Tests sind Pflicht — jeder relevante Code-Change bringt passenden Test-Code in derselben Änderung. Naming MethodName_Scenario_ExpectedResult; Remote-Layer (WinRM) IMMER gemockt; DB-Tests SQLite in-memory. Coverage-Gate Backend Line >= 85 % / Branch >= 70 %, erzwungen in .github/workflows/ci.yml, das ist die einzige autoritative Zahl (Ratsche — nur anheben, nie senken); Frontend siehe vitest.config.ts. Messverfahren, Assembly-Filter und die [ExcludeFromCodeCoverage]-Regel: docs/claude-reference.md § Coverage-Messung.
Tests schreiben ≠ alle Tests ausführen. Die Pflicht oben gilt unverändert für das Schreiben; lokal ausgeführt wird nur, was die Änderung betrifft. Die Voll-Suite ist gemessen unverhältnismäßig (6.597 Backend-Testfälle, 242 Vitest-Dateien, 77 E2E-Specs — die beiden Frontend-Zahlen hält DocumentationCountsTests an der Dateiliste fest, die Backend-Zahl bleibt ein Handmaß) und liefert lokal kein neues Signal: das Netz hängt an ci.yml, das auf jedem PR und jedem Push auf main läuft (Coverage-Gate + E2E eingeschlossen).
Der Nightly ist kein verlässlicher zweiter Boden. Er läuft als Windows-Task um 22:00 gegen den ausgecheckten Baum und wird verpasst, sobald die Maschine dann aus ist. Wer sich auf ihn beruft, prüft vorher C:\temp\nodepilot-nightly\latest.md auf sein Datum.
Markdown-Ausnahme: Ein PR, der ausschließlich *.md oder docs/images/** anfasst, überspringt Frontend, Desktop, Database und E2E (changes-Job). Backend und docs-ui laufen immer, weil Markdown für sie eine Eingabe ist (DocumentationCountsTests, SettingsSchemaDocumentationTests, MonitoringDeploymentSecurityTests, Sprach-Parity-Guard). Pushes auf main laufen immer vollständig; jeder Fehlerpfad der Erkennung endet bei „alles ausführen".
Default bei Feature-Arbeit:
# Backend — ein Projekt, eine Klasse/ein Namespace
dotnet test tests/NodePilot.Engine.Tests --filter "FullyQualifiedName~WorkflowCallGraphBuilder"
# Frontend — einzelne Datei oder Verzeichnis
cd src\nodepilot-ui; npx vitest run src/__tests__/lib/opsTimeline.test.ts
# E2E — eine Spec, gegen laufenden Dev-Server (kein Build)
cd src\nodepilot-ui; npx playwright test e2e/operations.spec.ts --config=playwright.dev.config.tsEskalation nur bei Anlass, nie prophylaktisch:
- Scoped (Default) — Filter auf die geänderte Klasse/Komponente.
- Projekt-Suite (
dotnet test tests/NodePilot.Api.Tests) — wenn die Änderung innerhalb des Projekts quer liegt: geteilte Basisklasse, DI-Verdrahtung,Program.cs. - Voll-Suite — nur bei (a) expliziter Bitte des Users, (b) Release-Cut/Direct-Push auf main, (c) inhärent globaler Änderung (
Directory.Packages.props, Dependency-Bump, projektweites Refactoring).
Coverage lokal nie messen — --collect:"XPlat Code Coverage" bzw. npm run test:coverage sind CI-Jobs, kein lokaler Schritt.
Reporting: nicht „alle Tests grün", sondern welche gelaufen sind (Projekt + Filter + Anzahl). Was nicht lief, wird als „von CI abgedeckt" benannt, nicht verschwiegen.
Parity-/Drift-Tests erzwingen Konsistenz zwischen weit auseinanderliegenden Dateien und liegen über sechs Testprojekte verteilt. Deshalb: eine Auslöser-Fläche angefasst → genau diesen Test fahren, statt sicherheitshalber alles.
| Angefasst | Guard-Test | Projekt |
|---|---|---|
Activity + activity-config-reference.json + Frontend-Katalog-Spiegel |
ActivityCatalogTests, ActivityConfigReferenceTests, ActivityCatalogFrontendSyncTests |
Engine.Tests |
KnownProgramLaunchers / lib/knownProgramLaunchers.ts |
KnownProgramLaunchersFrontendSyncTests |
Engine.Tests |
| Neue EF-Migration / Designer-Postprocessing | MigrationDriftTests |
Data.Tests |
*.csproj-Referenzen / Dep-Graph |
DependencyDirectionTests |
Api.Tests |
| Neuer Audit-Code | AuditActionsCatalogTests |
Api.Tests |
| API-DTO (+ CLI-Spiegel) | ApiDtoParityTests |
Cli.Tests |
| Trigger-Config-Key | TriggerContractParityTests |
Engine.Tests |
SettingsSchema.cs / Admin-Settings-UI |
AdminSettingsFrontendSyncTests, SettingsSchemaDocumentationTests |
Api.Tests |
| AI-Prompt-Katalog | PromptCatalogDriftTest |
Ai.Tests |
| Alerting-Katalog / System-Policies | AlertingCatalogFrontendSyncTests, SystemAlertCatalogTests |
Engine.Tests |
Workflow-Analyzer (WorkflowAnalyzer/WorkflowDataBusAnalyzer in Core — MCP und AI-Chat) |
WorkflowAnalyzerFrontendParityTests |
Engine.Tests |
| Template-Grammatik / Variable-Resolution | TemplateGrammarParityTests |
Engine.Tests |
| Metrics-Dashboard-Katalog | MetricsDashboardCatalogTests |
Api.Tests |
| Zahl-tragende Doku-Behauptung (MCP-Tool-Zahl, Activity-Typen, Skins), eine neue Vitest-/E2E-Datei oder der Umfang dieser Datei | DocumentationCountsTests |
Mcp.Tests |
RequestSizeLimit an /import//import-scorch oder die Upload-Gates in WorkflowsPage.tsx |
ImportSizeLimitFrontendSyncTests |
Api.Tests |
LLM-Profil-Defaults (LlmProfileOptions, LlmProfileSettingsDto, SettingsSections.cs, IntegrationsSection.tsx) |
LlmProfileDefaultsTests |
Api.Tests |
vite.config.ts-Proxy / Dev-Ports |
AppSettingsHygieneTests |
Api.Tests |
OpenTelemetry-Block in Directory.Packages.props |
OpenTelemetryVersionParityTests |
Api.Tests |
Neues Testprojekt in NodePilot.slnx / coverage.runsettings |
TestRunSettingsTests |
Api.Tests |
Browser-Demo (demo/), Hub-/Doku-/Auth-Naht, root-absolute URL-Literale in src/ |
src/__tests__/demo/* + e2e-demo/*.spec.ts (Naht, Routing, alle zehn Touren) |
nodepilot-ui |
index.css / designer-atelier.css designer-light tokens |
designerLightParity.test.ts |
nodepilot-ui |
| Font-Tokens / Monaco-Stack | fontTokens.test.ts |
nodepilot-ui |
E2E (Playwright): hermetische Specs in src/nodepilot-ui/e2e/, alle APIs gemockt — Konventionen in src/nodepilot-ui/CLAUDE.md + src/nodepilot-ui/e2e/README.md. Desktop-Shell: eigene vitest-Suite und CI-Job desktop, siehe src/nodepilot-desktop/README.md. Nightly: scripts/nightly-tests.ps1 (Windows-Task, 22:00, Report nach C:\temp\nodepilot-nightly\); Zeit ändern per scripts/register-nightly-task.ps1 -Time HH:mm.
Beide sind reine HTTP-Clients gegen die REST-API — kein eigener Backend-Pfad; der MCP-Server ergänzt In-Proc-Analyse gegen NodePilot.Core (102 Tools, 3 Resources, stdio). Packaging, Anmeldewege, geteilte Client-Infrastruktur und Tool-Katalog: src/NodePilot.Cli/CLAUDE.md, src/NodePilot.Mcp/CLAUDE.md, docs/mcp-server.md.
Jeder neue API-Endpoint braucht beide Clients (Guard: EndpointClientCoverageTests).
| Endpoint | Admin | Operator | Viewer |
|---|---|---|---|
GET /api/{workflows,executions,machines} |
✓ | ✓ | ✓ |
POST /api/workflows, PUT, POST /{id}/duplicate|execute |
✓ | ✓ | ✗ |
POST /api/machines, PUT |
✓ | ✓ | ✗ |
GET|POST|PUT /api/credentials |
✓ | ✓ | ✗ |
POST /api/executions/{id}/cancel |
✓ | ✓ | ✗ |
DELETE /{workflows,machines,credentials}/{id} |
✓ | ✗ | ✗ |
DELETE /api/shared-workflow-folders/{id} |
Folder-Edit, nur leer |
Folder-Edit, nur leer |
✗ |
DELETE /api/shared-workflow-folders/{id}?recursive=true |
✓ | ✗ | ✗ |
GET /api/alerting/rules, POST /preview-filter |
✓ | ✓ | ✗ |
POST/PUT/DELETE /api/alerting/rules, POST /{id}/enable|disable|test-fire |
✓ | ✗ | ✗ |
POST /api/trigger/{name} |
API-Key via X-Api-Key-Header |
Ordner-Löschen hat zwei Sicherheitsgrenzen: Ein leerer Ordner bleibt eine Folder-Edit-Mutation. ?recursive=true entfernt auch Workflows und deren Execution-Historie und ist deshalb wie DELETE /api/workflows/{id} global Admin-only. Die Folder-Capabilities liefern dafür canDelete getrennt von canEdit; die UI darf den rekursiven Delete nicht aus canEdit ableiten.
Der Global-Variablen-Ordnerbaum kennt dieselbe Mechanik, bleibt aber Admin-only — dort gibt es kein Per-Ordner-RBAC, an dem sich lockern ließe.
- Session: JWT-Cookie + CSRF-Token, absolute Lebensdauer 8h (
Authentication:SessionAbsoluteLifetimeHours; Refresh verlängert sie nicht),jti-Revocation. Auth-Pfade (Local-BCrypt mit ProduktionsdefaultLocalLoginMode=BreakGlassOnly, LDAP, Windows-Negotiate, OIDC + SCIM):docs/ldap-windows-sso.md; Detailsdocs/claude-reference.md§ Security. - External Trigger:
X-Api-Keygegen SHA-256-Hashes unterExternalTrigger:Keys:<id>; jeder Eintrag hat eine GUID-onlyAllowedWorkflowIds-Liste. DieKeys-Map kommt atomar aus dem höchstprioren Provider (Keys: {}widerruft alle niedrigeren); Scope-Arrays ebenso ([]= deny-all). Der Workflow braucht zusätzlich einen aktivenmanualTrigger. Legacy-ApiKeyist ohne eigene Liste inert. - Idempotency:
POST /api/trigger/{name}akzeptiertIdempotency-Key, Replay nur innerhalb desselben Key-Principals und Workflows;Pending+ Reservation + Dispatch Intent entstehen in einer Transaktion und überleben Failover. - Rate-Limiting (per-IP, Sliding-Window): login 50/Min, refresh 20/Min, webhook 60/Min, trigger 30/Min, ai-generate 20/Min, audit 60/Min, alerting-heavy 20/Min, backup 10/Min.
- Output-Redaction:
OutputRedactormaskiert Secrets. Immer aktiv. Custom-Patterns viaLogging:Redaction:Patterns. - Localhost-Bypass / Operator-Trust: ohne Credentials läuft in-process unter der NodePilot-Service-Identität.
Operatorist bewusst ein vertrauenswürdiger Automation-Author und darf solchen Workflow-Code publizieren/ausführen. Folder-RBAC ist keine Code-Sandbox. Produkt-Feature, keinen Require-Target-Guard einziehen. - Security-Headers (Non-Dev): HSTS, CSP, X-Frame-Options=DENY, nosniff, Referrer-Policy. SignalR-Auth über das httpOnly
np_auth-Cookie beim WebSocket-Upgrade (nur/hubs/); kein?access_token=-Querystring. - REST-API-Proxy:
RestApi:Proxy:Enabled(defaultfalse). Per-Step-Override viaproxyMode.
Hardening-Flags — Tabelle mit Defaults und Wirkung: docs/claude-reference.md. Zwei Feinheiten: Webhook:RequireSecret ist default true (fehlender Key liest als true), und WaitForCondition:AllowedHosts ist eine eigene Liste für die Probes portOpen/httpOk, bewusst getrennt von RestApi:AllowedHosts und alleinige Autorität für beide Probe-Typen.
Admin-Settings-Saves persistieren atomar nach appsettings.runtime.json (reloadOnChange: true). Pro Sektion trägt SettingsSchema.cs ein IsHotReloadable-Flag; nur false-Sektionen setzen den Restart-Marker. 13 Sektionen sind hot-reloadable, 9 restart-pflichtig; harter Kern (JWT, DB, Kestrel, Cluster/HA, Remote:Provider) bleibt boot-fixed. Matrix: docs/claude-reference.md § Hot-Reload-Matrix.
Consumer-Regel: hot-reloadable Werte via IOptionsMonitor<T>.CurrentValue bzw. rohes IConfiguration pro Use/Pass lesen — nie IOptions<T>.Value-Snapshot.
Dimensionierung: Performance:ManualTuning (default false) entscheidet, ob Runspace-, Step-, Threading- und Dispatch-Worker-Zahlen aus erkannter CPU+RAM abgeleitet oder verbatim aus der Config genommen werden; restart-pflichtig. Engine:MaxConcurrentExecutions:* ist ausgenommen (Sicherheits-Cap, nicht Tuning). Details: docs/performance-improvements.md.
IAuditWriter injizieren, await _audit.LogAsync(AuditActions.VerbNomen, "Resource", resourceId, detailsJson, ct) nach SaveChanges. Schreibfehler darf normale Mutation nie abbrechen. Ausnahme: DB-Admin-Write-SQL läuft fail-closed — ohne vorab persistierten DBADMIN_SQL_WRITE_ATTEMPTED-Eintrag wird das SQL nicht ausgeführt. Passwörter/Secrets nie in Details. Codes folgen VERB_NOMEN und sind zentral in NodePilot.Core.Audit.AuditActions registriert — nie ein rohes String-Literal am Call-Site (Guard: AuditActionsCatalogTests). Code-Übersicht und Pipeline: docs/claude-reference.md § Audit-Codes.
Opt-in (Llm:Enabled=false default), OpenAI-kompatibler Endpunkt, Rate-Limit 20/min/IP. Volle Doku: docs/ai-features.md + docs/claude-reference.md § KI-Features.
POST /api/ai/generate-script(Admin/Op, SSE-Streaming) +POST /api/ai/generate-workflow(Admin/Op, JSON).POST /api/ai/chat(alle Rollen, SSE) — Workflow-Assistent; Proposals nur Admin/Op, Merge per Node-ID aufs unredigierte Original. Secrets werden vor jedem LLM-Call redigiert (WorkflowSecretRedactor).POST /api/ai/knowledge/ask(SSE) — globaler Wissens-Assistent in/ai-chat, vier admin-toggelbare Quellen (SektionAiKnowledge). DB / text2sql ausschließlich globaler Admin (zentraler Guard überISqlKnowledgeReader); Folder-Grants erhöhen nie auf Raw-SQL.llmQuery-Activity: Engine-lokal, per-Node-Overrides, gated durchLlm:Enabled; einziger BaseUrl-Validierungspunkt istLlmEndpointGuard.
Profile: Llm:Profiles:<id> ist ein Objekt gekeyt nach unveränderlicher Id, kein Array; Llm:ActiveProfileId wählt das aktive — kein „nimm das erste"-Fallback (503 LLM_NO_ACTIVE_PROFILE). Keine scoped ILlmClient-Registrierung; Consumer nehmen ILlmClientFactory. Wire-Dialekte, Timeouts, Proxy und Hardening: docs/ai-features.md.
GET /{id}/export / GET /export / POST /import, Envelope nodepilot-workflow-export/v1. Import erzeugt neue Einträge immer disabled; Ziel-Folder via ?folderId=, RBAC = Edit darauf. Secrets werden redigiert (***) — Teilen-Artefakt, kein DR. SCOrch-Import (POST /import-scorch): docs/claude-reference.md § SCOrch-Import.
Getrennt vom Workflow-Export: portables, passphrasenverschlüsseltes Konfigurations-Backup (.npbackup, Envelope nodepilot-system-backup/v4), Admin-only, fail-closed bei unvollständigem Export oder Restore; keine Execution-History und kein vollständiges DR. UI /backup, CLI np backup. Details: docs/adr/0001-system-configuration-backup-restore.md + docs/claude-reference.md.
Rollout über deploy/-Skripte (Freigabe-Regel unter Projekt starten). Doku: deploy/README.md; Architektur, Config-Keys und Stolperfallen: docs/claude-reference.md § Production Deployment. Desktop-App (Electron, deploy/desktop/, Deployment:Mode=Desktop): relaxiert nur loopback-DB-TLS + Kestrel-ListenLocalhost, der Rest bleibt Production-gehärtet — deploy/desktop/README.md.
Vor jedem Release Pflicht: die Lab-Matrix scripts/release-lab/ gegen die signierten Artefakte aus out\ — Server-Setup in allen Identitäts-/DB-Kombinationen frisch und als Update, jeweils mit Deinstallation, dazu das Desktop-Setup auf dem Lab-Client, danach die Test-Suite scripts/test-suite/ auf der gehärteten Lab-Instanz mit fail=0. Ohne grünen Lauf aller drei kein Tag (RELEASING.md Schritt 6). Der Computer-Use-Lauf scripts/release-lab/computer-use/ ist experimentell und kein Gate.