gcf-board 0.2.13 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/server/app.js +27 -6
- package/dist/server/auth.js +4 -3
- package/dist/server/cli.js +2 -0
- package/dist/server/moves.js +4 -2
- package/dist/server/runs.js +96 -24
- package/dist/web/assets/{index-CE-603CK.css → index-CjG8ddKQ.css} +1 -1
- package/dist/web/assets/index-z61pr0qO.js +33 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-BbIDvesS.js +0 -31
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@ It puts **GCF Board** on the desktop (Windows: a shortcut whose server console s
|
|
|
42
42
|
- `run_mode: worktree`: one worktree and one agent session per item, all at once, then the integrator;
|
|
43
43
|
- `run_mode: main`: one item at a time in the project folder. A run asked for meanwhile waits in the board's **queue**; **remove from queue** takes it out.
|
|
44
44
|
|
|
45
|
-
A run asks first: it spends the subscription. **Stop** on a running item ends its session; the item stays ready and its work is kept. A queued main run that the next one would repeat (the harness did not start, or another run works the checkout) pauses the queue: the header shows **queue paused — resume**; fix the cause, then resume. A card marked **interrupted** had a run that stopped (a timeout, a stop, a crash, the usage limit): **resume** continues its agent session with all its context, **start fresh** drops it. After the usage limit the card reads **paused: limit** and the board resumes it by itself at the reset time; the header shows **resumes HH:MM** with **cancel
|
|
45
|
+
A run asks first: it spends the subscription. **Stop** on a running item ends its session; the item stays ready and its work is kept. A queued main run that the next one would repeat (the harness did not start, or another run works the checkout) pauses the queue: the header shows **queue paused — resume**; fix the cause, then resume. A card marked **interrupted** had a run that stopped (a timeout, a stop, a crash, the usage limit): **resume** continues its agent session with all its context, **start fresh** drops it. After the usage limit the card reads **paused: limit** and the board resumes it by itself at the reset time; the header shows **resumes HH:MM** with **cancel**, and a queue the limit paused resumes only by that clock until the operator cancels it; a pause after a failure keeps its **resume** button. When a run ends, a report card lists each item's result and stays until closed, across page reloads. **What the board did by itself** lists its automatic acts with their times: a usage limit, a context handoff, the queue paused or started, a resume run or cancelled. A run confirmation names what each interrupted item picks up. The queue, the reports and that list live in the board's memory: a board restart forgets them.
|
|
46
|
+
- **Who runs it** (core 0.7.13): a running card tells a console run and **your session** (a `gcf claim` mark) from the board's own run; a session offers **release mark** instead of **stop**. A ready card whose dependencies are not built yet shows **waits for <dep>**; the drawer shows when an item began to wait (`since_at`).
|
|
46
47
|
- **Run next N independent**: the framework's gate picks the ready items that can run together now, by priority, and the board runs them after asking.
|
|
47
48
|
- **Priority**: drop a ready card on another to put it before; the column's ranks are committed (`gcf prioritize`).
|
|
48
49
|
- **Questions**: each D1 shows its question and proposed answer; **Answer** writes yours into its record (`gcf question answer`), and the next agent on its units puts it in place.
|
package/dist/server/app.js
CHANGED
|
@@ -14,7 +14,7 @@ import { followLog, readShown, shownShape } from './files.js';
|
|
|
14
14
|
import { HttpError } from './http-error.js';
|
|
15
15
|
import { moveArgs, runIds } from './moves.js';
|
|
16
16
|
import { addProject, listProjects, projectOf, removeProject } from './registry.js';
|
|
17
|
-
import { cancelResume, forgetProject, resumeQueue, runsOf, startRun, unqueue } from './runs.js';
|
|
17
|
+
import { cancelResume, closeReport, forgetProject, resumeQueue, runsOf, startRun, unqueue, } from './runs.js';
|
|
18
18
|
const send = (res, status, body) => {
|
|
19
19
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
20
20
|
res.end(JSON.stringify(body));
|
|
@@ -90,8 +90,13 @@ async function sendPage(res, web, pathname) {
|
|
|
90
90
|
}
|
|
91
91
|
export function createApp(o) {
|
|
92
92
|
const watchers = new Map();
|
|
93
|
+
let closed = false;
|
|
93
94
|
const watchAll = async () => {
|
|
94
|
-
|
|
95
|
+
const projects = await listProjects();
|
|
96
|
+
// A watcher set up after the server closed would keep the process alive.
|
|
97
|
+
if (closed)
|
|
98
|
+
return;
|
|
99
|
+
for (const p of projects)
|
|
95
100
|
if (!watchers.has(p.name))
|
|
96
101
|
watchers.set(p.name, watchProject(p));
|
|
97
102
|
};
|
|
@@ -140,10 +145,15 @@ export function createApp(o) {
|
|
|
140
145
|
// count of plan writes, which tells the page an open plan's text changed.
|
|
141
146
|
const own = { runs: runsOf(p.name), plans_rev: planRev(p.name) };
|
|
142
147
|
const board = await boardOf(p);
|
|
143
|
-
// A running item's last log write tells a working session from a stuck one
|
|
144
|
-
|
|
148
|
+
// A running item's last log write tells a working session from a stuck one; a run's
|
|
149
|
+
// lock is this board's run or a console's.
|
|
150
|
+
const mine = new Set(own.runs.active.flatMap((r) => r.ids));
|
|
151
|
+
for (const i of board.items) {
|
|
145
152
|
if (i.running)
|
|
146
153
|
i.active_at = i.log ? activeAt(path.join(p.path, i.log)) : null;
|
|
154
|
+
if (i.running_kind === 'run')
|
|
155
|
+
i.running_kind = mine.has(i.id) ? 'board' : 'console';
|
|
156
|
+
}
|
|
147
157
|
send(res, 200, { ...board, ...own });
|
|
148
158
|
},
|
|
149
159
|
],
|
|
@@ -249,6 +259,14 @@ export function createApp(o) {
|
|
|
249
259
|
send(res, 200, { ok: true });
|
|
250
260
|
},
|
|
251
261
|
],
|
|
262
|
+
[
|
|
263
|
+
'DELETE',
|
|
264
|
+
/^\/api\/p\/([^/]+)\/reports\/(r\d+)$/,
|
|
265
|
+
async (_req, res, m) => {
|
|
266
|
+
closeReport((await projectOf(m[1])).name, m[2]);
|
|
267
|
+
send(res, 200, { ok: true });
|
|
268
|
+
},
|
|
269
|
+
],
|
|
252
270
|
[
|
|
253
271
|
'DELETE',
|
|
254
272
|
/^\/api\/p\/([^/]+)\/queue\/([a-z0-9][a-z0-9-]*)$/,
|
|
@@ -290,15 +308,18 @@ export function createApp(o) {
|
|
|
290
308
|
const server = createServer((req, res) => {
|
|
291
309
|
handle(req, res).catch((e) => {
|
|
292
310
|
if (res.headersSent)
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
res.end();
|
|
312
|
+
else
|
|
313
|
+
send(res, e instanceof HttpError ? e.status : 500, { error: e.message });
|
|
295
314
|
});
|
|
296
315
|
});
|
|
297
316
|
// A broken projects.json must not take the server down: the page shows the error instead.
|
|
298
317
|
server.on('listening', () => watchAll().catch((e) => console.error(`gcf-board: ${e.message}`)));
|
|
299
318
|
server.on('close', () => {
|
|
319
|
+
closed = true;
|
|
300
320
|
for (const stop of watchers.values())
|
|
301
321
|
stop();
|
|
322
|
+
watchers.clear();
|
|
302
323
|
});
|
|
303
324
|
return server;
|
|
304
325
|
}
|
package/dist/server/auth.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The board starts agents that commit and push: whoever talks to it runs code on this machine.
|
|
2
|
-
// It listens on 127.0.0.1 only
|
|
3
|
-
// cookie
|
|
4
|
-
// reach 127.0.0.1 (DNS rebinding, a
|
|
2
|
+
// It listens on 127.0.0.1 only. The URL printed at start carries the token; that visit gets a
|
|
3
|
+
// session cookie (one per server process, never the token itself), and every request needs it.
|
|
4
|
+
// Host and Origin are checked too: a page in another tab can reach 127.0.0.1 (DNS rebinding, a
|
|
5
|
+
// cross-site POST), but it cannot fake those headers.
|
|
5
6
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
6
7
|
import { existsSync } from 'node:fs';
|
|
7
8
|
import { mkdir, readFile } from 'node:fs/promises';
|
package/dist/server/cli.js
CHANGED
|
@@ -71,6 +71,8 @@ export function runCli(entry, cwd, args, timeoutMs = 120_000) {
|
|
|
71
71
|
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
72
72
|
else
|
|
73
73
|
child.kill();
|
|
74
|
+
// A grandchild holding the pipes keeps close from firing: the answer does not wait for it.
|
|
75
|
+
setTimeout(() => resolve({ ok: false, code: null, out, err }), 5_000).unref();
|
|
74
76
|
}, timeoutMs);
|
|
75
77
|
child.on('error', (e) => {
|
|
76
78
|
clearTimeout(timer);
|
package/dist/server/moves.js
CHANGED
|
@@ -60,8 +60,8 @@ const NEW_ITEM_OPTIONS = [
|
|
|
60
60
|
'effort',
|
|
61
61
|
];
|
|
62
62
|
const MOVES = {
|
|
63
|
-
submit: (b) => ['submit', id(b)],
|
|
64
|
-
close: (b) => ['close', id(b), ...many(b, 'constraint')],
|
|
63
|
+
submit: (b) => ['submit', id(b), ...opt(b, 'unverified')],
|
|
64
|
+
close: (b) => ['close', id(b), ...many(b, 'constraint'), ...opt(b, 'unverified')],
|
|
65
65
|
verdict: (b) => [
|
|
66
66
|
'verdict',
|
|
67
67
|
id(b),
|
|
@@ -80,6 +80,8 @@ const MOVES = {
|
|
|
80
80
|
],
|
|
81
81
|
block: (b) => ['block', id(b), `--reason=${required(b, 'reason')}`],
|
|
82
82
|
unblock: (b) => ['unblock', id(b), ...opt(b, 'note')],
|
|
83
|
+
// A session's mark taken off: the item no longer shows running (Board-PRD §3.2, kind c).
|
|
84
|
+
release: (b) => ['release', id(b)],
|
|
83
85
|
drop: (b) => [
|
|
84
86
|
'drop',
|
|
85
87
|
id(b),
|
package/dist/server/runs.js
CHANGED
|
@@ -19,6 +19,22 @@ const scheduled = new Map();
|
|
|
19
19
|
const limitPaused = new Set();
|
|
20
20
|
// Runs going on for a project the operator removed: their end touches no project state.
|
|
21
21
|
const forgotten = new WeakSet();
|
|
22
|
+
// shortcut: reports and acts live in memory, like the queue: a board restart forgets them;
|
|
23
|
+
// persist them with the queue if restarts become common.
|
|
24
|
+
const reports = new Map();
|
|
25
|
+
const acts = new Map();
|
|
26
|
+
const KEEP_REPORTS = 20;
|
|
27
|
+
const KEEP_ACTS = 50;
|
|
28
|
+
let reportSeq = 0;
|
|
29
|
+
function trace(project, act, text) {
|
|
30
|
+
const list = [...(acts.get(project) ?? []), { at: new Date().toISOString(), act, text }];
|
|
31
|
+
acts.set(project, list.slice(-KEEP_ACTS));
|
|
32
|
+
}
|
|
33
|
+
/** The operator closed a run report. */
|
|
34
|
+
export function closeReport(project, id) {
|
|
35
|
+
reports.set(project, (reports.get(project) ?? []).filter((r) => r.id !== id));
|
|
36
|
+
broadcast({ type: 'board', project });
|
|
37
|
+
}
|
|
22
38
|
export const activeRuns = (project) => [...active].filter((r) => r.project === project);
|
|
23
39
|
/** What runs and what waits, for a project; a paused queue waits for `resumeQueue`. */
|
|
24
40
|
export function runsOf(project) {
|
|
@@ -27,8 +43,12 @@ export function runsOf(project) {
|
|
|
27
43
|
active: activeRuns(project),
|
|
28
44
|
queued: (queues.get(project) ?? []).flatMap((q) => q.ids),
|
|
29
45
|
paused: paused.has(project),
|
|
46
|
+
// Why it waits: the usage limit (its reset may lift it) or a failure (only the operator).
|
|
47
|
+
paused_by: paused.has(project) ? (limitPaused.has(project) ? 'limit' : 'failure') : null,
|
|
30
48
|
// When the items a usage limit stopped run again by themselves, and the queue with them.
|
|
31
49
|
resume: s ? { at: s.at.toISOString(), ids: s.run.ids } : null,
|
|
50
|
+
reports: reports.get(project) ?? [],
|
|
51
|
+
acts: acts.get(project) ?? [],
|
|
32
52
|
};
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
@@ -59,7 +79,10 @@ export function resetAt(text, now = new Date()) {
|
|
|
59
79
|
}
|
|
60
80
|
/** Drop the scheduled resume; the queue stays as it is. */
|
|
61
81
|
export function cancelResume(project) {
|
|
62
|
-
|
|
82
|
+
const s = scheduled.get(project);
|
|
83
|
+
if (s?.run.ids.length)
|
|
84
|
+
trace(project, 'resume-cancelled', `the resume of ${s.run.ids.join(', ')} is cancelled`);
|
|
85
|
+
clearTimeout(s?.timer);
|
|
63
86
|
scheduled.delete(project);
|
|
64
87
|
broadcast({ type: 'board', project });
|
|
65
88
|
}
|
|
@@ -77,6 +100,8 @@ export function forgetProject(project) {
|
|
|
77
100
|
queues.delete(project);
|
|
78
101
|
paused.delete(project);
|
|
79
102
|
limitPaused.delete(project);
|
|
103
|
+
reports.delete(project);
|
|
104
|
+
acts.delete(project);
|
|
80
105
|
}
|
|
81
106
|
/**
|
|
82
107
|
* At the reset: run the stopped items again (the harness resumes their sessions), then the queue.
|
|
@@ -93,11 +118,18 @@ function schedule(run, at) {
|
|
|
93
118
|
scheduled.delete(project);
|
|
94
119
|
liftLimitPause(project);
|
|
95
120
|
try {
|
|
96
|
-
if (s)
|
|
97
|
-
startRun(s.run.entry, s.run.p, s.run.ids, s.run.mode);
|
|
121
|
+
if (s) {
|
|
122
|
+
const r = startRun(s.run.entry, s.run.p, s.run.ids, s.run.mode);
|
|
123
|
+
const how = 'queued' in r ? 'wait in the queue' : 'run again';
|
|
124
|
+
trace(project, 'resume', `the usage limit reset: ${s.run.ids.join(', ')} ${how}`);
|
|
125
|
+
}
|
|
98
126
|
}
|
|
99
|
-
catch {
|
|
100
|
-
//
|
|
127
|
+
catch (e) {
|
|
128
|
+
// The board's own run of an item takes it out of the schedule first, so this is rare;
|
|
129
|
+
// the queue still goes on.
|
|
130
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
131
|
+
if (s)
|
|
132
|
+
trace(project, 'resume-failed', `${s.run.ids.join(', ')}: ${why}`);
|
|
101
133
|
advance(project);
|
|
102
134
|
}
|
|
103
135
|
broadcast({ type: 'board', project });
|
|
@@ -108,12 +140,10 @@ function schedule(run, at) {
|
|
|
108
140
|
/**
|
|
109
141
|
* A run some of whose items hit the usage limit: the main queue would hit it too, so it pauses,
|
|
110
142
|
* and those items run again at the reset when the message names a time. The reason, for its
|
|
111
|
-
*
|
|
143
|
+
* report.
|
|
112
144
|
*/
|
|
113
145
|
function limited(run, result) {
|
|
114
|
-
const
|
|
115
|
-
?.items;
|
|
116
|
-
const hit = (items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
146
|
+
const hit = (result?.items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
117
147
|
if (!hit.length)
|
|
118
148
|
return '';
|
|
119
149
|
const project = run.p.name;
|
|
@@ -127,18 +157,28 @@ function limited(run, result) {
|
|
|
127
157
|
schedule({ ...run, ids: hit.map((i) => i.id), fresh: false }, at);
|
|
128
158
|
const when = at ? `they resume at ${at.toTimeString().slice(0, 5)}` : 'run them after the reset';
|
|
129
159
|
const queue = run.mode === 'main' ? ', the queue is paused' : '';
|
|
130
|
-
|
|
160
|
+
const reason = `usage limit${resets ? `, resets ${resets}` : ''}${queue}: ${when}`;
|
|
161
|
+
trace(project, 'limit', `${hit.map((i) => i.id).join(', ')}: ${reason}`);
|
|
162
|
+
return reason;
|
|
131
163
|
}
|
|
132
164
|
/** Start the next queued batch when the checkout is free: after a run, or on the operator's resume. */
|
|
133
165
|
function advance(project) {
|
|
134
166
|
if (paused.has(project) || activeRuns(project).some((r) => r.mode === 'main'))
|
|
135
167
|
return;
|
|
136
168
|
const next = queues.get(project)?.shift();
|
|
137
|
-
if (next)
|
|
138
|
-
|
|
169
|
+
if (!next)
|
|
170
|
+
return;
|
|
171
|
+
trace(project, 'queue-run', `the queue starts ${next.ids.join(', ')}`);
|
|
172
|
+
spawnRun(next, next.mode);
|
|
139
173
|
}
|
|
140
174
|
/** The operator resumes a paused queue (after fixing what stopped it). */
|
|
141
175
|
export function resumeQueue(project) {
|
|
176
|
+
// The limit still holds until its reset: the next run would hit it again.
|
|
177
|
+
const s = scheduled.get(project);
|
|
178
|
+
if (s && limitPaused.has(project))
|
|
179
|
+
throw new HttpError(409, `the queue resumes by itself at ${s.at.toTimeString().slice(0, 5)}, after the usage limit; cancel that first to resume now`);
|
|
180
|
+
if (paused.has(project))
|
|
181
|
+
trace(project, 'queue-resumed', 'the operator resumed the queue');
|
|
142
182
|
paused.delete(project);
|
|
143
183
|
limitPaused.delete(project);
|
|
144
184
|
advance(project);
|
|
@@ -163,6 +203,34 @@ export function stalled(ok, result) {
|
|
|
163
203
|
const items = result?.items ?? [];
|
|
164
204
|
return (items.length > 0 && items.every((i) => /another run works in the main/.test(i.error ?? '')));
|
|
165
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* The harness report a run printed, or null for none. A report of another shape counts as none:
|
|
208
|
+
* the close listener that reads it would throw, and take the server down with it.
|
|
209
|
+
*/
|
|
210
|
+
export function harnessReport(out) {
|
|
211
|
+
let r;
|
|
212
|
+
try {
|
|
213
|
+
r = JSON.parse(out);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// No report: the error says why.
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
220
|
+
return null;
|
|
221
|
+
const { gate, items, orphans } = r;
|
|
222
|
+
const list = (v) => v === undefined || (Array.isArray(v) && v.every(isItem));
|
|
223
|
+
return (gate === undefined || typeof gate === 'string') && list(items) && list(orphans)
|
|
224
|
+
? r
|
|
225
|
+
: null;
|
|
226
|
+
}
|
|
227
|
+
// The fields read as more than text: the id, and the reset a resume is scheduled at.
|
|
228
|
+
function isItem(i) {
|
|
229
|
+
if (typeof i !== 'object' || i === null)
|
|
230
|
+
return false;
|
|
231
|
+
const { id, limit_resets: resets } = i;
|
|
232
|
+
return typeof id === 'string' && (resets == null || typeof resets === 'string');
|
|
233
|
+
}
|
|
166
234
|
function spawnRun(q, mode) {
|
|
167
235
|
const { entry, p, ids } = q;
|
|
168
236
|
const run = { project: p.name, ids, started: new Date().toISOString(), mode };
|
|
@@ -183,13 +251,7 @@ function spawnRun(q, mode) {
|
|
|
183
251
|
// A spawn that fails fires both error and close: one end only.
|
|
184
252
|
if (!active.delete(run))
|
|
185
253
|
return;
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
result = JSON.parse(out);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
// No report: the error says why.
|
|
192
|
-
}
|
|
254
|
+
const result = harnessReport(out);
|
|
193
255
|
// A run of a project removed meanwhile only reports: it pauses and schedules nothing.
|
|
194
256
|
const kept = !forgotten.has(run);
|
|
195
257
|
const stop = kept && mode === 'main' && queues.get(p.name)?.length && stalled(ok, result);
|
|
@@ -201,16 +263,24 @@ function spawnRun(q, mode) {
|
|
|
201
263
|
? ''
|
|
202
264
|
: limited(q, result) ||
|
|
203
265
|
(stop ? 'the queue is paused: fix the cause, then resume it on the board' : '');
|
|
266
|
+
if (stop)
|
|
267
|
+
trace(p.name, 'queue-paused', `after ${ids.join(', ')}: ${note}`);
|
|
268
|
+
for (const i of result?.items ?? [])
|
|
269
|
+
if (kept && i.handoffs)
|
|
270
|
+
trace(p.name, 'handoff', `${i.id}: ${i.handoffs} context handoff(s) to a fresh session`);
|
|
204
271
|
const brief = err && briefError(err);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
272
|
+
const report = {
|
|
273
|
+
id: `r${++reportSeq}`,
|
|
274
|
+
at: new Date().toISOString(),
|
|
208
275
|
ids,
|
|
209
276
|
ok,
|
|
210
277
|
result,
|
|
211
278
|
err: (brief + message).trim(),
|
|
212
279
|
note,
|
|
213
|
-
}
|
|
280
|
+
};
|
|
281
|
+
// A project removed meanwhile has no board to show it on.
|
|
282
|
+
if (kept)
|
|
283
|
+
reports.set(p.name, [...(reports.get(p.name) ?? []), report].slice(-KEEP_REPORTS));
|
|
214
284
|
// The checkout is free now, for whatever queue the name has: a project added back meanwhile.
|
|
215
285
|
if (mode === 'main')
|
|
216
286
|
advance(p.name);
|
|
@@ -222,7 +292,7 @@ function spawnRun(q, mode) {
|
|
|
222
292
|
}
|
|
223
293
|
/**
|
|
224
294
|
* Start a batch in the project's run mode, or queue it behind the main run going on. Its end
|
|
225
|
-
* is a
|
|
295
|
+
* is a run report with the harness report, or what went wrong. `fresh` starts new agent
|
|
226
296
|
* sessions where the harness would resume the interrupted ones.
|
|
227
297
|
*/
|
|
228
298
|
export function startRun(entry, p, ids, mode = 'worktree', fresh = false) {
|
|
@@ -235,6 +305,8 @@ export function startRun(entry, p, ids, mode = 'worktree', fresh = false) {
|
|
|
235
305
|
// now, and the limit's pause with it; the rest of the schedule stays.
|
|
236
306
|
const s = scheduled.get(p.name);
|
|
237
307
|
if (s?.run.ids.some((id) => ids.includes(id))) {
|
|
308
|
+
const early = s.run.ids.filter((id) => ids.includes(id));
|
|
309
|
+
trace(p.name, 'resume-cancelled', `${early.join(', ')}: the operator runs them before the reset`);
|
|
238
310
|
s.run = { ...s.run, ids: s.run.ids.filter((id) => !ids.includes(id)) };
|
|
239
311
|
if (!s.run.ids.length)
|
|
240
312
|
cancelResume(p.name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg:#14161a;--panel:#1d2026;--card:#262a32;--line:#343a45;--text:#e6e8ec;--muted:#8b93a1;--accent:#5b9cff;--ok:#46c08a;--warn:#e8a33d;--bad:#e5534b;--verdict:#b48cff;--auto:#46c08a;--decision:#e8a33d;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;font:14px/1.4 system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}body{background:var(--bg);color:var(--text);margin:0}button,input,select,textarea{font:inherit;color:inherit;background:var(--card);border:1px solid var(--line);border-radius:6px;padding:6px 10px}button{cursor:pointer}button:disabled{opacity:.5;cursor:default}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.danger{border-color:var(--bad);color:var(--bad)}code,pre{font-family:ui-monospace,Cascadia Code,Consolas,monospace}.muted{color:var(--muted)}.error,.warn{color:var(--bad)}.ok{color:var(--ok)}.badge{background:var(--line);border-radius:4px;padding:1px 6px;font-size:12px;display:inline-block}.topbar{background:var(--panel);border-bottom:1px solid var(--line);z-index:5;align-items:center;gap:12px;padding:8px 16px;display:flex;position:sticky;top:0}.projects-menu{position:relative}.projects-menu>:not(summary){margin-top:8px}.projects-menu[open]{background:var(--panel);border:1px solid var(--line);z-index:20;border-radius:8px;width:min(520px,92vw);padding:10px;position:absolute;top:8px;right:16px}.add-project{flex-wrap:wrap;gap:6px;display:flex}.add-project input:first-child{flex:260px}.empty{padding:32px}.project{padding:12px 16px 48px}.banner{border:1px solid var(--bad);border-radius:6px;margin-bottom:8px;padding:8px 12px}.header{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px;display:flex}.phase{flex-wrap:wrap;flex:1;align-items:center;gap:8px;display:flex}.criterion{color:var(--muted);font-size:12px}.criterion.met-true{color:var(--ok)}.criterion.met-false{color:var(--warn)}.wip.over{color:var(--bad);font-weight:600}.tabs{gap:4px;margin:8px 0;display:flex;overflow-x:auto}.tabs button{background:0 0;border-color:#0000}.tabs button.active{border-color:var(--line);background:var(--panel)}.board-actions{align-items:center;gap:12px;margin-bottom:8px;display:flex}.graveyard-zone{border:1px dashed var(--line);color:var(--muted);text-align:center;border-radius:6px;flex:1;padding:6px 10px}.graveyard-zone.over{border-color:var(--bad);color:var(--bad)}.columns{scroll-snap-type:x mandatory;grid-auto-columns:minmax(230px,1fr);grid-auto-flow:column;gap:10px;padding-bottom:8px;display:grid;overflow-x:auto}.column{background:var(--panel);border:1px solid var(--line);scroll-snap-align:start;border-radius:8px;min-height:200px;padding:8px}.column.over{border-color:var(--accent)}.column h3{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:13px}.count{color:var(--text)}.card{background:var(--card);border:1px solid var(--line);border-left:3px solid var(--line);cursor:pointer;touch-action:manipulation;border-radius:6px;margin-bottom:8px;padding:8px}.card.gate-verdict{border-left-color:var(--verdict)}.card.gate-auto{border-left-color:var(--auto)}.card.gate-decision{border-left-color:var(--decision)}.card.dragging{opacity:.7;z-index:10;position:relative}.card-top,.card-bottom{flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.card .id{font-size:13px;font-weight:600}.card .type,.card .model,.card .tune{color:var(--muted)}.card .lessons{color:var(--warn)}button.link{cursor:pointer;background:0 0;border:none;padding:0;text-decoration:underline}.drawer .lessons{margin:8px 0}.gate{margin-left:auto;font-size:11px}.gate-verdict.gate{color:var(--verdict)}.gate-auto.gate{color:var(--auto)}.gate-decision.gate{color:var(--decision)}.title{margin:4px 0}.plan{background:var(--line);border-radius:4px;padding:0 5px}.verified{color:var(--ok)}.queued{color:var(--warn)}.activity{color:var(--muted)}.quiet{color:var(--warn)}.rank{color:var(--muted);font-size:11px}.card.drop-before{box-shadow:0 -3px 0 var(--accent)}.run-next{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.run-next input{width:56px}.sev-critical,.sev-high{color:var(--bad)}.blocked-why{color:var(--warn);white-space:pre-wrap;margin-top:4px;font-size:12px}.drawer{background:var(--panel);border-left:1px solid var(--line);z-index:30;flex-direction:column;width:min(640px,100vw);padding:12px 16px;display:flex;position:fixed;top:0;bottom:0;right:0;box-shadow:-8px 0 24px #0006}.drawer>header{justify-content:space-between;align-items:center;display:flex}.drawer h2{margin:0}.facts{grid-template-columns:max-content 1fr;gap:2px 12px;margin:8px 0;display:grid}.facts dt{color:var(--muted)}.facts dd{margin:0}.moves{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.moves details{gap:6px;display:inline-flex}.moves details[open]{flex-wrap:wrap;gap:6px;display:flex}.drawer-body{border-top:1px solid var(--line);flex:1;padding-top:8px;overflow:auto}.frontmatter{color:var(--muted);background:var(--card);white-space:pre-wrap;border-radius:6px;padding:8px;font-size:12px}.log{white-space:pre-wrap;word-break:break-word;margin:0;font-size:12px}.modal{z-index:40;background:#0000008c;place-items:center;padding:12px;display:grid;position:fixed;inset:0}.dialog{background:var(--panel);border:1px solid var(--line);border-radius:10px;flex-direction:column;gap:10px;width:min(480px,100%);max-height:90vh;padding:16px;display:flex;overflow:auto}.dialog h2{margin:0;font-size:16px}.dialog label{color:var(--muted);flex-direction:column;gap:4px;font-size:13px;display:flex}.dialog label>*{color:var(--text)}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.list-view ul{padding:0;list-style:none}.list-view li{border-bottom:1px solid var(--line);align-items:center;gap:8px;padding:6px 0;display:flex}.row-actions{gap:6px;margin-left:auto;display:flex}.inline-form{gap:6px;margin:8px 0;display:flex}.inline-form input{flex:1}.playtest .build{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.check{background:var(--card);border:1px solid var(--line);border-radius:8px;margin-bottom:8px;padding:10px}.check p{margin:4px 0}.ended{grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;display:grid}.ended table{border-collapse:collapse;width:100%}.ended td{border-bottom:1px solid var(--line);vertical-align:top;padding:4px 6px}.toasts{z-index:50;flex-direction:column;gap:6px;max-width:min(480px,92vw);display:flex;position:fixed;bottom:12px;right:12px}.toast{background:var(--card);border:1px solid var(--ok);white-space:pre-wrap;border-radius:6px;padding:8px 12px}.toast-close{float:right;background:0 0;border:none;margin:-4px -6px 0 8px;padding:0 6px}.toast.error{border-color:var(--bad)}@media (width<=700px){.project{padding:8px}.columns{grid-auto-columns:85vw}}
|
|
1
|
+
:root{--bg:#14161a;--panel:#1d2026;--card:#262a32;--line:#343a45;--text:#e6e8ec;--muted:#8b93a1;--accent:#5b9cff;--ok:#46c08a;--warn:#e8a33d;--bad:#e5534b;--verdict:#b48cff;--auto:#46c08a;--decision:#e8a33d;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;font:14px/1.4 system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}body{background:var(--bg);color:var(--text);margin:0}button,input,select,textarea{font:inherit;color:inherit;background:var(--card);border:1px solid var(--line);border-radius:6px;padding:6px 10px}button{cursor:pointer}button:disabled{opacity:.5;cursor:default}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.danger{border-color:var(--bad);color:var(--bad)}code,pre{font-family:ui-monospace,Cascadia Code,Consolas,monospace}.muted{color:var(--muted)}.error,.warn{color:var(--bad)}.ok{color:var(--ok)}.badge{background:var(--line);border-radius:4px;padding:1px 6px;font-size:12px;display:inline-block}.topbar{background:var(--panel);border-bottom:1px solid var(--line);z-index:5;align-items:center;gap:12px;padding:8px 16px;display:flex;position:sticky;top:0}.projects-menu{position:relative}.projects-menu>:not(summary){margin-top:8px}.projects-menu[open]{background:var(--panel);border:1px solid var(--line);z-index:20;border-radius:8px;width:min(520px,92vw);padding:10px;position:absolute;top:8px;right:16px}.add-project{flex-wrap:wrap;gap:6px;display:flex}.add-project input:first-child{flex:260px}.empty{padding:32px}.project{padding:12px 16px 48px}.banner{border:1px solid var(--bad);border-radius:6px;margin-bottom:8px;padding:8px 12px}.header{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px;display:flex}.phase{flex-wrap:wrap;flex:1;align-items:center;gap:8px;display:flex}.criterion{color:var(--muted);font-size:12px}.criterion.met-true{color:var(--ok)}.criterion.met-false{color:var(--warn)}.wip.over{color:var(--bad);font-weight:600}.tabs{gap:4px;margin:8px 0;display:flex;overflow-x:auto}.tabs button{background:0 0;border-color:#0000}.tabs button.active{border-color:var(--line);background:var(--panel)}.board-actions{align-items:center;gap:12px;margin-bottom:8px;display:flex}.graveyard-zone{border:1px dashed var(--line);color:var(--muted);text-align:center;border-radius:6px;flex:1;padding:6px 10px}.graveyard-zone.over{border-color:var(--bad);color:var(--bad)}.columns{scroll-snap-type:x mandatory;grid-auto-columns:minmax(230px,1fr);grid-auto-flow:column;gap:10px;padding-bottom:8px;display:grid;overflow-x:auto}.column{background:var(--panel);border:1px solid var(--line);scroll-snap-align:start;border-radius:8px;min-height:200px;padding:8px}.column.over{border-color:var(--accent)}.column h3{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:13px}.count{color:var(--text)}.card{background:var(--card);border:1px solid var(--line);border-left:3px solid var(--line);cursor:pointer;touch-action:manipulation;border-radius:6px;margin-bottom:8px;padding:8px}.card.gate-verdict{border-left-color:var(--verdict)}.card.gate-auto{border-left-color:var(--auto)}.card.gate-decision{border-left-color:var(--decision)}.card.dragging{opacity:.7;z-index:10;position:relative}.card-top,.card-bottom{flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.card .id{font-size:13px;font-weight:600}.card .type,.card .model,.card .tune{color:var(--muted)}.card .lessons{color:var(--warn)}button.link{cursor:pointer;background:0 0;border:none;padding:0;text-decoration:underline}.drawer .lessons{margin:8px 0}.gate{margin-left:auto;font-size:11px}.gate-verdict.gate{color:var(--verdict)}.gate-auto.gate{color:var(--auto)}.gate-decision.gate{color:var(--decision)}.title{margin:4px 0}.plan{background:var(--line);border-radius:4px;padding:0 5px}.verified{color:var(--ok)}.queued{color:var(--warn)}.activity{color:var(--muted)}.quiet{color:var(--warn)}.rank{color:var(--muted);font-size:11px}.card.drop-before{box-shadow:0 -3px 0 var(--accent)}.run-next{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.run-next input{width:56px}.sev-critical,.sev-high{color:var(--bad)}.blocked-why{color:var(--warn);white-space:pre-wrap;margin-top:4px;font-size:12px}.drawer{background:var(--panel);border-left:1px solid var(--line);z-index:30;flex-direction:column;width:min(640px,100vw);padding:12px 16px;display:flex;position:fixed;top:0;bottom:0;right:0;box-shadow:-8px 0 24px #0006}.drawer>header{justify-content:space-between;align-items:center;display:flex}.drawer h2{margin:0}.facts{grid-template-columns:max-content 1fr;gap:2px 12px;margin:8px 0;display:grid}.facts dt{color:var(--muted)}.facts dd{margin:0}.moves{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.moves details{gap:6px;display:inline-flex}.moves details[open]{flex-wrap:wrap;gap:6px;display:flex}.drawer-body{border-top:1px solid var(--line);flex:1;padding-top:8px;overflow:auto}.frontmatter{color:var(--muted);background:var(--card);white-space:pre-wrap;border-radius:6px;padding:8px;font-size:12px}.log{white-space:pre-wrap;word-break:break-word;margin:0;font-size:12px}.modal{z-index:40;background:#0000008c;place-items:center;padding:12px;display:grid;position:fixed;inset:0}.dialog{background:var(--panel);border:1px solid var(--line);border-radius:10px;flex-direction:column;gap:10px;width:min(480px,100%);max-height:90vh;padding:16px;display:flex;overflow:auto}.dialog h2{margin:0;font-size:16px}.dialog label{color:var(--muted);flex-direction:column;gap:4px;font-size:13px;display:flex}.dialog label>*{color:var(--text)}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.list-view ul{padding:0;list-style:none}.list-view li{border-bottom:1px solid var(--line);align-items:center;gap:8px;padding:6px 0;display:flex}.row-actions{gap:6px;margin-left:auto;display:flex}.inline-form{gap:6px;margin:8px 0;display:flex}.inline-form input{flex:1}.playtest .build{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.check{background:var(--card);border:1px solid var(--line);border-radius:8px;margin-bottom:8px;padding:10px}.check p{margin:4px 0}.ended{grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;display:grid}.ended table{border-collapse:collapse;width:100%}.ended td{border-bottom:1px solid var(--line);vertical-align:top;padding:4px 6px}.toasts{z-index:50;flex-direction:column;gap:6px;max-width:min(480px,92vw);display:flex;position:fixed;bottom:12px;right:12px}.toast{background:var(--card);border:1px solid var(--ok);white-space:pre-wrap;border-radius:6px;padding:8px 12px}.toast-close{float:right;background:0 0;border:none;margin:-4px -6px 0 8px;padding:0 6px}.toast.error{border-color:var(--bad)}.run-reports{flex-direction:column;gap:6px;margin:6px 0;display:flex}.acts ul{margin:4px 0;padding-left:18px}@media (width<=700px){.project{padding:8px}.columns{grid-auto-columns:85vw}}
|