@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/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
- if (isAssistant)
2189
- return false; // an assistant has no "own work" — only its person's asks
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([...myBoards]) !== undefined ? { board: boardArg([...myBoards]) } : {}),
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
- didWork = !(e instanceof CouldNotWork);
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 (isAssistant) {
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 && !isAssistant && event.cardKey && event.event !== 'review' && event.event !== 'ask' && !sessions.has(event.cardKey)) {
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 = isAssistant ? ASSISTANT_KEY : event.cardKey || (event.event === 'ask' ? 'ask' : event.event === 'sweep' ? 'sweep' : event.event === 'scheduled' ? `schedule-${event.scheduleId}` : 'inbox');
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
- const where = await resolveCheckout(config.repo, event, scope, opts.agent ?? '');
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 = isAssistant && key === ASSISTANT_KEY && !sessions.get(key) ? latestCliSession(config.repo) : null;
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 = isAssistant ? config.repo : tree?.path ?? repo;
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 = !isAssistant && process.env['FLAREDECK_AGENT_HELPERS'] !== '0'
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, ...(isAssistant ? { assistantMode: true } : {}) },
3878
- ...(isAssistant ? { background: true } : {}),
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
- ...(isAssistant ? {} : { sandbox: tree ? tree.path : scratchDir() }),
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'], isAssistant),
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 && isAssistant && key === ASSISTANT_KEY && pickedUp ? { resume: pickedUp, forkSession: true } : {}),
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
- (isAssistant ? assistantBrief(person) : '') +
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 (isAssistant)
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 (isAssistant && liveFeed) {
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 (isAssistant)
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 (isAssistant)
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`);