@onepointfour-packs/flaredeck-agent 0.2.198 → 0.2.200
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.d.ts +14 -1
- package/dist/agent.js +34 -3
- package/dist/agent.js.map +1 -1
- package/dist/release.d.ts +2 -0
- package/dist/release.js +6 -0
- package/dist/release.js.map +1 -1
- package/dist/serve.d.ts +10 -0
- package/dist/serve.js +67 -23
- package/dist/serve.js.map +1 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -9,7 +9,7 @@ import { freshGitHubToken } from './githubauth.js';
|
|
|
9
9
|
import { answerFrom, createPresence, heldFor, modeCommand } from './presence.js';
|
|
10
10
|
import { defaultUploadRoots, fileTools, flaredeckStores } from './filestore.js';
|
|
11
11
|
import { scratchFor } from './cardmess.js';
|
|
12
|
-
import { fetchLadder, isSoloWorker, namedOnLadders, releaseTools } from './release.js';
|
|
12
|
+
import { boardsNaming, fetchLadder, isSoloWorker, namedOnLadders, releaseTools } from './release.js';
|
|
13
13
|
import { LiveFeed, liveSender, midTurnMessage, ASSISTANT_KEY, claudeProjectDir, historyMessage, recentExchanges, latestCliSession, assistantBrief, assistantIgnores, assistantPerson, asAssistantEvent, loadAssistantSession, saveAssistantSession } from './assistant.js';
|
|
14
14
|
import { codePrefix, loadPrefixes, savePrefixes, SCRATCH_SWEEP_EVERY_MS, sweepScratch } from './scratch.js';
|
|
15
15
|
import { findImages, namedForAgent } from './images.js';
|
|
@@ -27,6 +27,19 @@ import { CouldNotWork, engineFor } from './engine.js';
|
|
|
27
27
|
/** The run failed on its Claude subscription — capped or token rejected — not on its work. */
|
|
28
28
|
class RetryOnAnotherPlan extends CouldNotWork {
|
|
29
29
|
}
|
|
30
|
+
/** Where an assistant's card repositories are cloned: the harness's own folder, never its person's project. */
|
|
31
|
+
export const cardReposRoot = () => join(configDir(), 'card-repos');
|
|
32
|
+
/** A clone's folder: the repository's own name, made safe. */
|
|
33
|
+
export const cardRepoDir = (url) => (url.replace(/\/+$/, '').split('/').pop() ?? 'repo').replace(/\.git$/i, '').replace(/[^A-Za-z0-9._-]/g, '-') || 'repo';
|
|
34
|
+
/**
|
|
35
|
+
* The turn failed before the model ran. `spawn ETXTBSY`: the harness's own update was rewriting
|
|
36
|
+
* the Claude binary at the moment the turn started it. Counted as done, the event was acknowledged
|
|
37
|
+
* and lost — JARVIS's review of HR-1106 (20 Oct), two handover notes before it.
|
|
38
|
+
*/
|
|
39
|
+
export function neverStarted(e) {
|
|
40
|
+
const code = e?.code;
|
|
41
|
+
return code === 'ETXTBSY' || /\bspawn ETXTBSY\b/.test(e instanceof Error ? e.message : String(e));
|
|
42
|
+
}
|
|
30
43
|
import { isJustReading, jevGuard, judgeCommand } from './jevguard.js';
|
|
31
44
|
import { loadProviders } from './providers.js';
|
|
32
45
|
import { cardWorkOnBase, currentBranch, ensureWorktree, finishRefusalStands, freeDiskBytes, headOf, leadGaveItBack, sweepWorktrees, releaseSharedPushGuard, existingWorktree, produced, releaseWorktree, tooEarlyToFinish, uncommittedCount, worktreeBrief } from './worktree.js';
|
|
@@ -1021,6 +1034,14 @@ export async function serve(opts) {
|
|
|
1021
1034
|
* Looked up when the team is read and every 10 minutes, so a change on the board takes effect.
|
|
1022
1035
|
*/
|
|
1023
1036
|
let namedReleaser = false;
|
|
1037
|
+
/**
|
|
1038
|
+
* The boards that name this agent as their releaser. An ASSISTANT works the cards assigned to it
|
|
1039
|
+
* on these boards like a worker — its own checkout, the release tool — and stays its person's
|
|
1040
|
+
* assistant everywhere else (Cobus, 20 Oct: FRIDAY releases the website, and the website's cards
|
|
1041
|
+
* are assigned to it). Both have to be true: assigned to it, on a board it releases for.
|
|
1042
|
+
*/
|
|
1043
|
+
let releaseBoards = new Set();
|
|
1044
|
+
const assistantCard = (e) => isAssistant && !!(e.cardKey || e.cardCode) && !!e.boardKey && releaseBoards.has(e.boardKey);
|
|
1024
1045
|
/**
|
|
1025
1046
|
* One person's own agent: on no team, never dispatched to, listening only to `person`. One long
|
|
1026
1047
|
* conversation, never handed over — the SDK compacts it — and loose rules. See ASSISTANT_BRIEF.
|
|
@@ -1178,6 +1199,7 @@ export async function serve(opts) {
|
|
|
1178
1199
|
const ladders = await Promise.all((config.boards ?? []).map((b) => fetchLadder(config.mcpUrl, releaseTenant, b.key, () => opts.auth()).catch(() => null)));
|
|
1179
1200
|
const was = namedReleaser;
|
|
1180
1201
|
namedReleaser = namedOnLadders(ladders, selfId);
|
|
1202
|
+
releaseBoards = new Set(boardsNaming(ladders, selfId));
|
|
1181
1203
|
if (namedReleaser && !was)
|
|
1182
1204
|
log('tool', 'a board names you as the one who releases into an environment — you have the release tools');
|
|
1183
1205
|
};
|
|
@@ -2185,8 +2207,9 @@ export async function serve(opts) {
|
|
|
2185
2207
|
...[...resumedIdle].filter(([, n]) => n >= RESUME_IDLE_LIMIT).map(([c]) => c),
|
|
2186
2208
|
])];
|
|
2187
2209
|
const resumeOwnWork = async () => {
|
|
2188
|
-
|
|
2189
|
-
|
|
2210
|
+
// An assistant's own work is only the cards on boards it releases for; otherwise its person's asks.
|
|
2211
|
+
if (isAssistant && !releaseBoards.size)
|
|
2212
|
+
return false;
|
|
2190
2213
|
/* Not `live.size`: a session kept WARM for a follow-up is a cache, not work, and blocking on
|
|
2191
2214
|
it meant the card in progress was never resumed within fifteen minutes of any turn. */
|
|
2192
2215
|
if (paused || !sweepsEnabled || stopping || working || queue.length)
|
|
@@ -2197,8 +2220,9 @@ export async function serve(opts) {
|
|
|
2197
2220
|
of its own waiting. */
|
|
2198
2221
|
let answer = '';
|
|
2199
2222
|
try {
|
|
2223
|
+
const pullFrom = isAssistant ? [...releaseBoards] : [...myBoards];
|
|
2200
2224
|
answer = String(await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'pull-next-card', {
|
|
2201
|
-
...(boardArg(
|
|
2225
|
+
...(boardArg(pullFrom) !== undefined ? { board: boardArg(pullFrom) } : {}),
|
|
2202
2226
|
// Cards this harness will not work now: waiting on a person, or picked up with nothing new.
|
|
2203
2227
|
// Without this FlareDeck offered the same one every time — JARVIS, 16 Oct, "picking HR-0904
|
|
2204
2228
|
// back up … waiting on a person — left alone" round and round, never reaching another card.
|
|
@@ -2530,7 +2554,8 @@ export async function serve(opts) {
|
|
|
2530
2554
|
}
|
|
2531
2555
|
catch (e) {
|
|
2532
2556
|
// One bad event must not take the daemon down — that is the failure it exists to fix.
|
|
2533
|
-
|
|
2557
|
+
// A turn that could not even start is not done: kept, and tried again in a minute.
|
|
2558
|
+
didWork = !(e instanceof CouldNotWork) && !neverStarted(e);
|
|
2534
2559
|
log('warn', `${current}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2535
2560
|
if (e instanceof RetryOnAnotherPlan)
|
|
2536
2561
|
switchPlan = true;
|
|
@@ -2867,6 +2892,8 @@ export async function serve(opts) {
|
|
|
2867
2892
|
}, reason, kind, undefined, undefined, undefined, undefined, true);
|
|
2868
2893
|
};
|
|
2869
2894
|
async function handle(event) {
|
|
2895
|
+
// An assistant working a card on a board it releases for is a worker for this turn.
|
|
2896
|
+
const isAssistantChat = isAssistant && !assistantCard(event);
|
|
2870
2897
|
const prefix = codePrefix(event.cardCode);
|
|
2871
2898
|
if (prefix && !scratchPrefixes.has(prefix)) {
|
|
2872
2899
|
scratchPrefixes.add(prefix);
|
|
@@ -2937,7 +2964,7 @@ export async function serve(opts) {
|
|
|
2937
2964
|
return;
|
|
2938
2965
|
}
|
|
2939
2966
|
// An assistant hears its person and nothing else, and works outside the card machinery.
|
|
2940
|
-
if (
|
|
2967
|
+
if (isAssistantChat) {
|
|
2941
2968
|
const why = assistantIgnores(event, person?.uid ?? null);
|
|
2942
2969
|
if (why) {
|
|
2943
2970
|
log('event', `${event.cardCode || event.event}: ${why} — not for ${person?.name ?? 'anyone'}'s assistant`);
|
|
@@ -2961,7 +2988,7 @@ export async function serve(opts) {
|
|
|
2961
2988
|
FlareDeck hands it out by priority when the current one is done — and a comment on one gets
|
|
2962
2989
|
a reply only. "In progress" is FlareDeck's answer, skipping cards parked on a person. */
|
|
2963
2990
|
let queuedBehind = '';
|
|
2964
|
-
if (!isLead && !isManager && !
|
|
2991
|
+
if (!isLead && !isManager && !isAssistantChat && event.cardKey && event.event !== 'review' && event.event !== 'ask' && !sessions.has(event.cardKey)) {
|
|
2965
2992
|
const here = await opts.deck?.card(event.cardCode || event.cardKey).catch(() => null);
|
|
2966
2993
|
if (here && !here.done && String(here.lane?.type ?? '').toLowerCase() !== 'inprogress') {
|
|
2967
2994
|
const pv = String(await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'pull-next-card', {
|
|
@@ -2978,7 +3005,7 @@ export async function serve(opts) {
|
|
|
2978
3005
|
}
|
|
2979
3006
|
}
|
|
2980
3007
|
}
|
|
2981
|
-
let key =
|
|
3008
|
+
let key = isAssistantChat ? ASSISTANT_KEY : event.cardKey || (event.event === 'ask' ? 'ask' : event.event === 'sweep' ? 'sweep' : event.event === 'scheduled' ? `schedule-${event.scheduleId}` : 'inbox');
|
|
2982
3009
|
let turnTokens = 0;
|
|
2983
3010
|
let nudged = false;
|
|
2984
3011
|
// A checkout of its own per card, so it cannot edit a working tree someone else is in.
|
|
@@ -3007,7 +3034,24 @@ export async function serve(opts) {
|
|
|
3007
3034
|
event = { ...event, baseBranches: fromCard };
|
|
3008
3035
|
}
|
|
3009
3036
|
let repoChoiceNote = '';
|
|
3010
|
-
|
|
3037
|
+
let where = await resolveCheckout(config.repo, event, scope, opts.agent ?? '');
|
|
3038
|
+
// An assistant's card for a repository that is not on this machine — its own folder is another
|
|
3039
|
+
// project (FRIDAY's is an HR Goat checkout, the website is HrGoatMarketing): cloned once, with
|
|
3040
|
+
// FlareDeck's GitHub connection, into the harness's own folder, and worked from there.
|
|
3041
|
+
if (isAssistant && !isAssistantChat && where.kind !== 'repo') {
|
|
3042
|
+
const cardRoot = cardReposRoot();
|
|
3043
|
+
const gh = flaredeckGithub;
|
|
3044
|
+
for (const url of new Set((event.baseBranches ?? []).map((e) => e.repository).filter(Boolean))) {
|
|
3045
|
+
const dir = join(cardRoot, cardRepoDir(String(url)));
|
|
3046
|
+
if (gh?.token && isEmptyFolder(dir))
|
|
3047
|
+
await cloneBoardRepos(dir, [{ url: String(url).replace(/\.git$/, '') }], gh.token, (t, warn) => log(warn ? 'warn' : 'tool', t));
|
|
3048
|
+
}
|
|
3049
|
+
const there = await resolveCheckout(cardRoot, event, undefined, opts.agent ?? '').catch(() => null);
|
|
3050
|
+
if (there?.kind === 'repo')
|
|
3051
|
+
where = there;
|
|
3052
|
+
else if (!gh?.token)
|
|
3053
|
+
log('warn', `${event.cardCode || event.cardKey}: its repository is not on this machine and FlareDeck has no GitHub connection to clone it with`);
|
|
3054
|
+
}
|
|
3011
3055
|
let chosen = where.kind === 'repo' ? where.checkout : null;
|
|
3012
3056
|
let fromCard = where.kind === 'repo' && where.branch ? { branch: where.branch, role: where.role } : null;
|
|
3013
3057
|
if (where.kind === 'ambiguous') {
|
|
@@ -3147,7 +3191,7 @@ export async function serve(opts) {
|
|
|
3147
3191
|
// An assistant's very first turn, with no conversation of its own: its person's newest Claude
|
|
3148
3192
|
// CLI session in this folder, carried on as a FORK — the history comes across, the person's
|
|
3149
3193
|
// own session file is left as it was, and the two never write to the same place.
|
|
3150
|
-
const pickedUp =
|
|
3194
|
+
const pickedUp = isAssistantChat && key === ASSISTANT_KEY && !sessions.get(key) ? latestCliSession(config.repo) : null;
|
|
3151
3195
|
if (pickedUp) {
|
|
3152
3196
|
log('event', `carrying on from your Claude session ${pickedUp.slice(0, 8)} in this folder (a copy — yours is left as it is)`);
|
|
3153
3197
|
// So the person sees, in FlareDeck, where the conversation was: the last thirty exchanges,
|
|
@@ -3169,7 +3213,7 @@ export async function serve(opts) {
|
|
|
3169
3213
|
}
|
|
3170
3214
|
// An assistant always works from the folder it was started in: that is where its person's own
|
|
3171
3215
|
// Claude sessions live, and where `continue` finds the one to carry on.
|
|
3172
|
-
const cwd =
|
|
3216
|
+
const cwd = isAssistantChat ? config.repo : tree?.path ?? repo;
|
|
3173
3217
|
// Back on this checkout: whatever was waiting to take its environment down can stand down.
|
|
3174
3218
|
turnCwd = cwd;
|
|
3175
3219
|
const pendingStop = idleStops.get(cwd);
|
|
@@ -3806,7 +3850,7 @@ export async function serve(opts) {
|
|
|
3806
3850
|
background is typed into THIS turn; if the turn has ended by then, it starts the next one
|
|
3807
3851
|
on the card, so it is never lost and never polled for. */
|
|
3808
3852
|
// Read from the file each turn, so turning helpers on or off needs no restart.
|
|
3809
|
-
const helpersOn = !
|
|
3853
|
+
const helpersOn = !isAssistantChat && process.env['FLAREDECK_AGENT_HELPERS'] !== '0'
|
|
3810
3854
|
&& (loadConfig(config.repo)?.helpers ?? config.helpers) !== false;
|
|
3811
3855
|
let thisRun = null;
|
|
3812
3856
|
const deliverHelper = async (text) => {
|
|
@@ -3874,8 +3918,8 @@ export async function serve(opts) {
|
|
|
3874
3918
|
}) : null;
|
|
3875
3919
|
const engineContext = {
|
|
3876
3920
|
...(leadGoal ? { goal: leadGoal } : {}),
|
|
3877
|
-
config: { ...config, repo: cwd, ...(
|
|
3878
|
-
...(
|
|
3921
|
+
config: { ...config, repo: cwd, ...(isAssistantChat ? { assistantMode: true } : {}) },
|
|
3922
|
+
...(isAssistantChat ? { background: true } : {}),
|
|
3879
3923
|
// Only a real worktree confines anything; without one nothing is enforced here, and the
|
|
3880
3924
|
// guard below makes sure a turn that would CHANGE code never gets that far.
|
|
3881
3925
|
//
|
|
@@ -3891,12 +3935,12 @@ export async function serve(opts) {
|
|
|
3891
3935
|
// own. Without a worktree there is nowhere legitimate to write, so the turn gets scratch
|
|
3892
3936
|
// space and has to hand the card back instead.
|
|
3893
3937
|
// An assistant is not confined to a checkout: it works for one person, anywhere they need.
|
|
3894
|
-
...(
|
|
3938
|
+
...(isAssistantChat ? {} : { sandbox: tree ? tree.path : scratchDir() }),
|
|
3895
3939
|
decider: sessionDecider,
|
|
3896
3940
|
// Both only reachable in-process, so an out-of-process engine is handed them explicitly
|
|
3897
3941
|
// rather than being left unable to publish and unchecked when it says it has.
|
|
3898
3942
|
...(job ? { publish: async () => publishBranch(job) } : {}),
|
|
3899
|
-
standing: standingBrief(!!teamSet, config.style ?? process.env['FLAREDECK_AGENT_STYLE'],
|
|
3943
|
+
standing: standingBrief(!!teamSet, config.style ?? process.env['FLAREDECK_AGENT_STYLE'], isAssistantChat, config.mcpServers),
|
|
3900
3944
|
finish: finishGate,
|
|
3901
3945
|
noteCard,
|
|
3902
3946
|
keep: keepTool,
|
|
@@ -3905,7 +3949,7 @@ export async function serve(opts) {
|
|
|
3905
3949
|
...(resume ? { resume } : {}),
|
|
3906
3950
|
// An assistant with no conversation of its own yet carries on its person's last Claude
|
|
3907
3951
|
// session in this folder — the one they were working in before the assistant existed.
|
|
3908
|
-
...(!resume &&
|
|
3952
|
+
...(!resume && isAssistantChat && key === ASSISTANT_KEY && pickedUp ? { resume: pickedUp, forkSession: true } : {}),
|
|
3909
3953
|
...(prepared?.model ? { model: prepared.model } : {}),
|
|
3910
3954
|
...(prepared?.size ? { size: prepared.size } : {}),
|
|
3911
3955
|
...(claudePlan?.use === 'token' ? { claudeToken: claudePlan.token } : {}),
|
|
@@ -3951,7 +3995,7 @@ export async function serve(opts) {
|
|
|
3951
3995
|
freeWorkerBrief(event) + (leadWithoutCode ? LEAD_WITHOUT_CODE_BRIEF : '') +
|
|
3952
3996
|
(queuedBehind ? `\n\nThis card is QUEUED behind ${queuedBehind}, which you are working now. Reply to what was said here if it needs a reply, and stop: do not start this card's work, change its code or move it. It is handed to you when ${queuedBehind} is done.` : '') +
|
|
3953
3997
|
(isManager ? MANAGER_BRIEF + (event.event === 'sweep' ? MANAGER_SWEEP_BRIEF : '') : '') +
|
|
3954
|
-
(
|
|
3998
|
+
(isAssistantChat ? assistantBrief(person) : '') +
|
|
3955
3999
|
// Only tell an agent about the environment when the card actually needs one running.
|
|
3956
4000
|
environmentBrief(env, cwd, needsRunning) + notStartedBrief(forACard && !needsRunning, cwd) +
|
|
3957
4001
|
browserBrief(env.endpoints, declaredSites, forACard, web) +
|
|
@@ -3965,7 +4009,7 @@ export async function serve(opts) {
|
|
|
3965
4009
|
const run = turnEngine.run(brief + (helpersOn ? (turnEngine.name === 'antigravity' ? AGY_HELPERS_BRIEF : HELPERS_BRIEF) : ''), engineContext);
|
|
3966
4010
|
liveRun = run;
|
|
3967
4011
|
thisRun = run;
|
|
3968
|
-
if (
|
|
4012
|
+
if (isAssistantChat)
|
|
3969
4013
|
liveFeed?.start(String(event.message ?? event.line ?? '').split('\n').filter(Boolean).pop() ?? '');
|
|
3970
4014
|
// Connected MCP servers' tokens, kept current for as long as the session runs. FlareDeck hands
|
|
3971
4015
|
// a token out with at least five minutes left, so a check every four keeps one valid.
|
|
@@ -4058,7 +4102,7 @@ export async function serve(opts) {
|
|
|
4058
4102
|
// An answer to a question typed here is the whole point of the turn, so it is shown whole;
|
|
4059
4103
|
// a card turn's narration is one line each, the card holds the full account.
|
|
4060
4104
|
// What the person watching an assistant sees: the main conversation only, not sub-agents.
|
|
4061
|
-
if (
|
|
4105
|
+
if (isAssistantChat && liveFeed) {
|
|
4062
4106
|
if (out.kind === 'text' && !out.sub)
|
|
4063
4107
|
liveFeed.add('text', out.text);
|
|
4064
4108
|
else if (out.kind === 'tool' && !out.sub)
|
|
@@ -4141,7 +4185,7 @@ export async function serve(opts) {
|
|
|
4141
4185
|
}
|
|
4142
4186
|
clearInterval(renewTimer);
|
|
4143
4187
|
liveRun = null;
|
|
4144
|
-
if (
|
|
4188
|
+
if (isAssistantChat)
|
|
4145
4189
|
void liveFeed?.end();
|
|
4146
4190
|
/**
|
|
4147
4191
|
* The plan failed, not the work: switch plans and run this event again, now.
|
|
@@ -4175,7 +4219,7 @@ export async function serve(opts) {
|
|
|
4175
4219
|
throw runError;
|
|
4176
4220
|
// An assistant's conversation is never handed over or closed: it is one long talk with one
|
|
4177
4221
|
// person, compacted by the SDK as it grows, and resumed after a restart from the saved id.
|
|
4178
|
-
if (
|
|
4222
|
+
if (isAssistantChat)
|
|
4179
4223
|
return;
|
|
4180
4224
|
// A question from the pane: no card to hand over to. Its session is kept so the next question
|
|
4181
4225
|
// can build on this one, and simply dropped once it has grown too large to be worth carrying.
|
|
@@ -4417,7 +4461,7 @@ export async function serve(opts) {
|
|
|
4417
4461
|
// rather than that the agent has gone deaf.
|
|
4418
4462
|
// An assistant's person is the only one it listens to: anything else is dropped HERE, before
|
|
4419
4463
|
// intake reads the card and a judge is paid to size it (FRIDAY did both for a JARVIS comment).
|
|
4420
|
-
if (isAssistant) {
|
|
4464
|
+
if (isAssistant && !assistantCard(event)) {
|
|
4421
4465
|
const why = assistantIgnores(event, person?.uid ?? null);
|
|
4422
4466
|
if (why) {
|
|
4423
4467
|
log('event', `${event.cardCode || event.event}: ${why} — not for ${person?.name ?? 'anyone'}'s assistant`);
|