@automagik/genie 4.260327.6 → 4.260327.7
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/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +3 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/__tests__/resume.test.ts +3 -1
- package/src/genie.ts +18 -8
- package/src/lib/db.test.ts +8 -6
- package/src/lib/db.ts +152 -123
- package/src/lib/scheduler-daemon.ts +10 -0
- package/src/term-commands/log.test.ts +4 -1
- package/src/term-commands/state.test.ts +4 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260327.
|
|
13
|
+
"version": "4.260327.7",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
package/dist/genie.js
CHANGED
|
@@ -146,8 +146,7 @@ disable_paste_burst = true
|
|
|
146
146
|
tell.location
|
|
147
147
|
from (select lo_tell64($1) as location) tell
|
|
148
148
|
) seek
|
|
149
|
-
`};return resolve2(lo),new Promise(async(r)=>finish=r);async function readable({highWaterMark=16384,start=0,end=1/0}={}){let max=end-start;return start&&await lo.seek(start),new Stream2.Readable({highWaterMark,async read(size){let l=size>max?size-max:size;max-=size;let[{data}]=await lo.read(l);if(this.push(data),data.length<size)this.push(null)}})}async function writable({highWaterMark=16384,start=0}={}){return start&&await lo.seek(start),new Stream2.Writable({highWaterMark,write(chunk,encoding,callback){lo.write(chunk).then(()=>callback(),callback)}})}}).catch(reject)})}var init_large=()=>{};var exports_src={};__export(exports_src,{default:()=>src_default});import os from"os";import fs from"fs";function Postgres(a,b2){let options=parseOptions(a,b2),subscribe=options.no_subscribe||Subscribe(Postgres,{...options}),ending=!1,queries=queue_default(),connecting=queue_default(),reserved=queue_default(),closed=queue_default(),ended=queue_default(),open=queue_default(),busy=queue_default(),full=queue_default(),queues={connecting,reserved,closed,ended,open,busy,full},connections=[...Array(options.max)].map(()=>connection_default(options,queues,{onopen,onend,onclose})),sql=Sql(handler);return Object.assign(sql,{get parameters(){return options.parameters},largeObject:largeObject.bind(null,sql),subscribe,CLOSE,END:CLOSE,PostgresError,options,reserve,listen,begin,close,end}),sql;function Sql(handler2){return handler2.debug=options.debug,Object.entries(options.types).reduce((acc,[name,type])=>{return acc[name]=(x)=>new Parameter(x,type.to),acc},typed),Object.assign(sql2,{types:typed,typed,unsafe,notify,array,json,file}),sql2;function typed(value,type){return new Parameter(value,type)}function sql2(strings,...args){return strings&&Array.isArray(strings.raw)?new Query(strings,args,handler2,cancel):typeof strings==="string"&&!args.length?new Identifier(options.transform.column.to?options.transform.column.to(strings):strings):new Builder(strings,args)}function unsafe(string,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([string],args,handler2,cancel,{prepare:!1,...options2,simple:"simple"in options2?options2.simple:args.length===0})}function file(path,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([],args,(query2)=>{fs.readFile(path,"utf8",(err,string)=>{if(err)return query2.reject(err);query2.strings=[string],handler2(query2)})},cancel,{...options2,simple:"simple"in options2?options2.simple:args.length===0})}}async function listen(name,fn,onlisten){let listener={fn,onlisten},sql2=listen.sql||(listen.sql=Postgres({...options,max:1,idle_timeout:null,max_lifetime:null,fetch_types:!1,onclose(){Object.entries(listen.channels).forEach(([name2,{listeners}])=>{delete listen.channels[name2],Promise.all(listeners.map((l)=>listen(name2,l.fn,l.onlisten).catch(()=>{})))})},onnotify(c,x){c in listen.channels&&listen.channels[c].listeners.forEach((l)=>l.fn(x))}})),channels=listen.channels||(listen.channels={});if(name in channels){channels[name].listeners.push(listener);let result2=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result2.state,unlisten}}channels[name]={result:sql2`listen ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`,listeners:[listener]};let result=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result.state,unlisten};async function unlisten(){if(name in channels===!1)return;if(channels[name].listeners=channels[name].listeners.filter((x)=>x!==listener),channels[name].listeners.length)return;return delete channels[name],sql2`unlisten ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`}}async function notify(channel,payload){return await sql`select pg_notify(${channel}, ${""+payload})`}async function reserve(){let queue=queue_default(),c=open.length?open.shift():await new Promise((resolve2,reject)=>{let query={reserve:resolve2,reject};queries.push(query),closed.length&&connect(closed.shift(),query)});move(c,reserved),c.reserved=()=>queue.length?c.execute(queue.shift()):move(c,reserved),c.reserved.release=!0;let sql2=Sql(handler2);return sql2.release=()=>{c.reserved=null,onopen(c)},sql2;function handler2(q){c.queue===full?queue.push(q):c.execute(q)||move(c,full)}}async function begin(options2,fn){!fn&&(fn=options2,options2="");let queries2=queue_default(),savepoints=0,connection2,prepare=null;try{return await sql.unsafe("begin "+options2.replace(/[^a-z ]/ig,""),[],{onexecute}).execute(),await Promise.race([scope(connection2,fn),new Promise((_,reject)=>connection2.onclose=reject)])}catch(error2){throw error2}async function scope(c,fn2,name){let sql2=Sql(handler2);sql2.savepoint=savepoint,sql2.prepare=(x)=>prepare=x.replace(/[^a-z0-9$-_. ]/gi);let uncaughtError,result;name&&await sql2`savepoint ${sql2(name)}`;try{if(result=await new Promise((resolve2,reject)=>{let x=fn2(sql2);Promise.resolve(Array.isArray(x)?Promise.all(x):x).then(resolve2,reject)}),uncaughtError)throw uncaughtError}catch(e){throw await(name?sql2`rollback to ${sql2(name)}`:sql2`rollback`),e instanceof PostgresError&&e.code==="25P02"&&uncaughtError||e}if(!name)prepare?await sql2`prepare transaction '${sql2.unsafe(prepare)}'`:await sql2`commit`;return result;function savepoint(name2,fn3){if(name2&&Array.isArray(name2.raw))return savepoint((sql3)=>sql3.apply(sql3,arguments));return arguments.length===1&&(fn3=name2,name2=null),scope(c,fn3,"s"+savepoints+++(name2?"_"+name2:""))}function handler2(q){q.catch((e)=>uncaughtError||(uncaughtError=e)),c.queue===full?queries2.push(q):c.execute(q)||move(c,full)}}function onexecute(c){connection2=c,move(c,reserved),c.reserved=()=>queries2.length?c.execute(queries2.shift()):move(c,reserved)}}function move(c,queue){return c.queue.remove(c),queue.push(c),c.queue=queue,queue===open?c.idleTimer.start():c.idleTimer.cancel(),c}function json(x){return new Parameter(x,3802)}function array(x,type){if(!Array.isArray(x))return array(Array.from(arguments));return new Parameter(x,type||(x.length?inferType(x)||25:0),options.shared.typeArrayMap)}function handler(query){if(ending)return query.reject(Errors.connection("CONNECTION_ENDED",options,options));if(open.length)return go(open.shift(),query);if(closed.length)return connect(closed.shift(),query);busy.length?go(busy.shift(),query):queries.push(query)}function go(c,query){return c.execute(query)?move(c,busy):move(c,full)}function cancel(query){return new Promise((resolve2,reject)=>{query.state?query.active?connection_default(options).cancel(query.state,resolve2,reject):query.cancelled={resolve:resolve2,reject}:(queries.remove(query),query.cancelled=!0,query.reject(Errors.generic("57014","canceling statement due to user request")),resolve2())})}async function end({timeout=null}={}){if(ending)return ending;await 1;let timer2;return ending=Promise.race([new Promise((r)=>timeout!==null&&(timer2=setTimeout(destroy,timeout*1000,r))),Promise.all(connections.map((c)=>c.end()).concat(listen.sql?listen.sql.end({timeout:0}):[],subscribe.sql?subscribe.sql.end({timeout:0}):[]))]).then(()=>clearTimeout(timer2))}async function close(){await Promise.all(connections.map((c)=>c.end()))}async function destroy(resolve2){await Promise.all(connections.map((c)=>c.terminate()));while(queries.length)queries.shift().reject(Errors.connection("CONNECTION_DESTROYED",options));resolve2()}function connect(c,query){return move(c,connecting),c.connect(query),c}function onend(c){move(c,ended)}function onopen(c){if(queries.length===0)return move(c,open);let max=Math.ceil(queries.length/(connecting.length+1)),ready=!0;while(ready&&queries.length&&max-- >0){let query=queries.shift();if(query.reserve)return query.reserve(c);ready=c.execute(query)}ready?move(c,busy):move(c,full)}function onclose(c,e){move(c,closed),c.reserved=null,c.onclose&&(c.onclose(e),c.onclose=null),options.onclose&&options.onclose(c.id),queries.length&&connect(c,queries.shift())}}function parseOptions(a,b2){if(a&&a.shared)return a;let env=process.env,o=(!a||typeof a==="string"?b2:a)||{},{url,multihost}=parseUrl(a),query=[...url.searchParams].reduce((a2,[b3,c])=>(a2[b3]=c,a2),{}),host=o.hostname||o.host||multihost||url.hostname||env.PGHOST||"localhost",port=o.port||url.port||env.PGPORT||5432,user=o.user||o.username||url.username||env.PGUSERNAME||env.PGUSER||osUsername();o.no_prepare&&(o.prepare=!1),query.sslmode&&(query.ssl=query.sslmode,delete query.sslmode),"timeout"in o&&(console.log("The timeout option is deprecated, use idle_timeout instead"),o.idle_timeout=o.timeout),query.sslrootcert==="system"&&(query.ssl="verify-full");let ints=["idle_timeout","connect_timeout","max_lifetime","max_pipeline","backoff","keep_alive"],defaults={max:globalThis.Cloudflare?3:10,ssl:!1,sslnegotiation:null,idle_timeout:null,connect_timeout:30,max_lifetime,max_pipeline:100,backoff,keep_alive:60,prepare:!0,debug:!1,fetch_types:!0,publications:"alltables",target_session_attrs:null};return{host:Array.isArray(host)?host:host.split(",").map((x)=>x.split(":")[0]),port:Array.isArray(port)?port:host.split(",").map((x)=>parseInt(x.split(":")[1]||port)),path:o.path||host.indexOf("/")>-1&&host+"/.s.PGSQL."+port,database:o.database||o.db||(url.pathname||"").slice(1)||env.PGDATABASE||user,user,pass:o.pass||o.password||url.password||env.PGPASSWORD||"",...Object.entries(defaults).reduce((acc,[k,d])=>{let value=k in o?o[k]:(k in query)?query[k]==="disable"||query[k]==="false"?!1:query[k]:env["PG"+k.toUpperCase()]||d;return acc[k]=typeof value==="string"&&ints.includes(k)?+value:value,acc},{}),connection:{application_name:env.PGAPPNAME||"postgres.js",...o.connection,...Object.entries(query).reduce((acc,[k,v])=>((k in defaults)||(acc[k]=v),acc),{})},types:o.types||{},target_session_attrs:tsa(o,url,env),onnotice:o.onnotice,onnotify:o.onnotify,onclose:o.onclose,onparameter:o.onparameter,socket:o.socket,transform:parseTransform(o.transform||{undefined:void 0}),parameters:{},shared:{retries:0,typeArrayMap:{}},...mergeUserTypes(o.types)}}function tsa(o,url,env){let x=o.target_session_attrs||url.searchParams.get("target_session_attrs")||env.PGTARGETSESSIONATTRS;if(!x||["read-write","read-only","primary","standby","prefer-standby"].includes(x))return x;throw Error("target_session_attrs "+x+" is not supported")}function backoff(retries){return(0.5+Math.random()/2)*Math.min(3**retries/100,20)}function max_lifetime(){return 60*(30+Math.random()*30)}function parseTransform(x){return{undefined:x.undefined,column:{from:typeof x.column==="function"?x.column:x.column&&x.column.from,to:x.column&&x.column.to},value:{from:typeof x.value==="function"?x.value:x.value&&x.value.from,to:x.value&&x.value.to},row:{from:typeof x.row==="function"?x.row:x.row&&x.row.from,to:x.row&&x.row.to}}}function parseUrl(url){if(!url||typeof url!=="string")return{url:{searchParams:new Map}};let host=url;host=host.slice(host.indexOf("://")+3).split(/[?/]/)[0],host=decodeURIComponent(host.slice(host.indexOf("@")+1));let urlObj=new URL(url.replace(host,host.split(",")[0]));return{url:{username:decodeURIComponent(urlObj.username),password:decodeURIComponent(urlObj.password),host:urlObj.host,hostname:urlObj.hostname,port:urlObj.port,pathname:urlObj.pathname,searchParams:urlObj.searchParams},multihost:host.indexOf(",")>-1&&host}}function osUsername(){try{return os.userInfo().username}catch(_){return process.env.USERNAME||process.env.USER||process.env.LOGNAME}}var src_default;var init_src=__esm(()=>{init_types2();init_connection();init_query();init_queue();init_errors3();init_large();Object.assign(Postgres,{PostgresError,toPascal,pascal,toCamel,camel,toKebab,kebab,fromPascal,fromCamel,fromKebab,BigInt:{to:20,from:[20],parse:(x)=>BigInt(x),serialize:(x)=>x.toString()}});src_default=Postgres});var exports_db={};__export(exports_db,{shutdown:()=>shutdown,resetConnection:()=>resetConnection,isAvailable:()=>isAvailable,getLockfilePath:()=>getLockfilePath,getDataDir:()=>getDataDir,getConnection:()=>getConnection,getActivePort:()=>getActivePort,ensurePgserve:()=>ensurePgserve});import{execSync as execSync2}from"child_process";import{existsSync as existsSync11,mkdirSync as mkdirSync5,readFileSync as readFileSync6,renameSync,unlinkSync as unlinkSync3,writeFileSync as writeFileSync5}from"fs";import{createConnection}from"net";import{homedir as homedir10}from"os";import{join as join11}from"path";function maskCredentials(url){return url.replace(/\/\/.*@/,"//***@")}function killOrphanedPostgres(dataDir){let pidFile=join11(dataDir,"postmaster.pid");if(!existsSync11(pidFile))return;try{let content=readFileSync6(pidFile,"utf-8"),pid=Number.parseInt(content.split(`
|
|
150
|
-
`)[0],10);if(Number.isNaN(pid)||pid<=0)return;let cmdline;try{cmdline=execSync2(`ps -o command= -p ${pid} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return}if(!cmdline.includes("postgres"))return;try{process.kill(pid,"SIGTERM")}catch{return}let deadline=Date.now()+5000;while(Date.now()<deadline)try{process.kill(pid,0),execSync2("sleep 0.2",{stdio:"ignore"})}catch{return}try{process.kill(pid,"SIGKILL")}catch{}}catch{}}function getPort(){let envPort=process.env.GENIE_PG_PORT;if(envPort){let parsed=Number.parseInt(envPort,10);if(!Number.isNaN(parsed)&&parsed>0&&parsed<65536)return parsed}return DEFAULT_PORT}function isPortListening(port,host){return new Promise((resolve2)=>{let socket=createConnection({port,host},()=>{socket.destroy(),resolve2(!0)});socket.on("error",()=>{socket.destroy(),resolve2(!1)}),socket.setTimeout(1000,()=>{socket.destroy(),resolve2(!1)})})}function readLockfile(){try{let content=readFileSync6(LOCKFILE_PATH,"utf-8").trim(),port=Number.parseInt(content,10);if(!Number.isNaN(port)&&port>0&&port<65536)return port}catch{}return null}function writeLockfile(port){try{mkdirSync5(GENIE_HOME2,{recursive:!0});let tmpPath=`${LOCKFILE_PATH}.tmp.${process.pid}`;writeFileSync5(tmpPath,String(port),"utf-8"),renameSync(tmpPath,LOCKFILE_PATH)}catch{}}function removeLockfile(){try{unlinkSync3(LOCKFILE_PATH)}catch{}}async function ensurePgserve(){if(ensurePromise)return ensurePromise;ensurePromise=_ensurePgserve();try{return await ensurePromise}finally{ensurePromise=null}}async function _ensurePgserve(){if(activePort!==null&&pgserveServer)return activePort;if(activePort!==null)return activePort;let port=getPort(),reusedPort=await tryReuseLockfile();if(reusedPort!==null)return reusedPort;if(await isPortListening(port,DEFAULT_HOST))return markPortActive(port,!0);mkdirSync5(DATA_DIR,{recursive:!0}),killOrphanedPostgres(DATA_DIR);try{let startedPort=await startPgserveOnPort(port);return registerExitHandler(),startedPort}catch(err){return tryFallbackPorts(port,err)}}async function tryReuseLockfile(){let lockfilePort=readLockfile();if(lockfilePort===null)return null;if(await isPortListening(lockfilePort,DEFAULT_HOST))return markPortActive(lockfilePort,!1);return removeLockfile(),null}function markPortActive(port,writeLock){if(activePort=port,process.env.GENIE_PG_AVAILABLE="true",writeLock)writeLockfile(port);return port}async function tryFallbackPorts(basePort,originalErr){for(let offset=1;offset<=MAX_PORT_RETRIES;offset++){let fallbackPort=basePort+offset;if(await isPortListening(fallbackPort,DEFAULT_HOST))return markPortActive(fallbackPort,!0);try{let startedPort=await startPgserveOnPort(fallbackPort);return registerExitHandler(),startedPort}catch{}}process.env.GENIE_PG_AVAILABLE="false";let message=originalErr instanceof Error?originalErr.message:String(originalErr);throw console.warn(`Warning: pgserve failed to start: ${maskCredentials(message)}`),Error(`pgserve failed to start on port ${basePort} (and fallbacks ${basePort+1}-${basePort+MAX_PORT_RETRIES}): ${maskCredentials(message)}`)}async function startPgserveOnPort(port){let{startMultiTenantServer}=await import("pgserve");return pgserveServer=await startMultiTenantServer({port,host:DEFAULT_HOST,baseDir:DATA_DIR,logLevel:"warn",autoProvision:!0}),activePort=port,ownsLockfile=!0,process.env.GENIE_PG_AVAILABLE="true",writeLockfile(port),port}function registerExitHandler(){if(exitHandlerRegistered)return;exitHandlerRegistered=!0;let cleanup=()=>{if(ownsLockfile)removeLockfile(),ownsLockfile=!1};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit(130)}),process.on("SIGTERM",()=>{cleanup(),process.exit(143)})}async function getConnection(){if(sqlClient)return sqlClient;let port=await ensurePgserve(),postgres2=(await Promise.resolve().then(() => (init_src(),exports_src))).default,testSchema=process.env.GENIE_TEST_SCHEMA;if(sqlClient=postgres2({host:DEFAULT_HOST,port,database:DB_NAME,username:"postgres",password:"postgres",max:10,idle_timeout:1,connect_timeout:5,onnotice:()=>{},connection:{client_min_messages:"warning",...testSchema?{search_path:`${testSchema}, public`}:{}}}),await runMigrations(sqlClient),!testSchema&&needsSeed())await runSeed(sqlClient);return sqlClient}async function resetConnection(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null}async function isAvailable(){try{return await(await getConnection())`SELECT 1`,!0}catch{return!1}}async function shutdown(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null;if(ownsLockfile)removeLockfile(),ownsLockfile=!1}function getDataDir(){return DATA_DIR}function getActivePort(){return activePort??getPort()}function getLockfilePath(){return LOCKFILE_PATH}var DEFAULT_PORT=19642,DEFAULT_HOST="127.0.0.1",MAX_PORT_RETRIES=3,GENIE_HOME2,DATA_DIR,LOCKFILE_PATH,DB_NAME="genie",pgserveServer=null,sqlClient=null,activePort=null,ensurePromise=null,ownsLockfile=!1,exitHandlerRegistered=!1;var init_db=__esm(()=>{init_db_migrations();init_pg_seed();GENIE_HOME2=process.env.GENIE_HOME??join11(homedir10(),".genie"),DATA_DIR=join11(GENIE_HOME2,"data","pgserve"),LOCKFILE_PATH=join11(GENIE_HOME2,"pgserve.port")});var exports_audit={};__export(exports_audit,{recordAuditEvent:()=>recordAuditEvent,queryToolUsage:()=>queryToolUsage,queryTimeline:()=>queryTimeline,querySummary:()=>querySummary,queryErrorPatterns:()=>queryErrorPatterns,queryCostBreakdown:()=>queryCostBreakdown,queryAuditEvents:()=>queryAuditEvents,getActor:()=>getActor,generateTraceId:()=>generateTraceId});async function recordAuditEvent(entityType,entityId,eventType,actor,details){try{if(!await isAvailable())return;let sql=await getConnection();await sql`
|
|
149
|
+
`};return resolve2(lo),new Promise(async(r)=>finish=r);async function readable({highWaterMark=16384,start=0,end=1/0}={}){let max=end-start;return start&&await lo.seek(start),new Stream2.Readable({highWaterMark,async read(size){let l=size>max?size-max:size;max-=size;let[{data}]=await lo.read(l);if(this.push(data),data.length<size)this.push(null)}})}async function writable({highWaterMark=16384,start=0}={}){return start&&await lo.seek(start),new Stream2.Writable({highWaterMark,write(chunk,encoding,callback){lo.write(chunk).then(()=>callback(),callback)}})}}).catch(reject)})}var init_large=()=>{};var exports_src={};__export(exports_src,{default:()=>src_default});import os from"os";import fs from"fs";function Postgres(a,b2){let options=parseOptions(a,b2),subscribe=options.no_subscribe||Subscribe(Postgres,{...options}),ending=!1,queries=queue_default(),connecting=queue_default(),reserved=queue_default(),closed=queue_default(),ended=queue_default(),open=queue_default(),busy=queue_default(),full=queue_default(),queues={connecting,reserved,closed,ended,open,busy,full},connections=[...Array(options.max)].map(()=>connection_default(options,queues,{onopen,onend,onclose})),sql=Sql(handler);return Object.assign(sql,{get parameters(){return options.parameters},largeObject:largeObject.bind(null,sql),subscribe,CLOSE,END:CLOSE,PostgresError,options,reserve,listen,begin,close,end}),sql;function Sql(handler2){return handler2.debug=options.debug,Object.entries(options.types).reduce((acc,[name,type])=>{return acc[name]=(x)=>new Parameter(x,type.to),acc},typed),Object.assign(sql2,{types:typed,typed,unsafe,notify,array,json,file}),sql2;function typed(value,type){return new Parameter(value,type)}function sql2(strings,...args){return strings&&Array.isArray(strings.raw)?new Query(strings,args,handler2,cancel):typeof strings==="string"&&!args.length?new Identifier(options.transform.column.to?options.transform.column.to(strings):strings):new Builder(strings,args)}function unsafe(string,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([string],args,handler2,cancel,{prepare:!1,...options2,simple:"simple"in options2?options2.simple:args.length===0})}function file(path,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([],args,(query2)=>{fs.readFile(path,"utf8",(err,string)=>{if(err)return query2.reject(err);query2.strings=[string],handler2(query2)})},cancel,{...options2,simple:"simple"in options2?options2.simple:args.length===0})}}async function listen(name,fn,onlisten){let listener={fn,onlisten},sql2=listen.sql||(listen.sql=Postgres({...options,max:1,idle_timeout:null,max_lifetime:null,fetch_types:!1,onclose(){Object.entries(listen.channels).forEach(([name2,{listeners}])=>{delete listen.channels[name2],Promise.all(listeners.map((l)=>listen(name2,l.fn,l.onlisten).catch(()=>{})))})},onnotify(c,x){c in listen.channels&&listen.channels[c].listeners.forEach((l)=>l.fn(x))}})),channels=listen.channels||(listen.channels={});if(name in channels){channels[name].listeners.push(listener);let result2=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result2.state,unlisten}}channels[name]={result:sql2`listen ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`,listeners:[listener]};let result=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result.state,unlisten};async function unlisten(){if(name in channels===!1)return;if(channels[name].listeners=channels[name].listeners.filter((x)=>x!==listener),channels[name].listeners.length)return;return delete channels[name],sql2`unlisten ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`}}async function notify(channel,payload){return await sql`select pg_notify(${channel}, ${""+payload})`}async function reserve(){let queue=queue_default(),c=open.length?open.shift():await new Promise((resolve2,reject)=>{let query={reserve:resolve2,reject};queries.push(query),closed.length&&connect(closed.shift(),query)});move(c,reserved),c.reserved=()=>queue.length?c.execute(queue.shift()):move(c,reserved),c.reserved.release=!0;let sql2=Sql(handler2);return sql2.release=()=>{c.reserved=null,onopen(c)},sql2;function handler2(q){c.queue===full?queue.push(q):c.execute(q)||move(c,full)}}async function begin(options2,fn){!fn&&(fn=options2,options2="");let queries2=queue_default(),savepoints=0,connection2,prepare=null;try{return await sql.unsafe("begin "+options2.replace(/[^a-z ]/ig,""),[],{onexecute}).execute(),await Promise.race([scope(connection2,fn),new Promise((_,reject)=>connection2.onclose=reject)])}catch(error2){throw error2}async function scope(c,fn2,name){let sql2=Sql(handler2);sql2.savepoint=savepoint,sql2.prepare=(x)=>prepare=x.replace(/[^a-z0-9$-_. ]/gi);let uncaughtError,result;name&&await sql2`savepoint ${sql2(name)}`;try{if(result=await new Promise((resolve2,reject)=>{let x=fn2(sql2);Promise.resolve(Array.isArray(x)?Promise.all(x):x).then(resolve2,reject)}),uncaughtError)throw uncaughtError}catch(e){throw await(name?sql2`rollback to ${sql2(name)}`:sql2`rollback`),e instanceof PostgresError&&e.code==="25P02"&&uncaughtError||e}if(!name)prepare?await sql2`prepare transaction '${sql2.unsafe(prepare)}'`:await sql2`commit`;return result;function savepoint(name2,fn3){if(name2&&Array.isArray(name2.raw))return savepoint((sql3)=>sql3.apply(sql3,arguments));return arguments.length===1&&(fn3=name2,name2=null),scope(c,fn3,"s"+savepoints+++(name2?"_"+name2:""))}function handler2(q){q.catch((e)=>uncaughtError||(uncaughtError=e)),c.queue===full?queries2.push(q):c.execute(q)||move(c,full)}}function onexecute(c){connection2=c,move(c,reserved),c.reserved=()=>queries2.length?c.execute(queries2.shift()):move(c,reserved)}}function move(c,queue){return c.queue.remove(c),queue.push(c),c.queue=queue,queue===open?c.idleTimer.start():c.idleTimer.cancel(),c}function json(x){return new Parameter(x,3802)}function array(x,type){if(!Array.isArray(x))return array(Array.from(arguments));return new Parameter(x,type||(x.length?inferType(x)||25:0),options.shared.typeArrayMap)}function handler(query){if(ending)return query.reject(Errors.connection("CONNECTION_ENDED",options,options));if(open.length)return go(open.shift(),query);if(closed.length)return connect(closed.shift(),query);busy.length?go(busy.shift(),query):queries.push(query)}function go(c,query){return c.execute(query)?move(c,busy):move(c,full)}function cancel(query){return new Promise((resolve2,reject)=>{query.state?query.active?connection_default(options).cancel(query.state,resolve2,reject):query.cancelled={resolve:resolve2,reject}:(queries.remove(query),query.cancelled=!0,query.reject(Errors.generic("57014","canceling statement due to user request")),resolve2())})}async function end({timeout=null}={}){if(ending)return ending;await 1;let timer2;return ending=Promise.race([new Promise((r)=>timeout!==null&&(timer2=setTimeout(destroy,timeout*1000,r))),Promise.all(connections.map((c)=>c.end()).concat(listen.sql?listen.sql.end({timeout:0}):[],subscribe.sql?subscribe.sql.end({timeout:0}):[]))]).then(()=>clearTimeout(timer2))}async function close(){await Promise.all(connections.map((c)=>c.end()))}async function destroy(resolve2){await Promise.all(connections.map((c)=>c.terminate()));while(queries.length)queries.shift().reject(Errors.connection("CONNECTION_DESTROYED",options));resolve2()}function connect(c,query){return move(c,connecting),c.connect(query),c}function onend(c){move(c,ended)}function onopen(c){if(queries.length===0)return move(c,open);let max=Math.ceil(queries.length/(connecting.length+1)),ready=!0;while(ready&&queries.length&&max-- >0){let query=queries.shift();if(query.reserve)return query.reserve(c);ready=c.execute(query)}ready?move(c,busy):move(c,full)}function onclose(c,e){move(c,closed),c.reserved=null,c.onclose&&(c.onclose(e),c.onclose=null),options.onclose&&options.onclose(c.id),queries.length&&connect(c,queries.shift())}}function parseOptions(a,b2){if(a&&a.shared)return a;let env=process.env,o=(!a||typeof a==="string"?b2:a)||{},{url,multihost}=parseUrl(a),query=[...url.searchParams].reduce((a2,[b3,c])=>(a2[b3]=c,a2),{}),host=o.hostname||o.host||multihost||url.hostname||env.PGHOST||"localhost",port=o.port||url.port||env.PGPORT||5432,user=o.user||o.username||url.username||env.PGUSERNAME||env.PGUSER||osUsername();o.no_prepare&&(o.prepare=!1),query.sslmode&&(query.ssl=query.sslmode,delete query.sslmode),"timeout"in o&&(console.log("The timeout option is deprecated, use idle_timeout instead"),o.idle_timeout=o.timeout),query.sslrootcert==="system"&&(query.ssl="verify-full");let ints=["idle_timeout","connect_timeout","max_lifetime","max_pipeline","backoff","keep_alive"],defaults={max:globalThis.Cloudflare?3:10,ssl:!1,sslnegotiation:null,idle_timeout:null,connect_timeout:30,max_lifetime,max_pipeline:100,backoff,keep_alive:60,prepare:!0,debug:!1,fetch_types:!0,publications:"alltables",target_session_attrs:null};return{host:Array.isArray(host)?host:host.split(",").map((x)=>x.split(":")[0]),port:Array.isArray(port)?port:host.split(",").map((x)=>parseInt(x.split(":")[1]||port)),path:o.path||host.indexOf("/")>-1&&host+"/.s.PGSQL."+port,database:o.database||o.db||(url.pathname||"").slice(1)||env.PGDATABASE||user,user,pass:o.pass||o.password||url.password||env.PGPASSWORD||"",...Object.entries(defaults).reduce((acc,[k,d])=>{let value=k in o?o[k]:(k in query)?query[k]==="disable"||query[k]==="false"?!1:query[k]:env["PG"+k.toUpperCase()]||d;return acc[k]=typeof value==="string"&&ints.includes(k)?+value:value,acc},{}),connection:{application_name:env.PGAPPNAME||"postgres.js",...o.connection,...Object.entries(query).reduce((acc,[k,v])=>((k in defaults)||(acc[k]=v),acc),{})},types:o.types||{},target_session_attrs:tsa(o,url,env),onnotice:o.onnotice,onnotify:o.onnotify,onclose:o.onclose,onparameter:o.onparameter,socket:o.socket,transform:parseTransform(o.transform||{undefined:void 0}),parameters:{},shared:{retries:0,typeArrayMap:{}},...mergeUserTypes(o.types)}}function tsa(o,url,env){let x=o.target_session_attrs||url.searchParams.get("target_session_attrs")||env.PGTARGETSESSIONATTRS;if(!x||["read-write","read-only","primary","standby","prefer-standby"].includes(x))return x;throw Error("target_session_attrs "+x+" is not supported")}function backoff(retries){return(0.5+Math.random()/2)*Math.min(3**retries/100,20)}function max_lifetime(){return 60*(30+Math.random()*30)}function parseTransform(x){return{undefined:x.undefined,column:{from:typeof x.column==="function"?x.column:x.column&&x.column.from,to:x.column&&x.column.to},value:{from:typeof x.value==="function"?x.value:x.value&&x.value.from,to:x.value&&x.value.to},row:{from:typeof x.row==="function"?x.row:x.row&&x.row.from,to:x.row&&x.row.to}}}function parseUrl(url){if(!url||typeof url!=="string")return{url:{searchParams:new Map}};let host=url;host=host.slice(host.indexOf("://")+3).split(/[?/]/)[0],host=decodeURIComponent(host.slice(host.indexOf("@")+1));let urlObj=new URL(url.replace(host,host.split(",")[0]));return{url:{username:decodeURIComponent(urlObj.username),password:decodeURIComponent(urlObj.password),host:urlObj.host,hostname:urlObj.hostname,port:urlObj.port,pathname:urlObj.pathname,searchParams:urlObj.searchParams},multihost:host.indexOf(",")>-1&&host}}function osUsername(){try{return os.userInfo().username}catch(_){return process.env.USERNAME||process.env.USER||process.env.LOGNAME}}var src_default;var init_src=__esm(()=>{init_types2();init_connection();init_query();init_queue();init_errors3();init_large();Object.assign(Postgres,{PostgresError,toPascal,pascal,toCamel,camel,toKebab,kebab,fromPascal,fromCamel,fromKebab,BigInt:{to:20,from:[20],parse:(x)=>BigInt(x),serialize:(x)=>x.toString()}});src_default=Postgres});var exports_db={};__export(exports_db,{shutdown:()=>shutdown,resetConnection:()=>resetConnection,isConnected:()=>isConnected,isAvailable:()=>isAvailable,getLockfilePath:()=>getLockfilePath,getDataDir:()=>getDataDir,getConnection:()=>getConnection,getActivePort:()=>getActivePort,ensurePgserve:()=>ensurePgserve});import{execSync as execSync2}from"child_process";import{existsSync as existsSync11,mkdirSync as mkdirSync5,readFileSync as readFileSync6,renameSync,unlinkSync as unlinkSync3,writeFileSync as writeFileSync5}from"fs";import{homedir as homedir10}from"os";import{join as join11}from"path";function maskCredentials(url){return url.replace(/\/\/.*@/,"//***@")}function selfHealPostgres(dataDir){try{execSync2(`pkill -9 -f "postgres.*${dataDir.replace(/\//g,"\\/")}" 2>/dev/null || true`,{stdio:"ignore",timeout:5000})}catch{}let pidFile=join11(dataDir,"postmaster.pid");if(existsSync11(pidFile))try{unlinkSync3(pidFile)}catch{}try{execSync2("ipcs -m 2>/dev/null | awk '$6 == 0 {print $2}' | xargs -I{} ipcrm -m {} 2>/dev/null || true",{stdio:"ignore",timeout:5000})}catch{}}function getPort(){let envPort=process.env.GENIE_PG_PORT;if(envPort){let parsed=Number.parseInt(envPort,10);if(!Number.isNaN(parsed)&&parsed>0&&parsed<65536)return parsed}return DEFAULT_PORT}async function isPostgresHealthy(port){try{let pg=(await Promise.resolve().then(() => (init_src(),exports_src))).default,probe=pg({host:DEFAULT_HOST,port,database:DB_NAME,username:"postgres",password:"postgres",max:1,connect_timeout:3,idle_timeout:1});return await probe`SELECT 1`,await probe.end({timeout:2}),!0}catch{return!1}}function readLockfile(){try{let content=readFileSync6(LOCKFILE_PATH,"utf-8").trim(),port=Number.parseInt(content,10);if(!Number.isNaN(port)&&port>0&&port<65536)return port}catch{}return null}function writeLockfile(port){try{mkdirSync5(GENIE_HOME2,{recursive:!0});let tmpPath=`${LOCKFILE_PATH}.tmp.${process.pid}`;writeFileSync5(tmpPath,String(port),"utf-8"),renameSync(tmpPath,LOCKFILE_PATH)}catch{}}function removeLockfile(){try{unlinkSync3(LOCKFILE_PATH)}catch{}}async function ensurePgserve(){if(ensurePromise)return ensurePromise;ensurePromise=_ensurePgserve();try{return await ensurePromise}finally{ensurePromise=null}}async function _ensurePgserve(){if(activePort!==null)return activePort;let port=getPort(),portFromFile=readLockfile();if(portFromFile!==null&&await isPostgresHealthy(portFromFile))return activePort=portFromFile,process.env.GENIE_PG_AVAILABLE="true",portFromFile;if(await isPostgresHealthy(port))return activePort=port,process.env.GENIE_PG_AVAILABLE="true",writeLockfile(port),port;if(!(process.env.CI==="true"||!!process.env.GENIE_TEST_SCHEMA)){let daemonRunning=isDaemonRunning();if(daemonRunning){selfHealPostgres(DATA_DIR);let recovered=await waitForPortFile(DAEMON_BOOT_TIMEOUT_MS);if(recovered!==null)return recovered}if(!daemonRunning){try{execSync2("genie daemon start",{stdio:"ignore",timeout:5000})}catch{}let booted=await waitForPortFile(DAEMON_BOOT_TIMEOUT_MS);if(booted!==null)return booted}}mkdirSync5(DATA_DIR,{recursive:!0}),selfHealPostgres(DATA_DIR);try{let startedPort=await startPgserveOnPort(port);return registerExitHandler(),startedPort}catch(err){process.env.GENIE_PG_AVAILABLE="false";let message=err instanceof Error?err.message:String(err);throw Error(`pgserve failed to start: ${maskCredentials(message)}`)}}function isDaemonRunning(){try{let pid=Number.parseInt(readFileSync6(DAEMON_PID_PATH,"utf-8").trim(),10);if(Number.isNaN(pid)||pid<=0)return!1;return process.kill(pid,0),!0}catch{return!1}}async function waitForPortFile(timeoutMs){let deadline=Date.now()+timeoutMs;while(Date.now()<deadline){let port=readLockfile();if(port!==null&&await isPostgresHealthy(port))return activePort=port,process.env.GENIE_PG_AVAILABLE="true",port;await new Promise((r)=>setTimeout(r,500))}return null}async function startPgserveOnPort(port){let{startMultiTenantServer}=await import("pgserve");return pgserveServer=await startMultiTenantServer({port,host:DEFAULT_HOST,baseDir:DATA_DIR,logLevel:"warn",autoProvision:!0}),activePort=port,ownsLockfile=!0,process.env.GENIE_PG_AVAILABLE="true",writeLockfile(port),port}function registerExitHandler(){if(exitHandlerRegistered)return;exitHandlerRegistered=!0;let cleanup=()=>{if(ownsLockfile)removeLockfile(),ownsLockfile=!1};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit(130)}),process.on("SIGTERM",()=>{cleanup(),process.exit(143)})}async function getConnection(){if(sqlClient)try{return await sqlClient`SELECT 1`,sqlClient}catch{try{await sqlClient.end({timeout:2})}catch{}sqlClient=null,activePort=null}let port=await ensurePgserve(),postgres2=(await Promise.resolve().then(() => (init_src(),exports_src))).default,testSchema=process.env.GENIE_TEST_SCHEMA;sqlClient=postgres2({host:DEFAULT_HOST,port,database:DB_NAME,username:"postgres",password:"postgres",max:10,idle_timeout:1,connect_timeout:5,onnotice:()=>{},connection:{client_min_messages:"warning",...testSchema?{search_path:`${testSchema}, public`}:{}}});try{if(await runMigrations(sqlClient),!testSchema&&needsSeed())await runSeed(sqlClient)}catch(err){try{await sqlClient.end({timeout:2})}catch{}throw sqlClient=null,err}return sqlClient}function isConnected(){return sqlClient!==null}async function resetConnection(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null}async function isAvailable(){try{return await(await getConnection())`SELECT 1`,!0}catch{return!1}}async function shutdown(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null;if(ownsLockfile)removeLockfile(),ownsLockfile=!1}function getDataDir(){return DATA_DIR}function getActivePort(){return activePort??getPort()}function getLockfilePath(){return LOCKFILE_PATH}var DEFAULT_PORT=19642,DEFAULT_HOST="127.0.0.1",GENIE_HOME2,DATA_DIR,LOCKFILE_PATH,DB_NAME="genie",pgserveServer=null,sqlClient=null,activePort=null,ensurePromise=null,ownsLockfile=!1,exitHandlerRegistered=!1,DAEMON_PID_PATH,DAEMON_BOOT_TIMEOUT_MS=15000;var init_db=__esm(()=>{init_db_migrations();init_pg_seed();GENIE_HOME2=process.env.GENIE_HOME??join11(homedir10(),".genie"),DATA_DIR=join11(GENIE_HOME2,"data","pgserve"),LOCKFILE_PATH=join11(GENIE_HOME2,"pgserve.port");DAEMON_PID_PATH=join11(GENIE_HOME2,"scheduler.pid")});var exports_audit={};__export(exports_audit,{recordAuditEvent:()=>recordAuditEvent,queryToolUsage:()=>queryToolUsage,queryTimeline:()=>queryTimeline,querySummary:()=>querySummary,queryErrorPatterns:()=>queryErrorPatterns,queryCostBreakdown:()=>queryCostBreakdown,queryAuditEvents:()=>queryAuditEvents,getActor:()=>getActor,generateTraceId:()=>generateTraceId});async function recordAuditEvent(entityType,entityId,eventType,actor,details){try{if(!await isAvailable())return;let sql=await getConnection();await sql`
|
|
151
150
|
INSERT INTO audit_events (entity_type, entity_id, event_type, actor, details)
|
|
152
151
|
VALUES (${entityType}, ${entityId}, ${eventType}, ${actor??null}, ${sql.json(details??{})})
|
|
153
152
|
`}catch{}}function parseSince(since){let match=since.match(/^(\d+)([smhd])$/);if(!match)return since;let amount=Number.parseInt(match[1],10),unit=match[2],ms={s:1000,m:60000,h:3600000,d:86400000}[unit]??3600000;return new Date(Date.now()-amount*ms).toISOString()}async function queryAuditEvents(options={}){let sql=await getConnection(),conditions=[],values2=[],paramIdx=1;if(options.type)conditions.push(`event_type = $${paramIdx++}`),values2.push(options.type);if(options.entity)conditions.push(`(entity_type = $${paramIdx} OR entity_id = $${paramIdx})`),paramIdx++,values2.push(options.entity);if(options.since)conditions.push(`created_at >= $${paramIdx++}::timestamptz`),values2.push(parseSince(options.since));if(options.errorsOnly)conditions.push("event_type LIKE '%error%' OR (details::text LIKE '%error%')");let where=conditions.length>0?`WHERE ${conditions.join(" AND ")}`:"",limit=options.limit??50;return await sql.unsafe(`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
|
|
@@ -1199,7 +1198,7 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1199
1198
|
`,failedCount++,deps.log({timestamp:now.toISOString(),level:"warn",event:"orphan_run_failed",run_id:run.id,worker_id:run.worker_id,dead_heartbeats:threshold})}if(failedCount>0)deps.log({timestamp:now.toISOString(),level:"info",event:"orphan_reconciliation_completed",failed_count:failedCount});return failedCount}async function collectMachineSnapshot(deps){let sql=await deps.getConnection(),now=deps.now(),snapshotId=deps.generateId(),workers=await deps.listWorkers(),activeWorkers=workers.filter((w)=>!["done","error","suspended"].includes(w.state)).length,teams=new Set(workers.filter((w)=>w.team).map((w)=>w.team)),tmuxSessions=await deps.countTmuxSessions(),cpuPercent=null,memoryMb=null;try{let mem=process.memoryUsage();memoryMb=Math.round(mem.rss/1024/1024)}catch{}try{let cpus=(await import("os")).cpus();if(cpus.length>0){let total=cpus.reduce((acc,cpu)=>{let t=Object.values(cpu.times).reduce((a,b2)=>a+b2,0);return acc+t-cpu.times.idle},0),totalAll=cpus.reduce((acc,cpu)=>acc+Object.values(cpu.times).reduce((a,b2)=>a+b2,0),0);cpuPercent=totalAll>0?Math.round(total/totalAll*100):null}}catch{}await sql`
|
|
1200
1199
|
INSERT INTO machine_snapshots (id, active_workers, active_teams, tmux_sessions, cpu_percent, memory_mb, created_at)
|
|
1201
1200
|
VALUES (${snapshotId}, ${activeWorkers}, ${teams.size}, ${tmuxSessions}, ${cpuPercent}, ${memoryMb}, ${now})
|
|
1202
|
-
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id);if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"});else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"}),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"})}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"}),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}},done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}try{let sql=await deps.getConnection();listenConnection=sql,await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await processTriggers()}),deps.log({timestamp:deps.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"listen_failed",error:message})}heartbeatTimer=setInterval(async()=>{if(!running2)return;try{await collectHeartbeats(deps),await collectMachineSnapshot(deps),await emitWorkerEvents(deps);try{let retSql=await deps.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}},config.heartbeatIntervalMs),orphanTimer=setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(deps,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},config.orphanCheckIntervalMs),inboxWatcherHandle=startInboxWatcherIfEnabled(deps);try{let captureSql=await deps.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"}),captureFallbackTimer=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},config.heartbeatIntervalMs)}startBackfill2(captureSql).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"backfill_error",error:message})})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message})}await processTriggers();while(running2){if(await new Promise((resolve4)=>{pollResolve=resolve4,pollTimeout=setTimeout(resolve4,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_protocol_router={};__export(exports_protocol_router,{sendMessage:()=>sendMessage2,getInbox:()=>getInbox});async function waitForWorkerReady(paneId,timeoutMs=AUTO_SPAWN_READY_TIMEOUT_MS){let start=Date.now();while(Date.now()-start<timeoutMs){try{let content=await capturePaneContent(paneId,30);if(detectState(content).type==="idle")return!0}catch{}await new Promise((r)=>setTimeout(r,AUTO_SPAWN_POLL_INTERVAL_MS))}return!1}async function resolveRecipient(recipientId){let allWorkers=await list(),byId=[],byRole=[],byTeamRole=[];for(let w of allWorkers){if(w.state==="suspended")continue;if(!await isPaneAlive(w.paneId))continue;if(w.id===recipientId)byId.push(w);else if(w.role===recipientId)byRole.push(w);else if(`${w.team}:${w.role}`===recipientId)byTeamRole.push(w)}if(byId.length>0)return byId;if(byRole.length>0)return byRole;return byTeamRole}async function findLiveWorkerFuzzy(recipientId){let matches=await resolveRecipient(recipientId);return matches.length===1?matches[0]:null}async function ensureWorkerAlive(worker,recipientId){if(worker&&worker.state!=="suspended"&&await isPaneAlive(worker.paneId))return{worker,respawned:!1};let live=await findLiveWorkerFuzzy(recipientId);if(live)return{worker:live,respawned:!1};if(!process.env.TMUX)return null;let templates=await listTemplates(),candidates=[worker?.role,worker?.id,recipientId].filter((v)=>Boolean(v)),uniqueCandidates=[...new Set(candidates)],workerTeam=worker?.team,template=templates.find((t)=>{if(workerTeam&&t.team!==workerTeam)return!1;return uniqueCandidates.some((q)=>t.id===q||t.role===q||`${t.team}:${t.role}`===q)});if(!template)return null;let resumeSessionId=template.provider==="claude"&&worker?.claudeSessionId?worker.claudeSessionId:void 0;try{if(await cleanupDeadWorkers(recipientId,workerTeam),worker)await unregister(worker.id);let{spawnWorkerFromTemplate:spawnWorkerFromTemplate2}=await Promise.resolve().then(() => (init_protocol_router_spawn(),exports_protocol_router_spawn)),result=await spawnWorkerFromTemplate2(template,resumeSessionId);if(await saveTemplate({...template,lastSpawnedAt:new Date().toISOString()}),await waitForWorkerReady(result.paneId),!await isPaneAlive(result.paneId))return await unregister(result.worker.id),null;return{worker:result.worker,respawned:!0}}catch{return null}}async function cleanupDeadWorkers(recipientId,team){let allWorkers=await list();for(let w of allWorkers){if(team&&w.team!==team)continue;if(!(w.role===recipientId||w.id===recipientId))continue;if(await isPaneAlive(w.paneId))continue;await unregister(w.id)}}async function deliverToWorker(repoPath,from,worker,body){let message=await send(repoPath,from,worker.id,body),delivered=!1;if(worker.nativeTeamEnabled&&worker.team&&worker.role)delivered=await writeToNativeInbox(worker,message);else delivered=await injectToTmuxPane(worker,message);if(!delivered&&worker.team){let agentName=worker.role||worker.id.split("-").slice(-1)[0]||worker.id;try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue");await writeNativeInbox(worker.team,agentName,nativeMsg),delivered=!0}catch{}}if(delivered)await markDelivered(repoPath,worker.id,message.id);return{messageId:message.id,workerId:worker.id,delivered}}async function deliverViaNativeInbox(repoPath,from,to,body,teamName){let resolvedTeam=teamName??await discoverTeamName();if(!resolvedTeam)return null;let config=await loadConfig(resolvedTeam).catch(()=>null);if(!config)return null;let sanitizedTo=sanitizeTeamName(to),matchedMember=config.members?.find((m)=>m.name===to||m.name===sanitizedTo||m.agentId===`${to}@${resolvedTeam}`||m.agentId===`${sanitizedTo}@${resolvedTeam}`);if(!matchedMember)return null;let inboxName=matchedMember.name??to;try{let message=await send(repoPath,from,to,body),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1};return await writeNativeInbox(resolvedTeam,inboxName,nativeMsg),await markDelivered(repoPath,to,message.id),{messageId:message.id,workerId:to,delivered:!0}}catch{return null}}async function sendMessage2(repoPath,from,to,body,teamName){if(from===to)return{messageId:"",workerId:to,delivered:!0,reason:"Self-delivery suppressed"};let liveMatches=await resolveRecipient(to);if(liveMatches.length===1)return deliverToWorker(repoPath,from,liveMatches[0],body);if(liveMatches.length>1)return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" is ambiguous. Found ${liveMatches.length} live matches: ${liveMatches.map((m)=>m.id).join(", ")}. Use exact worker ID.`};let{resolve:resolve4}=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),dirResolved=await resolve4(to),worker=await get(to);if(!worker){let allWorkers=await list();worker=allWorkers.find((w)=>w.role===to&&w.state==="suspended")??allWorkers.find((w)=>w.role===to)??null}if(dirResolved||worker){let alive=await ensureWorkerAlive(worker,to);if(alive)return deliverToWorker(repoPath,from,alive.worker,body)}let nativeResult=await deliverViaNativeInbox(repoPath,from,to,body,teamName);if(nativeResult)return nativeResult;return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" not found or not alive`}}async function writeToNativeInbox(worker,message){try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue"),agentName=worker.role??worker.id;return await writeNativeInbox(worker.team??"",agentName,nativeMsg),!0}catch{return!1}}async function injectToTmuxPane(worker,message){if(!worker.paneId)return!1;if(!/^%\d+$/.test(worker.paneId))return!1;try{let escaped=message.body.replace(/'/g,"'\\''");return await executeTmux2(`send-keys -t '${worker.paneId}' '${escaped}'`),await new Promise((resolve4)=>setTimeout(resolve4,200)),await executeTmux2(`send-keys -t '${worker.paneId}' Enter`),!0}catch{return!1}}async function getInbox(repoPath,workerId){return inbox(repoPath,workerId)}var AUTO_SPAWN_READY_TIMEOUT_MS=15000,AUTO_SPAWN_POLL_INTERVAL_MS=1000;var init_protocol_router=__esm(()=>{init_agent_registry();init_claude_native_teams();init_mailbox();init_orchestrator();init_tmux()});var exports_export_format={};__export(exports_export_format,{validateExportDocument:()=>validateExportDocument,createExportDocument:()=>createExportDocument,GROUP_TABLES:()=>GROUP_TABLES,EXPORT_VERSION:()=>EXPORT_VERSION,ALL_GROUPS:()=>ALL_GROUPS});function createExportDocument(type2,groups,genieVersion,actor){return{version:"1.0",exportedAt:new Date().toISOString(),exportedBy:actor,genieVersion,type:type2,groups,skippedTables:[],data:{}}}function validateExportDocument(obj){if(!obj||typeof obj!=="object")return{valid:!1,error:"Not a valid JSON object"};let doc=obj;if(doc.version!=="1.0")return{valid:!1,error:`Unsupported version: ${doc.version} (expected 1.0)`};if(!doc.exportedAt||typeof doc.exportedAt!=="string")return{valid:!1,error:"Missing or invalid exportedAt"};if(!doc.data||typeof doc.data!=="object")return{valid:!1,error:"Missing or invalid data"};return{valid:!0,doc}}var EXPORT_VERSION="1.0",GROUP_TABLES,ALL_GROUPS;var init_export_format=__esm(()=>{GROUP_TABLES={boards:["boards","board_templates","task_types"],tasks:["tasks","task_tags","task_actors","task_dependencies","task_stage_log"],tags:["tags"],projects:["projects"],schedules:["schedules"],agents:["agents","agent_templates","agent_checkpoints"],apps:["app_store","installed_apps","app_versions"],comms:["conversations","conversation_members","messages","mailbox","team_chat","notification_preferences"],config:["os_config","instances","warm_pool","golden_images"]},ALL_GROUPS=["boards","tasks","tags","projects","schedules","agents","apps","comms","config"]});var exports_table_detect={};__export(exports_table_detect,{filterAvailableTables:()=>filterAvailableTables});async function getAvailableTables(sql){return(await sql`
|
|
1201
|
+
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id);if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"});else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"}),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"})}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"}),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{}),Promise.resolve().then(() => (init_db(),exports_db)).then(({getLockfilePath:getLockfilePath2})=>{try{__require("fs").unlinkSync(getLockfilePath2())}catch{}}).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}},done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}try{let sql=await deps.getConnection();listenConnection=sql,await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await processTriggers()}),deps.log({timestamp:deps.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"listen_failed",error:message})}heartbeatTimer=setInterval(async()=>{if(!running2)return;try{await collectHeartbeats(deps),await collectMachineSnapshot(deps),await emitWorkerEvents(deps);try{let retSql=await deps.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}},config.heartbeatIntervalMs),orphanTimer=setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(deps,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},config.orphanCheckIntervalMs),inboxWatcherHandle=startInboxWatcherIfEnabled(deps);try{let captureSql=await deps.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"}),captureFallbackTimer=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},config.heartbeatIntervalMs)}startBackfill2(captureSql).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"backfill_error",error:message})})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message})}await processTriggers();while(running2){if(await new Promise((resolve4)=>{pollResolve=resolve4,pollTimeout=setTimeout(resolve4,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_protocol_router={};__export(exports_protocol_router,{sendMessage:()=>sendMessage2,getInbox:()=>getInbox});async function waitForWorkerReady(paneId,timeoutMs=AUTO_SPAWN_READY_TIMEOUT_MS){let start=Date.now();while(Date.now()-start<timeoutMs){try{let content=await capturePaneContent(paneId,30);if(detectState(content).type==="idle")return!0}catch{}await new Promise((r)=>setTimeout(r,AUTO_SPAWN_POLL_INTERVAL_MS))}return!1}async function resolveRecipient(recipientId){let allWorkers=await list(),byId=[],byRole=[],byTeamRole=[];for(let w of allWorkers){if(w.state==="suspended")continue;if(!await isPaneAlive(w.paneId))continue;if(w.id===recipientId)byId.push(w);else if(w.role===recipientId)byRole.push(w);else if(`${w.team}:${w.role}`===recipientId)byTeamRole.push(w)}if(byId.length>0)return byId;if(byRole.length>0)return byRole;return byTeamRole}async function findLiveWorkerFuzzy(recipientId){let matches=await resolveRecipient(recipientId);return matches.length===1?matches[0]:null}async function ensureWorkerAlive(worker,recipientId){if(worker&&worker.state!=="suspended"&&await isPaneAlive(worker.paneId))return{worker,respawned:!1};let live=await findLiveWorkerFuzzy(recipientId);if(live)return{worker:live,respawned:!1};if(!process.env.TMUX)return null;let templates=await listTemplates(),candidates=[worker?.role,worker?.id,recipientId].filter((v)=>Boolean(v)),uniqueCandidates=[...new Set(candidates)],workerTeam=worker?.team,template=templates.find((t)=>{if(workerTeam&&t.team!==workerTeam)return!1;return uniqueCandidates.some((q)=>t.id===q||t.role===q||`${t.team}:${t.role}`===q)});if(!template)return null;let resumeSessionId=template.provider==="claude"&&worker?.claudeSessionId?worker.claudeSessionId:void 0;try{if(await cleanupDeadWorkers(recipientId,workerTeam),worker)await unregister(worker.id);let{spawnWorkerFromTemplate:spawnWorkerFromTemplate2}=await Promise.resolve().then(() => (init_protocol_router_spawn(),exports_protocol_router_spawn)),result=await spawnWorkerFromTemplate2(template,resumeSessionId);if(await saveTemplate({...template,lastSpawnedAt:new Date().toISOString()}),await waitForWorkerReady(result.paneId),!await isPaneAlive(result.paneId))return await unregister(result.worker.id),null;return{worker:result.worker,respawned:!0}}catch{return null}}async function cleanupDeadWorkers(recipientId,team){let allWorkers=await list();for(let w of allWorkers){if(team&&w.team!==team)continue;if(!(w.role===recipientId||w.id===recipientId))continue;if(await isPaneAlive(w.paneId))continue;await unregister(w.id)}}async function deliverToWorker(repoPath,from,worker,body){let message=await send(repoPath,from,worker.id,body),delivered=!1;if(worker.nativeTeamEnabled&&worker.team&&worker.role)delivered=await writeToNativeInbox(worker,message);else delivered=await injectToTmuxPane(worker,message);if(!delivered&&worker.team){let agentName=worker.role||worker.id.split("-").slice(-1)[0]||worker.id;try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue");await writeNativeInbox(worker.team,agentName,nativeMsg),delivered=!0}catch{}}if(delivered)await markDelivered(repoPath,worker.id,message.id);return{messageId:message.id,workerId:worker.id,delivered}}async function deliverViaNativeInbox(repoPath,from,to,body,teamName){let resolvedTeam=teamName??await discoverTeamName();if(!resolvedTeam)return null;let config=await loadConfig(resolvedTeam).catch(()=>null);if(!config)return null;let sanitizedTo=sanitizeTeamName(to),matchedMember=config.members?.find((m)=>m.name===to||m.name===sanitizedTo||m.agentId===`${to}@${resolvedTeam}`||m.agentId===`${sanitizedTo}@${resolvedTeam}`);if(!matchedMember)return null;let inboxName=matchedMember.name??to;try{let message=await send(repoPath,from,to,body),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1};return await writeNativeInbox(resolvedTeam,inboxName,nativeMsg),await markDelivered(repoPath,to,message.id),{messageId:message.id,workerId:to,delivered:!0}}catch{return null}}async function sendMessage2(repoPath,from,to,body,teamName){if(from===to)return{messageId:"",workerId:to,delivered:!0,reason:"Self-delivery suppressed"};let liveMatches=await resolveRecipient(to);if(liveMatches.length===1)return deliverToWorker(repoPath,from,liveMatches[0],body);if(liveMatches.length>1)return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" is ambiguous. Found ${liveMatches.length} live matches: ${liveMatches.map((m)=>m.id).join(", ")}. Use exact worker ID.`};let{resolve:resolve4}=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),dirResolved=await resolve4(to),worker=await get(to);if(!worker){let allWorkers=await list();worker=allWorkers.find((w)=>w.role===to&&w.state==="suspended")??allWorkers.find((w)=>w.role===to)??null}if(dirResolved||worker){let alive=await ensureWorkerAlive(worker,to);if(alive)return deliverToWorker(repoPath,from,alive.worker,body)}let nativeResult=await deliverViaNativeInbox(repoPath,from,to,body,teamName);if(nativeResult)return nativeResult;return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" not found or not alive`}}async function writeToNativeInbox(worker,message){try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue"),agentName=worker.role??worker.id;return await writeNativeInbox(worker.team??"",agentName,nativeMsg),!0}catch{return!1}}async function injectToTmuxPane(worker,message){if(!worker.paneId)return!1;if(!/^%\d+$/.test(worker.paneId))return!1;try{let escaped=message.body.replace(/'/g,"'\\''");return await executeTmux2(`send-keys -t '${worker.paneId}' '${escaped}'`),await new Promise((resolve4)=>setTimeout(resolve4,200)),await executeTmux2(`send-keys -t '${worker.paneId}' Enter`),!0}catch{return!1}}async function getInbox(repoPath,workerId){return inbox(repoPath,workerId)}var AUTO_SPAWN_READY_TIMEOUT_MS=15000,AUTO_SPAWN_POLL_INTERVAL_MS=1000;var init_protocol_router=__esm(()=>{init_agent_registry();init_claude_native_teams();init_mailbox();init_orchestrator();init_tmux()});var exports_export_format={};__export(exports_export_format,{validateExportDocument:()=>validateExportDocument,createExportDocument:()=>createExportDocument,GROUP_TABLES:()=>GROUP_TABLES,EXPORT_VERSION:()=>EXPORT_VERSION,ALL_GROUPS:()=>ALL_GROUPS});function createExportDocument(type2,groups,genieVersion,actor){return{version:"1.0",exportedAt:new Date().toISOString(),exportedBy:actor,genieVersion,type:type2,groups,skippedTables:[],data:{}}}function validateExportDocument(obj){if(!obj||typeof obj!=="object")return{valid:!1,error:"Not a valid JSON object"};let doc=obj;if(doc.version!=="1.0")return{valid:!1,error:`Unsupported version: ${doc.version} (expected 1.0)`};if(!doc.exportedAt||typeof doc.exportedAt!=="string")return{valid:!1,error:"Missing or invalid exportedAt"};if(!doc.data||typeof doc.data!=="object")return{valid:!1,error:"Missing or invalid data"};return{valid:!0,doc}}var EXPORT_VERSION="1.0",GROUP_TABLES,ALL_GROUPS;var init_export_format=__esm(()=>{GROUP_TABLES={boards:["boards","board_templates","task_types"],tasks:["tasks","task_tags","task_actors","task_dependencies","task_stage_log"],tags:["tags"],projects:["projects"],schedules:["schedules"],agents:["agents","agent_templates","agent_checkpoints"],apps:["app_store","installed_apps","app_versions"],comms:["conversations","conversation_members","messages","mailbox","team_chat","notification_preferences"],config:["os_config","instances","warm_pool","golden_images"]},ALL_GROUPS=["boards","tasks","tags","projects","schedules","agents","apps","comms","config"]});var exports_table_detect={};__export(exports_table_detect,{filterAvailableTables:()=>filterAvailableTables});async function getAvailableTables(sql){return(await sql`
|
|
1203
1202
|
SELECT table_name
|
|
1204
1203
|
FROM information_schema.tables
|
|
1205
1204
|
WHERE table_schema = current_schema()
|
|
@@ -1717,4 +1716,4 @@ Valid columns for board "${board.name}": ${validCols}`),process.exit(1)}catch{}}
|
|
|
1717
1716
|
Board: ${board.name} (${board.id})`),console.log("\u2550".repeat(40));let columns=[...board.columns].sort((a,b2)=>a.position-b2.position);for(let col of columns)printColumnTasks(col.label,tasks.filter((t)=>t.columnId===col.id));let columnIds=new Set(columns.map((c)=>c.id)),orphaned=tasks.filter((t)=>t.columnId&&!columnIds.has(t.columnId));if(orphaned.length>0)printColumnTasks("Orphaned",orphaned,!1);console.log("")}async function handleTaskCreate(title,options){let ts2=await getTaskService7(),actor=currentActor2(),repoPath,projectId;if(options.project){let project=await ts2.getProjectByName(options.project);if(!project)project=await ts2.createProject({name:options.project});projectId=project.id,repoPath=project.repoPath??void 0}let parentId;if(options.parent){if(parentId=await ts2.resolveTaskId(options.parent,repoPath)??void 0,!parentId)console.error(`Error: Parent task not found: ${options.parent}`),process.exit(1)}let boardId=await resolveBoardOption(options.board),task=await ts2.createTask({title,typeId:options.type,priority:options.priority,dueDate:options.due,startDate:options.start,parentId,description:options.description,estimatedEffort:options.effort,boardId},repoPath,projectId);if(await ts2.assignTask(task.id,actor,"creator",{},task.repoPath),options.assign)await ts2.assignTask(task.id,localActor2(options.assign),"assignee",{},task.repoPath);if(options.tags){let tagIds=options.tags.split(",").map((t)=>t.trim());await ts2.tagTask(task.id,tagIds,actor,task.repoPath)}if(options.comment)await ts2.commentOnTask(task.id,actor,options.comment,task.repoPath);if(console.log(`Created task #${task.seq}: ${task.title}`),console.log(` ID: ${task.id}`),console.log(` Stage: ${task.stage} | Priority: ${task.priority}`),options.due)console.log(` Due: ${options.due}`)}function registerTaskCommands(program2){let task=program2.command("task").description("Task lifecycle management");task.command("create <title>").description("Create a new task").option("--type <type>","Task type","software").option("--priority <priority>","Priority: urgent, high, normal, low","normal").option("--due <date>","Due date (YYYY-MM-DD)").option("--start <date>","Start date (YYYY-MM-DD)").option("--tags <tags>","Comma-separated tag IDs").option("--parent <id>","Parent task ID or #seq").option("--assign <name>","Assign to local actor").option("--description <text>","Task description").option("--effort <effort>",'Estimated effort (e.g., "2h", "3 points")').option("--comment <msg>","Initial comment on the task").option("--project <name>","Create task in a specific project (overrides CWD)").option("--board <name>","Board name to assign task to").action(async(title,options)=>{try{await handleTaskCreate(title,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("list").description("List tasks with filters").option("--stage <stage>","Filter by stage").option("--type <type>","Filter by type").option("--status <status>","Filter by status").option("--priority <priority>","Filter by priority").option("--release <release>","Filter by release").option("--due-before <date>","Filter by due date").option("--mine","Show only tasks assigned to me").option("--project <name>","Show tasks for a specific project").option("--board <name>","Filter by board name").option("--by-column","Group tasks by board column (kanban view)").option("--include-done","Include done tasks in kanban view (hidden by default)").option("--all","Show tasks from ALL projects").option("--json","Output as JSON").action(async(options)=>{try{let ts2=await getTaskService7(),filters={stage:options.stage,typeId:options.type,status:options.status,priority:options.priority,releaseId:options.release,dueBefore:options.dueBefore,projectName:options.project,boardName:options.board,allProjects:options.all,...options.all?{limit:1e4}:{}},tasks;if(options.mine)tasks=await ts2.listTasksForActor(currentActor2(),filters);else tasks=await ts2.listTasks(filters);if(options.byColumn){if(!options.board)console.error("Error: --by-column requires --board"),process.exit(1);if(!options.includeDone)tasks=tasks.filter((t)=>t.status!=="done");await printByColumn(tasks,options.board);return}if(options.json){console.log(JSON.stringify(tasks,null,2));return}printTaskList(tasks,options.all)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("show <id>").description("Show task detail (accepts task-id or #seq)").option("--json","Output as JSON").action(async(id,options)=>{try{let t=await(await getTaskService7()).getTask(id);if(!t)console.error(`Error: Task not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}await printTaskDetail(t)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("move <id>").description("Move task to a new stage").requiredOption("--to <stage>","Target stage").option("--comment <msg>","Comment on the move").action(async(id,options)=>{try{let ts2=await getTaskService7(),actor=currentActor2(),t=await ts2.moveTask(id,options.to,actor,options.comment);console.log(`Moved task #${t.seq} to stage "${t.stage}".`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("Invalid stage"))await handleInvalidStageError(id,message);console.error(`Error: ${message}`),process.exit(1)}}),task.command("assign <id>").description("Assign an actor to a task").requiredOption("--to <name>","Actor name").option("--role <role>","Actor role","assignee").option("--comment <msg>","Comment on the assignment").action(async(id,options)=>{try{let ts2=await getTaskService7(),actor=currentActor2();if(await ts2.assignTask(id,localActor2(options.to),options.role,{}),options.comment)await ts2.commentOnTask(id,actor,options.comment);console.log(`Assigned "${options.to}" as ${options.role??"assignee"} on task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("tag <id> <tags...>").description("Add tags to a task").action(async(id,tags)=>{try{await(await getTaskService7()).tagTask(id,tags,currentActor2()),console.log(`Tagged task ${id} with: ${tags.join(", ")}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("comment <id> <message>").description("Add a comment to a task").option("--reply-to <msgId>","Reply to a specific message ID").action(async(id,message,options)=>{try{let ts2=await getTaskService7(),replyTo=options.replyTo?Number(options.replyTo):void 0,msg=await ts2.commentOnTask(id,currentActor2(),message,void 0,replyTo);console.log(`Comment #${msg.id} added to task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("block <id>").description("Mark task as blocked").requiredOption("--reason <reason>","Reason for blocking").option("--comment <msg>","Additional comment").action(async(id,options)=>{try{let ts2=await getTaskService7(),actor=currentActor2(),t=await ts2.blockTask(id,options.reason,actor,options.comment);console.log(`Task #${t.seq} blocked: ${options.reason}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unblock <id>").description("Unblock a task").option("--comment <msg>","Comment on unblock").action(async(id,options)=>{try{let ts2=await getTaskService7(),actor=currentActor2(),t=await ts2.unblockTask(id,actor,options.comment);console.log(`Task #${t.seq} unblocked.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("done <id>").description("Mark task as done").option("--comment <msg>","Comment on completion").action(async(id,options)=>{try{let ts2=await getTaskService7(),actor=currentActor2(),t=await ts2.markDone(id,actor,options.comment);console.log(`Task #${t.seq} marked as done.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("checkout <id>").description("Atomically claim a task for execution").action(async(id)=>{try{let ts2=await getTaskService7(),runId=getRunId(),t=await ts2.checkoutTask(id,runId);console.log(`Checked out task #${t.seq} for run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("release <id>").description("Release task checkout claim").action(async(id)=>{try{let ts2=await getTaskService7(),runId=getRunId(),t=await ts2.releaseTask(id,runId);console.log(`Released task #${t.seq} from run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unlock <id>").description("Force-release a stale checkout (admin override)").action(async(id)=>{try{let t=await(await getTaskService7()).forceUnlockTask(id);console.log(`Force-unlocked task #${t.seq}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("dep <id>").description("Manage task dependencies").option("--depends-on <id2>","This task depends on id2").option("--blocks <id2>","This task blocks id2").option("--relates-to <id2>","This task relates to id2").option("--remove <id2>","Remove dependency on id2").action(async(id,options)=>{try{let ts2=await getTaskService7();if(options.remove){if(await ts2.removeDependency(id,options.remove))console.log(`Removed dependency between ${id} and ${options.remove}.`);else console.log("No dependency found to remove.");return}if(options.dependsOn)await ts2.addDependency(id,options.dependsOn,"depends_on"),console.log(`${id} now depends on ${options.dependsOn}.`);if(options.blocks)await ts2.addDependency(id,options.blocks,"blocks"),console.log(`${id} now blocks ${options.blocks}.`);if(options.relatesTo)await ts2.addDependency(id,options.relatesTo,"relates_to"),console.log(`${id} now relates to ${options.relatesTo}.`);if(!options.dependsOn&&!options.blocks&&!options.relatesTo)console.error("Error: Specify --depends-on, --blocks, --relates-to, or --remove."),process.exit(1)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_team_manager();import{existsSync as existsSync30}from"fs";import{copyFile as copyFile2,cp,mkdir as mkdir7}from"fs/promises";import{join as join41,resolve as resolve6}from"path";function registerTeamNamespace(program2){let team=program2.command("team").description("Team lifecycle management");team.command("create <name>").description("Create a new team with a git worktree").requiredOption("--repo <path>","Path to the git repository").option("--branch <branch>","Base branch to create from","dev").option("--wish <slug>","Wish slug \u2014 auto-spawns a task leader with wish context").option("--session <name>","Tmux session name (avoids session explosion on parallel creates)").option("--no-spawn","Create team and copy wish without spawning the leader (useful for testing)").action(async(name,options)=>{try{await handleTeamCreate(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("hire <agent>").description('Add an agent to a team ("council" hires all 10 council members)').option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);let added=await hireAgent(teamName,agent);if(added.length===0)console.log(`Agent "${agent}" is already a member of "${teamName}".`);else if(agent==="council"){console.log(`Hired ${added.length} council members to "${teamName}":`);for(let name of added)console.log(` + ${name}`)}else console.log(`Hired "${agent}" to team "${teamName}".`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("fire <agent>").description("Remove an agent from a team").option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);if(await fireAgent(teamName,agent))console.log(`Fired "${agent}" from team "${teamName}".`);else console.error(`Agent "${agent}" is not a member of "${teamName}".`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("ls [name]").alias("list").description("List teams or members of a team").option("--json","Output as JSON").action(async(name,options)=>{try{if(name)await printMembers(name,options.json);else await printTeams(options.json)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("disband <name>").description("Disband a team: kill members, remove worktree, delete config").action(async(name)=>{try{if(await disbandTeam(name))console.log(`Team "${name}" disbanded.`);else console.error(`Team "${name}" not found.`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("done <name>").description("Mark a team as done and kill all members").action(async(name)=>{try{await setTeamStatus(name,"done"),await killTeamMembers(name),console.log(`Team "${name}" marked as done. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("blocked <name>").description("Mark a team as blocked and kill all members").action(async(name)=>{try{await setTeamStatus(name,"blocked"),await killTeamMembers(name),console.log(`Team "${name}" marked as blocked. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}async function handleTeamCreate(name,options){if(options.wish){let resolvedRepo=resolve6(options.repo),wishPath=join41(resolvedRepo,".genie","wishes",options.wish,"WISH.md");if(!existsSync30(wishPath)){let cwdWishDir=join41(process.cwd(),".genie","wishes",options.wish),cwdWishPath=join41(cwdWishDir,"WISH.md");if(existsSync30(cwdWishPath)){let destDir=join41(resolvedRepo,".genie","wishes",options.wish);await mkdir7(destDir,{recursive:!0}),await cp(cwdWishDir,destDir,{recursive:!0}),console.log(`Wish: copied ${options.wish}/WISH.md to repo`)}else console.error(`Error: Wish not found at ${wishPath}`),process.exit(1)}}let config=await createTeam(name,options.repo,options.branch),needsUpdate=!1;if(options.wish)config.wishSlug=options.wish,needsUpdate=!0;if(options.session)config.tmuxSessionName=options.session,needsUpdate=!0;if(needsUpdate)await updateTeamConfig(name,config);if(console.log(`Team "${config.name}" created.`),console.log(` Worktree: ${config.worktreePath}`),console.log(` Branch: ${config.name} (from ${config.baseBranch})`),config.tmuxSessionName)console.log(` Session: ${config.tmuxSessionName}`);if(config.nativeTeamsEnabled)console.log(" Native teams: enabled");if(options.wish&&options.spawn!==!1)await spawnLeaderWithWish(config,options.wish,options.repo,options.session)}async function spawnLeaderWithWish(config,slug,repoPath,sessionOverride){let{handleWorkerSpawn:handleWorkerSpawn2}=await Promise.resolve().then(() => (init_agents(),exports_agents)),{getCurrentSessionName:getCurrentSessionName2}=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),resolvedRepo=resolve6(repoPath),tmuxSession=sessionOverride??await getCurrentSessionName2(config.name)??config.name;config.tmuxSessionName=tmuxSession,await updateTeamConfig(config.name,config);let sourceWishPath=join41(resolvedRepo,".genie","wishes",slug,"WISH.md");if(!existsSync30(sourceWishPath))console.error(`Error: Wish not found at ${sourceWishPath}`),process.exit(1);let destWishDir=join41(config.worktreePath,".genie","wishes",slug);await mkdir7(destWishDir,{recursive:!0});let destWishPath=join41(destWishDir,"WISH.md");await copyFile2(sourceWishPath,destWishPath),console.log(` Wish: copied ${slug}/WISH.md into worktree`);let standardTeam=["team-lead","engineer","reviewer","qa","fix"];for(let role of standardTeam)await hireAgent(config.name,role);console.log(` Team: hired ${standardTeam.join(", ")}`);let members=standardTeam.filter((r)=>r!=="team-lead").join(", "),kickoffPrompt=`Your team is "${config.name}". Repo: ${config.repo}. Branch: ${config.name}. Worktree: ${config.worktreePath}. Wish slug: ${slug}. Your team members are: ${members} (already hired \u2014 genie work will spawn them automatically). Read the wish at .genie/wishes/${slug}/WISH.md and execute the full lifecycle autonomously.`;await handleWorkerSpawn2("team-lead",{provider:"claude",team:config.name,cwd:config.worktreePath,session:tmuxSession,initialPrompt:kickoffPrompt});let result=await(await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router))).sendMessage(config.worktreePath,"cli","team-lead",kickoffPrompt);if(!result.delivered)console.warn(`\u26A0 Backup delivery to team-lead failed: ${result.reason??"unknown"}`);console.log(" Leader: spawned and working")}async function autoDetectTeam(){let envTeam=process.env.GENIE_TEAM;if(envTeam)return envTeam;let teams=await listTeams2();if(teams.length===1)return teams[0].name;return null}async function printMembers(name,json2){let members=await listMembers(name);if(members===null)console.error(`Team "${name}" not found.`),process.exit(1);if(json2){console.log(JSON.stringify(members,null,2));return}if(members.length===0){console.log(`Team "${name}" has no members. Hire agents with: genie team hire <agent> --team ${name}`);return}console.log(""),console.log(`MEMBERS of "${name}"`),console.log("-".repeat(60));for(let m of members)console.log(` ${m}`);console.log("")}async function printTeams(json2){let teams=await listTeams2();if(json2){console.log(JSON.stringify(teams,null,2));return}if(teams.length===0){console.log("No teams found. Create one with: genie team create <name> --repo <path>");return}console.log(""),console.log("TEAMS"),console.log("-".repeat(60));for(let t of teams)printTeamSummary(t);console.log("")}function printTeamSummary(t){let status=t.status??"in_progress";console.log(` ${t.name} [${status}]`),console.log(` Repo: ${t.repo}`),console.log(` Branch: ${t.name} (from ${t.baseBranch})`),console.log(` Worktree: ${t.worktreePath}`),console.log(` Members: ${t.members.length}`)}var _taskService8;async function getTaskService8(){if(!_taskService8)_taskService8=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService8}function printTypeTable(types4){console.log(` ${padRight("ID",20)} ${padRight("NAME",30)} ${padRight("STAGES",8)} BUILTIN`),console.log(` ${"\u2500".repeat(70)}`);for(let t of types4){let stageCount=Array.isArray(t.stages)?t.stages.length:0,builtin=t.isBuiltin?"yes":"no";console.log(` ${padRight(t.id,20)} ${padRight(t.name,30)} ${padRight(String(stageCount),8)} ${builtin}`)}console.log(`
|
|
1718
1717
|
${types4.length} type${types4.length===1?"":"s"}`)}function printTypePipeline(t){if(console.log(`
|
|
1719
1718
|
Type: ${t.name} (${t.id})`),t.description)console.log(`Description: ${t.description}`);if(t.icon)console.log(`Icon: ${t.icon}`);console.log(`Built-in: ${t.isBuiltin?"yes":"no"}`),console.log("\u2500".repeat(60)),console.log(`
|
|
1720
|
-
Stage Pipeline:`);let stages=t.stages;for(let i2=0;i2<stages.length;i2++){let s=stages[i2],arrow=i2<stages.length-1?" \u2192":"",gate=s.gate?` [gate: ${s.gate}]`:"",action=s.action?` (action: ${s.action})`:"",auto=s.auto_advance?" [auto]":"";console.log(` ${i2+1}. ${s.label??s.name}${gate}${action}${auto}${arrow}`)}console.log("")}async function handleTypeList(options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let types4=await(await getTaskService8()).listTypes();if(options.json){console.log(JSON.stringify(types4,null,2));return}printTypeTable(types4)}async function handleTypeShow(id,options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let t=await(await getTaskService8()).getType(id);if(!t)console.error(`Error: Type not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}printTypePipeline(t)}async function handleTypeCreate(name,options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let ts2=await getTaskService8(),stages;try{if(stages=JSON.parse(options.stages),!Array.isArray(stages))throw Error("Stages must be a JSON array")}catch(err){console.error(`Error: Invalid stages JSON. ${err instanceof Error?err.message:String(err)}`),process.exit(1)}for(let s of stages)if(typeof s!=="object"||s===null||!("name"in s))console.error('Error: Each stage must have at least a "name" field.'),process.exit(1);let id=name.toLowerCase().replace(/\s+/g,"-"),t=await ts2.createType({id,name,description:options.description,icon:options.icon,stages});console.log(`Created type "${t.name}" (${t.id}) with ${stages.length} stages.`)}function registerTypeCommands(program2){let type2=program2.command("type").description("Task type management");type2.command("list").description("List all task types").option("--json","Output as JSON").action(async(options)=>{try{await handleTypeList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("show <id>").description("Show task type detail with stage pipeline").option("--json","Output as JSON").action(async(id,options)=>{try{await handleTypeShow(id,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("create <name>").description("Create a custom task type").requiredOption("--stages <json>","Stages JSON array").option("--description <text>","Type description").option("--icon <icon>","Type icon").action(async(name,options)=>{try{await handleTypeCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}try{let{execSync:execSyncStartup}=__require("child_process");if(execSyncStartup("git config core.bare",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()==="true")execSyncStartup("git config core.bare false",{stdio:["pipe","pipe","pipe"]})}catch{}var program2=new Command;program2.name("genie").description("Genie CLI - AI-assisted development").version(VERSION);async function startNamedSession(name){let{buildTeamLeadCommand:buildTeamLeadCommand2,sessionExists:sessionExists2}=await Promise.resolve().then(() => (init_team_lead_command(),exports_team_lead_command)),{getAgentsFilePath:getAgentsFilePath2}=await Promise.resolve().then(() => (init_session(),exports_session)),systemPromptFile=getAgentsFilePath2(),hasPriorSession=sessionExists2(name),cmd=buildTeamLeadCommand2(name,{systemPromptFile:systemPromptFile??void 0,continueName:hasPriorSession?name:void 0});console.log(hasPriorSession?`Resuming session: ${name}`:`Starting new session: ${name}`);let{spawnSync:spawnSync2}=await import("child_process"),result=spawnSync2("sh",["-c",cmd],{stdio:"inherit"});if(result.status)process.exit(result.status)}program2.command("setup").description("Configure genie settings").option("--quick","Accept all defaults").option("--shortcuts","Only configure keyboard shortcuts").option("--codex","Only configure Codex integration").option("--terminal","Only configure terminal defaults").option("--session","Only configure session settings").option("--reset","Reset configuration to defaults").option("--show","Show current configuration").action(async(options)=>{await setupCommand(options)});program2.command("doctor").description("Run diagnostic checks on genie installation").action(doctorCommand);program2.command("update").description("Update Genie CLI to the latest version").option("--next","Switch to dev builds (npm @next tag)").option("--stable","Switch to stable releases (npm @latest tag)").action(updateCommand);program2.command("uninstall").description("Remove Genie CLI and clean up hooks").action(uninstallCommand);var shortcuts=program2.command("shortcuts").description("Manage tmux keyboard shortcuts");shortcuts.action(shortcutsShowCommand);shortcuts.command("show").description("Show available shortcuts and installation status").action(shortcutsShowCommand);shortcuts.command("install").description("Install shortcuts to config files (~/.tmux.conf, shell rc)").action(shortcutsInstallCommand);shortcuts.command("uninstall").description("Remove shortcuts from config files").action(shortcutsUninstallCommand);registerTeamNamespace(program2);registerDirNamespace(program2);registerAgentNamespace(program2);registerSendInboxCommands(program2);registerStateCommands(program2);registerDispatchCommands(program2);registerHookNamespace(program2);registerDbCommands(program2);registerScheduleCommands(program2);registerDaemonCommands(program2);registerTaskCommands(program2);registerTypeCommands(program2);registerBoardCommands(program2);registerTagCommands(program2);registerReleaseCommands(program2);registerProjectCommands(program2);registerNotifyCommands(program2);registerEventsCommands(program2);registerSessionsCommands(program2);registerMetricsCommands(program2);registerExportCommands(program2);registerImportCommands(program2);registerInstallCommand(program2);registerPublishCommand(program2);var itemCmd=program2.command("item").description("Item registry management");registerItemUninstallCommand(itemCmd);registerItemUpdateCommand(itemCmd);var auditTimers=new Map;program2.hook("preAction",(_thisCommand,actionCommand)=>{let name=actionCommand.name();auditTimers.set(name,Date.now()),recordAuditEvent("command",name,"command_start",getActor(),{args:actionCommand.args}).catch(()=>{})});program2.hook("postAction",(_thisCommand,actionCommand)=>{let name=actionCommand.name(),startMs=auditTimers.get(name),durationMs=startMs?Date.now()-startMs:void 0;auditTimers.delete(name),recordAuditEvent("command",name,"command_success",getActor(),{args:actionCommand.args,duration_ms:durationMs}).catch(()=>{})});program2.command("spawn <name>").description("Spawn a new agent by name (resolves from directory or built-ins)").option("--provider <provider>","Provider: claude or codex","claude").option("--team <team>","Team name",process.env.GENIE_TEAM??"genie").option("--model <model>","Model override (e.g., sonnet, opus)").option("--skill <skill>","Skill to load (optional)").option("--layout <layout>","Layout mode: mosaic (default) or vertical").option("--color <color>","Teammate pane border color").option("--plan-mode","Start teammate in plan mode").option("--permission-mode <mode>","Permission mode (e.g., acceptEdits)").option("--extra-args <args...>","Extra CLI args forwarded to provider").option("--cwd <path>","Working directory for the agent (overrides directory entry)").option("--session <session>","Tmux session name to spawn into").option("--no-auto-resume","Disable auto-resume on pane death").action(async(name,options)=>{try{await handleWorkerSpawn(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("stop <name>").description("Stop an agent (preserves session for resume)").action(async(name)=>{try{await handleWorkerStop(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("resume [name]").description("Resume a suspended/failed agent with its Claude session").option("--all","Resume all eligible agents").action(async(name,options)=>{try{await handleWorkerResume(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("history <name>").description("Show compressed session history for an agent").option("--full","Show full conversation without compression").option("--since <n>","Show last N user/assistant exchanges",Number.parseInt).option("--last <n>","Show last N transcript entries",Number.parseInt).option("--type <role>","Filter by role (user, assistant, tool_call)").option("--after <timestamp>","Only entries after ISO timestamp").option("--json","Output as JSON").option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--raw","Output raw JSONL entries").option("--log-file <path>","Direct path to log file (for testing)").action(async(name,options)=>{await historyCommand(name,options)});program2.command("log [agent]").description("Unified observability feed \u2014 aggregates transcript, DMs, team chat").option("--team <name>","Show interleaved feed for all agents in a team").option("--type <kind>","Filter by event kind (transcript, message, tool_call, state, system)").option("--since <timestamp>","Only events after ISO timestamp").option("--last <n>","Show last N events",Number.parseInt).option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--json","Output as pretty JSON").option("-f, --follow","Follow mode \u2014 real-time streaming").action(async(agent,options)=>{await logCommand(agent,options)});var qaCmd=program2.command("qa").description("QA \u2014 self-testing system for genie CLI");qaCmd.command("run [target]",{isDefault:!0}).description("Run QA specs (all, a domain, or a single spec)").option("--timeout <seconds>","Max seconds per spec",(v)=>Number(v),60).option("--parallel <n>","Max specs to run in parallel",(v)=>Number(v),5).option("--verbose","Show all collected events").option("--ndjson","Machine-readable NDJSON output").action(async(target,options)=>{await qaCommand(target,options)});qaCmd.command("status").description("Show QA dashboard with last results per spec").option("--json","Output as JSON").action(async(options)=>{await qaStatusCommand(options)});qaCmd.command("history").description("Show recent QA runs").action(async()=>{await qaHistoryCommand()});program2.command("qa-report <json>").description("Publish QA result via NATS (called by QA team-lead)").action(async(json2)=>{let team=process.env.GENIE_TEAM;if(!team)console.error("Error: GENIE_TEAM not set. This command must be run by a QA team-lead agent."),process.exit(1);try{let{publish:publish2,close:close2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client)),data=JSON.parse(json2);await publish2(`genie.qa.${team}.result`,data),await close2(),console.log(`QA result published to genie.qa.${team}.result`)}catch(err){console.error(`Failed to publish QA result: ${err}`),process.exit(1)}});program2.command("read <name>").description("Read terminal output from an agent pane").option("-n, --lines <number>","Number of lines to read").option("--from <line>","Start line").option("--to <line>","End line").option("--range <range>",'Line range (e.g., "10-20")').option("--search <text>","Search for text").option("--grep <pattern>","Grep for pattern").option("-f, --follow","Follow mode (like tail -f)").option("--all","Show all output").option("-r, --reverse","Reverse order").option("--json","Output as JSON").action(async(name,options)=>{await readSessionLogs2(name,options)});program2.command("answer <name> <choice>").description('Answer a question for an agent (use "text:..." for text input)').action(async(name,choice)=>{await answerQuestion(name,choice)});program2.command("ls").description("List registered agents with runtime status").option("--json","Output as JSON").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});var args=process.argv.slice(2);if(args.length===0||args.every((a)=>a==="--reset")){let{sessionCommand:sessionCommand2}=await Promise.resolve().then(() => (init_session(),exports_session));await sessionCommand2({reset:args.includes("--reset")}),process.exit(0)}var sessionIdx=args.indexOf("--session");if(sessionIdx!==-1&&sessionIdx+1<args.length){let sessionName=args[sessionIdx+1];if(!args.filter((_,i2)=>i2!==sessionIdx&&i2!==sessionIdx+1).some((a)=>!a.startsWith("-")))try{await startNamedSession(sessionName),process.exit(0)}catch(err){console.error(`Error: ${err instanceof Error?err.message:err}`),process.exit(1)}else program2.parse()}else program2.parse();
|
|
1719
|
+
Stage Pipeline:`);let stages=t.stages;for(let i2=0;i2<stages.length;i2++){let s=stages[i2],arrow=i2<stages.length-1?" \u2192":"",gate=s.gate?` [gate: ${s.gate}]`:"",action=s.action?` (action: ${s.action})`:"",auto=s.auto_advance?" [auto]":"";console.log(` ${i2+1}. ${s.label??s.name}${gate}${action}${auto}${arrow}`)}console.log("")}async function handleTypeList(options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let types4=await(await getTaskService8()).listTypes();if(options.json){console.log(JSON.stringify(types4,null,2));return}printTypeTable(types4)}async function handleTypeShow(id,options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let t=await(await getTaskService8()).getType(id);if(!t)console.error(`Error: Type not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}printTypePipeline(t)}async function handleTypeCreate(name,options){console.warn("Warning: `genie type` is deprecated. Use `genie board` instead.");let ts2=await getTaskService8(),stages;try{if(stages=JSON.parse(options.stages),!Array.isArray(stages))throw Error("Stages must be a JSON array")}catch(err){console.error(`Error: Invalid stages JSON. ${err instanceof Error?err.message:String(err)}`),process.exit(1)}for(let s of stages)if(typeof s!=="object"||s===null||!("name"in s))console.error('Error: Each stage must have at least a "name" field.'),process.exit(1);let id=name.toLowerCase().replace(/\s+/g,"-"),t=await ts2.createType({id,name,description:options.description,icon:options.icon,stages});console.log(`Created type "${t.name}" (${t.id}) with ${stages.length} stages.`)}function registerTypeCommands(program2){let type2=program2.command("type").description("Task type management");type2.command("list").description("List all task types").option("--json","Output as JSON").action(async(options)=>{try{await handleTypeList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("show <id>").description("Show task type detail with stage pipeline").option("--json","Output as JSON").action(async(id,options)=>{try{await handleTypeShow(id,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("create <name>").description("Create a custom task type").requiredOption("--stages <json>","Stages JSON array").option("--description <text>","Type description").option("--icon <icon>","Type icon").action(async(name,options)=>{try{await handleTypeCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}try{let{execSync:execSyncStartup}=__require("child_process");if(execSyncStartup("git config core.bare",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()==="true")execSyncStartup("git config core.bare false",{stdio:["pipe","pipe","pipe"]})}catch{}var program2=new Command;program2.name("genie").description("Genie CLI - AI-assisted development").version(VERSION);async function startNamedSession(name){let{buildTeamLeadCommand:buildTeamLeadCommand2,sessionExists:sessionExists2}=await Promise.resolve().then(() => (init_team_lead_command(),exports_team_lead_command)),{getAgentsFilePath:getAgentsFilePath2}=await Promise.resolve().then(() => (init_session(),exports_session)),systemPromptFile=getAgentsFilePath2(),hasPriorSession=sessionExists2(name),cmd=buildTeamLeadCommand2(name,{systemPromptFile:systemPromptFile??void 0,continueName:hasPriorSession?name:void 0});console.log(hasPriorSession?`Resuming session: ${name}`:`Starting new session: ${name}`);let{spawnSync:spawnSync2}=await import("child_process"),result=spawnSync2("sh",["-c",cmd],{stdio:"inherit"});if(result.status)process.exit(result.status)}program2.command("setup").description("Configure genie settings").option("--quick","Accept all defaults").option("--shortcuts","Only configure keyboard shortcuts").option("--codex","Only configure Codex integration").option("--terminal","Only configure terminal defaults").option("--session","Only configure session settings").option("--reset","Reset configuration to defaults").option("--show","Show current configuration").action(async(options)=>{await setupCommand(options)});program2.command("doctor").description("Run diagnostic checks on genie installation").action(doctorCommand);program2.command("update").description("Update Genie CLI to the latest version").option("--next","Switch to dev builds (npm @next tag)").option("--stable","Switch to stable releases (npm @latest tag)").action(updateCommand);program2.command("uninstall").description("Remove Genie CLI and clean up hooks").action(uninstallCommand);var shortcuts=program2.command("shortcuts").description("Manage tmux keyboard shortcuts");shortcuts.action(shortcutsShowCommand);shortcuts.command("show").description("Show available shortcuts and installation status").action(shortcutsShowCommand);shortcuts.command("install").description("Install shortcuts to config files (~/.tmux.conf, shell rc)").action(shortcutsInstallCommand);shortcuts.command("uninstall").description("Remove shortcuts from config files").action(shortcutsUninstallCommand);registerTeamNamespace(program2);registerDirNamespace(program2);registerAgentNamespace(program2);registerSendInboxCommands(program2);registerStateCommands(program2);registerDispatchCommands(program2);registerHookNamespace(program2);registerDbCommands(program2);registerScheduleCommands(program2);registerDaemonCommands(program2);registerTaskCommands(program2);registerTypeCommands(program2);registerBoardCommands(program2);registerTagCommands(program2);registerReleaseCommands(program2);registerProjectCommands(program2);registerNotifyCommands(program2);registerEventsCommands(program2);registerSessionsCommands(program2);registerMetricsCommands(program2);registerExportCommands(program2);registerImportCommands(program2);registerInstallCommand(program2);registerPublishCommand(program2);var itemCmd=program2.command("item").description("Item registry management");registerItemUninstallCommand(itemCmd);registerItemUpdateCommand(itemCmd);var auditTimers=new Map;program2.hook("preAction",(_thisCommand,actionCommand)=>{let name=actionCommand.name();auditTimers.set(name,Date.now()),Promise.resolve().then(() => (init_db(),exports_db)).then(({isConnected:isConnected2})=>{if(!isConnected2())return;recordAuditEvent("command",name,"command_start",getActor(),{args:actionCommand.args}).catch(()=>{})}).catch(()=>{})});program2.hook("postAction",(_thisCommand,actionCommand)=>{let name=actionCommand.name(),startMs=auditTimers.get(name),durationMs=startMs?Date.now()-startMs:void 0;auditTimers.delete(name),Promise.resolve().then(() => (init_db(),exports_db)).then(({isConnected:isConnected2})=>{if(!isConnected2())return;recordAuditEvent("command",name,"command_success",getActor(),{args:actionCommand.args,duration_ms:durationMs}).catch(()=>{})}).catch(()=>{})});program2.command("spawn <name>").description("Spawn a new agent by name (resolves from directory or built-ins)").option("--provider <provider>","Provider: claude or codex","claude").option("--team <team>","Team name",process.env.GENIE_TEAM??"genie").option("--model <model>","Model override (e.g., sonnet, opus)").option("--skill <skill>","Skill to load (optional)").option("--layout <layout>","Layout mode: mosaic (default) or vertical").option("--color <color>","Teammate pane border color").option("--plan-mode","Start teammate in plan mode").option("--permission-mode <mode>","Permission mode (e.g., acceptEdits)").option("--extra-args <args...>","Extra CLI args forwarded to provider").option("--cwd <path>","Working directory for the agent (overrides directory entry)").option("--session <session>","Tmux session name to spawn into").option("--no-auto-resume","Disable auto-resume on pane death").action(async(name,options)=>{try{await handleWorkerSpawn(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("stop <name>").description("Stop an agent (preserves session for resume)").action(async(name)=>{try{await handleWorkerStop(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("resume [name]").description("Resume a suspended/failed agent with its Claude session").option("--all","Resume all eligible agents").action(async(name,options)=>{try{await handleWorkerResume(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("history <name>").description("Show compressed session history for an agent").option("--full","Show full conversation without compression").option("--since <n>","Show last N user/assistant exchanges",Number.parseInt).option("--last <n>","Show last N transcript entries",Number.parseInt).option("--type <role>","Filter by role (user, assistant, tool_call)").option("--after <timestamp>","Only entries after ISO timestamp").option("--json","Output as JSON").option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--raw","Output raw JSONL entries").option("--log-file <path>","Direct path to log file (for testing)").action(async(name,options)=>{await historyCommand(name,options)});program2.command("log [agent]").description("Unified observability feed \u2014 aggregates transcript, DMs, team chat").option("--team <name>","Show interleaved feed for all agents in a team").option("--type <kind>","Filter by event kind (transcript, message, tool_call, state, system)").option("--since <timestamp>","Only events after ISO timestamp").option("--last <n>","Show last N events",Number.parseInt).option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--json","Output as pretty JSON").option("-f, --follow","Follow mode \u2014 real-time streaming").action(async(agent,options)=>{await logCommand(agent,options)});var qaCmd=program2.command("qa").description("QA \u2014 self-testing system for genie CLI");qaCmd.command("run [target]",{isDefault:!0}).description("Run QA specs (all, a domain, or a single spec)").option("--timeout <seconds>","Max seconds per spec",(v)=>Number(v),60).option("--parallel <n>","Max specs to run in parallel",(v)=>Number(v),5).option("--verbose","Show all collected events").option("--ndjson","Machine-readable NDJSON output").action(async(target,options)=>{await qaCommand(target,options)});qaCmd.command("status").description("Show QA dashboard with last results per spec").option("--json","Output as JSON").action(async(options)=>{await qaStatusCommand(options)});qaCmd.command("history").description("Show recent QA runs").action(async()=>{await qaHistoryCommand()});program2.command("qa-report <json>").description("Publish QA result via NATS (called by QA team-lead)").action(async(json2)=>{let team=process.env.GENIE_TEAM;if(!team)console.error("Error: GENIE_TEAM not set. This command must be run by a QA team-lead agent."),process.exit(1);try{let{publish:publish2,close:close2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client)),data=JSON.parse(json2);await publish2(`genie.qa.${team}.result`,data),await close2(),console.log(`QA result published to genie.qa.${team}.result`)}catch(err){console.error(`Failed to publish QA result: ${err}`),process.exit(1)}});program2.command("read <name>").description("Read terminal output from an agent pane").option("-n, --lines <number>","Number of lines to read").option("--from <line>","Start line").option("--to <line>","End line").option("--range <range>",'Line range (e.g., "10-20")').option("--search <text>","Search for text").option("--grep <pattern>","Grep for pattern").option("-f, --follow","Follow mode (like tail -f)").option("--all","Show all output").option("-r, --reverse","Reverse order").option("--json","Output as JSON").action(async(name,options)=>{await readSessionLogs2(name,options)});program2.command("answer <name> <choice>").description('Answer a question for an agent (use "text:..." for text input)').action(async(name,choice)=>{await answerQuestion(name,choice)});program2.command("ls").description("List registered agents with runtime status").option("--json","Output as JSON").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});var args=process.argv.slice(2);if(args.length===0||args.every((a)=>a==="--reset")){let{sessionCommand:sessionCommand2}=await Promise.resolve().then(() => (init_session(),exports_session));await sessionCommand2({reset:args.includes("--reset")}),process.exit(0)}var sessionIdx=args.indexOf("--session");if(sessionIdx!==-1&&sessionIdx+1<args.length){let sessionName=args[sessionIdx+1];if(!args.filter((_,i2)=>i2!==sessionIdx&&i2!==sessionIdx+1).some((a)=>!a.startsWith("-")))try{await startNamedSession(sessionName),process.exit(0)}catch(err){console.error(`Error: ${err instanceof Error?err.message:err}`),process.exit(1)}else program2.parse()}else program2.parse();
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260327.
|
|
3
|
+
"version": "4.260327.7",
|
|
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"
|
|
@@ -80,7 +80,9 @@ function createMockDeps(overrides: Partial<SchedulerDeps> = {}) {
|
|
|
80
80
|
|
|
81
81
|
let cleanupSchema: () => Promise<void>;
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
const DB_AVAILABLE = process.env.GENIE_PG_AVAILABLE === 'true' || !process.env.CI;
|
|
84
|
+
|
|
85
|
+
describe.skipIf(!DB_AVAILABLE)('resume', () => {
|
|
84
86
|
beforeAll(async () => {
|
|
85
87
|
cleanupSchema = await setupTestSchema();
|
|
86
88
|
});
|
package/src/genie.ts
CHANGED
|
@@ -189,10 +189,15 @@ const auditTimers = new Map<string, number>();
|
|
|
189
189
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
190
190
|
const name = actionCommand.name();
|
|
191
191
|
auditTimers.set(name, Date.now());
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
// Only record audit if DB is already connected — never trigger independent startup
|
|
193
|
+
import('./lib/db.js')
|
|
194
|
+
.then(({ isConnected }) => {
|
|
195
|
+
if (!isConnected()) return;
|
|
196
|
+
recordAuditEvent('command', name, 'command_start', getActor(), {
|
|
197
|
+
args: actionCommand.args,
|
|
198
|
+
}).catch(() => {});
|
|
199
|
+
})
|
|
200
|
+
.catch(() => {});
|
|
196
201
|
});
|
|
197
202
|
|
|
198
203
|
program.hook('postAction', (_thisCommand, actionCommand) => {
|
|
@@ -200,10 +205,15 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
|
|
|
200
205
|
const startMs = auditTimers.get(name);
|
|
201
206
|
const durationMs = startMs ? Date.now() - startMs : undefined;
|
|
202
207
|
auditTimers.delete(name);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
import('./lib/db.js')
|
|
209
|
+
.then(({ isConnected }) => {
|
|
210
|
+
if (!isConnected()) return;
|
|
211
|
+
recordAuditEvent('command', name, 'command_success', getActor(), {
|
|
212
|
+
args: actionCommand.args,
|
|
213
|
+
duration_ms: durationMs,
|
|
214
|
+
}).catch(() => {});
|
|
215
|
+
})
|
|
216
|
+
.catch(() => {});
|
|
207
217
|
});
|
|
208
218
|
|
|
209
219
|
// ============================================================================
|
package/src/lib/db.test.ts
CHANGED
|
@@ -262,12 +262,14 @@ describe('getLockfilePath', () => {
|
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
-
describe('
|
|
266
|
-
test('
|
|
267
|
-
// Read the source to verify the constant was changed
|
|
265
|
+
describe('daemon-owned pgserve', () => {
|
|
266
|
+
test('db.ts uses health check instead of port retry loop', async () => {
|
|
268
267
|
const source = readFileSync(join(__dirname, 'db.ts'), 'utf-8');
|
|
269
|
-
|
|
270
|
-
expect(
|
|
271
|
-
|
|
268
|
+
// No more MAX_PORT_RETRIES — daemon owns PG, no fallback ports
|
|
269
|
+
expect(source.includes('MAX_PORT_RETRIES')).toBe(false);
|
|
270
|
+
// Uses real postgres health check, not TCP-only
|
|
271
|
+
expect(source.includes('isPostgresHealthy')).toBe(true);
|
|
272
|
+
// Self-heal function exists
|
|
273
|
+
expect(source.includes('selfHealPostgres')).toBe(true);
|
|
272
274
|
});
|
|
273
275
|
});
|
package/src/lib/db.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Database connection management for Genie.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The daemon owns pgserve. CLI commands read the port file and connect.
|
|
5
|
+
* If no daemon is running, the CLI auto-starts it.
|
|
6
|
+
* Self-healing: health checks on every connection, automatic recovery.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { createConnection } from 'node:net';
|
|
12
11
|
import { homedir } from 'node:os';
|
|
13
12
|
import { join } from 'node:path';
|
|
14
13
|
import type { MultiTenantRouter } from 'pgserve';
|
|
@@ -25,7 +24,6 @@ export type Sql = postgres.Sql;
|
|
|
25
24
|
|
|
26
25
|
const DEFAULT_PORT = 19642;
|
|
27
26
|
const DEFAULT_HOST = '127.0.0.1';
|
|
28
|
-
const MAX_PORT_RETRIES = 3;
|
|
29
27
|
const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
|
|
30
28
|
const DATA_DIR = join(GENIE_HOME, 'data', 'pgserve');
|
|
31
29
|
const LOCKFILE_PATH = join(GENIE_HOME, 'pgserve.port');
|
|
@@ -37,56 +35,38 @@ function maskCredentials(url: string): string {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* sends SIGTERM → waits 5s → SIGKILL if still alive.
|
|
38
|
+
* Self-heal: kill stale postgres processes, clean shared memory, remove stale PID files.
|
|
39
|
+
* Handles zombies (which can't be killed) by cleaning their artifacts instead.
|
|
43
40
|
*/
|
|
44
|
-
function
|
|
45
|
-
const pidFile = join(dataDir, 'postmaster.pid');
|
|
46
|
-
if (!existsSync(pidFile)) return;
|
|
47
|
-
|
|
41
|
+
function selfHealPostgres(dataDir: string): void {
|
|
48
42
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} catch {
|
|
58
|
-
// Process doesn't exist — stale pid file, safe to ignore
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!cmdline.includes('postgres')) return;
|
|
43
|
+
// Kill any stale postgres processes associated with pgserve data dir
|
|
44
|
+
execSync(`pkill -9 -f "postgres.*${dataDir.replace(/\//g, '\\/')}" 2>/dev/null || true`, {
|
|
45
|
+
stdio: 'ignore',
|
|
46
|
+
timeout: 5000,
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
// Best effort
|
|
50
|
+
}
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
// Remove stale postmaster.pid
|
|
53
|
+
const pidFile = join(dataDir, 'postmaster.pid');
|
|
54
|
+
if (existsSync(pidFile)) {
|
|
65
55
|
try {
|
|
66
|
-
|
|
56
|
+
unlinkSync(pidFile);
|
|
67
57
|
} catch {
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Wait up to 5s for graceful shutdown
|
|
72
|
-
const deadline = Date.now() + 5000;
|
|
73
|
-
while (Date.now() < deadline) {
|
|
74
|
-
try {
|
|
75
|
-
process.kill(pid, 0); // Check if alive
|
|
76
|
-
execSync('sleep 0.2', { stdio: 'ignore' });
|
|
77
|
-
} catch {
|
|
78
|
-
return; // Process exited
|
|
79
|
-
}
|
|
58
|
+
// May still be locked
|
|
80
59
|
}
|
|
60
|
+
}
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
62
|
+
// Clean stale shared memory segments owned by this user
|
|
63
|
+
try {
|
|
64
|
+
execSync("ipcs -m 2>/dev/null | awk '$6 == 0 {print $2}' | xargs -I{} ipcrm -m {} 2>/dev/null || true", {
|
|
65
|
+
stdio: 'ignore',
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
});
|
|
88
68
|
} catch {
|
|
89
|
-
// Best effort
|
|
69
|
+
// Best effort
|
|
90
70
|
}
|
|
91
71
|
}
|
|
92
72
|
|
|
@@ -100,22 +80,26 @@ function getPort(): number {
|
|
|
100
80
|
return DEFAULT_PORT;
|
|
101
81
|
}
|
|
102
82
|
|
|
103
|
-
/**
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
resolve(false);
|
|
83
|
+
/** Health check: actually connect to postgres and run SELECT 1 */
|
|
84
|
+
async function isPostgresHealthy(port: number): Promise<boolean> {
|
|
85
|
+
try {
|
|
86
|
+
const pg = (await import('postgres')).default;
|
|
87
|
+
const probe = pg({
|
|
88
|
+
host: DEFAULT_HOST,
|
|
89
|
+
port,
|
|
90
|
+
database: DB_NAME,
|
|
91
|
+
username: 'postgres',
|
|
92
|
+
password: 'postgres',
|
|
93
|
+
max: 1,
|
|
94
|
+
connect_timeout: 3,
|
|
95
|
+
idle_timeout: 1,
|
|
117
96
|
});
|
|
118
|
-
|
|
97
|
+
await probe`SELECT 1`;
|
|
98
|
+
await probe.end({ timeout: 2 });
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
119
103
|
}
|
|
120
104
|
|
|
121
105
|
/** Read port from lockfile. Returns null if lockfile missing or invalid. */
|
|
@@ -179,83 +163,95 @@ export async function ensurePgserve(): Promise<number> {
|
|
|
179
163
|
}
|
|
180
164
|
}
|
|
181
165
|
|
|
166
|
+
const DAEMON_PID_PATH = join(GENIE_HOME, 'scheduler.pid');
|
|
167
|
+
const DAEMON_BOOT_TIMEOUT_MS = 15000;
|
|
168
|
+
|
|
182
169
|
async function _ensurePgserve(): Promise<number> {
|
|
183
|
-
// Already
|
|
184
|
-
if (activePort !== null
|
|
185
|
-
|
|
170
|
+
// Already connected in this process
|
|
171
|
+
if (activePort !== null) return activePort;
|
|
172
|
+
|
|
173
|
+
const port = getPort();
|
|
174
|
+
|
|
175
|
+
// 1. Read port file — daemon may have written it
|
|
176
|
+
const portFromFile = readLockfile();
|
|
177
|
+
if (portFromFile !== null && (await isPostgresHealthy(portFromFile))) {
|
|
178
|
+
activePort = portFromFile;
|
|
179
|
+
process.env.GENIE_PG_AVAILABLE = 'true';
|
|
180
|
+
return portFromFile;
|
|
186
181
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
|
|
183
|
+
// 2. Check default port (daemon may be running without port file, or external PG)
|
|
184
|
+
if (await isPostgresHealthy(port)) {
|
|
185
|
+
activePort = port;
|
|
186
|
+
process.env.GENIE_PG_AVAILABLE = 'true';
|
|
187
|
+
writeLockfile(port);
|
|
188
|
+
return port;
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
|
|
191
|
+
// 3. No healthy PG found — try daemon-based recovery (skip in CI/test environments)
|
|
192
|
+
const isCI = process.env.CI === 'true' || !!process.env.GENIE_TEST_SCHEMA;
|
|
193
|
+
|
|
194
|
+
if (!isCI) {
|
|
195
|
+
const daemonRunning = isDaemonRunning();
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
if (daemonRunning) {
|
|
198
|
+
// Daemon is running but PG is unhealthy — self-heal and wait for recovery
|
|
199
|
+
selfHealPostgres(DATA_DIR);
|
|
200
|
+
const recovered = await waitForPortFile(DAEMON_BOOT_TIMEOUT_MS);
|
|
201
|
+
if (recovered !== null) return recovered;
|
|
202
|
+
}
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
// 4. No daemon running — auto-start daemon in background
|
|
205
|
+
if (!daemonRunning) {
|
|
206
|
+
try {
|
|
207
|
+
execSync('genie daemon start', { stdio: 'ignore', timeout: 5000 });
|
|
208
|
+
} catch {
|
|
209
|
+
// Daemon start may detach and return non-zero; that's OK
|
|
210
|
+
}
|
|
211
|
+
const booted = await waitForPortFile(DAEMON_BOOT_TIMEOUT_MS);
|
|
212
|
+
if (booted !== null) return booted;
|
|
213
|
+
}
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
//
|
|
216
|
+
// 5. Last resort: start pgserve directly in this process (backwards compat)
|
|
204
217
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
selfHealPostgres(DATA_DIR);
|
|
207
219
|
try {
|
|
208
220
|
const startedPort = await startPgserveOnPort(port);
|
|
209
221
|
registerExitHandler();
|
|
210
222
|
return startedPort;
|
|
211
223
|
} catch (err) {
|
|
212
|
-
|
|
224
|
+
process.env.GENIE_PG_AVAILABLE = 'false';
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
throw new Error(`pgserve failed to start: ${maskCredentials(message)}`);
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return
|
|
230
|
+
/** Check if the genie daemon is running via PID file. */
|
|
231
|
+
function isDaemonRunning(): boolean {
|
|
232
|
+
try {
|
|
233
|
+
const pid = Number.parseInt(readFileSync(DAEMON_PID_PATH, 'utf-8').trim(), 10);
|
|
234
|
+
if (Number.isNaN(pid) || pid <= 0) return false;
|
|
235
|
+
process.kill(pid, 0); // Throws if process doesn't exist
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
223
239
|
}
|
|
224
|
-
// Stale lockfile — port not listening
|
|
225
|
-
removeLockfile();
|
|
226
|
-
return null;
|
|
227
240
|
}
|
|
228
241
|
|
|
229
|
-
/**
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
async function tryFallbackPorts(basePort: number, originalErr: unknown): Promise<number> {
|
|
239
|
-
for (let offset = 1; offset <= MAX_PORT_RETRIES; offset++) {
|
|
240
|
-
const fallbackPort = basePort + offset;
|
|
241
|
-
if (await isPortListening(fallbackPort, DEFAULT_HOST)) {
|
|
242
|
-
return markPortActive(fallbackPort, true);
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
const startedPort = await startPgserveOnPort(fallbackPort);
|
|
246
|
-
registerExitHandler();
|
|
247
|
-
return startedPort;
|
|
248
|
-
} catch {
|
|
249
|
-
// Try next port
|
|
242
|
+
/** Wait for port file to appear with a healthy PG behind it. */
|
|
243
|
+
async function waitForPortFile(timeoutMs: number): Promise<number | null> {
|
|
244
|
+
const deadline = Date.now() + timeoutMs;
|
|
245
|
+
while (Date.now() < deadline) {
|
|
246
|
+
const port = readLockfile();
|
|
247
|
+
if (port !== null && (await isPostgresHealthy(port))) {
|
|
248
|
+
activePort = port;
|
|
249
|
+
process.env.GENIE_PG_AVAILABLE = 'true';
|
|
250
|
+
return port;
|
|
250
251
|
}
|
|
252
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
251
253
|
}
|
|
252
|
-
|
|
253
|
-
process.env.GENIE_PG_AVAILABLE = 'false';
|
|
254
|
-
const message = originalErr instanceof Error ? originalErr.message : String(originalErr);
|
|
255
|
-
console.warn(`Warning: pgserve failed to start: ${maskCredentials(message)}`);
|
|
256
|
-
throw new Error(
|
|
257
|
-
`pgserve failed to start on port ${basePort} (and fallbacks ${basePort + 1}-${basePort + MAX_PORT_RETRIES}): ${maskCredentials(message)}`,
|
|
258
|
-
);
|
|
254
|
+
return null;
|
|
259
255
|
}
|
|
260
256
|
|
|
261
257
|
/** Start pgserve on a specific port, update singleton state and lockfile. */
|
|
@@ -310,7 +306,22 @@ function registerExitHandler(): void {
|
|
|
310
306
|
* This isolates test data from production tables.
|
|
311
307
|
*/
|
|
312
308
|
export async function getConnection() {
|
|
313
|
-
|
|
309
|
+
// If we have a cached client, health-check it before returning
|
|
310
|
+
if (sqlClient) {
|
|
311
|
+
try {
|
|
312
|
+
await sqlClient`SELECT 1`;
|
|
313
|
+
return sqlClient;
|
|
314
|
+
} catch {
|
|
315
|
+
// Connection is broken — reset and retry once
|
|
316
|
+
try {
|
|
317
|
+
await sqlClient.end({ timeout: 2 });
|
|
318
|
+
} catch {
|
|
319
|
+
/* ignore */
|
|
320
|
+
}
|
|
321
|
+
sqlClient = null;
|
|
322
|
+
activePort = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
314
325
|
|
|
315
326
|
const port = await ensurePgserve();
|
|
316
327
|
const postgres = (await import('postgres')).default;
|
|
@@ -332,17 +343,35 @@ export async function getConnection() {
|
|
|
332
343
|
},
|
|
333
344
|
});
|
|
334
345
|
|
|
335
|
-
|
|
336
|
-
|
|
346
|
+
try {
|
|
347
|
+
// Always call runMigrations — it's idempotent (checks _genie_migrations table)
|
|
348
|
+
await runMigrations(sqlClient);
|
|
337
349
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
350
|
+
// Run idempotent JSON → PG seed if source files exist
|
|
351
|
+
if (!testSchema && needsSeed()) {
|
|
352
|
+
await runSeed(sqlClient);
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
// Migration/seed failure — reset client so next call retries
|
|
356
|
+
try {
|
|
357
|
+
await sqlClient.end({ timeout: 2 });
|
|
358
|
+
} catch {
|
|
359
|
+
/* ignore */
|
|
360
|
+
}
|
|
361
|
+
sqlClient = null;
|
|
362
|
+
throw err;
|
|
341
363
|
}
|
|
342
364
|
|
|
343
365
|
return sqlClient;
|
|
344
366
|
}
|
|
345
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Check if DB is already connected (for guard checks without triggering startup).
|
|
370
|
+
*/
|
|
371
|
+
export function isConnected(): boolean {
|
|
372
|
+
return sqlClient !== null;
|
|
373
|
+
}
|
|
374
|
+
|
|
346
375
|
/**
|
|
347
376
|
* Reset the connection singleton. Next call to getConnection() creates a fresh client.
|
|
348
377
|
* Used by test helpers to switch schemas between test runs.
|
|
@@ -1228,6 +1228,16 @@ export function startDaemon(
|
|
|
1228
1228
|
// Stop session capture layers
|
|
1229
1229
|
import('./session-filewatch.js').then((m) => m.stopFilewatch()).catch(() => {});
|
|
1230
1230
|
import('./session-backfill.js').then((m) => m.stopBackfill()).catch(() => {});
|
|
1231
|
+
// Remove port file — daemon no longer owns PG
|
|
1232
|
+
import('./db.js')
|
|
1233
|
+
.then(({ getLockfilePath }) => {
|
|
1234
|
+
try {
|
|
1235
|
+
require('node:fs').unlinkSync(getLockfilePath());
|
|
1236
|
+
} catch {
|
|
1237
|
+
/* already gone */
|
|
1238
|
+
}
|
|
1239
|
+
})
|
|
1240
|
+
.catch(() => {});
|
|
1231
1241
|
};
|
|
1232
1242
|
|
|
1233
1243
|
const processTriggers = async () => {
|
|
@@ -25,14 +25,17 @@ import {
|
|
|
25
25
|
// Helpers
|
|
26
26
|
// ============================================================================
|
|
27
27
|
|
|
28
|
+
const DB_AVAILABLE = process.env.GENIE_PG_AVAILABLE === 'true' || !process.env.CI;
|
|
29
|
+
|
|
28
30
|
let cleanup: () => Promise<void>;
|
|
29
31
|
|
|
30
32
|
beforeAll(async () => {
|
|
33
|
+
if (!DB_AVAILABLE) return;
|
|
31
34
|
cleanup = await setupTestSchema();
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
afterAll(async () => {
|
|
35
|
-
await cleanup();
|
|
38
|
+
if (cleanup) await cleanup();
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
function makeAgent(id: string, team?: string, repoPath?: string): Agent {
|
|
@@ -10,14 +10,17 @@ import { setupTestSchema } from '../lib/test-db.js';
|
|
|
10
10
|
import * as wishState from '../lib/wish-state.js';
|
|
11
11
|
import { detectWaveCompletion, ensureWorkPushed, parseRef, resolveWishPath } from './state.js';
|
|
12
12
|
|
|
13
|
+
const DB_AVAILABLE = process.env.GENIE_PG_AVAILABLE === 'true' || !process.env.CI;
|
|
14
|
+
|
|
13
15
|
let cleanupSchema: () => Promise<void>;
|
|
14
16
|
|
|
15
17
|
beforeAll(async () => {
|
|
18
|
+
if (!DB_AVAILABLE) return;
|
|
16
19
|
cleanupSchema = await setupTestSchema();
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
afterAll(async () => {
|
|
20
|
-
await cleanupSchema();
|
|
23
|
+
if (cleanupSchema) await cleanupSchema();
|
|
21
24
|
});
|
|
22
25
|
|
|
23
26
|
// ============================================================================
|