@onepointfour-packs/flaredeck-agent 0.2.112 → 0.2.113
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 +33 -0
- package/dist/assistant.js +149 -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 +97 -7
- 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, 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';
|
|
@@ -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,15 @@ 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
|
+
// An assistant always works from the folder it was started in: that is where its person's own
|
|
2507
|
+
// Claude sessions live, and where `continue` finds the one to carry on.
|
|
2508
|
+
const cwd = isAssistant ? config.repo : tree?.path ?? repo;
|
|
2465
2509
|
// Back on this checkout: whatever was waiting to take its environment down can stand down.
|
|
2466
2510
|
turnCwd = cwd;
|
|
2467
2511
|
const pendingStop = idleStops.get(cwd);
|
|
@@ -2982,6 +3026,7 @@ export async function serve(opts) {
|
|
|
2982
3026
|
state.claudeAccount = claudePlan.label;
|
|
2983
3027
|
// Measured again once this event is done — see measurePlan.
|
|
2984
3028
|
turnPlan = { id: claudePlan.id, label: claudePlan.label, token: claudePlan.token };
|
|
3029
|
+
planOfSession.set(key, claudePlan.token);
|
|
2985
3030
|
}
|
|
2986
3031
|
if (claudePlan?.use === 'none') {
|
|
2987
3032
|
const when = claudePlan.availableAt ? new Date(claudePlan.availableAt).toLocaleString() : 'later';
|
|
@@ -3018,7 +3063,7 @@ export async function serve(opts) {
|
|
|
3018
3063
|
}
|
|
3019
3064
|
const engineContext = {
|
|
3020
3065
|
...(leadGoal ? { goal: leadGoal } : {}),
|
|
3021
|
-
config: { ...config, repo: cwd },
|
|
3066
|
+
config: { ...config, repo: cwd, ...(isAssistant ? { assistantMode: true } : {}) },
|
|
3022
3067
|
// Only a real worktree confines anything; without one nothing is enforced here, and the
|
|
3023
3068
|
// guard below makes sure a turn that would CHANGE code never gets that far.
|
|
3024
3069
|
//
|
|
@@ -3033,7 +3078,8 @@ export async function serve(opts) {
|
|
|
3033
3078
|
// did exactly that, in the shared checkout every worktree is cut from, on a branch of its
|
|
3034
3079
|
// own. Without a worktree there is nowhere legitimate to write, so the turn gets scratch
|
|
3035
3080
|
// space and has to hand the card back instead.
|
|
3036
|
-
|
|
3081
|
+
// An assistant is not confined to a checkout: it works for one person, anywhere they need.
|
|
3082
|
+
...(isAssistant ? {} : { sandbox: tree ? tree.path : scratchDir() }),
|
|
3037
3083
|
decider: sessionDecider,
|
|
3038
3084
|
// Both only reachable in-process, so an out-of-process engine is handed them explicitly
|
|
3039
3085
|
// rather than being left unable to publish and unchecked when it says it has.
|
|
@@ -3045,6 +3091,9 @@ export async function serve(opts) {
|
|
|
3045
3091
|
saved: savedSoFar,
|
|
3046
3092
|
token: await opts.auth(),
|
|
3047
3093
|
...(resume ? { resume } : {}),
|
|
3094
|
+
// An assistant with no conversation of its own yet carries on its person's last Claude
|
|
3095
|
+
// session in this folder — the one they were working in before the assistant existed.
|
|
3096
|
+
...(!resume && isAssistant && key === ASSISTANT_KEY && pickedUp ? { resume: pickedUp, forkSession: true } : {}),
|
|
3048
3097
|
...(prepared?.model ? { model: prepared.model } : {}),
|
|
3049
3098
|
...(prepared?.size ? { size: prepared.size } : {}),
|
|
3050
3099
|
...(claudePlan?.use === 'token' ? { claudeToken: claudePlan.token } : {}),
|
|
@@ -3083,6 +3132,7 @@ export async function serve(opts) {
|
|
|
3083
3132
|
unclearBrief(prepared?.jev ?? null, leadName) + preflightBrief(checks) + backBrief(event) +
|
|
3084
3133
|
freeWorkerBrief(event) + (leadWithoutCode ? LEAD_WITHOUT_CODE_BRIEF : '') +
|
|
3085
3134
|
(isManager ? MANAGER_BRIEF + (event.event === 'sweep' ? MANAGER_SWEEP_BRIEF : '') : '') +
|
|
3135
|
+
(isAssistant ? assistantBrief(person) : '') +
|
|
3086
3136
|
// Only tell an agent about the environment when the card actually needs one running.
|
|
3087
3137
|
environmentBrief(env, cwd, needsRunning) + notStartedBrief(forACard && !needsRunning, cwd) +
|
|
3088
3138
|
browserBrief(env.endpoints, declaredSites, forACard, web) +
|
|
@@ -3189,8 +3239,11 @@ export async function serve(opts) {
|
|
|
3189
3239
|
void report();
|
|
3190
3240
|
}
|
|
3191
3241
|
else if (out.kind === 'ready') {
|
|
3192
|
-
if (out.sessionId)
|
|
3242
|
+
if (out.sessionId) {
|
|
3193
3243
|
sessions.set(key, out.sessionId);
|
|
3244
|
+
if (key === ASSISTANT_KEY)
|
|
3245
|
+
saveAssistantSession(assistantSessionFile, out.sessionId);
|
|
3246
|
+
}
|
|
3194
3247
|
// The preflight proved the server answers; this proves the SESSION got it. The SDK will
|
|
3195
3248
|
// start the model with a server still 'pending' or 'needs-auth' and list its tools from
|
|
3196
3249
|
// memory, so a count alone is not enough — the status has to say connected. Refuse the
|
|
@@ -3262,6 +3315,10 @@ export async function serve(opts) {
|
|
|
3262
3315
|
failedPlans.delete(event.id);
|
|
3263
3316
|
if (runError)
|
|
3264
3317
|
throw runError;
|
|
3318
|
+
// An assistant's conversation is never handed over or closed: it is one long talk with one
|
|
3319
|
+
// person, compacted by the SDK as it grows, and resumed after a restart from the saved id.
|
|
3320
|
+
if (isAssistant)
|
|
3321
|
+
return;
|
|
3265
3322
|
// A question from the pane: no card to hand over to. Its session is kept so the next question
|
|
3266
3323
|
// can build on this one, and simply dropped once it has grown too large to be worth carrying.
|
|
3267
3324
|
if (event.event === 'ask') {
|
|
@@ -3370,6 +3427,17 @@ export async function serve(opts) {
|
|
|
3370
3427
|
so the shared checkout serves. */
|
|
3371
3428
|
if (!existsSync(cwd))
|
|
3372
3429
|
cwd = config.repo;
|
|
3430
|
+
// The handover runs on a subscription FlareDeck picks, like any turn. It ran on this
|
|
3431
|
+
// machine's own Claude login instead, and when that account had used its week every handover
|
|
3432
|
+
// failed — "You've hit your weekly limit" — while the turns before it ran fine on the plan
|
|
3433
|
+
// they were given (Norm at 14:00, an HR Goat agent at 14:34, 14 Oct). With every plan spent
|
|
3434
|
+
// there is nothing to run it on; the card keeps its last note.
|
|
3435
|
+
const handoverPlan = await handoverSubscription(() => subscriptionsForAgent(config.mcpUrl, () => opts.auth(), (t) => log('tool', t)), planOfSession.get(key));
|
|
3436
|
+
if (handoverPlan === 'none') {
|
|
3437
|
+
log('warn', `${key}: every Claude subscription is used up — no handover note this time`);
|
|
3438
|
+
sessions.delete(key);
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3373
3441
|
try {
|
|
3374
3442
|
const wrapUp = engine.run((done
|
|
3375
3443
|
? [
|
|
@@ -3407,6 +3475,7 @@ export async function serve(opts) {
|
|
|
3407
3475
|
decider: sessionDecider,
|
|
3408
3476
|
token: await opts.auth(),
|
|
3409
3477
|
...(resume ? { resume } : {}),
|
|
3478
|
+
...(handoverPlan ? { claudeToken: handoverPlan } : {}),
|
|
3410
3479
|
tools: { team: teamSet, flaredeck: await proxyForWrapUp() },
|
|
3411
3480
|
});
|
|
3412
3481
|
liveRun = wrapUp;
|
|
@@ -3699,6 +3768,27 @@ export async function decideForSession(request, options, via) {
|
|
|
3699
3768
|
const v = await via.judge(command);
|
|
3700
3769
|
return v.allow ? { allow: true } : { allow: false, reason: v.reason ?? 'Not allowed.' };
|
|
3701
3770
|
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Which Claude token a handover runs on: a fresh pick from FlareDeck, because the plan the turn
|
|
3773
|
+
* used may have run out since; the turn's own plan when FlareDeck cannot be asked; 'none' when
|
|
3774
|
+
* every plan is spent; null to fall back to this machine's own login (no subscriptions set up).
|
|
3775
|
+
*/
|
|
3776
|
+
export async function handoverSubscription(subs, lastUsed) {
|
|
3777
|
+
try {
|
|
3778
|
+
const s = await subs();
|
|
3779
|
+
if (!s)
|
|
3780
|
+
return lastUsed ?? null;
|
|
3781
|
+
const pick = await s.choose([]);
|
|
3782
|
+
if (pick.use === 'token' && pick.token)
|
|
3783
|
+
return pick.token;
|
|
3784
|
+
if (pick.use === 'none')
|
|
3785
|
+
return 'none';
|
|
3786
|
+
return lastUsed ?? null;
|
|
3787
|
+
}
|
|
3788
|
+
catch {
|
|
3789
|
+
return lastUsed ?? null;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3702
3792
|
export function moveIsStale(event, laneNow) {
|
|
3703
3793
|
if (event.event !== 'moved' || !event.laneName || !laneNow)
|
|
3704
3794
|
return false;
|