gcf-board 0.2.19 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/server/app.js +41 -11
- package/dist/server/moves.js +51 -94
- package/dist/server/runs.js +9 -8
- package/dist/shared/board.js +1 -9
- package/dist/web/assets/index-DCkdSzDJ.css +1 -0
- package/dist/web/assets/index-EnODwFU4.js +34 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CgUcGPkn.js +0 -33
- package/dist/web/assets/index-CjG8ddKQ.css +0 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A local web board for game projects on [GCF](../gcf-aif-ext): the items by statu
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js ≥ 22.18
|
|
8
|
-
- [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.
|
|
8
|
+
- [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.10 (`gcf board --json` with `schema: 3`: the columns, the moves, the item's fields, the blocks and the Help terms come from it as data)
|
|
9
9
|
- for runs: [`gcf-harness-aif-ext`](../gcf-harness-aif-ext) ≥ 0.8 and Claude Code (`claude -p`)
|
|
10
10
|
- gcf-board 0.2 needs `gcf-aif-ext` ≥ 0.7 (priority, questions, session marks, `run_mode`)
|
|
11
11
|
|
|
@@ -30,8 +30,10 @@ It puts **GCF Board** on the desktop (Windows: a shortcut whose server console s
|
|
|
30
30
|
|
|
31
31
|
## What it does
|
|
32
32
|
|
|
33
|
+
The board knows no domain by name: the project's board read gives its columns, moves with their forms, item fields, blocks and Help terms (`domain`, Board-PRD decision 16). For a GCF project:
|
|
34
|
+
|
|
33
35
|
- **Board**: columns ready, running, blocked, awaiting-verdict, awaiting-decision, unplayed, parked. A card shows the type, the gate, the plan progress (`2/5`), the severity, `verified`, the model. A running card shows when its run log was last written (`active 12 s ago`), and `quiet 6 min` when the log has been silent for 5 minutes or more. A run's end report gives each item's cost when the agent reported it.
|
|
34
|
-
- **An item**: its file, its plan and its run log as it grows, and the moves the core offers for it (`items[].moves`, Methodology §4.1): verdict, decide, block, unblock, drop, close, submit.
|
|
36
|
+
- **An item**: its file, its plan and its run log as it grows, its fields, and the moves the core offers for it (`items[].moves`, Methodology §4.1): run, verdict, decide, block, unblock, drop, close, submit, depends. Each move's form comes from the core; the core still checks the rest itself, and the board shows its answer.
|
|
35
37
|
- **Drag**:
|
|
36
38
|
- a ready card into **running** starts its run;
|
|
37
39
|
- a blocked or parked card into **ready** unblocks it;
|
|
@@ -47,7 +49,9 @@ It puts **GCF Board** on the desktop (Windows: a shortcut whose server console s
|
|
|
47
49
|
- **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.
|
|
48
50
|
- **Priority**: drop a ready card on another to put it before; the column's ranks are committed (`gcf prioritize`).
|
|
49
51
|
- **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.
|
|
50
|
-
- **New item**: `gcf item new … --commit
|
|
52
|
+
- **New item**: `gcf item new … --commit`, its form from the core: a bug asks its severity, a release its version and channel. The item's `model` and `effort` go to its run.
|
|
53
|
+
- **Help** (**?**): the domain's terms, columns, moves and field values with what each means, and the words of runs, under one filter.
|
|
54
|
+
- **Settings**: the keys of `gcf/config.yml` and the agents' language (`language.ui`, `language.artifacts` of `.ai-factory/config.yaml`), each a `gcf config set` commit.
|
|
51
55
|
- **Inbox**: add, erase, or turn an entry into an item. Open questions are listed there too.
|
|
52
56
|
- **Lessons** (core 0.7.10, harness 0.9.2): what got in an agent's way, asked at the end of each session, lands in the inbox with its kind (`tool`, `rules`, `framework`, `template`, `other`) and its item. The inbox filters them; a card shows `N lessons`, and the drawer lists the item's lessons with the inbox's buttons. A run's report line ends with `, N lessons`, and with `— code written outside aif-implement` when a session skipped that skill.
|
|
53
57
|
- **Playtest**: `gcf playtest session` with the checklist grouped by subsystem; keep, tune or kill each item from the list. **Build** runs `dod.build`. A note goes to the inbox as `--source playtest`.
|
package/dist/server/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The board's HTTP server: the page, and an API that is a thin layer over `ai-factory gcf`.
|
|
2
2
|
// Reads: projects, the board, a shown file, a run log, the playtest session, the run metrics
|
|
3
|
-
// (`ai-factory
|
|
4
|
-
//
|
|
3
|
+
// (`ai-factory <harness> metrics`, the domain's harness). Writes: a project on the board, a
|
|
4
|
+
// move, a run. Every write is a command of the framework.
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { existsSync, statSync } from 'node:fs';
|
|
@@ -13,7 +13,7 @@ import { briefError, runCli } from './cli.js';
|
|
|
13
13
|
import { broadcast, planRev, subscribe, watchProject } from './events.js';
|
|
14
14
|
import { followLog, readShown, shownShape } from './files.js';
|
|
15
15
|
import { HttpError } from './http-error.js';
|
|
16
|
-
import { moveArgs, runIds } from './moves.js';
|
|
16
|
+
import { isCenterMove, moveArgs, runIds } from './moves.js';
|
|
17
17
|
import { addProject, listProjects, projectOf, removeProject } from './registry.js';
|
|
18
18
|
import { cancelResume, closeReport, forgetProject, resumeQueue, runsOf, startRun, unqueue, } from './runs.js';
|
|
19
19
|
const send = (res, status, body) => {
|
|
@@ -102,12 +102,32 @@ export function createApp(o) {
|
|
|
102
102
|
watchers.set(p.name, watchProject(p));
|
|
103
103
|
};
|
|
104
104
|
const cli = (p, args, timeout) => runCli(o.entry, p.path, args, timeout);
|
|
105
|
+
// The last domain each project's board read gave: an item move is built from its form.
|
|
106
|
+
const domains = new Map();
|
|
107
|
+
/**
|
|
108
|
+
* The harness of a project's domain, or a 400: its agent's runs need one. A fresh read: the
|
|
109
|
+
* harness may have been installed or removed since the last one; a read that fails (a schema
|
|
110
|
+
* mismatch mid-run) falls back on the last domain, so a Stop still stops.
|
|
111
|
+
*/
|
|
112
|
+
const harnessOf = async (p) => {
|
|
113
|
+
const domain = await boardOf(p).then((b) => b.domain, (e) => {
|
|
114
|
+
const last = domains.get(p.name);
|
|
115
|
+
if (!last)
|
|
116
|
+
throw e;
|
|
117
|
+
return last;
|
|
118
|
+
});
|
|
119
|
+
const h = domain.harness;
|
|
120
|
+
if (!h)
|
|
121
|
+
throw new HttpError(400, 'this project has no harness: its items run from a console');
|
|
122
|
+
return h;
|
|
123
|
+
};
|
|
105
124
|
/** `gcf board --json` of a project, refused when its schema is not the one this board reads. */
|
|
106
125
|
const boardOf = async (p) => {
|
|
107
126
|
const board = jsonOf(await cli(p, ['gcf', 'board', '--json']), 'gcf board');
|
|
108
127
|
if (board.schema !== BOARD_SCHEMA)
|
|
109
128
|
throw new HttpError(502, `gcf board gives schema ${board.schema ?? 'none'}, this board reads ${BOARD_SCHEMA}: ` +
|
|
110
129
|
'update gcf-aif-ext and gcf-board together');
|
|
130
|
+
domains.set(p.name, board.domain);
|
|
111
131
|
return board;
|
|
112
132
|
};
|
|
113
133
|
const routes = [
|
|
@@ -132,6 +152,7 @@ export function createApp(o) {
|
|
|
132
152
|
async (_req, res, m) => {
|
|
133
153
|
await removeProject(m[1]);
|
|
134
154
|
forgetProject(m[1]);
|
|
155
|
+
domains.delete(m[1]);
|
|
135
156
|
watchers.get(m[1])?.();
|
|
136
157
|
watchers.delete(m[1]);
|
|
137
158
|
send(res, 200, { ok: true });
|
|
@@ -190,8 +211,9 @@ export function createApp(o) {
|
|
|
190
211
|
'GET',
|
|
191
212
|
/^\/api\/p\/([^/]+)\/metrics$/,
|
|
192
213
|
async (_req, res, m) => {
|
|
193
|
-
const
|
|
194
|
-
|
|
214
|
+
const p = await projectOf(m[1]);
|
|
215
|
+
const harness = await harnessOf(p);
|
|
216
|
+
send(res, 200, jsonOf(await cli(p, [harness, 'metrics', '--json']), `${harness} metrics`));
|
|
195
217
|
},
|
|
196
218
|
],
|
|
197
219
|
[
|
|
@@ -210,7 +232,11 @@ export function createApp(o) {
|
|
|
210
232
|
/^\/api\/p\/([^/]+)\/move$/,
|
|
211
233
|
async (req, res, m) => {
|
|
212
234
|
const p = await projectOf(m[1]);
|
|
213
|
-
const
|
|
235
|
+
const body = await bodyOf(req);
|
|
236
|
+
const domain = isCenterMove(body.cmd)
|
|
237
|
+
? undefined
|
|
238
|
+
: (domains.get(p.name) ?? (await boardOf(p)).domain);
|
|
239
|
+
const r = await cli(p, moveArgs(body, domain?.moves));
|
|
214
240
|
broadcast({ type: 'board', project: p.name });
|
|
215
241
|
send(res, 200, { ok: r.ok, out: r.out.trim(), err: briefError(r.err) });
|
|
216
242
|
},
|
|
@@ -226,8 +252,11 @@ export function createApp(o) {
|
|
|
226
252
|
async (req, res, m) => {
|
|
227
253
|
const p = await projectOf(m[1]);
|
|
228
254
|
const body = await bodyOf(req);
|
|
229
|
-
const mode =
|
|
230
|
-
|
|
255
|
+
const { run_mode: mode, domain } = await boardOf(p);
|
|
256
|
+
if (!domain.harness)
|
|
257
|
+
throw new HttpError(400, 'this project has no harness: its items run from a console');
|
|
258
|
+
const ids = runIds(body);
|
|
259
|
+
send(res, 202, startRun(o.entry, p, ids, mode, body.fresh === true, domain.harness));
|
|
231
260
|
},
|
|
232
261
|
],
|
|
233
262
|
[
|
|
@@ -236,8 +265,9 @@ export function createApp(o) {
|
|
|
236
265
|
/^\/api\/p\/([^/]+)\/next$/,
|
|
237
266
|
async (_req, res, m, url) => {
|
|
238
267
|
const count = String(Math.max(1, Math.min(20, Number(url.searchParams.get('count')) || 1)));
|
|
239
|
-
const
|
|
240
|
-
|
|
268
|
+
const p = await projectOf(m[1]);
|
|
269
|
+
const harness = await harnessOf(p);
|
|
270
|
+
send(res, 200, jsonOf(await cli(p, [harness, 'next', count, '--json']), `${harness} next`));
|
|
241
271
|
},
|
|
242
272
|
],
|
|
243
273
|
[
|
|
@@ -246,7 +276,7 @@ export function createApp(o) {
|
|
|
246
276
|
async (req, res, m) => {
|
|
247
277
|
const p = await projectOf(m[1]);
|
|
248
278
|
const [id] = runIds({ ids: [(await bodyOf(req)).id] });
|
|
249
|
-
const r = await cli(p, [
|
|
279
|
+
const r = await cli(p, [await harnessOf(p), 'stop', id]);
|
|
250
280
|
broadcast({ type: 'board', project: p.name });
|
|
251
281
|
send(res, 200, { ok: r.ok, out: r.out.trim(), err: briefError(r.err) });
|
|
252
282
|
},
|
package/dist/server/moves.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// The moves a board may run, each one `ai-factory gcf` command (Methodology §4.1, §11). Anything
|
|
2
|
-
// else is refused: the board is a view, the commands own every rule. An operator's text goes
|
|
3
|
-
// as one `--flag=value` argument, so no word of it reads as a flag.
|
|
4
1
|
import { HttpError } from './http-error.js';
|
|
5
2
|
const SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
6
3
|
function id(b, key = 'id') {
|
|
@@ -23,23 +20,17 @@ function required(b, key) {
|
|
|
23
20
|
throw new HttpError(400, `${key} is required`);
|
|
24
21
|
return v;
|
|
25
22
|
}
|
|
26
|
-
function oneOf(b, key, allowed) {
|
|
27
|
-
const v = b[key];
|
|
28
|
-
if (typeof v !== 'string' || !allowed.includes(v))
|
|
29
|
-
throw new HttpError(400, `${key} must be ${allowed.join(' | ')}`);
|
|
30
|
-
return v;
|
|
31
|
-
}
|
|
32
23
|
const opt = (b, key, flag = key) => {
|
|
33
24
|
const v = text(b, key);
|
|
34
25
|
return v === undefined ? [] : [`--${flag}=${v}`];
|
|
35
26
|
};
|
|
36
|
-
const many = (b, key) => {
|
|
27
|
+
const many = (b, key, flag = key) => {
|
|
37
28
|
const v = b[key];
|
|
38
29
|
if (v === undefined)
|
|
39
30
|
return [];
|
|
40
31
|
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
|
|
41
32
|
throw new HttpError(400, `${key} must be a list of text`);
|
|
42
|
-
return v.filter(Boolean).map((x) => `--${
|
|
33
|
+
return v.filter(Boolean).map((x) => `--${flag}=${x}`);
|
|
43
34
|
};
|
|
44
35
|
// The core limits a tag to 40 characters too.
|
|
45
36
|
const TAG = /^[a-z0-9][a-z0-9:-]{0,39}$/;
|
|
@@ -52,88 +43,13 @@ const list = (b, key, re, what, flag = key) => {
|
|
|
52
43
|
throw new HttpError(400, `${key} must be a list of ${what}`);
|
|
53
44
|
return v.length ? [`--${flag}=${v.join(',')}`] : [];
|
|
54
45
|
};
|
|
55
|
-
const idList = (b, key) => list(b, key, SLUG, 'ids');
|
|
46
|
+
const idList = (b, key, flag = key) => list(b, key, SLUG, 'ids', flag);
|
|
56
47
|
const tagList = (b, key, flag = key) => list(b, key, TAG, 'tags', flag);
|
|
57
48
|
/** `--remove` / `--no-remove` when the operator chose; the command's default otherwise. */
|
|
58
|
-
const remove = (b) => b
|
|
59
|
-
const NEW_ITEM_OPTIONS = [
|
|
60
|
-
'gate',
|
|
61
|
-
'title',
|
|
62
|
-
'parent',
|
|
63
|
-
'system',
|
|
64
|
-
'depends',
|
|
65
|
-
'severity',
|
|
66
|
-
'blocked',
|
|
67
|
-
'from',
|
|
68
|
-
'reopens',
|
|
69
|
-
'milestone',
|
|
70
|
-
'release',
|
|
71
|
-
'channel',
|
|
72
|
-
'model',
|
|
73
|
-
'effort',
|
|
74
|
-
];
|
|
49
|
+
const remove = (b, key = 'remove', flag = key) => b[key] === true ? [`--${flag}`] : b[key] === false ? [`--no-${flag}`] : [];
|
|
75
50
|
const MOVES = {
|
|
76
|
-
submit: (b) => ['submit', id(b), ...opt(b, 'unverified')],
|
|
77
|
-
close: (b) => ['close', id(b), ...many(b, 'constraint'), ...opt(b, 'unverified')],
|
|
78
|
-
verdict: (b) => [
|
|
79
|
-
'verdict',
|
|
80
|
-
id(b),
|
|
81
|
-
oneOf(b, 'verdict', ['keep', 'tune', 'kill']),
|
|
82
|
-
...opt(b, 'note'),
|
|
83
|
-
...opt(b, 'tags'),
|
|
84
|
-
...many(b, 'constraint'),
|
|
85
|
-
...remove(b),
|
|
86
|
-
],
|
|
87
|
-
decide: (b) => [
|
|
88
|
-
'decide',
|
|
89
|
-
id(b),
|
|
90
|
-
oneOf(b, 'choice', ['go', 'no', 'wontfix', 'known', 'close', 'continue', 'kill']),
|
|
91
|
-
...opt(b, 'reason'),
|
|
92
|
-
...remove(b),
|
|
93
|
-
],
|
|
94
|
-
block: (b) => ['block', id(b), `--reason=${required(b, 'reason')}`],
|
|
95
|
-
unblock: (b) => ['unblock', id(b), ...opt(b, 'note')],
|
|
96
51
|
// A session's mark taken off: the item no longer shows running (Board-PRD §3.2, kind c).
|
|
97
52
|
release: (b) => ['release', id(b)],
|
|
98
|
-
drop: (b) => [
|
|
99
|
-
'drop',
|
|
100
|
-
id(b),
|
|
101
|
-
`--reason=${required(b, 'reason')}`,
|
|
102
|
-
...opt(b, 'tags'),
|
|
103
|
-
...remove(b),
|
|
104
|
-
],
|
|
105
|
-
'item-new': (b) => [
|
|
106
|
-
'item',
|
|
107
|
-
'new',
|
|
108
|
-
id(b),
|
|
109
|
-
`--type=${required(b, 'type')}`,
|
|
110
|
-
...NEW_ITEM_OPTIONS.flatMap((k) => opt(b, k)),
|
|
111
|
-
'--commit',
|
|
112
|
-
],
|
|
113
|
-
// `--` does not hold through ai-factory's nested commands: the text goes as one `--text=`.
|
|
114
|
-
'inbox-add': (b) => [
|
|
115
|
-
'inbox',
|
|
116
|
-
'add',
|
|
117
|
-
...opt(b, 'source'),
|
|
118
|
-
...tagList(b, 'tags'),
|
|
119
|
-
'--commit',
|
|
120
|
-
`--text=${required(b, 'text')}`,
|
|
121
|
-
],
|
|
122
|
-
'inbox-drop': (b) => ['inbox', 'drop', id(b), '--commit'],
|
|
123
|
-
'inbox-tag': (b) => [
|
|
124
|
-
'inbox',
|
|
125
|
-
'tag',
|
|
126
|
-
id(b),
|
|
127
|
-
...tagList(b, 'add'),
|
|
128
|
-
...tagList(b, 'remove'),
|
|
129
|
-
'--commit',
|
|
130
|
-
],
|
|
131
|
-
'phase-next': (b) => ['phase', 'next', ...opt(b, 'reason')],
|
|
132
|
-
// A column's new order after a drag: ranks 1, 2, 3… in one commit.
|
|
133
|
-
prioritize: (b) => ['prioritize', ...runIds(b)],
|
|
134
|
-
// The ids an item waits for, added and removed; the core refuses an unknown id and a cycle.
|
|
135
|
-
depends: (b) => ['depends', id(b), ...idList(b, 'add'), ...idList(b, 'remove')],
|
|
136
|
-
'question-answer': (b) => ['question', 'answer', id(b), `--text=${required(b, 'text')}`],
|
|
137
53
|
// One setting of gcf/config.yml; the core checks the key and the value. An empty value takes
|
|
138
54
|
// a number back to its default.
|
|
139
55
|
'config-set': (b) => {
|
|
@@ -156,12 +72,53 @@ const MOVES = {
|
|
|
156
72
|
return ['permissions', ...args];
|
|
157
73
|
},
|
|
158
74
|
};
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
75
|
+
/** Whether a field goes: one with `when` only while the field it names holds one of its values. */
|
|
76
|
+
const applies = (f, b) => !f.when || f.when.is.includes(String(b[f.when.key] ?? ''));
|
|
77
|
+
/** A choice: one of its options' values, or nothing. */
|
|
78
|
+
function choice(f, b, flag) {
|
|
79
|
+
const v = f.required ? required(b, f.key) : text(b, f.key);
|
|
80
|
+
if (v === undefined)
|
|
81
|
+
return [];
|
|
82
|
+
if (!(f.options ?? []).some((o) => o.value === v))
|
|
83
|
+
throw new HttpError(400, `${f.key} must be one of ${(f.options ?? []).map((o) => o.value).join(', ')}`);
|
|
84
|
+
return [`--${flag}=${v}`];
|
|
85
|
+
}
|
|
86
|
+
/** One field's arguments; a `slug` is the target itself (`{id}`), no flag. */
|
|
87
|
+
function fieldArgs(f, b) {
|
|
88
|
+
const flag = f.flag ?? f.key;
|
|
89
|
+
if (f.kind === 'slug')
|
|
90
|
+
return [];
|
|
91
|
+
if (f.kind === 'list')
|
|
92
|
+
return many(b, f.key, flag);
|
|
93
|
+
if (f.kind === 'remove')
|
|
94
|
+
return remove(b, f.key, flag);
|
|
95
|
+
if (f.kind === 'ids')
|
|
96
|
+
return idList(b, f.key, flag);
|
|
97
|
+
if (f.kind === 'tags')
|
|
98
|
+
return tagList(b, f.key, flag);
|
|
99
|
+
if (f.kind === 'choice')
|
|
100
|
+
return choice(f, b, flag);
|
|
101
|
+
return f.required ? [`--${flag}=${required(b, f.key)}`] : opt(b, f.key, flag);
|
|
102
|
+
}
|
|
103
|
+
/** A move by its domain form: its words with the target, then each field that goes. */
|
|
104
|
+
function domainMoveArgs(spec, b) {
|
|
105
|
+
const words = spec.args.flatMap((w) => w === '{id}' ? [id(b)] : w === '{ids}' ? runIds(b) : [w]);
|
|
106
|
+
return [...words, ...spec.fields.filter((f) => applies(f, b)).flatMap((f) => fieldArgs(f, b))];
|
|
107
|
+
}
|
|
108
|
+
/** Whether a move is the center's own: its arguments need no domain. */
|
|
109
|
+
export const isCenterMove = (cmd) => typeof cmd === 'string' && Object.hasOwn(MOVES, cmd);
|
|
110
|
+
/**
|
|
111
|
+
* The `ai-factory` arguments of a move, or a 400 naming what is wrong: a center move, or one of
|
|
112
|
+
* `moves`, the domain's forms from the project's board read. An agent's move is a run, not a
|
|
113
|
+
* command: it goes through the run route.
|
|
114
|
+
*/
|
|
115
|
+
export function moveArgs(b, moves = {}) {
|
|
116
|
+
const cmd = typeof b.cmd === 'string' ? b.cmd : '';
|
|
117
|
+
if (isCenterMove(cmd))
|
|
118
|
+
return ['gcf', ...MOVES[cmd](b)];
|
|
119
|
+
if (Object.hasOwn(moves, cmd) && moves[cmd].kind !== 'agent')
|
|
120
|
+
return domainMoveArgs(moves[cmd], b);
|
|
121
|
+
throw new HttpError(400, `unknown move: ${String(b.cmd)}`);
|
|
165
122
|
}
|
|
166
123
|
/** The ids of a run: one slug each, at least one. */
|
|
167
124
|
export function runIds(b) {
|
package/dist/server/runs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Runs the board started: `ai-factory
|
|
1
|
+
// Runs the board started: `ai-factory <harness> run <ids> --json` (GCF: gcf-harness), one process per batch, held
|
|
2
2
|
// by the server until every agent session ends. A run started from a console shows on the
|
|
3
3
|
// board too (its lock or session mark makes the item `running`); only its report is not here.
|
|
4
4
|
// In `run_mode: main` the checkout takes one run at a time: a run asked for meanwhile waits in
|
|
@@ -119,7 +119,7 @@ function schedule(run, at) {
|
|
|
119
119
|
liftLimitPause(project);
|
|
120
120
|
try {
|
|
121
121
|
if (s) {
|
|
122
|
-
const r = startRun(s.run.entry, s.run.p, s.run.ids, s.run.mode);
|
|
122
|
+
const r = startRun(s.run.entry, s.run.p, s.run.ids, s.run.mode, false, s.run.harness);
|
|
123
123
|
const how = 'queued' in r ? 'wait in the queue' : 'run again';
|
|
124
124
|
trace(project, 'resume', `the usage limit reset: ${s.run.ids.join(', ')} ${how}`);
|
|
125
125
|
}
|
|
@@ -232,10 +232,10 @@ function isItem(i) {
|
|
|
232
232
|
return typeof id === 'string' && (resets == null || typeof resets === 'string');
|
|
233
233
|
}
|
|
234
234
|
function spawnRun(q, mode) {
|
|
235
|
-
const { entry, p, ids } = q;
|
|
235
|
+
const { entry, p, ids, harness } = q;
|
|
236
236
|
const run = { project: p.name, ids, started: new Date().toISOString(), mode };
|
|
237
237
|
active.add(run);
|
|
238
|
-
const args = [entry,
|
|
238
|
+
const args = [entry, harness, 'run', ...ids, '--json', ...(q.fresh ? ['--fresh'] : [])];
|
|
239
239
|
const child = spawn(process.execPath, args, {
|
|
240
240
|
cwd: p.path,
|
|
241
241
|
shell: false,
|
|
@@ -293,9 +293,10 @@ function spawnRun(q, mode) {
|
|
|
293
293
|
/**
|
|
294
294
|
* Start a batch in the project's run mode, or queue it behind the main run going on. Its end
|
|
295
295
|
* is a run report with the harness report, or what went wrong. `fresh` starts new agent
|
|
296
|
-
* sessions where the harness would resume the interrupted ones.
|
|
296
|
+
* sessions where the harness would resume the interrupted ones. `harness` is the domain's
|
|
297
|
+
* (`domain.harness`); the tests take GCF's.
|
|
297
298
|
*/
|
|
298
|
-
export function startRun(entry, p, ids, mode = 'worktree', fresh = false) {
|
|
299
|
+
export function startRun(entry, p, ids, mode = 'worktree', fresh = false, harness = 'gcf-harness') {
|
|
299
300
|
// A second click on the same card: the harness would refuse it on its lock, noisily.
|
|
300
301
|
const { active: going, queued } = runsOf(p.name);
|
|
301
302
|
const busy = ids.filter((id) => queued.includes(id) || going.some((r) => r.ids.includes(id)));
|
|
@@ -314,9 +315,9 @@ export function startRun(entry, p, ids, mode = 'worktree', fresh = false) {
|
|
|
314
315
|
}
|
|
315
316
|
// A paused queue holds a new main run too: it would repeat the failure, and jump the queue.
|
|
316
317
|
if (mode === 'main' && (paused.has(p.name) || going.some((r) => r.mode === 'main'))) {
|
|
317
|
-
queues.set(p.name, [...(queues.get(p.name) ?? []), { entry, p, ids, mode, fresh }]);
|
|
318
|
+
queues.set(p.name, [...(queues.get(p.name) ?? []), { entry, p, ids, mode, fresh, harness }]);
|
|
318
319
|
broadcast({ type: 'board', project: p.name });
|
|
319
320
|
return { queued: ids };
|
|
320
321
|
}
|
|
321
|
-
return { started: spawnRun({ entry, p, ids, mode, fresh }, mode) };
|
|
322
|
+
return { started: spawnRun({ entry, p, ids, mode, fresh, harness }, mode) };
|
|
322
323
|
}
|
package/dist/shared/board.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
// The read model of `ai-factory gcf board --json` (Methodology §11): the board refuses a schema
|
|
2
2
|
// it does not know. Types only, plus the version this board reads.
|
|
3
|
-
export const BOARD_SCHEMA =
|
|
4
|
-
export const STATUSES = [
|
|
5
|
-
'ready',
|
|
6
|
-
'blocked',
|
|
7
|
-
'awaiting-verdict',
|
|
8
|
-
'awaiting-decision',
|
|
9
|
-
'unplayed',
|
|
10
|
-
'parked',
|
|
11
|
-
];
|
|
3
|
+
export const BOARD_SCHEMA = 3;
|
|
@@ -0,0 +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 .field{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,.card .field.waits_for{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}.card .severity-critical.severity,.card .severity-high.severity{color:var(--bad)}.long{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 .fact{display:contents}.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}}.dialog.help{max-height:80vh;overflow-y:auto}.dialog.help dt{margin-top:6px;font-weight:600}.dialog.help dd{color:var(--muted);margin:0 0 0 12px}
|