@onepointfour-packs/flaredeck-agent 0.2.111 → 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/runs.d.ts +2 -0
- package/dist/runs.js +3 -1
- package/dist/runs.js.map +1 -1
- package/dist/serve.d.ts +11 -0
- package/dist/serve.js +103 -10
- 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';
|
|
@@ -735,14 +736,14 @@ export async function serve(opts) {
|
|
|
735
736
|
const runsSet = opts.deck
|
|
736
737
|
? runTools(config.runs ?? {}, () => onCard, opts.deck, (t) => log('tool', t), {
|
|
737
738
|
attach: (card, path, comment) => attachFile(card, path, comment),
|
|
738
|
-
flag: async (reason, kind, ask) => {
|
|
739
|
+
flag: async (reason, kind, ask, flagOpts) => {
|
|
739
740
|
const cur = onCard;
|
|
740
741
|
if (!cur)
|
|
741
742
|
return null;
|
|
742
743
|
// A question, from a manager, with its person at this pane: ask FlareDeck to keep it off
|
|
743
744
|
// their phone. FlareDeck decides whether it is theirs; a script request is never held.
|
|
744
745
|
const hold = isManager && !!ask && presence.mode() === 'local';
|
|
745
|
-
return flagCard({ card: cur.card, board: cur.board, code: cur.code }, reason, kind, ask, undefined, hold);
|
|
746
|
+
return flagCard({ card: cur.card, board: cur.board, code: cur.code }, reason, kind, ask, undefined, hold, flagOpts?.quiet);
|
|
746
747
|
},
|
|
747
748
|
unflag: async (cur) => {
|
|
748
749
|
parked.delete(cur.card);
|
|
@@ -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)
|
|
@@ -2240,7 +2261,9 @@ export async function serve(opts) {
|
|
|
2240
2261
|
/** Somewhere for the person to GO, which is what a sign-in request is mostly made of. */
|
|
2241
2262
|
links,
|
|
2242
2263
|
/** The person this would go to may be at this pane: flag it, and keep it off their phone. */
|
|
2243
|
-
hold
|
|
2264
|
+
hold,
|
|
2265
|
+
/** It goes with a script request, which notifies by itself: list the card, ring nobody. */
|
|
2266
|
+
quiet) => {
|
|
2244
2267
|
const label = ref.code || ref.card || 'card';
|
|
2245
2268
|
try {
|
|
2246
2269
|
const out = await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'set-needs-human', {
|
|
@@ -2252,6 +2275,7 @@ export async function serve(opts) {
|
|
|
2252
2275
|
...(ask ? { question: ask.question, options: ask.options } : {}),
|
|
2253
2276
|
...(links?.length ? { links } : {}),
|
|
2254
2277
|
...(hold ? { hold: true } : {}),
|
|
2278
|
+
...(quiet ? { quiet: true } : {}),
|
|
2255
2279
|
});
|
|
2256
2280
|
if (out.trimStart().startsWith('\u274c') || out.trimStart().startsWith('\ud83d\udeab')) {
|
|
2257
2281
|
log('warn', `${label}: FlareDeck did not flag this card \u2014 ${out.split('\n')[0]}`);
|
|
@@ -2329,11 +2353,26 @@ export async function serve(opts) {
|
|
|
2329
2353
|
log('event', `${event.cardCode || event.cardKey}: ${event.actor} is talking to somebody else on the card — nothing to do`);
|
|
2330
2354
|
return;
|
|
2331
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
|
+
}
|
|
2332
2371
|
if (isBookkeeping(event, agentNames)) {
|
|
2333
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`);
|
|
2334
2373
|
return;
|
|
2335
2374
|
}
|
|
2336
|
-
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');
|
|
2337
2376
|
let turnTokens = 0;
|
|
2338
2377
|
let nudged = false;
|
|
2339
2378
|
// A checkout of its own per card, so it cannot edit a working tree someone else is in.
|
|
@@ -2458,7 +2497,15 @@ export async function serve(opts) {
|
|
|
2458
2497
|
*/
|
|
2459
2498
|
if (event.event === 'ask' && tree)
|
|
2460
2499
|
key = `ask:${tree.path}`;
|
|
2461
|
-
|
|
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;
|
|
2462
2509
|
// Back on this checkout: whatever was waiting to take its environment down can stand down.
|
|
2463
2510
|
turnCwd = cwd;
|
|
2464
2511
|
const pendingStop = idleStops.get(cwd);
|
|
@@ -2979,6 +3026,7 @@ export async function serve(opts) {
|
|
|
2979
3026
|
state.claudeAccount = claudePlan.label;
|
|
2980
3027
|
// Measured again once this event is done — see measurePlan.
|
|
2981
3028
|
turnPlan = { id: claudePlan.id, label: claudePlan.label, token: claudePlan.token };
|
|
3029
|
+
planOfSession.set(key, claudePlan.token);
|
|
2982
3030
|
}
|
|
2983
3031
|
if (claudePlan?.use === 'none') {
|
|
2984
3032
|
const when = claudePlan.availableAt ? new Date(claudePlan.availableAt).toLocaleString() : 'later';
|
|
@@ -3015,7 +3063,7 @@ export async function serve(opts) {
|
|
|
3015
3063
|
}
|
|
3016
3064
|
const engineContext = {
|
|
3017
3065
|
...(leadGoal ? { goal: leadGoal } : {}),
|
|
3018
|
-
config: { ...config, repo: cwd },
|
|
3066
|
+
config: { ...config, repo: cwd, ...(isAssistant ? { assistantMode: true } : {}) },
|
|
3019
3067
|
// Only a real worktree confines anything; without one nothing is enforced here, and the
|
|
3020
3068
|
// guard below makes sure a turn that would CHANGE code never gets that far.
|
|
3021
3069
|
//
|
|
@@ -3030,7 +3078,8 @@ export async function serve(opts) {
|
|
|
3030
3078
|
// did exactly that, in the shared checkout every worktree is cut from, on a branch of its
|
|
3031
3079
|
// own. Without a worktree there is nowhere legitimate to write, so the turn gets scratch
|
|
3032
3080
|
// space and has to hand the card back instead.
|
|
3033
|
-
|
|
3081
|
+
// An assistant is not confined to a checkout: it works for one person, anywhere they need.
|
|
3082
|
+
...(isAssistant ? {} : { sandbox: tree ? tree.path : scratchDir() }),
|
|
3034
3083
|
decider: sessionDecider,
|
|
3035
3084
|
// Both only reachable in-process, so an out-of-process engine is handed them explicitly
|
|
3036
3085
|
// rather than being left unable to publish and unchecked when it says it has.
|
|
@@ -3042,6 +3091,9 @@ export async function serve(opts) {
|
|
|
3042
3091
|
saved: savedSoFar,
|
|
3043
3092
|
token: await opts.auth(),
|
|
3044
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 } : {}),
|
|
3045
3097
|
...(prepared?.model ? { model: prepared.model } : {}),
|
|
3046
3098
|
...(prepared?.size ? { size: prepared.size } : {}),
|
|
3047
3099
|
...(claudePlan?.use === 'token' ? { claudeToken: claudePlan.token } : {}),
|
|
@@ -3080,6 +3132,7 @@ export async function serve(opts) {
|
|
|
3080
3132
|
unclearBrief(prepared?.jev ?? null, leadName) + preflightBrief(checks) + backBrief(event) +
|
|
3081
3133
|
freeWorkerBrief(event) + (leadWithoutCode ? LEAD_WITHOUT_CODE_BRIEF : '') +
|
|
3082
3134
|
(isManager ? MANAGER_BRIEF + (event.event === 'sweep' ? MANAGER_SWEEP_BRIEF : '') : '') +
|
|
3135
|
+
(isAssistant ? assistantBrief(person) : '') +
|
|
3083
3136
|
// Only tell an agent about the environment when the card actually needs one running.
|
|
3084
3137
|
environmentBrief(env, cwd, needsRunning) + notStartedBrief(forACard && !needsRunning, cwd) +
|
|
3085
3138
|
browserBrief(env.endpoints, declaredSites, forACard, web) +
|
|
@@ -3186,8 +3239,11 @@ export async function serve(opts) {
|
|
|
3186
3239
|
void report();
|
|
3187
3240
|
}
|
|
3188
3241
|
else if (out.kind === 'ready') {
|
|
3189
|
-
if (out.sessionId)
|
|
3242
|
+
if (out.sessionId) {
|
|
3190
3243
|
sessions.set(key, out.sessionId);
|
|
3244
|
+
if (key === ASSISTANT_KEY)
|
|
3245
|
+
saveAssistantSession(assistantSessionFile, out.sessionId);
|
|
3246
|
+
}
|
|
3191
3247
|
// The preflight proved the server answers; this proves the SESSION got it. The SDK will
|
|
3192
3248
|
// start the model with a server still 'pending' or 'needs-auth' and list its tools from
|
|
3193
3249
|
// memory, so a count alone is not enough — the status has to say connected. Refuse the
|
|
@@ -3259,6 +3315,10 @@ export async function serve(opts) {
|
|
|
3259
3315
|
failedPlans.delete(event.id);
|
|
3260
3316
|
if (runError)
|
|
3261
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;
|
|
3262
3322
|
// A question from the pane: no card to hand over to. Its session is kept so the next question
|
|
3263
3323
|
// can build on this one, and simply dropped once it has grown too large to be worth carrying.
|
|
3264
3324
|
if (event.event === 'ask') {
|
|
@@ -3367,6 +3427,17 @@ export async function serve(opts) {
|
|
|
3367
3427
|
so the shared checkout serves. */
|
|
3368
3428
|
if (!existsSync(cwd))
|
|
3369
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
|
+
}
|
|
3370
3441
|
try {
|
|
3371
3442
|
const wrapUp = engine.run((done
|
|
3372
3443
|
? [
|
|
@@ -3404,6 +3475,7 @@ export async function serve(opts) {
|
|
|
3404
3475
|
decider: sessionDecider,
|
|
3405
3476
|
token: await opts.auth(),
|
|
3406
3477
|
...(resume ? { resume } : {}),
|
|
3478
|
+
...(handoverPlan ? { claudeToken: handoverPlan } : {}),
|
|
3407
3479
|
tools: { team: teamSet, flaredeck: await proxyForWrapUp() },
|
|
3408
3480
|
});
|
|
3409
3481
|
liveRun = wrapUp;
|
|
@@ -3696,6 +3768,27 @@ export async function decideForSession(request, options, via) {
|
|
|
3696
3768
|
const v = await via.judge(command);
|
|
3697
3769
|
return v.allow ? { allow: true } : { allow: false, reason: v.reason ?? 'Not allowed.' };
|
|
3698
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
|
+
}
|
|
3699
3792
|
export function moveIsStale(event, laneNow) {
|
|
3700
3793
|
if (event.event !== 'moved' || !event.laneName || !laneNow)
|
|
3701
3794
|
return false;
|