agent-board-runner 0.1.7 → 0.1.8
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/cli.js +133 -29
- package/dist/index.js +133 -29
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -202,7 +202,7 @@ var BOX_DRAWING_PATTERN = /[─-╿]/g;
|
|
|
202
202
|
var WORKING_PATTERN = /esc to interrupt/i;
|
|
203
203
|
var BACKGROUND_SHELL_PATTERN = /\b[1-9]\d*\s+shells?\b/i;
|
|
204
204
|
function isAgentHookMoment(value) {
|
|
205
|
-
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
205
|
+
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "waiting_prototype_review" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
206
206
|
}
|
|
207
207
|
var USAGE_LIMIT_PATTERN = /(usage limit reached|limit reached[^\n]{0,60}resets|reached your (?:usage|session) limit|out of (?:weekly|session) (?:usage|limit))/i;
|
|
208
208
|
var PLAN_APPROVAL_PATTERN = /would you like to proceed\??/i;
|
|
@@ -246,6 +246,14 @@ var AgentActivityWatcher = class {
|
|
|
246
246
|
lastOutputAt = Date.now();
|
|
247
247
|
usageLimited = false;
|
|
248
248
|
planApprovalPending = false;
|
|
249
|
+
/**
|
|
250
|
+
* Latched like planApprovalPending, but with no text-fallback path: there is no on-screen CLI
|
|
251
|
+
* prompt for this (see publish_prototype / docs/ai-knowledges/prototipacao-visual.md), only the
|
|
252
|
+
* PostToolUse hook. The agent's own instructions have it stop the turn right after calling that
|
|
253
|
+
* tool, so the Stop event normally right behind it must not downgrade this to "done"/
|
|
254
|
+
* "waiting_user" — see the guard in reportHookMoment's "turn_stopped" case.
|
|
255
|
+
*/
|
|
256
|
+
prototypeReviewPending = false;
|
|
249
257
|
toolCallPending = false;
|
|
250
258
|
commitDetected = false;
|
|
251
259
|
timer;
|
|
@@ -287,6 +295,7 @@ var AgentActivityWatcher = class {
|
|
|
287
295
|
case "implementing":
|
|
288
296
|
this.usageLimited = false;
|
|
289
297
|
this.planApprovalPending = false;
|
|
298
|
+
this.prototypeReviewPending = false;
|
|
290
299
|
this.commitDetected = false;
|
|
291
300
|
this.setState("implementing");
|
|
292
301
|
break;
|
|
@@ -298,11 +307,16 @@ var AgentActivityWatcher = class {
|
|
|
298
307
|
this.planApprovalPending = true;
|
|
299
308
|
this.setState("waiting_plan_approval");
|
|
300
309
|
break;
|
|
310
|
+
case "waiting_prototype_review":
|
|
311
|
+
this.prototypeReviewPending = true;
|
|
312
|
+
this.setState("waiting_prototype_review");
|
|
313
|
+
break;
|
|
301
314
|
case "commit_detected":
|
|
302
315
|
this.commitDetected = true;
|
|
303
316
|
break;
|
|
304
317
|
case "turn_stopped":
|
|
305
318
|
if (this.usageLimited) break;
|
|
319
|
+
if (this.prototypeReviewPending) break;
|
|
306
320
|
if (BACKGROUND_SHELL_PATTERN.test(this.recentOutput())) {
|
|
307
321
|
this.setState("waiting_background");
|
|
308
322
|
break;
|
|
@@ -767,7 +781,7 @@ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
|
767
781
|
}
|
|
768
782
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
769
783
|
const { data, error } = await supabase.from("sessions").select(
|
|
770
|
-
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first, prerequisite_card_id)"
|
|
784
|
+
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
|
|
771
785
|
).eq("cards.assigned_user_id", ownerUserId).eq("status", "queued").order("created_at", { ascending: true });
|
|
772
786
|
if (error) throw error;
|
|
773
787
|
return data;
|
|
@@ -814,9 +828,11 @@ async function updateSession(supabase, sessionId, changes, activityContext) {
|
|
|
814
828
|
}
|
|
815
829
|
}
|
|
816
830
|
async function getActiveSessionForCard(supabase, cardId) {
|
|
817
|
-
const { data, error } = await supabase.from("sessions").select("id, status").eq("card_id", cardId).in("status", ["queued", "running", "waiting_input", "paused"]).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
831
|
+
const { data, error } = await supabase.from("sessions").select("id, status, agent_state").eq("card_id", cardId).in("status", ["queued", "running", "waiting_input", "paused"]).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
818
832
|
if (error) throw error;
|
|
819
|
-
return
|
|
833
|
+
if (!data) return null;
|
|
834
|
+
const row = data;
|
|
835
|
+
return { id: row.id, status: row.status, agentState: row.agent_state };
|
|
820
836
|
}
|
|
821
837
|
async function pauseRunningSession(supabase, sessionId, activityContext) {
|
|
822
838
|
const { data, error } = await supabase.from("sessions").update({ status: "paused" }).eq("id", sessionId).eq("status", "running").select("id");
|
|
@@ -839,6 +855,10 @@ async function getLastClaudeSessionIdForCard(supabase, cardId) {
|
|
|
839
855
|
return data?.claude_session_id ?? null;
|
|
840
856
|
}
|
|
841
857
|
async function getLatestAgentStateForCard(supabase, cardId) {
|
|
858
|
+
const { data: card, error: cardError } = await supabase.from("cards").select("column_name").eq("id", cardId).maybeSingle();
|
|
859
|
+
if (cardError) throw cardError;
|
|
860
|
+
const column = card?.column_name;
|
|
861
|
+
if (column && BOARD_COLUMNS.indexOf(column) > BOARD_COLUMNS.indexOf("desenvolvimento")) return "done";
|
|
842
862
|
const { data, error } = await supabase.from("sessions").select("agent_state").eq("card_id", cardId).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
843
863
|
if (error) throw error;
|
|
844
864
|
return data?.agent_state ?? null;
|
|
@@ -1063,6 +1083,13 @@ function detectMoment(input) {
|
|
|
1063
1083
|
// or self-corrects within a beat \u2014 never gets stuck on "waiting_plan_approval" forever.
|
|
1064
1084
|
if (eventName === "PostToolUse" && input.tool_name === "ExitPlanMode") return "implementing";
|
|
1065
1085
|
|
|
1086
|
+
// The agent just published a prototype Artifact and recorded it on the card (see
|
|
1087
|
+
// publish_prototype in boardTools.ts and docs/ai-knowledges/prototipacao-visual.md). Its own
|
|
1088
|
+
// instructions tell it to stop the turn right after this call, so the Stop event normally
|
|
1089
|
+
// right behind this one must not downgrade this to "turn_stopped" \u2014 see the
|
|
1090
|
+
// prototypeReviewPending latch in agentActivity.ts's reportHookMoment.
|
|
1091
|
+
if (eventName === "PostToolUse" && input.tool_name === "mcp__agent-board__publish_prototype") return "waiting_prototype_review";
|
|
1092
|
+
|
|
1066
1093
|
// A commit that actually succeeded, read off the Bash tool's own structured result instead
|
|
1067
1094
|
// of guessing from scrollback text (exact, and immune to locale/color/format changes to
|
|
1068
1095
|
// git's own banner). Any other Bash completion still reports "tool_active" below, same as
|
|
@@ -2834,18 +2861,18 @@ async function enterPlanMode(pty, cardId) {
|
|
|
2834
2861
|
subscription.dispose();
|
|
2835
2862
|
}
|
|
2836
2863
|
}
|
|
2837
|
-
function
|
|
2864
|
+
function sendMessageToIdleSession(pty, cardId, message) {
|
|
2838
2865
|
try {
|
|
2839
|
-
pty.write(`${PASTE_START}
|
|
2866
|
+
pty.write(`${PASTE_START}${message}${PASTE_END}`);
|
|
2840
2867
|
setTimeout(() => {
|
|
2841
2868
|
try {
|
|
2842
2869
|
pty.write("\r");
|
|
2843
2870
|
} catch (err) {
|
|
2844
|
-
logger.card.error(cardId,
|
|
2871
|
+
logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
|
|
2845
2872
|
}
|
|
2846
2873
|
}, SUBMIT_DELAY_MS);
|
|
2847
2874
|
} catch (err) {
|
|
2848
|
-
logger.card.error(cardId,
|
|
2875
|
+
logger.card.error(cardId, "Failed to send a message to the idle session:", err);
|
|
2849
2876
|
}
|
|
2850
2877
|
}
|
|
2851
2878
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
@@ -2931,6 +2958,46 @@ function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
|
2931
2958
|
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. If part of the delivery needs a human to do something you can't do yourself (rotate a secret, approve something in a third-party dashboard, delete a resource manually, etc.), register each step with \`add_manual_actions\` instead of just mentioning it in your final message \u2014 that's the only place humans will reliably see it. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2932
2959
|
}
|
|
2933
2960
|
|
|
2961
|
+
// src/prototypeGateNote.ts
|
|
2962
|
+
function buildInstructionsWithPrototypeGate(instructions, prototypeFirst) {
|
|
2963
|
+
if (!prototypeFirst) return instructions;
|
|
2964
|
+
return `${instructions}
|
|
2965
|
+
|
|
2966
|
+
Prototipagem visual obrigat\xF3ria antes de implementar (prototypeFirst est\xE1 ligado neste card):
|
|
2967
|
+
|
|
2968
|
+
1. Antes de qualquer altera\xE7\xE3o de c\xF3digo real, publique SOMENTE um prot\xF3tipo visual (um Claude
|
|
2969
|
+
Artifact \u2014 HTML est\xE1tico, sem l\xF3gica/integra\xE7\xE3o de verdade) do que voc\xEA vai construir.
|
|
2970
|
+
Reaproveite ao m\xE1ximo o design system REAL deste reposit\xF3rio (n\xE3o o do agent-board): leia o
|
|
2971
|
+
tailwind config, as CSS vars/tema e os componentes j\xE1 existentes neste \`cwd\` antes de
|
|
2972
|
+
desenhar qualquer coisa. Use as skills \`frontend-design\`/\`artifact-design\` j\xE1 dispon\xEDveis
|
|
2973
|
+
antes de publicar.
|
|
2974
|
+
2. Se os anexos/instru\xE7\xF5es deste card inclu\xEDrem um print de uma tela j\xE1 existente pedindo
|
|
2975
|
+
ajustes, o prot\xF3tipo \xE9 diferente: publique uma p\xE1gina INTERATIVA que mostra o print e deixa
|
|
2976
|
+
quem est\xE1 revisando clicar nele para soltar um pino numerado com um campo de texto por pino \u2014
|
|
2977
|
+
persista cada pino usando a capability \`db\` do pr\xF3prio Artifact (carregue a skill
|
|
2978
|
+
\`artifact-capabilities\` antes de declarar a capability). Nesse caso, a nota que voc\xEA passar
|
|
2979
|
+
para \`publish_prototype\` deve pedir para marcarem os pontos de ajuste nele, n\xE3o para
|
|
2980
|
+
aprovarem um resultado ainda.
|
|
2981
|
+
3. Chame a tool MCP \`publish_prototype\` com o link do Artifact e uma nota curta do que fez/pede.
|
|
2982
|
+
Essa chamada j\xE1 zera a aprova\xE7\xE3o e avisa os humanos do projeto \u2014 n\xE3o poste o link via
|
|
2983
|
+
add_card_comment manualmente tamb\xE9m, seria duplicado.
|
|
2984
|
+
4. Depois de chamar \`publish_prototype\`, PARE o turno \u2014 n\xE3o fique num loop verificando
|
|
2985
|
+
aprova\xE7\xE3o nem monitorando o artifact sozinho. A sess\xE3o ser\xE1 retomada automaticamente quando
|
|
2986
|
+
houver uma revis\xE3o nova (aprova\xE7\xE3o ou pedido de ajuste); tentar continuar por conta pr\xF3pria
|
|
2987
|
+
antes disso \xE9 desperd\xEDcio.
|
|
2988
|
+
5. Quando a sess\xE3o for retomada, a mensagem vai dizer se o prot\xF3tipo foi aprovado:
|
|
2989
|
+
- **Aprovado**: prossiga para a implementa\xE7\xE3o real agora, usando o prot\xF3tipo (e, se for o
|
|
2990
|
+
caso de ajuste em tela existente, os pontos marcados nele \u2014 leia-os de volta via leitura do
|
|
2991
|
+
banco do artifact) como refer\xEAncia. Se os pontos marcados forem grandes/numerosos o
|
|
2992
|
+
suficiente para valer separar, quebre em cards novos (\`create_card\` +
|
|
2993
|
+
\`prerequisiteCardId\`, encadeando os que dependem um do outro) e aplique o mesmo fluxo de
|
|
2994
|
+
prot\xF3tipo em cada um antes de implementar; resolva no pr\xF3prio card quando o ajuste for
|
|
2995
|
+
pequeno.
|
|
2996
|
+
- **N\xE3o aprovado**: confira os coment\xE1rios deixados no Artifact (e, se for a p\xE1gina de
|
|
2997
|
+
anota\xE7\xE3o, os pontos salvos), ajuste o prot\xF3tipo e publique a nova vers\xE3o de novo via
|
|
2998
|
+
\`publish_prototype\` na MESMA url \u2014 depois pare o turno de novo, voltando ao passo 4.`;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2934
3001
|
// src/usageResume.ts
|
|
2935
3002
|
var RESET_BUFFER_MS = 3e4;
|
|
2936
3003
|
var UNKNOWN_RESET_RETRY_MS = 5 * 6e4;
|
|
@@ -2952,7 +3019,7 @@ function scheduleUsageResume(supabase, config, cardId, sessionId, pty) {
|
|
|
2952
3019
|
);
|
|
2953
3020
|
const timer = setTimeout(() => {
|
|
2954
3021
|
pending.delete(sessionId);
|
|
2955
|
-
|
|
3022
|
+
sendMessageToIdleSession(pty, cardId, "continue");
|
|
2956
3023
|
}, delay);
|
|
2957
3024
|
timer.unref();
|
|
2958
3025
|
pending.set(sessionId, timer);
|
|
@@ -3020,7 +3087,7 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
3020
3087
|
}
|
|
3021
3088
|
const stale = data ?? [];
|
|
3022
3089
|
if (stale.length === 0) return;
|
|
3023
|
-
for (const { id: sessionId, card_id: cardId } of stale) {
|
|
3090
|
+
for (const { id: sessionId, card_id: cardId, cards } of stale) {
|
|
3024
3091
|
intentionallyStoppedSessionIds.add(sessionId);
|
|
3025
3092
|
stopPtySession(sessionId);
|
|
3026
3093
|
cancelUsageResume(sessionId);
|
|
@@ -3028,7 +3095,13 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
3028
3095
|
await updateSession(
|
|
3029
3096
|
supabase,
|
|
3030
3097
|
sessionId,
|
|
3031
|
-
{
|
|
3098
|
+
{
|
|
3099
|
+
status: "done",
|
|
3100
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3101
|
+
// See stopCardSession's doc comment: "fim" is the authoritative completion signal for
|
|
3102
|
+
// dispatchRunnerQueue's prerequisite gate, which only ever reads agent_state.
|
|
3103
|
+
...cards.column_name === "fim" ? { agentState: "done" } : {}
|
|
3104
|
+
},
|
|
3032
3105
|
{ projectId, cardId, userId: config.ownerUserId }
|
|
3033
3106
|
);
|
|
3034
3107
|
logger.card.info(cardId, `Ended stale session ${sessionId} (its card left "desenvolvimento" while this runner missed the event).`);
|
|
@@ -3039,6 +3112,7 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
3039
3112
|
if (dispatchingRunner) return;
|
|
3040
3113
|
dispatchingRunner = true;
|
|
3041
3114
|
let queued = [];
|
|
3115
|
+
const promotedCardIds = /* @__PURE__ */ new Set();
|
|
3042
3116
|
try {
|
|
3043
3117
|
queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
3044
3118
|
if (queued.length === 0) return;
|
|
@@ -3048,7 +3122,7 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
3048
3122
|
for (const project of config.registeredProjects) {
|
|
3049
3123
|
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
3050
3124
|
if (!running) continue;
|
|
3051
|
-
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
3125
|
+
if ((running.agentState === "done" || running.agentState === "waiting_prototype_review") && queuedProjectIds.has(project.id)) {
|
|
3052
3126
|
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
3053
3127
|
continue;
|
|
3054
3128
|
}
|
|
@@ -3075,22 +3149,24 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
3075
3149
|
try {
|
|
3076
3150
|
await promoteQueuedSession(supabase, config, entry);
|
|
3077
3151
|
promotedThisPass.add(projectId);
|
|
3152
|
+
promotedCardIds.add(entry.card_id);
|
|
3078
3153
|
freeProjectSlots -= 1;
|
|
3079
3154
|
} catch (err) {
|
|
3080
3155
|
logger.card.error(entry.card_id, `Failed to promote queued session ${entry.id}:`, err);
|
|
3081
3156
|
}
|
|
3082
3157
|
}
|
|
3083
3158
|
} finally {
|
|
3084
|
-
await publishQueueSnapshots(supabase, config, queued).catch(
|
|
3159
|
+
await publishQueueSnapshots(supabase, config, queued, promotedCardIds).catch(
|
|
3085
3160
|
(err) => logger.runner.error("ORCHESTRATOR", "Failed to publish queue snapshots:", err)
|
|
3086
3161
|
);
|
|
3087
3162
|
dispatchingRunner = false;
|
|
3088
3163
|
}
|
|
3089
3164
|
}
|
|
3090
|
-
async function publishQueueSnapshots(supabase, config, queued) {
|
|
3165
|
+
async function publishQueueSnapshots(supabase, config, queued, promotedCardIds) {
|
|
3091
3166
|
const paused = await listPausedSessionsForRunner(supabase, config.ownerUserId);
|
|
3092
3167
|
const queuedByProject = /* @__PURE__ */ new Map();
|
|
3093
3168
|
for (const entry of queued) {
|
|
3169
|
+
if (promotedCardIds.has(entry.card_id)) continue;
|
|
3094
3170
|
const list = queuedByProject.get(entry.cards.project_id) ?? [];
|
|
3095
3171
|
list.push(entry.card_id);
|
|
3096
3172
|
queuedByProject.set(entry.cards.project_id, list);
|
|
@@ -3108,6 +3184,9 @@ async function publishQueueSnapshots(supabase, config, queued) {
|
|
|
3108
3184
|
});
|
|
3109
3185
|
}
|
|
3110
3186
|
}
|
|
3187
|
+
function buildPrototypeReviewFollowUp(prototypeApproved, prototypeUrl) {
|
|
3188
|
+
return prototypeApproved ? "O prot\xF3tipo foi revisado e aprovado \u2014 prossiga para a implementa\xE7\xE3o real, usando-o como refer\xEAncia." : `O prot\xF3tipo ainda n\xE3o foi aprovado${prototypeUrl ? ` (${prototypeUrl})` : ""} \u2014 confira os coment\xE1rios deixados nele (e, se for um prot\xF3tipo de marca\xE7\xE3o de pontos, os pontos salvos) e ajuste, publicando a nova vers\xE3o com publish_prototype. Depois, pare o turno de novo: a sess\xE3o ser\xE1 retomada quando houver outra revis\xE3o.`;
|
|
3189
|
+
}
|
|
3111
3190
|
async function promoteQueuedSession(supabase, config, entry) {
|
|
3112
3191
|
const existingHandle = getPtySession(entry.id);
|
|
3113
3192
|
if (existingHandle) {
|
|
@@ -3120,6 +3199,13 @@ async function promoteQueuedSession(supabase, config, entry) {
|
|
|
3120
3199
|
);
|
|
3121
3200
|
activeSessionByCard.set(entry.card_id, entry.id);
|
|
3122
3201
|
logger.card.info(entry.card_id, "Resumed paused session from the front of the queue.");
|
|
3202
|
+
if (entry.agent_state === "waiting_prototype_review" && !existingHandle.exited) {
|
|
3203
|
+
sendMessageToIdleSession(
|
|
3204
|
+
existingHandle.pty,
|
|
3205
|
+
entry.card_id,
|
|
3206
|
+
buildPrototypeReviewFollowUp(entry.cards.prototype_approved, entry.cards.prototype_url)
|
|
3207
|
+
);
|
|
3208
|
+
}
|
|
3123
3209
|
return;
|
|
3124
3210
|
}
|
|
3125
3211
|
const spawned = await spawnSessionPty(supabase, config, entry);
|
|
@@ -3151,9 +3237,12 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
3151
3237
|
} else {
|
|
3152
3238
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
3153
3239
|
const instructions = buildInstructionsWithMcpContext(
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3240
|
+
buildInstructionsWithPrototypeGate(
|
|
3241
|
+
buildInstructionsWithFileReferences(
|
|
3242
|
+
buildInstructionsWithAttachments(card.instructions, attachmentPaths),
|
|
3243
|
+
card.file_references
|
|
3244
|
+
),
|
|
3245
|
+
card.prototype_first
|
|
3157
3246
|
),
|
|
3158
3247
|
cardId,
|
|
3159
3248
|
card.project_id
|
|
@@ -3233,7 +3322,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
3233
3322
|
} else {
|
|
3234
3323
|
cancelUsageResume(sessionId);
|
|
3235
3324
|
}
|
|
3236
|
-
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
3325
|
+
if (state === "done" || state === "waiting_prototype_review") void dispatchRunnerQueue(supabase, config);
|
|
3237
3326
|
});
|
|
3238
3327
|
pty.onData((chunk) => watcher.push(chunk));
|
|
3239
3328
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -3247,16 +3336,27 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
3247
3336
|
}
|
|
3248
3337
|
async function requeuePausedCard(supabase, config, card) {
|
|
3249
3338
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
3250
|
-
if (!active
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3339
|
+
if (!active) return;
|
|
3340
|
+
if (active.status === "paused") {
|
|
3341
|
+
await updateSession(
|
|
3342
|
+
supabase,
|
|
3343
|
+
active.id,
|
|
3344
|
+
{ status: "queued" },
|
|
3345
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3346
|
+
);
|
|
3347
|
+
await updateCard(supabase, card.id, { requeueRequestedAt: null });
|
|
3348
|
+
logger.card.info(card.id, `Session ${active.id} requeued at the front of the line (was paused).`);
|
|
3349
|
+
await dispatchRunnerQueue(supabase, config);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (active.status === "running" && active.agentState === "waiting_prototype_review") {
|
|
3353
|
+
const handle = getPtySession(active.id);
|
|
3354
|
+
await updateCard(supabase, card.id, { requeueRequestedAt: null });
|
|
3355
|
+
if (handle && !handle.exited) {
|
|
3356
|
+
sendMessageToIdleSession(handle.pty, card.id, buildPrototypeReviewFollowUp(card.prototype_approved, card.prototype_url));
|
|
3357
|
+
logger.card.info(card.id, `Session ${active.id} was never paused (alone in its project) \u2014 sent the prototype review follow-up directly.`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3260
3360
|
}
|
|
3261
3361
|
async function reconcileOrphanedSessions(supabase, config) {
|
|
3262
3362
|
for (const project of config.registeredProjects) {
|
|
@@ -3315,7 +3415,11 @@ async function stopCardSession(supabase, config, card) {
|
|
|
3315
3415
|
await updateSession(
|
|
3316
3416
|
supabase,
|
|
3317
3417
|
sessionId,
|
|
3318
|
-
{
|
|
3418
|
+
{
|
|
3419
|
+
status: "done",
|
|
3420
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3421
|
+
...card.column_name === "fim" ? { agentState: "done" } : {}
|
|
3422
|
+
},
|
|
3319
3423
|
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3320
3424
|
);
|
|
3321
3425
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ var BOX_DRAWING_PATTERN = /[─-╿]/g;
|
|
|
40
40
|
var WORKING_PATTERN = /esc to interrupt/i;
|
|
41
41
|
var BACKGROUND_SHELL_PATTERN = /\b[1-9]\d*\s+shells?\b/i;
|
|
42
42
|
function isAgentHookMoment(value) {
|
|
43
|
-
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
43
|
+
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "waiting_prototype_review" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
44
44
|
}
|
|
45
45
|
var USAGE_LIMIT_PATTERN = /(usage limit reached|limit reached[^\n]{0,60}resets|reached your (?:usage|session) limit|out of (?:weekly|session) (?:usage|limit))/i;
|
|
46
46
|
var PLAN_APPROVAL_PATTERN = /would you like to proceed\??/i;
|
|
@@ -84,6 +84,14 @@ var AgentActivityWatcher = class {
|
|
|
84
84
|
lastOutputAt = Date.now();
|
|
85
85
|
usageLimited = false;
|
|
86
86
|
planApprovalPending = false;
|
|
87
|
+
/**
|
|
88
|
+
* Latched like planApprovalPending, but with no text-fallback path: there is no on-screen CLI
|
|
89
|
+
* prompt for this (see publish_prototype / docs/ai-knowledges/prototipacao-visual.md), only the
|
|
90
|
+
* PostToolUse hook. The agent's own instructions have it stop the turn right after calling that
|
|
91
|
+
* tool, so the Stop event normally right behind it must not downgrade this to "done"/
|
|
92
|
+
* "waiting_user" — see the guard in reportHookMoment's "turn_stopped" case.
|
|
93
|
+
*/
|
|
94
|
+
prototypeReviewPending = false;
|
|
87
95
|
toolCallPending = false;
|
|
88
96
|
commitDetected = false;
|
|
89
97
|
timer;
|
|
@@ -125,6 +133,7 @@ var AgentActivityWatcher = class {
|
|
|
125
133
|
case "implementing":
|
|
126
134
|
this.usageLimited = false;
|
|
127
135
|
this.planApprovalPending = false;
|
|
136
|
+
this.prototypeReviewPending = false;
|
|
128
137
|
this.commitDetected = false;
|
|
129
138
|
this.setState("implementing");
|
|
130
139
|
break;
|
|
@@ -136,11 +145,16 @@ var AgentActivityWatcher = class {
|
|
|
136
145
|
this.planApprovalPending = true;
|
|
137
146
|
this.setState("waiting_plan_approval");
|
|
138
147
|
break;
|
|
148
|
+
case "waiting_prototype_review":
|
|
149
|
+
this.prototypeReviewPending = true;
|
|
150
|
+
this.setState("waiting_prototype_review");
|
|
151
|
+
break;
|
|
139
152
|
case "commit_detected":
|
|
140
153
|
this.commitDetected = true;
|
|
141
154
|
break;
|
|
142
155
|
case "turn_stopped":
|
|
143
156
|
if (this.usageLimited) break;
|
|
157
|
+
if (this.prototypeReviewPending) break;
|
|
144
158
|
if (BACKGROUND_SHELL_PATTERN.test(this.recentOutput())) {
|
|
145
159
|
this.setState("waiting_background");
|
|
146
160
|
break;
|
|
@@ -605,7 +619,7 @@ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
|
605
619
|
}
|
|
606
620
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
607
621
|
const { data, error } = await supabase.from("sessions").select(
|
|
608
|
-
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first, prerequisite_card_id)"
|
|
622
|
+
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
|
|
609
623
|
).eq("cards.assigned_user_id", ownerUserId).eq("status", "queued").order("created_at", { ascending: true });
|
|
610
624
|
if (error) throw error;
|
|
611
625
|
return data;
|
|
@@ -652,9 +666,11 @@ async function updateSession(supabase, sessionId, changes, activityContext) {
|
|
|
652
666
|
}
|
|
653
667
|
}
|
|
654
668
|
async function getActiveSessionForCard(supabase, cardId) {
|
|
655
|
-
const { data, error } = await supabase.from("sessions").select("id, status").eq("card_id", cardId).in("status", ["queued", "running", "waiting_input", "paused"]).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
669
|
+
const { data, error } = await supabase.from("sessions").select("id, status, agent_state").eq("card_id", cardId).in("status", ["queued", "running", "waiting_input", "paused"]).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
656
670
|
if (error) throw error;
|
|
657
|
-
return
|
|
671
|
+
if (!data) return null;
|
|
672
|
+
const row = data;
|
|
673
|
+
return { id: row.id, status: row.status, agentState: row.agent_state };
|
|
658
674
|
}
|
|
659
675
|
async function pauseRunningSession(supabase, sessionId, activityContext) {
|
|
660
676
|
const { data, error } = await supabase.from("sessions").update({ status: "paused" }).eq("id", sessionId).eq("status", "running").select("id");
|
|
@@ -677,6 +693,10 @@ async function getLastClaudeSessionIdForCard(supabase, cardId) {
|
|
|
677
693
|
return data?.claude_session_id ?? null;
|
|
678
694
|
}
|
|
679
695
|
async function getLatestAgentStateForCard(supabase, cardId) {
|
|
696
|
+
const { data: card, error: cardError } = await supabase.from("cards").select("column_name").eq("id", cardId).maybeSingle();
|
|
697
|
+
if (cardError) throw cardError;
|
|
698
|
+
const column = card?.column_name;
|
|
699
|
+
if (column && BOARD_COLUMNS.indexOf(column) > BOARD_COLUMNS.indexOf("desenvolvimento")) return "done";
|
|
680
700
|
const { data, error } = await supabase.from("sessions").select("agent_state").eq("card_id", cardId).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
681
701
|
if (error) throw error;
|
|
682
702
|
return data?.agent_state ?? null;
|
|
@@ -901,6 +921,13 @@ function detectMoment(input) {
|
|
|
901
921
|
// or self-corrects within a beat \u2014 never gets stuck on "waiting_plan_approval" forever.
|
|
902
922
|
if (eventName === "PostToolUse" && input.tool_name === "ExitPlanMode") return "implementing";
|
|
903
923
|
|
|
924
|
+
// The agent just published a prototype Artifact and recorded it on the card (see
|
|
925
|
+
// publish_prototype in boardTools.ts and docs/ai-knowledges/prototipacao-visual.md). Its own
|
|
926
|
+
// instructions tell it to stop the turn right after this call, so the Stop event normally
|
|
927
|
+
// right behind this one must not downgrade this to "turn_stopped" \u2014 see the
|
|
928
|
+
// prototypeReviewPending latch in agentActivity.ts's reportHookMoment.
|
|
929
|
+
if (eventName === "PostToolUse" && input.tool_name === "mcp__agent-board__publish_prototype") return "waiting_prototype_review";
|
|
930
|
+
|
|
904
931
|
// A commit that actually succeeded, read off the Bash tool's own structured result instead
|
|
905
932
|
// of guessing from scrollback text (exact, and immune to locale/color/format changes to
|
|
906
933
|
// git's own banner). Any other Bash completion still reports "tool_active" below, same as
|
|
@@ -2773,18 +2800,18 @@ async function enterPlanMode(pty, cardId) {
|
|
|
2773
2800
|
subscription.dispose();
|
|
2774
2801
|
}
|
|
2775
2802
|
}
|
|
2776
|
-
function
|
|
2803
|
+
function sendMessageToIdleSession(pty, cardId, message) {
|
|
2777
2804
|
try {
|
|
2778
|
-
pty.write(`${PASTE_START}
|
|
2805
|
+
pty.write(`${PASTE_START}${message}${PASTE_END}`);
|
|
2779
2806
|
setTimeout(() => {
|
|
2780
2807
|
try {
|
|
2781
2808
|
pty.write("\r");
|
|
2782
2809
|
} catch (err) {
|
|
2783
|
-
logger.card.error(cardId,
|
|
2810
|
+
logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
|
|
2784
2811
|
}
|
|
2785
2812
|
}, SUBMIT_DELAY_MS);
|
|
2786
2813
|
} catch (err) {
|
|
2787
|
-
logger.card.error(cardId,
|
|
2814
|
+
logger.card.error(cardId, "Failed to send a message to the idle session:", err);
|
|
2788
2815
|
}
|
|
2789
2816
|
}
|
|
2790
2817
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
@@ -2870,6 +2897,46 @@ function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
|
2870
2897
|
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. If part of the delivery needs a human to do something you can't do yourself (rotate a secret, approve something in a third-party dashboard, delete a resource manually, etc.), register each step with \`add_manual_actions\` instead of just mentioning it in your final message \u2014 that's the only place humans will reliably see it. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2871
2898
|
}
|
|
2872
2899
|
|
|
2900
|
+
// src/prototypeGateNote.ts
|
|
2901
|
+
function buildInstructionsWithPrototypeGate(instructions, prototypeFirst) {
|
|
2902
|
+
if (!prototypeFirst) return instructions;
|
|
2903
|
+
return `${instructions}
|
|
2904
|
+
|
|
2905
|
+
Prototipagem visual obrigat\xF3ria antes de implementar (prototypeFirst est\xE1 ligado neste card):
|
|
2906
|
+
|
|
2907
|
+
1. Antes de qualquer altera\xE7\xE3o de c\xF3digo real, publique SOMENTE um prot\xF3tipo visual (um Claude
|
|
2908
|
+
Artifact \u2014 HTML est\xE1tico, sem l\xF3gica/integra\xE7\xE3o de verdade) do que voc\xEA vai construir.
|
|
2909
|
+
Reaproveite ao m\xE1ximo o design system REAL deste reposit\xF3rio (n\xE3o o do agent-board): leia o
|
|
2910
|
+
tailwind config, as CSS vars/tema e os componentes j\xE1 existentes neste \`cwd\` antes de
|
|
2911
|
+
desenhar qualquer coisa. Use as skills \`frontend-design\`/\`artifact-design\` j\xE1 dispon\xEDveis
|
|
2912
|
+
antes de publicar.
|
|
2913
|
+
2. Se os anexos/instru\xE7\xF5es deste card inclu\xEDrem um print de uma tela j\xE1 existente pedindo
|
|
2914
|
+
ajustes, o prot\xF3tipo \xE9 diferente: publique uma p\xE1gina INTERATIVA que mostra o print e deixa
|
|
2915
|
+
quem est\xE1 revisando clicar nele para soltar um pino numerado com um campo de texto por pino \u2014
|
|
2916
|
+
persista cada pino usando a capability \`db\` do pr\xF3prio Artifact (carregue a skill
|
|
2917
|
+
\`artifact-capabilities\` antes de declarar a capability). Nesse caso, a nota que voc\xEA passar
|
|
2918
|
+
para \`publish_prototype\` deve pedir para marcarem os pontos de ajuste nele, n\xE3o para
|
|
2919
|
+
aprovarem um resultado ainda.
|
|
2920
|
+
3. Chame a tool MCP \`publish_prototype\` com o link do Artifact e uma nota curta do que fez/pede.
|
|
2921
|
+
Essa chamada j\xE1 zera a aprova\xE7\xE3o e avisa os humanos do projeto \u2014 n\xE3o poste o link via
|
|
2922
|
+
add_card_comment manualmente tamb\xE9m, seria duplicado.
|
|
2923
|
+
4. Depois de chamar \`publish_prototype\`, PARE o turno \u2014 n\xE3o fique num loop verificando
|
|
2924
|
+
aprova\xE7\xE3o nem monitorando o artifact sozinho. A sess\xE3o ser\xE1 retomada automaticamente quando
|
|
2925
|
+
houver uma revis\xE3o nova (aprova\xE7\xE3o ou pedido de ajuste); tentar continuar por conta pr\xF3pria
|
|
2926
|
+
antes disso \xE9 desperd\xEDcio.
|
|
2927
|
+
5. Quando a sess\xE3o for retomada, a mensagem vai dizer se o prot\xF3tipo foi aprovado:
|
|
2928
|
+
- **Aprovado**: prossiga para a implementa\xE7\xE3o real agora, usando o prot\xF3tipo (e, se for o
|
|
2929
|
+
caso de ajuste em tela existente, os pontos marcados nele \u2014 leia-os de volta via leitura do
|
|
2930
|
+
banco do artifact) como refer\xEAncia. Se os pontos marcados forem grandes/numerosos o
|
|
2931
|
+
suficiente para valer separar, quebre em cards novos (\`create_card\` +
|
|
2932
|
+
\`prerequisiteCardId\`, encadeando os que dependem um do outro) e aplique o mesmo fluxo de
|
|
2933
|
+
prot\xF3tipo em cada um antes de implementar; resolva no pr\xF3prio card quando o ajuste for
|
|
2934
|
+
pequeno.
|
|
2935
|
+
- **N\xE3o aprovado**: confira os coment\xE1rios deixados no Artifact (e, se for a p\xE1gina de
|
|
2936
|
+
anota\xE7\xE3o, os pontos salvos), ajuste o prot\xF3tipo e publique a nova vers\xE3o de novo via
|
|
2937
|
+
\`publish_prototype\` na MESMA url \u2014 depois pare o turno de novo, voltando ao passo 4.`;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2873
2940
|
// src/usageResume.ts
|
|
2874
2941
|
var RESET_BUFFER_MS = 3e4;
|
|
2875
2942
|
var UNKNOWN_RESET_RETRY_MS = 5 * 6e4;
|
|
@@ -2891,7 +2958,7 @@ function scheduleUsageResume(supabase, config, cardId, sessionId, pty) {
|
|
|
2891
2958
|
);
|
|
2892
2959
|
const timer = setTimeout(() => {
|
|
2893
2960
|
pending.delete(sessionId);
|
|
2894
|
-
|
|
2961
|
+
sendMessageToIdleSession(pty, cardId, "continue");
|
|
2895
2962
|
}, delay);
|
|
2896
2963
|
timer.unref();
|
|
2897
2964
|
pending.set(sessionId, timer);
|
|
@@ -2959,7 +3026,7 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
2959
3026
|
}
|
|
2960
3027
|
const stale = data ?? [];
|
|
2961
3028
|
if (stale.length === 0) return;
|
|
2962
|
-
for (const { id: sessionId, card_id: cardId } of stale) {
|
|
3029
|
+
for (const { id: sessionId, card_id: cardId, cards } of stale) {
|
|
2963
3030
|
intentionallyStoppedSessionIds.add(sessionId);
|
|
2964
3031
|
stopPtySession(sessionId);
|
|
2965
3032
|
cancelUsageResume(sessionId);
|
|
@@ -2967,7 +3034,13 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
2967
3034
|
await updateSession(
|
|
2968
3035
|
supabase,
|
|
2969
3036
|
sessionId,
|
|
2970
|
-
{
|
|
3037
|
+
{
|
|
3038
|
+
status: "done",
|
|
3039
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3040
|
+
// See stopCardSession's doc comment: "fim" is the authoritative completion signal for
|
|
3041
|
+
// dispatchRunnerQueue's prerequisite gate, which only ever reads agent_state.
|
|
3042
|
+
...cards.column_name === "fim" ? { agentState: "done" } : {}
|
|
3043
|
+
},
|
|
2971
3044
|
{ projectId, cardId, userId: config.ownerUserId }
|
|
2972
3045
|
);
|
|
2973
3046
|
logger.card.info(cardId, `Ended stale session ${sessionId} (its card left "desenvolvimento" while this runner missed the event).`);
|
|
@@ -2978,6 +3051,7 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2978
3051
|
if (dispatchingRunner) return;
|
|
2979
3052
|
dispatchingRunner = true;
|
|
2980
3053
|
let queued = [];
|
|
3054
|
+
const promotedCardIds = /* @__PURE__ */ new Set();
|
|
2981
3055
|
try {
|
|
2982
3056
|
queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2983
3057
|
if (queued.length === 0) return;
|
|
@@ -2987,7 +3061,7 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2987
3061
|
for (const project of config.registeredProjects) {
|
|
2988
3062
|
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
2989
3063
|
if (!running) continue;
|
|
2990
|
-
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
3064
|
+
if ((running.agentState === "done" || running.agentState === "waiting_prototype_review") && queuedProjectIds.has(project.id)) {
|
|
2991
3065
|
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
2992
3066
|
continue;
|
|
2993
3067
|
}
|
|
@@ -3014,22 +3088,24 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
3014
3088
|
try {
|
|
3015
3089
|
await promoteQueuedSession(supabase, config, entry);
|
|
3016
3090
|
promotedThisPass.add(projectId);
|
|
3091
|
+
promotedCardIds.add(entry.card_id);
|
|
3017
3092
|
freeProjectSlots -= 1;
|
|
3018
3093
|
} catch (err) {
|
|
3019
3094
|
logger.card.error(entry.card_id, `Failed to promote queued session ${entry.id}:`, err);
|
|
3020
3095
|
}
|
|
3021
3096
|
}
|
|
3022
3097
|
} finally {
|
|
3023
|
-
await publishQueueSnapshots(supabase, config, queued).catch(
|
|
3098
|
+
await publishQueueSnapshots(supabase, config, queued, promotedCardIds).catch(
|
|
3024
3099
|
(err) => logger.runner.error("ORCHESTRATOR", "Failed to publish queue snapshots:", err)
|
|
3025
3100
|
);
|
|
3026
3101
|
dispatchingRunner = false;
|
|
3027
3102
|
}
|
|
3028
3103
|
}
|
|
3029
|
-
async function publishQueueSnapshots(supabase, config, queued) {
|
|
3104
|
+
async function publishQueueSnapshots(supabase, config, queued, promotedCardIds) {
|
|
3030
3105
|
const paused = await listPausedSessionsForRunner(supabase, config.ownerUserId);
|
|
3031
3106
|
const queuedByProject = /* @__PURE__ */ new Map();
|
|
3032
3107
|
for (const entry of queued) {
|
|
3108
|
+
if (promotedCardIds.has(entry.card_id)) continue;
|
|
3033
3109
|
const list = queuedByProject.get(entry.cards.project_id) ?? [];
|
|
3034
3110
|
list.push(entry.card_id);
|
|
3035
3111
|
queuedByProject.set(entry.cards.project_id, list);
|
|
@@ -3047,6 +3123,9 @@ async function publishQueueSnapshots(supabase, config, queued) {
|
|
|
3047
3123
|
});
|
|
3048
3124
|
}
|
|
3049
3125
|
}
|
|
3126
|
+
function buildPrototypeReviewFollowUp(prototypeApproved, prototypeUrl) {
|
|
3127
|
+
return prototypeApproved ? "O prot\xF3tipo foi revisado e aprovado \u2014 prossiga para a implementa\xE7\xE3o real, usando-o como refer\xEAncia." : `O prot\xF3tipo ainda n\xE3o foi aprovado${prototypeUrl ? ` (${prototypeUrl})` : ""} \u2014 confira os coment\xE1rios deixados nele (e, se for um prot\xF3tipo de marca\xE7\xE3o de pontos, os pontos salvos) e ajuste, publicando a nova vers\xE3o com publish_prototype. Depois, pare o turno de novo: a sess\xE3o ser\xE1 retomada quando houver outra revis\xE3o.`;
|
|
3128
|
+
}
|
|
3050
3129
|
async function promoteQueuedSession(supabase, config, entry) {
|
|
3051
3130
|
const existingHandle = getPtySession(entry.id);
|
|
3052
3131
|
if (existingHandle) {
|
|
@@ -3059,6 +3138,13 @@ async function promoteQueuedSession(supabase, config, entry) {
|
|
|
3059
3138
|
);
|
|
3060
3139
|
activeSessionByCard.set(entry.card_id, entry.id);
|
|
3061
3140
|
logger.card.info(entry.card_id, "Resumed paused session from the front of the queue.");
|
|
3141
|
+
if (entry.agent_state === "waiting_prototype_review" && !existingHandle.exited) {
|
|
3142
|
+
sendMessageToIdleSession(
|
|
3143
|
+
existingHandle.pty,
|
|
3144
|
+
entry.card_id,
|
|
3145
|
+
buildPrototypeReviewFollowUp(entry.cards.prototype_approved, entry.cards.prototype_url)
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3062
3148
|
return;
|
|
3063
3149
|
}
|
|
3064
3150
|
const spawned = await spawnSessionPty(supabase, config, entry);
|
|
@@ -3090,9 +3176,12 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
3090
3176
|
} else {
|
|
3091
3177
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
3092
3178
|
const instructions = buildInstructionsWithMcpContext(
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3179
|
+
buildInstructionsWithPrototypeGate(
|
|
3180
|
+
buildInstructionsWithFileReferences(
|
|
3181
|
+
buildInstructionsWithAttachments(card.instructions, attachmentPaths),
|
|
3182
|
+
card.file_references
|
|
3183
|
+
),
|
|
3184
|
+
card.prototype_first
|
|
3096
3185
|
),
|
|
3097
3186
|
cardId,
|
|
3098
3187
|
card.project_id
|
|
@@ -3172,7 +3261,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
3172
3261
|
} else {
|
|
3173
3262
|
cancelUsageResume(sessionId);
|
|
3174
3263
|
}
|
|
3175
|
-
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
3264
|
+
if (state === "done" || state === "waiting_prototype_review") void dispatchRunnerQueue(supabase, config);
|
|
3176
3265
|
});
|
|
3177
3266
|
pty.onData((chunk) => watcher.push(chunk));
|
|
3178
3267
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -3186,16 +3275,27 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
3186
3275
|
}
|
|
3187
3276
|
async function requeuePausedCard(supabase, config, card) {
|
|
3188
3277
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
3189
|
-
if (!active
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3278
|
+
if (!active) return;
|
|
3279
|
+
if (active.status === "paused") {
|
|
3280
|
+
await updateSession(
|
|
3281
|
+
supabase,
|
|
3282
|
+
active.id,
|
|
3283
|
+
{ status: "queued" },
|
|
3284
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3285
|
+
);
|
|
3286
|
+
await updateCard(supabase, card.id, { requeueRequestedAt: null });
|
|
3287
|
+
logger.card.info(card.id, `Session ${active.id} requeued at the front of the line (was paused).`);
|
|
3288
|
+
await dispatchRunnerQueue(supabase, config);
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
if (active.status === "running" && active.agentState === "waiting_prototype_review") {
|
|
3292
|
+
const handle = getPtySession(active.id);
|
|
3293
|
+
await updateCard(supabase, card.id, { requeueRequestedAt: null });
|
|
3294
|
+
if (handle && !handle.exited) {
|
|
3295
|
+
sendMessageToIdleSession(handle.pty, card.id, buildPrototypeReviewFollowUp(card.prototype_approved, card.prototype_url));
|
|
3296
|
+
logger.card.info(card.id, `Session ${active.id} was never paused (alone in its project) \u2014 sent the prototype review follow-up directly.`);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3199
3299
|
}
|
|
3200
3300
|
async function reconcileOrphanedSessions(supabase, config) {
|
|
3201
3301
|
for (const project of config.registeredProjects) {
|
|
@@ -3254,7 +3354,11 @@ async function stopCardSession(supabase, config, card) {
|
|
|
3254
3354
|
await updateSession(
|
|
3255
3355
|
supabase,
|
|
3256
3356
|
sessionId,
|
|
3257
|
-
{
|
|
3357
|
+
{
|
|
3358
|
+
status: "done",
|
|
3359
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3360
|
+
...card.column_name === "fim" ? { agentState: "done" } : {}
|
|
3361
|
+
},
|
|
3258
3362
|
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3259
3363
|
);
|
|
3260
3364
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-board-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|