@netnodeag/kraftwerk 0.6.0 → 0.7.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.
- package/README.md +27 -2
- package/dist/cli/doctor.js +40 -3
- package/dist/cli/init.js +65 -15
- package/dist/cli/kraftwerk.js +9 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +3 -0
- package/dist/config.js +3 -2
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.js +10 -2
- package/dist/inspector/chat/backend.d.ts +6 -0
- package/dist/inspector/chat/pi.js +1 -0
- package/dist/inspector/chat/sessions.d.ts +4 -0
- package/dist/inspector/chat/sessions.js +105 -14
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/runner.d.ts +10 -0
- package/dist/inspector/runner.js +6 -8
- package/dist/inspector/runs.d.ts +2 -0
- package/dist/inspector/runs.js +17 -12
- package/dist/inspector/server.d.ts +2 -0
- package/dist/inspector/server.js +40 -2
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +7 -0
- package/dist/inspector/team.js +2 -0
- package/dist/runner/docker.d.ts +3 -1
- package/dist/runner/docker.js +5 -5
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +19 -0
- package/dist/yaml.js +2 -2
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
- package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BtcPa8nx.css +0 -1
- package/inspector/dist/assets/index-CImcmBVs.js +0 -10
|
@@ -4,16 +4,24 @@ import path from "node:path";
|
|
|
4
4
|
* Set once by startInspector() before any request is served.
|
|
5
5
|
*/
|
|
6
6
|
let outputDir = "";
|
|
7
|
+
let projectRoot = "";
|
|
7
8
|
export function setOutputDir(dir) {
|
|
8
9
|
outputDir = path.resolve(dir);
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
export function setProjectRoot(dir) {
|
|
12
|
+
projectRoot = path.resolve(dir);
|
|
13
|
+
}
|
|
14
|
+
/** Absolute output directory (runs/ + chats/ live inside). */
|
|
11
15
|
export function getOutputDir() {
|
|
12
16
|
if (!outputDir)
|
|
13
17
|
throw new Error("inspector context not initialized");
|
|
14
18
|
return outputDir;
|
|
15
19
|
}
|
|
16
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The consumer project root. Set explicitly at startup; falls back to the
|
|
22
|
+
* parent of the output dir (wrong for nested output dirs like
|
|
23
|
+
* kraftwerk-data/output — always pass projectRoot when known).
|
|
24
|
+
*/
|
|
17
25
|
export function getProjectRoot() {
|
|
18
|
-
return path.dirname(getOutputDir());
|
|
26
|
+
return projectRoot || path.dirname(getOutputDir());
|
|
19
27
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
3
|
+
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
4
|
+
* project, detached, with a pre-chosen --run-id so the browser can jump to
|
|
5
|
+
* /runs/<id> immediately and watch the (already polling) live timeline.
|
|
6
|
+
*
|
|
7
|
+
* Sandbox mode adds --sandbox: one Docker container per run, workflow
|
|
8
|
+
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
9
|
+
* kraftwerk/src/runner/docker.ts).
|
|
10
|
+
*/
|
|
1
11
|
export declare function dockerStatus(): {
|
|
2
12
|
available: boolean;
|
|
3
13
|
image: boolean;
|
package/dist/inspector/runner.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync, openSync, closeSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getProjectRoot } from "./context.js";
|
|
5
|
+
import { RUN_ID_RE, safeRunDir } from "./runs.js";
|
|
6
|
+
import { newRunId } from "../workflow.js";
|
|
5
7
|
/**
|
|
6
8
|
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
7
9
|
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
@@ -12,10 +14,6 @@ import { getProjectRoot } from "./context.js";
|
|
|
12
14
|
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
13
15
|
* kraftwerk/src/runner/docker.ts).
|
|
14
16
|
*/
|
|
15
|
-
function stamp(d = new Date()) {
|
|
16
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
17
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}-${p(d.getSeconds())}`;
|
|
18
|
-
}
|
|
19
17
|
export function dockerStatus() {
|
|
20
18
|
const daemon = spawnSync("docker", ["version", "--format", "ok"], {
|
|
21
19
|
stdio: "ignore",
|
|
@@ -37,9 +35,9 @@ export function triggerRun(opts) {
|
|
|
37
35
|
if (!docker.image)
|
|
38
36
|
throw new Error('Image "kraftwerk-runner" missing — run `kraftwerk runner build`.');
|
|
39
37
|
}
|
|
40
|
-
const runId =
|
|
41
|
-
// Matches the CLI's --run-id resolution: output/<id
|
|
42
|
-
const runDir =
|
|
38
|
+
const runId = newRunId(opts.workflowName);
|
|
39
|
+
// Matches the CLI's --run-id resolution: <output dir>/runs/<id>.
|
|
40
|
+
const runDir = safeRunDir(runId);
|
|
43
41
|
mkdirSync(runDir, { recursive: true });
|
|
44
42
|
const log = openSync(path.join(runDir, "trigger.log"), "a");
|
|
45
43
|
const args = ["kraftwerk", "run", "--yes", "--run-id", runId];
|
|
@@ -60,7 +58,7 @@ export function triggerRun(opts) {
|
|
|
60
58
|
}
|
|
61
59
|
export function stopRun(runId) {
|
|
62
60
|
// Sandboxed runs run in container kw-<runId>; docker stop ends them.
|
|
63
|
-
if (
|
|
61
|
+
if (!RUN_ID_RE.test(runId))
|
|
64
62
|
return false;
|
|
65
63
|
return (spawnSync("docker", ["stop", `kw-${runId}`], { stdio: "ignore", timeout: 30_000 }).status === 0);
|
|
66
64
|
}
|
package/dist/inspector/runs.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface RunDetail extends RunListItem {
|
|
|
49
49
|
files: FileView[];
|
|
50
50
|
}
|
|
51
51
|
export declare function listRuns(): Promise<RunListItem[]>;
|
|
52
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
53
|
+
export declare const RUN_ID_RE: RegExp;
|
|
52
54
|
export declare function safeRunDir(id: string): string;
|
|
53
55
|
export declare function getRun(id: string): Promise<RunDetail | null>;
|
|
54
56
|
export declare function readRunFile(id: string, name: string): Promise<{
|
package/dist/inspector/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { getOutputDir } from "./context.js";
|
|
4
4
|
/**
|
|
5
5
|
* Filesystem + trace.jsonl reading for the inspector. The output directory
|
|
6
|
-
* holds one folder per run (
|
|
6
|
+
* holds one folder per run (runs/<stamp>-<workflow>, legacy run-* at the root);
|
|
7
7
|
* trace.jsonl written by the framework plus the run's working files.
|
|
8
8
|
*/
|
|
9
9
|
/** A run with no trace update for this long and no summary counts as aborted. */
|
|
@@ -158,15 +158,14 @@ function shortenPath(p) {
|
|
|
158
158
|
return parts.length > 2 ? parts.slice(-2).join("/") : p;
|
|
159
159
|
}
|
|
160
160
|
export async function listRuns() {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
161
|
+
// New runs live under <output>/runs/; legacy run-* folders sit at the root.
|
|
162
|
+
const current = (await fs.readdir(path.join(getOutputDir(), "runs")).catch(() => []))
|
|
163
|
+
.filter((e) => RUN_ID_RE.test(e) && !e.startsWith("run-"));
|
|
164
|
+
const legacy = (await fs.readdir(getOutputDir()).catch(() => []))
|
|
165
|
+
.filter((e) => e.startsWith("run-") && RUN_ID_RE.test(e));
|
|
166
|
+
const entries = [...current, ...legacy];
|
|
168
167
|
const items = await Promise.all(entries.map(async (id) => {
|
|
169
|
-
const runDir =
|
|
168
|
+
const runDir = safeRunDir(id);
|
|
170
169
|
const st = await fs.stat(runDir).catch(() => null);
|
|
171
170
|
if (!st?.isDirectory())
|
|
172
171
|
return null;
|
|
@@ -188,12 +187,18 @@ export async function listRuns() {
|
|
|
188
187
|
costUsd: summary?.total?.costUsd,
|
|
189
188
|
};
|
|
190
189
|
}));
|
|
191
|
-
|
|
190
|
+
// Newest first; strip the legacy "run-" prefix so both id styles compare by stamp.
|
|
191
|
+
const stampOf = (r) => r.id.replace(/^run-/, "");
|
|
192
|
+
return items.filter(Boolean).sort((a, b) => stampOf(b).localeCompare(stampOf(a)));
|
|
192
193
|
}
|
|
194
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
195
|
+
export const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
193
196
|
export function safeRunDir(id) {
|
|
194
|
-
if (
|
|
197
|
+
if (!RUN_ID_RE.test(id))
|
|
195
198
|
throw new Error("invalid run id");
|
|
196
|
-
return
|
|
199
|
+
return id.startsWith("run-")
|
|
200
|
+
? path.join(getOutputDir(), id)
|
|
201
|
+
: path.join(getOutputDir(), "runs", id);
|
|
197
202
|
}
|
|
198
203
|
export async function getRun(id) {
|
|
199
204
|
const runDir = safeRunDir(id);
|
|
@@ -3,6 +3,8 @@ export interface InspectorOptions {
|
|
|
3
3
|
outputDir: string;
|
|
4
4
|
staticDir: string;
|
|
5
5
|
port: number;
|
|
6
|
+
/** Consumer project root; defaults to the parent of outputDir. */
|
|
7
|
+
projectRoot?: string;
|
|
6
8
|
}
|
|
7
9
|
/** Start the server; resolves once it listens. Runs until the process ends. */
|
|
8
10
|
export declare function startInspector(opts: InspectorOptions): Promise<http.Server>;
|
package/dist/inspector/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { setOutputDir, getOutputDir } from "./context.js";
|
|
4
|
+
import { setOutputDir, setProjectRoot, getOutputDir, getProjectRoot } from "./context.js";
|
|
5
|
+
import { resolveProject } from "../config.js";
|
|
5
6
|
import { listRuns, getRun, readRunFile } from "./runs.js";
|
|
6
7
|
import { listWorkflows, getWorkflow } from "./workflows.js";
|
|
7
8
|
import { dockerStatus, triggerRun, stopRun } from "./runner.js";
|
|
8
|
-
import { cancelChat, createChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
9
|
+
import { cancelChat, createChat, deleteChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
9
10
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
10
11
|
import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
|
|
12
|
+
import { listSkills } from "./skills.js";
|
|
11
13
|
import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
|
|
12
14
|
/**
|
|
13
15
|
* The inspector server: a plain node:http server with no dependencies.
|
|
@@ -53,9 +55,29 @@ function readBody(req) {
|
|
|
53
55
|
req.on("error", reject);
|
|
54
56
|
});
|
|
55
57
|
}
|
|
58
|
+
/** Package version, read once. Works from src/ (tsx) and dist/ alike. */
|
|
59
|
+
let pkgVersion;
|
|
60
|
+
async function getPkgVersion() {
|
|
61
|
+
if (pkgVersion === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await fs.readFile(new URL("../../package.json", import.meta.url), "utf8");
|
|
64
|
+
pkgVersion = JSON.parse(raw).version ?? "";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
pkgVersion = "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return pkgVersion;
|
|
71
|
+
}
|
|
56
72
|
async function handleApi(req, res, url) {
|
|
57
73
|
const seg = url.pathname.split("/").filter(Boolean); // ["api", ...]
|
|
58
74
|
const method = req.method ?? "GET";
|
|
75
|
+
// GET /api/meta — package version + project name ("environment") for the UI
|
|
76
|
+
if (seg.length === 2 && seg[1] === "meta" && method === "GET") {
|
|
77
|
+
const project = await resolveProject(getProjectRoot()).catch(() => null);
|
|
78
|
+
const projectName = project?.config.name ?? (project ? path.basename(project.root) : "");
|
|
79
|
+
return json(res, { version: await getPkgVersion(), projectName });
|
|
80
|
+
}
|
|
59
81
|
// GET /api/runs
|
|
60
82
|
if (seg.length === 2 && seg[1] === "runs" && method === "GET") {
|
|
61
83
|
return json(res, { outputDir: getOutputDir(), runs: await listRuns() });
|
|
@@ -227,6 +249,10 @@ async function handleApi(req, res, url) {
|
|
|
227
249
|
return json(res, { error: err.message }, 400);
|
|
228
250
|
}
|
|
229
251
|
}
|
|
252
|
+
// GET /api/skills — discovered skills (project + user .claude/skills)
|
|
253
|
+
if (seg.length === 2 && seg[1] === "skills" && method === "GET") {
|
|
254
|
+
return json(res, { skills: await listSkills() });
|
|
255
|
+
}
|
|
230
256
|
// GET/POST /api/team — member list / create a member
|
|
231
257
|
if (seg.length === 2 && seg[1] === "team") {
|
|
232
258
|
if (method === "GET") {
|
|
@@ -340,6 +366,16 @@ async function handleApi(req, res, url) {
|
|
|
340
366
|
}
|
|
341
367
|
}
|
|
342
368
|
}
|
|
369
|
+
// DELETE /api/chats/:id
|
|
370
|
+
if (seg.length === 3 && seg[1] === "chats" && method === "DELETE") {
|
|
371
|
+
try {
|
|
372
|
+
const result = await deleteChat(seg[2]);
|
|
373
|
+
return json(res, result, result.error ? 404 : 200);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return json(res, { error: "invalid chat id" }, 400);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
343
379
|
// GET /api/chats/:id
|
|
344
380
|
if (seg.length === 3 && seg[1] === "chats" && method === "GET") {
|
|
345
381
|
try {
|
|
@@ -438,6 +474,8 @@ async function serveStatic(res, staticDir, pathname) {
|
|
|
438
474
|
/** Start the server; resolves once it listens. Runs until the process ends. */
|
|
439
475
|
export function startInspector(opts) {
|
|
440
476
|
setOutputDir(opts.outputDir);
|
|
477
|
+
if (opts.projectRoot)
|
|
478
|
+
setProjectRoot(opts.projectRoot);
|
|
441
479
|
startRoutineScheduler();
|
|
442
480
|
const server = http.createServer(async (req, res) => {
|
|
443
481
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills: reusable instruction packages for chats, discovered from the
|
|
3
|
+
* Claude-style skill layout — one folder per skill holding a SKILL.md with
|
|
4
|
+
* YAML frontmatter (name, description). Two roots are scanned:
|
|
5
|
+
*
|
|
6
|
+
* <projectRoot>/.claude/skills/<name>/SKILL.md (git-tracked, per project)
|
|
7
|
+
* ~/.claude/skills/<name>/SKILL.md (personal, per user)
|
|
8
|
+
*
|
|
9
|
+
* A project skill shadows a user skill with the same name. Skills are
|
|
10
|
+
* surfaced to every chat as context (and expanded on /<name> invocation),
|
|
11
|
+
* so they work identically on claude, codex, and pi.
|
|
12
|
+
*/
|
|
13
|
+
export interface SkillInfo {
|
|
14
|
+
/** Frontmatter `name`, falling back to the folder name. */
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Absolute path to the SKILL.md file. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Absolute path to the skill folder (for harnesses that take a dir). */
|
|
20
|
+
dir: string;
|
|
21
|
+
source: "project" | "user";
|
|
22
|
+
}
|
|
23
|
+
/** All discovered skills; a project skill shadows a same-named user skill. */
|
|
24
|
+
export declare function listSkills(): Promise<SkillInfo[]>;
|
|
25
|
+
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
26
|
+
export declare function readSkill(skill: SkillInfo): Promise<string>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { getProjectRoot } from "./context.js";
|
|
6
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
7
|
+
function frontmatter(md) {
|
|
8
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(md);
|
|
9
|
+
if (!m)
|
|
10
|
+
return {};
|
|
11
|
+
try {
|
|
12
|
+
const parsed = parse(m[1]);
|
|
13
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function scanRoot(root, source) {
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
23
|
+
.filter((e) => e.isDirectory())
|
|
24
|
+
.map((e) => e.name);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const skills = await Promise.all(entries.map(async (dirName) => {
|
|
30
|
+
const dir = path.join(root, dirName);
|
|
31
|
+
const file = path.join(dir, "SKILL.md");
|
|
32
|
+
let md;
|
|
33
|
+
try {
|
|
34
|
+
md = await fs.readFile(file, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const fm = frontmatter(md);
|
|
40
|
+
const name = String(fm.name ?? dirName).trim() || dirName;
|
|
41
|
+
if (!NAME_RE.test(name))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
description: String(fm.description ?? "").trim(),
|
|
46
|
+
path: file,
|
|
47
|
+
dir,
|
|
48
|
+
source,
|
|
49
|
+
};
|
|
50
|
+
}));
|
|
51
|
+
return skills.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
/** All discovered skills; a project skill shadows a same-named user skill. */
|
|
54
|
+
export async function listSkills() {
|
|
55
|
+
const [project, user] = await Promise.all([
|
|
56
|
+
scanRoot(path.join(getProjectRoot(), ".claude", "skills"), "project"),
|
|
57
|
+
scanRoot(path.join(os.homedir(), ".claude", "skills"), "user"),
|
|
58
|
+
]);
|
|
59
|
+
const seen = new Set(project.map((s) => s.name.toLowerCase()));
|
|
60
|
+
return [...project, ...user.filter((s) => !seen.has(s.name.toLowerCase()))];
|
|
61
|
+
}
|
|
62
|
+
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
63
|
+
export function readSkill(skill) {
|
|
64
|
+
return fs.readFile(skill.path, "utf8").catch(() => "");
|
|
65
|
+
}
|
package/dist/inspector/team.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface TeamMember {
|
|
|
23
23
|
workflows: string[];
|
|
24
24
|
/** Knowledge bundles (OKF) this member works with. */
|
|
25
25
|
knowledge: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Skill allowlist (names from .claude/skills, project or user level).
|
|
28
|
+
* Absent = all discovered skills; empty list = no skills.
|
|
29
|
+
*/
|
|
30
|
+
skills?: string[];
|
|
26
31
|
}
|
|
27
32
|
export interface TeamMemberDetail extends TeamMember {
|
|
28
33
|
/** Contents of system.md — the member's system prompt. */
|
|
@@ -44,6 +49,8 @@ export interface SaveMemberInput {
|
|
|
44
49
|
effort?: string;
|
|
45
50
|
workflows?: string[];
|
|
46
51
|
knowledge?: string[];
|
|
52
|
+
/** Omit for "all skills"; a list (possibly empty) restricts to those names. */
|
|
53
|
+
skills?: string[];
|
|
47
54
|
system?: string;
|
|
48
55
|
}
|
|
49
56
|
export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
|
package/dist/inspector/team.js
CHANGED
|
@@ -50,6 +50,7 @@ function normalize(slug, raw) {
|
|
|
50
50
|
...(raw.effort ? { effort: String(raw.effort) } : {}),
|
|
51
51
|
workflows: Array.isArray(raw.workflows) ? raw.workflows.map(String) : [],
|
|
52
52
|
knowledge: Array.isArray(raw.knowledge) ? raw.knowledge.map(String) : [],
|
|
53
|
+
...(Array.isArray(raw.skills) ? { skills: raw.skills.map(String) } : {}),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
export async function listMembers() {
|
|
@@ -108,6 +109,7 @@ export async function saveMember(input) {
|
|
|
108
109
|
...(input.effort ? { effort: input.effort } : {}),
|
|
109
110
|
workflows: (input.workflows ?? []).map(String),
|
|
110
111
|
knowledge: (input.knowledge ?? []).map(String),
|
|
112
|
+
...(input.skills ? { skills: input.skills.map(String) } : {}),
|
|
111
113
|
};
|
|
112
114
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
113
115
|
await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
|
package/dist/runner/docker.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export interface SandboxOptions {
|
|
2
2
|
/** Consumer project root (where output/ lives). */
|
|
3
3
|
projectRoot: string;
|
|
4
|
+
/** Output directory; defaults to <projectRoot>/output (kraftwerk.yml `output:` may differ). */
|
|
5
|
+
outputDir?: string;
|
|
4
6
|
/** Workflow folder (folder mode) or .yml file path. */
|
|
5
7
|
workflowPath: string;
|
|
6
8
|
/** Workflow name as declared in the YAML (used for `kraftwerk run <name>`). */
|
|
7
9
|
workflowName: string;
|
|
8
10
|
request: string;
|
|
9
|
-
/** Pre-chosen run id
|
|
11
|
+
/** Pre-chosen run id; generated from workflow name + stamp when omitted. */
|
|
10
12
|
runId?: string;
|
|
11
13
|
/** Forward the host SSH agent + known_hosts into the container. */
|
|
12
14
|
ssh?: boolean;
|
package/dist/runner/docker.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { newRunId, runDirFor } from "../workflow.js";
|
|
8
8
|
/**
|
|
9
9
|
* Docker sandbox runner: one container per workflow run.
|
|
10
10
|
*
|
|
@@ -57,8 +57,8 @@ export async function runSandboxed(opts) {
|
|
|
57
57
|
if (!imageExists()) {
|
|
58
58
|
throw new Error(`Image "${IMAGE}" not found — run \`kraftwerk runner build\` first.`);
|
|
59
59
|
}
|
|
60
|
-
const runId = opts.runId ??
|
|
61
|
-
const runDir = path.join(opts.projectRoot, "output", runId);
|
|
60
|
+
const runId = opts.runId ?? newRunId(opts.workflowName);
|
|
61
|
+
const runDir = runDirFor(opts.outputDir ?? path.join(opts.projectRoot, "output"), runId);
|
|
62
62
|
await mkdir(runDir, { recursive: true });
|
|
63
63
|
const containerName = `kw-${runId}`;
|
|
64
64
|
// Folder mode and single-file mode both mount under src/workflows/ so
|
|
@@ -73,8 +73,8 @@ export async function runSandboxed(opts) {
|
|
|
73
73
|
"--memory", opts.memory ?? "2g",
|
|
74
74
|
"--cpus", opts.cpus ?? "2",
|
|
75
75
|
"-v", `${wfAbs}:${wfTarget}:ro`,
|
|
76
|
-
"-v", `${runDir}:/work/output/${runId}`,
|
|
77
|
-
"-e", `KRAFTWERK_RUN_DIR=/work/output/${runId}`,
|
|
76
|
+
"-v", `${runDir}:/work/output/runs/${runId}`,
|
|
77
|
+
"-e", `KRAFTWERK_RUN_DIR=/work/output/runs/${runId}`,
|
|
78
78
|
];
|
|
79
79
|
const envFile = path.join(opts.projectRoot, ENV_FILE);
|
|
80
80
|
if (existsSync(envFile))
|
package/dist/workflow.d.ts
CHANGED
|
@@ -22,3 +22,10 @@ export interface WorkflowDefinition {
|
|
|
22
22
|
}
|
|
23
23
|
/** Local-time run-folder stamp: "2026-08-13-1432-07" (sortable, no colons). */
|
|
24
24
|
export declare function runStamp(date?: Date): string;
|
|
25
|
+
/** Run-folder name: "<stamp>-<workflow-slug>", e.g. "2026-08-25-1432-07-hello". */
|
|
26
|
+
export declare function newRunId(workflowName: string, date?: Date): string;
|
|
27
|
+
/**
|
|
28
|
+
* Where a run's folder lives: <output>/runs/<id>. Legacy ids ("run-<stamp>",
|
|
29
|
+
* pre-runs-subfolder) stay at the output root so old runs remain readable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runDirFor(outputDir: string, runId: string): string;
|
package/dist/workflow.js
CHANGED
|
@@ -3,9 +3,28 @@
|
|
|
3
3
|
* A workflow owns its run directory, roster, prompts, and gates; the
|
|
4
4
|
* framework only provides the phase runner and the CLI dispatch.
|
|
5
5
|
*/
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
/** Local-time run-folder stamp: "2026-08-13-1432-07" (sortable, no colons). */
|
|
7
8
|
export function runStamp(date = new Date()) {
|
|
8
9
|
const pad = (n) => String(n).padStart(2, "0");
|
|
9
10
|
return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
10
11
|
`-${pad(date.getHours())}${pad(date.getMinutes())}-${pad(date.getSeconds())}`);
|
|
11
12
|
}
|
|
13
|
+
/** Run-folder name: "<stamp>-<workflow-slug>", e.g. "2026-08-25-1432-07-hello". */
|
|
14
|
+
export function newRunId(workflowName, date = new Date()) {
|
|
15
|
+
const slug = workflowName
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, 40) || "workflow";
|
|
20
|
+
return `${runStamp(date)}-${slug}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a run's folder lives: <output>/runs/<id>. Legacy ids ("run-<stamp>",
|
|
24
|
+
* pre-runs-subfolder) stay at the output root so old runs remain readable.
|
|
25
|
+
*/
|
|
26
|
+
export function runDirFor(outputDir, runId) {
|
|
27
|
+
return runId.startsWith("run-")
|
|
28
|
+
? path.join(outputDir, runId)
|
|
29
|
+
: path.join(outputDir, "runs", runId);
|
|
30
|
+
}
|
package/dist/yaml.js
CHANGED
|
@@ -6,7 +6,7 @@ import { envelopeContract } from "./envelope.js";
|
|
|
6
6
|
import { checkScript, containsText, fileNonEmpty, slotsFilled } from "./gates.js";
|
|
7
7
|
import { Run } from "./run.js";
|
|
8
8
|
import { summaryTable } from "./stats.js";
|
|
9
|
-
import {
|
|
9
|
+
import { newRunId } from "./workflow.js";
|
|
10
10
|
/**
|
|
11
11
|
* YAML-configured workflows (v1: a linear sequence of gated agent steps).
|
|
12
12
|
* Vocabulary borrows from GitHub Actions: `steps`, `runs-on` (the harness),
|
|
@@ -266,7 +266,7 @@ export async function loadWorkflow(givenPath) {
|
|
|
266
266
|
// the run directory upfront so it can mount/watch it by name.
|
|
267
267
|
const runDir = process.env.KRAFTWERK_RUN_DIR
|
|
268
268
|
? path.resolve(process.env.KRAFTWERK_RUN_DIR)
|
|
269
|
-
: path.resolve("output",
|
|
269
|
+
: path.resolve("output", "runs", newRunId(raw.name));
|
|
270
270
|
await mkdir(runDir, { recursive: true });
|
|
271
271
|
const run = new Run({
|
|
272
272
|
runDir,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/roboto-latin-400-normal-BqEyEoaF.woff2) format("woff2"),url(/assets/roboto-latin-400-normal-D27F5sTu.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/roboto-latin-500-normal-7RbcRiD8.woff2) format("woff2"),url(/assets/roboto-latin-500-normal-fsTFPL3E.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/roboto-latin-700-normal-BZpUvMxY.woff2) format("woff2"),url(/assets/roboto-latin-700-normal-YuyVweIx.woff) format("woff")}@font-face{font-family:Roboto Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/roboto-mono-latin-400-normal-GekRknry.woff2) format("woff2"),url(/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff) format("woff")}@font-face{font-family:Roboto Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2) format("woff2"),url(/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff) format("woff")}@font-face{font-family:Roboto Mono;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2) format("woff2"),url(/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff) format("woff")}:root{--font-sans: "Roboto";--font-mono: "Roboto Mono";--primary: #f2bd57;--on-primary: #422c00;--primary-container: #5f4100;--on-primary-container: #ffdea9;--secondary: #d9c4a0;--on-secondary: #3b2f15;--secondary-container: #53452a;--on-secondary-container: #f6e0bb;--tertiary: #b1cfa1;--tertiary-container: #354d2a;--on-tertiary-container: #cdebbb;--error: #ffb4ab;--on-error: #690005;--error-container: #93000a;--on-error-container: #ffdad6;--success: #8fd48c;--success-container: #12411a;--on-success-container: #aaf2a4;--surface: #16130e;--on-surface: #eae1d6;--on-surface-variant: #cfc5b4;--surface-container-lowest: #100d08;--surface-container-low: #1e1b14;--surface-container: #221f18;--surface-container-high: #2d2921;--surface-container-highest: #38332b;--outline: #99907f;--outline-variant: #4d4639;--inverse-surface: #eae1d6;--inverse-on-surface: #34302a;--elev-1: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 3px 1px rgba(0, 0, 0, .15);--elev-2: 0 1px 2px rgba(0, 0, 0, .3), 0 2px 6px 2px rgba(0, 0, 0, .15);--elev-3: 0 1px 3px rgba(0, 0, 0, .3), 0 4px 8px 3px rgba(0, 0, 0, .15);color-scheme:dark}:root[data-theme=light]{--primary: #7e5700;--on-primary: #ffffff;--primary-container: #ffdea9;--on-primary-container: #281900;--secondary: #6d5c3f;--on-secondary: #ffffff;--secondary-container: #f6e0bb;--on-secondary-container: #251a04;--tertiary: #4c6540;--tertiary-container: #cdebbb;--on-tertiary-container: #0a2005;--error: #ba1a1a;--on-error: #ffffff;--error-container: #ffdad6;--on-error-container: #410002;--success: #2e6b30;--success-container: #b0f1ab;--on-success-container: #002204;--surface: #fff8f2;--on-surface: #201b13;--on-surface-variant: #4e4639;--surface-container-lowest: #ffffff;--surface-container-low: #faf2e9;--surface-container: #f4ede3;--surface-container-high: #eee7dd;--surface-container-highest: #e9e1d8;--outline: #807663;--outline-variant: #d2c5b1;--inverse-surface: #35302a;--inverse-on-surface: #faf0e7;--elev-1: 0 1px 2px rgba(32, 27, 19, .18), 0 1px 3px 1px rgba(32, 27, 19, .08);--elev-2: 0 1px 2px rgba(32, 27, 19, .18), 0 2px 6px 2px rgba(32, 27, 19, .08);--elev-3: 0 1px 3px rgba(32, 27, 19, .18), 0 4px 8px 3px rgba(32, 27, 19, .08);color-scheme:light}*{box-sizing:border-box;margin:0;padding:0}html{height:100%}body{min-height:100%;background:var(--surface);color:var(--on-surface);font-family:var(--font-sans),system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:.01em;-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(--surface-container) 92%,transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--outline-variant)}.wordmark{display:flex;align-items:center;gap:10px}.wordmark .lamp-block{width:26px;height:26px;border-radius:9px;background:var(--primary);display:grid;place-items:center}.wordmark .lamp-block span{width:8px;height:8px;border-radius:50%;background:var(--on-primary)}.wordmark b{font-size:15px;font-weight:500;letter-spacing:0}.wordmark small{font-size:12px;color:var(--on-surface-variant)}.wordmark .env-name{font-size:12px;font-weight:500;letter-spacing:.01em;color:var(--on-surface-variant);padding:2px 9px;border-radius:999px;border:1px solid var(--outline-variant);white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}.topbar .spacer{flex:1}.topbar nav{display:flex;gap:4px;position:absolute;left:50%;transform:translate(-50%)}@media(max-width:1100px){.topbar nav{position:static;transform:none;margin-left:12px}}.topbar nav a{font-size:13px;font-weight:500;letter-spacing:.01em;text-transform:capitalize;color:var(--on-surface-variant);padding:6px 14px;border-radius:999px;transition:background .12s ease,color .12s ease}.topbar nav a:hover{color:var(--on-surface);background:color-mix(in srgb,var(--on-surface) 8%,transparent)}.topbar nav .nav-apart{margin-left:28px;position:relative}.topbar nav .nav-apart:before{content:"";position:absolute;left:-16px;top:50%;height:18px;width:1px;transform:translateY(-50%);background:var(--outline-variant)}.info-wrap{position:relative;display:inline-flex}.info-btn{display:grid;place-items:center;width:32px;height:32px;border-radius:50%;color:var(--on-surface-variant);transition:background .12s ease,color .12s ease}.info-btn:hover{background:color-mix(in srgb,var(--on-surface) 8%,transparent);color:var(--on-surface)}.info-pop{position:absolute;top:calc(100% + 8px);right:0;z-index:20;min-width:340px;max-width:min(480px,90vw);background:var(--surface-container-high);border-radius:12px;box-shadow:var(--elev-2);padding:8px 0}.info-row{display:flex;flex-direction:column;gap:2px;padding:8px 16px}.info-row+.info-row{border-top:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.info-v{font-size:13px;color:var(--on-surface)}.info-path{font-size:11px;color:var(--on-surface-variant);overflow-wrap:break-word;word-break:break-all}.info-actions{flex-direction:row;align-items:center;justify-content:space-between}.info-actions .theme-btn{flex:none}.theme-btn{border:1px solid var(--outline);border-radius:999px;padding:6px 14px;font-size:12.5px;font-weight:500;color:var(--on-surface-variant);transition:background .12s ease,color .12s ease}.theme-btn:hover{background:color-mix(in srgb,var(--primary) 10%,transparent);color:var(--on-surface)}.shell{max-width:1240px;margin:0 auto;padding:26px 20px 80px}.microlabel{font-size:11px;font-weight:500;letter-spacing:.045em;text-transform:capitalize;color:var(--on-surface-variant)}.num{font-variant-numeric:tabular-nums}.lamp{width:9px;height:9px;border-radius:50%;flex:none;position:relative}.lamp.ok{background:var(--success)}.lamp.failed,.lamp.blocked{background:var(--error)}.lamp.aborted{background:var(--outline-variant)}.lamp.pending{background:transparent;border:1.5px solid var(--outline)}.lamp.running{background:var(--primary);animation:lamp-pulse 1.4s ease-in-out infinite}@keyframes lamp-pulse{0%,to{box-shadow:0 0 color-mix(in srgb,var(--primary) 45%,transparent);opacity:1}50%{box-shadow:0 0 0 5px color-mix(in srgb,var(--primary) 18%,transparent);opacity:.8}}.status-word{font-size:12px;font-weight:500;letter-spacing:.03em;text-transform:capitalize}.status-word.ok{color:var(--success)}.status-word.failed,.status-word.blocked{color:var(--error)}.status-word.running{color:var(--primary)}.status-word.aborted,.status-word.pending{color:var(--on-surface-variant)}.page-head{display:flex;align-items:baseline;gap:14px;margin:6px 2px 20px}.page-head h1{font-size:24px;font-weight:400;letter-spacing:0}.page-head .count{color:var(--on-surface-variant);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(--surface-container-low);border:0;border-radius:12px;padding:14px 20px;box-shadow:var(--elev-1);transition:box-shadow .15s ease,background .15s ease}.run-card:hover{background:color-mix(in srgb,var(--on-surface) 5%,var(--surface-container-low));box-shadow:var(--elev-2)}.run-card .rid{font-family:var(--font-mono),monospace;font-size:12.5px;color:var(--on-surface-variant)}.run-card .wf{font-weight:500;font-size:14px}.run-card .req{color:var(--on-surface-variant);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(--on-surface-variant)}.progress-track{width:90px;height:4px;border-radius:999px;background:var(--surface-container-highest);overflow:hidden}.progress-track i{display:block;height:100%;background:var(--primary);border-radius:999px}.run-card.is-ok .progress-track i{background:var(--success)}.run-card.is-failed .progress-track i{background:var(--error)}.empty{border:1px dashed var(--outline-variant);border-radius:16px;padding:40px;text-align:center;color:var(--on-surface-variant)}.crumbs{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--on-surface-variant);margin-bottom:14px}.crumbs a:hover{color:var(--primary)}.crumbs .sep{color:var(--outline-variant)}.detail-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:6px}.detail-head h1{font-size:22px;font-weight:400}.detail-head .rid{font-family:var(--font-mono),monospace;font-size:12px;color:var(--on-surface-variant)}.detail-req{color:var(--on-surface-variant);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:500}.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(--surface-container-low);border:0;border-radius:16px;box-shadow:var(--elev-1);overflow:hidden}.panel-head{display:flex;align-items:center;gap:10px;padding:12px 18px;border-bottom:1px solid var(--outline-variant);background:var(--surface-container)}.panel-head .microlabel{color:var(--on-surface-variant);font-size:12px}.panel-head .spacer{flex:1}.phases{padding:6px 0}.phase-row{position:relative;padding:10px 18px 10px 42px}.phase-row+.phase-row{border-top:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.phase-row .lamp{position:absolute;left:19px;top:16px}.phase-row:before{content:"";position:absolute;left:23px;top:0;bottom:0;width:1px;background:var(--outline-variant)}.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:500;font-size:14px}.chip{font-family:var(--font-mono),monospace;font-size:11px;border:1px solid var(--outline-variant);border-radius:8px;padding:2px 10px;color:var(--on-surface-variant)}.chip.agent{border-color:transparent;background:var(--secondary-container);color:var(--on-secondary-container)}.phase-top .right{margin-left:auto;display:flex;gap:14px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--on-surface-variant)}.phase-sub{margin-top:3px;font-size:12.5px;color:var(--on-surface-variant);max-width:72ch}.phase-activity{margin-top:3px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--primary)}.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:11px;border-radius:8px;padding:2px 10px;background:var(--success-container);color:var(--on-success-container)}.gate.failed{background:var(--error-container);color:var(--on-error-container)}.gate-fail-msg{margin-top:4px;font-size:12px;color:var(--error)}.file-list{max-height:300px;overflow:auto}.file-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:8px 18px;font-family:var(--font-mono),monospace;font-size:12.5px;border-bottom:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent);color:var(--on-surface-variant);transition:background .1s ease}.file-row:last-child{border-bottom:0}.file-row:hover{background:color-mix(in srgb,var(--on-surface) 6%,transparent);color:var(--on-surface)}.file-row.active{background:var(--secondary-container);color:var(--on-secondary-container)}.file-row .fname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-row .fsize{color:var(--outline);font-size:11px}.viewer{border-top:1px solid var(--outline-variant)}.viewer-body pre{padding:14px 18px;overflow:auto;max-height:560px;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:var(--on-surface-variant)}.viewer-body iframe{width:100%;height:620px;border:0;background:#fff;display:block}.viewer-body img{max-width:100%;display:block;padding:14px 18px}.viewer-note{padding:6px 18px;font-size:11px;color:var(--on-surface-variant);border-bottom:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.open-raw{font-size:12px;font-weight:500;color:var(--primary);border:1px solid var(--outline);border-radius:999px;padding:3px 12px;transition:background .12s ease}.open-raw:hover{background:color-mix(in srgb,var(--primary) 10%,transparent)}.live-note{display:inline-flex;align-items:center;gap:7px;font-size:11px;color:var(--on-surface-variant)}.wf-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.wf-card{display:flex;flex-direction:column;gap:6px;background:var(--surface-container-low);border:0;border-radius:16px;padding:18px 20px;box-shadow:var(--elev-1);transition:box-shadow .15s ease,background .15s ease}.wf-card:hover{background:color-mix(in srgb,var(--on-surface) 5%,var(--surface-container-low));box-shadow:var(--elev-2)}.wf-card-head{display:flex;align-items:baseline;gap:10px}.wf-name{font-weight:500;font-size:16px}.wf-desc{color:var(--on-surface-variant);font-size:13px;flex:1}.wf-meta{display:flex;gap:8px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--on-surface-variant)}.wf-workspace{font-size:11.5px;color:var(--on-surface-variant);white-space:pre-wrap}.aid-0{--aid: var(--primary)}.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 color-mix(in srgb,var(--outline-variant) 55%,transparent);border-bottom:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.agent-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.agent-head b{font-size:14px;font-weight:500}.agent-head .spacer{flex:1}.agent-id{font-size:11px;color:var(--on-surface-variant)}.agent-dot{width:10px;height:10px;border-radius:50%;background:var(--aid)}.agent-tools{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.tool-chip{font-family:var(--font-mono),monospace;font-size:11px;border:1px solid var(--outline-variant);border-radius:8px;padding:1px 8px;color:var(--on-surface-variant)}.agent-persona{margin-top:8px;font-size:12px;color:var(--on-surface-variant);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 color-mix(in srgb,var(--outline-variant) 55%,transparent)}.step-node:before{content:"";position:absolute;left:31px;top:0;bottom:0;width:1px;background:var(--outline-variant)}.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:26px;height:26px;border-radius:50%;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) 18%,var(--surface-container-low));border:1px solid color-mix(in srgb,var(--aid) 55%,var(--outline-variant));color:var(--aid)}.step-index.is-script{--aid: var(--on-surface-variant)}.step-body{flex:1;min-width:0}.agent-chip{border-color:color-mix(in srgb,var(--aid) 50%,var(--outline-variant));color:var(--aid);background:transparent}.step-source{margin-top:6px}.step-source summary{cursor:pointer;font-size:12px;font-weight:500;color:var(--on-surface-variant);-webkit-user-select:none;user-select:none}.step-source summary:hover{color:var(--primary)}.step-source pre{margin-top:6px;padding:10px 12px;border:0;border-radius:12px;background:var(--surface-container-high);font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:var(--on-surface-variant);max-height:360px;overflow:auto}.gate.neutral{background:var(--surface-container-high);color:var(--on-surface-variant);border:0}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--outline-variant);border-radius:999px;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(--on-surface);background:var(--surface-container-high);border:1px solid transparent;border-radius:999px;padding:10px 18px;outline:none;transition:border-color .12s ease,background .12s ease}.run-form input[type=text]:focus{border-color:var(--primary);background:var(--surface-container-highest)}.run-btn{font:500 13.5px/1 var(--font-sans),sans-serif;white-space:nowrap;color:var(--on-primary);background:var(--primary);border-radius:999px;padding:11px 24px;transition:box-shadow .12s ease,filter .12s ease}.run-btn:hover{box-shadow:var(--elev-1);filter:brightness(1.06)}.run-btn:disabled{opacity:.38;cursor:not-allowed;filter:none;box-shadow:none}.run-opts{display:flex;flex-wrap:wrap;gap:6px 22px;margin-top:12px}.run-opts label{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--on-surface-variant);cursor:pointer}.run-opts input[type=checkbox]{accent-color:var(--primary);width:15px;height:15px}.run-opts .opt-hint{color:var(--outline)}.sandbox-chip{border:1px solid var(--outline-variant);color:var(--on-surface-variant)}.stop-btn{font:500 13px/1 var(--font-sans),sans-serif;color:var(--error);background:transparent;border:1px solid var(--outline);border-radius:999px;padding:8px 18px;transition:background .12s ease,border-color .12s ease}.stop-btn:hover{background:color-mix(in srgb,var(--error) 10%,transparent);border-color:var(--error)}.stop-btn:disabled{opacity:.38;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:47px;height:calc(100vh - 47px);display:flex;flex-direction:column;border-right:1px solid var(--outline-variant);background:var(--surface-container-low)}.side-head{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--outline-variant);background:var(--surface-container)}.side-head .spacer{flex:1}.side-list{flex:1;overflow-y:auto;padding:6px}.side-row{display:flex;align-items:flex-start;gap:9px;padding:9px 12px;border-radius:12px;margin-bottom:2px;transition:background .1s ease}.side-row .lamp{margin-top:4px}.side-row:hover{background:color-mix(in srgb,var(--on-surface) 6%,transparent)}.side-row.active{background:var(--secondary-container)}.side-row.active .side-wf{color:var(--on-secondary-container)}.side-row-body{flex:1;min-width:0}.side-row-top{display:flex;align-items:baseline;gap:8px}.side-wf{font-weight:500;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(--on-surface-variant)}.side-row-sub{display:flex;align-items:baseline;gap:8px;margin-top:1px}.side-row-sub .side-when{font-size:9px;opacity:.8}.side-req{flex:1;font-size:11.5px;color:var(--on-surface-variant);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.side-row.active .side-req{color:var(--on-secondary-container)}.side-meta{font-family:var(--font-mono),monospace;font-size:10.5px;color:var(--on-surface-variant)}.runs-main{padding:18px 22px 40px;min-width:0}.runs-main .detail-head .spacer{flex:1}.tabs{display:flex;gap:0;background:transparent;border:1px solid var(--outline);border-radius:999px;padding:0;overflow:hidden}.tabs button{font-size:12.5px;font-weight:500;letter-spacing:.01em;text-transform:capitalize;color:var(--on-surface-variant);padding:7px 18px;border-radius:0;transition:background .12s ease,color .12s ease}.tabs button+button{border-left:1px solid var(--outline)}.tabs button:hover{background:color-mix(in srgb,var(--on-surface) 6%,transparent);color:var(--on-surface)}.tabs button.active{background:var(--secondary-container);color:var(--on-secondary-container);box-shadow:none}.run-tab{max-width:1080px}.art-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;height:calc(100vh - 172px);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}}.chat-main{display:flex;flex-direction:column;height:calc(100vh - 47px);padding:16px 20px}.chat-thread{display:flex;flex-direction:column;flex:1;min-height:0;max-width:980px;width:100%;margin:0 auto}.chat-thread .detail-head{flex:none}.chat-thread .detail-head .rid{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:34ch}.chat-scroll{flex:1;min-height:0;overflow-y:auto;padding:14px 2px 20px;display:flex;flex-direction:column;gap:10px}.chat-scroll .empty{margin:auto;border:0}.msg{max-width:76ch;white-space:pre-wrap;word-break:break-word;font-size:14px;line-height:1.6}.msg.user{align-self:flex-end;background:var(--primary-container);color:var(--on-primary-container);border:0;border-radius:20px 20px 6px;padding:10px 16px}.msg.agent{align-self:flex-start;background:var(--surface-container);border:0;border-radius:20px 20px 20px 6px;padding:10px 16px}.msg.error{align-self:flex-start;color:var(--error);font-size:12.5px}.msg.thought{align-self:flex-start;max-width:76ch}.msg.thought summary{cursor:pointer;font-size:12px;font-weight:500;color:var(--on-surface-variant);-webkit-user-select:none;user-select:none}.msg.thought div{margin-top:5px;padding:8px 12px;border-left:2px solid var(--outline-variant);color:var(--on-surface-variant);font-size:12px;white-space:pre-wrap;word-break:break-word}.tool-card{align-self:flex-start;display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono),monospace;font-size:11.5px;color:var(--on-surface-variant);background:transparent;border:1px solid var(--outline-variant);border-radius:8px;padding:5px 12px;max-width:90ch}.tool-card .tool-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.perm-card{align-self:flex-start;border:0;background:var(--tertiary-container);color:var(--on-tertiary-container);border-radius:16px;padding:12px 16px;max-width:76ch;box-shadow:var(--elev-1)}.perm-card .perm-title{font-size:13.5px}.perm-card .perm-title .microlabel{color:var(--tertiary);margin-right:8px;font-weight:700}:root[data-theme=light] .perm-card .perm-title .microlabel{color:var(--on-tertiary-container)}.perm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.perm-btn{font:500 13px/1 var(--font-sans),sans-serif;border-radius:999px;padding:8px 18px;border:1px solid var(--outline);transition:background .12s ease,filter .12s ease}.perm-btn.allow{color:var(--on-primary);background:var(--primary);border-color:transparent}.perm-btn.allow:hover{filter:brightness(1.06);box-shadow:var(--elev-1)}.perm-btn.deny{color:inherit}.perm-btn.deny:hover{border-color:var(--error);color:var(--error)}.perm-resolved{margin-top:6px;font-size:12px;color:var(--on-surface-variant);font-family:var(--font-mono),monospace}.chat-working{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--on-surface-variant)}.composer{flex:none;display:flex;gap:10px;align-items:flex-end;padding:12px 0 4px;border-top:1px solid var(--outline-variant);position:relative}.skill-menu{position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;display:flex;flex-direction:column;overflow:hidden;background:var(--surface-container-high);border:1px solid var(--outline-variant);border-radius:14px;box-shadow:var(--elev-1);z-index:5}.skill-menu button{display:flex;align-items:baseline;gap:6px;text-align:left;padding:9px 14px;font-size:13px;background:transparent;border:0}.skill-menu button b{font-weight:500;font-family:var(--font-mono),monospace;font-size:12.5px}.skill-menu button span{color:var(--on-surface-variant);font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skill-menu button .skill-src{margin-left:auto;flex:none;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em}.skill-menu button:hover,.skill-menu button.active{background:color-mix(in srgb,var(--on-surface) 7%,transparent)}.composer textarea{flex:1;resize:none;font:14px/1.5 var(--font-sans),sans-serif;color:var(--on-surface);background:var(--surface-container-high);border:1px solid transparent;border-radius:24px;padding:12px 18px;outline:none;transition:border-color .12s ease,background .12s ease}.composer textarea:focus{border-color:var(--primary);background:var(--surface-container-highest)}.new-chat{max-width:640px;margin:0 auto;width:100%}.new-chat-panel{overflow:visible}.agent-pick{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:14px 16px}.agent-pick-btn{display:flex;flex-direction:column;gap:3px;text-align:left;border:1px solid var(--outline-variant);border-radius:12px;padding:12px 14px;background:transparent;transition:background .12s ease,border-color .12s ease}.agent-pick-btn b{font-size:13.5px;font-weight:500}.agent-pick-btn span{font-size:11.5px;color:var(--on-surface-variant)}.agent-pick-btn:hover{background:color-mix(in srgb,var(--on-surface) 5%,transparent)}.agent-pick-btn.active{border-color:transparent;background:var(--secondary-container);color:var(--on-secondary-container)}.agent-pick-btn.active span{color:var(--on-secondary-container)}@media(max-width:940px){.chat-main{height:auto;min-height:calc(100vh - 47px)}.agent-pick{grid-template-columns:1fr}}.know-intro{padding:12px 18px;font-size:13px;line-height:1.6;color:var(--on-surface-variant)}.know-intro code{font-size:11.5px}.know-newbundle{display:flex;gap:10px;padding:14px 18px}.know-newbundle input{flex:1;font:13px/1.4 var(--font-mono),monospace;color:var(--on-surface);background:var(--surface-container-high);border:1px solid transparent;border-radius:999px;padding:10px 18px;outline:none;transition:border-color .12s ease,background .12s ease}.know-newbundle input:focus{border-color:var(--primary);background:var(--surface-container-highest)}.know-table{width:100%;border-collapse:collapse;font-size:13px}.know-table td{padding:9px 18px;border-bottom:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent);vertical-align:baseline}.know-table tr:last-child td{border-bottom:none}.know-table a{color:var(--on-surface);text-decoration:none}.know-table a:hover b{color:var(--primary)}.know-id{font-family:var(--font-mono),monospace;font-size:11px;color:var(--on-surface-variant);margin-left:8px}.know-type{font-size:11.5px;color:var(--on-surface-variant);white-space:nowrap}.know-flags{text-align:right;white-space:nowrap}.chip.trust.unverified{color:var(--on-surface-variant)}.chip.trust.machine-confirmed{border-color:transparent;background:var(--secondary-container);color:var(--on-secondary-container)}.chip.trust.human-reviewed{border-color:transparent;color:var(--on-success-container);background:var(--success-container)}.chip.stale,.chip.deprecated{border-color:transparent;color:var(--on-error-container);background:var(--error-container)}.chip.draft{border-color:color-mix(in srgb,var(--primary) 45%,var(--outline-variant));color:var(--primary)}.know-sub{display:flex;align-items:center;gap:8px;margin:-8px 2px 14px}.know-sub .rid{font-family:var(--font-mono),monospace;font-size:12px;color:var(--on-surface-variant)}.know-meta{display:flex;flex-direction:column;gap:8px;padding:12px 18px;font-size:13px}.know-meta>div{display:flex;gap:14px;align-items:baseline}.know-meta .microlabel{width:90px;flex:none}.know-verify-entry{display:inline-block;margin-right:14px}.know-folder-row td{padding:10px 18px 4px;border-bottom:none}.know-folder{font-family:var(--font-mono),monospace;font-size:11px;color:var(--on-surface-variant);letter-spacing:.04em}.know-indent{padding-left:32px!important}.team-screen{grid-template-columns:240px minmax(0,1fr)}.team-screen.has-sessions{grid-template-columns:240px 260px minmax(0,1fr)}.team-emoji{font-size:17px;line-height:1.3;flex:none}.team-emoji-lg{font-size:34px;line-height:1}.member-view{display:flex;flex-direction:column;gap:16px}.member-view .detail-head{margin-bottom:0}.m3-list{display:flex;flex-direction:column;padding:4px 0}.m3-row{display:flex;align-items:center;gap:14px;padding:12px 18px;text-align:left;color:inherit}.m3-row+.m3-row,.m3-expand+.m3-row{border-top:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}button.m3-toggle{background:none;border:0;width:100%;font:inherit;cursor:pointer}button.m3-toggle:hover{background:color-mix(in srgb,var(--on-surface) 4%,transparent)}.m3-ico{flex:none;width:32px;height:32px;display:grid;place-items:center;font-size:14px;color:var(--on-secondary-container);background:var(--secondary-container);border-radius:50%}.m3-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.m3-head{font-size:13.5px;font-weight:500;display:flex;align-items:center;gap:8px}.m3-sub{font-size:12px;color:var(--on-surface-variant);line-height:1.5}.m3-sub code{font-size:11px}.m3-sub a{color:inherit}.m3-ellipsis{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.m3-chev{flex:none;color:var(--on-surface-variant);font-size:11px;transition:transform .14s ease}.m3-chev.open{transform:rotate(180deg)}.m3-expand{padding:2px 18px 16px 64px}.m3-expand pre{margin:0;white-space:pre-wrap;font:12.5px/1.65 var(--font-mono),monospace}.m3-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:3px}.m3-chips .chip{text-decoration:none}.m3-switch{position:relative;flex:none;display:inline-block;width:44px;height:24px;cursor:pointer}.m3-switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}.m3-switch-track{position:absolute;inset:0;border-radius:999px;pointer-events:none;background:var(--surface-container-highest);border:2px solid var(--outline-variant);transition:background .14s ease,border-color .14s ease}.m3-switch-track:after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--on-surface-variant);transition:transform .14s ease,background .14s ease}.m3-switch input:checked+.m3-switch-track{background:var(--primary);border-color:var(--primary)}.m3-switch input:checked+.m3-switch-track:after{transform:translate(20px);background:var(--on-primary)}a.m3-row{text-decoration:none}a.m3-row:hover{background:color-mix(in srgb,var(--on-surface) 4%,transparent)}a.m3-row:hover .m3-head{color:var(--primary)}.m3-subhead{padding:14px 18px 4px;font-family:var(--font-mono),monospace;font-size:11px;letter-spacing:.05em;color:var(--on-surface-variant)}.m3-row+.m3-subhead{border-top:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent);margin-top:4px}.know-log{padding:2px 0 10px}.know-log-row{display:flex;gap:10px;padding:4px 18px;align-items:baseline}.know-log-text{font-size:12.5px;color:var(--on-surface-variant);line-height:1.55;min-width:0;overflow-wrap:break-word}.know-log-text a{color:var(--primary)}.chip.log-kind{flex:none}.chip.log-verification{border-color:transparent;background:var(--success-container);color:var(--on-success-container)}.chip.log-creation,.chip.log-initialization{border-color:transparent;background:var(--secondary-container);color:var(--on-secondary-container)}.detail-cols{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:16px;align-items:start}@media(max-width:1020px){.detail-cols{grid-template-columns:1fr}}.detail-rail{display:flex;flex-direction:column;gap:16px;min-width:0}.rail-list{padding:4px 0}.rail-kv{display:flex;flex-direction:column;gap:3px;padding:10px 18px}.rail-kv+.rail-kv{border-top:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.rail-kv .v{font-size:12.5px;color:var(--on-surface);overflow-wrap:break-word;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.rail-kv .v.dim{color:var(--on-surface-variant)}.rail-kv a{color:var(--primary)}.concept-edit{display:block;width:100%;border:0;outline:none;resize:vertical;padding:14px 18px;background:var(--surface-container-lowest);color:var(--on-surface);font:12.5px/1.65 var(--font-mono),monospace;border-bottom:1px solid color-mix(in srgb,var(--outline-variant) 55%,transparent)}.side-row .row-x{flex:none;align-self:center;opacity:0;border:0;background:transparent;color:var(--on-surface-variant);font-size:11px;line-height:1;padding:4px 6px;border-radius:8px;cursor:pointer;transition:opacity .1s ease,background .1s ease,color .1s ease}.side-row:hover .row-x,.side-row .row-x:focus-visible{opacity:1}.side-row .row-x:hover{background:color-mix(in srgb,var(--error) 18%,transparent);color:var(--error)}.team-wf-list{display:flex;flex-wrap:wrap;gap:8px;padding:12px 18px}.team-wf-list .chip{text-decoration:none}.team-form{display:flex;flex-direction:column;gap:14px;padding:18px;max-width:680px}.team-form-row{display:flex;gap:10px;align-items:flex-end}.team-field{display:flex;flex-direction:column;gap:5px}.team-field,.team-field-label{font-size:11px;font-weight:500;color:var(--on-surface-variant);letter-spacing:.045em;text-transform:capitalize}.team-field input,.team-field select,.team-field textarea{font:13px/1.5 var(--font-mono),monospace;color:var(--on-surface);background:var(--surface-container-high);border:1px solid transparent;border-radius:12px;padding:9px 14px;outline:none;text-transform:none;letter-spacing:normal;transition:border-color .12s ease,background .12s ease}.team-field textarea{resize:vertical;font-family:var(--font-mono),monospace}.team-field input:focus,.team-field select:focus,.team-field textarea:focus{border-color:var(--primary);background:var(--surface-container-highest)}.emoji-pick{display:flex;gap:6px;flex-wrap:wrap}.emoji-pick button{font-size:16px;padding:6px 10px;background:transparent;border:1px solid var(--outline-variant);border-radius:12px;cursor:pointer;transition:background .12s ease,border-color .12s ease}.emoji-pick button:hover{background:color-mix(in srgb,var(--on-surface) 6%,transparent)}.emoji-pick button.active{border-color:transparent;background:var(--secondary-container)}.wf-checks{display:flex;flex-direction:column;gap:7px;padding:4px 0}.wf-checks label{display:flex;align-items:baseline;gap:8px;font-size:13px;color:var(--on-surface-variant);text-transform:none;letter-spacing:normal;cursor:pointer}.wf-checks input[type=checkbox]{accent-color:var(--primary)}.wf-checks b{font-family:var(--font-mono),monospace;font-size:12px}.wf-checks .opt-hint{color:var(--outline);font-size:11.5px}@media(max-width:900px){.team-screen,.team-screen.has-sessions{grid-template-columns:1fr}.team-form-row{flex-direction:column;align-items:stretch}}
|