@onepointfour-packs/flaredeck-agent 0.2.202 → 0.2.204

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,6 +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 { locateScripts } from './trusted-scripts.js';
12
13
  import { boardsNaming, fetchLadder, isSoloWorker, namedOnLadders, releaseTools } from './release.js';
13
14
  import { LiveFeed, liveSender, midTurnMessage, ASSISTANT_KEY, claudeProjectDir, historyMessage, recentExchanges, latestCliSession, assistantBrief, assistantIgnores, assistantPerson, asAssistantEvent, loadAssistantSession, saveAssistantSession } from './assistant.js';
14
15
  import { codePrefix, loadPrefixes, savePrefixes, SCRATCH_SWEEP_EVERY_MS, sweepScratch } from './scratch.js';
@@ -42,7 +43,7 @@ export function neverStarted(e) {
42
43
  }
43
44
  import { isJustReading, jevGuard, judgeCommand } from './jevguard.js';
44
45
  import { loadProviders } from './providers.js';
45
- import { cardWorkOnBase, currentBranch, ensureWorktree, finishRefusalStands, freeDiskBytes, headOf, leadGaveItBack, sweepWorktrees, releaseSharedPushGuard, existingWorktree, produced, releaseWorktree, tooEarlyToFinish, uncommittedCount, worktreeBrief } from './worktree.js';
46
+ import { cardWorkOnBase, currentBranch, ensureWorktree, finishRefusalStands, freeDiskBytes, headOf, leadGaveItBack, useGitHubToken, sweepWorktrees, releaseSharedPushGuard, existingWorktree, produced, releaseWorktree, tooEarlyToFinish, uncommittedCount, worktreeBrief } from './worktree.js';
46
47
  import { askingForComponent, checkouts, repoIdentity, resolveCheckout, voteByCode, workspaceBrief, describeCheckout } from './workspace.js';
47
48
  import { chooseRepoWithJev } from './jev.js';
48
49
  import { ROUTE, intake, intakeText, unclearBrief } from './intake.js';
@@ -68,7 +69,7 @@ import { mergeableComments, combineComments, unparkReason, isBossInstruction, is
68
69
  import { LogShipper } from './log-ship.js';
69
70
  import { isOwnMove } from './own-moves.js';
70
71
  import { baseBranchesFromComponent } from './workspace.js';
71
- import { isAfterReleaseMove } from './flaredeck-proxy.js';
72
+ import { isAfterReleaseMove, isReleasedClose } from './flaredeck-proxy.js';
72
73
  import { canPublish, canPushTo } from './githubauth.js';
73
74
  /**
74
75
  * The always-on agent.
@@ -525,6 +526,7 @@ export async function serve(opts) {
525
526
  log('tool', gh ? `GitHub: using FlareDeck's connection (@${gh.login ?? 'unknown'})` : 'GitHub: this machine\'s own login');
526
527
  }
527
528
  flaredeckGithub = gh;
529
+ useGitHubToken(gh?.token);
528
530
  /* Jam for intake: the workspace's jam connection, asked for again on each read (its token
529
531
  is refreshed in FlareDeck). Found by URL as well as name, so a connection named
530
532
  differently still counts. */
@@ -541,6 +543,19 @@ export async function serve(opts) {
541
543
  setProvidersFromFlaredeck(hydrated.providers);
542
544
  const { setSecretsFromFlaredeck } = await import('./secrets.js');
543
545
  setSecretsFromFlaredeck(hydrated.secrets);
546
+ // Trusted scripts, found in this machine's checkouts: this folder, its siblings, card clones.
547
+ {
548
+ const seen = new Map();
549
+ for (const root of [config.repo, dirname(config.repo), cardReposRoot()])
550
+ for (const c of await checkouts(root).catch(() => []))
551
+ seen.set(c.path, c);
552
+ const found = locateScripts(hydrated.allowedScripts ?? [], [...seen.values()]);
553
+ const was = (config.trustedScripts ?? []).map((t) => t.abs).join('|');
554
+ config.trustedScripts = found;
555
+ config.otherScripts = hydrated.otherScripts ?? [];
556
+ if (found.map((t) => t.abs).join('|') !== was && found.length)
557
+ log('tool', `trusted scripts: ${found.map((t) => t.path).join(', ')}`);
558
+ }
544
559
  // Skills shared from FlareDeck, kept current the same way. See skills.ts.
545
560
  const { flaredeckSkills, syncSkills } = await import('./skills.js');
546
561
  const plugin = await syncSkills(join(configDir(), 'skills', slug(config.repo)), flaredeckSkills(config.mcpUrl, tenantForMcp, () => opts.auth(), (config.boards ?? []).map((b) => b.key)), (t, warn) => log(warn ? 'warn' : 'tool', t));
@@ -868,6 +883,7 @@ export async function serve(opts) {
868
883
  repo: config.repo,
869
884
  card: () => onCard?.card ?? null,
870
885
  cwd: () => reachable?.cwd ?? job?.cwd ?? null,
886
+ trusted: () => config.trustedScripts ?? [],
871
887
  access: () => {
872
888
  const out = [];
873
889
  if (reachable?.cwd)
@@ -1041,10 +1057,17 @@ export async function serve(opts) {
1041
1057
  * are assigned to it). Both have to be true: assigned to it, on a board it releases for.
1042
1058
  */
1043
1059
  let releaseBoards = new Set();
1044
- const assistantCard = (e) => isAssistant && releaseBoards.size > 0 && (
1045
- // Its own sweep of those boards is card work too: it runs as a worker turn, not in the chat.
1060
+ /**
1061
+ * An assistant works the cards PEOPLE give it, on any board (Cobus, 20 Oct): FlareDeck refuses an
1062
+ * agent — a lead, a manager, another bot — that tries to assign it one, so any card it holds came
1063
+ * from a person. Card work is a worker's turn (its own checkout where the board has code); its
1064
+ * person's chat stays one conversation. Releasing is separate: only where a board names it.
1065
+ */
1066
+ const assistantCard = (e) => isAssistant && (
1067
+ // Its own sweep and its own pickups are card work too: worker turns, not the chat.
1046
1068
  (e.event === 'sweep' && e.id === 'sweep')
1047
- || (!!(e.cardKey || e.cardCode) && !!e.boardKey && releaseBoards.has(e.boardKey)));
1069
+ || String(e.id ?? '').startsWith('resume-')
1070
+ || (!!(e.cardKey || e.cardCode) && (e.assigneeKey === selfId || (e.event === 'assigned' && e.forYou === true))));
1048
1071
  /**
1049
1072
  * One person's own agent: on no team, never dispatched to, listening only to `person`. One long
1050
1073
  * conversation, never handed over — the SDK compacts it — and loose rules. See ASSISTANT_BRIEF.
@@ -2128,8 +2151,7 @@ export async function serve(opts) {
2128
2151
  await enterPause(wall);
2129
2152
  })();
2130
2153
  const sweepNow = () => {
2131
- // An assistant sweeps only the boards it releases for, where it has cards of its own.
2132
- if (paused || !sweepsEnabled || stopping || (isAssistant && !releaseBoards.size))
2154
+ if (paused || !sweepsEnabled || stopping)
2133
2155
  return;
2134
2156
  // Busy is reason enough to wait: the next tick is minutes away and the work is not going
2135
2157
  // anywhere.
@@ -2148,7 +2170,7 @@ export async function serve(opts) {
2148
2170
  // Asked here first, for nothing: a sweep used to start a whole session (28k tokens) only for
2149
2171
  // the model to call get-outstanding, hear "nothing", and write an empty handover — four idle
2150
2172
  // agents doing it in one half hour (19 Oct). The turn runs only when something is there.
2151
- const sweepBoards = isAssistant ? [...releaseBoards] : [...myBoards];
2173
+ const sweepBoards = [...myBoards];
2152
2174
  const outstanding = String(await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'get-outstanding', {
2153
2175
  ...(boardArg(sweepBoards) !== undefined ? { board: boardArg(sweepBoards) } : {}),
2154
2176
  }).catch(() => '') ?? '');
@@ -2158,7 +2180,7 @@ export async function serve(opts) {
2158
2180
  }
2159
2181
  if (working || queue.length || live.size)
2160
2182
  return;
2161
- const only = isAssistant ? ` on ${(config.boards ?? []).filter((b) => releaseBoards.has(b.key)).map((b) => b.name).join(', ') || 'the boards you release for'} only — your other boards are your person's to direct` : '';
2183
+ const only = isAssistant ? ' — only the cards people have given you; nothing from the pile, and anything else is your person\'s to direct' : '';
2162
2184
  if (enqueue({ id: 'sweep', event: 'sweep', line: `checking what is still assigned to you${only}` }))
2163
2185
  void drain();
2164
2186
  })();
@@ -2213,9 +2235,6 @@ export async function serve(opts) {
2213
2235
  ...[...resumedIdle].filter(([, n]) => n >= RESUME_IDLE_LIMIT).map(([c]) => c),
2214
2236
  ])];
2215
2237
  const resumeOwnWork = async () => {
2216
- // An assistant's own work is only the cards on boards it releases for; otherwise its person's asks.
2217
- if (isAssistant && !releaseBoards.size)
2218
- return false;
2219
2238
  /* Not `live.size`: a session kept WARM for a follow-up is a cache, not work, and blocking on
2220
2239
  it meant the card in progress was never resumed within fifteen minutes of any turn. */
2221
2240
  if (paused || !sweepsEnabled || stopping || working || queue.length)
@@ -2226,7 +2245,7 @@ export async function serve(opts) {
2226
2245
  of its own waiting. */
2227
2246
  let answer = '';
2228
2247
  try {
2229
- const pullFrom = isAssistant ? [...releaseBoards] : [...myBoards];
2248
+ const pullFrom = [...myBoards];
2230
2249
  answer = String(await callFlaredeckTool(config.mcpUrl, await opts.auth(), 'pull-next-card', {
2231
2250
  ...(boardArg(pullFrom) !== undefined ? { board: boardArg(pullFrom) } : {}),
2232
2251
  // Cards this harness will not work now: waiting on a person, or picked up with nothing new.
@@ -3043,18 +3062,25 @@ export async function serve(opts) {
3043
3062
  // An assistant's card on a board it releases for: the board says which repository. A card the
3044
3063
  // harness picked up itself carries none, and with one checkout here the turn took that one —
3045
3064
  // FRIDAY worked HRG-0001 in the HR Goat app checkout, not the website's (20 Oct).
3065
+ // A board with no repository at all (a sales board): the card has no code, so no checkout — it
3066
+ // is worked from the assistant's own folder, with its email and calendar tools.
3067
+ let assistantNoCode = false;
3046
3068
  if (isAssistant && !isAssistantChat && !(event.baseBranches ?? []).some((e) => e.repository) && event.boardKey) {
3047
3069
  const tid = tenantFromToken(await opts.auth());
3048
3070
  const l = tid ? await fetchLadder(config.mcpUrl, tid, event.boardKey, () => opts.auth()).catch(() => null) : null;
3049
3071
  const urls = (l?.repositories ?? []).map((r) => r.url).filter(Boolean);
3050
3072
  if (urls.length)
3051
3073
  event = { ...event, baseBranches: urls.map((u) => ({ component: '', repository: u, branch: '', role: 'feature' })) };
3074
+ else if (l)
3075
+ assistantNoCode = true;
3052
3076
  }
3053
3077
  let where = await resolveCheckout(config.repo, event, scope, opts.agent ?? '');
3054
3078
  // An assistant's card for a repository that is not on this machine — its own folder is another
3055
3079
  // project (FRIDAY's is an HR Goat checkout, the website is HrGoatMarketing): cloned once, with
3056
3080
  // FlareDeck's GitHub connection, into the harness's own folder, and worked from there.
3057
- if (isAssistant && !isAssistantChat && where.kind !== 'repo') {
3081
+ if (assistantNoCode)
3082
+ where = { kind: 'none' };
3083
+ else if (isAssistant && !isAssistantChat && where.kind !== 'repo') {
3058
3084
  const cardRoot = cardReposRoot();
3059
3085
  const gh = flaredeckGithub;
3060
3086
  for (const url of new Set((event.baseBranches ?? []).map((e) => e.repository).filter(Boolean))) {
@@ -3185,7 +3211,7 @@ export async function serve(opts) {
3185
3211
  }
3186
3212
  // An intake review reads the card and the code; it builds nothing, so it gets no checkout. HR-1004's
3187
3213
  // review made a full worktree (and from dev, which the board no longer uses) just to read a card.
3188
- const tree = event.event === 'intake' || isLeadDispatch(event, isLead) ? null
3214
+ const tree = event.event === 'intake' || isLeadDispatch(event, isLead) || assistantNoCode ? null
3189
3215
  : event.cardKey && (chosen || where.kind === 'none')
3190
3216
  ? await ensureWorktree(repo, key, (t) => log('tool', t), startFrom, config.worktreeLocalFiles)
3191
3217
  : await worktreeTheMessageIsAbout(event, repo, (t) => log('tool', t), (code) => ensureWorktree(repo, code, (t) => log('tool', t), startFrom, config.worktreeLocalFiles));
@@ -3289,12 +3315,17 @@ export async function serve(opts) {
3289
3315
  // here rather than trusted, because a turn that produced nothing still reports confidently.
3290
3316
  const finishGate = async (toLane) => {
3291
3317
  // A released card moving on to testing or review is the next step of the board's flow.
3292
- if (toLane && opts.deck) {
3318
+ if (opts.deck) {
3293
3319
  const here = await opts.deck.card(tag || event.cardCode || event.cardKey || '').catch(() => null);
3294
- if (here && isAfterReleaseMove(toLane, here.lane?.type, here.lane?.name, (Array.isArray(here.releases) && here.releases.length > 0) || !!here.deployedAt)) {
3320
+ const released = !!here && ((Array.isArray(here.releases) && here.releases.length > 0) || !!here.deployedAt);
3321
+ if (here && toLane && isAfterReleaseMove(toLane, here.lane?.type, here.lane?.name, released)) {
3295
3322
  log('tool', `moving ${here.code || tag} on to ${toLane} — it was released already, so this is the next step, not a hand-back`);
3296
3323
  return null;
3297
3324
  }
3325
+ if (here && isReleasedClose(toLane ?? '', released)) {
3326
+ log('tool', `closing ${here.code || tag} — its work was released already`);
3327
+ return null;
3328
+ }
3298
3329
  }
3299
3330
  const why = await tooEarlyToFinish(tree);
3300
3331
  /* A LEAD moving a card on with nothing of its own on the branch is dispatch, not a claim of
@@ -3956,7 +3987,7 @@ export async function serve(opts) {
3956
3987
  // Both only reachable in-process, so an out-of-process engine is handed them explicitly
3957
3988
  // rather than being left unable to publish and unchecked when it says it has.
3958
3989
  ...(job ? { publish: async () => publishBranch(job) } : {}),
3959
- standing: standingBrief(!!teamSet, config.style ?? process.env['FLAREDECK_AGENT_STYLE'], isAssistantChat, config.mcpServers),
3990
+ standing: standingBrief(!!teamSet, config.style ?? process.env['FLAREDECK_AGENT_STYLE'], isAssistantChat, config.mcpServers, config.trustedScripts, config.otherScripts),
3960
3991
  finish: finishGate,
3961
3992
  noteCard,
3962
3993
  keep: keepTool,