@automagik/genie 4.260507.7 → 4.260508.2
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/genie.js +20 -7
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/db/migrations/054_fix_subagent_team_inheritance.sql +64 -42
- package/src/db/migrations/054_fix_subagent_team_inheritance.test.ts +96 -0
- package/src/db/migrations/055_default_auto_resume_true.sql +27 -5
- package/src/db/migrations/055_default_auto_resume_true.test.ts +108 -0
package/dist/genie.js
CHANGED
|
@@ -204,7 +204,7 @@ PANE_ID="$1"
|
|
|
204
204
|
COLOR=$(${bin} display-message -p -t "$PANE_ID" '#{@genie_color}' 2>/dev/null)
|
|
205
205
|
[ -z "$COLOR" ] && COLOR="default"
|
|
206
206
|
${bin} set-option -w pane-active-border-style "fg=$COLOR"
|
|
207
|
-
`),chmodSync2(PANE_COLOR_SCRIPT,493)}async function applyPaneColor(paneId,color,windowId){let hex=TMUX_COLOR_MAP[color]??TMUX_COLOR_MAP.blue;try{ensurePaneColorScript(),await executeTmux2(`set-option -p -t '${paneId}' @genie_color '${hex}'`);try{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));await(await getConnection2())`UPDATE executors SET pane_color = ${hex} WHERE tmux_pane_id = ${paneId}`}catch{}if(windowId)await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function rehydratePaneColorHook(windowId){try{ensurePaneColorScript(),await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function resolveRepoSession(repoPath){let derived=basename(repoPath);try{let sessions=await listSessions(),exact=sessions.find((s)=>s.name===derived);if(exact)return exact.name;if(process.env.TMUX)try{let name=(await executeTmux2("display-message -p '#{session_name}'")).trim();if(name)return name}catch{}let partial=sessions.find((s)=>s.name.includes(derived));if(partial)return partial.name}catch{}return derived}function isTmuxSocketAlive(socketName){if(!socketName)return!1;let uid=process.getuid?.()??501;return existsSync7(join9(`/tmp/tmux-${uid}`,socketName))}async function isTmuxServerReachable(socketName){if(!socketName)return!1;if(!isTmuxSocketAlive(socketName))return!1;try{let{execSync:execSync2}=await import("child_process");return execSync2(`${tmuxBin()} -L ${shellQuote(socketName)} list-sessions -F ''`,{stdio:["ignore","ignore","ignore"],timeout:2000}),!0}catch{return!1}}async function isPaneAlive(paneId){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{return(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_dead}'`)).trim()==="0"}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting"))throw new TmuxUnreachableError(message);return!1}}async function isPaneProcessRunning(paneId,processName,execSyncFn){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{let panePid=(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_pid}'`)).trim();if(!panePid||!/^\d+$/.test(panePid))return!1;return(execSyncFn??(await import("child_process")).execSync)(`pgrep -la -P ${panePid} 2>/dev/null; for cpid in $(pgrep -P ${panePid} 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true`,{encoding:"utf-8",timeout:5000}).toLowerCase().includes(processName.toLowerCase())}catch{return!1}}async function killWindow(sessionName,windowName){try{return await executeTmux2(`kill-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),!0}catch{return!1}}var TMUX_COLOR_NAMES,TMUX_COLOR_MAP,PANE_COLOR_SCRIPT,TmuxUnreachableError;var init_tmux=__esm(()=>{init_genie_tokens();init_ensure_tmux();init_team_lead_command();TMUX_COLOR_NAMES=["red","blue","green","yellow","purple","orange","pink","cyan"],TMUX_COLOR_MAP=Object.fromEntries(TMUX_COLOR_NAMES.map((name,i2)=>[name,rotateHue(palette.accent,i2*45)])),PANE_COLOR_SCRIPT=`${__require("os").homedir()}/.genie/tmux-pane-color.sh`;TmuxUnreachableError=class TmuxUnreachableError extends Error{constructor(message){super(message);this.name="TmuxUnreachableError"}}});var exports_agent_registry={};__export(exports_agent_registry,{update:()=>update,unregister:()=>unregister,setCurrentExecutor:()=>setCurrentExecutor,saveTemplate:()=>saveTemplate,resolveAgentIdStrict:()=>resolveAgentIdStrict,resolveAgentId:()=>resolveAgentId,removeSubPane:()=>removeSubPane,register:()=>register,reconcileStaleSpawns:()=>reconcileStaleSpawns,listTemplates:()=>listTemplates,listForRender:()=>listForRender,listExhaustedZombies:()=>listExhaustedZombies,listAllExhaustedErrored:()=>listAllExhaustedErrored,listAgentsForRender:()=>listAgentsForRender,listAgents:()=>listAgents,list:()=>list,getTeamLeadEntry:()=>getTeamLeadEntry,getPane:()=>getPane,getElapsedTime:()=>getElapsedTime,getAgentResolverCounters:()=>getAgentResolverCounters,getAgentEffectiveState:()=>getAgentEffectiveState,getAgentByName:()=>getAgentByName,getAgent:()=>getAgent,get:()=>get,findOrCreateAgent:()=>findOrCreateAgent,findByWindow:()=>findByWindow,findByTask:()=>findByTask,findByPane:()=>findByPane,filterBySession:()=>filterBySession,auditAgentKind:()=>auditAgentKind,archiveExhaustedZombies:()=>archiveExhaustedZombies,archiveAllExhaustedErrored:()=>archiveAllExhaustedErrored,addSubPane:()=>addSubPane,_resetAgentResolverCountersForTests:()=>_resetAgentResolverCountersForTests});import{createHash,randomUUID}from"crypto";function resolveWorkerSocketName(){return process.env.GENIE_TMUX_SOCKET||"genie"}function ts(v){if(!v)return new Date().toISOString();return v instanceof Date?v.toISOString():v}function rowToAgent(r){let agent={id:r.id,paneId:r.pane_id,session:r.session,worktree:r.worktree??null,startedAt:ts(r.started_at),state:r.state,lastStateChange:ts(r.last_state_change),repoPath:r.repo_path};if(r.task_id!=null)agent.taskId=r.task_id;if(r.task_title!=null)agent.taskTitle=r.task_title;if(r.wish_slug!=null)agent.wishSlug=r.wish_slug;if(r.group_number!=null)agent.groupNumber=r.group_number;if(r.window_name!=null)agent.windowName=r.window_name;if(r.window_id!=null)agent.windowId=r.window_id;if(r.role!=null)agent.role=r.role;if(r.custom_name!=null)agent.customName=r.custom_name;if(r.sub_panes!=null){let sp=typeof r.sub_panes==="string"?JSON.parse(r.sub_panes):r.sub_panes;if(Array.isArray(sp)&&sp.length>0)agent.subPanes=sp}if(r.provider!=null)agent.provider=r.provider;if(r.transport!=null)agent.transport=r.transport;if(r.skill!=null)agent.skill=r.skill;if(r.team!=null)agent.team=r.team;if(r.tmux_window!=null)agent.window=r.tmux_window;if(r.native_agent_id!=null)agent.nativeAgentId=r.native_agent_id;if(r.native_color!=null)agent.nativeColor=r.native_color;if(r.native_team_enabled)agent.nativeTeamEnabled=r.native_team_enabled;if(r.parent_session_id!=null)agent.parentSessionId=r.parent_session_id;if(r.suspended_at!=null)agent.suspendedAt=ts(r.suspended_at);if(r.auto_resume!=null)agent.autoResume=r.auto_resume;if(r.resume_attempts!=null)agent.resumeAttempts=r.resume_attempts;if(r.last_resume_attempt!=null)agent.lastResumeAttempt=ts(r.last_resume_attempt);if(r.max_resume_attempts!=null)agent.maxResumeAttempts=r.max_resume_attempts;if(r.pane_color!=null)agent.paneColor=r.pane_color;if(agent.currentExecutorId=r.current_executor_id??null,agent.reportsTo=r.reports_to??null,agent.title=r.title??null,r.kind!=null)agent.kind=r.kind;return agent}function rowToTemplate(r){let tpl={id:r.id,provider:r.provider,team:r.team,cwd:r.cwd,lastSpawnedAt:ts(r.last_spawned_at)};if(r.role!=null)tpl.role=r.role;if(r.skill!=null)tpl.skill=r.skill;if(r.extra_args!=null){let ea=typeof r.extra_args==="string"?JSON.parse(r.extra_args):r.extra_args;if(Array.isArray(ea)&&ea.length>0)tpl.extraArgs=ea}if(r.native_team_enabled)tpl.nativeTeamEnabled=r.native_team_enabled;return tpl}function shortProjectHash(repoPath){return createHash("sha1").update(repoPath).digest("hex").slice(0,8)}function buildProjectTeamLeadEntryId(teamName,session,repoPath){return`team-lead:${session}:${shortProjectHash(repoPath)}:${teamName}`}function buildSessionTeamLeadEntryId(teamName,session){return`team-lead:${session}:${teamName}`}function buildLegacyTeamLeadEntryId(teamName){return`team-lead:${teamName}`}function assertRegisterableId(id){if(!REGISTER_ID_RE.test(id))throw Error(`register: refusing to insert non-UUID/non-dir agent id ${JSON.stringify(id)}. Spawn callers must resolve the durable identity row via findOrCreateAgent first and pass that UUID \u2014 the bare-name shadow path was retired in wish retire-session-names-id-only Group 3 (migration 061 enforces the same shape at the DB).`)}async function register(agent){assertRegisterableId(agent.id);let sql=await getConnection(),now=new Date().toISOString();if(agent.team){let existing=await sql`
|
|
207
|
+
`),chmodSync2(PANE_COLOR_SCRIPT,493)}async function applyPaneColor(paneId,color,windowId){let hex=TMUX_COLOR_MAP[color]??TMUX_COLOR_MAP.blue;try{ensurePaneColorScript(),await executeTmux2(`set-option -p -t '${paneId}' @genie_color '${hex}'`);try{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));await(await getConnection2())`UPDATE executors SET pane_color = ${hex} WHERE tmux_pane_id = ${paneId}`}catch{}if(windowId)await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function rehydratePaneColorHook(windowId){try{ensurePaneColorScript(),await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function resolveRepoSession(repoPath){let derived=basename(repoPath);try{let sessions=await listSessions(),exact=sessions.find((s)=>s.name===derived);if(exact)return exact.name;if(process.env.TMUX)try{let name=(await executeTmux2("display-message -p '#{session_name}'")).trim();if(name)return name}catch{}let partial=sessions.find((s)=>s.name.includes(derived));if(partial)return partial.name}catch{}return derived}function isTmuxSocketAlive(socketName){if(!socketName)return!1;let uid=process.getuid?.()??501;return existsSync7(join9(`/tmp/tmux-${uid}`,socketName))}async function isTmuxServerReachable(socketName){if(!socketName)return!1;if(!isTmuxSocketAlive(socketName))return!1;try{let{execSync:execSync2}=await import("child_process");return execSync2(`${tmuxBin()} -L ${shellQuote(socketName)} list-sessions -F ''`,{stdio:["ignore","ignore","ignore"],timeout:2000}),!0}catch{return!1}}async function isPaneAlive(paneId){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{return(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_dead}'`)).trim()==="0"}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting"))throw new TmuxUnreachableError(message);return!1}}async function isPaneProcessRunning(paneId,processName,execSyncFn){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{let panePid=(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_pid}'`)).trim();if(!panePid||!/^\d+$/.test(panePid))return!1;return(execSyncFn??(await import("child_process")).execSync)(`pgrep -la -P ${panePid} 2>/dev/null; for cpid in $(pgrep -P ${panePid} 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true`,{encoding:"utf-8",timeout:5000}).toLowerCase().includes(processName.toLowerCase())}catch{return!1}}async function killWindow(sessionName,windowName){try{return await executeTmux2(`kill-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),!0}catch{return!1}}var TMUX_COLOR_NAMES,TMUX_COLOR_MAP,PANE_COLOR_SCRIPT,TmuxUnreachableError;var init_tmux=__esm(()=>{init_genie_tokens();init_ensure_tmux();init_team_lead_command();TMUX_COLOR_NAMES=["red","blue","green","yellow","purple","orange","pink","cyan"],TMUX_COLOR_MAP=Object.fromEntries(TMUX_COLOR_NAMES.map((name,i2)=>[name,rotateHue(palette.accent,i2*45)])),PANE_COLOR_SCRIPT=`${__require("os").homedir()}/.genie/tmux-pane-color.sh`;TmuxUnreachableError=class TmuxUnreachableError extends Error{constructor(message){super(message);this.name="TmuxUnreachableError"}}});var exports_agent_registry={};__export(exports_agent_registry,{update:()=>update,unregister:()=>unregister,setCurrentExecutor:()=>setCurrentExecutor,saveTemplate:()=>saveTemplate,resolveAgentIdStrict:()=>resolveAgentIdStrict,resolveAgentId:()=>resolveAgentId,removeSubPane:()=>removeSubPane,register:()=>register,reconcileStaleSpawns:()=>reconcileStaleSpawns,listTemplates:()=>listTemplates,listForRender:()=>listForRender,listExhaustedZombies:()=>listExhaustedZombies,listAllExhaustedErrored:()=>listAllExhaustedErrored,listAgentsForRender:()=>listAgentsForRender,listAgents:()=>listAgents,list:()=>list,getTeamLeadEntry:()=>getTeamLeadEntry,getPane:()=>getPane,getElapsedTime:()=>getElapsedTime,getAgentResolverCounters:()=>getAgentResolverCounters,getAgentEffectiveState:()=>getAgentEffectiveState,getAgentByName:()=>getAgentByName,getAgent:()=>getAgent,get:()=>get,findOrCreateAgent:()=>findOrCreateAgent,findByWindow:()=>findByWindow,findByTask:()=>findByTask,findByPane:()=>findByPane,filterBySession:()=>filterBySession,auditAgentKind:()=>auditAgentKind,archiveExhaustedZombies:()=>archiveExhaustedZombies,archiveAllExhaustedErrored:()=>archiveAllExhaustedErrored,addSubPane:()=>addSubPane,_resetAgentResolverCountersForTests:()=>_resetAgentResolverCountersForTests});import{createHash,randomUUID}from"crypto";function resolveWorkerSocketName(){return process.env.GENIE_TMUX_SOCKET||"genie"}function ts(v){if(!v)return new Date().toISOString();return v instanceof Date?v.toISOString():v}function rowToAgent(r){let agent={id:r.id,paneId:r.pane_id,session:r.session,worktree:r.worktree??null,startedAt:ts(r.started_at),state:r.state,lastStateChange:ts(r.last_state_change),repoPath:r.repo_path};if(r.task_id!=null)agent.taskId=r.task_id;if(r.task_title!=null)agent.taskTitle=r.task_title;if(r.wish_slug!=null)agent.wishSlug=r.wish_slug;if(r.group_number!=null)agent.groupNumber=r.group_number;if(r.window_name!=null)agent.windowName=r.window_name;if(r.window_id!=null)agent.windowId=r.window_id;if(r.role!=null)agent.role=r.role;if(r.custom_name!=null)agent.customName=r.custom_name;if(r.sub_panes!=null){let sp=typeof r.sub_panes==="string"?JSON.parse(r.sub_panes):r.sub_panes;if(Array.isArray(sp)&&sp.length>0)agent.subPanes=sp}if(r.provider!=null)agent.provider=r.provider;if(r.transport!=null)agent.transport=r.transport;if(r.skill!=null)agent.skill=r.skill;if(r.team!=null)agent.team=r.team;if(r.tmux_window!=null)agent.window=r.tmux_window;if(r.native_agent_id!=null)agent.nativeAgentId=r.native_agent_id;if(r.native_color!=null)agent.nativeColor=r.native_color;if(r.native_team_enabled)agent.nativeTeamEnabled=r.native_team_enabled;if(r.parent_session_id!=null)agent.parentSessionId=r.parent_session_id;if(r.suspended_at!=null)agent.suspendedAt=ts(r.suspended_at);if(r.auto_resume!=null)agent.autoResume=r.auto_resume;if(r.resume_attempts!=null)agent.resumeAttempts=r.resume_attempts;if(r.last_resume_attempt!=null)agent.lastResumeAttempt=ts(r.last_resume_attempt);if(r.max_resume_attempts!=null)agent.maxResumeAttempts=r.max_resume_attempts;if(r.pane_color!=null)agent.paneColor=r.pane_color;if(agent.currentExecutorId=r.current_executor_id??null,agent.reportsTo=r.reports_to??null,agent.title=r.title??null,r.kind!=null)agent.kind=r.kind;return agent}function rowToTemplate(r){let tpl={id:r.name??r.id,provider:r.provider,team:r.team,cwd:r.cwd,lastSpawnedAt:ts(r.last_spawned_at)};if(r.role!=null)tpl.role=r.role;if(r.skill!=null)tpl.skill=r.skill;if(r.extra_args!=null){let ea=typeof r.extra_args==="string"?JSON.parse(r.extra_args):r.extra_args;if(Array.isArray(ea)&&ea.length>0)tpl.extraArgs=ea}if(r.native_team_enabled)tpl.nativeTeamEnabled=r.native_team_enabled;return tpl}function shortProjectHash(repoPath){return createHash("sha1").update(repoPath).digest("hex").slice(0,8)}function buildProjectTeamLeadEntryId(teamName,session,repoPath){return`team-lead:${session}:${shortProjectHash(repoPath)}:${teamName}`}function buildSessionTeamLeadEntryId(teamName,session){return`team-lead:${session}:${teamName}`}function buildLegacyTeamLeadEntryId(teamName){return`team-lead:${teamName}`}function assertRegisterableId(id){if(!REGISTER_ID_RE.test(id))throw Error(`register: refusing to insert non-UUID/non-dir agent id ${JSON.stringify(id)}. Spawn callers must resolve the durable identity row via findOrCreateAgent first and pass that UUID \u2014 the bare-name shadow path was retired in wish retire-session-names-id-only Group 3 (migration 061 enforces the same shape at the DB).`)}async function register(agent){assertRegisterableId(agent.id);let sql=await getConnection(),now=new Date().toISOString();if(agent.team){let existing=await sql`
|
|
208
208
|
SELECT team FROM agents WHERE id = ${agent.id} LIMIT 1
|
|
209
209
|
`;if(existing.length>0&&existing[0].team!==null&&existing[0].team!==agent.team)throw Error(`register: cross-team id collision \u2014 agent id "${agent.id}" already exists in team "${existing[0].team}", refusing to re-register under team "${agent.team}". Unregister the stale row first, or pick a different id/suffix for this spawn.`)}await sql`INSERT INTO agents (id, pane_id, session, worktree, task_id, task_title, wish_slug, group_number, started_at, state, last_state_change, repo_path, window_name, window_id, role, custom_name, sub_panes, provider, transport, skill, team, tmux_window, native_agent_id, native_color, native_team_enabled, parent_session_id, suspended_at, auto_resume, resume_attempts, last_resume_attempt, max_resume_attempts, pane_color) VALUES (${agent.id}, ${agent.paneId}, ${agent.session}, ${agent.worktree??null}, ${agent.taskId??null}, ${agent.taskTitle??null}, ${agent.wishSlug??null}, ${agent.groupNumber??null}, ${agent.startedAt??now}, ${agent.state??"spawning"}, ${agent.lastStateChange??now}, ${agent.repoPath}, ${agent.windowName??null}, ${agent.windowId??null}, ${agent.role??null}, ${agent.customName??null}, ${sql.json(agent.subPanes??[])}, ${agent.provider??null}, ${agent.transport??null}, ${agent.skill??null}, ${agent.team??null}, ${agent.window??null}, ${agent.nativeAgentId??null}, ${agent.nativeColor??null}, ${agent.nativeTeamEnabled??!1}, ${agent.parentSessionId??null}, ${agent.suspendedAt??null}, ${agent.autoResume??!1}, ${agent.resumeAttempts??0}, ${agent.lastResumeAttempt??null}, ${agent.maxResumeAttempts??3}, ${agent.paneColor??null}) ON CONFLICT (id) DO UPDATE SET pane_id = EXCLUDED.pane_id, session = EXCLUDED.session, state = EXCLUDED.state, last_state_change = EXCLUDED.last_state_change, team = COALESCE(agents.team, EXCLUDED.team), role = COALESCE(agents.role, EXCLUDED.role), custom_name = COALESCE(agents.custom_name, EXCLUDED.custom_name), native_team_enabled = EXCLUDED.native_team_enabled, native_agent_id = COALESCE(EXCLUDED.native_agent_id, agents.native_agent_id), native_color = COALESCE(EXCLUDED.native_color, agents.native_color), parent_session_id = COALESCE(EXCLUDED.parent_session_id, agents.parent_session_id), provider = COALESCE(EXCLUDED.provider, agents.provider), transport = COALESCE(EXCLUDED.transport, agents.transport, 'tmux'), updated_at = now()`}async function unregister(id){await(await getConnection())`DELETE FROM agents WHERE id = ${id}`}async function get(id){let rows=await(await getConnection())`SELECT * FROM agents WHERE id = ${id}`;return rows.length>0?rowToAgent(rows[0]):null}async function list(){return(await(await getConnection())`SELECT * FROM agents`).map(rowToAgent)}function dedupeShadowRows(rows,opts){let groups=new Map;for(let r of rows){let k=opts.keyFor(r),arr=groups.get(k);if(arr)arr.push(r);else groups.set(k,[r])}let out=[];for(let arr of groups.values()){if(arr.length===1){out.push(arr[0]);continue}arr.sort((a,b)=>{let aExec=opts.hasExecutor(a)?1:0,bExec=opts.hasExecutor(b)?1:0;if(aExec!==bExec)return bExec-aExec;return opts.startedAt(b).localeCompare(opts.startedAt(a))}),out.push(arr[0])}return out}function shadowKey(customName,id,team){return`${customName??id}\x00${team??""}`}async function listForRender(){let all=await list();return dedupeShadowRows(all,{keyFor:(a)=>shadowKey(a.customName,a.id,a.team),hasExecutor:(a)=>a.currentExecutorId!=null,startedAt:(a)=>a.startedAt})}async function reconcileStaleSpawns(thresholdSeconds=60){try{let sql=await getConnection(),rows=await sql`
|
|
210
210
|
UPDATE agents
|
|
@@ -2849,9 +2849,22 @@ Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${runn
|
|
|
2849
2849
|
WHERE p.proname IN ('notify_trigger', 'pg_notify')
|
|
2850
2850
|
)
|
|
2851
2851
|
ORDER BY source_table, trigger_name
|
|
2852
|
-
`}),reply(sub.settings.get(ORG_ID),async()=>{let genieHome4=process.env.GENIE_HOME??join65(homedir43(),".genie"),configPath2=join65(genieHome4,"config.json"),wsConfigPath=join65(process.cwd(),".genie","workspace.json"),config={},wsConfig={};try{if(existsSync53(configPath2))config=JSON.parse(readFileSync35(configPath2,"utf-8"))}catch{}try{if(existsSync53(wsConfigPath))wsConfig=JSON.parse(readFileSync35(wsConfigPath,"utf-8"))}catch{}return{config,workspace:wsConfig}}),reply(sub.settings.set(ORG_ID),async(params)=>{let genieHome4=process.env.GENIE_HOME??join65(homedir43(),".genie"),configPath2=join65(genieHome4,"config.json"),config={};try{if(existsSync53(configPath2))config=JSON.parse(readFileSync35(configPath2,"utf-8"))}catch{}return config[params.key]=params.value,writeFileSync21(configPath2,JSON.stringify(config,null,2)),{ok:!0}}),reply(sub.settings.templates(ORG_ID),async()=>{return sql`
|
|
2852
|
+
`}),reply(sub.settings.get(ORG_ID),async()=>{let genieHome4=process.env.GENIE_HOME??join65(homedir43(),".genie"),configPath2=join65(genieHome4,"config.json"),wsConfigPath=join65(process.cwd(),".genie","workspace.json"),config={},wsConfig={};try{if(existsSync53(configPath2))config=JSON.parse(readFileSync35(configPath2,"utf-8"))}catch{}try{if(existsSync53(wsConfigPath))wsConfig=JSON.parse(readFileSync35(wsConfigPath,"utf-8"))}catch{}return{config,workspace:wsConfig}}),reply(sub.settings.set(ORG_ID),async(params)=>{let genieHome4=process.env.GENIE_HOME??join65(homedir43(),".genie"),configPath2=join65(genieHome4,"config.json"),config={};try{if(existsSync53(configPath2))config=JSON.parse(readFileSync35(configPath2,"utf-8"))}catch{}return config[params.key]=params.value,writeFileSync21(configPath2,JSON.stringify(config,null,2)),{ok:!0}}),reply(sub.settings.templates(ORG_ID),async()=>{return sql`
|
|
2853
|
+
SELECT
|
|
2854
|
+
name AS id,
|
|
2855
|
+
provider,
|
|
2856
|
+
team,
|
|
2857
|
+
role,
|
|
2858
|
+
skill,
|
|
2859
|
+
cwd,
|
|
2860
|
+
extra_args,
|
|
2861
|
+
native_team_enabled,
|
|
2862
|
+
last_spawned_at
|
|
2863
|
+
FROM agent_templates
|
|
2864
|
+
ORDER BY last_spawned_at DESC
|
|
2865
|
+
`}),reply(sub.settings.skills(ORG_ID),async()=>{let skillsDir=findSkillsDir();if(!skillsDir)return(await sql`SELECT DISTINCT skill FROM agents WHERE skill IS NOT NULL ORDER BY skill`).map((r)=>({name:r.skill,description:"",path:""}));return scanSkillsDirectory(skillsDir)}),reply(sub.settings.rules(ORG_ID),async()=>{return scanRuleDirs()}),reply(sub.settings.templateSave(ORG_ID),async(params)=>{if(!params.id)return{error:"id is required"};return await sql`
|
|
2853
2866
|
INSERT INTO agent_templates (
|
|
2854
|
-
|
|
2867
|
+
name, provider, team, role, skill, cwd,
|
|
2855
2868
|
extra_args, native_team_enabled, last_spawned_at
|
|
2856
2869
|
) VALUES (
|
|
2857
2870
|
${params.id},
|
|
@@ -2860,18 +2873,18 @@ Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${runn
|
|
|
2860
2873
|
${params.role??null},
|
|
2861
2874
|
${params.skill??null},
|
|
2862
2875
|
${params.cwd??""},
|
|
2863
|
-
${
|
|
2876
|
+
${sql.json(params.extraArgs??[])},
|
|
2864
2877
|
${params.nativeTeamEnabled??!1},
|
|
2865
2878
|
${new Date().toISOString()}
|
|
2866
2879
|
)
|
|
2867
|
-
ON CONFLICT (
|
|
2880
|
+
ON CONFLICT (name, team) WHERE name IS NOT NULL AND team IS NOT NULL DO UPDATE SET
|
|
2868
2881
|
provider = EXCLUDED.provider,
|
|
2869
|
-
team = EXCLUDED.team,
|
|
2870
2882
|
role = EXCLUDED.role,
|
|
2871
2883
|
skill = EXCLUDED.skill,
|
|
2872
2884
|
cwd = EXCLUDED.cwd,
|
|
2873
2885
|
extra_args = EXCLUDED.extra_args,
|
|
2874
|
-
native_team_enabled = EXCLUDED.native_team_enabled
|
|
2886
|
+
native_team_enabled = EXCLUDED.native_team_enabled,
|
|
2887
|
+
last_spawned_at = EXCLUDED.last_spawned_at
|
|
2875
2888
|
`,{ok:!0}}),reply(sub.settings.testPg(ORG_ID),async()=>{try{return await sql`SELECT 1 AS ok`,{ok:!0,message:"Connection successful"}}catch(err){return{ok:!1,message:err instanceof Error?err.message:String(err)}}}),reply(sub.pty.create(ORG_ID),async(params)=>{if(params.agentName){let session2=await spawnForAgent(params.agentName,{cwd:params.cwd,cols:params.cols,rows:params.rows});return{sessionId:session2.id,agentId:session2.agentId,executorId:session2.executorId}}return{sessionId:spawnBash(params.cwd).id,agentId:null,executorId:null}}),subscribePtyWildcard(`khal.${ORG_ID}.genie.pty.*.input`,async(sessionId,params)=>{writeTerminal(sessionId,params.data)}),subscribePtyWildcard(`khal.${ORG_ID}.genie.pty.*.resize`,async(sessionId,params)=>{resizeTerminal(sessionId,params.cols,params.rows)}),subscribePtyWildcard(`khal.${ORG_ID}.genie.pty.*.kill`,async(sessionId)=>{await killTerminal(sessionId)}),reply(sub.fs.list(ORG_ID),async(params)=>{let targetPath=resolve12(params.path);if(!existsSync53(targetPath))return{error:"not_found"};try{return readdirSync13(targetPath,{withFileTypes:!0}).map((e)=>({name:e.name,isDirectory:e.isDirectory(),path:join65(targetPath,e.name)}))}catch(err){return{error:err instanceof Error?err.message:String(err)}}}),reply(sub.fs.read(ORG_ID),async(params)=>{let targetPath=resolve12(params.path);if(!existsSync53(targetPath))return{error:"not_found"};try{return{content:readFileSync35(targetPath,"utf-8")}}catch(err){return{error:err instanceof Error?err.message:String(err)}}}),reply(sub.fs.write(ORG_ID),async(params)=>{let targetPath=resolve12(params.path);try{return writeFileSync21(targetPath,params.content),{ok:!0}}catch(err){return{error:err instanceof Error?err.message:String(err)}}}),reply(sub.approval.resolve(ORG_ID),async(params)=>{return{ok:await resolveApproval(params.id,params.decision,params.decided_by)}}),reply(sub.approval.list(ORG_ID),async(params)=>{return listPendingApprovals(params.agent_name)}),console.log("[genie-app] All request/reply handlers registered")}function decodeParams(data){if(data.length===0)return{};try{return JSON.parse(sc2.decode(data))}catch{return{}}}function subscribePtyWildcard(subject,handler){if(!nc)return;let subscription=nc.subscribe(subject);(async()=>{for await(let msg of subscription)try{let params=decodeParams(msg.data),sessionId=msg.subject.split(".")[4];if(sessionId)await handler(sessionId,params)}catch(err){console.error(`[genie-app] PTY wildcard error on ${msg.subject}:`,err instanceof Error?err.message:String(err))}})()}function reply(subject,handler){if(!nc)return;let subscription=nc.subscribe(subject);processSubscription(subscription,subject,handler)}async function processSubscription(subscription,subject,handler){for await(let msg of subscription)try{let params=decodeParams(msg.data),result2=await handler(params);if(msg.reply)msg.respond(sc2.encode(JSON.stringify(result2)))}catch(err){let errorMsg=err instanceof Error?err.message:String(err);if(console.error(`[genie-app] Handler error on ${subject}:`,errorMsg),msg.reply)msg.respond(sc2.encode(JSON.stringify({error:errorMsg})))}}async function shutdown2(){if(shutdownRequested)return;if(shutdownRequested=!0,console.log("[genie-app] Shutting down..."),await killAll(),await stopListening(),nc){try{await nc.drain()}catch{}nc=null}console.log("[genie-app] Shutdown complete"),process.exit(0)}var import_nats6,NATS_URL,ORG_ID,_PG_URL,sc2,nc=null,shutdownRequested=!1;var init_src_backend=__esm(()=>{init_agent_observability();init_db();init_claude_sdk_remote_approval();init_subjects();init_pg_bridge();init_pty();import_nats6=__toESM(require_mod4(),1);process.env.GENIE_APP="1";NATS_URL=process.env.GENIE_NATS_URL??"nats://localhost:4222",ORG_ID=process.env.GENIE_ORG_ID??"default",_PG_URL=process.env.GENIE_PG_URL??"postgresql://localhost:19642/genie",sc2=import_nats6.StringCodec();process.on("SIGTERM",()=>void shutdown2());process.on("SIGINT",()=>void shutdown2());start2().catch((err)=>{console.error("[genie-app] Fatal error:",err),process.exit(1)})});var exports_event_renderer={};__export(exports_event_renderer,{renderRuntimeEvent:()=>renderRuntimeEvent,renderAuditEvent:()=>renderAuditEvent,formatEventLine:()=>formatEventLine});function renderAuditEvent(input){let details=input.details;if(typeof details==="string")try{details=JSON.parse(details)}catch{details={}}let normalized={...input,details:details??{}},renderer=auditRenderers[input.event_type];if(renderer)return renderer(normalized);let event=normalized,json2=JSON.stringify(event.details);return{indicator:color("dim","\u25CB"),content:`${color("gray",event.event_type)} ${json2==="{}"?"":color("dim",json2)}`,context:`${event.entity_type}:${event.entity_id}`}}function renderRuntimeEvent(input){let renderer=runtimeRenderers[input.kind];if(renderer)return renderer(input);return{indicator:color("dim","\u25CB"),content:`${color("gray",input.kind)} ${input.text}`,context:input.agent}}function formatEventLine(timeStr,rendered){let termWidth=process.stdout.columns||120,timeCol=color("gray",timeStr),timeWidth=stripAnsi2(timeCol).length,iconWidth=stripAnsi2(rendered.indicator).length,prefixWidth=timeWidth+2+iconWidth+2,contextStr=rendered.context?color("dim",rendered.context):"",contextWidth=contextStr?stripAnsi2(contextStr).length+2:0,contentWidth=Math.max(20,termWidth-prefixWidth-contextWidth),content=stripAnsi2(rendered.content).replace(/[\r\n]+/g," ");if(content.length<=contentWidth){let paddedContent=padToWidth(rendered.content,contentWidth);return`${timeCol} ${rendered.indicator} ${paddedContent}${contextStr?` ${contextStr}`:""}`}let chunks=wrapText(content,contentWidth),indent2=" ".repeat(prefixWidth),firstLine=`${timeCol} ${rendered.indicator} ${padToWidth(chunks[0],contentWidth)}${contextStr?` ${contextStr}`:""}`,rest=chunks.slice(1).map((c)=>`${indent2}${c}`);return[firstLine,...rest].join(`
|
|
2876
2889
|
`)}function padToWidth(coloredText,width){let visible=stripAnsi2(coloredText).length,pad=Math.max(0,width-visible);return coloredText+" ".repeat(pad)}function wrapText(text,width){let chunks=[],remaining=text;while(remaining.length>width){let breakAt=remaining.lastIndexOf(" ",width);if(breakAt<=0||breakAt<width/2)breakAt=width;chunks.push(remaining.slice(0,breakAt).trimEnd()),remaining=remaining.slice(breakAt).trimStart()}if(remaining.length>0)chunks.push(remaining);return chunks}function formatDuration3(ms){if(ms<1000)return`${ms}ms`;if(ms<60000)return`${(ms/1000).toFixed(1)}s`;let s2=Math.floor(ms/1000);return`${Math.floor(s2/60)}m${s2%60}s`}var shortEntity=(id)=>id.split(":")[0]??id,auditRenderers,runtimeRenderers;var init_event_renderer=__esm(()=>{init_term_format();auditRenderers={"sdk.assistant.message":(e)=>({indicator:color("brightCyan","\uD83D\uDCAC"),content:color("brightCyan",`"${e.details.textPreview??""}"`),context:shortEntity(e.entity_id)}),"sdk.user.message":(e)=>{let d=e.details,text=d.textPreview;if(typeof text==="string"&&text.length>0){let tag=d.isReplay?color("dim"," (replay)"):d.isSynthetic?color("dim"," (synthetic)"):"";return{indicator:color("cyan","\uD83D\uDC64"),content:`${color("cyan",`"${text}"`)}${tag}`,context:shortEntity(e.entity_id)}}let kind=d.isReplay?"replay":d.isSynthetic?"synthetic":"turn";return{indicator:color("cyan","\uD83D\uDC64"),content:color("dim",`user ${kind}`),context:shortEntity(e.entity_id)}},"sdk.hook.started":(e)=>({indicator:color("yellow","\uD83E\uDE9D"),content:`${e.details.hookName??"?"} ${color("dim","started")}`,context:shortEntity(e.entity_id)}),"sdk.hook.response":(e)=>{let outcome=e.details.outcome==="success"?color("green","\u2713"):color("red","\u2717"),hookId=e.details.hookId?color("dim",` ${String(e.details.hookId).slice(0,8)}`):"";return{indicator:color("yellow","\uD83E\uDE9D"),content:`${e.details.hookName??"?"} ${outcome}${hookId}`,context:shortEntity(e.entity_id)}},"sdk.system":(e)=>{let model=String(e.details.model??"?").replace(/^claude-/,""),tools=e.details.tools??0,session=e.details.sessionId?` \xB7 ${color("dim",String(e.details.sessionId).slice(0,8))}`:"";return{indicator:color("gray","\u2699"),content:`init ${color("cyan",model)} \xB7 ${tools} tools${session}`,context:shortEntity(e.entity_id)}},"sdk.result.success":(e)=>{let d=e.details,dur=d.durationMs?formatDuration3(d.durationMs):"",cost=typeof d.totalCostUsd==="number"?`$${d.totalCostUsd.toFixed(4)}`:"",preview=d.resultPreview?` \xB7 "${d.resultPreview}"`:"";return{indicator:color("green","\u2728"),content:`${color("brightGreen",dur)} \xB7 ${color("yellow",cost)}${color("dim",preview)}`,context:shortEntity(e.entity_id)}},"sdk.rate_limit":(e)=>({indicator:color("gray","\u23F1"),content:color("dim",`rate limit: ${e.details.status??"?"}`),context:shortEntity(e.entity_id)}),"sdk.api.retry":(e)=>{let d=e.details,attempt=d.attempt??"?",max=d.maxRetries??"?",delay=d.retryDelayMs?`${Math.round(d.retryDelayMs)}ms`:"";return{indicator:color("yellow","\u21BB"),content:color("yellow",`retry ${attempt}/${max} \xB7 ${d.error??"?"}${delay?` \xB7 ${delay}`:""}`),context:shortEntity(e.entity_id)}},"session.created_fresh":(e)=>({indicator:color("green","\u2726"),content:color("green",`session created \xB7 ${e.details.agent_id??"?"}`),context:shortEntity(e.entity_id)}),"session.resumed":(e)=>({indicator:color("cyan","\u21BB"),content:color("cyan",`session resumed \xB7 ${e.details.agent_id??"?"}`),context:shortEntity(e.entity_id)}),"deliver.start":(e)=>({indicator:color("blue","\u25B8"),content:color("dim",`deliver \u2192 ${e.details.agent_id??"?"}`),context:shortEntity(e.entity_id)}),"deliver.end":(e)=>{let turns=e.details.turn_count?` \xB7 turn ${e.details.turn_count}`:"";return{indicator:color("blue","\u25C2"),content:color("dim",`deliver done${turns}`),context:shortEntity(e.entity_id)}},state_changed:(e)=>({indicator:color("gray","\u25C9"),content:color("dim",`state \u2192 ${e.details.state??"?"}`),context:shortEntity(e.entity_id)}),"executor.spawn":(e)=>({indicator:color("green","\u25B6"),content:color("green",`spawn ${e.details.source??""}`),context:e.entity_id}),"executor.terminate":(e)=>({indicator:color("yellow","\u25A0"),content:color("yellow",`terminate ${e.details.source??""}`),context:e.entity_id}),"executor.deliver":(e)=>{let d=e.details,dur=d.durationMs?formatDuration3(d.durationMs):"",tokens2=d.tokens,tokStr=tokens2?`${tokens2.input??0}\u2192${tokens2.output??0}`:"",parts=[dur,tokStr].filter(Boolean);return{indicator:color("blue","\u2192"),content:parts.join(" \xB7 "),context:e.entity_id}},"task.error":(e)=>({indicator:color("red","\u2717"),content:color("red",String(e.details.error??"unknown error")),context:e.entity_id}),command_success:(e)=>({indicator:color("dim","\xB7"),content:color("dim",`${e.details.duration_ms??0}ms`),context:e.entity_id})};runtimeRenderers={user:(e)=>({indicator:color("brightCyan","\uD83D\uDC64"),content:e.text,context:e.agent}),assistant:(e)=>({indicator:color("cyan","\uD83E\uDD16"),content:color("cyan",e.text),context:e.agent}),message:(e)=>({indicator:color("magenta","\u2709"),content:e.text,context:e.agent}),tool_call:(e)=>({indicator:color("yellow","\uD83D\uDD27"),content:color("yellow",e.text),context:e.agent}),tool_result:(e)=>({indicator:color("gray","\u2B90"),content:color("dim",e.text),context:e.agent}),state:(e)=>({indicator:color("gray","\u25C9"),content:color("dim",e.text),context:e.agent}),system:(e)=>({indicator:color("gray","\u2699"),content:color("dim",e.text),context:e.agent}),qa:(e)=>({indicator:color("magenta","\uD83E\uDDEA"),content:e.text,context:e.agent})}});var exports_board_service={};__export(exports_board_service,{updateColumn:()=>updateColumn,updateBoard:()=>updateBoard,reorderColumns:()=>reorderColumns,removeColumn:()=>removeColumn,reconcileBoard:()=>reconcileBoard,listBoards:()=>listBoards,importBoard:()=>importBoard,getColumns:()=>getColumns,getBoard:()=>getBoard,exportBoard:()=>exportBoard,deleteBoard:()=>deleteBoard,createBoard:()=>createBoard,addColumn:()=>addColumn});function str3(v){return v!=null?String(v):null}function strOrDefault2(v,def){return v!=null?String(v):def}function parseJsonb(val,fallback){if(val==null)return fallback;if(typeof val==="string")try{return JSON.parse(val)}catch{return fallback}return val}function mapBoard(row2){return{id:row2.id,name:row2.name,projectId:str3(row2.project_id),description:str3(row2.description),status:strOrDefault2(row2.status,"active"),archivedAt:str3(row2.archived_at),columns:parseJsonb(row2.columns,[]),config:parseJsonb(row2.config,{}),createdAt:strOrDefault2(row2.created_at,""),updatedAt:strOrDefault2(row2.updated_at,"")}}function generateColumnId(){return crypto.randomUUID()}function fillColumnDefaults(col,position){return{id:col.id??generateColumnId(),name:col.name??`column-${position}`,label:col.label??col.name??`Column ${position}`,gate:col.gate??"human",action:col.action??null,auto_advance:col.auto_advance??!1,transitions:col.transitions??[],roles:col.roles??["*"],color:col.color??palette.textDim,parallel:col.parallel??!1,on_fail:col.on_fail??null,position}}async function createBoard(input){let sql=await getConnection(),columns=[];if(input.fromTemplate){let tmplRows=await sql`
|
|
2877
2890
|
SELECT columns FROM board_templates WHERE name = ${input.fromTemplate} OR id = ${input.fromTemplate} LIMIT 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automagik/genie",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.260508.2",
|
|
4
4
|
"description": "Collaborative terminal toolkit for human + AI workflows. NOTE: the npm distribution is being soft-deprecated — the canonical install is `curl -fsSL https://get.automagik.dev/genie | bash` (cosign + SLSA verified). See https://automagik.dev/genie/security/distribution-sovereignty",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.260508.2",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -23,48 +23,70 @@
|
|
|
23
23
|
|
|
24
24
|
BEGIN;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
--
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
DO $$
|
|
27
|
+
DECLARE
|
|
28
|
+
builtin_names text[] := ARRAY[
|
|
29
|
+
-- BUILTIN_ROLES (plugins/genie/agents/*/AGENTS.md, category=role)
|
|
30
|
+
'docs',
|
|
31
|
+
'engineer',
|
|
32
|
+
'fix',
|
|
33
|
+
'pm',
|
|
34
|
+
'qa',
|
|
35
|
+
'refactor',
|
|
36
|
+
'reviewer',
|
|
37
|
+
'team-lead',
|
|
38
|
+
'trace',
|
|
39
|
+
-- BUILTIN_COUNCIL_MEMBERS (plugins/genie/agents/council*, category=council)
|
|
40
|
+
'council',
|
|
41
|
+
'council--architect',
|
|
42
|
+
'council--benchmarker',
|
|
43
|
+
'council--deployer',
|
|
44
|
+
'council--ergonomist',
|
|
45
|
+
'council--measurer',
|
|
46
|
+
'council--operator',
|
|
47
|
+
'council--questioner',
|
|
48
|
+
'council--sentinel',
|
|
49
|
+
'council--simplifier',
|
|
50
|
+
'council--tracer'
|
|
51
|
+
];
|
|
52
|
+
has_name_column boolean;
|
|
53
|
+
BEGIN
|
|
54
|
+
SELECT EXISTS (
|
|
55
|
+
SELECT 1
|
|
56
|
+
FROM information_schema.columns
|
|
57
|
+
WHERE table_schema = current_schema()
|
|
58
|
+
AND table_name = 'agent_templates'
|
|
59
|
+
AND column_name = 'name'
|
|
60
|
+
) INTO has_name_column;
|
|
37
61
|
|
|
38
|
-
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'council--tracer'
|
|
68
|
-
);
|
|
62
|
+
IF has_name_column THEN
|
|
63
|
+
-- Post-061 schema: agent_templates.id is a UUID PK; name is the text
|
|
64
|
+
-- template key used by roles, built-ins, and parent/child hierarchy.
|
|
65
|
+
EXECUTE $sql$
|
|
66
|
+
UPDATE agent_templates AS child
|
|
67
|
+
SET team = parent.team,
|
|
68
|
+
updated_at = now()
|
|
69
|
+
FROM agent_templates AS parent
|
|
70
|
+
WHERE child.name LIKE parent.name || '/%'
|
|
71
|
+
AND parent.name NOT LIKE '%/%'
|
|
72
|
+
AND child.team IS DISTINCT FROM parent.team
|
|
73
|
+
$sql$;
|
|
74
|
+
|
|
75
|
+
EXECUTE 'DELETE FROM agent_templates WHERE name = ANY ($1)' USING builtin_names;
|
|
76
|
+
ELSE
|
|
77
|
+
-- Pre-061 / fresh-install ordering: id is still the text template key.
|
|
78
|
+
EXECUTE $sql$
|
|
79
|
+
UPDATE agent_templates AS child
|
|
80
|
+
SET team = parent.team,
|
|
81
|
+
updated_at = now()
|
|
82
|
+
FROM agent_templates AS parent
|
|
83
|
+
WHERE child.id LIKE parent.id || '/%'
|
|
84
|
+
AND parent.id NOT LIKE '%/%'
|
|
85
|
+
AND child.team IS DISTINCT FROM parent.team
|
|
86
|
+
$sql$;
|
|
87
|
+
|
|
88
|
+
EXECUTE 'DELETE FROM agent_templates WHERE id = ANY ($1)' USING builtin_names;
|
|
89
|
+
END IF;
|
|
90
|
+
END $$;
|
|
69
91
|
|
|
70
92
|
COMMIT;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { type Sql, getConnection } from '../../lib/db.js';
|
|
5
|
+
import { DB_AVAILABLE, setupTestDatabase } from '../../lib/test-db.js';
|
|
6
|
+
|
|
7
|
+
const MIGRATION_PATH = join(import.meta.dir, '054_fix_subagent_team_inheritance.sql');
|
|
8
|
+
|
|
9
|
+
async function applyMigration(): Promise<void> {
|
|
10
|
+
const sql = await getConnection();
|
|
11
|
+
const migration = await readFile(MIGRATION_PATH, 'utf-8');
|
|
12
|
+
await sql.begin(async (tx: Sql) => {
|
|
13
|
+
await tx.unsafe(migration);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe.skipIf(!DB_AVAILABLE)('migration 054 — subagent team inheritance', () => {
|
|
18
|
+
let cleanup: () => Promise<void>;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
cleanup = await setupTestDatabase();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
const sql = await getConnection();
|
|
30
|
+
await sql`DROP TABLE IF EXISTS agent_templates CASCADE`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('heals pre-061 text-id schema during fresh install ordering', async () => {
|
|
34
|
+
const sql = await getConnection();
|
|
35
|
+
await sql`
|
|
36
|
+
CREATE TABLE agent_templates (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
provider TEXT NOT NULL DEFAULT 'claude',
|
|
39
|
+
team TEXT NOT NULL,
|
|
40
|
+
cwd TEXT NOT NULL DEFAULT '/tmp',
|
|
41
|
+
last_spawned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
42
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
43
|
+
)
|
|
44
|
+
`;
|
|
45
|
+
await sql`
|
|
46
|
+
INSERT INTO agent_templates (id, team)
|
|
47
|
+
VALUES
|
|
48
|
+
('genie-omni', 'genie'),
|
|
49
|
+
('genie-omni/dog-fooder', 'felipe'),
|
|
50
|
+
('engineer', 'felipe')
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
await applyMigration();
|
|
54
|
+
|
|
55
|
+
const child = await sql<{ team: string }[]>`
|
|
56
|
+
SELECT team FROM agent_templates WHERE id = 'genie-omni/dog-fooder'
|
|
57
|
+
`;
|
|
58
|
+
expect(child[0].team).toBe('genie');
|
|
59
|
+
|
|
60
|
+
const builtin = await sql`SELECT 1 FROM agent_templates WHERE id = 'engineer'`;
|
|
61
|
+
expect(builtin.length).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('heals post-061 UUID-id schema via template name', async () => {
|
|
65
|
+
const sql = await getConnection();
|
|
66
|
+
await sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`;
|
|
67
|
+
await sql`
|
|
68
|
+
CREATE TABLE agent_templates (
|
|
69
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
70
|
+
name TEXT NOT NULL,
|
|
71
|
+
provider TEXT NOT NULL DEFAULT 'claude',
|
|
72
|
+
team TEXT NOT NULL,
|
|
73
|
+
cwd TEXT NOT NULL DEFAULT '/tmp',
|
|
74
|
+
last_spawned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
75
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
76
|
+
)
|
|
77
|
+
`;
|
|
78
|
+
await sql`
|
|
79
|
+
INSERT INTO agent_templates (name, team)
|
|
80
|
+
VALUES
|
|
81
|
+
('genie-omni', 'genie'),
|
|
82
|
+
('genie-omni/dog-fooder', 'felipe'),
|
|
83
|
+
('engineer', 'felipe')
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
await applyMigration();
|
|
87
|
+
|
|
88
|
+
const child = await sql<{ team: string }[]>`
|
|
89
|
+
SELECT team FROM agent_templates WHERE name = 'genie-omni/dog-fooder'
|
|
90
|
+
`;
|
|
91
|
+
expect(child[0].team).toBe('genie');
|
|
92
|
+
|
|
93
|
+
const builtin = await sql`SELECT 1 FROM agent_templates WHERE name = 'engineer'`;
|
|
94
|
+
expect(builtin.length).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -19,21 +19,43 @@
|
|
|
19
19
|
-- silent default-off that triggered the misleading message in the first
|
|
20
20
|
-- place.
|
|
21
21
|
--
|
|
22
|
+
-- Heal-not-wipe interaction with migration 061
|
|
23
|
+
-- --------------------------------------------
|
|
24
|
+
-- Migration 061 added `agents_id_shape_check` as `NOT VALID` so legacy
|
|
25
|
+
-- bare-name rows (archived by 050/053) stay in the table. Postgres still
|
|
26
|
+
-- enforces the constraint on every UPDATE — even when the id column is
|
|
27
|
+
-- not in the SET list — so an unconditional `UPDATE agents SET auto_resume
|
|
28
|
+
-- = true` errors out with `agents_id_shape_check` violation on any host
|
|
29
|
+
-- carrying archived bare-name rows. The whole boot path then fails,
|
|
30
|
+
-- because runMigrations runs inside getConnection's post-connect setup.
|
|
31
|
+
--
|
|
32
|
+
-- Filter the UPDATEs to UUID/dir-prefixed ids only. Bare-name rows are
|
|
33
|
+
-- already archived (state='archived', auto_resume=false locked by 050/053)
|
|
34
|
+
-- so flipping their auto_resume is meaningless — they are not resume
|
|
35
|
+
-- candidates regardless of the column value.
|
|
36
|
+
--
|
|
22
37
|
-- Idempotent: re-running this migration is a no-op on rows that are already
|
|
23
38
|
-- true and on a column whose default is already true.
|
|
24
39
|
|
|
25
40
|
BEGIN;
|
|
26
41
|
|
|
27
|
-
-- 1. Flip every existing false row
|
|
28
|
-
--
|
|
42
|
+
-- 1. Flip every existing false row that satisfies the id shape constraint.
|
|
43
|
+
-- Bare-name rows (archived legacy) are skipped — see header comment.
|
|
29
44
|
UPDATE agents
|
|
30
45
|
SET auto_resume = true
|
|
31
|
-
WHERE auto_resume = false
|
|
46
|
+
WHERE auto_resume = false
|
|
47
|
+
AND (id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
48
|
+
OR id LIKE 'dir:%');
|
|
32
49
|
|
|
33
50
|
-- 2. Change the column DEFAULT so future spawns inherit the safe value.
|
|
34
51
|
-- NULLs (legacy rows that pre-date the column) become true — same as
|
|
35
|
-
-- the safe-default the runtime treats them as.
|
|
52
|
+
-- the safe-default the runtime treats them as. Same id-shape filter
|
|
53
|
+
-- applies for the same reason.
|
|
36
54
|
ALTER TABLE agents ALTER COLUMN auto_resume SET DEFAULT true;
|
|
37
|
-
UPDATE agents
|
|
55
|
+
UPDATE agents
|
|
56
|
+
SET auto_resume = true
|
|
57
|
+
WHERE auto_resume IS NULL
|
|
58
|
+
AND (id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
59
|
+
OR id LIKE 'dir:%');
|
|
38
60
|
|
|
39
61
|
COMMIT;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { type Sql, getConnection } from '../../lib/db.js';
|
|
6
|
+
import { DB_AVAILABLE, setupTestDatabase } from '../../lib/test-db.js';
|
|
7
|
+
|
|
8
|
+
const MIGRATION_PATH = join(import.meta.dir, '055_default_auto_resume_true.sql');
|
|
9
|
+
|
|
10
|
+
async function runMigration(sql: Sql): Promise<void> {
|
|
11
|
+
const sqlText = await readFile(MIGRATION_PATH, 'utf-8');
|
|
12
|
+
// Mirror runMigrations() in db-migrations.ts: wrap in sql.begin so the
|
|
13
|
+
// pool sees a single reserved connection. The migration file's own
|
|
14
|
+
// BEGIN/COMMIT becomes a no-op inside the outer transaction (postgres
|
|
15
|
+
// emits a "there is already a transaction in progress" warning and
|
|
16
|
+
// COMMIT closes the outer txn, same as production).
|
|
17
|
+
await sql.begin(async (tx) => {
|
|
18
|
+
await tx.unsafe(sqlText);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe.skipIf(!DB_AVAILABLE)('migration 055 — default auto_resume true', () => {
|
|
23
|
+
let cleanup: () => Promise<void>;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
cleanup = await setupTestDatabase();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await cleanup();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('does not fail when archived bare-name rows are present', async () => {
|
|
34
|
+
const sql = await getConnection();
|
|
35
|
+
|
|
36
|
+
// The template DB has migration 055 already applied. To replay it as if
|
|
37
|
+
// it were pending against a host that carries archived bare-name rows
|
|
38
|
+
// (legacy 050/053 grandfather), we have to re-create the failure shape:
|
|
39
|
+
// 1. Drop the id-shape CHECK so we can insert a bare-name row.
|
|
40
|
+
// 2. Insert one archived bare-name agent (mirrors production data).
|
|
41
|
+
// 3. Re-add the CHECK as NOT VALID (mirrors migration 061's pattern).
|
|
42
|
+
// 4. Reset a UUID row's auto_resume to false (something for 055 to flip).
|
|
43
|
+
// 5. Re-run 055.
|
|
44
|
+
// The pre-fix migration would error here with agents_id_shape_check;
|
|
45
|
+
// the post-fix migration filters bare-name rows out of the UPDATE.
|
|
46
|
+
await sql`ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_id_shape_check`;
|
|
47
|
+
|
|
48
|
+
const bareName = `legacy-bare-${Date.now()}`;
|
|
49
|
+
await sql`
|
|
50
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume)
|
|
51
|
+
VALUES (${bareName}, 'legacy', NULL, 'legacy-team', '/tmp/legacy', now(), 'archived', false)
|
|
52
|
+
ON CONFLICT (id) DO NOTHING
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
await sql.unsafe(
|
|
56
|
+
`ALTER TABLE agents
|
|
57
|
+
ADD CONSTRAINT agents_id_shape_check
|
|
58
|
+
CHECK (id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' OR id LIKE 'dir:%')
|
|
59
|
+
NOT VALID`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const liveId = randomUUID();
|
|
63
|
+
await sql`
|
|
64
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume)
|
|
65
|
+
VALUES (${liveId}, 'engineer', 'live-engineer', 'live-team', '/tmp/live', now(), 'idle', false)
|
|
66
|
+
ON CONFLICT (id) DO NOTHING
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
// The pre-fix migration would throw here with
|
|
70
|
+
// `new row for relation "agents" violates check constraint "agents_id_shape_check"`.
|
|
71
|
+
await expect(runMigration(sql)).resolves.toBeUndefined();
|
|
72
|
+
|
|
73
|
+
const live = await sql<{ auto_resume: boolean }[]>`
|
|
74
|
+
SELECT auto_resume FROM agents WHERE id = ${liveId}
|
|
75
|
+
`;
|
|
76
|
+
expect(live[0].auto_resume).toBe(true);
|
|
77
|
+
|
|
78
|
+
const bare = await sql<{ auto_resume: boolean }[]>`
|
|
79
|
+
SELECT auto_resume FROM agents WHERE id = ${bareName}
|
|
80
|
+
`;
|
|
81
|
+
// Bare-name rows are archived legacy; their auto_resume is intentionally
|
|
82
|
+
// not flipped because the row would fail the id-shape check.
|
|
83
|
+
expect(bare[0].auto_resume).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('flips auto_resume on UUID and dir: rows, leaves NULL rows true', async () => {
|
|
87
|
+
const sql = await getConnection();
|
|
88
|
+
|
|
89
|
+
const uuidId = randomUUID();
|
|
90
|
+
const dirId = `dir:test-dir-${Date.now()}`;
|
|
91
|
+
await sql`
|
|
92
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume)
|
|
93
|
+
VALUES
|
|
94
|
+
(${uuidId}, 'engineer', 'a', 'team-a', '/tmp/a', now(), 'idle', false),
|
|
95
|
+
(${dirId}, 'engineer', 'b', 'team-b', '/tmp/b', now(), 'idle', false)
|
|
96
|
+
ON CONFLICT (id) DO NOTHING
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
await runMigration(sql);
|
|
100
|
+
|
|
101
|
+
const rows = await sql<{ id: string; auto_resume: boolean }[]>`
|
|
102
|
+
SELECT id, auto_resume FROM agents WHERE id IN (${uuidId}, ${dirId})
|
|
103
|
+
`;
|
|
104
|
+
for (const r of rows) {
|
|
105
|
+
expect(r.auto_resume).toBe(true);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|