@onepointfour-packs/flaredeck-agent 0.2.112 → 0.2.114
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/agent.js +3 -1
- package/dist/agent.js.map +1 -1
- package/dist/assistant.d.ts +48 -0
- package/dist/assistant.js +215 -0
- package/dist/assistant.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js.map +1 -1
- package/dist/engine-claude.js +1 -1
- package/dist/engine-claude.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js.map +1 -1
- package/dist/serve.d.ts +11 -0
- package/dist/serve.js +116 -9
- package/dist/serve.js.map +1 -1
- package/dist/stream.d.ts +2 -0
- package/dist/stream.js.map +1 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -8,6 +8,7 @@ import { freshGitHubToken } from './githubauth.js';
|
|
|
8
8
|
import { answerFrom, createPresence, heldFor, modeCommand } from './presence.js';
|
|
9
9
|
import { defaultUploadRoots, fileTools, flaredeckStores } from './filestore.js';
|
|
10
10
|
import { scratchFor } from './cardmess.js';
|
|
11
|
+
import { ASSISTANT_KEY, claudeProjectDir, historyMessage, recentExchanges, latestCliSession, assistantBrief, assistantIgnores, assistantPerson, asAssistantEvent, loadAssistantSession, saveAssistantSession } from './assistant.js';
|
|
11
12
|
import { codePrefix, loadPrefixes, savePrefixes, SCRATCH_SWEEP_EVERY_MS, sweepScratch } from './scratch.js';
|
|
12
13
|
import { findImages, namedForAgent } from './images.js';
|
|
13
14
|
import { loadBaseBranch, loadMergeConfig, preflight, preflightBrief } from './preflight.js';
|
|
@@ -35,8 +36,8 @@ import { isCheckout, newerHarness, readyOnDisk, restartDecision, updateSelf } fr
|
|
|
35
36
|
import { cloneBoardRepos, isEmptyFolder, parseBoardRepos } from './bootstrap-repo.js';
|
|
36
37
|
import { updateSidecarSdk } from './updater-python.js';
|
|
37
38
|
import { managedPython, usingManagedPython } from './engine-antigravity.js';
|
|
38
|
-
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
39
|
-
import { join } from 'node:path';
|
|
39
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import { dirname, join } from 'node:path';
|
|
40
41
|
import { configDir, loadGitHub, saveConfig, slug } from './config.js';
|
|
41
42
|
import { boardArg, onMyBoards, outstandingCards } from './boards.js';
|
|
42
43
|
import { browserTools, ensureBrowser } from './browser.js';
|
|
@@ -858,6 +859,13 @@ export async function serve(opts) {
|
|
|
858
859
|
let isLead = false;
|
|
859
860
|
/** A manager: above the leads, with no lead of its own. See MANAGER_BRIEF. */
|
|
860
861
|
let isManager = false;
|
|
862
|
+
/**
|
|
863
|
+
* One person's own agent: on no team, never dispatched to, listening only to `person`. One long
|
|
864
|
+
* conversation, never handed over — the SDK compacts it — and loose rules. See ASSISTANT_BRIEF.
|
|
865
|
+
*/
|
|
866
|
+
let isAssistant = false;
|
|
867
|
+
let person = null;
|
|
868
|
+
const assistantSessionFile = join(configDir(), 'assistant-sessions', `${slug(`${opts.agent ?? 'agent'}-${config.repo}`)}.json`);
|
|
861
869
|
/** The mode and the held questions, as the pane draws them. */
|
|
862
870
|
const showPresence = () => {
|
|
863
871
|
if (!isManager) {
|
|
@@ -869,7 +877,7 @@ export async function serve(opts) {
|
|
|
869
877
|
const until = presence.deadline();
|
|
870
878
|
state.asks = presence.held().map((a) => ({ card: a.code || a.card, question: a.question, options: a.options, person: a.person, ...(until ? { until } : {}) }));
|
|
871
879
|
};
|
|
872
|
-
const keepTool = (name) => offeredTo(isManager ? 'manager' : isLead ? 'lead' : 'worker', name);
|
|
880
|
+
const keepTool = (name) => offeredTo(isManager || isAssistant ? 'manager' : isLead ? 'lead' : 'worker', name);
|
|
873
881
|
/** Cards already handed to the lead, so a retried tool call does not assign or message twice. */
|
|
874
882
|
const handedBack = new Set();
|
|
875
883
|
try {
|
|
@@ -898,6 +906,15 @@ export async function serve(opts) {
|
|
|
898
906
|
// The role exists in FlareDeck before the harness has a manager mode (docs/spec-manager-role.md
|
|
899
907
|
// §14). Said plainly, so nobody reads a manager account running here as a manager at work.
|
|
900
908
|
isManager = team.you.role === 'manager';
|
|
909
|
+
isAssistant = team.you.role === 'assistant';
|
|
910
|
+
if (isAssistant) {
|
|
911
|
+
const tenant = tenantFromToken(await opts.auth());
|
|
912
|
+
person = tenant ? await assistantPerson(config.mcpUrl, tenant, () => opts.auth()) : null;
|
|
913
|
+
const kept = loadAssistantSession(assistantSessionFile);
|
|
914
|
+
log('event', person
|
|
915
|
+
? `You are ${person.name}'s assistant — you listen only to them, in FlareDeck chat${kept ? ', carrying on your conversation from before' : ''}.`
|
|
916
|
+
: 'You are an assistant, but nobody is set as the person you work for — set "Works for" on this agent in FlareDeck. Until then nothing reaches you.');
|
|
917
|
+
}
|
|
901
918
|
showPresence();
|
|
902
919
|
if (isManager) {
|
|
903
920
|
log('event', 'You are a manager here — list-agents shows the teams that report to you and the machines they run on; machine-exec reaches those machines.');
|
|
@@ -1452,6 +1469,8 @@ export async function serve(opts) {
|
|
|
1452
1469
|
files: fileTools(stores, source, () => onCard ? { card: onCard.card, ...(onCard.code ? { code: onCard.code } : {}), ...(envKey ? { environment: envKey } : {}) } : null, () => defaultUploadRoots(cwd, forACard ? scratchFor(cwd) : undefined), (t) => log('tool', t)),
|
|
1453
1470
|
};
|
|
1454
1471
|
};
|
|
1472
|
+
/** The subscription each open session last ran on — a fallback for its handover. */
|
|
1473
|
+
const planOfSession = new Map();
|
|
1455
1474
|
/** One session per card, so an agent picking a card back up remembers starting it. */
|
|
1456
1475
|
const sessions = new Map();
|
|
1457
1476
|
/**
|
|
@@ -1696,7 +1715,7 @@ export async function serve(opts) {
|
|
|
1696
1715
|
await enterPause(wall);
|
|
1697
1716
|
})();
|
|
1698
1717
|
const sweepNow = () => {
|
|
1699
|
-
if (paused || !sweepsEnabled || stopping)
|
|
1718
|
+
if (paused || !sweepsEnabled || stopping || isAssistant)
|
|
1700
1719
|
return;
|
|
1701
1720
|
// Busy is reason enough to wait: the next tick is minutes away and the work is not going
|
|
1702
1721
|
// anywhere.
|
|
@@ -1738,6 +1757,8 @@ export async function serve(opts) {
|
|
|
1738
1757
|
const resumedIdle = new Map();
|
|
1739
1758
|
const RESUME_IDLE_LIMIT = 2;
|
|
1740
1759
|
const resumeOwnWork = async () => {
|
|
1760
|
+
if (isAssistant)
|
|
1761
|
+
return false; // an assistant has no "own work" — only its person's asks
|
|
1741
1762
|
/* Not `live.size`: a session kept WARM for a follow-up is a cache, not work, and blocking on
|
|
1742
1763
|
it meant the card in progress was never resumed within fifteen minutes of any turn. */
|
|
1743
1764
|
if (paused || !sweepsEnabled || stopping || working || queue.length)
|
|
@@ -2332,11 +2353,26 @@ export async function serve(opts) {
|
|
|
2332
2353
|
log('event', `${event.cardCode || event.cardKey}: ${event.actor} is talking to somebody else on the card — nothing to do`);
|
|
2333
2354
|
return;
|
|
2334
2355
|
}
|
|
2356
|
+
// An assistant hears its person and nothing else, and works outside the card machinery.
|
|
2357
|
+
if (isAssistant) {
|
|
2358
|
+
const why = assistantIgnores(event, person?.uid ?? null);
|
|
2359
|
+
if (why) {
|
|
2360
|
+
log('event', `${event.cardCode || event.event}: ${why} — not for ${person?.name ?? 'anyone'}'s assistant`);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
event = asAssistantEvent(event, person?.name ?? 'your person');
|
|
2364
|
+
// The conversation from before a restart, picked up where it was.
|
|
2365
|
+
if (!sessions.has(ASSISTANT_KEY)) {
|
|
2366
|
+
const kept = loadAssistantSession(assistantSessionFile);
|
|
2367
|
+
if (kept)
|
|
2368
|
+
sessions.set(ASSISTANT_KEY, kept);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2335
2371
|
if (isBookkeeping(event, agentNames)) {
|
|
2336
2372
|
log('event', `${event.cardCode || event.cardKey}: ${event.event === 'commented' ? 'a harness\'s own record on the card' : event.event === 'approval' ? 'an approval for a person to give' : event.event === 'updated' ? 'the card\'s record changed' : 'the card was flagged for a person'} — nothing to do`);
|
|
2337
2373
|
return;
|
|
2338
2374
|
}
|
|
2339
|
-
let key = event.cardKey || (event.event === 'ask' ? 'ask' : event.event === 'sweep' ? 'sweep' : event.event === 'scheduled' ? `schedule-${event.scheduleId}` : 'inbox');
|
|
2375
|
+
let key = isAssistant ? ASSISTANT_KEY : event.cardKey || (event.event === 'ask' ? 'ask' : event.event === 'sweep' ? 'sweep' : event.event === 'scheduled' ? `schedule-${event.scheduleId}` : 'inbox');
|
|
2340
2376
|
let turnTokens = 0;
|
|
2341
2377
|
let nudged = false;
|
|
2342
2378
|
// A checkout of its own per card, so it cannot edit a working tree someone else is in.
|
|
@@ -2461,7 +2497,32 @@ export async function serve(opts) {
|
|
|
2461
2497
|
*/
|
|
2462
2498
|
if (event.event === 'ask' && tree)
|
|
2463
2499
|
key = `ask:${tree.path}`;
|
|
2464
|
-
|
|
2500
|
+
// An assistant's very first turn, with no conversation of its own: its person's newest Claude
|
|
2501
|
+
// CLI session in this folder, carried on as a FORK — the history comes across, the person's
|
|
2502
|
+
// own session file is left as it was, and the two never write to the same place.
|
|
2503
|
+
const pickedUp = isAssistant && key === ASSISTANT_KEY && !sessions.get(key) ? latestCliSession(config.repo) : null;
|
|
2504
|
+
if (pickedUp) {
|
|
2505
|
+
log('event', `carrying on from your Claude session ${pickedUp.slice(0, 8)} in this folder (a copy — yours is left as it is)`);
|
|
2506
|
+
// So the person sees, in FlareDeck, where the conversation was: the last thirty exchanges,
|
|
2507
|
+
// once, as one message marked as the past.
|
|
2508
|
+
const posted = `${assistantSessionFile}.history`;
|
|
2509
|
+
if (person && !existsSync(posted)) {
|
|
2510
|
+
const ex = recentExchanges(join(claudeProjectDir(config.repo), `${pickedUp}.jsonl`));
|
|
2511
|
+
if (ex.length) {
|
|
2512
|
+
const name = (opts.agent ?? 'your assistant').replace(/\s*\(.*$/, '');
|
|
2513
|
+
await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'send-message', { to: person.uid, message: historyMessage(ex, name) })
|
|
2514
|
+
.then(() => { try {
|
|
2515
|
+
mkdirSync(dirname(posted), { recursive: true });
|
|
2516
|
+
writeFileSync(posted, pickedUp);
|
|
2517
|
+
}
|
|
2518
|
+
catch { /* posted; a repeat is harmless */ } log('tool', `posted the last ${ex.length} exchanges to ${person.name}'s chat`); })
|
|
2519
|
+
.catch((e) => log('warn', `could not post the earlier conversation to the chat: ${e instanceof Error ? e.message : String(e)}`));
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
// An assistant always works from the folder it was started in: that is where its person's own
|
|
2524
|
+
// Claude sessions live, and where `continue` finds the one to carry on.
|
|
2525
|
+
const cwd = isAssistant ? config.repo : tree?.path ?? repo;
|
|
2465
2526
|
// Back on this checkout: whatever was waiting to take its environment down can stand down.
|
|
2466
2527
|
turnCwd = cwd;
|
|
2467
2528
|
const pendingStop = idleStops.get(cwd);
|
|
@@ -2982,6 +3043,7 @@ export async function serve(opts) {
|
|
|
2982
3043
|
state.claudeAccount = claudePlan.label;
|
|
2983
3044
|
// Measured again once this event is done — see measurePlan.
|
|
2984
3045
|
turnPlan = { id: claudePlan.id, label: claudePlan.label, token: claudePlan.token };
|
|
3046
|
+
planOfSession.set(key, claudePlan.token);
|
|
2985
3047
|
}
|
|
2986
3048
|
if (claudePlan?.use === 'none') {
|
|
2987
3049
|
const when = claudePlan.availableAt ? new Date(claudePlan.availableAt).toLocaleString() : 'later';
|
|
@@ -3018,7 +3080,7 @@ export async function serve(opts) {
|
|
|
3018
3080
|
}
|
|
3019
3081
|
const engineContext = {
|
|
3020
3082
|
...(leadGoal ? { goal: leadGoal } : {}),
|
|
3021
|
-
config: { ...config, repo: cwd },
|
|
3083
|
+
config: { ...config, repo: cwd, ...(isAssistant ? { assistantMode: true } : {}) },
|
|
3022
3084
|
// Only a real worktree confines anything; without one nothing is enforced here, and the
|
|
3023
3085
|
// guard below makes sure a turn that would CHANGE code never gets that far.
|
|
3024
3086
|
//
|
|
@@ -3033,7 +3095,8 @@ export async function serve(opts) {
|
|
|
3033
3095
|
// did exactly that, in the shared checkout every worktree is cut from, on a branch of its
|
|
3034
3096
|
// own. Without a worktree there is nowhere legitimate to write, so the turn gets scratch
|
|
3035
3097
|
// space and has to hand the card back instead.
|
|
3036
|
-
|
|
3098
|
+
// An assistant is not confined to a checkout: it works for one person, anywhere they need.
|
|
3099
|
+
...(isAssistant ? {} : { sandbox: tree ? tree.path : scratchDir() }),
|
|
3037
3100
|
decider: sessionDecider,
|
|
3038
3101
|
// Both only reachable in-process, so an out-of-process engine is handed them explicitly
|
|
3039
3102
|
// rather than being left unable to publish and unchecked when it says it has.
|
|
@@ -3045,6 +3108,9 @@ export async function serve(opts) {
|
|
|
3045
3108
|
saved: savedSoFar,
|
|
3046
3109
|
token: await opts.auth(),
|
|
3047
3110
|
...(resume ? { resume } : {}),
|
|
3111
|
+
// An assistant with no conversation of its own yet carries on its person's last Claude
|
|
3112
|
+
// session in this folder — the one they were working in before the assistant existed.
|
|
3113
|
+
...(!resume && isAssistant && key === ASSISTANT_KEY && pickedUp ? { resume: pickedUp, forkSession: true } : {}),
|
|
3048
3114
|
...(prepared?.model ? { model: prepared.model } : {}),
|
|
3049
3115
|
...(prepared?.size ? { size: prepared.size } : {}),
|
|
3050
3116
|
...(claudePlan?.use === 'token' ? { claudeToken: claudePlan.token } : {}),
|
|
@@ -3083,6 +3149,7 @@ export async function serve(opts) {
|
|
|
3083
3149
|
unclearBrief(prepared?.jev ?? null, leadName) + preflightBrief(checks) + backBrief(event) +
|
|
3084
3150
|
freeWorkerBrief(event) + (leadWithoutCode ? LEAD_WITHOUT_CODE_BRIEF : '') +
|
|
3085
3151
|
(isManager ? MANAGER_BRIEF + (event.event === 'sweep' ? MANAGER_SWEEP_BRIEF : '') : '') +
|
|
3152
|
+
(isAssistant ? assistantBrief(person) : '') +
|
|
3086
3153
|
// Only tell an agent about the environment when the card actually needs one running.
|
|
3087
3154
|
environmentBrief(env, cwd, needsRunning) + notStartedBrief(forACard && !needsRunning, cwd) +
|
|
3088
3155
|
browserBrief(env.endpoints, declaredSites, forACard, web) +
|
|
@@ -3189,8 +3256,11 @@ export async function serve(opts) {
|
|
|
3189
3256
|
void report();
|
|
3190
3257
|
}
|
|
3191
3258
|
else if (out.kind === 'ready') {
|
|
3192
|
-
if (out.sessionId)
|
|
3259
|
+
if (out.sessionId) {
|
|
3193
3260
|
sessions.set(key, out.sessionId);
|
|
3261
|
+
if (key === ASSISTANT_KEY)
|
|
3262
|
+
saveAssistantSession(assistantSessionFile, out.sessionId);
|
|
3263
|
+
}
|
|
3194
3264
|
// The preflight proved the server answers; this proves the SESSION got it. The SDK will
|
|
3195
3265
|
// start the model with a server still 'pending' or 'needs-auth' and list its tools from
|
|
3196
3266
|
// memory, so a count alone is not enough — the status has to say connected. Refuse the
|
|
@@ -3262,6 +3332,10 @@ export async function serve(opts) {
|
|
|
3262
3332
|
failedPlans.delete(event.id);
|
|
3263
3333
|
if (runError)
|
|
3264
3334
|
throw runError;
|
|
3335
|
+
// An assistant's conversation is never handed over or closed: it is one long talk with one
|
|
3336
|
+
// person, compacted by the SDK as it grows, and resumed after a restart from the saved id.
|
|
3337
|
+
if (isAssistant)
|
|
3338
|
+
return;
|
|
3265
3339
|
// A question from the pane: no card to hand over to. Its session is kept so the next question
|
|
3266
3340
|
// can build on this one, and simply dropped once it has grown too large to be worth carrying.
|
|
3267
3341
|
if (event.event === 'ask') {
|
|
@@ -3370,6 +3444,17 @@ export async function serve(opts) {
|
|
|
3370
3444
|
so the shared checkout serves. */
|
|
3371
3445
|
if (!existsSync(cwd))
|
|
3372
3446
|
cwd = config.repo;
|
|
3447
|
+
// The handover runs on a subscription FlareDeck picks, like any turn. It ran on this
|
|
3448
|
+
// machine's own Claude login instead, and when that account had used its week every handover
|
|
3449
|
+
// failed — "You've hit your weekly limit" — while the turns before it ran fine on the plan
|
|
3450
|
+
// they were given (Norm at 14:00, an HR Goat agent at 14:34, 14 Oct). With every plan spent
|
|
3451
|
+
// there is nothing to run it on; the card keeps its last note.
|
|
3452
|
+
const handoverPlan = await handoverSubscription(() => subscriptionsForAgent(config.mcpUrl, () => opts.auth(), (t) => log('tool', t)), planOfSession.get(key));
|
|
3453
|
+
if (handoverPlan === 'none') {
|
|
3454
|
+
log('warn', `${key}: every Claude subscription is used up — no handover note this time`);
|
|
3455
|
+
sessions.delete(key);
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3373
3458
|
try {
|
|
3374
3459
|
const wrapUp = engine.run((done
|
|
3375
3460
|
? [
|
|
@@ -3407,6 +3492,7 @@ export async function serve(opts) {
|
|
|
3407
3492
|
decider: sessionDecider,
|
|
3408
3493
|
token: await opts.auth(),
|
|
3409
3494
|
...(resume ? { resume } : {}),
|
|
3495
|
+
...(handoverPlan ? { claudeToken: handoverPlan } : {}),
|
|
3410
3496
|
tools: { team: teamSet, flaredeck: await proxyForWrapUp() },
|
|
3411
3497
|
});
|
|
3412
3498
|
liveRun = wrapUp;
|
|
@@ -3699,6 +3785,27 @@ export async function decideForSession(request, options, via) {
|
|
|
3699
3785
|
const v = await via.judge(command);
|
|
3700
3786
|
return v.allow ? { allow: true } : { allow: false, reason: v.reason ?? 'Not allowed.' };
|
|
3701
3787
|
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Which Claude token a handover runs on: a fresh pick from FlareDeck, because the plan the turn
|
|
3790
|
+
* used may have run out since; the turn's own plan when FlareDeck cannot be asked; 'none' when
|
|
3791
|
+
* every plan is spent; null to fall back to this machine's own login (no subscriptions set up).
|
|
3792
|
+
*/
|
|
3793
|
+
export async function handoverSubscription(subs, lastUsed) {
|
|
3794
|
+
try {
|
|
3795
|
+
const s = await subs();
|
|
3796
|
+
if (!s)
|
|
3797
|
+
return lastUsed ?? null;
|
|
3798
|
+
const pick = await s.choose([]);
|
|
3799
|
+
if (pick.use === 'token' && pick.token)
|
|
3800
|
+
return pick.token;
|
|
3801
|
+
if (pick.use === 'none')
|
|
3802
|
+
return 'none';
|
|
3803
|
+
return lastUsed ?? null;
|
|
3804
|
+
}
|
|
3805
|
+
catch {
|
|
3806
|
+
return lastUsed ?? null;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3702
3809
|
export function moveIsStale(event, laneNow) {
|
|
3703
3810
|
if (event.event !== 'moved' || !event.laneName || !laneNow)
|
|
3704
3811
|
return false;
|