@netnodeag/kraftwerk 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/ui.js +32 -55
  3. package/dist/inspector/context.d.ts +5 -0
  4. package/dist/inspector/context.js +19 -0
  5. package/dist/inspector/runner.d.ts +13 -0
  6. package/dist/inspector/runner.js +66 -0
  7. package/dist/inspector/runs.d.ts +57 -0
  8. package/dist/inspector/runs.js +240 -0
  9. package/dist/inspector/server.d.ts +8 -0
  10. package/dist/inspector/server.js +186 -0
  11. package/dist/inspector/workflows.d.ts +45 -0
  12. package/dist/inspector/workflows.js +186 -0
  13. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  14. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  15. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  16. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  17. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  18. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  19. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
  20. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
  21. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  22. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  23. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  24. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  25. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  26. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  27. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  28. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  29. package/inspector/dist/assets/index-BCSWkslF.css +1 -0
  30. package/inspector/dist/assets/index-D6WuziFQ.js +9 -0
  31. package/inspector/dist/index.html +24 -0
  32. package/package.json +4 -10
  33. package/inspector/README.md +0 -67
  34. package/inspector/app/api/runs/[id]/file/route.ts +0 -56
  35. package/inspector/app/api/runs/[id]/route.ts +0 -15
  36. package/inspector/app/api/runs/[id]/stop/route.ts +0 -15
  37. package/inspector/app/api/runs/route.ts +0 -9
  38. package/inspector/app/api/workflows/[slug]/route.ts +0 -11
  39. package/inspector/app/api/workflows/[slug]/run/route.ts +0 -37
  40. package/inspector/app/api/workflows/route.ts +0 -8
  41. package/inspector/app/globals.css +0 -460
  42. package/inspector/app/layout.tsx +0 -54
  43. package/inspector/app/page.tsx +0 -16
  44. package/inspector/app/runs/[id]/page.tsx +0 -6
  45. package/inspector/app/runs/[id]/run-detail.tsx +0 -343
  46. package/inspector/app/shared.tsx +0 -81
  47. package/inspector/app/theme-toggle.tsx +0 -19
  48. package/inspector/app/workflows/[slug]/page.tsx +0 -6
  49. package/inspector/app/workflows/[slug]/workflow-view.tsx +0 -271
  50. package/inspector/app/workflows/page.tsx +0 -43
  51. package/inspector/lib/runner.ts +0 -78
  52. package/inspector/lib/runs.ts +0 -313
  53. package/inspector/lib/workflows.ts +0 -240
  54. package/inspector/next.config.ts +0 -5
  55. package/inspector/package-lock.json +0 -982
  56. package/inspector/package.json +0 -24
  57. package/inspector/tsconfig.json +0 -21
@@ -0,0 +1,186 @@
1
+ import { promises as fs } from "node:fs";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { setOutputDir, getOutputDir } from "./context.js";
5
+ import { listRuns, getRun, readRunFile } from "./runs.js";
6
+ import { listWorkflows, getWorkflow } from "./workflows.js";
7
+ import { dockerStatus, triggerRun, stopRun } from "./runner.js";
8
+ /**
9
+ * The inspector server: a plain node:http server with no dependencies.
10
+ * Serves the prebuilt SPA (inspector/dist) plus the JSON/file API the
11
+ * frontend polls. Realtime is polling — no daemon, no socket, works on a
12
+ * plain filesystem.
13
+ */
14
+ const MIME = {
15
+ ".html": "text/html; charset=utf-8",
16
+ ".js": "text/javascript; charset=utf-8",
17
+ ".css": "text/css; charset=utf-8",
18
+ ".svg": "image/svg+xml",
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".gif": "image/gif",
23
+ ".webp": "image/webp",
24
+ ".ico": "image/x-icon",
25
+ ".pdf": "application/pdf",
26
+ ".json": "application/json; charset=utf-8",
27
+ ".woff": "font/woff",
28
+ ".woff2": "font/woff2",
29
+ ".txt": "text/plain; charset=utf-8",
30
+ };
31
+ /** Text preview payloads are capped; the tail matters most for logs. */
32
+ const MAX_TEXT = 400_000;
33
+ function json(res, body, status = 200) {
34
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
35
+ res.end(JSON.stringify(body));
36
+ }
37
+ function readBody(req) {
38
+ return new Promise((resolve, reject) => {
39
+ let data = "";
40
+ req.on("data", (c) => {
41
+ data += c;
42
+ if (data.length > 1_000_000)
43
+ reject(new Error("body too large"));
44
+ });
45
+ req.on("end", () => resolve(data));
46
+ req.on("error", reject);
47
+ });
48
+ }
49
+ async function handleApi(req, res, url) {
50
+ const seg = url.pathname.split("/").filter(Boolean); // ["api", ...]
51
+ const method = req.method ?? "GET";
52
+ // GET /api/runs
53
+ if (seg.length === 2 && seg[1] === "runs" && method === "GET") {
54
+ return json(res, { outputDir: getOutputDir(), runs: await listRuns() });
55
+ }
56
+ // GET /api/runs/:id
57
+ if (seg.length === 3 && seg[1] === "runs" && method === "GET") {
58
+ try {
59
+ const run = await getRun(seg[2]);
60
+ return run ? json(res, run) : json(res, { error: "not found" }, 404);
61
+ }
62
+ catch {
63
+ return json(res, { error: "invalid run id" }, 400);
64
+ }
65
+ }
66
+ // GET /api/runs/:id/file?name=...&raw=1
67
+ if (seg.length === 4 && seg[1] === "runs" && seg[3] === "file" && method === "GET") {
68
+ const name = url.searchParams.get("name") ?? "";
69
+ const raw = url.searchParams.get("raw") === "1";
70
+ let file;
71
+ try {
72
+ file = await readRunFile(seg[2], name);
73
+ }
74
+ catch {
75
+ return json(res, { error: "invalid request" }, 400);
76
+ }
77
+ if (!file)
78
+ return json(res, { error: "not found" }, 404);
79
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
80
+ const buf = await fs.readFile(file.absPath);
81
+ if (raw) {
82
+ res.writeHead(200, {
83
+ "content-type": MIME[ext] ?? "text/plain; charset=utf-8",
84
+ "cache-control": "no-store",
85
+ });
86
+ return void res.end(buf);
87
+ }
88
+ let text = buf.toString("utf8");
89
+ let truncated = false;
90
+ if (text.length > MAX_TEXT) {
91
+ text = text.slice(-MAX_TEXT);
92
+ truncated = true;
93
+ }
94
+ return json(res, { name, size: file.size, truncated, content: text });
95
+ }
96
+ // POST /api/runs/:id/stop
97
+ if (seg.length === 4 && seg[1] === "runs" && seg[3] === "stop" && method === "POST") {
98
+ return stopRun(seg[2])
99
+ ? json(res, { stopped: true })
100
+ : json(res, { error: "no running sandbox container for this run (local runs cannot be stopped here)" }, 404);
101
+ }
102
+ // GET /api/workflows
103
+ if (seg.length === 2 && seg[1] === "workflows" && method === "GET") {
104
+ return json(res, await listWorkflows());
105
+ }
106
+ // GET /api/workflows/:slug
107
+ if (seg.length === 3 && seg[1] === "workflows" && method === "GET") {
108
+ const wf = await getWorkflow(decodeURIComponent(seg[2]));
109
+ return wf ? json(res, wf) : json(res, { error: "not found" }, 404);
110
+ }
111
+ // GET/POST /api/workflows/:slug/run
112
+ if (seg.length === 4 && seg[1] === "workflows" && seg[3] === "run") {
113
+ if (method === "GET")
114
+ return json(res, dockerStatus());
115
+ if (method === "POST") {
116
+ const wf = await getWorkflow(decodeURIComponent(seg[2]));
117
+ if (!wf || wf.error || !wf.name)
118
+ return json(res, { error: "workflow not found or broken" }, 404);
119
+ let body;
120
+ try {
121
+ body = JSON.parse(await readBody(req));
122
+ }
123
+ catch {
124
+ return json(res, { error: "invalid JSON body" }, 400);
125
+ }
126
+ const request = (body.request ?? "").trim();
127
+ if (!request)
128
+ return json(res, { error: "request text is required" }, 400);
129
+ try {
130
+ const { runId } = triggerRun({
131
+ workflowName: wf.name,
132
+ request,
133
+ sandbox: body.sandbox ?? true,
134
+ ssh: !!body.ssh,
135
+ });
136
+ return json(res, { runId });
137
+ }
138
+ catch (err) {
139
+ return json(res, { error: err.message }, 503);
140
+ }
141
+ }
142
+ }
143
+ json(res, { error: "not found" }, 404);
144
+ }
145
+ async function serveStatic(res, staticDir, pathname) {
146
+ // SPA: unknown paths fall back to index.html (routing is client-side).
147
+ const rel = pathname === "/" ? "index.html" : pathname.slice(1);
148
+ let abs = path.resolve(staticDir, rel);
149
+ if (!abs.startsWith(path.resolve(staticDir) + path.sep) && abs !== path.resolve(staticDir)) {
150
+ return json(res, { error: "not found" }, 404);
151
+ }
152
+ let buf = await fs.readFile(abs).catch(() => null);
153
+ if (buf === null) {
154
+ abs = path.join(staticDir, "index.html");
155
+ buf = await fs.readFile(abs).catch(() => null);
156
+ }
157
+ if (buf === null)
158
+ return json(res, { error: "inspector assets missing" }, 500);
159
+ const ext = path.extname(abs).toLowerCase();
160
+ res.writeHead(200, {
161
+ "content-type": MIME[ext] ?? "application/octet-stream",
162
+ // Vite emits content-hashed asset names; index.html must stay fresh.
163
+ "cache-control": abs.endsWith("index.html") ? "no-store" : "public, max-age=31536000, immutable",
164
+ });
165
+ res.end(buf);
166
+ }
167
+ /** Start the server; resolves once it listens. Runs until the process ends. */
168
+ export function startInspector(opts) {
169
+ setOutputDir(opts.outputDir);
170
+ const server = http.createServer(async (req, res) => {
171
+ try {
172
+ const url = new URL(req.url ?? "/", "http://localhost");
173
+ if (url.pathname.startsWith("/api/"))
174
+ await handleApi(req, res, url);
175
+ else
176
+ await serveStatic(res, opts.staticDir, url.pathname);
177
+ }
178
+ catch (err) {
179
+ json(res, { error: err.message }, 500);
180
+ }
181
+ });
182
+ return new Promise((resolve, reject) => {
183
+ server.once("error", reject);
184
+ server.listen(opts.port, () => resolve(server));
185
+ });
186
+ }
@@ -0,0 +1,45 @@
1
+ export interface AgentInfo {
2
+ id: string;
3
+ name?: string;
4
+ model?: string;
5
+ harness?: string;
6
+ effort?: string;
7
+ tools: string[];
8
+ persona?: string;
9
+ }
10
+ export interface StepInfo {
11
+ name: string;
12
+ kind: "agent" | "script";
13
+ agent?: string;
14
+ /** Resolved prompt text (agent steps). */
15
+ prompt?: string;
16
+ /** The file reference as written in the yml, if the value was a file ref. */
17
+ sourceRef?: string;
18
+ /** Resolved script text (script steps). */
19
+ script?: string;
20
+ gates: string[];
21
+ }
22
+ export interface WorkflowSummary {
23
+ slug: string;
24
+ name?: string;
25
+ description?: string;
26
+ agents: number;
27
+ steps: number;
28
+ error?: string;
29
+ }
30
+ export interface WorkflowDetail {
31
+ slug: string;
32
+ dir: string;
33
+ name?: string;
34
+ description?: string;
35
+ workspace?: string;
36
+ agents: AgentInfo[];
37
+ steps: StepInfo[];
38
+ files: string[];
39
+ error?: string;
40
+ }
41
+ export declare function listWorkflows(): Promise<{
42
+ root?: string;
43
+ workflows: WorkflowSummary[];
44
+ }>;
45
+ export declare function getWorkflow(slug: string): Promise<WorkflowDetail | null>;
@@ -0,0 +1,186 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { getProjectRoot } from "./context.js";
5
+ /**
6
+ * Workflow discovery + parsing for the inspector. Mirrors the kraftwerk CLI
7
+ * conventions (src/workflows/ or workflows/ under the project root, folder
8
+ * mode with workflow.yml or single .yml files) but parses the YAML raw, so
9
+ * even a workflow the framework would reject still renders — with its error.
10
+ */
11
+ async function workflowsRoot() {
12
+ for (const candidate of ["src/workflows", "workflows"]) {
13
+ const p = path.join(getProjectRoot(), candidate);
14
+ const st = await fs.stat(p).catch(() => null);
15
+ if (st?.isDirectory())
16
+ return p;
17
+ }
18
+ return undefined;
19
+ }
20
+ function gateLabel(g) {
21
+ if (g == null)
22
+ return "?";
23
+ if (typeof g.file_non_empty === "string")
24
+ return `file_non_empty(${g.file_non_empty})`;
25
+ if (typeof g.slots_filled === "string")
26
+ return `slots_filled(${g.slots_filled})`;
27
+ if (g.contains)
28
+ return `contains(${g.contains.file}, ${g.contains.label ?? g.contains.text})`;
29
+ return JSON.stringify(g);
30
+ }
31
+ /** Folder mode: a single-line value references a file inside the folder. */
32
+ async function resolveText(value, baseDir) {
33
+ if (typeof value !== "string")
34
+ return {};
35
+ const line = value.trim();
36
+ if (!baseDir || line.includes("\n"))
37
+ return { text: line };
38
+ const candidate = path.resolve(baseDir, line);
39
+ if (!candidate.startsWith(path.resolve(baseDir) + path.sep))
40
+ return { text: line };
41
+ const content = await fs.readFile(candidate, "utf8").catch(() => null);
42
+ if (content !== null)
43
+ return { text: content.trim(), sourceRef: line };
44
+ return { text: line };
45
+ }
46
+ async function listDirFiles(dir, prefix = "") {
47
+ const out = [];
48
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
49
+ for (const e of entries) {
50
+ if (e.name.startsWith("."))
51
+ continue;
52
+ if (e.isDirectory()) {
53
+ out.push(...(await listDirFiles(path.join(dir, e.name), `${prefix}${e.name}/`)));
54
+ }
55
+ else {
56
+ out.push(`${prefix}${e.name}`);
57
+ }
58
+ }
59
+ return out.sort();
60
+ }
61
+ async function locate() {
62
+ const root = await workflowsRoot();
63
+ if (!root)
64
+ return [];
65
+ const found = [];
66
+ for (const entry of await fs.readdir(root, { withFileTypes: true })) {
67
+ const entryPath = path.join(root, entry.name);
68
+ if (entry.isDirectory()) {
69
+ for (const f of ["workflow.yml", "workflow.yaml"]) {
70
+ const st = await fs.stat(path.join(entryPath, f)).catch(() => null);
71
+ if (st?.isFile()) {
72
+ found.push({ slug: entry.name, yamlPath: path.join(entryPath, f), baseDir: entryPath });
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ else if (/\.ya?ml$/.test(entry.name)) {
78
+ found.push({ slug: entry.name.replace(/\.ya?ml$/, ""), yamlPath: entryPath, baseDir: null });
79
+ }
80
+ }
81
+ return found.sort((a, b) => a.slug.localeCompare(b.slug));
82
+ }
83
+ async function parseRaw(l) {
84
+ const text = await fs.readFile(l.yamlPath, "utf8");
85
+ return parseYaml(text);
86
+ }
87
+ export async function listWorkflows() {
88
+ const root = await workflowsRoot();
89
+ const located = await locate();
90
+ const workflows = await Promise.all(located.map(async (l) => {
91
+ try {
92
+ const raw = await parseRaw(l);
93
+ return {
94
+ slug: l.slug,
95
+ name: raw?.name,
96
+ description: raw?.description,
97
+ agents: Object.keys(raw?.agents ?? {}).length,
98
+ steps: (raw?.steps ?? []).length,
99
+ };
100
+ }
101
+ catch (err) {
102
+ return { slug: l.slug, agents: 0, steps: 0, error: err.message };
103
+ }
104
+ }));
105
+ return { root, workflows };
106
+ }
107
+ export async function getWorkflow(slug) {
108
+ const located = await locate();
109
+ const l = located.find((x) => x.slug === slug) ??
110
+ // Fallback: match by workflow name (run traces carry the name, not the slug).
111
+ (await (async () => {
112
+ for (const x of located) {
113
+ try {
114
+ if ((await parseRaw(x))?.name === slug)
115
+ return x;
116
+ }
117
+ catch { }
118
+ }
119
+ return undefined;
120
+ })());
121
+ if (!l)
122
+ return null;
123
+ let raw;
124
+ try {
125
+ raw = await parseRaw(l);
126
+ }
127
+ catch (err) {
128
+ return {
129
+ slug: l.slug,
130
+ dir: path.dirname(l.yamlPath),
131
+ agents: [],
132
+ steps: [],
133
+ files: [],
134
+ error: err.message,
135
+ };
136
+ }
137
+ const agents = [];
138
+ for (const [id, a] of Object.entries(raw?.agents ?? {})) {
139
+ const persona = await resolveText(a?.persona, l.baseDir);
140
+ agents.push({
141
+ id,
142
+ name: a?.name,
143
+ model: a?.model,
144
+ harness: a?.harness,
145
+ effort: a?.effort,
146
+ tools: a?.tools ?? [],
147
+ persona: persona.text,
148
+ });
149
+ }
150
+ const steps = [];
151
+ for (const s of raw?.steps ?? []) {
152
+ const gates = (s?.gates ?? []).map(gateLabel);
153
+ if (s?.run !== undefined) {
154
+ const script = await resolveText(s.run, l.baseDir);
155
+ steps.push({
156
+ name: s.name,
157
+ kind: "script",
158
+ script: script.text,
159
+ sourceRef: script.sourceRef,
160
+ gates,
161
+ });
162
+ }
163
+ else {
164
+ const prompt = await resolveText(s?.prompt, l.baseDir);
165
+ steps.push({
166
+ name: s?.name,
167
+ kind: "agent",
168
+ agent: s?.agent,
169
+ prompt: prompt.text,
170
+ sourceRef: prompt.sourceRef,
171
+ gates,
172
+ });
173
+ }
174
+ }
175
+ const workspace = await resolveText(raw?.workspace, l.baseDir);
176
+ return {
177
+ slug: l.slug,
178
+ dir: path.dirname(l.yamlPath),
179
+ name: raw?.name,
180
+ description: raw?.description,
181
+ workspace: workspace.text,
182
+ agents,
183
+ steps,
184
+ files: l.baseDir ? await listDirFiles(l.baseDir) : [path.basename(l.yamlPath)],
185
+ };
186
+ }
@@ -0,0 +1 @@
1
+ @font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff) format("woff")}:root{--font-sans: "IBM Plex Sans";--font-mono: "IBM Plex Mono";--bg: #141512;--bg-deep: #0e0f0d;--panel: #1b1d19;--panel-2: #22241f;--line: #2e312a;--line-soft: #262822;--ink: #ecebe3;--ink-2: #b3b1a4;--muted: #7c7a6e;--accent: #f5a623;--accent-dim: rgba(245, 166, 35, .14);--ok: #56c15a;--ok-dim: rgba(86, 193, 90, .14);--fail: #e5484d;--fail-dim: rgba(229, 72, 77, .14);--run: #f5a623;--pending: #4c4e46;--shadow: 0 1px 0 rgba(255, 255, 255, .03) inset, 0 8px 24px rgba(0, 0, 0, .35);color-scheme:dark}:root[data-theme=light]{--bg: #f1f0ea;--bg-deep: #e8e7df;--panel: #fbfaf6;--panel-2: #f3f2ec;--line: #d9d7cc;--line-soft: #e3e1d6;--ink: #1d1e19;--ink-2: #55564c;--muted: #8a887b;--accent: #c77c11;--accent-dim: rgba(199, 124, 17, .12);--ok: #2e8f38;--ok-dim: rgba(46, 143, 56, .12);--fail: #c73538;--fail-dim: rgba(199, 53, 56, .1);--run: #c77c11;--pending: #c9c7bb;--shadow: 0 1px 2px rgba(29, 30, 25, .06);color-scheme:light}*{box-sizing:border-box;margin:0;padding:0}html{height:100%}body{min-height:100%;background:radial-gradient(1200px 400px at 50% -200px,rgba(245,166,35,.05),transparent 70%),var(--bg);color:var(--ink);font-family:var(--font-sans),sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}code,pre,.mono{font-family:var(--font-mono),monospace}.topbar{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:14px;padding:10px 20px;background:color-mix(in srgb,var(--bg) 86%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}.wordmark{display:flex;align-items:center;gap:10px}.wordmark .lamp-block{width:22px;height:22px;border-radius:5px;background:linear-gradient(135deg,var(--accent),#d2820b);display:grid;place-items:center;box-shadow:0 0 14px #f5a62359}.wordmark .lamp-block span{width:7px;height:7px;border-radius:2px;background:#141512}.wordmark b{font-family:var(--font-mono),monospace;font-size:13.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}.wordmark small{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted)}.topbar .spacer{flex:1}.topbar nav{display:flex;gap:4px;margin-left:10px}.topbar nav a{font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);padding:4px 10px;border-radius:6px}.topbar nav a:hover{color:var(--ink);background:var(--panel-2)}.topbar .outdir{font-family:var(--font-mono),monospace;font-size:11px;color:var(--muted);max-width:44vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.theme-btn{border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2)}.theme-btn:hover{border-color:var(--accent);color:var(--ink)}.shell{max-width:1240px;margin:0 auto;padding:26px 20px 80px}.microlabel{font-size:10.5px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}.num{font-variant-numeric:tabular-nums}.lamp{width:9px;height:9px;border-radius:50%;flex:none;position:relative}.lamp.ok{background:var(--ok);box-shadow:0 0 6px color-mix(in srgb,var(--ok) 60%,transparent)}.lamp.failed,.lamp.blocked{background:var(--fail);box-shadow:0 0 6px color-mix(in srgb,var(--fail) 60%,transparent)}.lamp.aborted{background:var(--pending)}.lamp.pending{background:transparent;border:1.5px solid var(--pending)}.lamp.running{background:var(--run);animation:lamp-pulse 1.4s ease-in-out infinite}@keyframes lamp-pulse{0%,to{box-shadow:0 0 4px color-mix(in srgb,var(--run) 55%,transparent);opacity:1}50%{box-shadow:0 0 14px var(--run);opacity:.75}}.status-word{font-size:11px;font-weight:650;letter-spacing:.14em;text-transform:uppercase}.status-word.ok{color:var(--ok)}.status-word.failed,.status-word.blocked{color:var(--fail)}.status-word.running{color:var(--run)}.status-word.aborted,.status-word.pending{color:var(--muted)}.page-head{display:flex;align-items:baseline;gap:14px;margin:6px 2px 18px}.page-head h1{font-size:19px;font-weight:650;letter-spacing:-.01em}.page-head .count{color:var(--muted);font-size:13px}.run-list{display:grid;gap:10px}.run-card{display:grid;grid-template-columns:12px 200px 1fr auto;gap:8px 16px;align-items:center;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:13px 18px;box-shadow:var(--shadow);transition:border-color .12s ease,transform .12s ease}.run-card:hover{border-color:color-mix(in srgb,var(--accent) 55%,var(--line));transform:translateY(-1px)}.run-card .rid{font-family:var(--font-mono),monospace;font-size:12.5px;color:var(--ink-2)}.run-card .wf{font-weight:650;font-size:14px}.run-card .req{color:var(--muted);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:52ch}.run-card .meta{display:flex;gap:18px;align-items:center;font-family:var(--font-mono),monospace;font-size:12px;color:var(--ink-2)}.progress-track{width:90px;height:4px;border-radius:2px;background:var(--line-soft);overflow:hidden}.progress-track i{display:block;height:100%;background:var(--accent);border-radius:2px}.run-card.is-ok .progress-track i{background:var(--ok)}.run-card.is-failed .progress-track i{background:var(--fail)}.empty{border:1px dashed var(--line);border-radius:8px;padding:40px;text-align:center;color:var(--muted)}.crumbs{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);margin-bottom:14px}.crumbs a:hover{color:var(--accent)}.crumbs .sep{color:var(--line)}.detail-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:6px}.detail-head h1{font-size:18px;font-weight:650}.detail-head .rid{font-family:var(--font-mono),monospace;font-size:12px;color:var(--muted)}.detail-req{color:var(--ink-2);font-size:13px;margin-bottom:18px;max-width:90ch}.statgrid{display:flex;flex-wrap:wrap;gap:26px;margin-bottom:22px}.stat .microlabel{margin-bottom:1px}.stat .v{font-family:var(--font-mono),monospace;font-size:15px;font-weight:600}.columns{display:grid;grid-template-columns:minmax(430px,5fr) minmax(360px,4fr);gap:18px;align-items:start}@media(max-width:1020px){.columns{grid-template-columns:1fr}}.panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;box-shadow:var(--shadow);overflow:hidden}.panel-head{display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid var(--line-soft);background:var(--panel-2)}.panel-head .microlabel{color:var(--ink-2)}.panel-head .spacer{flex:1}.phases{padding:6px 0}.phase-row{position:relative;padding:10px 16px 10px 40px}.phase-row+.phase-row{border-top:1px solid var(--line-soft)}.phase-row .lamp{position:absolute;left:17px;top:16px}.phase-row:before{content:"";position:absolute;left:21px;top:0;bottom:0;width:1px;background:var(--line-soft)}.phase-row:first-child:before{top:16px}.phase-row:last-child:before{bottom:auto;height:16px}.phase-row .lamp{z-index:1}.phase-top{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}.phase-name{font-weight:650;font-size:13.5px}.chip{font-family:var(--font-mono),monospace;font-size:10.5px;border:1px solid var(--line);border-radius:4px;padding:1px 7px;color:var(--ink-2)}.chip.agent{border-color:color-mix(in srgb,var(--accent) 40%,var(--line));color:var(--accent)}.phase-top .right{margin-left:auto;display:flex;gap:14px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--muted)}.phase-sub{margin-top:3px;font-size:12.5px;color:var(--ink-2);max-width:72ch}.phase-activity{margin-top:3px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--run)}.phase-activity:before{content:"▸ "}.gate-line{margin-top:5px;display:flex;flex-wrap:wrap;gap:6px}.gate{display:inline-flex;align-items:center;gap:5px;font-family:var(--font-mono),monospace;font-size:10.5px;border-radius:4px;padding:1.5px 7px;background:var(--ok-dim);color:var(--ok)}.gate.failed{background:var(--fail-dim);color:var(--fail)}.gate-fail-msg{margin-top:4px;font-size:12px;color:var(--fail)}.file-list{max-height:300px;overflow:auto}.file-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:7px 16px;font-family:var(--font-mono),monospace;font-size:12.5px;border-bottom:1px solid var(--line-soft);color:var(--ink-2)}.file-row:last-child{border-bottom:0}.file-row:hover{background:var(--panel-2);color:var(--ink)}.file-row.active{background:var(--accent-dim);color:var(--ink)}.file-row .fname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-row .fsize{color:var(--muted);font-size:11px}.viewer{border-top:1px solid var(--line)}.viewer-body pre{padding:14px 16px;overflow:auto;max-height:560px;font-size:11.8px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:var(--ink-2)}.viewer-body iframe{width:100%;height:620px;border:0;background:#fff;display:block}.viewer-body img{max-width:100%;display:block;padding:14px 16px}.viewer-note{padding:6px 16px;font-size:11px;color:var(--muted);border-bottom:1px solid var(--line-soft)}.open-raw{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:5px;padding:2px 8px}.open-raw:hover{color:var(--accent);border-color:var(--accent)}.live-note{display:inline-flex;align-items:center;gap:7px;font-size:11px;color:var(--muted)}.wf-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.wf-card{display:flex;flex-direction:column;gap:6px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px 18px;box-shadow:var(--shadow);transition:border-color .12s ease,transform .12s ease}.wf-card:hover{border-color:color-mix(in srgb,var(--accent) 55%,var(--line));transform:translateY(-1px)}.wf-card-head{display:flex;align-items:baseline;gap:10px}.wf-name{font-weight:650;font-size:15px;font-family:var(--font-mono),monospace}.wf-desc{color:var(--ink-2);font-size:12.5px;flex:1}.wf-meta{display:flex;gap:8px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--muted)}.wf-workspace{font-size:11.5px;color:var(--ink-2);white-space:pre-wrap}.aid-0{--aid: var(--accent)}.aid-1{--aid: #4fb8a8}.aid-2{--aid: #a78bfa}.aid-3{--aid: #e5793b}.agent-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:0}.agent-card{padding:14px 18px;border-right:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft)}.agent-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.agent-head b{font-size:13.5px}.agent-head .spacer{flex:1}.agent-id{font-size:11px;color:var(--muted)}.agent-dot{width:10px;height:10px;border-radius:3px;background:var(--aid);box-shadow:0 0 8px color-mix(in srgb,var(--aid) 45%,transparent)}.agent-tools{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.tool-chip{font-family:var(--font-mono),monospace;font-size:10.5px;border:1px solid var(--line);border-radius:4px;padding:1px 6px;color:var(--ink-2)}.agent-persona{margin-top:8px;font-size:12px;color:var(--muted);line-height:1.55;white-space:pre-wrap}.pipeline{padding:8px 0}.step-node{position:relative;display:flex;gap:14px;padding:12px 18px}.step-node+.step-node{border-top:1px solid var(--line-soft)}.step-node:before{content:"";position:absolute;left:30px;top:0;bottom:0;width:1px;background:var(--line-soft)}.step-node:first-child:before{top:18px}.step-node:last-child:before{bottom:auto;height:18px}.step-index{position:relative;z-index:1;flex:none;width:25px;height:25px;border-radius:6px;margin-top:1px;display:grid;place-items:center;font-family:var(--font-mono),monospace;font-size:11.5px;font-weight:700;background:color-mix(in srgb,var(--aid) 16%,var(--panel));border:1px solid color-mix(in srgb,var(--aid) 55%,var(--line));color:var(--aid)}.step-index.is-script{--aid: var(--ink-2)}.step-body{flex:1;min-width:0}.agent-chip{border-color:color-mix(in srgb,var(--aid) 50%,var(--line));color:var(--aid)}.step-source{margin-top:6px}.step-source summary{cursor:pointer;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);-webkit-user-select:none;user-select:none}.step-source summary:hover{color:var(--accent)}.step-source pre{margin-top:6px;padding:10px 12px;border:1px solid var(--line-soft);border-radius:6px;background:var(--bg-deep);font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:var(--ink-2);max-height:360px;overflow:auto}.gate.neutral{background:var(--panel-2);color:var(--ink-2);border:1px solid var(--line-soft)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px;border:2px solid transparent;background-clip:content-box}::-webkit-scrollbar-track{background:transparent}.run-form{display:flex;gap:10px}.run-form input[type=text]{flex:1;font:13px/1.4 var(--font-mono),monospace;color:var(--ink);background:var(--panel-2);border:1px solid var(--line);border-radius:7px;padding:9px 12px;outline:none}.run-form input[type=text]:focus{border-color:var(--accent)}.run-btn{font:600 12.5px/1 var(--font-sans),sans-serif;white-space:nowrap;color:#141512;background:var(--accent);border-radius:7px;padding:10px 16px}.run-btn:hover{filter:brightness(1.08)}.run-btn:disabled{opacity:.45;cursor:not-allowed;filter:none}.run-opts{display:flex;flex-wrap:wrap;gap:6px 22px;margin-top:10px}.run-opts label{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:var(--ink-2);cursor:pointer}.run-opts input[type=checkbox]{accent-color:var(--accent)}.run-opts .opt-hint{color:var(--muted)}.sandbox-chip{border:1px solid var(--line);color:var(--ink-2)}.stop-btn{font:600 12px/1 var(--font-sans),sans-serif;color:var(--fail);background:var(--fail-dim);border:1px solid color-mix(in srgb,var(--fail) 40%,transparent);border-radius:7px;padding:7px 13px}.stop-btn:hover{border-color:var(--fail)}.stop-btn:disabled{opacity:.5;cursor:default}.shell:has(.runs-screen){max-width:none;padding:0}.runs-screen{display:grid;grid-template-columns:300px minmax(0,1fr);align-items:start}.runs-side{position:sticky;top:45px;height:calc(100vh - 45px);display:flex;flex-direction:column;border-right:1px solid var(--line);background:var(--panel)}.side-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line-soft);background:var(--panel-2)}.side-head .spacer{flex:1}.side-list{flex:1;overflow-y:auto}.side-row{display:flex;align-items:flex-start;gap:9px;padding:9px 12px 9px 14px;border-bottom:1px solid var(--line-soft);border-left:2px solid transparent}.side-row .lamp{margin-top:4px}.side-row:hover{background:var(--panel-2)}.side-row.active{background:var(--accent-dim);border-left-color:var(--accent)}.side-row-body{flex:1;min-width:0}.side-row-top{display:flex;align-items:baseline;gap:8px}.side-wf{font-weight:650;font-size:12.5px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.side-when{font-family:var(--font-mono),monospace;font-size:10.5px;color:var(--muted)}.side-row-sub{display:flex;align-items:baseline;gap:8px;margin-top:1px}.side-req{flex:1;font-size:11.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.side-row.active .side-req{color:var(--ink-2)}.side-meta{font-family:var(--font-mono),monospace;font-size:10.5px;color:var(--muted)}.runs-main{padding:16px 20px 40px;min-width:0}.runs-main .detail-head .spacer{flex:1}.tabs{display:flex;gap:2px;background:var(--panel-2);border:1px solid var(--line);border-radius:7px;padding:2px}.tabs button{font-size:11px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:5px 13px;border-radius:5px}.tabs button:hover{color:var(--ink)}.tabs button.active{background:var(--panel);color:var(--ink);box-shadow:var(--shadow)}.run-tab{max-width:1080px}.art-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;height:calc(100vh - 168px);min-height:420px}.art-files{display:flex;flex-direction:column;min-height:0}.art-files .file-list{flex:1;max-height:none}.art-viewer{display:flex;flex-direction:column;min-height:0}.art-viewer .viewer{flex:1;display:flex;flex-direction:column;min-height:0;border-top:0}.art-viewer .viewer-body{flex:1;min-height:0}.art-viewer .viewer-body iframe{height:100%}.art-viewer .viewer-body pre{max-height:none;height:100%;box-sizing:border-box}.art-viewer .viewer-body img{max-height:100%;object-fit:contain}.art-viewer .empty{flex:1;display:grid;place-items:center}@media(max-width:940px){.runs-screen{grid-template-columns:1fr}.runs-side{display:none}.art-layout{grid-template-columns:1fr;height:auto}.art-files .file-list{max-height:240px}.art-viewer .viewer-body iframe{height:560px}}