Download app.js from smlflg/harness-tutorials: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/smlflg/harness-tutorials/resolve/main/app.js
- Command line
-
hf download hf://smlflg/harness-tutorials/app.js
-
curl -L -o app.js https://huggingface.co/smlflg/harness-tutorials/resolve/main/app.js
16.9 kB
| const stages = { | |
| owner: { | |
| kicker: "01 / Human Ownership", | |
| title: "Der Mensch setzt Ziel, Risiko und Abbruchlinie.", | |
| body: "Agenten arbeiten erst sinnvoll, wenn Ziel, Nicht-Ziel und Human-only Entscheidungen sichtbar sind.", | |
| active: 0, | |
| }, | |
| scope: { | |
| kicker: "02 / Scope Boundary", | |
| title: "Der Auftrag wird klein genug, dass Erfolg pruefbar ist.", | |
| body: "Das Harness trennt Wunsch, Kontext, Nicht-Ziel und erlaubte Aktionen, bevor der Agent arbeitet.", | |
| active: 1, | |
| }, | |
| delegate: { | |
| kicker: "03 / Delegation Card", | |
| title: "Das Tool bekommt Rolle, Grenze und Artefaktpflicht.", | |
| body: "Die Delegation Card ersetzt den diffusen Prompt durch Ziel, Scope, Human Gates und Verifikation.", | |
| active: 2, | |
| }, | |
| verify: { | |
| kicker: "04 / Verifier Loop", | |
| title: "Done ist erst wahr, wenn Belege danebenliegen.", | |
| body: "Tests, Diffs, Dateien, Screenshots oder Reports schlagen die Erfolgserzaehlung des Agenten.", | |
| active: 3, | |
| }, | |
| handoff: { | |
| kicker: "05 / Next Action", | |
| title: "Am Ende steht eine Entscheidung, kein Rohlog.", | |
| body: "Der Mensch sieht, was geaendert wurde, welche Belege existieren, was offen bleibt und was als naechstes passiert.", | |
| active: 4, | |
| }, | |
| }; | |
| const taskData = { | |
| docs: { | |
| goal: "Baue oder verbessere ein Tutorial-Artefakt.", | |
| artifact: "Markdown-Dokumente, Quellenliste, Completion Checklist", | |
| verify: "rg-Coverage, Link-Check, git diff --check", | |
| }, | |
| bug: { | |
| goal: "Reproduziere und behebe einen konkreten Fehler.", | |
| artifact: "kleiner Diff, fokussierter Test oder Repro-Log", | |
| verify: "Fehler reproduziert, Fix ausgefuehrt, Test/Smoke-Check gruen", | |
| }, | |
| ui: { | |
| goal: "Verbessere eine konkrete UI-Ansicht.", | |
| artifact: "HTML/CSS/JS-Diff, Screenshot, responsive Check", | |
| verify: "Headless Screenshot, mobile/desktop Sichtpruefung, keine Overlaps", | |
| }, | |
| audit: { | |
| goal: "Pruefe ein Repo oder Ergebnis read-only.", | |
| artifact: "Audit-Report mit Findings, Belegen und Next Action", | |
| verify: "Datei-/Zeilenbelege, keine Schreiboperationen, offene Risiken markiert", | |
| }, | |
| }; | |
| const toolData = { | |
| codex: { | |
| name: "Codex", | |
| source: "source: official OpenAI docs + local tutorial", | |
| role: "Lokaler Coding-Agent fuer Repo-Arbeit, Review, Tests und sichtbare Diffs.", | |
| use: "bounded implementation, repo audit, tests, docs", | |
| needs: "AGENTS.md, konkrete Aufgabe, Done when, Verifikation", | |
| artifact: "Diff, Tests, Verification Report, Next Action", | |
| gate: "Scope drift, dependencies, destructive actions, deploy", | |
| promptHandle: "Codex arbeitet im aktuellen Repo und liefert Diffs, Checks und Handoff.", | |
| }, | |
| claude: { | |
| name: "Claude Code", | |
| source: "source: local HAI notes + Claude Code practice layer", | |
| role: "Tiefer Codebase-Arbeiter fuer Planung, Implementierung und Review gegen Repo-Kontext.", | |
| use: "komplexe Codebase-Arbeit, Architekturfragen, laengere Debug-Loops", | |
| needs: "CLAUDE.md/Repo-Regeln, kleine Tasks, klare Tests, sichtbarer Diff", | |
| artifact: "Plan, Patch, Tests, Review Notes", | |
| gate: "Engine-Grenzen ehrlich markieren, breite Refactors und Tool-Rechte freigeben lassen", | |
| promptHandle: "Claude Code nutzt Repo-Kontext, soll aber mit klarer Human Gate- und Testpflicht arbeiten.", | |
| }, | |
| hermes: { | |
| name: "Hermes", | |
| source: "source: local Zustand-Hermes", | |
| role: "Control Plane mit Profilen, Skills, Memory, Verifiern und Owner-Schutz.", | |
| use: "Projektzustand laden, Profil waehlen, kleine naechste Schritte fuehren", | |
| needs: "explizites Profil, Scope, Artefaktpflicht, Verifier oder Decision-Control", | |
| artifact: "Owner Packet, Delegation Card, Verifier Report, Resume State", | |
| gate: "Samuel entscheidet Produkt, Scope, Architektur, irreversible Freigaben", | |
| promptHandle: "Hermes soll als Control Plane arbeiten: Profil waehlen, Briefing bauen, bounded delegieren.", | |
| }, | |
| openclaw: { | |
| name: "OpenClaw", | |
| source: "source: local OpenClaw commands + architecture notes", | |
| role: "Router/Gateway-System fuer Agenten, Sessions, Devices und komplexe Scout-Build-Verify-Flows.", | |
| use: "Frontdoor-Routing, Gateway-Status, Agent-Sessions, komplexe Projektpipeline", | |
| needs: "router/hermes/specialist topology, gateway health, session choice, explicit approvals", | |
| artifact: "Routing Decision, Session State, Scout-Build-Verify Trace", | |
| gate: "gateway/config/devices/auth commands and new specialists need explicit human approval", | |
| promptHandle: "OpenClaw soll Routing und Sessions kontrolliert fuehren; komplexe Aufgaben gehen Scout -> Build -> Verify.", | |
| }, | |
| opencode: { | |
| name: "OpenCode", | |
| source: "source: local OpenCode tutorials + handoff docs", | |
| role: "Schneller Builder und Reality-Check-Agent mit strenger Scope-Anweisung.", | |
| use: "one-shot Drafts, Gegenentwurf, schnelle Dateistrukturen, Implementierung", | |
| needs: "vollstaendiger Kontext pro Run, Output-Pfad, Nicht-Ziele, Tests", | |
| artifact: "kleiner Diff, Risiken, bewusst weggelassene Features", | |
| gate: "wenn der erste Run falsch scoped ist: neue Session mit besserem Briefing statt Reparatur-Chat", | |
| promptHandle: "OpenCode bekommt einen engen one-shot Auftrag mit Output-Pfad und expliziten Nicht-Zielen.", | |
| }, | |
| pi: { | |
| name: "Pi", | |
| source: "source: generic HAI profile; no canonical local tool source found", | |
| role: "Persoenlicher Intake- und Denkdialog fuer unstrukturierte menschliche Ausgangslage.", | |
| use: "Wunsch klaeren, emotionale/kognitive Lage sortieren, Owner Packet vorbereiten", | |
| needs: "keine Writes, keine Done-Claims, klare Uebergabe an Arbeitsagent", | |
| artifact: "Problemklassifikation, Human-only Liste, naechste Frage", | |
| gate: "Reflexion ist kein Beleg; Umsetzung startet erst in einem anderen bounded Tool", | |
| promptHandle: "Pi soll nur klaeren und verdichten, nicht implementieren oder Erfolg behaupten.", | |
| }, | |
| conductor: { | |
| name: "Conductor", | |
| source: "source: generic HAI orchestration profile; no canonical local tool source found", | |
| role: "Orchestrierungsrolle fuer mehrere Worker, ohne selbst die Ownership zu uebernehmen.", | |
| use: "Run-Leitung, Task-Slicing, Worker-Reihenfolge, Handoff und Integration", | |
| needs: "ein Lead, getrennte Write Scopes, Stop Rules, gemeinsamer Verifier", | |
| artifact: "Run Plan, Worker Contracts, Integration Checklist", | |
| gate: "kein Fanout ohne disjunkte Schreibbereiche und klares Integrationsartefakt", | |
| promptHandle: "Conductor koordiniert nur bounded Worker und muss Human Gates und Verifier sichtbar halten.", | |
| }, | |
| }; | |
| const riskData = { | |
| low: { | |
| label: "bounded", | |
| meter: "74%", | |
| gate: "Stoppe bei Scope Drift oder fehlenden Belegen.", | |
| }, | |
| medium: { | |
| label: "gated", | |
| meter: "58%", | |
| gate: "Frage vor Dependencies, breiteren Datei-Aenderungen und unklaren Tests.", | |
| }, | |
| high: { | |
| label: "human gate required", | |
| meter: "34%", | |
| gate: "Keine Ausfuehrung ohne menschliche Freigabe fuer Risiko, Auth, Deploy oder destruktive Aktionen.", | |
| }, | |
| }; | |
| const canvas = document.querySelector("#harnessCanvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const stageButtons = Array.from(document.querySelectorAll("[data-stage]")); | |
| const flowNodes = Array.from(document.querySelectorAll(".flow-node")); | |
| const stageKicker = document.querySelector("#stageKicker"); | |
| const stageTitle = document.querySelector("#stageTitle"); | |
| const stageBody = document.querySelector("#stageBody"); | |
| const promptOutput = document.querySelector("#promptOutput"); | |
| const readinessMeter = document.querySelector("#readinessMeter"); | |
| const readinessLabel = document.querySelector("#readinessLabel"); | |
| const copyPrompt = document.querySelector("#copyPrompt"); | |
| const toolSelect = document.querySelector("#toolSelect"); | |
| const toolTabs = Array.from(document.querySelectorAll("[data-tool]")); | |
| const toolSource = document.querySelector("#toolSource"); | |
| const toolName = document.querySelector("#toolName"); | |
| const toolRole = document.querySelector("#toolRole"); | |
| const toolUse = document.querySelector("#toolUse"); | |
| const toolNeeds = document.querySelector("#toolNeeds"); | |
| const toolArtifact = document.querySelector("#toolArtifact"); | |
| const toolGate = document.querySelector("#toolGate"); | |
| const gateInputs = Array.from(document.querySelectorAll(".gate-list input")); | |
| const gateVerdict = document.querySelector(".gate-verdict"); | |
| const gateMode = document.querySelector("#gateMode"); | |
| const gateTitle = document.querySelector("#gateTitle"); | |
| const gateBody = document.querySelector("#gateBody"); | |
| let currentStage = "owner"; | |
| let currentTool = "codex"; | |
| let currentTask = "docs"; | |
| let currentRisk = "low"; | |
| let pointer = { x: 0.72, y: 0.38 }; | |
| function setStage(stageId) { | |
| const stage = stages[stageId]; | |
| if (!stage) return; | |
| currentStage = stageId; | |
| stageKicker.textContent = stage.kicker; | |
| stageTitle.textContent = stage.title; | |
| stageBody.textContent = stage.body; | |
| stageButtons.forEach((button) => { | |
| button.classList.toggle("is-active", button.dataset.stage === stageId); | |
| }); | |
| flowNodes.forEach((node) => { | |
| node.classList.toggle("is-active", node.dataset.stage === stageId); | |
| }); | |
| } | |
| function buildPrompt() { | |
| const task = taskData[currentTask]; | |
| const risk = riskData[currentRisk]; | |
| const tool = toolData[currentTool]; | |
| readinessMeter.style.width = risk.meter; | |
| readinessLabel.textContent = risk.label; | |
| promptOutput.textContent = [ | |
| "Tool:", | |
| tool.name, | |
| "", | |
| "Ziel:", | |
| task.goal, | |
| "", | |
| "Tool-Rolle:", | |
| "- " + tool.promptHandle, | |
| "", | |
| "Kontext:", | |
| "- Nutze das bestehende Repo und lies betroffene Dateien zuerst.", | |
| "- Behandle Chat als Steuerpult, nicht als Arbeitsflaeche.", | |
| "- Tool braucht: " + tool.needs + ".", | |
| "", | |
| "Scope:", | |
| "- Arbeite nur an dieser Aufgabe.", | |
| "- Erzeuge ein pruefbares Artefakt: " + task.artifact + ".", | |
| "- Tool-Artefakt: " + tool.artifact + ".", | |
| "", | |
| "Nicht tun:", | |
| "- keine Commits ohne Freigabe", | |
| "- keine neuen Dependencies ohne Gate", | |
| "- keine Scope-Erweiterung ohne Rueckfrage", | |
| "", | |
| "Human Gates:", | |
| "- " + tool.gate + ".", | |
| "- " + risk.gate, | |
| "- Stoppe bei destruktiven Aktionen, Secrets/Auth/Deploy oder unklarem Arbeitsordner.", | |
| "", | |
| "Verifikation:", | |
| "- " + task.verify, | |
| "- Liefere einen Verification Report mit Belegen.", | |
| "", | |
| "Abschluss:", | |
| "- Geaendert", | |
| "- Belege", | |
| "- Checks", | |
| "- Offen", | |
| "- Naechste Entscheidung", | |
| ].join("\n"); | |
| } | |
| function setTool(toolId) { | |
| const tool = toolData[toolId]; | |
| if (!tool) return; | |
| currentTool = toolId; | |
| toolSource.textContent = tool.source; | |
| toolName.textContent = tool.name; | |
| toolRole.textContent = tool.role; | |
| toolUse.textContent = tool.use; | |
| toolNeeds.textContent = tool.needs; | |
| toolArtifact.textContent = tool.artifact; | |
| toolGate.textContent = tool.gate; | |
| toolSelect.value = toolId; | |
| toolTabs.forEach((tab) => { | |
| tab.classList.toggle("is-active", tab.dataset.tool === toolId); | |
| }); | |
| buildPrompt(); | |
| } | |
| function updateGate() { | |
| const active = gateInputs.filter((input) => input.checked).map((input) => input.value); | |
| const hardStop = active.some((value) => ["dependency", "destructive", "secret", "scope"].includes(value)); | |
| gateVerdict.classList.toggle("is-stop", hardStop); | |
| if (hardStop) { | |
| gateMode.textContent = "STOP"; | |
| gateTitle.textContent = "Erst Human Gate, dann Codex."; | |
| gateBody.textContent = "Mindestens ein harter Gate-Faktor ist aktiv. Der Mensch entscheidet Scope, Risiko oder Freigabe, bevor der Agent weiterarbeitet."; | |
| } else if (active.includes("unknown")) { | |
| gateMode.textContent = "WARN"; | |
| gateTitle.textContent = "Der Agent darf nur diagnostizieren."; | |
| gateBody.textContent = "Unklare Tests oder unbekannte Fehler brauchen erst Reproduktion und Beleg, nicht direkt einen Fix."; | |
| } else { | |
| gateMode.textContent = "RUN"; | |
| gateTitle.textContent = "Das Tool darf bounded arbeiten."; | |
| gateBody.textContent = "Kein harter Human Gate aktiv. Der Auftrag braucht trotzdem Artefaktpflicht und Verifikation."; | |
| } | |
| } | |
| function resizeCanvas() { | |
| const ratio = window.devicePixelRatio || 1; | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = Math.max(1, Math.floor(rect.width * ratio)); | |
| canvas.height = Math.max(1, Math.floor(rect.height * ratio)); | |
| ctx.setTransform(ratio, 0, 0, ratio, 0, 0); | |
| } | |
| function drawCanvas(time) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const width = rect.width; | |
| const height = rect.height; | |
| ctx.clearRect(0, 0, width, height); | |
| ctx.fillStyle = "#0d100c"; | |
| ctx.fillRect(0, 0, width, height); | |
| const grid = 46; | |
| ctx.strokeStyle = "rgba(238, 228, 207, 0.05)"; | |
| ctx.lineWidth = 1; | |
| for (let x = -grid; x < width + grid; x += grid) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + ((time / 80) % grid), 0); | |
| ctx.lineTo(x + ((time / 80) % grid), height); | |
| ctx.stroke(); | |
| } | |
| for (let y = 0; y < height; y += grid) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(width, y); | |
| ctx.stroke(); | |
| } | |
| const nodes = [ | |
| { label: "OWNER", x: 0.62, y: 0.24, color: "#a4d65e" }, | |
| { label: "SCOPE", x: 0.78, y: 0.36, color: "#4fa997" }, | |
| { label: toolData[currentTool].name.toUpperCase().slice(0, 9), x: 0.68, y: 0.54, color: "#547aa5" }, | |
| { label: "VERIFY", x: 0.84, y: 0.66, color: "#c76b42" }, | |
| { label: "NEXT", x: 0.55, y: 0.72, color: "#9b82c7" }, | |
| ]; | |
| const activeIndex = stages[currentStage].active; | |
| ctx.lineWidth = 2; | |
| for (let i = 0; i < nodes.length - 1; i += 1) { | |
| const from = nodes[i]; | |
| const to = nodes[i + 1]; | |
| const fx = from.x * width; | |
| const fy = from.y * height; | |
| const tx = to.x * width; | |
| const ty = to.y * height; | |
| ctx.strokeStyle = i < activeIndex ? "rgba(164, 214, 94, 0.7)" : "rgba(238, 228, 207, 0.18)"; | |
| ctx.beginPath(); | |
| ctx.moveTo(fx, fy); | |
| ctx.lineTo((fx + tx) / 2, fy); | |
| ctx.lineTo((fx + tx) / 2, ty); | |
| ctx.lineTo(tx, ty); | |
| ctx.stroke(); | |
| } | |
| nodes.forEach((node, index) => { | |
| const x = node.x * width + (pointer.x - 0.5) * 16; | |
| const y = node.y * height + (pointer.y - 0.5) * 16; | |
| const active = index === activeIndex; | |
| const size = active ? 88 : 72; | |
| ctx.save(); | |
| ctx.translate(x, y); | |
| ctx.rotate(Math.PI / 4); | |
| ctx.fillStyle = active ? node.color : "rgba(238, 228, 207, 0.08)"; | |
| ctx.strokeStyle = active ? "rgba(255,255,255,0.62)" : "rgba(238, 228, 207, 0.22)"; | |
| ctx.lineWidth = active ? 2 : 1; | |
| ctx.fillRect(-size / 2, -size / 2, size, size); | |
| ctx.strokeRect(-size / 2, -size / 2, size, size); | |
| ctx.restore(); | |
| ctx.fillStyle = active ? "#0d100c" : "rgba(238, 228, 207, 0.72)"; | |
| ctx.font = "12px IBM Plex Mono, Consolas, monospace"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText(node.label, x, y + 4); | |
| }); | |
| requestAnimationFrame(drawCanvas); | |
| } | |
| stageButtons.forEach((button) => { | |
| button.addEventListener("click", () => setStage(button.dataset.stage)); | |
| }); | |
| toolTabs.forEach((button) => { | |
| button.addEventListener("click", () => setTool(button.dataset.tool)); | |
| }); | |
| toolSelect.addEventListener("change", () => setTool(toolSelect.value)); | |
| document.querySelectorAll("[data-task]").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| currentTask = button.dataset.task; | |
| document.querySelectorAll("[data-task]").forEach((item) => item.classList.toggle("is-active", item === button)); | |
| buildPrompt(); | |
| }); | |
| }); | |
| document.querySelectorAll("[data-risk]").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| currentRisk = button.dataset.risk; | |
| document.querySelectorAll("[data-risk]").forEach((item) => item.classList.toggle("is-active", item === button)); | |
| buildPrompt(); | |
| }); | |
| }); | |
| copyPrompt.addEventListener("click", async () => { | |
| try { | |
| await navigator.clipboard.writeText(promptOutput.textContent); | |
| copyPrompt.textContent = "Copied"; | |
| setTimeout(() => { | |
| copyPrompt.textContent = "Copy"; | |
| }, 1200); | |
| } catch { | |
| copyPrompt.textContent = "Select"; | |
| setTimeout(() => { | |
| copyPrompt.textContent = "Copy"; | |
| }, 1200); | |
| } | |
| }); | |
| gateInputs.forEach((input) => input.addEventListener("change", updateGate)); | |
| document.querySelectorAll(".run-step").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| const detail = button.nextElementSibling; | |
| const open = button.classList.contains("is-open"); | |
| document.querySelectorAll(".run-step").forEach((item) => item.classList.remove("is-open")); | |
| document.querySelectorAll(".run-detail").forEach((item) => item.classList.remove("is-open")); | |
| if (!open) { | |
| button.classList.add("is-open"); | |
| detail.classList.add("is-open"); | |
| } | |
| }); | |
| }); | |
| window.addEventListener("resize", resizeCanvas); | |
| window.addEventListener("pointermove", (event) => { | |
| pointer = { | |
| x: event.clientX / Math.max(1, window.innerWidth), | |
| y: event.clientY / Math.max(1, window.innerHeight), | |
| }; | |
| }); | |
| resizeCanvas(); | |
| setStage("owner"); | |
| setTool("codex"); | |
| updateGate(); | |
| requestAnimationFrame(drawCanvas); | |