@onepointfour-packs/flaredeck-agent 0.2.37 → 0.2.38
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/boards.d.ts +72 -0
- package/dist/boards.js +136 -0
- package/dist/boards.js.map +1 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +77 -11
- package/dist/browser.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/environment.d.ts +3 -3
- package/dist/environment.js +46 -9
- package/dist/environment.js.map +1 -1
- package/dist/flaredeck-proxy.d.ts +11 -1
- package/dist/flaredeck-proxy.js +27 -1
- package/dist/flaredeck-proxy.js.map +1 -1
- package/dist/main.js +56 -15
- package/dist/main.js.map +1 -1
- package/dist/picker.d.ts +10 -0
- package/dist/picker.js +104 -0
- package/dist/picker.js.map +1 -0
- package/dist/repopicker.js +8 -96
- package/dist/repopicker.js.map +1 -1
- package/dist/runs.d.ts +9 -2
- package/dist/runs.js +33 -5
- package/dist/runs.js.map +1 -1
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +209 -38
- package/dist/serve.js.map +1 -1
- package/dist/stream.d.ts +2 -0
- package/dist/stream.js.map +1 -1
- package/dist/team.js +7 -1
- package/dist/team.js.map +1 -1
- package/dist/tmux.d.ts +1 -0
- package/dist/tmux.js +47 -0
- package/dist/tmux.js.map +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/workspace.d.ts +15 -1
- package/dist/workspace.js +25 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { ensureTmuxScrollback } from './tmux.js';
|
|
2
4
|
import { START_SCRIPT as START_SCRIPT_NAME, browserBrief, crossWired, environmentBrief, notStartedBrief, startEnvironment, stopEnvironment, stopLeftoverEnvironments } from './environment.js';
|
|
3
5
|
import { GIT_TOOL_NAMES, githubTools, publishBranch } from './github.js';
|
|
4
6
|
import { freshGitHubToken } from './githubauth.js';
|
|
@@ -24,6 +26,7 @@ import { managedPython, usingManagedPython } from './engine-antigravity.js';
|
|
|
24
26
|
import { mkdirSync } from 'node:fs';
|
|
25
27
|
import { join } from 'node:path';
|
|
26
28
|
import { configDir, saveConfig } from './config.js';
|
|
29
|
+
import { boardArg, onMyBoards, outstandingCards } from './boards.js';
|
|
27
30
|
import { browserTools } from './browser.js';
|
|
28
31
|
import { watchForDesperation } from './desperation.js';
|
|
29
32
|
import { note, recallWorktree, rememberWorktree, resumeBrief } from './journal.js';
|
|
@@ -173,7 +176,11 @@ export async function serve(opts) {
|
|
|
173
176
|
if (!['message', 'commented', 'ask'].includes(event.event))
|
|
174
177
|
return null;
|
|
175
178
|
const text = `${event.line ?? ''} ${event.message ?? ''}`;
|
|
176
|
-
|
|
179
|
+
// Prose a person typed, so there is no field to read and a pattern is the only way. The
|
|
180
|
+
// bound was ten characters before the dash, which silently excluded real boards —
|
|
181
|
+
// DOCUMENTREPOSITOR-0022 is seventeen. Where a TOOL answers, the code is read from the
|
|
182
|
+
// fields it prints instead; see outstandingCards.
|
|
183
|
+
const codes = [...new Set(text.match(/\b[A-Z][A-Z0-9]{1,23}-\d{3,}\b/g) ?? [])];
|
|
177
184
|
for (const code of codes) {
|
|
178
185
|
// What the harness wrote down when it made the worktree, first: the directory is named from
|
|
179
186
|
// the card's internal id and the branch from whatever the agent chose, so neither can be
|
|
@@ -228,6 +235,8 @@ export async function serve(opts) {
|
|
|
228
235
|
if (await releaseSharedPushGuard(c.path).catch(() => false))
|
|
229
236
|
log('tool', `restored push on ${c.path} (a worktree's push guard had leaked into it)`);
|
|
230
237
|
}
|
|
238
|
+
// Inside tmux, the history this pane prints is only reachable with tmux's mouse mode on.
|
|
239
|
+
void ensureTmuxScrollback((t) => log('tool', t)).catch(() => { });
|
|
231
240
|
// A restart begins with no environments: the last run's are nobody's now (see the function).
|
|
232
241
|
for (const path of new Set([config.repo, ...inScope.map((c) => c.path)])) {
|
|
233
242
|
void stopLeftoverEnvironments(path, (t) => log('tool', t)).catch(() => { });
|
|
@@ -336,13 +345,22 @@ export async function serve(opts) {
|
|
|
336
345
|
const reader = opts.me ?? opts.deck ?? null;
|
|
337
346
|
const answeredHere = new Map();
|
|
338
347
|
let refreshing = null;
|
|
348
|
+
/** This agent's own id, and (on a lead) the agents that report to it — whose requests it sees. */
|
|
349
|
+
let selfId = '';
|
|
350
|
+
let teamIds = new Set();
|
|
339
351
|
const refreshApprovals = () => {
|
|
340
352
|
if (!reader)
|
|
341
353
|
return Promise.resolve();
|
|
342
354
|
if (refreshing)
|
|
343
355
|
return refreshing;
|
|
344
356
|
refreshing = reader.pendingApprovals().then((all) => {
|
|
345
|
-
|
|
357
|
+
// Its own requests, and on a lead its team's. Every pending request in the workspace used
|
|
358
|
+
// to be listed on every pane, so CASE showed HAL's request as if it were CASE's business.
|
|
359
|
+
// Until this agent knows who it is (the team read failed), nothing is hidden.
|
|
360
|
+
const ours = (a) => !selfId
|
|
361
|
+
|| a.sender_key === selfId
|
|
362
|
+
|| (isLead && (a.review?.by === selfId || (!!a.sender_key && teamIds.has(a.sender_key))));
|
|
363
|
+
const mine = all.filter((a) => a.tool_name === SCRIPT_REQUEST && ours(a));
|
|
346
364
|
state.approvals = mine.map((a) => {
|
|
347
365
|
const lines = (a.input_preview || '').split('\n');
|
|
348
366
|
const preview = lines.find((l) => l.startsWith('-- RAN') || /would_touch/.test(l))?.replace(/^-- /, '').replace(/^harness preview.*?:\s*/, '');
|
|
@@ -457,29 +475,58 @@ export async function serve(opts) {
|
|
|
457
475
|
return null;
|
|
458
476
|
return flagCard({ card: cur.card, board: cur.board, code: cur.code }, reason, kind);
|
|
459
477
|
},
|
|
460
|
-
unflag: async () => {
|
|
461
|
-
const cur = onCard;
|
|
462
|
-
if (!cur)
|
|
463
|
-
return;
|
|
478
|
+
unflag: async (cur) => {
|
|
464
479
|
parked.delete(cur.card);
|
|
465
480
|
await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'clear-needs-human', {
|
|
466
481
|
card_id: cur.code || cur.card,
|
|
467
482
|
...(cur.board ? { board_id: cur.board } : {}),
|
|
468
483
|
}).catch((e) => log('warn', `could not clear needs-human: ${e instanceof Error ? e.message : String(e)}`));
|
|
469
484
|
},
|
|
470
|
-
}, () => leadId, onAsk)
|
|
485
|
+
}, () => leadId, onAsk, (card, text) => resumeParked(card, text))
|
|
471
486
|
: undefined;
|
|
472
487
|
// Answering from this keyboard: the person's own login decides, through the same endpoint the
|
|
473
488
|
// phone uses, so the agent waiting on it sees the verdict as any other.
|
|
474
|
-
|
|
489
|
+
/**
|
|
490
|
+
* The person answering from this pane. Signed in once per machine, from the pane when nobody
|
|
491
|
+
* has: press a/d and it asks. Never the agent's own login — an approval is a person's decision,
|
|
492
|
+
* and the agent has a shell on this box and its own credentials in it.
|
|
493
|
+
*/
|
|
494
|
+
let me = opts.me;
|
|
495
|
+
state.person = !!me;
|
|
496
|
+
const startSignIn = () => {
|
|
497
|
+
if (signingIn) {
|
|
498
|
+
log('event', 'still waiting for the sign-in — paste the address your browser ended on');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!opts.signInMe) {
|
|
502
|
+
log('warn', 'nobody is signed in here to answer with — run: flaredeck-agent login --me');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
signingIn = true;
|
|
506
|
+
log('event', 'Sign in as YOURSELF to answer from this pane (once per machine). Open the address below, sign in, then paste the address your browser ends up on into the box here and press Enter.');
|
|
507
|
+
opts.signInMe((t) => { if (t.trim())
|
|
508
|
+
log('event', t); }, signInReader)
|
|
509
|
+
.then((client) => {
|
|
510
|
+
me = client ?? undefined;
|
|
511
|
+
state.person = !!me;
|
|
512
|
+
log(me ? 'event' : 'warn', me ? 'signed in — a approves, d denies, from this pane' : 'signed in, but that login has no workspace here');
|
|
513
|
+
})
|
|
514
|
+
.catch((e) => log('warn', `sign-in did not finish: ${e instanceof Error ? e.message : String(e)}`))
|
|
515
|
+
.finally(() => { signingIn = false; });
|
|
516
|
+
};
|
|
517
|
+
const decide = opts.signInMe || opts.me
|
|
475
518
|
? (requestId, verdict) => {
|
|
519
|
+
if (!me) {
|
|
520
|
+
startSignIn();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
476
523
|
const row = (state.approvals ?? []).find((a) => a.requestId === requestId);
|
|
477
524
|
if (!row || row.stage !== 'ready')
|
|
478
525
|
return;
|
|
479
526
|
answeredHere.set(requestId, verdict);
|
|
480
527
|
row.stage = 'answered';
|
|
481
528
|
row.verdict = verdict;
|
|
482
|
-
void
|
|
529
|
+
void me.decide(row.commentId, verdict)
|
|
483
530
|
.then(() => log('you', `${verdict === 'allow' ? 'approved' : 'denied'} ${row.name} (${row.agent}, ${row.card}) from here`))
|
|
484
531
|
.catch((e) => { answeredHere.delete(requestId); log('warn', `could not record that: ${e instanceof Error ? e.message : String(e)}`); void refreshApprovals(); });
|
|
485
532
|
}
|
|
@@ -563,6 +610,8 @@ export async function serve(opts) {
|
|
|
563
610
|
if (leadId)
|
|
564
611
|
log('tool', `your lead is ${leadName} — ${leadName} verifies your work before it goes anywhere`);
|
|
565
612
|
isLead = team.you.role === 'lead';
|
|
613
|
+
selfId = team.you.agentId;
|
|
614
|
+
teamIds = new Set(team.agents.filter((a) => a.reportsTo === team.you.agentId).map((a) => a.agentId));
|
|
566
615
|
if (team.you.role === 'lead') {
|
|
567
616
|
teamSet = {
|
|
568
617
|
// After a review the list changes — an endorsed request is now answerable, from THIS
|
|
@@ -587,11 +636,34 @@ export async function serve(opts) {
|
|
|
587
636
|
* Polled rather than pushed, because nothing streams another agent's status — and slowly,
|
|
588
637
|
* since agents check in every five minutes, so a faster poll would show the same row again.
|
|
589
638
|
*/
|
|
639
|
+
/**
|
|
640
|
+
* An idle worker is the lead's problem, and the lead already knows within a minute.
|
|
641
|
+
*
|
|
642
|
+
* It polls its team every sixty seconds to draw the pane, so "CASE idle 4m ago" is sitting on
|
|
643
|
+
* its screen — and it did nothing with it. Dispatch waited for the hourly sweep, or for the
|
|
644
|
+
* worker to say it was free, which for a while it never did. Eleven cards waited in Ready For
|
|
645
|
+
* Dev while two agents sat idle and the lead reported "waiting for work".
|
|
646
|
+
*
|
|
647
|
+
* The poll is already happening, so noticing costs nothing. Rate-limited because an idle
|
|
648
|
+
* worker with genuinely nothing to give it stays idle, and a lead asked about it every minute
|
|
649
|
+
* learns to say "nothing" without looking.
|
|
650
|
+
*/
|
|
651
|
+
const DISPATCH_AGAIN_MS = 10 * 60 * 1000;
|
|
652
|
+
let lastDispatch = 0;
|
|
653
|
+
const workersAreIdle = (team) => myTeam(team).mine.filter((a) => !a.card && a.state !== 'stopped' && !a.stale).map((a) => a.name);
|
|
590
654
|
if (leadOfTeam) {
|
|
591
655
|
const teamTimer = setInterval(() => {
|
|
592
656
|
void (async () => {
|
|
593
657
|
try {
|
|
594
|
-
|
|
658
|
+
const team = await fetchTeam(opts.baseUrl, await opts.auth());
|
|
659
|
+
showTeam(team);
|
|
660
|
+
const free = workersAreIdle(team);
|
|
661
|
+
if (free.length && !working && !queue.length && Date.now() - lastDispatch > DISPATCH_AGAIN_MS) {
|
|
662
|
+
lastDispatch = Date.now();
|
|
663
|
+
log('event', `${free.join(', ')} ${free.length === 1 ? 'is' : 'are'} idle — looking for work to hand over`);
|
|
664
|
+
if (enqueue({ id: `dispatch-${Date.now()}`, event: 'sweep', line: `${free.join(', ')} have nothing to do` }))
|
|
665
|
+
void drain();
|
|
666
|
+
}
|
|
595
667
|
}
|
|
596
668
|
catch {
|
|
597
669
|
// A team the lead briefly cannot see is not worth a line in the log every minute; the
|
|
@@ -623,12 +695,40 @@ export async function serve(opts) {
|
|
|
623
695
|
if (engine.name !== 'claude')
|
|
624
696
|
log('event', `${engine.name} is answering on this agent`);
|
|
625
697
|
let liveRun = null;
|
|
698
|
+
/**
|
|
699
|
+
* This turn's browser, so it can be closed when the turn ends.
|
|
700
|
+
*
|
|
701
|
+
* A fresh tool set is built per turn, so without this the previous turn's Chromium was both
|
|
702
|
+
* unreachable and still running — a leaked browser tree for every turn that looked at
|
|
703
|
+
* anything, on a process that stays up for days.
|
|
704
|
+
*/
|
|
705
|
+
let turnEyes = null;
|
|
626
706
|
/**
|
|
627
707
|
* Typed with nothing running: a question for the agent, answered now. It goes through the same
|
|
628
708
|
* queue as a card event so it is serialised with real work, but it is local — never acknowledged
|
|
629
709
|
* to the server, never a card — and it opens one long-lived session of its own so a follow-up
|
|
630
710
|
* question remembers the last one.
|
|
631
711
|
*/
|
|
712
|
+
/**
|
|
713
|
+
* A parked card's request was answered: put the card back at the FRONT of the queue, resuming
|
|
714
|
+
* its own session in its own worktree, with what happened. The turn on whatever card is running
|
|
715
|
+
* now finishes first.
|
|
716
|
+
*/
|
|
717
|
+
const resumeParked = (card, text) => {
|
|
718
|
+
const event = {
|
|
719
|
+
id: `run-${card.card}-${Date.now()}`,
|
|
720
|
+
event: 'run-answered',
|
|
721
|
+
line: text,
|
|
722
|
+
message: text,
|
|
723
|
+
cardKey: card.card,
|
|
724
|
+
...(card.board ? { boardKey: card.board } : {}),
|
|
725
|
+
...(card.code ? { cardCode: card.code } : {}),
|
|
726
|
+
};
|
|
727
|
+
log('event', `${card.code || card.card}: request answered — back at the front of the queue`);
|
|
728
|
+
if (enqueue(event, true)) {
|
|
729
|
+
void drain();
|
|
730
|
+
}
|
|
731
|
+
};
|
|
632
732
|
const ask = (text) => {
|
|
633
733
|
const event = { id: `ask-${Date.now()}`, event: 'ask', line: text, message: text };
|
|
634
734
|
if (enqueue(event)) {
|
|
@@ -643,6 +743,9 @@ export async function serve(opts) {
|
|
|
643
743
|
* blocking call here would freeze the pane and the agent's own event stream for as long as the
|
|
644
744
|
* command took. The agent never sees it; say on the card or in a steer if it should know.
|
|
645
745
|
*/
|
|
746
|
+
/** A person signing in from the pane: lines typed meanwhile go to the sign-in, not the agent. */
|
|
747
|
+
let signingIn = false;
|
|
748
|
+
const signInReader = Object.assign(new EventEmitter(), { prompt() { }, setPrompt() { }, close() { } });
|
|
646
749
|
/** The checkout the current turn is working in ('' for one that has none). */
|
|
647
750
|
let turnCwd = '';
|
|
648
751
|
/** Worktrees whose environment comes down unless a turn returns to them first. */
|
|
@@ -690,6 +793,11 @@ export async function serve(opts) {
|
|
|
690
793
|
});
|
|
691
794
|
};
|
|
692
795
|
const steer = (text) => {
|
|
796
|
+
// Mid sign-in, the next line is the redirect address — for the sign-in, never the agent.
|
|
797
|
+
if (signingIn) {
|
|
798
|
+
signInReader.emit('line', text);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
693
801
|
if (text.startsWith('!')) {
|
|
694
802
|
const command = text.slice(1).trim();
|
|
695
803
|
if (command)
|
|
@@ -760,6 +868,11 @@ export async function serve(opts) {
|
|
|
760
868
|
const pending = new Set();
|
|
761
869
|
/** Keep the pushed status in step with the queue — it is what a lead's pane reads. */
|
|
762
870
|
const syncQueue = () => status.setQueue(queue.length, queue[0]?.cardCode || queue[0]?.cardKey || '');
|
|
871
|
+
/**
|
|
872
|
+
* The boards this agent works, by key. Empty means all of them, which is what every install
|
|
873
|
+
* did before this existed.
|
|
874
|
+
*/
|
|
875
|
+
const myBoards = new Set((config.boards ?? []).map((b) => b.key));
|
|
763
876
|
const enqueue = (event, front = false) => {
|
|
764
877
|
if (pending.has(event.id))
|
|
765
878
|
return false;
|
|
@@ -785,6 +898,8 @@ export async function serve(opts) {
|
|
|
785
898
|
const wantsIntake = (event) => !!event.cardKey
|
|
786
899
|
&& event.event !== 'review'
|
|
787
900
|
&& event.event !== 'ask'
|
|
901
|
+
// A parked card coming back is the same work resuming, not a new card to brief.
|
|
902
|
+
&& event.event !== 'run-answered'
|
|
788
903
|
&& !(event.event === 'commented' && event.cardDone && !event.mentioned)
|
|
789
904
|
// Someone else's card is not worth reading ahead. `drain` already declines to WORK these —
|
|
790
905
|
// a card this agent once touched keeps sending it every move and comment made by whoever
|
|
@@ -909,10 +1024,19 @@ export async function serve(opts) {
|
|
|
909
1024
|
return;
|
|
910
1025
|
let mine = [];
|
|
911
1026
|
try {
|
|
912
|
-
|
|
1027
|
+
// Scoped, like every other account-wide question. This one is asked by the HARNESS rather
|
|
1028
|
+
// than by the model, so the proxy that narrows the model's calls never saw it — and a
|
|
1029
|
+
// board-scoped agent went straight back to picking up another team's cards: KD-1361, then
|
|
1030
|
+
// KD-1362, from a board nobody had ticked. The card then 404s, because it is not on a
|
|
1031
|
+
// board this agent works.
|
|
1032
|
+
const out = await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'get-outstanding', { ...(boardArg([...myBoards]) !== undefined ? { board: boardArg([...myBoards]) } : {}) });
|
|
913
1033
|
const text = typeof out === 'string' ? out : JSON.stringify(out);
|
|
914
|
-
//
|
|
915
|
-
|
|
1034
|
+
// Read from the fields the answer prints, and filtered here as well as asked for — the
|
|
1035
|
+
// server is deployed separately from this, so the scope cannot depend on its version.
|
|
1036
|
+
// In the order it names them: priority order, which is the board's.
|
|
1037
|
+
mine = outstandingCards(text)
|
|
1038
|
+
.filter((c) => onMyBoards(myBoards, c))
|
|
1039
|
+
.map((c) => ({ code: c.code }));
|
|
916
1040
|
}
|
|
917
1041
|
catch {
|
|
918
1042
|
return; // unreachable board is the stream's problem to report, not this timer's
|
|
@@ -1028,7 +1152,7 @@ export async function serve(opts) {
|
|
|
1028
1152
|
status.finishCard(didWork);
|
|
1029
1153
|
void report();
|
|
1030
1154
|
pending.delete(event.id);
|
|
1031
|
-
if (event.event === 'ask' || event.event === 'sweep') { /* local: nothing to acknowledge, nothing to retry */ }
|
|
1155
|
+
if (event.event === 'ask' || event.event === 'sweep' || event.event === 'run-answered') { /* local: nothing to acknowledge, nothing to retry */ }
|
|
1032
1156
|
else if (didWork)
|
|
1033
1157
|
await stream.ack([event.id]);
|
|
1034
1158
|
else {
|
|
@@ -1049,6 +1173,13 @@ export async function serve(opts) {
|
|
|
1049
1173
|
// soon — and then taken down, so a day of cards does not leave a day of servers running.
|
|
1050
1174
|
if (turnCwd && turnCwd !== config.repo)
|
|
1051
1175
|
scheduleEnvironmentStop(turnCwd);
|
|
1176
|
+
// The environment is kept warm; the browser is not. It holds a Chromium tree, the next
|
|
1177
|
+
// turn builds its own, and a page left open is not something a later turn can reach.
|
|
1178
|
+
if (turnEyes) {
|
|
1179
|
+
const e = turnEyes;
|
|
1180
|
+
turnEyes = null;
|
|
1181
|
+
await e.close().catch(() => { });
|
|
1182
|
+
}
|
|
1052
1183
|
current = '';
|
|
1053
1184
|
delete state.plan;
|
|
1054
1185
|
state.working = '';
|
|
@@ -1062,9 +1193,15 @@ export async function serve(opts) {
|
|
|
1062
1193
|
finally {
|
|
1063
1194
|
working = false;
|
|
1064
1195
|
}
|
|
1065
|
-
//
|
|
1066
|
-
// once, rather than leaving them to notice.
|
|
1067
|
-
|
|
1196
|
+
// An empty queue at the end of a drain means nothing is running and nothing is waiting: this
|
|
1197
|
+
// agent has nothing to do. Tell its lead, once, rather than leaving them to notice.
|
|
1198
|
+
//
|
|
1199
|
+
// This used to require `!live.size` as well, which reads like "nothing running" and is not.
|
|
1200
|
+
// `live` holds sessions kept WARM for fifteen minutes so a follow-up on the same card costs
|
|
1201
|
+
// a resume instead of a re-orientation — a cache, not work. So every agent went quiet for
|
|
1202
|
+
// fifteen minutes after finishing anything, which is exactly the window in which it most
|
|
1203
|
+
// needs a new card. Two sat idle and silent while eleven cards waited.
|
|
1204
|
+
if (!queue.length)
|
|
1068
1205
|
void offerToLead();
|
|
1069
1206
|
}
|
|
1070
1207
|
/**
|
|
@@ -1416,7 +1553,7 @@ export async function serve(opts) {
|
|
|
1416
1553
|
tag = code;
|
|
1417
1554
|
state.working = code;
|
|
1418
1555
|
};
|
|
1419
|
-
const flaredeck = flaredeckProxy(config.mcpServerName, config.mcpUrl, opts.auth, check.discovered, (w) => log('warn', w), finishGate, noteCard, keepTool, savedSoFar);
|
|
1556
|
+
const flaredeck = flaredeckProxy(config.mcpServerName, config.mcpUrl, opts.auth, check.discovered, (w) => log('warn', w), finishGate, noteCard, keepTool, savedSoFar, [...myBoards]);
|
|
1420
1557
|
const held = live.get(key);
|
|
1421
1558
|
if (held?.timer)
|
|
1422
1559
|
clearTimeout(held.timer);
|
|
@@ -1500,6 +1637,20 @@ export async function serve(opts) {
|
|
|
1500
1637
|
// The guard is built once at startup; what this agent can reach changes per card. Handing it
|
|
1501
1638
|
// the live picture is the difference between judging "$DATABASE_URL" as a scratch database
|
|
1502
1639
|
// and judging it as a live one.
|
|
1640
|
+
// Deployed addresses the board declared for this card's repository — the dev or UAT site. A
|
|
1641
|
+
// card is usually about code that is already live somewhere, and an agent that can only see
|
|
1642
|
+
// what it started locally cannot tell its own change from how the thing already behaved.
|
|
1643
|
+
const declaredSites = where.kind === 'repo' ? where.sites ?? {} : {};
|
|
1644
|
+
const eyes = forACard
|
|
1645
|
+
? turnEyes = browserTools({
|
|
1646
|
+
repo: config.repo,
|
|
1647
|
+
endpoints: () => env.endpoints,
|
|
1648
|
+
sites: () => declaredSites,
|
|
1649
|
+
contract: () => env.contract,
|
|
1650
|
+
cwd,
|
|
1651
|
+
onNote: (t) => log('tool', t),
|
|
1652
|
+
})
|
|
1653
|
+
: null;
|
|
1503
1654
|
reachable = { cwd, endpoints: env.endpoints };
|
|
1504
1655
|
if (forACard && !needsRunning)
|
|
1505
1656
|
log('tool', 'environment not started — this card does not look like it needs one');
|
|
@@ -1535,19 +1686,12 @@ export async function serve(opts) {
|
|
|
1535
1686
|
...(prepared?.size ? { size: prepared.size } : {}),
|
|
1536
1687
|
tools: {
|
|
1537
1688
|
...(job ? { git: { server: gitServer, toolNames: GIT_TOOL_NAMES } } : {}),
|
|
1538
|
-
// Eyes,
|
|
1539
|
-
//
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
endpoints: () => env.endpoints,
|
|
1545
|
-
contract: () => env.contract,
|
|
1546
|
-
cwd,
|
|
1547
|
-
onNote: (t) => log('tool', t),
|
|
1548
|
-
}),
|
|
1549
|
-
}
|
|
1550
|
-
: {}),
|
|
1689
|
+
// Eyes, for any card. They used to be mounted only when the harness had itself started
|
|
1690
|
+
// something, which left an agent working alone unable to get them: no start script meant
|
|
1691
|
+
// no browser AND no mention of one, so it could not know that writing the script it is
|
|
1692
|
+
// perfectly able to write was the thing standing between it and seeing its own change.
|
|
1693
|
+
// The endpoints are read through a getter, so something the agent starts mid-turn counts.
|
|
1694
|
+
...(eyes ? { browser: eyes } : {}),
|
|
1551
1695
|
team: teamSet,
|
|
1552
1696
|
flaredeck,
|
|
1553
1697
|
runs: runsSet,
|
|
@@ -1568,7 +1712,7 @@ export async function serve(opts) {
|
|
|
1568
1712
|
freeWorkerBrief(event) +
|
|
1569
1713
|
// Only tell an agent about the environment when the card actually needs one running.
|
|
1570
1714
|
environmentBrief(env, cwd, needsRunning) + notStartedBrief(forACard && !needsRunning, cwd) +
|
|
1571
|
-
browserBrief(env.
|
|
1715
|
+
browserBrief(env.endpoints, declaredSites, forACard) +
|
|
1572
1716
|
(wrongEnv.length
|
|
1573
1717
|
? `\n\nSTOP — the environment that started is not yours:\n${wrongEnv.map((w) => ` · ${w}`).join('\n')}\n\n` +
|
|
1574
1718
|
`Anything you test against it tells you about another card's code, and it will answer ` +
|
|
@@ -1832,11 +1976,26 @@ export async function serve(opts) {
|
|
|
1832
1976
|
beat();
|
|
1833
1977
|
continue;
|
|
1834
1978
|
}
|
|
1979
|
+
// Another board's work. Acknowledged so it does not come round again, and never read: no
|
|
1980
|
+
// intake, no Jev call, no turn. One line, so it is visible that scope is doing something
|
|
1981
|
+
// rather than that the agent has gone deaf.
|
|
1982
|
+
if (!onMyBoards(myBoards, event)) {
|
|
1983
|
+
log('tool', `not this agent's board (${event.boardName || event.boardKey}) — ${event.line}`);
|
|
1984
|
+
await stream.ack([event.id]);
|
|
1985
|
+
beat();
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1835
1988
|
// A review request for this lead: the model handles it as a turn, but the list can show
|
|
1836
1989
|
// the row straight away.
|
|
1837
1990
|
if (event.event === 'review')
|
|
1838
1991
|
void refreshApprovals();
|
|
1839
|
-
|
|
1992
|
+
// A review jumps the queue. It has a deadline and a worker blocked on it; behind a backlog
|
|
1993
|
+
// it waited for every card ahead of it. HAL's request sat behind seven events on a freshly
|
|
1994
|
+
// restarted JARVIS, listed in the pane as "with lead" and never looked at, while the hour
|
|
1995
|
+
// ran down. Front, not interrupt: the turn already running finishes first.
|
|
1996
|
+
if (enqueue(event, event.event === 'review')) {
|
|
1997
|
+
if (event.event === 'review')
|
|
1998
|
+
log('event', `review waiting — taken next: ${event.line}`);
|
|
1840
1999
|
prepareAhead(event);
|
|
1841
2000
|
void drain();
|
|
1842
2001
|
}
|
|
@@ -1900,6 +2059,16 @@ function prompt(event, isLead = false) {
|
|
|
1900
2059
|
'assign-card, newest bugs first. One each — an agent already on a card is refused a',
|
|
1901
2060
|
'second, and it messages you the moment it is free.',
|
|
1902
2061
|
'',
|
|
2062
|
+
'Then look at the balance. list_agents shows what each one holds and how many are',
|
|
2063
|
+
'queued behind it. Where one worker has several cards waiting and another has none,',
|
|
2064
|
+
'move a card across with assign-card: a card queued behind a busy agent is not being',
|
|
2065
|
+
'worked, and the idle one could finish it today.',
|
|
2066
|
+
'',
|
|
2067
|
+
'Each agent is shown with the repository it has checked out. Move a card only to an',
|
|
2068
|
+
'agent that has the repository the card is for — on a board where everyone works the',
|
|
2069
|
+
'same one they are interchangeable, and on a board with a ui agent and an api agent',
|
|
2070
|
+
'they are not.',
|
|
2071
|
+
'',
|
|
1903
2072
|
]
|
|
1904
2073
|
: []),
|
|
1905
2074
|
'If it returns nothing, say so in one line and stop. That is a perfectly good outcome and',
|
|
@@ -1910,11 +2079,13 @@ function prompt(event, isLead = false) {
|
|
|
1910
2079
|
'',
|
|
1911
2080
|
'- A card for one of the agents who report to you: assign it to the right one. That dispatch',
|
|
1912
2081
|
' IS the work, and this turn is done.',
|
|
1913
|
-
'- A card that is yours and NOT already in progress:
|
|
1914
|
-
' with
|
|
1915
|
-
'
|
|
1916
|
-
'
|
|
1917
|
-
'
|
|
2082
|
+
'- A card that is yours and NOT already in progress: hand it to a worker if you have one free,',
|
|
2083
|
+
' with assign-card. You run one turn at a time, and a card takes minutes — for all of them you',
|
|
2084
|
+
' would be unable to assign anything or answer anyone, which is the one job only you can do.',
|
|
2085
|
+
' With nobody free, move it to the board\'s in-progress lane with update-card-status and stop:',
|
|
2086
|
+
' that hands it back to you as a card of its own, with a checkout and a branch, which is the',
|
|
2087
|
+
' only place work can be done and published from. Either way, stop here — reading it, starting',
|
|
2088
|
+
' it or commenting on it in this turn all edit the shared checkout, which publishes nowhere.',
|
|
1918
2089
|
'- A card already in progress: say which one in a line, and stop. The harness picks those up',
|
|
1919
2090
|
' by itself, shortly, with a checkout of its own — you do not need to do anything for it.',
|
|
1920
2091
|
'',
|