gcf-board 0.2.16 → 0.2.18
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
CHANGED
|
@@ -52,6 +52,7 @@ It puts **GCF Board** on the desktop (Windows: a shortcut whose server console s
|
|
|
52
52
|
- **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
53
|
- **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`.
|
|
54
54
|
- **Ended**: history (accepted) and graveyard (killed, dropped, won't-fix).
|
|
55
|
+
- **Metrics**: `gcf-harness metrics --json` of the project — how many runs reached submit or close, what they cost, handoffs, plan checks, DoD attempts, guard firings, block reasons — and, with harness 0.9.5, the cost per item over all its runs and per week and day.
|
|
55
56
|
|
|
56
57
|
The page updates by itself: the server watches each project's `gcf/` and `.gcf-session/runs/`.
|
|
57
58
|
|
package/dist/server/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The board's HTTP server: the page, and an API that is a thin layer over `ai-factory gcf`.
|
|
2
|
-
// Reads: projects, the board, a shown file, a run log, the playtest session
|
|
2
|
+
// Reads: projects, the board, a shown file, a run log, the playtest session, the run metrics
|
|
3
|
+
// (`ai-factory gcf-harness metrics`). Writes: a project
|
|
3
4
|
// on the board, a move, a run. Every write is a command of the framework.
|
|
4
5
|
import { randomBytes } from 'node:crypto';
|
|
5
6
|
import { createServer } from 'node:http';
|
|
@@ -90,8 +91,13 @@ async function sendPage(res, web, pathname) {
|
|
|
90
91
|
}
|
|
91
92
|
export function createApp(o) {
|
|
92
93
|
const watchers = new Map();
|
|
94
|
+
let closed = false;
|
|
93
95
|
const watchAll = async () => {
|
|
94
|
-
|
|
96
|
+
const projects = await listProjects();
|
|
97
|
+
// A watcher set up after the server closed would keep the process alive.
|
|
98
|
+
if (closed)
|
|
99
|
+
return;
|
|
100
|
+
for (const p of projects)
|
|
95
101
|
if (!watchers.has(p.name))
|
|
96
102
|
watchers.set(p.name, watchProject(p));
|
|
97
103
|
};
|
|
@@ -180,6 +186,14 @@ export function createApp(o) {
|
|
|
180
186
|
send(res, 200, jsonOf(r, 'gcf playtest session'));
|
|
181
187
|
},
|
|
182
188
|
],
|
|
189
|
+
[
|
|
190
|
+
'GET',
|
|
191
|
+
/^\/api\/p\/([^/]+)\/metrics$/,
|
|
192
|
+
async (_req, res, m) => {
|
|
193
|
+
const r = await cli(await projectOf(m[1]), ['gcf-harness', 'metrics', '--json']);
|
|
194
|
+
send(res, 200, jsonOf(r, 'gcf-harness metrics'));
|
|
195
|
+
},
|
|
196
|
+
],
|
|
183
197
|
[
|
|
184
198
|
// A build runs the project's own command: a write, so a POST with its Origin checked.
|
|
185
199
|
'POST',
|
|
@@ -303,15 +317,18 @@ export function createApp(o) {
|
|
|
303
317
|
const server = createServer((req, res) => {
|
|
304
318
|
handle(req, res).catch((e) => {
|
|
305
319
|
if (res.headersSent)
|
|
306
|
-
|
|
307
|
-
|
|
320
|
+
res.end();
|
|
321
|
+
else
|
|
322
|
+
send(res, e instanceof HttpError ? e.status : 500, { error: e.message });
|
|
308
323
|
});
|
|
309
324
|
});
|
|
310
325
|
// A broken projects.json must not take the server down: the page shows the error instead.
|
|
311
326
|
server.on('listening', () => watchAll().catch((e) => console.error(`gcf-board: ${e.message}`)));
|
|
312
327
|
server.on('close', () => {
|
|
328
|
+
closed = true;
|
|
313
329
|
for (const stop of watchers.values())
|
|
314
330
|
stop();
|
|
331
|
+
watchers.clear();
|
|
315
332
|
});
|
|
316
333
|
return server;
|
|
317
334
|
}
|
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
|
@@ -43,11 +43,13 @@ function globalRoots() {
|
|
|
43
43
|
return roots;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
46
|
+
* The refusal an operator reads: the core throws, and node prints the stack under an
|
|
47
|
+
* `Error: <message>` header, every line of the message (a git failure's cause comes after its
|
|
48
|
+
* first line). Anything else is shown as it came.
|
|
48
49
|
*/
|
|
49
50
|
export function briefError(err) {
|
|
50
|
-
|
|
51
|
+
const m = err.match(/^\w*Error: ([\s\S]*?)\r?\n {4}at /m) ?? err.match(/^\w*Error: (.+)$/m);
|
|
52
|
+
return (m?.[1] ?? err).replace(/\r/g, '').trim();
|
|
51
53
|
}
|
|
52
54
|
/** Run `ai-factory <args>` in a project; a move can wait up to a minute on gcf's main lock. */
|
|
53
55
|
export function runCli(entry, cwd, args, timeoutMs = 120_000) {
|
|
@@ -71,6 +73,8 @@ export function runCli(entry, cwd, args, timeoutMs = 120_000) {
|
|
|
71
73
|
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
72
74
|
else
|
|
73
75
|
child.kill();
|
|
76
|
+
// A grandchild holding the pipes keeps close from firing: the answer does not wait for it.
|
|
77
|
+
setTimeout(() => resolve({ ok: false, code: null, out, err }), 5_000).unref();
|
|
74
78
|
}, timeoutMs);
|
|
75
79
|
child.on('error', (e) => {
|
|
76
80
|
clearTimeout(timer);
|
package/dist/server/runs.js
CHANGED
|
@@ -127,8 +127,9 @@ function schedule(run, at) {
|
|
|
127
127
|
catch (e) {
|
|
128
128
|
// The board's own run of an item takes it out of the schedule first, so this is rare;
|
|
129
129
|
// the queue still goes on.
|
|
130
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
130
131
|
if (s)
|
|
131
|
-
trace(project, 'resume-failed', `${s.run.ids.join(', ')}: ${
|
|
132
|
+
trace(project, 'resume-failed', `${s.run.ids.join(', ')}: ${why}`);
|
|
132
133
|
advance(project);
|
|
133
134
|
}
|
|
134
135
|
broadcast({ type: 'board', project });
|
|
@@ -142,9 +143,7 @@ function schedule(run, at) {
|
|
|
142
143
|
* report.
|
|
143
144
|
*/
|
|
144
145
|
function limited(run, result) {
|
|
145
|
-
const
|
|
146
|
-
?.items;
|
|
147
|
-
const hit = (items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
146
|
+
const hit = (result?.items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
148
147
|
if (!hit.length)
|
|
149
148
|
return '';
|
|
150
149
|
const project = run.p.name;
|
|
@@ -204,6 +203,34 @@ export function stalled(ok, result) {
|
|
|
204
203
|
const items = result?.items ?? [];
|
|
205
204
|
return (items.length > 0 && items.every((i) => /another run works in the main/.test(i.error ?? '')));
|
|
206
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
|
+
}
|
|
207
234
|
function spawnRun(q, mode) {
|
|
208
235
|
const { entry, p, ids } = q;
|
|
209
236
|
const run = { project: p.name, ids, started: new Date().toISOString(), mode };
|
|
@@ -224,13 +251,7 @@ function spawnRun(q, mode) {
|
|
|
224
251
|
// A spawn that fails fires both error and close: one end only.
|
|
225
252
|
if (!active.delete(run))
|
|
226
253
|
return;
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
result = JSON.parse(out);
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
// No report: the error says why.
|
|
233
|
-
}
|
|
254
|
+
const result = harnessReport(out);
|
|
234
255
|
// A run of a project removed meanwhile only reports: it pauses and schedules nothing.
|
|
235
256
|
const kept = !forgotten.has(run);
|
|
236
257
|
const stop = kept && mode === 'main' && queues.get(p.name)?.length && stalled(ok, result);
|
|
@@ -244,8 +265,7 @@ function spawnRun(q, mode) {
|
|
|
244
265
|
(stop ? 'the queue is paused: fix the cause, then resume it on the board' : '');
|
|
245
266
|
if (stop)
|
|
246
267
|
trace(p.name, 'queue-paused', `after ${ids.join(', ')}: ${note}`);
|
|
247
|
-
const
|
|
248
|
-
for (const i of handed ?? [])
|
|
268
|
+
for (const i of result?.items ?? [])
|
|
249
269
|
if (kept && i.handoffs)
|
|
250
270
|
trace(p.name, 'handoff', `${i.id}: ${i.handoffs} context handoff(s) to a fresh session`);
|
|
251
271
|
const brief = err && briefError(err);
|