@onepointfour-packs/flaredeck-agent 0.2.39 → 0.2.40
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/dist/hydrate.d.ts +38 -0
- package/dist/hydrate.js +3 -1
- package/dist/hydrate.js.map +1 -1
- package/dist/main.js +68 -89
- package/dist/main.js.map +1 -1
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +23 -0
- package/dist/providers.js.map +1 -1
- package/dist/serve.js +112 -8
- package/dist/serve.js.map +1 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -347,6 +347,14 @@ export async function serve(opts) {
|
|
|
347
347
|
*/
|
|
348
348
|
const localMcp = [...(config.mcpServers ?? [])];
|
|
349
349
|
let lastMcpNote = '';
|
|
350
|
+
/**
|
|
351
|
+
* The workspace's GitHub connection, from FlareDeck — current at every session, refreshed there
|
|
352
|
+
* with the OAuth App's secret this public package cannot hold. When set it wins over the
|
|
353
|
+
* machine's own github.json; when absent the machine's login works exactly as before.
|
|
354
|
+
*/
|
|
355
|
+
let flaredeckGithub = null;
|
|
356
|
+
/** The command guard's config, so a rotated OpenRouter key reaches it (see refreshMcp). */
|
|
357
|
+
let jevCfg = null;
|
|
350
358
|
const refreshMcp = async () => {
|
|
351
359
|
try {
|
|
352
360
|
const { hydrateMcpServers, mcpConfigFetcher } = await import('./hydrate.js');
|
|
@@ -359,6 +367,16 @@ export async function serve(opts) {
|
|
|
359
367
|
lastMcpNote = hydrated.note;
|
|
360
368
|
log('tool', hydrated.note);
|
|
361
369
|
}
|
|
370
|
+
const gh = hydrated.github ?? null;
|
|
371
|
+
if (!!gh !== !!flaredeckGithub || gh?.login !== flaredeckGithub?.login) {
|
|
372
|
+
log('tool', gh ? `GitHub: using FlareDeck's connection (@${gh.login ?? 'unknown'})` : 'GitHub: this machine\'s own login');
|
|
373
|
+
}
|
|
374
|
+
flaredeckGithub = gh;
|
|
375
|
+
// The workspace's model keys: Jev's OpenRouter key and Gemini, kept current every session.
|
|
376
|
+
const { setProvidersFromFlaredeck } = await import('./providers.js');
|
|
377
|
+
setProvidersFromFlaredeck(hydrated.providers);
|
|
378
|
+
if (jevCfg)
|
|
379
|
+
jevCfg.apiKey = loadProviders().openrouter?.apiKey ?? jevCfg.apiKey;
|
|
362
380
|
return hydrated.missingRequired ?? null;
|
|
363
381
|
}
|
|
364
382
|
catch (e) {
|
|
@@ -568,7 +586,8 @@ export async function serve(opts) {
|
|
|
568
586
|
* Its refusals and its asks both end up in decisions.jsonl beside the config.
|
|
569
587
|
*/
|
|
570
588
|
const jevKey = loadProviders().openrouter?.apiKey;
|
|
571
|
-
|
|
589
|
+
// Kept as an object so a key rotated in FlareDeck reaches the guard at the next session.
|
|
590
|
+
const guard = jevGuard(jevCfg = {
|
|
572
591
|
apiKey: jevKey ?? '',
|
|
573
592
|
decider: (req, opt) => decider(req, opt),
|
|
574
593
|
log: (t) => log('tool', t),
|
|
@@ -844,16 +863,23 @@ export async function serve(opts) {
|
|
|
844
863
|
let readyReported = false;
|
|
845
864
|
/** Walls the agent cannot climb, checked in code — never by asking the model. */
|
|
846
865
|
const walls = async () => {
|
|
847
|
-
|
|
848
|
-
|
|
866
|
+
/* FlareDeck's token is checked from a LOCAL, never put into config.github: saveConfig writes
|
|
867
|
+
config.github to github.json, and the hourly OAuth refresh calls saveConfig — so FlareDeck's
|
|
868
|
+
token would have been copied onto this machine's disk, and survived Disconnect there. */
|
|
869
|
+
if (!flaredeckGithub)
|
|
870
|
+
config.github = loadGitHub() ?? config.github;
|
|
871
|
+
const gh = flaredeckGithub ? { accessToken: flaredeckGithub.token } : config.github;
|
|
872
|
+
const login = await canPublish(gh).catch(() => null);
|
|
849
873
|
if (login)
|
|
850
|
-
return { check: 'github', reason:
|
|
851
|
-
|
|
874
|
+
return { check: 'github', reason: flaredeckGithub
|
|
875
|
+
? `GitHub refuses FlareDeck's connection (@${flaredeckGithub.login ?? 'unknown'}), so nothing I write can be published. Reconnect GitHub on Integrations.`
|
|
876
|
+
: `GitHub refuses this machine's login, so nothing I write can be published. Somebody signs it in again on this machine: \`flaredeck-agent login --github-token ghp_…\` (a classic token with repo and workflow scope does not expire).` };
|
|
877
|
+
if (gh?.accessToken) {
|
|
852
878
|
for (const c of [{ path: config.repo }, ...(await checkouts(config.repo, config.repos))]) {
|
|
853
879
|
const remote = await new Promise((resolve) => {
|
|
854
880
|
execFile('git', ['remote', 'get-url', 'origin'], { cwd: c.path }, (err, out) => resolve(err ? '' : String(out).trim()));
|
|
855
881
|
});
|
|
856
|
-
const why = remote ? await canPushTo(
|
|
882
|
+
const why = remote ? await canPushTo(gh.accessToken, remote) : null;
|
|
857
883
|
if (why)
|
|
858
884
|
return { check: 'github', reason: `${why} Nothing I write in ${c.path} could be published. Fix the access, or sign this machine in with an account that has it.` };
|
|
859
885
|
}
|
|
@@ -1035,6 +1061,14 @@ export async function serve(opts) {
|
|
|
1035
1061
|
/** Worktrees whose environment comes down unless a turn returns to them first. */
|
|
1036
1062
|
const idleStops = new Map();
|
|
1037
1063
|
const ENV_IDLE_MS = 20 * 60_000;
|
|
1064
|
+
/**
|
|
1065
|
+
* Checkouts with a sign-in still waiting for a person, and since when. Their environment is
|
|
1066
|
+
* kept up: the sign-in browser is open ON it, and stopping it left that page retrying a dead
|
|
1067
|
+
* server — hundreds of ERR_CONNECTION_REFUSED lines — and a sign-in nobody could finish.
|
|
1068
|
+
* Bounded, so a sign-in that never settles cannot keep an environment up for ever.
|
|
1069
|
+
*/
|
|
1070
|
+
const signInPending = new Map();
|
|
1071
|
+
const SIGN_IN_HOLD_MS = 2 * 60 * 60_000;
|
|
1038
1072
|
const scheduleEnvironmentStop = (cwd) => {
|
|
1039
1073
|
const before = idleStops.get(cwd);
|
|
1040
1074
|
if (before)
|
|
@@ -1044,6 +1078,11 @@ export async function serve(opts) {
|
|
|
1044
1078
|
// Never the checkout a turn is in right now, even if the timer and the turn cross.
|
|
1045
1079
|
if (cwd === turnCwd && state.working)
|
|
1046
1080
|
return;
|
|
1081
|
+
const waiting = signInPending.get(cwd);
|
|
1082
|
+
if (waiting && Date.now() - waiting < SIGN_IN_HOLD_MS) {
|
|
1083
|
+
scheduleEnvironmentStop(cwd);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1047
1086
|
void stopEnvironment(cwd).then((ok) => { if (ok)
|
|
1048
1087
|
log('tool', `environment stopped in ${cwd.split('/').pop()} (idle 20m)`); }).catch(() => { });
|
|
1049
1088
|
}, ENV_IDLE_MS));
|
|
@@ -1393,6 +1432,15 @@ export async function serve(opts) {
|
|
|
1393
1432
|
* harness reads it and queues the card itself, as the ordinary card event it should always
|
|
1394
1433
|
* have been — with a worktree, a branch, and everything else a card turn gets.
|
|
1395
1434
|
*/
|
|
1435
|
+
/**
|
|
1436
|
+
* Pickups per card since anything real happened on it. A card whose work is done but which was
|
|
1437
|
+
* never moved out of the in-progress lane came back every sweep and every restart, each time a
|
|
1438
|
+
* turn that found "nothing new to act on" — and it blocked every other card. After two such
|
|
1439
|
+
* pickups the harness leaves it and says so; any real event on the card (a comment, a move, an
|
|
1440
|
+
* assignment) clears the count.
|
|
1441
|
+
*/
|
|
1442
|
+
const resumedIdle = new Map();
|
|
1443
|
+
const RESUME_IDLE_LIMIT = 2;
|
|
1396
1444
|
const resumeOwnWork = async () => {
|
|
1397
1445
|
/* Not `live.size`: a session kept WARM for a follow-up is a cache, not work, and blocking on
|
|
1398
1446
|
it meant the card in progress was never resumed within fifteen minutes of any turn. */
|
|
@@ -1426,7 +1474,13 @@ export async function serve(opts) {
|
|
|
1426
1474
|
for (const c of mine) {
|
|
1427
1475
|
if (sessions.has(c.code) || live.has(c.code))
|
|
1428
1476
|
continue;
|
|
1477
|
+
if ((resumedIdle.get(c.code) ?? 0) >= RESUME_IDLE_LIMIT)
|
|
1478
|
+
continue;
|
|
1429
1479
|
const card = await opts.deck?.card(c.code).catch(() => null);
|
|
1480
|
+
// Waiting on a person: nothing to do until they answer, and their answer is an event that
|
|
1481
|
+
// wakes this agent anyway. Reopening it only spends a turn finding that out again.
|
|
1482
|
+
if (card?.needs_human)
|
|
1483
|
+
continue;
|
|
1430
1484
|
if (String(card?.lane?.type ?? '').toLowerCase() === 'inprogress') {
|
|
1431
1485
|
next = c;
|
|
1432
1486
|
break;
|
|
@@ -1434,6 +1488,11 @@ export async function serve(opts) {
|
|
|
1434
1488
|
}
|
|
1435
1489
|
if (!next)
|
|
1436
1490
|
return false;
|
|
1491
|
+
const n = (resumedIdle.get(next.code) ?? 0) + 1;
|
|
1492
|
+
resumedIdle.set(next.code, n);
|
|
1493
|
+
if (n === RESUME_IDLE_LIMIT) {
|
|
1494
|
+
log('warn', `${next.code}: picked up ${n} times with nothing new on it — after this one I leave it. Move it out of in-progress, or comment on it, to change that.`);
|
|
1495
|
+
}
|
|
1437
1496
|
log('tool', `picking ${next.code} back up — it is yours and already in progress`);
|
|
1438
1497
|
if (enqueue({
|
|
1439
1498
|
id: `resume-${next.code}-${Date.now()}`,
|
|
@@ -1526,6 +1585,10 @@ export async function serve(opts) {
|
|
|
1526
1585
|
if (event.event !== 'ask')
|
|
1527
1586
|
log('event', event.line);
|
|
1528
1587
|
// A sweep or a typed question is work, but it is not a card — see startCard.
|
|
1588
|
+
// Something real happened on this card — not the harness's own pickup — so it may have
|
|
1589
|
+
// work again: clear the idle-pickup count that stops it being resumed.
|
|
1590
|
+
if (event.cardCode && !String(event.id ?? '').startsWith('resume-'))
|
|
1591
|
+
resumedIdle.delete(event.cardCode);
|
|
1529
1592
|
status.startCard(current, !!event.cardKey);
|
|
1530
1593
|
void report();
|
|
1531
1594
|
let didWork = true;
|
|
@@ -2115,9 +2178,18 @@ export async function serve(opts) {
|
|
|
2115
2178
|
base: startFrom ?? (await baseBranch(cwd)),
|
|
2116
2179
|
card: event.cardCode || key,
|
|
2117
2180
|
onAuthFailure: (why) => {
|
|
2118
|
-
void enterPause({ check: 'github', reason:
|
|
2181
|
+
void enterPause({ check: 'github', reason: flaredeckGithub
|
|
2182
|
+
? `GitHub refused FlareDeck's connection (@${flaredeckGithub.login ?? 'unknown'}) when I pushed (${why.slice(0, 160)}), so nothing I write can be published. Reconnect GitHub on Integrations, and check that the account can push to this repository (an org may need to approve the OAuth App).`
|
|
2183
|
+
: `GitHub refused this machine's login when I pushed (${why.slice(0, 160)}), so nothing I write can be published. Somebody signs it in again on this machine: \`flaredeck-agent login --github-token ghp_…\` (a classic token with repo and workflow scope does not expire).` });
|
|
2119
2184
|
},
|
|
2120
2185
|
token: async () => {
|
|
2186
|
+
// FlareDeck's connection first, asked again at every push: it refreshes the token
|
|
2187
|
+
// centrally, so an agent running all day never pushes with an expired one.
|
|
2188
|
+
if (flaredeckGithub) {
|
|
2189
|
+
await refreshMcp();
|
|
2190
|
+
if (flaredeckGithub)
|
|
2191
|
+
return flaredeckGithub.token;
|
|
2192
|
+
}
|
|
2121
2193
|
// Read fresh at every push: the login is the machine's, so signing in again from
|
|
2122
2194
|
// any pane fixes an agent that has been running all day with the old one.
|
|
2123
2195
|
config.github = loadGitHub() ?? config.github;
|
|
@@ -2245,6 +2317,25 @@ export async function serve(opts) {
|
|
|
2245
2317
|
...(card.board ? { board: card.board } : {}),
|
|
2246
2318
|
...(card.code ? { code: card.code } : {}),
|
|
2247
2319
|
};
|
|
2320
|
+
// Held up while the person has not signed in yet — see signInPending.
|
|
2321
|
+
const signCwd = turnCwd;
|
|
2322
|
+
if (signCwd)
|
|
2323
|
+
signInPending.set(signCwd, Date.now());
|
|
2324
|
+
/* The page's console, forwarded — but a page retrying something gone repeats one
|
|
2325
|
+
line hundreds of times. The first is logged; repeats are counted and summarised. */
|
|
2326
|
+
let lastNote = '';
|
|
2327
|
+
let repeats = 0;
|
|
2328
|
+
const pageNote = (t) => {
|
|
2329
|
+
if (t === lastNote) {
|
|
2330
|
+
repeats++;
|
|
2331
|
+
if (repeats === 3 || repeats % 50 === 0)
|
|
2332
|
+
log('tool', `sign-in: (the same line ${repeats + 1}× so far) ${t.slice(0, 120)}`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
lastNote = t;
|
|
2336
|
+
repeats = 0;
|
|
2337
|
+
log('tool', `sign-in: ${t}`);
|
|
2338
|
+
};
|
|
2248
2339
|
void signInSession({
|
|
2249
2340
|
url,
|
|
2250
2341
|
launch: () => chromium.launch(HEADLESS_FULL),
|
|
@@ -2257,9 +2348,11 @@ export async function serve(opts) {
|
|
|
2257
2348
|
`session is kept for the whole project, so this is once, not once per card.`, 'sign_in', undefined, [{ url: viewer, label: 'Sign in' }]);
|
|
2258
2349
|
settle({ asked: true, where: viewer });
|
|
2259
2350
|
},
|
|
2260
|
-
onNote:
|
|
2351
|
+
onNote: pageNote,
|
|
2261
2352
|
})
|
|
2262
2353
|
.then(async (r) => {
|
|
2354
|
+
if (signCwd)
|
|
2355
|
+
signInPending.delete(signCwd);
|
|
2263
2356
|
if (!r.ok || !r.session) {
|
|
2264
2357
|
log('warn', `sign-in for "${part}" did not finish: ${r.why ?? 'nothing was saved'}`);
|
|
2265
2358
|
/* Say so ON THE CARD. The flag already raised carries a link to a viewer
|
|
@@ -2979,10 +3072,21 @@ function prompt(event, isLead = false) {
|
|
|
2979
3072
|
'dispatch, re-assign, or change anything else — this is a review, not a card to work.',
|
|
2980
3073
|
].join('\n');
|
|
2981
3074
|
}
|
|
3075
|
+
// A card the HARNESS picked back up because it sits in progress with this agent's name on it.
|
|
3076
|
+
// Finished work left there comes back on every check and blocks every other card.
|
|
3077
|
+
const resumed = String(event.id ?? '').startsWith('resume-')
|
|
3078
|
+
? ['', 'You were handed this card again only because it is still in an in-progress lane assigned to',
|
|
3079
|
+
'you. If its work is already finished — answered, delivered, nothing left for you — move it on to',
|
|
3080
|
+
'its next lane with update-card-status NOW. Left in progress, it is handed back to you on every',
|
|
3081
|
+
'check and FlareDeck will not let you start any other card. If it is waiting on a PERSON and you',
|
|
3082
|
+
'may not move it yourself, call set-needs-human saying what they need to do — a card waiting on a',
|
|
3083
|
+
'person is not handed back to you, and their answer wakes you.']
|
|
3084
|
+
: [];
|
|
2982
3085
|
return [
|
|
2983
3086
|
`A FlareDeck event just arrived for you:`,
|
|
2984
3087
|
'',
|
|
2985
3088
|
event.line,
|
|
3089
|
+
...resumed,
|
|
2986
3090
|
'',
|
|
2987
3091
|
'Deal with it. Read the card before you change anything — INCLUDING its agent notes, where a',
|
|
2988
3092
|
'previous session may have left you a handover: what it got to, what it tried that failed,',
|