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.
Files changed (30) hide show
  1. package/.github/ISSUE_TEMPLATE/factory-task.yml +5 -2
  2. package/THIRD_PARTY_NOTICES.md +4 -3
  3. package/bin/software-defence-factory.mjs +15 -24
  4. package/docs/quickstart.md +17 -2
  5. package/docs/recovery.md +9 -0
  6. package/docs/setup.md +17 -5
  7. package/factory/image-install.mjs +189 -0
  8. package/factory/installation-lock.mjs +15 -0
  9. package/factory/services.mjs +2 -2
  10. package/factory/supervisor.mjs +10 -2
  11. package/factory/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  12. package/factory/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  13. package/factory/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  14. package/factory/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  15. package/factory/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  16. package/factory/ui/assets/index-BWfaUr91.css +1 -0
  17. package/factory/ui/assets/index-CnV0vWSl.js +11 -0
  18. package/factory/ui/index.html +2 -2
  19. package/kit/README.md +4 -0
  20. package/kit/repository.md +86 -0
  21. package/package.json +1 -1
  22. package/scripts/export-kit.mjs +1 -1
  23. package/factory/ui/assets/index-BS9sWkVd.js +0 -10
  24. package/factory/ui/assets/index-DIgJUDQ1.css +0 -1
  25. package/factory/ui/assets/manrope-cyrillic-ext-wght-normal-C8S-KRRz.woff2 +0 -0
  26. package/factory/ui/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
  27. package/factory/ui/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
  28. package/factory/ui/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
  29. package/factory/ui/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  30. 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, relevant files, allowed changes and rollback.
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, computer use, tests, web research or security review. Never paste credentials.
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.
@@ -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 interface is retained at the owner's request; branding and runtime integration are modified.
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/manrope 5.3.0
34
+ ## @fontsource-variable/geist 5.3.0
35
35
 
36
- Copyright 2019 The Manrope Project Authors (https://github.com/sharanda/manrope)
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
- run('docker',['info','--format','{{.ServerVersion}}']);
65
- retainImage(config.image);retainImage(PINS.jobImage);
66
- await stream('docker',['build','-t',PINS.jobImage,join(ROOT,'factory/image')]);
67
- config.image=retainImage(PINS.jobImage);
68
- if(!config.image)throw new Error('Built job image is unavailable');
69
- save(join(state,'factory.json'),config);save(join(state,'engine.json'),{runtime:'native-node',version:VERSION,image:config.image});
70
- console.log('Installed. Job image is pinned to its local image ID. No model call made.');
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
- if(!existsSync(join(state,'engine.json')))throw new Error('Run install first');
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
- run('docker',['image','inspect',config.image]);await portFree(config.port);
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
- if(!existsSync(join(state,'engine.json')))throw new Error('Run install first');
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);console.log(JSON.stringify({node:process.version,docker:run('docker',['info','--format','{{.ServerVersion}}']),engineInstalled:existsSync(join(state,'engine.json')),repo:config.repo,agent:config.agent,checksConfigured:!!config.check?.trim(),inference:'Not called or verified',dashboard:`http://127.0.0.1:${config.port}`},null,2));
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 Build the isolated job image; the controller ships with the CLI
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
@@ -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
- Pin an application-specific image with the required toolchains. In `factory.json`,
65
- `cpus` (1–32, default 2), `pidsLimit` (64–16384, default 256), `memoryMiB` and
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. `install`
127
- builds the standard image: do not use it to overwrite an existing custom image
128
- selection. Follow the application image's own build/pinning procedure instead.
129
- `init` does not edit the app, install personal skills or submit a task. Runtime
130
- jobs receive the bundled method and six skills automatically.
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
+ }
@@ -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
- if (!existsSync(join(state, 'engine.json'))) throw new Error('Run install for this state before installing its service');
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) {
@@ -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 config = configAt(state), lock = join(state, 'supervisor.json');
8
- writeFileSync(lock, JSON.stringify({ pid: process.pid }), { flag: 'wx', mode: 0o600 });
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;
@@ -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}