cubefarm 0.2.0 → 0.3.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.
@@ -0,0 +1,234 @@
1
+ // server/ptyHost.ts
2
+ import fs from "node:fs";
3
+ import http from "node:http";
4
+ import net from "node:net";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import * as nodePty from "@lydell/node-pty";
8
+
9
+ // server/ptyProtocol.ts
10
+ var PTY_PROTOCOL = 1;
11
+ var encode = (m) => `${JSON.stringify(m)}
12
+ `;
13
+ function readMessages(sock, onMessage) {
14
+ let buf = "";
15
+ sock.setEncoding("utf8");
16
+ sock.on("data", (chunk) => {
17
+ buf += chunk;
18
+ let nl;
19
+ while ((nl = buf.indexOf("\n")) >= 0) {
20
+ const line = buf.slice(0, nl);
21
+ buf = buf.slice(nl + 1);
22
+ if (!line) continue;
23
+ let m;
24
+ try {
25
+ m = JSON.parse(line);
26
+ } catch {
27
+ continue;
28
+ }
29
+ onMessage(m);
30
+ }
31
+ });
32
+ }
33
+
34
+ // server/ptyHost.ts
35
+ var BACKLOG_BYTES = 2 * 1024 * 1024;
36
+ var HOOK_WAIT_MS = 11e4;
37
+ var ORPHAN_MS = 10 * 6e4;
38
+ var IDLE_EXIT_MS = 6e4;
39
+ var [mode, socketPath, secretFile] = process.argv.slice(2);
40
+ if (mode === "launch") {
41
+ spawn(process.execPath, [...process.execArgv, process.argv[1], "serve", socketPath, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).unref();
42
+ process.exit(0);
43
+ }
44
+ var logFile = path.join(path.dirname(secretFile), "pty-host.log");
45
+ var note = (text) => fs.appendFile(logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${text}
46
+ `, () => void 0);
47
+ process.on("uncaughtException", (err) => note(`uncaught: ${err.stack ?? err}`));
48
+ for (const sig of ["SIGINT", "SIGHUP", "SIGBREAK"]) process.on(sig, () => void 0);
49
+ var secret = fs.readFileSync(secretFile, "utf8").trim();
50
+ var held = /* @__PURE__ */ new Map();
51
+ var office = null;
52
+ var officeLeft = Date.now();
53
+ var hooksOpen = false;
54
+ var send = (m) => {
55
+ if (office && !office.destroyed) office.write(encode(m));
56
+ };
57
+ function killTree(pid) {
58
+ if (process.platform === "win32") {
59
+ spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }).once("error", () => void 0);
60
+ return;
61
+ }
62
+ try {
63
+ process.kill(-pid, "SIGTERM");
64
+ } catch {
65
+ try {
66
+ process.kill(pid, "SIGTERM");
67
+ } catch {
68
+ }
69
+ }
70
+ }
71
+ function start(m) {
72
+ let proc;
73
+ try {
74
+ proc = nodePty.spawn(m.file, m.args, { name: "xterm-256color", cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env });
75
+ } catch (err) {
76
+ send({ op: "failed", id: m.id, error: err.message });
77
+ return;
78
+ }
79
+ const h = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null };
80
+ held.set(m.id, h);
81
+ send({ op: "spawned", id: m.id, pid: proc.pid });
82
+ proc.onData((data) => {
83
+ if (h.attached && office) return send({ op: "data", id: m.id, data });
84
+ h.backlog.push(data);
85
+ h.bytes += data.length;
86
+ while (h.bytes > BACKLOG_BYTES && h.backlog.length > 1) h.bytes -= h.backlog.shift().length;
87
+ });
88
+ proc.onExit(({ exitCode }) => {
89
+ h.exit = exitCode;
90
+ if (h.attached && office) {
91
+ send({ op: "exit", id: m.id, code: exitCode });
92
+ held.delete(m.id);
93
+ }
94
+ });
95
+ }
96
+ function attach(id) {
97
+ const h = held.get(id);
98
+ if (!h) return send({ op: "exit", id, code: -1 });
99
+ h.attached = true;
100
+ if (h.backlog.length) send({ op: "data", id, data: h.backlog.join("") });
101
+ h.backlog = [];
102
+ h.bytes = 0;
103
+ if (h.exit !== null) {
104
+ send({ op: "exit", id, code: h.exit });
105
+ held.delete(id);
106
+ }
107
+ }
108
+ function stopAll() {
109
+ for (const h of held.values()) if (h.exit === null) killTree(h.proc.pid);
110
+ }
111
+ var waiting = /* @__PURE__ */ new Map();
112
+ var nextRid = 1;
113
+ function answer(rid, reply) {
114
+ const w = waiting.get(rid);
115
+ if (!w) return;
116
+ waiting.delete(rid);
117
+ clearTimeout(w.timer);
118
+ if (!w.res.writableEnded) w.res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(reply ?? {}));
119
+ }
120
+ function passHooks() {
121
+ if (!office || !hooksOpen) return;
122
+ for (const [rid, w] of waiting) {
123
+ if (w.sent) continue;
124
+ w.sent = true;
125
+ send({ op: "hook", rid, token: w.token, body: w.body });
126
+ }
127
+ }
128
+ var hooks = http.createServer((req, res) => {
129
+ const token = req.method === "POST" ? req.url?.match(/^\/api\/hooks\/([\w-]+)$/)?.[1] : void 0;
130
+ if (!token) return void res.writeHead(404).end();
131
+ const chunks = [];
132
+ let size = 0;
133
+ req.on("data", (c) => {
134
+ size += c.length;
135
+ if (size <= 64 * 1024 * 1024) chunks.push(c);
136
+ });
137
+ req.on("end", () => {
138
+ let body = {};
139
+ try {
140
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
141
+ } catch {
142
+ }
143
+ const rid = nextRid++;
144
+ waiting.set(rid, { token, body, res, timer: setTimeout(() => answer(rid, {}), HOOK_WAIT_MS), sent: false });
145
+ passHooks();
146
+ });
147
+ });
148
+ function connected(sock) {
149
+ let helloed = false;
150
+ readMessages(sock, (m) => {
151
+ if (!helloed) {
152
+ if (m.op !== "hello" || m.secret !== secret) return void sock.destroy();
153
+ helloed = true;
154
+ if (office && office !== sock) office.destroy();
155
+ office = sock;
156
+ hooksOpen = false;
157
+ const ptys = [...held].map(([id, h2]) => ({ id, pid: h2.proc.pid, meta: h2.meta, exit: h2.exit }));
158
+ send({ op: "ready", version: PTY_PROTOCOL, hookPort: hooks.address().port, ptys, pid: process.pid });
159
+ return;
160
+ }
161
+ const h = "id" in m ? held.get(m.id) : void 0;
162
+ switch (m.op) {
163
+ case "spawn":
164
+ return start(m);
165
+ case "attach":
166
+ return attach(m.id);
167
+ case "write":
168
+ return void (h && h.exit === null && quietly(() => h.proc.write(m.data)));
169
+ case "resize":
170
+ return void (h && h.exit === null && quietly(() => h.proc.resize(m.cols, m.rows)));
171
+ case "meta":
172
+ if (h) h.meta = m.meta;
173
+ return;
174
+ case "kill":
175
+ if (h?.exit === null) killTree(h.proc.pid);
176
+ else if (h) held.delete(m.id);
177
+ return;
178
+ case "hooksReady":
179
+ hooksOpen = true;
180
+ return passHooks();
181
+ case "hookReply":
182
+ return answer(m.rid, m.answer);
183
+ case "shutdown":
184
+ stopAll();
185
+ setTimeout(() => process.exit(0), 3e3);
186
+ return;
187
+ }
188
+ });
189
+ sock.on("error", () => void 0);
190
+ sock.on("close", () => {
191
+ if (office !== sock) return;
192
+ office = null;
193
+ officeLeft = Date.now();
194
+ for (const h of held.values()) h.attached = false;
195
+ for (const w of waiting.values()) w.sent = false;
196
+ });
197
+ }
198
+ function quietly(fn) {
199
+ try {
200
+ fn();
201
+ } catch {
202
+ }
203
+ }
204
+ var server = net.createServer(connected);
205
+ server.on("error", (err) => {
206
+ if (err.code !== "EADDRINUSE" || process.platform === "win32") {
207
+ note(`cannot listen on ${socketPath}: ${err.message}`);
208
+ process.exit(1);
209
+ }
210
+ const probe = net.connect(socketPath, () => {
211
+ probe.destroy();
212
+ process.exit(0);
213
+ });
214
+ probe.on("error", () => {
215
+ fs.rmSync(socketPath, { force: true });
216
+ server.listen(socketPath);
217
+ });
218
+ });
219
+ hooks.listen(0, "127.0.0.1", () => server.listen(socketPath));
220
+ var orphaned = false;
221
+ setInterval(() => {
222
+ if (office) return void (orphaned = false);
223
+ const away = Date.now() - officeLeft;
224
+ if (away > ORPHAN_MS) {
225
+ for (const [id, h] of held) if (h.exit !== null) held.delete(id);
226
+ if (held.size && !orphaned) {
227
+ orphaned = true;
228
+ note(`no office for ${Math.round(away / 6e4)} min: stopping ${held.size} terminal(s)`);
229
+ stopAll();
230
+ }
231
+ }
232
+ if (held.size === 0 && away > IDLE_EXIT_MS) process.exit(0);
233
+ }, 1e4).unref();
234
+ //# sourceMappingURL=ptyHost.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../server/ptyHost.ts", "../server/ptyProtocol.ts"],
4
+ "sourcesContent": ["import fs from 'node:fs';\nimport http from 'node:http';\nimport net from 'node:net';\nimport path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport * as nodePty from '@lydell/node-pty';\nimport { encode, PTY_PROTOCOL, readMessages, type FromHost, type HeldPty, type ToHost } from './ptyProtocol.ts';\n\n// The office's terminal keeper: a small process of its own that runs the agents' CLIs in pseudo-terminals, so they\n// keep working while the office restarts (an update, a crash, a code change in development). The office connects over\n// a local socket (one office at a time), starts and drives CLIs through it and, when it comes back, picks up the ones\n// still running. The CLIs' hooks come in here too and wait while the office is away, so none is lost and none fails.\n//\n// node ptyHost.js launch <socket> <secret file> starts a keeper outside the caller's process tree, then exits\n// node ptyHost.js serve <socket> <secret file> is the keeper\n\n/** Output kept per terminal while no office is listening. */\nconst BACKLOG_BYTES = 2 * 1024 * 1024;\n/** A hook waits this long for an office (the CLIs are told 120 s). */\nconst HOOK_WAIT_MS = 110_000;\n/** With no office for this long, nobody is coming back: the terminals are stopped. */\nconst ORPHAN_MS = 10 * 60_000;\n/** Nothing left to keep and no office: the keeper goes. */\nconst IDLE_EXIT_MS = 60_000;\n\nconst [mode, socketPath, secretFile] = process.argv.slice(2);\n\nif (mode === 'launch') {\n // A detached child whose parent exits at once: the office's process tree (which the launcher kills on a restart)\n // doesn't include it, and on Windows nothing ties it to a console window.\n spawn(process.execPath, [...process.execArgv, process.argv[1], 'serve', socketPath, secretFile], { detached: true, stdio: 'ignore', windowsHide: true }).unref();\n process.exit(0);\n}\n\nconst logFile = path.join(path.dirname(secretFile), 'pty-host.log');\nconst note = (text: string) => fs.appendFile(logFile, `${new Date().toISOString()} ${text}\\n`, () => undefined);\nprocess.on('uncaughtException', (err) => note(`uncaught: ${err.stack ?? err}`));\nfor (const sig of ['SIGINT', 'SIGHUP', 'SIGBREAK'] as const) process.on(sig, () => undefined); // the office's console isn't ours\n\nconst secret = fs.readFileSync(secretFile, 'utf8').trim();\n\ninterface Held {\n proc: nodePty.IPty;\n meta: unknown;\n /** An office is taking this terminal's output right now. */\n attached: boolean;\n backlog: string[];\n bytes: number;\n exit: number | null;\n}\nconst held = new Map<string, Held>();\nlet office: net.Socket | null = null;\nlet officeLeft = Date.now();\nlet hooksOpen = false; // the connected office has picked up its terminals and takes hooks\n\nconst send = (m: FromHost) => {\n if (office && !office.destroyed) office.write(encode(m));\n};\n\nfunction killTree(pid: number) {\n if (process.platform === 'win32') {\n spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).once('error', () => undefined);\n return;\n }\n try {\n process.kill(-pid, 'SIGTERM'); // the CLI leads its own session: take its MCP servers and tools with it\n } catch {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // already gone\n }\n }\n}\n\n// ---------- terminals ----------\n\nfunction start(m: Extract<ToHost, { op: 'spawn' }>) {\n let proc: nodePty.IPty;\n try {\n proc = nodePty.spawn(m.file, m.args, { name: 'xterm-256color', cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env });\n } catch (err) {\n send({ op: 'failed', id: m.id, error: (err as Error).message });\n return;\n }\n const h: Held = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null };\n held.set(m.id, h);\n send({ op: 'spawned', id: m.id, pid: proc.pid });\n proc.onData((data) => {\n if (h.attached && office) return send({ op: 'data', id: m.id, data });\n h.backlog.push(data);\n h.bytes += data.length;\n while (h.bytes > BACKLOG_BYTES && h.backlog.length > 1) h.bytes -= h.backlog.shift()!.length;\n });\n proc.onExit(({ exitCode }) => {\n h.exit = exitCode;\n if (h.attached && office) {\n send({ op: 'exit', id: m.id, code: exitCode });\n held.delete(m.id);\n }\n });\n}\n\nfunction attach(id: string) {\n const h = held.get(id);\n if (!h) return send({ op: 'exit', id, code: -1 });\n h.attached = true;\n if (h.backlog.length) send({ op: 'data', id, data: h.backlog.join('') });\n h.backlog = [];\n h.bytes = 0;\n if (h.exit !== null) {\n send({ op: 'exit', id, code: h.exit });\n held.delete(id);\n }\n}\n\nfunction stopAll() {\n for (const h of held.values()) if (h.exit === null) killTree(h.proc.pid);\n}\n\n// ---------- hooks: CLIs call here, the office answers ----------\n\ninterface Waiting {\n token: string;\n body: unknown;\n res: http.ServerResponse;\n timer: NodeJS.Timeout;\n sent: boolean;\n}\nconst waiting = new Map<number, Waiting>();\nlet nextRid = 1;\n\nfunction answer(rid: number, reply: unknown) {\n const w = waiting.get(rid);\n if (!w) return;\n waiting.delete(rid);\n clearTimeout(w.timer);\n if (!w.res.writableEnded) w.res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(reply ?? {}));\n}\n\n/** Hooks that waited for an office go to the one that's ready (again, if the office before it went without answering). */\nfunction passHooks() {\n if (!office || !hooksOpen) return;\n for (const [rid, w] of waiting) {\n if (w.sent) continue;\n w.sent = true;\n send({ op: 'hook', rid, token: w.token, body: w.body });\n }\n}\n\nconst hooks = http.createServer((req, res) => {\n const token = req.method === 'POST' ? req.url?.match(/^\\/api\\/hooks\\/([\\w-]+)$/)?.[1] : undefined;\n if (!token) return void res.writeHead(404).end();\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size <= 64 * 1024 * 1024) chunks.push(c);\n });\n req.on('end', () => {\n let body: unknown = {};\n try {\n body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');\n } catch {\n // not JSON: an empty hook\n }\n const rid = nextRid++;\n waiting.set(rid, { token, body, res, timer: setTimeout(() => answer(rid, {}), HOOK_WAIT_MS), sent: false });\n passHooks();\n });\n});\n\n// ---------- the office ----------\n\nfunction connected(sock: net.Socket) {\n let helloed = false;\n readMessages<ToHost>(sock, (m) => {\n if (!helloed) {\n if (m.op !== 'hello' || m.secret !== secret) return void sock.destroy();\n helloed = true;\n if (office && office !== sock) office.destroy();\n office = sock;\n hooksOpen = false;\n const ptys: HeldPty[] = [...held].map(([id, h]) => ({ id, pid: h.proc.pid, meta: h.meta, exit: h.exit }));\n send({ op: 'ready', version: PTY_PROTOCOL, hookPort: (hooks.address() as net.AddressInfo).port, ptys, pid: process.pid });\n return;\n }\n const h = 'id' in m ? held.get(m.id) : undefined;\n switch (m.op) {\n case 'spawn':\n return start(m);\n case 'attach':\n return attach(m.id);\n case 'write':\n return void (h && h.exit === null && quietly(() => h.proc.write(m.data)));\n case 'resize':\n return void (h && h.exit === null && quietly(() => h.proc.resize(m.cols, m.rows)));\n case 'meta':\n if (h) h.meta = m.meta;\n return;\n case 'kill':\n if (h?.exit === null) killTree(h.proc.pid);\n else if (h) held.delete(m.id);\n return;\n case 'hooksReady':\n hooksOpen = true;\n return passHooks();\n case 'hookReply':\n return answer(m.rid, m.answer);\n case 'shutdown':\n stopAll();\n setTimeout(() => process.exit(0), 3000);\n return;\n }\n });\n sock.on('error', () => undefined);\n sock.on('close', () => {\n if (office !== sock) return;\n office = null;\n officeLeft = Date.now();\n for (const h of held.values()) h.attached = false;\n for (const w of waiting.values()) w.sent = false; // unanswered: the next office gets them\n });\n}\n\n/** A pseudo-terminal whose CLI just died throws on writes and resizes before it reports the exit. */\nfunction quietly(fn: () => void) {\n try {\n fn();\n } catch {\n // its exit is on the way\n }\n}\n\nconst server = net.createServer(connected);\nserver.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code !== 'EADDRINUSE' || process.platform === 'win32') {\n note(`cannot listen on ${socketPath}: ${err.message}`);\n process.exit(1);\n }\n // A Unix socket file left behind: take it over, unless a keeper still answers on it.\n const probe = net.connect(socketPath, () => {\n probe.destroy();\n process.exit(0);\n });\n probe.on('error', () => {\n fs.rmSync(socketPath, { force: true });\n server.listen(socketPath);\n });\n});\n\nhooks.listen(0, '127.0.0.1', () => server.listen(socketPath));\n\nlet orphaned = false;\nsetInterval(() => {\n if (office) return void (orphaned = false);\n const away = Date.now() - officeLeft;\n if (away > ORPHAN_MS) {\n for (const [id, h] of held) if (h.exit !== null) held.delete(id);\n if (held.size && !orphaned) {\n orphaned = true;\n note(`no office for ${Math.round(away / 60_000)} min: stopping ${held.size} terminal(s)`);\n stopAll();\n }\n }\n if (held.size === 0 && away > IDLE_EXIT_MS) process.exit(0);\n}, 10_000).unref();\n", "import crypto from 'node:crypto';\nimport path from 'node:path';\nimport type { Socket } from 'node:net';\n\n// What the office and its terminal keeper (ptyHost.ts) say to each other over their local socket: one JSON message\n// per line. Kept free of the rest of the server so the keeper stays small and starts fast.\n\n/** Bumped when the messages change: an office that finds a keeper on another version replaces it. */\nexport const PTY_PROTOCOL = 1;\n\n/** A terminal the keeper holds, as it reports it to an office that (re)connects. */\nexport interface HeldPty {\n id: string;\n pid: number;\n /** What the office stored with it (who it belongs to, its session). */\n meta: unknown;\n /** The CLI has exited (while no office was listening): its exit code. */\n exit: number | null;\n}\n\nexport type ToHost =\n | { op: 'hello'; secret: string; version: number }\n | { op: 'hooksReady' } // the office has picked up its terminals: hooks that waited for it can come in now\n | { op: 'spawn'; id: string; file: string; args: string[]; cwd: string; env: Record<string, string>; cols: number; rows: number; meta: unknown }\n | { op: 'attach'; id: string } // an office picking up a terminal it found: what it missed, then everything live\n | { op: 'write'; id: string; data: string }\n | { op: 'resize'; id: string; cols: number; rows: number }\n | { op: 'meta'; id: string; meta: unknown }\n | { op: 'kill'; id: string }\n | { op: 'hookReply'; rid: number; answer: unknown }\n | { op: 'shutdown' }; // stop every terminal and exit (a keeper on another version)\n\nexport type FromHost =\n | { op: 'ready'; version: number; hookPort: number; ptys: HeldPty[]; pid?: number }\n | { op: 'spawned'; id: string; pid: number }\n | { op: 'failed'; id: string; error: string }\n | { op: 'data'; id: string; data: string }\n | { op: 'exit'; id: string; code: number }\n | { op: 'hook'; rid: number; token: string; body: unknown };\n\n/** The keeper's socket for an office home: a named pipe on Windows, a Unix socket in the home elsewhere. */\nexport function hostSocket(home: string, platform = process.platform): string {\n if (platform !== 'win32') return path.join(home, 'pty.sock');\n const tag = crypto.createHash('sha1').update(path.resolve(home).toLowerCase()).digest('hex').slice(0, 12);\n return `\\\\\\\\.\\\\pipe\\\\cubefarm-pty-${tag}`;\n}\n\nexport const encode = (m: ToHost | FromHost) => `${JSON.stringify(m)}\\n`;\n\n/** Calls back with each message a socket sends (newline-delimited JSON); a line that isn't JSON is skipped. */\nexport function readMessages<T>(sock: Socket, onMessage: (m: T) => void) {\n let buf = '';\n sock.setEncoding('utf8');\n sock.on('data', (chunk: string) => {\n buf += chunk;\n let nl: number;\n while ((nl = buf.indexOf('\\n')) >= 0) {\n const line = buf.slice(0, nl);\n buf = buf.slice(nl + 1);\n if (!line) continue;\n let m: T;\n try {\n m = JSON.parse(line) as T;\n } catch {\n continue;\n }\n onMessage(m);\n }\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["h"]
7
+ }
package/package.json CHANGED
@@ -1,78 +1,87 @@
1
- {
2
- "name": "cubefarm",
3
- "version": "0.2.0",
4
- "description": "A cartoon 3D office where a team of Claude Code agents works through your GitHub issues.",
5
- "keywords": [
6
- "claude-code",
7
- "claude-agent-sdk",
8
- "ai-agents",
9
- "coding-agents",
10
- "github-issues",
11
- "automation",
12
- "threejs"
13
- ],
14
- "homepage": "https://github.com/leonvanzyl/cubefarm#readme",
15
- "bugs": "https://github.com/leonvanzyl/cubefarm/issues",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/leonvanzyl/cubefarm.git"
19
- },
20
- "license": "MIT",
21
- "author": "Leon van Zyl",
22
- "type": "module",
23
- "bin": {
24
- "cubefarm": "bin/cubefarm.js"
25
- },
26
- "files": [
27
- "bin",
28
- "dist",
29
- "dist-server"
30
- ],
31
- "engines": {
32
- "node": ">=22"
33
- },
34
- "scripts": {
35
- "dev": "node scripts/office.mjs --dev",
36
- "demo": "node scripts/office.mjs --dev --demo",
37
- "build": "tsc --noEmit -p tsconfig.json && vite build && node scripts/build-server.mjs",
38
- "start": "node scripts/office.mjs",
39
- "typecheck": "tsc --noEmit -p tsconfig.json",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "test:e2e": "playwright test",
43
- "prepublishOnly": "npm test && npm run build"
44
- },
45
- "dependencies": {
46
- "@anthropic-ai/claude-agent-sdk": "^0.3.283",
47
- "express": "^5.2.1",
48
- "ws": "^8.22.0",
49
- "zod": "^4.0.0"
50
- },
51
- "devDependencies": {
52
- "@dimforge/rapier3d-compat": "0.19.2",
53
- "@playwright/test": "1.56.1",
54
- "@react-three/drei": "^10.7.9",
55
- "@react-three/fiber": "^9.8.1",
56
- "@react-three/rapier": "^2.2.0",
57
- "@types/express": "^5.0.6",
58
- "@types/node": "^26.6.3",
59
- "@types/react": "^19.3.0",
60
- "@types/react-dom": "^19.3.0",
61
- "@types/three": "^0.186.0",
62
- "@types/ws": "^8.18.1",
63
- "@vitejs/plugin-react": "^6.1.1",
64
- "concurrently": "^10.0.5",
65
- "esbuild": "^0.28.2",
66
- "react": "^19.3.0",
67
- "react-dom": "^19.3.0",
68
- "react-markdown": "^10.1.0",
69
- "remark-breaks": "^4.0.0",
70
- "remark-gfm": "^4.0.1",
71
- "three": "^0.186.1",
72
- "tsx": "^4.23.15",
73
- "typescript": "^7.0.2",
74
- "vite": "^8.3.1",
75
- "vitest": "^5.0.2",
76
- "zustand": "^5.0.15"
77
- }
78
- }
1
+ {
2
+ "name": "cubefarm",
3
+ "version": "0.3.0",
4
+ "description": "A cartoon 3D office where a team of AI coding agents works through your GitHub issues.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "codex",
8
+ "opencode",
9
+ "claude-agent-sdk",
10
+ "ai-agents",
11
+ "coding-agents",
12
+ "github-issues",
13
+ "automation",
14
+ "threejs"
15
+ ],
16
+ "homepage": "https://github.com/leonvanzyl/cubefarm#readme",
17
+ "bugs": "https://github.com/leonvanzyl/cubefarm/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/leonvanzyl/cubefarm.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Leon van Zyl",
24
+ "type": "module",
25
+ "bin": {
26
+ "cubefarm": "bin/cubefarm.js"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "dist",
31
+ "dist-server"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "scripts": {
37
+ "dev": "node scripts/office.mjs --dev",
38
+ "demo": "node scripts/office.mjs --dev --demo",
39
+ "build": "tsc --noEmit -p tsconfig.json && vite build && node scripts/build-server.mjs",
40
+ "start": "node scripts/office.mjs",
41
+ "typecheck": "tsc --noEmit -p tsconfig.json",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "test:e2e": "playwright test",
45
+ "prepublishOnly": "npm test && npm run build"
46
+ },
47
+ "dependencies": {
48
+ "@anthropic-ai/claude-agent-sdk": "^0.3.283",
49
+ "@lydell/node-pty": "1.2.0-beta.15",
50
+ "@modelcontextprotocol/sdk": "^1.30.1",
51
+ "@xterm/addon-serialize": "^0.14.0",
52
+ "@xterm/headless": "^6.0.0",
53
+ "express": "^5.2.1",
54
+ "ws": "^8.22.0",
55
+ "zod": "^4.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "@dimforge/rapier3d-compat": "0.19.2",
59
+ "@playwright/test": "1.56.1",
60
+ "@react-three/drei": "^10.7.9",
61
+ "@react-three/fiber": "^9.8.1",
62
+ "@react-three/rapier": "^2.2.0",
63
+ "@types/express": "^5.0.6",
64
+ "@types/node": "^26.6.3",
65
+ "@types/react": "^19.3.0",
66
+ "@types/react-dom": "^19.3.0",
67
+ "@types/three": "^0.186.0",
68
+ "@types/ws": "^8.18.1",
69
+ "@vitejs/plugin-react": "^6.1.1",
70
+ "@xterm/addon-fit": "^0.11.0",
71
+ "@xterm/addon-web-links": "^0.12.0",
72
+ "@xterm/xterm": "^6.0.0",
73
+ "concurrently": "^10.0.5",
74
+ "esbuild": "^0.28.2",
75
+ "react": "^19.3.0",
76
+ "react-dom": "^19.3.0",
77
+ "react-markdown": "^10.1.0",
78
+ "remark-breaks": "^4.0.0",
79
+ "remark-gfm": "^4.0.1",
80
+ "three": "^0.186.1",
81
+ "tsx": "^4.23.15",
82
+ "typescript": "^7.0.2",
83
+ "vite": "^8.3.1",
84
+ "vitest": "^5.0.2",
85
+ "zustand": "^5.0.15"
86
+ }
87
+ }
@@ -1 +0,0 @@
1
- :root{--ink:#1f1d2b;--paper:#fffdf6;--muted:#6c7086;--good:#2dc653;--bad:#ef476f;--warn:#ffb703;--accent:#ff8a5b;--font:Fredoka, "Segoe UI", system-ui, sans-serif;--mono:"JetBrains Mono", Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}*{box-sizing:border-box}html,body,#root{height:100%;font-family:var(--font);color:var(--ink);background:#bfe3ff;margin:0;overflow:hidden}canvas{display:block}a{color:#3a56d4}kbd{border:2px solid var(--ink);min-width:1.6em;font-family:var(--font);text-align:center;background:#fff;border-bottom-width:4px;border-radius:6px;margin:0 .1em;padding:0 .4em;font-size:.85em;font-weight:600;line-height:1.5;display:inline-block}code{font-family:var(--mono);background:#0000000f;border-radius:5px;padding:1px 5px;font-size:.85em}.muted{color:var(--muted)}.small{font-size:.85rem}.spacer{flex:1}.grow{flex:1;min-width:0}.nowrap{white-space:nowrap}.btn{font-family:var(--font);border:2.5px solid var(--ink);color:var(--ink);box-shadow:0 3px 0 var(--ink);cursor:pointer;white-space:nowrap;background:#fff;border-radius:12px;padding:8px 14px;font-size:.95rem;font-weight:600;transition:transform 60ms,box-shadow 60ms}.btn:hover:not(:disabled){box-shadow:0 4px 0 var(--ink);transform:translateY(-1px)}.btn:active:not(:disabled){box-shadow:0 1px 0 var(--ink);transform:translateY(2px)}.btn:disabled{opacity:.45;cursor:not-allowed}.btn-good{background:#80ed99}.btn-bad{background:#ff8fa3}.btn-ghost{box-shadow:none;background:0 0;border-color:#1f1d2b59}.btn-small{box-shadow:0 2px 0 var(--ink);border-width:2px;border-radius:9px;padding:4px 10px;font-size:.82rem}.btn-ghost.btn-small{box-shadow:none}.btn-big{background:var(--accent);color:#fff;text-shadow:0 2px #0003;box-shadow:0 5px 0 var(--ink);border-radius:18px;padding:14px 32px;font-size:1.3rem}a.btn{text-decoration:none;display:inline-block}input,textarea,select{font-family:var(--font);color:var(--ink);background:#fff;border:2px solid #1f1d2b40;border-radius:10px;outline:none;width:100%;padding:8px 10px;font-size:.95rem}input:focus,textarea:focus,select:focus{border-color:var(--accent)}input[type=checkbox],input[type=radio]{width:auto;accent-color:var(--accent)}input[type=color]{width:42px;height:34px;padding:2px}textarea{resize:vertical}textarea.msg-box{resize:none;flex:1;min-width:0;max-height:calc(8.1em + 20px);line-height:1.35;overflow-y:auto}input.inline{background:0 0;border-color:#0000;padding:4px 6px}input.inline:hover,input.inline:focus{background:#fff;border-color:#1f1d2b40}.row{align-items:center;gap:8px;margin:6px 0;display:flex}.row.wrap{flex-wrap:wrap}.toggle{cursor:pointer;align-items:center;gap:6px;font-size:.92rem;display:inline-flex}.toggle.block{align-items:flex-start;margin:8px 0;display:flex}.pill,.chip{border:2px solid var(--ink);background:#fff;border-radius:999px;align-items:center;gap:4px;padding:3px 10px;font-size:.8rem;font-weight:600;display:inline-flex}.chip{color:var(--ink);border-width:1.5px;padding:1px 8px;font-size:.72rem;text-decoration:none}.chip-good,.pill-ok{background:#b9fbc0}.pill-bad{background:#ffc2d1}.pill-demo{background:#ffd166}.dot{border:2px solid var(--ink);border-radius:50%;width:12px;height:12px;display:inline-block}.agent-chip{align-items:center;gap:5px;font-size:.85rem;font-weight:600;display:inline-flex}.status{border:2px solid var(--ink);text-transform:uppercase;letter-spacing:.03em;border-radius:999px;padding:1px 9px;font-size:.75rem;font-weight:600;display:inline-block}.status-idle{background:#e9ecef}.status-preparing{background:#ffe066}.status-working{background:#74c0fc}.status-done{background:#8ce99a}.status-error{background:#ffa8a8}.status-stopped{background:#dee2e6}.status-installing,.status-starting{background:#ffe066}.status-running{background:#8ce99a}.status-unconfigured{background:#e9ecef}.hud{pointer-events:none;z-index:10;position:fixed;inset:0}.hud>*{pointer-events:auto}.hud-floor{background:var(--paper);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);border-radius:18px;align-items:center;gap:12px;max-width:min(520px,100vw - 32px);padding:8px 16px 8px 8px;display:flex;position:absolute;top:16px;left:16px}.floor-num{background:var(--accent);border:3px solid var(--ink);color:#fff;text-shadow:0 2px #00000040;border-radius:12px;flex:none;place-items:center;width:46px;height:46px;font-size:1.5rem;font-weight:700;display:grid}.floor-name{text-overflow:ellipsis;white-space:nowrap;font-size:1.1rem;font-weight:700;overflow:hidden}.floor-sub{color:var(--muted);font-size:.85rem}.hud-status{flex-wrap:wrap;justify-content:flex-end;gap:6px;max-width:50vw;display:flex;position:absolute;top:16px;right:16px}.hud-banner{border:3px solid var(--ink);background:#ffe066;border-radius:14px;max-width:90vw;padding:8px 16px;font-weight:600;position:absolute;top:84px;left:50%;transform:translate(-50%)}.office-chip{max-width:min(420px,100vw - 32px);font-family:var(--font);color:var(--ink);border:2px solid var(--ink);box-shadow:0 3px 0 var(--ink);cursor:pointer;text-align:left;background:#ffe8a3;border-radius:999px;padding:5px 12px;font-size:.8rem;font-weight:600;position:absolute;top:100px;left:16px}.office-chip:hover{transform:translateY(-1px)}.office-chip:focus-visible{outline:3px solid var(--accent,#ff8a5b);outline-offset:2px}.crosshair{border:2px solid var(--ink);pointer-events:none;background:#fff;border-radius:50%;width:8px;height:8px;margin:-4px 0 0 -4px;transition:transform .15s;position:absolute;top:50%;left:50%}.crosshair-hot{background:var(--accent);transform:scale(1.8)}.hud-hint{background:var(--paper);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);white-space:nowrap;pointer-events:none;border-radius:14px;padding:8px 16px;font-size:1.05rem;font-weight:600;position:absolute;bottom:22%;left:50%;transform:translate(-50%)}.hud-held{font-size:.95rem;bottom:calc(22% - 56px)}.charge{border:2px solid var(--ink);background:var(--paper);opacity:0;pointer-events:none;border-radius:6px;width:56px;height:9px;margin-left:-28px;transition:opacity .1s;position:absolute;top:calc(50% + 14px);left:50%;overflow:hidden}.charge-on{opacity:1}.charge-fill{transform-origin:0;background:#ffd166;height:100%;transform:scaleX(0)}.charge-full .charge-fill{background:#ff8a5b}.hud-ammo{background:var(--paper);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);pointer-events:none;border-radius:14px;flex-direction:column;align-items:flex-end;gap:4px;padding:8px 14px;font-weight:700;display:flex;position:absolute;bottom:72px;right:16px}.hud-ammo-count{font-variant-numeric:tabular-nums;font-size:1.15rem}.hud-ammo-empty .hud-ammo-count{color:#ef476f}.hud-ammo-note{opacity:.75;font-size:.8rem;font-weight:600}.hud-ammo-bar{border:2px solid var(--ink);border-radius:6px;width:100%;min-width:110px;height:8px;overflow:hidden}.hud-ammo-fill{transform-origin:0;background:#ffd166;height:100%;transform:scaleX(0)}.hud-resume{color:#fff;text-shadow:0 2px 0 var(--ink), 0 0 6px var(--ink);pointer-events:none;font-weight:600;position:absolute;top:calc(50% + 26px);left:50%;transform:translate(-50%)}.workers{background:var(--paper);border:3px solid var(--ink);width:min(360px,100vw - 32px);max-height:calc(100vh - 180px);box-shadow:0 4px 0 var(--ink);border-radius:18px;flex-direction:column;display:flex;position:absolute;top:60px;right:16px;overflow:hidden}.workers-closed{width:auto}.wk-head{font-family:var(--font);letter-spacing:.08em;text-transform:uppercase;color:var(--ink);cursor:pointer;text-align:left;background:0 0;border:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 14px;font-size:.8rem;font-weight:600;display:flex}.wk-head b{color:#1e9e45}.wk-chevron{color:var(--muted);letter-spacing:0;text-transform:none}.wk-list{padding:0 6px 6px;overflow-y:auto}.wk-empty{color:var(--muted);padding:4px 10px 8px;font-size:.85rem}.wk-row{width:100%;font-family:var(--font);color:var(--ink);text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;padding:5px 8px 5px 26px;display:grid}.wk-row:hover{background:#1f1d2b0f}.wk-dot{border:2px solid var(--ink);border-radius:50%;width:12px;height:12px}.wk-main{flex-direction:column;min-width:0;display:flex}.wk-name{white-space:nowrap;text-overflow:ellipsis;font-size:.95rem;font-weight:600;overflow:hidden}.wk-task{color:var(--muted);margin-left:6px;font-size:.78rem;font-weight:500}.wk-line{gap:6px;min-width:0;display:flex}.wk-last{min-width:0;font-family:var(--mono);color:var(--muted);white-space:nowrap;text-overflow:ellipsis;flex:auto;font-size:.72rem;overflow:hidden}.wk-last-text{font-family:var(--font);color:var(--ink);font-size:.8rem}.wk-last-thinking{font-style:italic}.wk-last-error{color:var(--bad)}.wk-ago{color:var(--muted);flex:none;font-size:.72rem}.wk-group+.wk-group{margin-top:4px}.wk-group-head{width:100%;font-family:var(--font);letter-spacing:.05em;text-transform:uppercase;color:var(--muted);text-align:left;cursor:pointer;background:0 0;border:0;border-radius:10px;align-items:center;gap:8px;padding:6px 8px 2px;font-size:.75rem;font-weight:600;display:flex}.wk-group-head:hover{color:var(--ink)}.wk-group-arrow{width:10px}.wk-swatch{border:2px solid var(--ink);border-radius:4px;flex:none;width:12px;height:12px}.wk-group-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--ink);overflow:hidden}.wk-group-count{color:var(--ink);background:#1f1d2b14;border-radius:999px;margin-left:auto;padding:0 7px}@media (width<=700px){.workers{display:none}}.hud-help{pointer-events:none;background:#fffdf6d9;border-radius:12px;padding:6px 10px;font-size:.85rem;position:absolute;bottom:14px;left:16px}.stats{z-index:60;font:10px/1.2 var(--mono);color:#7cffb2;white-space:nowrap;pointer-events:none;background:#15151fcc;border-bottom-right-radius:8px;padding:1px 8px;position:fixed;top:0;left:0}.fade{opacity:0;pointer-events:none;background:#15151f;place-items:center;transition:opacity .6s;display:grid;position:absolute;inset:0}.fade-in{opacity:1}.fade-label{color:#7cffb2;font-size:3rem;font-weight:700}.toasts{z-index:40;flex-direction:column;align-items:flex-end;gap:8px;max-width:min(420px,100vw - 32px);display:flex;position:absolute;bottom:16px;right:16px}.toast{background:var(--paper);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);cursor:pointer;border-radius:14px;padding:10px 14px;font-weight:500;animation:.25s ease-out pop}.toast-success{background:#d8f9df}.toast-error{background:#ffd6de}@keyframes pop{0%{opacity:0;transform:translateY(12px)scale(.95)}}.start{z-index:50;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:radial-gradient(circle at 30% 20%,#ffd6a5bf,#74c0fccc);place-items:center;padding:16px;display:grid;position:fixed;inset:0}.start-card{background:var(--paper);border:4px solid var(--ink);width:min(640px,100%);box-shadow:0 8px 0 var(--ink);text-align:center;border-radius:28px;max-height:calc(100vh - 32px);padding:28px 32px;overflow:auto}.start-logo{color:var(--accent);font-size:4rem;line-height:1;animation:12s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.start h1{margin:6px 0 4px;font-size:2.6rem}.start-tag{color:var(--muted);margin:0 0 16px;font-size:1.1rem}.start-list{text-align:left;gap:8px;margin:0 0 22px;padding:0;list-style:none;display:grid}.start-meta{color:var(--muted);flex-wrap:wrap;justify-content:center;align-items:center;gap:10px;margin-top:16px;display:flex}.overlay{z-index:30;background:#15151f59;place-items:center;padding:16px;animation:.15s ease-out fadein;display:grid;position:fixed;inset:0}@keyframes fadein{0%{opacity:0}}.panel{background:var(--paper);border:4px solid var(--ink);width:min(640px,100%);max-height:calc(100vh - 32px);box-shadow:0 8px 0 var(--ink);border-radius:24px;flex-direction:column;animation:.2s ease-out pop;display:flex;overflow:hidden}.panel-wide{width:min(1320px,100%);height:calc(100vh - 32px)}.panel-head{background:var(--accent);border-bottom:4px solid var(--ink);align-items:center;gap:10px;padding:12px 16px;display:flex}.panel-title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:1.25rem;font-weight:700;overflow:hidden}.panel-title a{color:var(--ink)}.panel-x{border:3px solid var(--ink);cursor:pointer;background:#fff;border-radius:12px;flex:none;width:36px;height:36px;font-weight:700}.panel-body{flex-direction:column;flex:1;min-height:0;padding:16px;display:flex;overflow:auto}.help h3{margin:14px 0 4px}.help p{margin:0;line-height:1.55}.mouse-settings{border:2px solid var(--ink);background:#fff8ef;border-radius:12px;flex-direction:column;gap:8px;margin-top:10px;padding:10px 12px;display:flex}.mouse-sens{align-items:center;gap:10px;font-size:.92rem;display:flex}.mouse-sens input[type=range]{max-width:260px;accent-color:var(--accent,#ff8a5b);flex:1}.mouse-sens b{font-variant-numeric:tabular-nums;min-width:3.4em}.sound{gap:10px 18px}.sound-volume{flex:200px;align-items:center;gap:8px;max-width:340px;display:flex}.sound-volume input[type=range]{accent-color:var(--accent);background:0 0;border:0;flex:1;padding:0}.sound-pct{text-align:right;font-variant-numeric:tabular-nums;min-width:3.2em}.elevator{flex-direction:column;gap:10px;display:flex}.floor-btn{text-align:left;border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);cursor:pointer;font-family:var(--font);background:#fff;border-radius:16px;grid-template-rows:auto auto;grid-template-columns:54px 1fr;align-items:center;column-gap:12px;padding:10px;display:grid}.floor-btn:hover{transform:translateY(-1px)}.floor-btn-here{outline:3px dashed var(--accent);outline-offset:3px;background:#fff4e6}.floor-btn-num{border:3px solid var(--ink);background:var(--accent);color:#fff;text-shadow:0 2px #00000040;border-radius:50%;grid-row:span 2;place-items:center;width:54px;height:54px;font-size:1.5rem;font-weight:700;display:grid}.floor-btn-name{text-overflow:ellipsis;white-space:nowrap;font-size:1.1rem;font-weight:700;overflow:hidden}.floor-btn-meta{color:var(--muted);font-size:.85rem}.term-title{align-items:center;gap:10px;display:flex}.avatar{border:3px solid var(--ink);color:#fff;text-shadow:0 1px #0000004d;border-radius:50%;flex:none;place-items:center;width:34px;height:34px;font-weight:700;display:grid}.term-meta{flex-wrap:wrap;align-items:center;gap:6px 14px;margin-bottom:10px;font-size:.92rem;display:flex}.term-error{word-break:break-word;background:#ffe3e3;border:2px solid #ff8787;border-radius:10px;margin-bottom:8px;padding:6px 10px;font-size:.88rem}.term-split{flex:1;grid-template-columns:1fr;gap:12px;min-height:0;display:grid}.term-split-2{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr)}.term{color:#e8e8f2;border:3px solid var(--ink);font-family:var(--mono);background:#1b1b29;border-radius:14px;min-height:200px;padding:12px 14px;font-size:13px;line-height:1.5;overflow:auto}.term-line{white-space:pre-wrap;word-break:break-word}.term-tool{color:#8be9fd}.term-result{color:#8d8da8}.term-system{color:#bd93f9}.term-error.term-line,.term-line.term-error{color:#ff6b6b;font-size:inherit;background:0 0;border:none;margin:0;padding:0}.term-manager{color:#ffb86c;font-weight:600}.term-done{color:#50fa7b;font-weight:600}.term-thinking{color:#f1fa8c;font-style:italic}.term-spin{color:#ff9e64;animation:1.4s ease-in-out infinite blink}@keyframes blink{50%{opacity:.55}}.browser{flex-direction:column;gap:6px;min-height:0;display:flex}.browser-bar{border:3px solid var(--ink);text-overflow:ellipsis;white-space:nowrap;background:#e9ecf2;border-bottom:none;border-radius:14px 14px 0 0;padding:6px 12px;font-size:.85rem;overflow:hidden}.browser-view{border:3px solid var(--ink);background:#fff;border-radius:0 0 14px 14px;flex:1;min-height:0;margin-top:-6px;overflow:auto}.browser-view img{width:100%;display:block}.term-input{align-items:flex-end;gap:8px;margin-top:12px;display:flex}.term-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px;display:flex}.term-actions select{width:auto;max-width:420px}.kanban-toolbar{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:12px;display:flex}.issue-form{background:#fff;border:3px dashed #1f1d2b4d;border-radius:16px;flex-direction:column;gap:8px;margin-bottom:12px;padding:12px;display:flex}.issue-form .row select{flex:1}.kanban{flex:1;grid-template-columns:repeat(4,minmax(220px,1fr));gap:12px;min-height:0;display:grid;overflow-x:auto}.kcol{border:3px solid var(--ink);background:#f4f1ea;border-radius:18px;flex-direction:column;min-height:0;display:flex;overflow:hidden}.kcol-head{border-bottom:3px solid var(--ink);padding:10px 12px;font-weight:700}.kcol-backlog .kcol-head{background:#ffd166}.kcol-progress .kcol-head{background:#4cc9f0}.kcol-review .kcol-head{background:#80ed99}.kcol-qa .kcol-head{background:#ff9f68}.kanban-5{grid-template-columns:repeat(5,minmax(210px,1fr))}.kcol-merged .kcol-head{background:#c77dff}.count{border:2px solid var(--ink);text-align:center;background:#fff;border-radius:999px;min-width:24px;padding:0 6px;font-size:.8rem;display:inline-block}.kcol-body{flex-direction:column;gap:8px;padding:10px;display:flex;overflow:auto}.kempty{padding:8px}.kcard{border:2.5px solid var(--ink);background:#fff;border-radius:12px;padding:8px 10px;box-shadow:0 3px #1f1d2bcc}.kcard-warn{background:#fff3d6}.kcard-bad{background:#ffe0e6}.kcard-good{background:#e3fbe7}.kcard-title{font-size:.92rem;font-weight:500;line-height:1.35}.kcard-title a{font-weight:700}.kcard-foot{align-items:center;gap:6px;margin-top:6px;display:flex}.kcard-foot select{width:auto;margin-left:auto;padding:3px 6px;font-size:.82rem}.kcard-foot-wrap{flex-wrap:wrap}.panel-app{width:100%;max-width:1600px}.panel-app .panel-body{padding:12px 16px}.app-title{align-items:center;gap:10px;min-width:0;display:flex}.app-title-name,.app-title-ref{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.app-title-name{flex:0 auto}.app-title-ref{color:var(--ink);opacity:.75;flex:0 2 auto;font-weight:500}.app-title .status{flex:none}.app-toolbar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px;display:flex}.app-ref{flex:0 auto;width:260px;min-width:0;padding:5px 8px;font-size:.88rem}.seg{border:2px solid var(--ink);box-shadow:0 2px 0 var(--ink);border-radius:9px;display:inline-flex;overflow:hidden}.seg-btn{font-family:var(--font);color:var(--ink);cursor:pointer;white-space:nowrap;background:#fff;border:0;padding:4px 10px;font-size:.82rem;font-weight:600}.seg-btn+.seg-btn{border-left:2px solid var(--ink)}.seg-btn.seg-on{background:var(--accent)}.seg-btn:focus-visible{outline-offset:-3px;outline:3px solid #3a56d4}.app-stage{border:3px solid var(--ink);background:repeating-linear-gradient(45deg,#efeadf 0 12px,#f6f2e9 12px 24px);border-radius:14px;flex:1;justify-content:center;min-height:280px;display:flex;position:relative;overflow:hidden}.app-frame{background:#fff;border:0;width:100%;height:100%;display:block}.app-stage-phone .app-frame{border-left:3px solid var(--ink);border-right:3px solid var(--ink);width:396px;max-width:100%}.app-frame-hint{border:2.5px solid var(--ink);width:min(560px,100% - 24px);box-shadow:0 3px 0 var(--ink);background:#fff3d6;border-radius:12px;padding:10px 14px;font-size:.9rem;line-height:1.4;position:absolute;bottom:16px;left:50%;transform:translate(-50%)}.app-foot{margin:8px 0 0;line-height:1.4}.app-state{text-align:center;background:#fff;border:3px dashed #1f1d2b4d;border-radius:14px;flex-direction:column;flex:1;align-items:center;gap:10px;min-height:0;padding:24px 16px;display:flex;overflow:auto}.app-state h3{margin:0}.app-state p{max-width:560px;margin:0}.app-state-left{text-align:left;align-items:stretch}.app-state-left p{max-width:none}.app-state-icon{font-size:2.6rem;line-height:1}.app-spin{animation:2.4s linear infinite spin}.app-steps{text-align:left;color:var(--muted);margin:0;padding:0;font-weight:500;list-style:none}.app-steps li{padding:2px 0}.app-step-done{color:#1b873a}.app-step-now{color:var(--ink);font-weight:700}.app-log{text-align:left;white-space:pre-wrap;word-break:break-word;width:100%;max-width:900px;min-height:0;max-height:320px;margin:0;font-size:12px}.app-state-left .app-log{flex:0 auto;max-width:none}.app-settings{width:min(640px,100%)}.preview-form{flex-direction:column;gap:2px;display:flex}.preview-form input,.preview-form textarea{font-family:var(--mono);font-size:.85rem}.preview-form p{margin:2px 0 4px}.preview-details summary{cursor:pointer;font-weight:600}.preview-details summary code{word-break:break-all;font-weight:400}.preview-details[open] summary{margin-bottom:6px}.preview-details .status{font-size:.68rem}@media (width<=520px){.panel-app .panel-body{padding:10px}.app-ref{flex:100%;max-width:none}.app-toolbar .spacer,.app-title-ref{display:none}}.tabs{flex-wrap:wrap;gap:6px;margin-bottom:12px;display:flex}.tab{font-family:var(--font);border:2.5px solid var(--ink);cursor:pointer;background:#fff;border-radius:12px;padding:8px 14px;font-size:.95rem;font-weight:600}.tab-on{background:var(--accent);color:#fff;text-shadow:0 1px #00000040;box-shadow:0 3px 0 var(--ink)}.tab-body{flex:1;min-height:0;overflow:auto}.tab-grid{grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);align-items:start;gap:16px;display:grid}@media (width<=900px){.tab-grid{grid-template-columns:1fr}.kanban,.kanban-5{grid-template-columns:repeat(5,250px)}.term-split-2{grid-template-rows:minmax(200px,1fr) auto;grid-template-columns:1fr}}.section{margin:0 0 10px}.card{border:3px solid var(--ink);background:#fff;border-radius:18px;flex-direction:column;gap:6px;margin-bottom:12px;padding:12px 14px;display:flex}.card h3{margin:0 0 4px}.floor-card{border-left:12px solid var(--accent)}.floor-badge{background:var(--accent);border:2.5px solid var(--ink);color:#fff;text-shadow:0 1px #00000040;border-radius:10px;flex:none;place-items:center;width:36px;height:36px;font-weight:700;display:grid}.office-card{border-left:12px solid var(--ink)}.office-badge{text-shadow:none;background:#fff3c4}.office-card .grow{min-width:190px}.office-state{overflow-wrap:anywhere;font-weight:600}.office-state-ok{color:#1b873a}.office-state-busy{color:#9a5b00}.office-state-bad{color:#c0223b}.office-actions{flex:none;gap:6px;margin-left:auto;display:flex}.repo-list{border:2px solid #1f1d2b1f;border-radius:12px;flex-direction:column;gap:4px;max-height:260px;padding:6px;display:flex;overflow:auto}.repo-list.tall{max-height:520px}.repo-row{border-radius:10px;justify-content:space-between;align-items:center;gap:10px;padding:6px 8px;display:flex}.repo-row:hover{background:#fff4e6}.repo-row select{width:auto}.links{font-size:.85rem}.team{border-collapse:collapse;width:100%}.team td{vertical-align:middle;border-top:1.5px solid #1f1d2b14;padding:4px 6px}.team select{width:auto;padding:4px 6px}.field{flex-direction:column;gap:4px;margin:6px 0;font-size:.9rem;font-weight:600;display:flex}.wizard{text-align:left;width:min(680px,100%)}.wizard h1,.wizard .start-logo,.wizard .start-tag,.wizard h2,.wizard-icon{text-align:center}.wizard h2{margin:4px 0 6px;font-size:1.9rem}.wizard-icon{font-size:3rem;line-height:1.1}.wizard .start-logo{width:fit-content;margin:0 auto;display:block}.wizard .btn-big{margin:0 auto;display:block}.wizard-steps{justify-content:center;gap:8px;margin-bottom:10px;display:flex}.wizard-dot{border:2px solid var(--ink);background:#fff;border-radius:50%;width:12px;height:12px}.wizard-dot-done{background:#80ed99}.wizard-dot-on{background:var(--accent);transform:scale(1.25)}.wizard-nav{align-items:center;gap:10px;margin-top:18px;display:flex}.wizard-done{text-align:center;padding:10px 0}.wizard-done p{margin:6px 0}.ceo-setup{align-items:center;gap:18px;display:flex}.ceo-portrait{flex:none;width:120px;height:120px}.swatch{border:2.5px solid var(--ink);cursor:pointer;border-radius:50%;width:24px;height:24px;padding:0}.swatch-on{box-shadow:0 0 0 3px #fff, 0 0 0 5px var(--ink)}.picker{flex-direction:column;gap:8px;display:flex}.picker-tabs{flex-wrap:wrap;gap:6px;display:flex}.picker-tab{font-family:var(--font);border:2.5px solid var(--ink);cursor:pointer;white-space:nowrap;background:#fff;border-radius:12px;flex:1;padding:8px 10px;font-size:.88rem;font-weight:600}.picker-tab-on{background:var(--accent);color:#fff;text-shadow:0 1px #00000040;box-shadow:0 3px 0 var(--ink)}.picker-mode{flex-direction:column;gap:6px;display:flex}.picker-mode .row{margin:0}.folder-list{max-height:240px}.chip-warn{background:#fff3bf}.tour{z-index:45;background:var(--paper);border:3px solid var(--ink);border-left:10px solid var(--accent);width:min(390px,100vw - 32px);box-shadow:0 5px 0 var(--ink);border-radius:18px;padding:10px 14px;animation:.25s ease-out pop;position:fixed;bottom:16px;left:16px}.tour-cheer{border-left-color:var(--good);background:#eafbee}.tour-head{align-items:center;display:flex}.tour-count{color:var(--muted);font-size:.78rem;font-weight:700}.tour-title{margin:2px 0 4px;font-size:1.1rem;font-weight:700}.tour-body{font-size:.9rem;line-height:1.5}.tour-nav{align-items:center;margin-top:8px;display:flex}.ceo-term{flex:none;height:340px}.job-brief{margin:0 0 8px}.job-brief summary{cursor:pointer;font-weight:600}.job-brief .md{margin-top:4px;line-height:1.45}.confirm-overlay{z-index:60;background:#15151f73;place-items:center;padding:16px;animation:.12s ease-out fadein;display:grid;position:fixed;inset:0}.confirm{background:var(--paper);border:4px solid var(--ink);width:min(440px,100%);box-shadow:0 8px 0 var(--ink);text-align:center;border-radius:24px;padding:20px 22px 14px;animation:.18s ease-out pop}.confirm-danger{border-top:12px solid var(--bad)}.confirm-warn{border-top:12px solid var(--warn)}.confirm-good{border-top:12px solid var(--good)}.confirm-icon{font-size:2.4rem;line-height:1}.confirm h3{margin:8px 0 6px;font-size:1.25rem}.confirm-body{color:#4a4e69;margin-bottom:4px;line-height:1.5}.confirm-actions{justify-content:center;gap:10px;margin-top:14px;display:flex}.confirm-keys{margin-top:10px}.btn-warn{background:#ffd166}.badge{background:var(--bad);color:#fff;border:2px solid var(--ink);border-radius:999px;place-items:center;min-width:20px;height:20px;padding:0 6px;font-size:.72rem;font-weight:700;line-height:1;display:inline-grid}.toasts{bottom:88px}.phone-btn{font-family:var(--font);color:var(--ink);background:var(--paper);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);cursor:pointer;border-radius:18px;align-items:center;gap:8px;padding:6px 14px 6px 8px;font-size:.9rem;font-weight:600;display:flex;position:absolute;bottom:16px;right:16px}.phone-btn:hover{transform:translateY(-1px)}.phone-btn-icon{font-size:1.7rem;line-height:1;display:inline-block}.phone-btn-ring .phone-btn-icon{animation:2.4s ease-in-out infinite ring}@keyframes ring{0%,72%,to{transform:rotate(0)}76%{transform:rotate(-16deg)}80%{transform:rotate(14deg)}84%{transform:rotate(-10deg)}88%{transform:rotate(8deg)}}.phone-btn-badge{position:absolute;top:-8px;left:26px}.phone-overlay{background:#15151f47;place-items:end;padding:16px 24px}.phone{border:4px solid var(--ink);width:min(400px,100%);height:min(760px,100vh - 32px);box-shadow:0 10px 0 var(--ink), inset 0 0 0 3px #3b3f5c;background:#23263a;border-radius:38px;flex-direction:column;padding:10px 10px 8px;animation:.22s ease-out slideup;display:flex}@keyframes slideup{0%{opacity:0;transform:translateY(40px)}}.phone-status{color:#e9e9f5;justify-content:space-between;align-items:center;padding:2px 16px 8px;font-size:.8rem;font-weight:600;display:flex}.phone-notch{background:#15151f;border-radius:999px;width:90px;height:18px}.phone-screen{background:var(--paper);border-radius:26px 26px 12px 12px;flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.phone-tabs{gap:4px;padding:8px 4px 2px;display:flex}.phone-tab{color:#b8bbd6;font-family:var(--font);cursor:pointer;background:0 0;border:none;border-radius:14px;flex-direction:column;flex:1;align-items:center;gap:2px;padding:6px 4px;font-size:.78rem;font-weight:600;display:flex}.phone-tab-on{color:#fff;background:#3b3f5c}.phone-tab-icon{font-size:1.3rem;position:relative}.badge-dot{min-width:18px;height:18px;font-size:.65rem;position:absolute;top:-6px;right:-14px}.phone-hint{text-align:center;color:#8e92b3;padding-top:4px;font-size:.72rem}.phone-hint kbd{font-size:.7rem}.phone-scroll{flex:1;padding:12px;overflow:auto}.phone-h{align-items:center;gap:6px;margin:10px 2px 8px;font-size:1rem;display:flex}.phone-h:first-child{margin-top:0}.phone-empty{padding:10px 6px;line-height:1.5}.linkish{color:#3a56d4;font-family:var(--font);cursor:pointer;text-align:left;background:0 0;border:none;padding:0;font-weight:600}.phone-chat{flex-direction:column;flex:1;min-height:0;display:flex}.chat-head{background:#fff;border-bottom:2px solid #1f1d2b14;align-items:center;gap:10px;padding:10px 12px;display:flex}.presence-busy{color:#d9480f;font-weight:600}.chat-log{background:#f4f1ea;flex-direction:column;flex:1;gap:8px;padding:10px;display:flex;overflow:auto}.bubble-row{display:flex}.bubble-row-me{justify-content:flex-end}.bubble{border:2px solid var(--ink);border-radius:16px;max-width:86%;padding:8px 11px 5px;font-size:.9rem;line-height:1.4}.bubble-them{background:#fff;border-bottom-left-radius:4px}.bubble-me{background:#ffd6a5;border-bottom-right-radius:4px}.bubble-from{color:var(--muted);font-size:.72rem;font-weight:700}.bubble-text{white-space:pre-wrap;overflow-wrap:anywhere}.bubble:has(.md pre,.md-table){width:86%}.md{overflow-wrap:anywhere;min-width:0}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p,.md ul,.md ol,.md pre,.md blockquote,.md .md-table{margin:0 0 .55em}.md ul,.md ol{padding-left:1.3em}.md li>ul,.md li>ol{margin:.15em 0 .2em}.md li+li{margin-top:.15em}.md li>p{margin:0}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.7em 0 .3em;font-size:1em;line-height:1.25}.md h1{font-size:1.15em}.md h2{font-size:1.08em}.md h5,.md h6{color:var(--muted)}.md a{color:#3a56d4;overflow-wrap:anywhere;font-weight:600}.md a:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:3px}.md-deadlink{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.md code{font-family:var(--mono);background:#1f1d2b14;border-radius:5px;padding:.1em .3em;font-size:.86em}.md pre,.md-table{contain:inline-size;overflow-x:auto}.md pre{background:#1f1d2b14;border-radius:8px;padding:7px 9px;line-height:1.35}.md pre code{white-space:pre;overflow-wrap:normal;background:0 0;padding:0}.md blockquote{color:#4a4e69;border-left:3px solid #1f1d2b40;padding:0 0 0 9px}.md hr{border:none;border-top:1.5px solid #1f1d2b26;margin:.6em 0}.md table{border-collapse:collapse;font-size:.9em}.md th,.md td{text-align:left;overflow-wrap:normal;border:1px solid #1f1d2b33;padding:3px 7px}.md th{background:#1f1d2b0f}.md td code{white-space:nowrap}.md input[type=checkbox]{vertical-align:middle;margin:0 4px 0 0}.md li:has(>input[type=checkbox]){margin-left:-1.1em;list-style:none}.bubble-time{text-align:right;color:var(--muted);margin-top:2px;font-size:.65rem}.bubble-office{text-align:center;color:#4a4e69;background:#1f1d2b12;border-radius:999px;align-self:center;max-width:92%;padding:3px 10px;font-size:.76rem}.bubble-typing{gap:4px;padding:10px 12px;display:flex}.bubble-typing span{background:var(--muted);border-radius:50%;width:7px;height:7px;animation:1.2s infinite typing}.bubble-typing span:nth-child(2){animation-delay:.15s}.bubble-typing span:nth-child(3){animation-delay:.3s}@keyframes typing{0%,60%,to{opacity:.5;transform:translateY(0)}30%{opacity:1;transform:translateY(-4px)}}.quick{background:#fff;gap:6px;padding:6px 10px 0;display:flex;overflow-x:auto}.quick-chip{font-family:var(--font);cursor:pointer;background:#fff;border:1.5px solid #1f1d2b4d;border-radius:999px;flex:none;padding:4px 10px;font-size:.75rem;font-weight:600}.quick-chip:hover{border-color:var(--accent)}.chat-input{background:#fff;align-items:flex-end;gap:6px;padding:8px 10px 10px;display:flex}.resume{border:3px solid var(--ink);color:var(--ink);background:#fff;border-radius:16px;flex-direction:column;gap:6px;margin-bottom:10px;padding:10px 12px;display:flex}.bubble .resume{border-width:2px;margin:6px 0 2px}.resume-letgo{background:#fff4f2}.resume-hot{box-shadow:0 0 0 4px var(--accent)}.resume-done{opacity:.75}.resume-head{align-items:center;gap:10px;display:flex}.resume-name{font-size:1.05rem;font-weight:700}.resume-title{color:var(--muted);font-size:.85rem;font-weight:600}.resume-meta{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.resume-reason{border-left:3px solid #1f1d2b33;padding-left:9px;font-size:.88rem;line-height:1.45}.resume-brief{background:#f6f3ec;border-radius:10px;margin:0;padding:8px;font-size:.82rem;line-height:1.45}.tiles{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.tile{border:2.5px solid var(--ink);text-align:center;background:#fff;border-radius:14px;padding:8px}.tile-value{white-space:nowrap;font-size:1.15rem;font-weight:700}.tile-label{color:var(--muted);font-size:.7rem;font-weight:600}.report{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.report li{background:#fff;border:2px solid #1f1d2b1f;border-radius:12px;align-items:flex-start;gap:8px;padding:7px 10px;font-size:.86rem;line-height:1.4;display:flex}.report li.report-good{background:#e7fbe9;border-color:#80ed99}.report li.report-warn{background:#fff4d6;border-color:#ffd166}.proj{border:2.5px solid var(--ink);border-left:10px solid var(--accent);background:#fff;border-radius:14px;margin-bottom:8px;padding:6px 10px 8px}.proj .row{margin:2px 0}.proj .floor-badge{width:30px;height:30px}.proj-name{text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.proj-sum{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.proj-stats{flex-wrap:wrap;gap:10px;font-weight:600;display:flex}.proj-ready{color:#1b873a}@media (width<=520px){.phone-overlay{place-items:center;padding:8px}.phone-btn-label{display:none}}