software-defence-factory 0.4.3 → 0.4.5
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/.github/ISSUE_TEMPLATE/factory-task.yml +5 -2
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/bin/software-defence-factory.mjs +15 -24
- package/docs/quickstart.md +17 -2
- package/docs/recovery.md +9 -0
- package/docs/setup.md +17 -5
- package/factory/image-install.mjs +189 -0
- package/factory/installation-lock.mjs +15 -0
- package/factory/services.mjs +2 -2
- package/factory/supervisor.mjs +10 -2
- package/factory/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/factory/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/factory/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/factory/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/factory/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/factory/ui/assets/index-BWfaUr91.css +1 -0
- package/factory/ui/assets/index-CnV0vWSl.js +11 -0
- package/factory/ui/index.html +2 -2
- package/kit/README.md +4 -0
- package/kit/repository.md +86 -0
- package/package.json +1 -1
- package/scripts/export-kit.mjs +1 -1
- package/factory/ui/assets/index-BS9sWkVd.js +0 -10
- package/factory/ui/assets/index-DIgJUDQ1.css +0 -1
- package/factory/ui/assets/manrope-cyrillic-ext-wght-normal-C8S-KRRz.woff2 +0 -0
- package/factory/ui/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
- package/factory/ui/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
- package/factory/ui/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
- package/factory/ui/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
- package/factory/ui/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
name: Factory task
|
|
2
2
|
description: Describe a business outcome and observable acceptance criteria.
|
|
3
3
|
title: "[Factory] "
|
|
4
|
+
labels: ["factory:triage"]
|
|
4
5
|
body:
|
|
5
6
|
- type: textarea
|
|
6
7
|
id: need
|
|
@@ -20,7 +21,9 @@ body:
|
|
|
20
21
|
id: scope
|
|
21
22
|
attributes:
|
|
22
23
|
label: Scope, constraints and recovery
|
|
23
|
-
description: Affected repository,
|
|
24
|
+
description: Affected repository, allowed changes, non-goals, dependencies and rollback. Link blocking issues.
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
24
27
|
- type: dropdown
|
|
25
28
|
id: track
|
|
26
29
|
attributes:
|
|
@@ -32,4 +35,4 @@ body:
|
|
|
32
35
|
id: capabilities
|
|
33
36
|
attributes:
|
|
34
37
|
label: Required capabilities
|
|
35
|
-
description: For example browser,
|
|
38
|
+
description: For example browser, tests, platform tools or security review. Name any external operator proof needed. Never paste credentials or sensitive findings into a public issue.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The dashboard is adapted from the MIT-licensed interface at
|
|
6
6
|
https://github.com/owainlewis/machinist/tree/39435164faf1ff7fad49e41c38a7eb1a00538f21/internal/controlplane/web.
|
|
7
|
-
The
|
|
7
|
+
The dashboard retains adapted components from that source. Its project-focused layout, branding, styles and runtime integration are rebuilt for Factory.
|
|
8
8
|
The upstream execution engine is not bundled.
|
|
9
9
|
|
|
10
10
|
MIT License
|
|
@@ -31,9 +31,10 @@ SOFTWARE.
|
|
|
31
31
|
|
|
32
32
|
## Bundled UI dependencies
|
|
33
33
|
|
|
34
|
-
## @fontsource-variable/
|
|
34
|
+
## @fontsource-variable/geist 5.3.0
|
|
35
35
|
|
|
36
|
-
Copyright
|
|
36
|
+
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
|
|
37
|
+
Geist-Italic[wght].ttf: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
|
|
37
38
|
|
|
38
39
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
39
40
|
This license is copied below, and is also available with a FAQ at:
|
|
@@ -5,6 +5,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { createServer } from 'node:net';
|
|
7
7
|
import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers } from '../factory/lib.mjs';
|
|
8
|
+
import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
|
|
8
9
|
import { admitIncident } from '../factory/incident.mjs';
|
|
9
10
|
import { DEFAULT_DEMO_STATE } from '../factory/paths.mjs';
|
|
10
11
|
import { bootstrap, registerInstallation, VERSION } from '../factory/updates.mjs';
|
|
@@ -47,27 +48,16 @@ function init(repo, agent='codex', check='', port=7331) {
|
|
|
47
48
|
console.log(`Configured ${state}\nApp files were not changed. Only committed code is cloned into jobs.`);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function retainImage(reference) {
|
|
51
|
-
let image;
|
|
52
|
-
try { image=run('docker',['image','inspect','--format','{{.Id}}',reference]); }
|
|
53
|
-
catch(error) { if(/No such image|No such object/.test(error.message))return null;throw error; }
|
|
54
|
-
if(!/^sha256:[a-f0-9]{64}$/.test(image))throw new Error('Docker returned an unexpected image identity');
|
|
55
|
-
// Containerd can drop an untagged manifest when the shared build tag moves.
|
|
56
|
-
// Keep the exact object reachable; existing installations retain their pins.
|
|
57
|
-
run('docker',['tag',image,`software-defence-factory-retained:${image.slice(7)}`]);
|
|
58
|
-
return image;
|
|
59
|
-
}
|
|
60
51
|
async function install() {
|
|
61
52
|
const config=configAt(state);
|
|
62
|
-
if(existsSync(join(state,'supervisor.json'))&&alive(json(join(state,'supervisor.json')).pid))throw new Error('Stop the factory before installing or updating its runtime');
|
|
63
53
|
if (!['darwin','linux'].includes(process.platform)||!['arm64','x64'].includes(process.arch)) throw new Error('Use macOS/Linux arm64/amd64, or WSL2');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.log(
|
|
54
|
+
if(flags.image) {
|
|
55
|
+
const selected=installCustomJobImage(state,flags.image,{version:VERSION});
|
|
56
|
+
console.log(`Installed custom job image ${selected.image} from local reference ${selected.reference}. No image was downloaded or built; no model call made.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const installed=await installStandardJobImage(state,{version:VERSION});
|
|
60
|
+
console.log(`Installed standard job image ${installed.image}. No model call made.`);
|
|
71
61
|
}
|
|
72
62
|
async function portFree(port) {
|
|
73
63
|
await new Promise((ok,fail)=>{const server=createServer();server.once('error',fail);server.listen(port,'127.0.0.1',()=>server.close(ok));});
|
|
@@ -76,9 +66,9 @@ async function up() {
|
|
|
76
66
|
const config=configAt(state),lock=join(state,'supervisor.json');
|
|
77
67
|
registerInstallation(state);
|
|
78
68
|
if(existsSync(lock)) { if(alive(json(lock).pid))throw new Error('Supervisor already running; use status');rmSync(lock); }
|
|
79
|
-
|
|
69
|
+
assertInstalledJobImage(state,config);
|
|
80
70
|
if(process.getuid()===0)throw new Error('Run the controller as a dedicated unprivileged user with Docker access');
|
|
81
|
-
|
|
71
|
+
await portFree(config.port);
|
|
82
72
|
const fd=openSync(join(state,'supervisor.log'),'a',0o600);
|
|
83
73
|
const child=spawn(process.execPath,[join(ROOT,'factory/supervisor.mjs'),state],{detached:true,stdio:['ignore',fd,fd]});
|
|
84
74
|
closeSync(fd);child.unref();
|
|
@@ -139,8 +129,7 @@ try {
|
|
|
139
129
|
const launch=async()=>{
|
|
140
130
|
if(process.getuid()===0)throw new Error('Use a dedicated unprivileged operator account');
|
|
141
131
|
const lock=join(state,'supervisor.json');if(existsSync(lock)){if(alive(json(lock).pid))throw new Error('Supervisor already running');rmSync(lock);}
|
|
142
|
-
|
|
143
|
-
run('docker',['image','inspect',configAt(state).image]);
|
|
132
|
+
assertInstalledJobImage(state,configAt(state));
|
|
144
133
|
registerInstallation(state);await portFree(configAt(state).port);
|
|
145
134
|
const { supervise } = await import('../factory/supervisor.mjs');await supervise(state);
|
|
146
135
|
};
|
|
@@ -153,7 +142,9 @@ try {
|
|
|
153
142
|
else if(command==='tunnel')await manageService('tunnel',positional[0],state,flags);
|
|
154
143
|
else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
|
|
155
144
|
else if(command==='doctor') {
|
|
156
|
-
const config=configAt(state)
|
|
145
|
+
const config=configAt(state),dockerVersion=run('docker',['info','--format','{{.ServerVersion}}']),imageStatus=inspectImageInstallation(state,config);
|
|
146
|
+
console.log(JSON.stringify({node:process.version,docker:dockerVersion,engineInstalled:imageStatus.installed,image:imageStatus.image,repo:config.repo,agent:config.agent,checksConfigured:!!config.check?.trim(),inference:'Not called or verified',qualification:{model:'not assessed',toolchain:'not assessed'},dashboard:`http://127.0.0.1:${config.port}`},null,2));
|
|
147
|
+
if(!imageStatus.installed)process.exitCode=1;
|
|
157
148
|
} else if(command==='run') {
|
|
158
149
|
let spec;
|
|
159
150
|
if(flags.issue) {
|
|
@@ -195,7 +186,7 @@ try {
|
|
|
195
186
|
demo Install and run a synthetic sample (no model key)
|
|
196
187
|
qualify --state PATH Exercise recovery and isolation with a stopped demo job
|
|
197
188
|
init --repo PATH --agent codex|pi|custom --check "npm ci && npm test"
|
|
198
|
-
install
|
|
189
|
+
install [--image LOCAL_REF] Build the standard image, or select an existing local image
|
|
199
190
|
doctor | up | status | stop Inspect / operate your private installation
|
|
200
191
|
serve Foreground supervisor
|
|
201
192
|
service [print] Print a systemd user-service definition
|
package/docs/quickstart.md
CHANGED
|
@@ -61,8 +61,23 @@ Use `status`, `cancel JOB_ID`, `retry JOB_ID` and `stop`, always with the select
|
|
|
61
61
|
|
|
62
62
|
## Native application builds
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
Build a compatible application image on the worker, then select its existing
|
|
65
|
+
local tag through the CLI:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
software-defence-factory install --image LOCAL_IMAGE_REF --state /private/state/my-app
|
|
69
|
+
software-defence-factory doctor --state /private/state/my-app
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Selection resolves and retains the immutable image ID. It does not pull or build
|
|
73
|
+
the reference. An unavailable image, a running controller, an unreconciled job
|
|
74
|
+
or a remaining job container makes selection fail without changing the last
|
|
75
|
+
working configuration. Plain `install` remains the standard-image build path
|
|
76
|
+
and selects the standard image again. `doctor` verifies that the recorded image
|
|
77
|
+
metadata matches the image selected in private state; it reports model and
|
|
78
|
+
toolchain qualification separately, and does not perform either qualification.
|
|
79
|
+
|
|
80
|
+
In `factory.json`, `cpus` (1–32, default 2), `pidsLimit` (64–16384, default 256), `memoryMiB` and
|
|
66
81
|
`timeoutSeconds` bound the job's resources. Verification uses a separate
|
|
67
82
|
disk-backed checkout, keeping the candidate read-only. Its private scratch
|
|
68
83
|
directory is removed after container termination is confirmed. Interrupted
|
package/docs/recovery.md
CHANGED
|
@@ -90,3 +90,12 @@ and any contemporaneous private configuration backup. Record only corroborated
|
|
|
90
90
|
facts in a separate private incident note with evidence paths and unknowns; leave
|
|
91
91
|
the original queue/evidence unchanged. Without that evidence, model/image facts
|
|
92
92
|
cannot be reconstructed reliably.
|
|
93
|
+
|
|
94
|
+
### Interrupted image selection or controller startup
|
|
95
|
+
|
|
96
|
+
`installation.lock` serializes image changes with controller startup, including
|
|
97
|
+
managed boot/restarts. A live supervisor then prevents image changes while work
|
|
98
|
+
can run. If an operation is interrupted, preserve the lock and inspect its PID
|
|
99
|
+
and action. Remove it only after confirming that process and its image build or
|
|
100
|
+
startup have stopped, then run `doctor` and reconcile image metadata before
|
|
101
|
+
starting again. The CLI does not automatically clear an unknown lock.
|
package/docs/setup.md
CHANGED
|
@@ -104,6 +104,9 @@ anything. Verify the canonical GitHub origin and main branch's tracking target;
|
|
|
104
104
|
a fork may still track its upstream product. Move application sources into the
|
|
105
105
|
chosen workspace, not into the npm package or private runtime state.
|
|
106
106
|
|
|
107
|
+
Use [repository readiness](../kit/repository.md) for issue forms, labels, CI
|
|
108
|
+
policy and the explicit issue-to-job handoff. A ready label does not start a job.
|
|
109
|
+
|
|
107
110
|
Before admitting development work, establish:
|
|
108
111
|
|
|
109
112
|
- Reproducible toolchain/dependency pins and an actual build/check command.
|
|
@@ -123,11 +126,20 @@ software-defence-factory install --state /private/state/my-app
|
|
|
123
126
|
software-defence-factory doctor --state /private/state/my-app
|
|
124
127
|
```
|
|
125
128
|
|
|
126
|
-
Replace the agent/check/paths with the accepted application profile.
|
|
127
|
-
builds the standard image
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
Replace the agent/check/paths with the accepted application profile. Plain
|
|
130
|
+
`install` builds the standard image. To use an application-specific image, build
|
|
131
|
+
it on the worker first and select its existing local tag instead:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
software-defence-factory install --image LOCAL_IMAGE_REF --state /private/state/my-app
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This command checks the local Docker daemon, records and retains the exact image
|
|
138
|
+
ID, and does not download or build the selected image. Stop the installation and
|
|
139
|
+
reconcile every job before changing its image. Running plain `install` later
|
|
140
|
+
still rebuilds and selects the standard image. `init` does not edit the app,
|
|
141
|
+
install personal skills or submit a task. Runtime jobs receive the bundled
|
|
142
|
+
method and six skills automatically.
|
|
131
143
|
|
|
132
144
|
For a local model, verify the existing model service, intended model name and
|
|
133
145
|
its startup. Test the model API from a disposable container using the **selected
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync, readFileSync, readdirSync, writeFileSync, renameSync, rmSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { configAt, instanceLabel, json, PINS, ROOT, run, stream } from './lib.mjs';
|
|
7
|
+
import { acquireInstallationLock } from './installation-lock.mjs';
|
|
8
|
+
import { VERSION } from './updates.mjs';
|
|
9
|
+
|
|
10
|
+
const imageIdPattern = /^sha256:[a-f0-9]{64}$/;
|
|
11
|
+
const imageReferencePattern = /^[a-zA-Z0-9][a-zA-Z0-9_./:@-]*$/;
|
|
12
|
+
const missingImage = error => /no such (image|object)/i.test(error.message);
|
|
13
|
+
const alive = pid => {
|
|
14
|
+
try { process.kill(pid, 0); return true; }
|
|
15
|
+
catch (error) { if (error.code === 'ESRCH') return false; throw error; }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function docker(args, runner = run) { return runner('docker', args); }
|
|
19
|
+
|
|
20
|
+
export function inspectLocalImage(reference, runner = run) {
|
|
21
|
+
if (typeof reference !== 'string' || !imageReferencePattern.test(reference))
|
|
22
|
+
throw new Error('Invalid Docker image reference');
|
|
23
|
+
let id;
|
|
24
|
+
try { id = docker(['image', 'inspect', '--format', '{{.Id}}', reference], runner); }
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (missingImage(error)) return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (!imageIdPattern.test(id)) throw new Error('Docker returned an unexpected image identity');
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function retainImage(image, runner = run) {
|
|
34
|
+
if (!imageIdPattern.test(image || '')) throw new Error('Expected an immutable Docker image ID');
|
|
35
|
+
// The shared build tag can move. Keep every selected image reachable by ID.
|
|
36
|
+
docker(['image', 'tag', image, `software-defence-factory-retained:${image.slice(7)}`], runner);
|
|
37
|
+
return image;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function retainIfPresent(reference, runner) {
|
|
41
|
+
const image = inspectLocalImage(reference, runner);
|
|
42
|
+
if (image) retainImage(image, runner);
|
|
43
|
+
return image;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertImageChangeSafe(state, { runner = run, isAlive = alive } = {}) {
|
|
47
|
+
const supervisor = join(state, 'supervisor.json');
|
|
48
|
+
if (existsSync(supervisor)) {
|
|
49
|
+
let record;
|
|
50
|
+
try { record = json(supervisor); }
|
|
51
|
+
catch { throw new Error('Supervisor state is unreadable; reconcile the installation before changing its image'); }
|
|
52
|
+
if (!Number.isSafeInteger(record.pid) || record.pid < 1)
|
|
53
|
+
throw new Error('Supervisor state is unverified; reconcile the installation before changing its image');
|
|
54
|
+
if (isAlive(record.pid)) throw new Error('Stop the Factory before changing its job image');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const jobs = join(state, 'jobs');
|
|
58
|
+
if (existsSync(jobs)) {
|
|
59
|
+
for (const job of readdirSync(jobs)) {
|
|
60
|
+
if (existsSync(join(jobs, job, 'active.json')))
|
|
61
|
+
throw new Error(`Job ${job} has active or unreconciled execution; reconcile it before changing the image`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const containers = docker(['ps', '-aq', '--filter', `label=sdf.factory=${instanceLabel(state)}`], runner)
|
|
66
|
+
.split('\n').filter(Boolean);
|
|
67
|
+
if (containers.length) throw new Error('Factory job containers remain; stop and reconcile them before changing the image');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function persistInstallation(state, config, metadata, filesystem = undefined) {
|
|
71
|
+
const fs = filesystem || { readFileSync, writeFileSync, renameSync, rmSync };
|
|
72
|
+
const configPath = join(state, 'factory.json'), enginePath = join(state, 'engine.json');
|
|
73
|
+
const previousConfig = fs.readFileSync(configPath);
|
|
74
|
+
const token = `${process.pid}.${randomBytes(8).toString('hex')}`;
|
|
75
|
+
const configTemp = `${configPath}.${token}.next`, engineTemp = `${enginePath}.${token}.next`;
|
|
76
|
+
let configReplaced = false;
|
|
77
|
+
try {
|
|
78
|
+
fs.writeFileSync(configTemp, `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
79
|
+
fs.writeFileSync(engineTemp, `${JSON.stringify(metadata, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
80
|
+
fs.renameSync(configTemp, configPath);
|
|
81
|
+
configReplaced = true;
|
|
82
|
+
fs.renameSync(engineTemp, enginePath);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (configReplaced) {
|
|
85
|
+
const rollback = `${configPath}.${token}.rollback`;
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(rollback, previousConfig, { flag: 'wx', mode: 0o600 });
|
|
88
|
+
fs.renameSync(rollback, configPath);
|
|
89
|
+
} catch (rollbackError) {
|
|
90
|
+
throw new Error(`Image installation metadata could not be completed (${error.message}); restoring factory.json also failed (${rollbackError.message})`);
|
|
91
|
+
} finally { fs.rmSync(rollback, { force: true }); }
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
} finally {
|
|
95
|
+
fs.rmSync(configTemp, { force: true });
|
|
96
|
+
fs.rmSync(engineTemp, { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function metadataFor(image, source, reference, version) {
|
|
101
|
+
return {
|
|
102
|
+
runtime: 'native-node',
|
|
103
|
+
version,
|
|
104
|
+
image,
|
|
105
|
+
imageSource: source,
|
|
106
|
+
imageReference: reference,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function installCustomJobImage(state, reference, { runner = run, version = VERSION, isAlive = alive, filesystem } = {}) {
|
|
111
|
+
const release = acquireInstallationLock(state, 'select image');
|
|
112
|
+
try {
|
|
113
|
+
const config = configAt(state);
|
|
114
|
+
assertImageChangeSafe(state, { runner, isAlive });
|
|
115
|
+
const image = inspectLocalImage(reference, runner);
|
|
116
|
+
if (!image) throw new Error(`Docker image ${reference} is not present locally; build or pull it explicitly before selecting it`);
|
|
117
|
+
retainImage(image, runner);
|
|
118
|
+
const nextConfig = { ...config, image };
|
|
119
|
+
const metadata = metadataFor(image, 'custom', reference, version);
|
|
120
|
+
persistInstallation(state, nextConfig, metadata, filesystem);
|
|
121
|
+
return { image, reference, config: nextConfig, metadata };
|
|
122
|
+
} finally { release(); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function installStandardJobImage(state, { runner = run, build = stream, version = VERSION, isAlive = alive, filesystem } = {}) {
|
|
126
|
+
const release = acquireInstallationLock(state, 'build image');
|
|
127
|
+
try {
|
|
128
|
+
const config = configAt(state);
|
|
129
|
+
assertImageChangeSafe(state, { runner, isAlive });
|
|
130
|
+
docker(['info', '--format', '{{.ServerVersion}}'], runner);
|
|
131
|
+
retainIfPresent(config.image, runner);
|
|
132
|
+
retainIfPresent(PINS.jobImage, runner);
|
|
133
|
+
await build('docker', ['build', '-t', PINS.jobImage, join(ROOT, 'factory/image')]);
|
|
134
|
+
const image = inspectLocalImage(PINS.jobImage, runner);
|
|
135
|
+
if (!image) throw new Error('Built job image is unavailable');
|
|
136
|
+
retainImage(image, runner);
|
|
137
|
+
const nextConfig = { ...config, image };
|
|
138
|
+
const metadata = metadataFor(image, 'standard', PINS.jobImage, version);
|
|
139
|
+
persistInstallation(state, nextConfig, metadata, filesystem);
|
|
140
|
+
return { image, config: nextConfig, metadata };
|
|
141
|
+
} finally { release(); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readMetadata(state) {
|
|
145
|
+
const path = join(state, 'engine.json');
|
|
146
|
+
if (!existsSync(path)) return null;
|
|
147
|
+
try { return json(path); } catch { return null; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function inspectImageInstallation(state, config = configAt(state), runner = run) {
|
|
151
|
+
const metadata = readMetadata(state);
|
|
152
|
+
let image = null;
|
|
153
|
+
image = inspectLocalImage(config.image, runner);
|
|
154
|
+
|
|
155
|
+
const metadataValid = Boolean(metadata
|
|
156
|
+
&& metadata.runtime === 'native-node'
|
|
157
|
+
&& typeof metadata.version === 'string'
|
|
158
|
+
&& imageIdPattern.test(metadata.image || '')
|
|
159
|
+
&& (!metadata.imageSource || ['standard', 'custom'].includes(metadata.imageSource))
|
|
160
|
+
&& (!metadata.imageReference || imageReferencePattern.test(metadata.imageReference)));
|
|
161
|
+
const imageAvailable = image !== null;
|
|
162
|
+
const markerMatches = metadataValid && imageAvailable && metadata.image === image;
|
|
163
|
+
const installed = Boolean(markerMatches);
|
|
164
|
+
return {
|
|
165
|
+
installed,
|
|
166
|
+
image: {
|
|
167
|
+
reference: config.image,
|
|
168
|
+
id: image,
|
|
169
|
+
available: imageAvailable,
|
|
170
|
+
pinned: imageAvailable && config.image === image,
|
|
171
|
+
markerImage: metadataValid ? metadata.image : null,
|
|
172
|
+
source: metadataValid ? (metadata.imageSource || 'legacy') : null,
|
|
173
|
+
referenceAtInstall: metadataValid ? (metadata.imageReference || null) : null,
|
|
174
|
+
metadataValid,
|
|
175
|
+
markerMatches: Boolean(markerMatches),
|
|
176
|
+
error: imageAvailable ? null : `Selected image ${config.image} is not present locally`,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function assertInstalledJobImage(state, config = configAt(state), runner = run) {
|
|
182
|
+
const status = inspectImageInstallation(state, config, runner);
|
|
183
|
+
if (!status.installed) {
|
|
184
|
+
if (!status.image.available) throw new Error(`Selected job image ${config.image} is not present locally; run install with an available image`);
|
|
185
|
+
if (!status.image.metadataValid) throw new Error('Runtime image metadata is missing or invalid; run install');
|
|
186
|
+
throw new Error('Selected job image does not match the installed image metadata; run install');
|
|
187
|
+
}
|
|
188
|
+
return status;
|
|
189
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Per installation: managed boot bypasses the CLI's global service-operation
|
|
5
|
+
// lock. Hold this fence until image metadata is complete or the supervisor PID
|
|
6
|
+
// is visible, before any queued work can snapshot an execution profile.
|
|
7
|
+
export function acquireInstallationLock(state, action) {
|
|
8
|
+
const path = join(state, 'installation.lock');
|
|
9
|
+
try { writeFileSync(path, JSON.stringify({ pid: process.pid, action }), { flag: 'wx', mode: 0o600 }); }
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === 'EEXIST') throw new Error(`Another installation operation may own ${path}; reconcile its PID before removing the lock`);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
return () => rmSync(path);
|
|
15
|
+
}
|
package/factory/services.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { createServer } from 'node:net';
|
|
6
6
|
import { ROOT, STATE_HOME, DATA_HOME, SOURCE_CHECKOUT } from './paths.mjs';
|
|
7
7
|
import { configAt, json, save, run, api, sleep, digest } from './lib.mjs';
|
|
8
|
+
import { assertInstalledJobImage } from './image-install.mjs';
|
|
8
9
|
import { VERSION, newer, latestVersion, installRelease, busyInstallations } from './updates.mjs';
|
|
9
10
|
import { serviceId, systemdUnit, launchAgent, tunnelArguments, groupArguments, runtimeLauncher } from './service-files.mjs';
|
|
10
11
|
|
|
@@ -127,9 +128,8 @@ async function install(kind, state, flags) {
|
|
|
127
128
|
console.log(JSON.stringify(await serviceStatus(previous), null, 2)); return;
|
|
128
129
|
}
|
|
129
130
|
if (kind === 'controller') {
|
|
130
|
-
|
|
131
|
+
assertInstalledJobImage(state, configAt(state));
|
|
131
132
|
if (existsSync(join(state, 'supervisor.json')) && alive(json(join(state, 'supervisor.json')).pid)) throw new Error('Stop the manually started controller before adopting it as a service');
|
|
132
|
-
run('docker', ['image', 'inspect', configAt(state).image]);
|
|
133
133
|
}
|
|
134
134
|
let argv = kind === 'controller' ? [process.execPath, launcher(), 'serve', '--state', state] : tunnelArguments(flags.host, port);
|
|
135
135
|
if (flags.group) {
|
package/factory/supervisor.mjs
CHANGED
|
@@ -2,10 +2,18 @@ import { writeFileSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { configAt, stopContainers } from './lib.mjs';
|
|
5
|
+
import { acquireInstallationLock } from './installation-lock.mjs';
|
|
6
|
+
import { assertInstalledJobImage } from './image-install.mjs';
|
|
5
7
|
import { createController } from './server.mjs';
|
|
6
8
|
export async function supervise(state) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
+
const lock = join(state, 'supervisor.json');
|
|
10
|
+
const release = acquireInstallationLock(state, 'start controller');
|
|
11
|
+
let config;
|
|
12
|
+
try {
|
|
13
|
+
config = configAt(state);
|
|
14
|
+
assertInstalledJobImage(state, config);
|
|
15
|
+
writeFileSync(lock, JSON.stringify({ pid: process.pid }), { flag: 'wx', mode: 0o600 });
|
|
16
|
+
} finally { release(); }
|
|
9
17
|
let controller, stopping = false;
|
|
10
18
|
async function shutdown(code = 0) {
|
|
11
19
|
if (stopping) return; stopping = true;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Geist Variable", Geist, ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-sm:24rem;--container-2xl:42rem;--container-4xl:56rem;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wider:.05em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Geist Variable", Geist, ui-sans-serif, system-ui, sans-serif;--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:0}.z-20{z-index:20}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.-mb-px{margin-bottom:-1px}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-3{margin-left:calc(var(--spacing) * -3)}.ml-auto{margin-left:auto}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[60vh\]{max-height:60vh}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-32{min-height:calc(var(--spacing) * 32)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[1000px\]{max-width:1000px}.max-w-\[1500px\]{max-width:1500px}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.resize-y{resize:vertical}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[minmax\(8rem\,1fr\)_minmax\(8rem\,1fr\)_10rem_12rem\]{grid-template-columns:minmax(8rem,1fr) minmax(8rem,1fr) 10rem 12rem}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-7>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 7) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 7) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--border)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-border,.border-border\/70{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/70{border-color:color-mix(in oklab, var(--border) 70%, transparent)}}.border-danger\/25{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/25{border-color:color-mix(in oklab, var(--danger) 25%, transparent)}}.border-danger\/35{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/35{border-color:color-mix(in oklab, var(--danger) 35%, transparent)}}.border-danger\/40{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/40{border-color:color-mix(in oklab, var(--danger) 40%, transparent)}}.border-primary,.border-primary\/25{border-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.border-primary\/25{border-color:color-mix(in oklab, var(--primary) 25%, transparent)}}.border-success\/25{border-color:var(--success)}@supports (color:color-mix(in lab, red, red)){.border-success\/25{border-color:color-mix(in oklab, var(--success) 25%, transparent)}}.border-transparent{border-color:#0000}.border-warning\/25{border-color:var(--warning)}@supports (color:color-mix(in lab, red, red)){.border-warning\/25{border-color:color-mix(in oklab, var(--warning) 25%, transparent)}}.bg-background{background-color:var(--background)}.bg-current{background-color:currentColor}.bg-danger\/5{background-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.bg-danger\/5{background-color:color-mix(in oklab, var(--danger) 5%, transparent)}}.bg-danger\/10{background-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.bg-danger\/10{background-color:color-mix(in oklab, var(--danger) 10%, transparent)}}.bg-muted,.bg-muted\/20{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/20{background-color:color-mix(in oklab, var(--muted) 20%, transparent)}}.bg-muted\/35{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/35{background-color:color-mix(in oklab, var(--muted) 35%, transparent)}}.bg-muted\/40{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.bg-primary,.bg-primary\/5{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--primary) 5%, transparent)}}.bg-sidebar{background-color:var(--sidebar)}.bg-success\/10{background-color:var(--success)}@supports (color:color-mix(in lab, red, red)){.bg-success\/10{background-color:color-mix(in oklab, var(--success) 10%, transparent)}}.bg-surface{background-color:var(--surface)}.bg-transparent{background-color:#0000}.bg-warning\/10{background-color:var(--warning)}@supports (color:color-mix(in lab, red, red)){.bg-warning\/10{background-color:color-mix(in oklab, var(--warning) 10%, transparent)}}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.p-12{padding:calc(var(--spacing) * 12)}.px-0{padding-inline:0}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-14{padding-block:calc(var(--spacing) * 14)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pl-4{padding-left:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:Geist Variable,Geist,ui-sans-serif,system-ui,sans-serif}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:.875rem;line-height:var(--tw-leading,1.25rem)}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:.75rem;line-height:var(--tw-leading,1rem)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-danger{color:var(--danger)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-success{color:var(--success)}.text-warning{color:var(--warning)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--primary) 90%, transparent)}}.hover\:text-danger:hover{color:var(--danger)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:decoration-2:hover{text-decoration-thickness:2px}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-ring\/40:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/40:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 40%, transparent)}}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-ring:focus-visible{outline-color:var(--ring)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=40rem){.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:w-40{width:calc(var(--spacing) * 40)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(8rem\,1fr\)_minmax\(8rem\,1fr\)_10rem_12rem\]{grid-template-columns:minmax(8rem,1fr) minmax(8rem,1fr) 10rem 12rem}.sm\:grid-cols-\[minmax\(12rem\,1fr\)_minmax\(12rem\,1fr\)_10rem\]{grid-template-columns:minmax(12rem,1fr) minmax(12rem,1fr) 10rem}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:text-right{text-align:right}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-sm{font-size:.875rem;line-height:var(--tw-leading,1.25rem)}}@media (width>=48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-screen{height:100vh}.md\:w-\[203px\]{width:203px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:items-stretch{align-items:stretch}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:px-4{padding-inline:calc(var(--spacing) * 4)}.md\:py-5{padding-block:calc(var(--spacing) * 5)}}@media (width>=64rem){.lg\:grid-cols-\[minmax\(0\,1\.4fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr)}.lg\:p-8{padding:calc(var(--spacing) * 8)}}@media (width>=80rem){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--background:#fff;--foreground:#0a0a0a;--surface:#fff;--sidebar:#fff;--muted:#f4f4f5;--muted-foreground:#737373;--border:#e5e5e5;--primary:#0a0a0a;--primary-foreground:#fff;--success:#15803d;--warning:#a16207;--danger:#b91c1c;--ring:#525252}.dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--background:#09090b;--foreground:#f5f5f5;--surface:#0f0f10;--sidebar:#09090b;--muted:#18181b;--muted-foreground:#a1a1aa;--border:#27272a;--primary:#f5f5f5;--primary-foreground:#09090b;--success:#4ade80;--warning:#fbbf24;--danger:#f87171;--ring:#a1a1aa}*{border-color:var(--border)}html{background:var(--background)}body{background:var(--background);min-width:320px;min-height:100vh;color:var(--foreground);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;margin:0;font-size:14px;line-height:1.5}button,input,textarea,select{font:inherit}button,a,summary,select{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:not-allowed}::selection{background:var(--foreground)}@supports (color:color-mix(in lab, red, red)){::selection{background:color-mix(in srgb, var(--foreground) 16%, transparent)}}:focus-visible{outline:2px solid var(--ring);outline-offset:2px}.app-shell{min-height:100vh}.app-sidebar{background:var(--sidebar);border-right:1px solid var(--border);flex-direction:column;align-items:stretch;width:203px;height:100vh;padding:20px 14px 14px;display:flex;position:sticky;top:0}.brand-lockup{gap:9px;height:36px;padding:0 4px}.brand-wordmark{letter-spacing:-.025em;font-size:15px;font-weight:600}.factory-mark{width:30px;height:28px;color:var(--foreground);overflow:visible}.factory-mark path{fill:none;stroke-width:5px;stroke:currentColor;stroke-linecap:square;stroke-linejoin:round}.factory-mark .factory-mark-piece-a,.factory-mark .factory-mark-piece-b{stroke:currentColor}.desktop-nav{gap:2px;margin-top:30px;display:grid}.mobile-nav{display:none}.nav-item{height:36px;color:var(--muted-foreground);border-radius:6px;align-items:center;gap:10px;padding:0 10px;font-size:13px;line-height:20px;text-decoration:none;transition:background-color .15s,color .15s;display:flex;position:relative}.nav-item:hover{background:var(--muted);color:var(--foreground)}.nav-item-active{background:var(--muted);color:var(--foreground);font-weight:500}.nav-count{color:var(--muted-foreground);font-variant-numeric:tabular-nums;margin-left:auto;font-size:11px}.sidebar-bottom{border-top:1px solid var(--border);gap:4px;margin-top:auto;padding-top:12px;display:grid}.theme-switch{text-align:left;background:0 0;border:0;width:100%}.mobile-theme{display:none}.workshop{background:var(--background);flex:1;min-width:0}.project-header{border-bottom:1px solid var(--border);background:var(--background);padding:36px 56px 22px}.project-heading{justify-content:space-between;align-items:flex-start;gap:24px;min-width:0;max-width:1500px;margin:0 auto;display:flex}.project-heading-copy{flex:1;min-width:0}.project-eyebrow{color:var(--muted-foreground);margin-bottom:5px;font-size:12px;line-height:16px;display:block}.project-header h1{overflow-wrap:anywhere;max-width:100%;color:var(--foreground);letter-spacing:-.035em;margin:0;font-size:36px;font-weight:600;line-height:40px}.project-path{color:var(--muted-foreground);margin-top:7px;font-size:12px;line-height:18px}.project-path summary{cursor:pointer;width:fit-content}.project-path div{overflow-wrap:anywhere;max-width:100%;padding-top:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.project-freshness{color:var(--muted-foreground);margin-top:7px;font-size:11px;line-height:16px;display:block}.project-freshness.is-stale{color:var(--warning)}.project-new-task{flex:none;margin-top:17px;border-radius:7px!important;font-size:13px!important}.runs-page{gap:22px;max-width:1500px;margin:0 auto;padding:27px 56px 56px;display:grid}.tasks-toolbar{justify-content:space-between;align-items:flex-end;gap:24px;min-width:0;display:flex}.tasks-heading h2{letter-spacing:-.025em;margin:0;font-size:22px;font-weight:600;line-height:28px}.tasks-heading p{color:var(--muted-foreground);margin:3px 0 0;font-size:12px;line-height:18px}.tasks-tools{justify-content:flex-end;align-items:center;gap:10px;min-width:0;display:flex}.task-search{border:1px solid var(--border);background:var(--surface);width:min(320px,34vw);min-width:180px;height:36px;color:var(--muted-foreground);border-radius:8px;align-items:center;gap:8px;padding:0 10px;display:flex}.task-search:focus-within{border-color:var(--foreground)}@supports (color:color-mix(in lab, red, red)){.task-search:focus-within{border-color:color-mix(in srgb, var(--foreground) 45%, var(--border))}}.task-search input{width:100%;min-width:0;height:100%;color:var(--foreground);background:0 0;border:0;outline:0;padding:0;font-size:13px}.task-search input::placeholder{color:var(--muted-foreground)}.task-search input::-webkit-search-cancel-button{display:none}.clear-search{width:22px;height:22px;color:var(--muted-foreground);background:0 0;border:0;border-radius:4px;flex:none;place-items:center;display:grid}.clear-search:hover{background:var(--muted);color:var(--foreground)}.view-toggle{border:1px solid var(--border);border-radius:8px;flex:none;align-items:center;gap:2px;height:36px;padding:3px;display:flex}.view-toggle button{border-radius:5px;height:28px;padding:0 9px;font-size:12px}.view-toggle .view-active{background:var(--muted);color:var(--foreground)}.run-workspace{grid-template-columns:212px minmax(0,1fr);align-items:start;gap:48px;min-width:0;display:grid}.task-filter-rail{gap:8px;min-width:0;display:grid}.task-filter-rail>h2{color:var(--muted-foreground);letter-spacing:.04em;text-transform:uppercase;margin:0 0 2px;font-size:11px;font-weight:500;line-height:16px}.filter-card{border:1px solid var(--border);background:var(--surface);border-radius:8px;min-width:0;overflow:hidden}.filter-card-main{width:100%;color:var(--foreground);text-align:left;background:0 0;border:0;padding:10px 11px 9px;display:block}.filter-card-main:hover,.filter-card-main[aria-pressed=true]{background:var(--muted)}.filter-card-main:disabled,.filter-substate:disabled{cursor:default;opacity:.7}.filter-card-heading{justify-content:space-between;align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:18px;display:flex}.filter-card-count{color:var(--foreground);font-variant-numeric:tabular-nums;font-size:13px}.filter-card-description{color:var(--muted-foreground);margin-top:2px;font-size:11px;line-height:15px;display:block}.filter-card-children{border-top:1px solid var(--border);gap:1px;padding:4px 7px 6px;display:grid}.filter-substate{min-width:0;min-height:25px;color:var(--muted-foreground);text-align:left;background:0 0;border:0;border-radius:4px;justify-content:space-between;align-items:center;gap:8px;padding:3px 5px;font-size:11px;line-height:16px;display:flex}.filter-substate:hover,.filter-substate[aria-pressed=true]{background:var(--muted);color:var(--foreground)}.filter-substate span:first-child{overflow-wrap:anywhere;min-width:0}.filter-substate span:last-child{font-variant-numeric:tabular-nums;flex:none}.filter-substate.is-subset{border-left:1px solid var(--border);margin-left:4px;padding-left:8px}.task-results{min-width:0}.mobile-filter{display:none}.task-list{border-top:1px solid var(--border);min-width:0}.task-row{border-bottom:1px solid var(--border);min-width:0}.task-row-link{min-width:0;color:var(--foreground);border-radius:4px;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:18px;padding:12px 4px 13px;text-decoration:none;display:grid}.task-row-link:hover{background:var(--muted)}@supports (color:color-mix(in lab, red, red)){.task-row-link:hover{background:color-mix(in srgb, var(--muted) 68%, transparent)}}.task-row-content{min-width:0}.task-row-title{overflow-wrap:anywhere;margin:0;font-size:14px;font-weight:500;line-height:20px}.task-row-meta{min-width:0;color:var(--muted-foreground);flex-wrap:wrap;align-items:baseline;gap:2px 14px;margin-top:4px;font-size:11px;line-height:16px;display:flex}.task-row-meta>*{overflow-wrap:anywhere}.task-row-next{color:var(--muted-foreground)}.task-state{border-radius:5px;flex:none;padding:2px 8px;font-size:11px;font-weight:500;line-height:16px}.run-column{border:1px solid var(--border);background:var(--surface);border-radius:8px;min-width:0;overflow:hidden}.run-card-link{min-width:0;color:var(--foreground);padding:12px;text-decoration:none;display:block}.run-card-link:hover{background:var(--muted)}.run-card-title{overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:19px}.run-card-meta{color:var(--muted-foreground);margin:6px 0 0;font-size:11px;line-height:16px}.run-card-status{flex-wrap:wrap;align-items:center;gap:7px;margin-top:9px;display:flex}.run-card-status>span:last-child{color:var(--muted-foreground);font-size:11px}.empty-tasks,.task-message{border:1px dashed var(--border);text-align:center;border-radius:8px;align-content:center;justify-items:center;gap:7px;min-height:230px;padding:28px 18px;display:grid}.empty-tasks h3,.task-message h3{margin:0;font-size:14px;font-weight:600;line-height:20px}.empty-tasks p,.task-message p{max-width:32rem;color:var(--muted-foreground);overflow-wrap:anywhere;margin:0;font-size:12px;line-height:18px}.empty-tasks button,.task-message button{margin-top:6px}.task-message-error{border-style:solid;border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.task-message-error{border-color:color-mix(in srgb, var(--danger) 35%, var(--border))}}.task-message-error h3{color:var(--danger)}.stale-banner{border:1px solid var(--warning);justify-content:space-between;align-items:center;gap:14px;padding:9px 12px;display:flex}@supports (color:color-mix(in lab, red, red)){.stale-banner{border:1px solid color-mix(in srgb, var(--warning) 32%, var(--border))}}.stale-banner{color:var(--muted-foreground);border-radius:7px;font-size:12px;line-height:18px}.stale-banner strong{color:var(--warning)}.stale-banner button{flex:none}.form-error{border:1px solid var(--danger);padding:9px 12px}@supports (color:color-mix(in lab, red, red)){.form-error{border:1px solid color-mix(in srgb, var(--danger) 35%, var(--border))}}.form-error{background:var(--danger);border-radius:7px}@supports (color:color-mix(in lab, red, red)){.form-error{background:color-mix(in srgb, var(--danger) 8%, var(--background))}}.form-error{color:var(--danger);font-size:13px}.field-label{color:var(--muted-foreground);margin-bottom:5px;font-size:12px;font-weight:500;line-height:17px;display:block}.field-control{border:1px solid var(--border);background:var(--background);width:100%;min-height:36px;color:var(--foreground);border-radius:7px;outline:none;padding:7px 10px;font-size:13px;line-height:20px;transition:border-color .15s,box-shadow .15s}.field-control::placeholder{color:var(--muted-foreground);opacity:.78}.field-control:focus{border-color:var(--foreground)}@supports (color:color-mix(in lab, red, red)){.field-control:focus{border-color:color-mix(in srgb, var(--foreground) 50%, var(--border))}}.field-control:focus{box-shadow:0 0 0 2px var(--foreground)}@supports (color:color-mix(in lab, red, red)){.field-control:focus{box-shadow:0 0 0 2px color-mix(in srgb, var(--foreground) 12%, transparent)}}@media (width<=1199px) and (width>=961px){.project-header,.runs-page{padding-left:38px;padding-right:38px}.run-workspace{gap:24px}}@media (width<=960px){.task-filter-rail{display:none}.mobile-filter{max-width:100%;color:var(--muted-foreground);gap:5px;margin:0 0 12px;font-size:11px;line-height:15px;display:grid}.run-workspace{display:block}}@media (width<=767px){.app-shell{display:block}.app-sidebar{z-index:40;border-right:0;border-bottom:1px solid var(--border);flex-direction:row;align-items:center;gap:8px;width:100%;height:56px;padding:0 12px}.brand-lockup{gap:8px}.brand-wordmark{font-size:14px;display:inline}.factory-mark{width:27px;height:25px}.desktop-nav,.sidebar-bottom{display:none}.mobile-nav{margin-left:auto;display:block;position:relative}.mobile-nav summary{border:1px solid var(--border);height:36px;color:var(--foreground);cursor:pointer;border-radius:7px;align-items:center;gap:7px;padding:0 9px;font-size:12px;list-style:none;display:flex}.mobile-nav summary::-webkit-details-marker{display:none}.mobile-nav nav{z-index:50;border:1px solid var(--border);background:var(--surface);border-radius:9px;gap:3px;width:min(260px,100vw - 24px);padding:8px;display:grid;position:absolute;top:42px;right:0;box-shadow:0 12px 30px #00000024}.mobile-nav nav .nav-item{width:100%}.mobile-theme{color:var(--muted-foreground);background:0 0;border:1px solid #0000;border-radius:7px;flex:none;place-items:center;display:grid}.mobile-theme:hover{border-color:var(--border);color:var(--foreground)}.project-header{padding:25px 20px 17px}.project-heading{flex-direction:column;align-items:stretch;gap:12px}.project-header h1{letter-spacing:-.03em;font-size:30px;line-height:34px}.project-new-task{align-self:flex-start;margin-top:0}.runs-page{gap:18px;padding:21px 20px 36px}.tasks-toolbar{align-items:start;gap:14px;display:grid}.tasks-heading h2{font-size:20px;line-height:26px}.tasks-tools{flex-wrap:wrap;justify-content:space-between;gap:8px;width:100%}.task-search{width:100%;min-width:0}.view-toggle{margin-left:auto}.task-row-link{flex-direction:column;align-items:flex-start;gap:7px;padding:12px 2px;display:flex}.task-row-content{width:100%}.task-row-title{font-size:14px;line-height:20px}.task-row-meta{gap:2px;margin-top:4px;font-size:11px;line-height:16px;display:grid}.task-state{align-self:flex-start}.run-column{border-radius:7px}.stale-banner{flex-direction:column;align-items:flex-start}.stale-banner button{align-self:flex-start}}@media (width<=390px){.project-header{padding-left:16px;padding-right:16px}.project-heading{flex-wrap:wrap}.project-heading-copy{width:100%}.project-header h1{font-size:28px;line-height:32px}.project-new-task{margin-top:0}.runs-page{padding-left:16px;padding-right:16px}.tasks-tools{align-items:stretch}.view-toggle{margin-left:0}.filter-control{width:100%}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|