alchemylab 1.4.7 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -227,21 +227,29 @@ for(const t of e){if(this.inFlight.has(t.id))continue
227
227
  const e=t.metadata||{}
228
228
  if((e.status||o.QUEST_STATUS_OPEN)!==o.QUEST_STATUS_OPEN)continue
229
229
  const r=e.tickIntervalSeconds||o.QUEST_DEFAULTS.tickIntervalSeconds,s=e.lastTickAt?Date.parse(e.lastTickAt):NaN,n=Number.isFinite(s)?s:0,i=Date.now()
230
- n>0&&r>(i-n)/1e3||(this.inFlight.add(t.id),this.fireTick(t).finally((()=>this.inFlight.delete(t.id))))}}}async fireTick(e){const t=e.metadata||{},n=t.maxInFlight??o.QUEST_DEFAULTS.maxInFlight,i=t.maxPerDay??o.QUEST_DEFAULTS.maxPerDay,a=e.localId||e.id
231
- let c
232
- try{c=await this.fetchQuestRequests(e.id)}catch(e){return void this.logger.warn("Quest tick: fetch requests failed; skipping",{questId:a,error:e+""})}const l=new Set
233
- try{const e=await this.apiClient.get(`/api/projects/${this.projectId}/tasks?status=in_progress,pending&limit=1000`),t=e?.data||e||[],r=new Set(c.map((e=>e.id)))
234
- for(const e of t)e.requestId&&r.has(e.requestId)&&l.add(e.requestId)}catch(e){this.logger.warn("Quest tick: task fetch failed; counting by status only",{questId:a,error:e+""})}const{openCount:d,statusCount:u,taskOnlyCount:h}=r(c,l)
235
- if(d>=n)return void this.logger.info("Quest tick: maxInFlight reached, skipping",{questId:a,openCount:d,maxInFlight:n,statusCount:u,taskOnlyCount:h})
236
- const g=(new Date).toISOString().slice(0,10),p=t.ticksTodayDate===g?t.ticksTodayCount??0:0
237
- if(p>=i)return void this.logger.info("Quest tick: maxPerDay reached, skipping",{questId:a,ticksToday:p,maxPerDay:i})
238
- const m=await this.findAlchemistSessionId()
239
- if(!m)return void this.logger.info("Quest tick: no persistent alchemist session in project, skipping",{questId:a,projectId:this.projectId})
240
- const f=(t.currentIteration||0)+1
241
- let y=!1
242
- try{y=await this.bumpQuestMetadata(e,f)}catch(e){return void this.logger.warn("Quest tick: metadata PATCH failed; skipping prompt",{questId:a,error:e+""})}if(!y)return void this.logger.info("Quest tick: status changed during tick; skipping prompt",{questId:a})
243
- const v=this.buildTickPrompt(e,f,d,h,p+1),w={logger:this.logger,projectId:this.projectId,projectPath:this.projectPath,apiClient:this.apiClient,cachedDefaultModel:this.getDefaultModel()??null},I={alchemy:{kind:"quest-tick",payload:{questId:a,iteration:f,criteria:t.criteria||"",openCount:d,taskOnlyCount:h,maxInFlight:n,ticksToday:p+1,maxPerDay:i,minIterations:t.minIterations??o.QUEST_DEFAULTS.minIterations}}}
244
- try{await(0,s.executeSameSession)(v,m,w,I),this.logger.info("Quest tick fired",{questId:a,iteration:f,alchemistSessionId:m})}catch(e){this.logger.warn("Quest tick: prompt send failed",{questId:a,error:e+""})}}invalidateCache(){this.lastQuestsFetch=0}invalidateAlchemistCache(){this.cachedAlchemistSessionId=null,this.cachedAlchemistAt=0}async fetchOpenQuests(){const e=Date.now()
230
+ n>0&&r>(i-n)/1e3||(this.inFlight.add(t.id),this.fireTick(t).finally((()=>this.inFlight.delete(t.id))))}}}async nudge(e){if(!this.projectId)return
231
+ if(this.inFlight.has(e))return void this.logger.info("Quest nudge: already in flight, skipping",{questId:e})
232
+ let t
233
+ try{const r=await this.apiClient.get(`/api/projects/${this.projectId}/documents/${encodeURIComponent(e)}`),s=r?.data||r
234
+ if(!s||"Quest"!==s.type)return void this.logger.warn("Quest nudge: doc not found or wrong type",{questId:e,type:s?.type})
235
+ t=s}catch(t){return void this.logger.warn("Quest nudge: fetch failed",{questId:e,error:t+""})}const r=t.metadata||{}
236
+ if((r.status||o.QUEST_STATUS_OPEN)===o.QUEST_STATUS_OPEN){this.inFlight.add(t.id)
237
+ try{await this.fireTick(t,{idleOnly:!0})}finally{this.inFlight.delete(t.id)}}else this.logger.info("Quest nudge: not open, skipping",{questId:e,status:r.status})}async fireTick(e,t){const n=!0===t?.idleOnly,i=e.metadata||{},a=i.maxInFlight??o.QUEST_DEFAULTS.maxInFlight,c=i.maxPerDay??o.QUEST_DEFAULTS.maxPerDay,l=e.localId||e.id
238
+ let d
239
+ try{d=await this.fetchQuestRequests(e.id)}catch(e){return void this.logger.warn("Quest tick: fetch requests failed; skipping",{questId:l,error:e+""})}const u=new Set
240
+ try{const e=await this.apiClient.get(`/api/projects/${this.projectId}/tasks?status=in_progress,pending&limit=1000`),t=e?.data||e||[],r=new Set(d.map((e=>e.id)))
241
+ for(const e of t)e.requestId&&r.has(e.requestId)&&u.add(e.requestId)}catch(e){this.logger.warn("Quest tick: task fetch failed; counting by status only",{questId:l,error:e+""})}const{openCount:h,statusCount:g,taskOnlyCount:p}=r(d,u)
242
+ if(h>=a)return void this.logger.info("Quest tick: maxInFlight reached, skipping",{questId:l,openCount:h,maxInFlight:a,statusCount:g,taskOnlyCount:p})
243
+ if(n&&h>0)return void this.logger.info("Quest nudge: not idle, skipping",{questId:l,openCount:h})
244
+ const m=(new Date).toISOString().slice(0,10),f=i.ticksTodayDate===m?i.ticksTodayCount??0:0
245
+ if(f>=c)return void this.logger.info("Quest tick: maxPerDay reached, skipping",{questId:l,ticksToday:f,maxPerDay:c})
246
+ const y=await this.findAlchemistSessionId()
247
+ if(!y)return void this.logger.info("Quest tick: no persistent alchemist session in project, skipping",{questId:l,projectId:this.projectId})
248
+ const v=(i.currentIteration||0)+1
249
+ let w=!1
250
+ try{w=await this.bumpQuestMetadata(e,v)}catch(e){return void this.logger.warn("Quest tick: metadata PATCH failed; skipping prompt",{questId:l,error:e+""})}if(!w)return void this.logger.info("Quest tick: status changed during tick; skipping prompt",{questId:l})
251
+ const I=this.buildTickPrompt(e,v,h,p,f+1),b={logger:this.logger,projectId:this.projectId,projectPath:this.projectPath,apiClient:this.apiClient,cachedDefaultModel:this.getDefaultModel()??null},E={alchemy:{kind:"quest-tick",payload:{questId:l,iteration:v,criteria:i.criteria||[],openCount:h,taskOnlyCount:p,maxInFlight:a,ticksToday:f+1,maxPerDay:c,minIterations:i.minIterations??(0,o.defaultMinIterations)(i.completionMode),completionMode:i.completionMode||o.QUEST_COMPLETION_MODE_DEFAULT,consecutiveDoneAudits:i.consecutiveDoneAudits??0}}}
252
+ try{await(0,s.executeSameSession)(I,y,b,E),this.logger.info("Quest tick fired",{questId:l,iteration:v,alchemistSessionId:y})}catch(e){this.logger.warn("Quest tick: prompt send failed",{questId:l,error:e+""})}}invalidateCache(){this.lastQuestsFetch=0}invalidateAlchemistCache(){this.cachedAlchemistSessionId=null,this.cachedAlchemistAt=0}async fetchOpenQuests(){const e=Date.now()
245
253
  if(18e5>e-this.lastQuestsFetch)return this.cachedQuests
246
254
  try{const t=await this.apiClient.get(`/api/projects/${this.projectId}/plans?type=Quest&limit=100`),r=t?.data||t||[]
247
255
  return this.cachedQuests=r.filter((e=>e&&(e.metadata?.status||o.QUEST_STATUS_OPEN)===o.QUEST_STATUS_OPEN)),this.lastQuestsFetch=e,this.cachedQuests}catch(t){throw this.lastQuestsFetch=e,t}}async fetchQuestRequests(e){const t=await this.apiClient.get(`/api/projects/${this.projectId}/requests?questDocumentId=${encodeURIComponent(e)}&limit=1000`)
@@ -254,8 +262,8 @@ this.logger.info("Quest tick: project active alchemist not active, ignoring",{pr
254
262
  return 0===r.length?(this.logger.info("Quest tick: no alchemist session found in project",{projectId:this.projectId}),e(null)):(this.logger.info("Quest tick: active-alchemist not set, using first alchemist as fallback",{projectId:this.projectId,alchemistCount:r.length}),e(r[0].externalSessionId))}catch(e){return this.logger.warn("Quest tick: alchemist fallback lookup failed",{error:e+""}),null}}async bumpQuestMetadata(e,t){const r=await this.apiClient.get(`/api/projects/${this.projectId}/documents/${encodeURIComponent(e.id)}`),s=r?.data||r,n=s?.metadata||{}
255
263
  if((n.status||o.QUEST_STATUS_OPEN)!==o.QUEST_STATUS_OPEN)return!1
256
264
  const i=(new Date).toISOString().slice(0,10),a=(n.ticksTodayDate===i?n.ticksTodayCount??0:0)+1,c={...n,lastTickAt:(new Date).toISOString(),currentIteration:t,ticksTodayDate:i,ticksTodayCount:a}
257
- return await this.apiClient.patch(`/api/projects/${this.projectId}/documents/${encodeURIComponent(e.id)}`,{metadata:c}),!0}buildTickPrompt(e,t,r,s,n){const i=e.metadata||{},a=e.localId||e.id,c=i.minIterations??o.QUEST_DEFAULTS.minIterations
258
- return[`[Quest tick: ${a}, iteration ${t}]`,"","Filename: "+(e.filename||"(unknown)"),"Criteria: "+(i.criteria||"(none specified)"),"",`Caps: maxInFlight=${i.maxInFlight??o.QUEST_DEFAULTS.maxInFlight}, maxPerDay=${i.maxPerDay??o.QUEST_DEFAULTS.maxPerDay}, minIterations=${c}.`,`Currently in flight under this quest: ${r}${s>0?` (incl. ${s} via in-progress/pending tasks under non-active requests)`:""}. Ticks today (incl. this): ${n}/${i.maxPerDay??o.QUEST_DEFAULTS.maxPerDay}.`,'\nDecide what to take action on this tick. If progress requires a request, call specialist_ops with operation="delegate". Inside the prompt you write for the specialist, you MUST include this exact line so the spawned request is tagged to the quest:',` "When you call create_request, pass questDocumentId="${a}"."`,"Without that instruction in the specialist's prompt, the spawned request will not be tagged and the runner can't track it.\nDo NOT mark this quest complete \u2014 quest end is a user action, not yours. Just do this tick's work and stop."].join("\n")}}},1405(e,exports,t){"use strict"
265
+ return await this.apiClient.patch(`/api/projects/${this.projectId}/documents/${encodeURIComponent(e.id)}`,{metadata:c}),!0}buildTickPrompt(e,t,r,s,n){const i=e.metadata||{},a=e.localId||e.id,c=i.minIterations??(0,o.defaultMinIterations)(i.completionMode),l=i.maxInFlight??o.QUEST_DEFAULTS.maxInFlight,d=i.maxPerDay??o.QUEST_DEFAULTS.maxPerDay,u=i.completionMode||o.QUEST_COMPLETION_MODE_DEFAULT,h=i.consecutiveDoneAudits??0,g="auto-close"===u?["A) If every requirement is satisfied with concrete evidence:",` Call quest_audit_complete(questId="${a}", summary, evidence?). The server bumps your audit streak. When the streak reaches ${c}, the quest auto-closes (the user opted into auto-close mode). Do NOT spawn work in this branch, that resets the streak to 0.`]:["A) If every requirement is satisfied with concrete evidence:",` Call quest_audit_complete(questId="${a}", summary, evidence?). The server bumps your audit streak. When the streak reaches ${c}, status flips to pending_review and the user adjudicates (close or reopen). Do NOT spawn work in this branch, that resets the streak to 0.`]
266
+ return[`[Quest tick: ${a}, iteration ${t}]`,"","Filename: "+(e.filename||"(unknown)"),"","## Quest body (prose context written by the planner)","",e.content&&e.content.trim().length>0?e.content.trim():"(empty body \u2014 no Goal / Criteria / Notes have been written yet)","",...i.criteria&&Array.isArray(i.criteria)&&i.criteria.length>0?[`## Structured criteria checklist (${i.criteria.filter((e=>"verified"===e.status)).length}/${i.criteria.length} verified)`,"",...i.criteria.map((e=>`- [${"verified"===e.status?"x":" "}] ${e.id}: ${e.description}${e.evidence?" \u2014 evidence: "+e.evidence:""}`)),"","Per-item statuses are authoritative when this list is present. The audit step below should reason per-criterion rather than against prose alone.",""]:[],"completionMode: "+u,`Audit streak: ${h}/${c} consecutive criteria-met audits`,`Caps: maxInFlight=${l} (room for ${Math.max(0,l-r)} more this tick), maxPerDay=${d}.`,`Currently in flight under this quest: ${r}${s>0?` (incl. ${s} via in-progress/pending tasks under non-active requests)`:""}. Ticks today (incl. this): ${n}/${d}.`,"",...1===t?["## First tick \u2014 kickoff","","This is the first tick of this quest. No work has been spawned yet and no prior state to audit. Skip the usual audit-then-act flow; instead:","1. Read the quest body above carefully. Derive concrete requirements from the Goal / Criteria for done / Notes (and the structured checklist if one is set up).",`2. Plan the initial work. You have room for ${l} parallel request(s) this tick. Identify which requirements can be tackled now and how they split into independent specialist requests.`,"3. Spawn one or more specialists via specialist_ops delegate \u2014 one per independent piece, up to the room above. If the work is one cohesive task, spawn a single request and let it finish before the next tick fans out.","4. Every specialist_ops delegate call MUST include this exact line in the prompt you write for the specialist so the spawned request is tagged to the quest:",` "When you call create_request, pass questDocumentId="${a}"."`," Without that the request will not be tagged and the runner cannot track it.","","If the quest body is empty or unclear, do NOT spawn work blind \u2014 ask the user a brief clarifying question via session_message_operations send instead. The quest-planner should have shaped the body during planning; if it didn't, something went wrong upstream.","","Do NOT call quest_audit_complete on the first tick. From the next tick onward you'll get the full audit-then-act prompt as the quest's state evolves.",""]:["## Audit step (do this first, every tick)","","Before deciding what to do, audit the current state against the criteria above. Criteria can come from two places: the quest body (prose Goal / Criteria for done / Notes the planner wrote) and, optionally, the structured checklist (per-item statuses). When the structured checklist is present, treat per-item statuses as the source of truth and use the body as context. When only the body is present, derive concrete requirements from the prose.","- Derive concrete requirements from the body (and per-item checklist if present). Number them if useful.","- For each requirement, identify the authoritative evidence that would prove it: files, command output, test results, merged requests, runtime behavior.","- Inspect the current evidence in the worktree right now. Use read_files, search, query_history, request_diff, get_request_information as needed. Do not rely on memory of earlier ticks.","- For each requirement, judge: proved, contradicted, incomplete, too-weak-to-verify, or missing.","- Treat indirect, weak, or merely-consistent-with-completion evidence as not achieved.","- Match the verification scope to the requirement's scope; do not use a narrow check to support a broad claim.","","## Action this tick (pick exactly one based on what the audit shows)","",...g,"","B) If the audit shows remaining work:",` You have room for ${Math.max(0,l-r)} more request(s) this tick (maxInFlight=${l}, currently in flight=${r}). Use the full fan-out width when the remaining work splits cleanly.`," - If the remaining work breaks into INDEPENDENT pieces (different files, no merge conflicts between them, no ordering dependency), make multiple specialist_ops delegate calls THIS tick \u2014 one per piece, up to the room above. Each goes to its own specialist; do not pack multiple pieces into one specialist prompt."," - If the remaining work is one cohesive task, or the pieces depend on each other, spawn a single request and let it finish before the next tick fans out."," Every specialist_ops delegate call you make this tick MUST include this exact line in the prompt you write for the specialist so the spawned request is tagged to the quest:",` "When you call create_request, pass questDocumentId="${a}"."`," Without that the request will not be tagged and the runner cannot track it. Spawning any request resets the audit streak to 0 (criteria are not actually met yet).",""],"## Fidelity","","- Optimize this tick for movement toward the actual end state described in the criteria, not the safest-looking next step.","- Do not narrow the criteria to fit what is already done.","- An edit, request, or audit is aligned only if it makes the criteria more true. Useful-looking work that preserves a different end state is misaligned.","- Only the user can flip status to 'closed' (except via auto-close mode, which is an explicit user opt-in on this quest). You may flip to 'pending_review' via quest_audit_complete when audit-then-pending mode is configured."].join("\n")}}},1405(e,exports,t){"use strict"
259
267
  Object.defineProperty(exports,"__esModule",{value:!0}),exports.handleStartBuildingAction=async function(e,t){const{plannerSessionId:n,parentSessionId:i,requestIds:a,summary:c}=e,{logger:l,apiClient:d,projectId:u,projectPath:h}=t
260
268
  if(!n)return{success:!1,message:"plannerSessionId is required",error:"missing_params"}
261
269
  l.info("Starting start-building action",{plannerSessionId:n,requestIds:a,payloadParent:i})
@@ -298,7 +306,7 @@ return{...e,_meta:{...s,alchemy:{...o,...r,kind:t}}}},exports.readAlchemyMeta=fu
298
306
  var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
299
307
  Object.defineProperty(exports,"__esModule",{value:!0}),exports.AlchemyActionServer=void 0
300
308
  const s=r(t(7252)),o=r(t(8577)),n=t(9896),i=t(5959),a=t(2640),c=t(4192),l=t(1152),d=r(t(6928)),u=r(t(9896)),h=r(t(857)),g=t(5607),p=t(8984),m=t(8524),f=t(8824),y=t(2096),v=t(7210),w=t(7227),I=t(5419),b=t(3962),E=t(2928),S=t(6563),C=t(1401),k=t(1292),T=t(8649),_=t(4306),j=t(9068),A=t(3435),P=t(8463),M=t(489),O=t(3807),R=t(7478),D=t(5317),x=t(355),$=t(833)
301
- class F{updateAuthToken(e){this.options.authToken=e,this.apiClient.authToken=e,this.configurer.authToken=e}constructor(e,t){this.directReportDedup=new Map,this.activeMcpCalls=new Map,this.pendingAgentResync=!1,this.settingsSyncCounter=0,this.versionWarnings=[],this.updateBlocked=!1,this._pendingUpdateBlock=!1,this.isPolling=!1,this.isProcessingAction=!1,this.openCodeUrlCache=null,this.openCodePort=null,this.proxyAbortControllers=new Map,this.trackedChildPids=new Set,this.sseEventBuffer=[],this.droppedEventCount=0,this.firstDropAt=0,this.hasRemoteEventConsumers=!1,this.cachedProjectConfig=null,this.logger=e,this.options=t,this.port=t.port||3456,this.startTime=new Date
309
+ class q{updateAuthToken(e){this.options.authToken=e,this.apiClient.authToken=e,this.configurer.authToken=e}constructor(e,t){this.directReportDedup=new Map,this.activeMcpCalls=new Map,this.pendingAgentResync=!1,this.settingsSyncCounter=0,this.versionWarnings=[],this.updateBlocked=!1,this._pendingUpdateBlock=!1,this.isPolling=!1,this.isProcessingAction=!1,this.openCodeUrlCache=null,this.openCodePort=null,this.proxyAbortControllers=new Map,this.trackedChildPids=new Set,this.sseEventBuffer=[],this.droppedEventCount=0,this.firstDropAt=0,this.hasRemoteEventConsumers=!1,this.cachedProjectConfig=null,this.logger=e,this.options=t,this.port=t.port||3456,this.startTime=new Date
302
310
  const r=(0,l.createMetricsCollector)(!1)
303
311
  this.apiClient=new c.BaseApiClient(t.remoteServerUrl,e,r,t.authToken,{timeout:35e3}),this.configurer=new p.MCPClientConfigurer(e,t.remoteServerUrl,t.authToken),this.cliBridge=new m.CLIBridgeWebSocket(e),(0,m.setCLIBridge)(this.cliBridge),this.hookExecutor=new v.HookExecutor(e,"",t.projectPath,this.apiClient,{cachedDefaultModel:this.cachedDefaultModel,trackChildPid:e=>this.trackedChildPids.add(e)}),this.app=this.createApp()}createApp(){const e=(0,s.default)()
304
312
  return e.use(((e,t,r)=>{t.setHeader("Access-Control-Allow-Private-Network","true"),r()})),e.use((0,o.default)()),e.use(s.default.json({limit:"10mb"})),e.get("/health",this.handleHealth.bind(this)),e.get("/api/commands/running",(async(e,t)=>{try{const e=16384,r=await Promise.all((0,i.listEntries)().map((async t=>{let r,s="",o=0,i=t.finishedAt??t.startedAt
@@ -444,7 +452,7 @@ t.json({success:!0,data:{sessionId:i.sessionId,worktreePath:i.worktreePath,branc
444
452
  if(!r)return void t.status(400).json({success:!1,error:"Project not registered"})
445
453
  const{agentName:s}=e.params,o=await this.apiClient.get(`/api/projects/${r}/agents`),n=(o?.data||o||[]).find((e=>e.name===s))
446
454
  if(!n)return void t.status(404).json({success:!1,error:`Agent '${s}' not found`})
447
- t.json({success:!0,data:n})}catch(e){this.logger.debug("Failed to get agent by name",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}readProjectConfig(){if(this.cachedProjectConfig&&Date.now()-this.cachedProjectConfig.timestamp<F.PROJECT_CONFIG_TTL)return this.cachedProjectConfig.config
455
+ t.json({success:!0,data:n})}catch(e){this.logger.debug("Failed to get agent by name",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}readProjectConfig(){if(this.cachedProjectConfig&&Date.now()-this.cachedProjectConfig.timestamp<q.PROJECT_CONFIG_TTL)return this.cachedProjectConfig.config
448
456
  const e=d.default.join(this.options.projectPath,".alchemy_lab","project.json")
449
457
  try{if(!u.default.existsSync(e))return this.cachedProjectConfig={config:null,timestamp:Date.now()},null
450
458
  const t=u.default.readFileSync(e,"utf-8"),r=JSON.parse(t)
@@ -543,7 +551,9 @@ if(n.success&&n.data&&void 0!==n.data.hasRemoteEventConsumers&&(this.hasRemoteEv
543
551
  if(void 0!==e&&this.updateDefaultModel(e),void 0!==t&&t!==this.cachedAutoJournal){this.logger.info("autoJournal setting changed",{oldValue:this.cachedAutoJournal,newValue:t})
544
552
  const e=new y.ProjectConfigService(this.logger)
545
553
  await e.saveProjectConfig(this.options.projectPath,t),this.cachedAutoJournal=t}}if(n.success&&Array.isArray(n.data?.invalidations)&&n.data.invalidations.length>0){const e=n.data.invalidations
546
- this.logger.info("Received cache invalidations from remote",{invalidations:e}),e.includes("hooks")&&this.timerHookScheduler?.invalidateCache(),e.includes("quests")&&this.questTickScheduler?.invalidateCache(),e.includes("alchemist")&&this.questTickScheduler?.invalidateAlchemistCache()}if(n.success&&Array.isArray(n.data?.versionWarnings)&&this.applyVersionWarnings(n.data.versionWarnings),n.success&&n.data?.actions?.length>0){const e=[],t=[]
554
+ this.logger.info("Received cache invalidations from remote",{invalidations:e}),e.includes("hooks")&&this.timerHookScheduler?.invalidateCache(),e.includes("quests")&&this.questTickScheduler?.invalidateCache(),e.includes("alchemist")&&this.questTickScheduler?.invalidateAlchemistCache()}if(n.success&&Array.isArray(n.data?.questNudges)&&n.data.questNudges.length>0){const e=n.data.questNudges
555
+ this.logger.info("Received quest nudges from remote",{questNudges:e})
556
+ for(const t of e)this.questTickScheduler?.nudge(t).catch((e=>{this.logger.warn("Quest nudge dispatch failed",{questId:t,error:e+""})}))}if(n.success&&Array.isArray(n.data?.versionWarnings)&&this.applyVersionWarnings(n.data.versionWarnings),n.success&&n.data?.actions?.length>0){const e=[],t=[]
547
557
  for(const r of n.data.actions)"opencode-proxy"===r.type||"run-tests"===r.type?e.push(r):t.push(r)
548
558
  for(const t of e)this.processAction(t).catch((e=>this.logger.warn("Proxy action failed",{actionId:t.id,error:(0,f.getErrorMessage)(e)})))
549
559
  if(t.length>0&&!this.isProcessingAction){this.isProcessingAction=!0
@@ -602,7 +612,7 @@ for(const[e]of this.proxyAbortControllers)try{e.abort()}catch{}this.proxyAbortCo
602
612
  try{const e=d.default.join(this.options.projectPath,".alchemy_lab","action-server.json")
603
613
  u.default.existsSync(e)&&u.default.unlinkSync(e)}catch{}if(this.projectId)try{const{removeEntry:e}=await Promise.resolve().then(t.bind(t,2640))
604
614
  e(this.projectId)}catch{}if(this.isPolling=!1,this.pollAbortController&&this.pollAbortController.abort(),this.pollingInterval&&clearInterval(this.pollingInterval),this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.serverId)try{await this.apiClient.post(`/api/servers/${this.serverId}/disconnect`)}catch{}this.cliBridge.close(),await new Promise((e=>{if(this.server){const t=setTimeout((()=>{this.logger.warn("Graceful shutdown timed out after 10s, forcing close"),e()}),1e4)
605
- this.server.close((()=>{clearTimeout(t),e()}))}else e()})),this.logger.info("AlchemyActionServer stopped")}getPort(){return this.port}isRunning(){return!!this.server}}exports.AlchemyActionServer=F,F.PROJECT_CONFIG_TTL=3e4},2424(e,exports,t){"use strict"
615
+ this.server.close((()=>{clearTimeout(t),e()}))}else e()})),this.logger.info("AlchemyActionServer stopped")}getPort(){return this.port}isRunning(){return!!this.server}}exports.AlchemyActionServer=q,q.PROJECT_CONFIG_TTL=3e4},2424(e,exports,t){"use strict"
606
616
  Object.defineProperty(exports,"__esModule",{value:!0}),exports.ensureRequestWorktree=async function(e,t,i){const a="requests/"+t,c=(0,s.join)(e,n.ALCHEMY_DIR_NAME,"worktrees"),l=(0,s.join)(c,t)
607
617
  if((0,r.existsSync)(l))return void i.info("Worktree already exists on disk, skipping pre-spawn creation",{requestId:t,worktreePath:l})
608
618
  const d=(0,o.simpleGit)(e);(0,r.mkdirSync)(c,{recursive:!0})
@@ -707,7 +717,7 @@ break
707
717
  case"parent_session":case"ancestor_chain":await(0,o.executeInjectPrompt)(e,e.promptTemplate,i,a,this.ctx)}else await(0,o.executeBackground)(e,t,i,a.requestId,this.ctx)
708
718
  const c=(0,s.determineEventToEmit)(e,a)
709
719
  c&&await this.processCustomEvent(c,a,r+1)}}async notifyHookTrigger(e){try{await this.ctx.apiClient.post(`/api/projects/${this.ctx.projectId}/hook-triggers`,{hookId:e.id,hookName:e.name,eventType:e.eventType,action:e.action,target:e.target,agent:e.agent})}catch{}}}},3416(e,exports){"use strict"
710
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.KIND_META=exports.QUEST_STATUS_DRAFT=exports.QUEST_STATUS_CLOSED=exports.QUEST_STATUS_OPEN=exports.QUEST_DEFAULTS=exports.PLAN_KINDS=void 0,exports.PLAN_KINDS=["plan","quest"],exports.QUEST_DEFAULTS={tickIntervalSeconds:1800,maxInFlight:1,maxPerDay:24,minIterations:3},exports.QUEST_STATUS_OPEN="open",exports.QUEST_STATUS_CLOSED="closed",exports.QUEST_STATUS_DRAFT="draft",exports.KIND_META={plan:{docType:"Plan",filenamePrefix:"PLAN-",label:"plan",idLabel:"Plan ID",headingCreated:"Plan Created",errorVerb:"create plan",alchemyKind:"plan-create",canBeActivePlan:!0},quest:{docType:"Quest",filenamePrefix:"QUEST-",label:"quest",idLabel:"Quest ID",headingCreated:"Quest Created",errorVerb:"create quest",alchemyKind:"quest-create",canBeActivePlan:!1}}},3435(e,exports,t){"use strict"
720
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.QUEST_METADATA_KEYS=exports.QUEST_COMPLETION_MODE_DEFAULT=exports.QUEST_COMPLETION_MODES=exports.QUEST_STATUSES=exports.QUEST_STATUS_DRAFT=exports.QUEST_STATUS_CLOSED=exports.QUEST_STATUS_PENDING_REVIEW=exports.QUEST_STATUS_OPEN=exports.QUEST_DEFAULTS=void 0,exports.defaultMinIterations=function(e){return"auto-close"===e?3:1},exports.QUEST_DEFAULTS={tickIntervalSeconds:1800,maxInFlight:1,maxPerDay:24,minIterations:3,consecutiveDoneAudits:0},exports.QUEST_STATUS_OPEN="open",exports.QUEST_STATUS_PENDING_REVIEW="pending_review",exports.QUEST_STATUS_CLOSED="closed",exports.QUEST_STATUS_DRAFT="draft",exports.QUEST_STATUSES=[exports.QUEST_STATUS_DRAFT,exports.QUEST_STATUS_OPEN,exports.QUEST_STATUS_PENDING_REVIEW,exports.QUEST_STATUS_CLOSED],exports.QUEST_COMPLETION_MODES=["auto-close","audit-then-pending"],exports.QUEST_COMPLETION_MODE_DEFAULT="audit-then-pending",exports.QUEST_METADATA_KEYS=["criteria","tickIntervalSeconds","maxInFlight","maxPerDay","minIterations","status","completionMode"]},3435(e,exports,t){"use strict"
711
721
  Object.defineProperty(exports,"__esModule",{value:!0}),exports.validateRequest=function(e,t,r,s=[]){if(!r)return t.status(400).json({success:!1,error:"No project configured"}),!1
712
722
  const o=e.body??{},n=s.filter((e=>void 0===o[e]||null===o[e]||""===o[e]))
713
723
  if(n.length>0){const e=1===n.length?"is":"are"
@@ -1440,7 +1450,7 @@ e&&e.error&&"retryable"in e.error&&(this.details.retryable=!!e.error.retryable)}
1440
1450
  Object.defineProperty(exports,"__esModule",{value:!0}),exports.spawnPlannerWithDoc=async function(e){const{agent:t,prompt:o,title:n,parentSessionId:i,requestId:a,apiClient:c,logger:l,projectId:d,projectPath:u,trackChildPid:h}=e
1441
1451
  let g=e.planId
1442
1452
  if(!g){const r=`PLAN-${(e.planName||n).replace(/\.md$/i,"")}.md`
1443
- try{const e=await c.post(`/api/projects/${d}/documents`,{filename:r,content:" ",type:"Plan"})
1453
+ try{const e=await c.post(`/api/projects/${d}/documents`,{filename:r,content:" ",type:"Plan",metadata:{isStub:!0}})
1444
1454
  g=e?.data?.id,l.info("spawnPlannerWithDoc: pre-created plan document",{planId:g,filename:r,agent:t})}catch(e){l.warn("spawnPlannerWithDoc: failed to pre-create plan document; planner will fall back to plan_write create",{error:(0,s.getErrorMessage)(e),agent:t})}}const p="quest-planner"===t?"quest":"plan",m=g?`[Session Context]\n- ${p}Id: ${g} \u2014 this is YOUR associated ${p} document. It already exists with a placeholder filename.\n- Use plan_write (update / append / replace) with planId "${g}" to fill it in.${"quest-planner"===t?" For quests, plan_write update also accepts criteria / tickIntervalSeconds / maxInFlight / maxPerDay / minIterations / status.":""}${"quest-planner"===t?"":'\n- On your FIRST plan_write call, include a "name" field with a short descriptive title so the document gets a meaningful filename.'}\n- Do NOT call plan_write "create" \u2014 always modify the document above.\n- Do NOT call plan_read "get-active" \u2014 your associated ${p} is given here, not via the active-${p} slot.\n\n${o}`:o,f=await(0,r.spawnActionAgent)({agent:t,prompt:m,cwd:u,projectPath:u,projectId:d,title:n,requestId:a,parentSessionId:i,apiClient:c,logger:l,trackChildPid:h})
1445
1455
  if(!f.success||!f.sessionId)return{success:!1,error:f.error||"spawnActionAgent returned no sessionId"}
1446
1456
  l.info("spawnPlannerWithDoc: spawn succeeded",{agent:t,sessionId:f.sessionId,pid:f.pid,planId:g})