@onepointfour-packs/flaredeck-agent 0.2.215 → 0.2.217

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
@@ -11,7 +11,7 @@ import { answerFrom, createPresence, heldFor, modeCommand } from './presence.js'
11
11
  import { defaultUploadRoots, fileTools, flaredeckStores } from './filestore.js';
12
12
  import { scratchFor } from './cardmess.js';
13
13
  import { installScripts, scriptTools, SCRIPT_TOOL_NAMES } from './trusted-scripts.js';
14
- import { boardsNaming, fetchLadder, isSoloWorker, namedOnLadders, releaseTools } from './release.js';
14
+ import { boardsNaming, fetchLadder, isSoloWorker, namedOnLadders, releasePermissionAsk, releaseRightsBrief, releaseRightsForJudge, releaseTools } from './release.js';
15
15
  import { PROFILES, profileFor, releaseRole, releasesOwnCards } from './profile.js';
16
16
  import { ASSISTANT_KEY } from './assistant.js';
17
17
  import { codePrefix, loadPrefixes, savePrefixes, SCRATCH_SWEEP_EVERY_MS, sweepScratch } from './scratch.js';
@@ -22,6 +22,7 @@ import { fetchTeam, myTeam, teamTools, TEAM_TOOL_NAMES } from './team.js';
22
22
  import { scheduleTools, SCHEDULE_TOOL_NAMES } from './schedules.js';
23
23
  import { preflightFlaredeck } from './flaredeck-check.js';
24
24
  import { callFlaredeckTool, flaredeckProxy, offeredTo } from './flaredeck-proxy.js';
25
+ import { NO_TIMESHEETS, TimesheetLedger, timesheetRule } from './timesheets.js';
25
26
  import { SCRIPT_REQUEST, allTargets, rerouteTo, resumeApprovedRuns, runTools } from './runs.js';
26
27
  import { deliverDecision, flaredeckDecider } from './approval.js';
27
28
  import { subscriptionsForAgent } from './subscriptions.js';
@@ -717,13 +718,36 @@ export async function serve(opts) {
717
718
  `your branch and your notes are as you left them. If it does not, say so and ask again ` +
718
719
  `rather than guessing.\n`);
719
720
  };
721
+ /** When the turn now running began: anything a person said after that, the agent has not read. */
722
+ let turnSince = 0;
720
723
  const park = (cardKey, why) => {
721
724
  if (!cardKey)
722
725
  return;
723
- parked.set(cardKey, { why, since: Date.now() });
726
+ /* From the start of the turn that parks it, not the moment it parks. HRG-0012 (30 Oct): Cobus
727
+ answered "Yes, release to prod" while FRIDAY's turn was still flagging the card; the answer
728
+ arrived four seconds before the park, counted as "before the question", and the card sat
729
+ until somebody else replied. What arrived before the turn began is still excluded (HR-0932). */
730
+ const since = turnSince && Date.now() - turnSince < 2 * 60 * 60_000 ? turnSince : Date.now();
731
+ parked.set(cardKey, { why, since });
724
732
  };
725
733
  /** What the agent can reach for the card in hand, for the guard's judge. Set per card. */
726
734
  let reachable = null;
735
+ /** The ladder of the board this turn's card is on — the release settings Jev and the agent are told. */
736
+ let boardLadder = null;
737
+ const ladderCache = new Map();
738
+ const ladderFor = async (boardId) => {
739
+ if (!boardId)
740
+ return null;
741
+ const hit = ladderCache.get(boardId);
742
+ if (hit && Date.now() - hit.at < 5 * 60_000)
743
+ return hit.ladder;
744
+ const tid = tenantFromToken(await opts.auth());
745
+ const ladder = tid ? await fetchLadder(config.mcpUrl, tid, boardId, () => opts.auth()).catch(() => null) : null;
746
+ ladderCache.set(boardId, { at: Date.now(), ladder });
747
+ return ladder;
748
+ };
749
+ /** The role the release tool checks against — said the same way to the agent and to Jev. */
750
+ const myReleaseRole = () => releaseRole(profile, soloWorker);
727
751
  /**
728
752
  * The pane's list of script requests waiting on a person: this agent's own request and, on a
729
753
  * lead's pane, its team's. Read from the server — the one place every request and verdict
@@ -856,6 +880,9 @@ export async function serve(opts) {
856
880
  : '')
857
881
  : 'no running services and no database of its own — nothing is started for this card');
858
882
  out.push('it CANNOT push to any git branch: publishing goes through a reviewed path it does not control');
883
+ const rights = releaseRightsForJudge(boardLadder, myReleaseRole(), selfId ?? '');
884
+ if (rights)
885
+ out.push(rights);
859
886
  const targets = Object.keys(config.runs ?? {});
860
887
  out.push(targets.length
861
888
  ? `production systems the harness can run an APPROVED script against: ${targets.join(', ')} — only after a person approves that exact script, never by a command the agent runs itself`
@@ -879,6 +906,7 @@ export async function serve(opts) {
879
906
  const runsSet = opts.deck
880
907
  ? runTools(config.runs ?? {}, () => onCard, opts.deck, (t) => log('tool', t), {
881
908
  attach: (card, path, comment) => attachFile(card, path, comment),
909
+ releaseAsk: (text) => releasePermissionAsk(text, boardLadder, myReleaseRole(), selfId ?? ''),
882
910
  sendFile: (path, to, message) => sendFile(onCard?.cwd ?? config.repo, path, to || chat.person()?.uid || '', message),
883
911
  flag: async (reason, kind, ask, flagOpts) => {
884
912
  const cur = onCard;
@@ -1053,7 +1081,10 @@ export async function serve(opts) {
1053
1081
  const until = presence.deadline();
1054
1082
  state.asks = presence.held().map((a) => ({ card: a.code || a.card, question: a.question, options: a.options, person: a.person, ...(until ? { until } : {}) }));
1055
1083
  };
1056
- const keepTool = (name) => offeredTo(profile.tools, name);
1084
+ /** The workspace's timesheet rule (Integrations → Agent timesheets), and what has been logged. */
1085
+ let timesheets = NO_TIMESHEETS;
1086
+ const timesheetLedger = new TimesheetLedger();
1087
+ const keepTool = (name) => offeredTo(profile.tools, name, timesheets.required);
1057
1088
  /** Cards handed to the lead for review, and the branch head then. See features/hand-back.ts. */
1058
1089
  const handedBack = handBackMemory();
1059
1090
  try {
@@ -1087,6 +1118,9 @@ export async function serve(opts) {
1087
1118
  if (leadId)
1088
1119
  log('tool', `your lead is ${leadName} — ${leadName} verifies your work before it goes anywhere`);
1089
1120
  profile = profileFor(team.you.role);
1121
+ timesheets = timesheetRule(team.you.timesheets);
1122
+ if (timesheets.required)
1123
+ log('tool', 'this workspace bills agents\' time — every card you finish gets a timesheet entry');
1090
1124
  if (profile.chat) {
1091
1125
  state.waitingFor = undefined;
1092
1126
  await chat.start(tenantFromToken(await opts.auth()));
@@ -2851,6 +2885,7 @@ export async function serve(opts) {
2851
2885
  if (beside.length && !event.alongside?.length)
2852
2886
  event = { ...event, alongside: beside };
2853
2887
  }
2888
+ turnSince = Date.now();
2854
2889
  let repoChoiceNote = '';
2855
2890
  // An assistant's card on a board it releases for: the board says which repository. A card the
2856
2891
  // harness picked up itself carries none, and with one checkout here the turn took that one —
@@ -3785,6 +3820,7 @@ export async function serve(opts) {
3785
3820
  lead: leadId ? { id: leadId, name: leadName } : null,
3786
3821
  selfId,
3787
3822
  handedBack,
3823
+ timesheet: () => timesheetLedger.gate(tag || event.cardCode || event.cardKey || '', timesheets, event.cardKey || ''),
3788
3824
  handToLead: async (code, message) => {
3789
3825
  const token = await opts.auth();
3790
3826
  await callFlaredeckTool(config.mcpUrl, token, 'assign-card', { card_id: code, assignee: leadId });
@@ -3810,7 +3846,7 @@ export async function serve(opts) {
3810
3846
  tag = code;
3811
3847
  state.working = code;
3812
3848
  };
3813
- const flaredeck = flaredeckProxy(config.mcpServerName, config.mcpUrl, opts.auth, check.discovered, (w) => log('warn', w), gate, noteCard, keepTool, savedSoFar, [...myBoards]);
3849
+ const flaredeck = flaredeckProxy(config.mcpServerName, config.mcpUrl, opts.auth, check.discovered, (w) => log('warn', w), gate, noteCard, keepTool, savedSoFar, [...myBoards], (name, args) => timesheetLedger.record(name, args));
3814
3850
  const card = await prepareCard(event, key, repo, queuedBehind);
3815
3851
  if (!card)
3816
3852
  return;
@@ -3828,6 +3864,7 @@ export async function serve(opts) {
3828
3864
  * named. FlareDeck takes a human code wherever it takes a card id, so the code alone is
3829
3865
  * enough to flag, comment and clear.
3830
3866
  */
3867
+ boardLadder = await ladderFor(event.boardKey);
3831
3868
  onCard = event.cardKey
3832
3869
  ? {
3833
3870
  card: event.cardKey,
@@ -3925,6 +3962,7 @@ export async function serve(opts) {
3925
3962
  standing: standingBrief(!!teamSet, config.style ?? process.env['FLAREDECK_AGENT_STYLE'], isAssistantChat, config.mcpServers, config.trustedScripts),
3926
3963
  finish: gate,
3927
3964
  noteCard,
3965
+ called: (name, args) => timesheetLedger.record(name, args),
3928
3966
  keep: keepTool,
3929
3967
  saved: savedSoFar,
3930
3968
  token: await opts.auth(),
@@ -3983,6 +4021,7 @@ export async function serve(opts) {
3983
4021
  const brief = waitedNote(event) + prompt(event, profile.dispatch) + thread + intakeText(prepared) +
3984
4022
  workspaceBrief(config.repo, await checkouts(config.repo, scope), chosen) +
3985
4023
  worktreeBrief(tree) + (tree ? folderBrief(tree) : '') + notes + repoChoiceNote + (await resumeBrief(event.cardCode || event.cardKey || '', tree)) +
4024
+ releaseRightsBrief(boardLadder, myReleaseRole(), selfId ?? '') +
3986
4025
  unclearBrief(prepared?.jev ?? null, leadName) + preflightBrief(checks) + backBrief(event) +
3987
4026
  // What this kind of agent is told on top of the card: see roleBriefs in harness/prompt.ts.
3988
4027
  roleBriefs(profile, { event, chat: isAssistantChat, person: chat.person(), leadWithoutCode, queuedBehind }) +