Port dream passes and built-in triggers without chapters

This commit is contained in:
2026-11-07 15:21:36 +00:00
parent f90637a765
commit 6ebf4211c5
8 changed files with 32185 additions and 1 deletions
+54
View File
@@ -0,0 +1,54 @@
# Dream
A dream is a read-only review in an ordinary branch. It does not change code, memory, settings, or storage automatically.
The user decides which proposals to accept. Approved proposals become ordinary work.
## For @ultimate
A user request or a built-in automation can request a dream.
If a dream branch is active, report that fact. Do not start another branch.
Otherwise, write a handoff while the current context still contains the relevant evidence.
Include open work, decisions, user corrections, memory slugs, and the next steps.
Use `branch` with an explicit repository path and that handoff in its prompt.
Ask the branch to follow this document.
Review the branch report with the user. Apply approved memory changes through the memory workflow.
Start ordinary branches for approved code or instruction changes.
Use `dream` with `action: prepare`, exactly three `learned` lines, and a fresh `handoff` when the review finishes.
Each learned line has at most 200 characters. The handoff has at most 12,000 characters.
A later preparation replaces the previous prepared handoff.
The preparation adds a passive `dream.prepared` entry. It does not switch storage or restart the host.
There are no chapters, seeds, or Fall asleep actions.
## For the dream branch
Coordinate these six read-only passes with background `dream-pass` subagents:
- **memory:** Propose evidence-based additions, updates, or deletions. Preserve the memory about the user.
- **failures:** Group repeated failures by cause. Propose small corrections.
- **tools:** Examine supplied tool results and repeated manual steps. Do not invent trace statistics.
- **corrections:** Identify repeated user corrections and requests.
- **codify:** Identify lessons that belong in code, agent instructions, or a reusable procedure.
- **project:** Identify repeated work that a project-specific program can simplify.
Give every pass the handoff and its relevant evidence. The handoff is the primary input, not proof by itself.
P3 supplies no general history-query or trace database interface. Name missing evidence rather than invent a query.
Each pass cites conversation IDs, commits, or explicit file paths and line numbers.
Check each report against its evidence. Return three learned lines, a journal, and one proposal per slug.
Each proposal names its pass, title, size, risk, evidence, and any memory it replaces.
Return the final answer normally. The messages service reports the branch answer to its starter.
Do not edit files, apply proposals, or switch storage during a dream.
## Built-in automations
Dream creates two ordinary context automations after @ultimate exists:
- `Dream before compaction`: the context exceeds 70%.
- `Dream after a pause`: the context exceeds 50% after eight hours without user input.
The automations wake @ultimate. Each fire also adds one passive `dream.requested` entry.
The automation ID and fire number identify that entry across replay and reopen.
Creation keys recover a lost receipt without a duplicate automation.
A user can pause or delete an automation. Reopen preserves those choices.
+40
View File
@@ -0,0 +1,40 @@
/** Two ordinary context automations, with durable creation and deletion receipts. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import { defineDoc, ROOT_CONVERSATION_ID as ROOT } from '@earendil-works/pi-durable'
import { Automations, AutomationsApi } from '../automations/server.ts'
import { Standing } from '../subagents/api.ts'
import { deliver } from './delivery.ts'
export const AUTOMATION_NAME='Dream before compaction'
export const PAUSE_NAME='Dream after a pause'
export const BuiltInDoc=defineDoc<{id?:string;deleted?:true}>({kind:'dream.after-compaction',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
export const PauseDoc=defineDoc<{id?:string;deleted?:true}>({kind:'dream.after-pause',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
const builtIns=[
{doc:BuiltInDoc,name:AUTOMATION_NAME,condition:{metric:'context',of:'@ultimate',above:70}},
{doc:PauseDoc,name:PAUSE_NAME,condition:{metric:'context',of:'@ultimate',above:50,idleFor:8*3600_000}},
]
export const DREAM_ASK='Dream now: follow "For @ultimate" in /src/sausage/extensions/dream/DREAM.md. Write your handoff into the dream branch prompt in this turn, while your context still holds everything. If a dream branch is already active, say so and do not start another.'
export async function ensureAutomation(host:any) {
const service=host.want(AutomationsApi)
if(!service || (await host.harness().snapshot(Standing,ROOT,context))?.ultimate===undefined) return
for(const {doc,name,condition} of builtIns) {
const state=await host.harness().snapshot(doc,ROOT,context)
if(state?.deleted) continue
if(state?.id) {
const list=await service.operate({action:'list'})
if(!list.some((a:any)=>a.id===state.id)) await host.harness().commit(async(tx:any)=>{(await tx.doc(doc,ROOT)).deleted=true},context)
} else {
const made=await service.operate({action:'create',name,condition,subscribers:['@ultimate'],key:`dream:${doc.definition.kind}`})
await host.harness().commit(async(tx:any)=>{(await tx.doc(doc,ROOT)).id=made.id},context)
}
}
}
export async function fired(host:any,tx:any,id:string,fire:number) {
const builtIn=await Promise.all(builtIns.map(async(item)=>({item,id:(await tx.doc(item.doc,ROOT)).id})))
const found=builtIn.find(item=>item.id===id)?.item
const ultimate=(await tx.doc(Standing,ROOT)).ultimate
if(!found || ultimate===undefined) return
const automation=(await tx.doc(Automations)).automations[id]
const pause=found.doc===PauseDoc && automation ? Date.now()-(automation.condition?.inputAt??automation.createdAt) : undefined
const data={automation:{id,name:automation?.name??found.name,...(pause===undefined?{}:{pause})}}
await deliver(host,tx,ultimate,{type:'write',requestId:`dream.requested:${id}:${fire}`,kind:'dream.requested',data,content:`Automation ${id} "${data.automation.name}" fired${pause===undefined?'':` after ${Math.round(pause/3600_000)} h away`}. ${DREAM_ASK}`})
}
+16
View File
@@ -0,0 +1,16 @@
/** Durable dream inputs and passive notes, admitted once per request ID. */
import { defineTask } from '@earendil-works/pi-durable'
const terminal = {status:'terminal',outcome:{status:'completed',result:null}} as const
export const deliverTask = (host:any) => defineTask<any,{phase:'submit'},null>({
name:'dream.deliver', version:1, initial:()=>({phase:'submit'}),
phases:{submit:async(task,runtime,ctx)=>{
const {to,type,requestId,kind,data,content}=task.input
const target=(await host.harness().conversation(to,ctx))!
await target.submit(type==='input'
? {type,requestId,content,entry:{kind,data},whenBusy:'followUp'}
: {type,requestId,entry:{kind,data,model:[{role:'user',content,timestamp:Date.now()}]}},ctx)
await runtime.commit(()=>terminal,ctx)
}},
abort:(_task,runtime,ctx)=>runtime.commit(()=>({status:'terminal',outcome:{status:'aborted'}}),ctx),
})
export const deliver = (host:any,tx:any,to:number,delivery:any) => tx.createTask(deliverTask(host),{to,...delivery},{conversationId:to,ownership:{kind:'conversation'},background:true})
+58
View File
@@ -0,0 +1,58 @@
/** Dream requests and prepared handoffs in the current world. No storage chapters or host restart. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import { defineDoc, defineExtension, defineTool, ROOT_CONVERSATION_ID as ROOT } from '@earendil-works/pi-durable'
import { arr, obj, opt, str } from '@earendil-works/pi-schema'
import { Fired } from '../automations/refs.ts'
import { Standing, Started } from '../subagents/api.ts'
import { BuiltInDoc, PauseDoc, DREAM_ASK, ensureAutomation, fired } from './built-ins.ts'
import { deliver, deliverTask } from './delivery.ts'
export const DreamApi={name:'dream'}
export const Next=defineDoc<any>({kind:'dream.next',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
const Receipts=defineDoc<Record<string,any>>({kind:'dream.tool-receipts',version:1,scope:'task',checkpointWhen:()=>false,initial:()=>({})})
const plain=(value:any)=>JSON.parse(JSON.stringify(value))
export function prepare(args:any,now:number) {
if(!Array.isArray(args?.learned)||args.learned.length!==3) throw new Error('learned must be 3 lines')
const text=(value:any,name:string,max:number)=>{
if(typeof value!=='string'||!value.trim()) throw new Error(`${name} must be nonempty text`)
if(value.trim().length>max) throw new Error(`${name} must be at most ${max} characters`)
return value.trim()
}
return {learned:args.learned.map((line:any)=>text(line,'learned',200)),handoff:text(args.handoff,'handoff',12000),at:now}
}
export default function dream(host:any) {
const queue=host.resource('dream.ensure',()=>({last:Promise.resolve()}))
const ensure=()=>{
const run=queue.last.then(()=>ensureAutomation(host))
queue.last=run.catch(()=>undefined)
return run.catch((error:any)=>host.notice(`Dream automations: ${String(error)}`))
}
host.onOpen(ensure)
host.react('ultimate-started',[Started],(_tx:any,{data}:any)=>{
// Creation commits run after this reactor transaction completes.
if(data.kind==='standing'&&data.agent==='ultimate') void ensure()
},{from:'now'})
host.react('requested',[Fired],(tx:any,{data}:any)=>fired(host,tx,data.id,data.fire),{from:'now'})
async function change(tx:any,args:any) {
const ultimate=(await tx.doc(Standing,ROOT)).ultimate
if(ultimate===undefined) throw new Error('@ultimate has no conversation yet')
if(args.action==='start') {
await deliver(host,tx,ultimate,{type:'input',requestId:`dream.requested:${crypto.randomUUID()}`,kind:'dream.requested',data:{},content:`The user requested a dream. ${DREAM_ASK}`})
return {requested:true}
}
if(args.action!=='prepare') throw new Error(`Unknown dream action ${args.action}`)
const prepared=prepare(args,Date.now())
;(await tx.doc(Next,ROOT)).prepared=prepared
await deliver(host,tx,ultimate,{type:'write',requestId:`dream.prepared:${crypto.randomUUID()}`,kind:'dream.prepared',data:prepared,content:`Prepared dream handoff:\n${prepared.learned.join('\n')}\n\n${prepared.handoff}\nContinue in this world. No chapter switch is available.`})
return prepared
}
host.implement(DreamApi,{operate:(args:any)=>host.harness().commit((tx:any)=>change(tx,args),context)})
return defineExtension({name:'dream',tasks:[deliverTask(host)],tools:[defineTool({name:'dream',description:'Request a read-only dream branch, or prepare three learned lines and a handoff in the current world. No chapter switch.',parameters:obj({action:str(),learned:opt(arr(str())),handoff:opt(str())}),execute:async(args,api,ctx)=>{
const result=await api.commit(async(tx:any)=>{
const receipts=await tx.doc(Receipts,api.taskId)
if(receipts[api.callId]===undefined) receipts[api.callId]=await change(tx,args)
return plain(receipts[api.callId])
},ctx)
return {content:[{type:'text',text:JSON.stringify(result)}]}
}})]})
}
export const docs=[Next,BuiltInDoc,PauseDoc]
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
/** Real dream triggers, message reports, and built-in receipts across a harness reopen. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import { createModels } from '@earendil-works/pi-ai/models'
import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux'
import { Harness, createRegistry, defineExtension, defineTool } from '@earendil-works/pi-durable'
import { obj } from '@earendil-works/pi-schema'
import { loadExtensions } from './extensions'
import { openStorage } from './sqlite'
const assert=(ok:any,message:string)=>{if(!ok) throw new Error(message)}
const until=async(test:()=>any)=>{const deadline=Date.now()+20000;while(!await test()){if(Date.now()>deadline)throw new Error(`Dream deadline: ${JSON.stringify((await entries()).filter((e:any)=>['pi.tool-result','board.reply'].includes(e.kind)))}`);await new Promise(r=>setTimeout(r,0))}}
const models=createModels(),faux=fauxProvider({tokensPerSecond:100000});models.setProvider(faux.provider)
const registry=createRegistry(),storage=await openStorage(globalThis.storagePath)
const harness=await Harness.open(storage,{models,registry,settings:{retry:{enabled:false},get extensions(){return registry.snapshot().installed()}}},context)
const root=await harness.root(context,{agent:{model:{provider:'faux',modelId:'faux-1'},cwd:'/src/demo'}})
const route=(transcript:any):any=>{
faux.appendResponses([route])
const last=transcript.messages.findLast((m:any)=>m.role!=='system')
const text=typeof last?.content==='string'?last.content:(last?.content??[]).filter((p:any)=>p.type==='text').map((p:any)=>p.text).join('')
if(last?.role==='user'&&text.startsWith('CALL ')){const call=JSON.parse(text.slice(5));return fauxAssistantMessage([fauxToolCall(call.name,call.args)],{stopReason:'toolUse'})}
if(last?.role==='user'&&text.includes('Dream journal pass')) return fauxAssistantMessage([fauxToolCall('subagent',{action:'start',agent:'dream-pass',task:'Review supplied evidence',background:true,isolate:'none'})],{stopReason:'toolUse'})
if(last?.role==='user'&&text==='Review supplied evidence') return fauxAssistantMessage('Evidence #1: keep the review read-only.')
if(last?.role==='user'&&/^(Reply|Message|Note) from/.test(text)) return fauxAssistantMessage('Received')
if(last?.role==='toolResult'||text.includes('Evidence #1')) return fauxAssistantMessage('Dream journal: keep the review read-only. Evidence #1.')
return fauxAssistantMessage('Received')
}
faux.setResponses([route])
// The role requires the normal host read/bash tools. The scripted pass does not invoke them.
const baseTools=defineExtension({name:'dream-test-tools',tools:['read','bash'].map(name=>defineTool({name,description:'Unused fixture tool',parameters:obj({}),execute:async()=>{throw new Error('Unexpected fixture tool invocation')}}))})
const loader=await loadExtensions(harness,registry,storage,models,context,[baseTools])
assert(!loader.state().error,`Extension load: ${loader.state().error}`)
const base=`file:///run/sausage/extensions/${loader.state().good}/extensions`
const {BuiltInDoc,PauseDoc,ensureAutomation,fired,AUTOMATION_NAME,PAUSE_NAME}=await import(`${base}/dream/built-ins.ts`)
const {Next}=await import(`${base}/dream/server.ts`)
const {Automations}=await import(`${base}/automations/server.ts`)
const {ContextUsageDoc}=await import(`${base}/automations/triggers.ts`)
const {Fired}=await import(`${base}/automations/refs.ts`)
const {Messages}=await import(`${base}/messages/api.ts`)
const primary=(await harness.conversation(await globalThis.dreamSubagents.standing('ultimate',context),context))!
const entries=async()=> (await primary.context(context)).entries
const ledger=async()=> (await harness.snapshot(Automations,context))?.automations??{}
const requested=async()=> (await entries()).filter((e:any)=>e.kind==='dream.requested')
const operation=(args:any)=>globalThis.dreamAutomations.operate(args)
harness.resume()
try {
if(globalThis.dreamMode==='reopen') {
await ensureAutomation(globalThis.dreamHost)
const all=Object.values(await ledger()) as any[]
assert(all.length===1&&all[0].name===PAUSE_NAME&&!all[0].enabled,'Reopen must preserve deletion and pause')
assert((await requested()).length===2,'Reopen duplicated dream requests')
assert((await harness.snapshot(Next,root.id,context)).prepared.handoff==='Replacement handoff','Prepared handoff must survive reopen')
assert((await harness.snapshot(Messages,root.id,context)).messages.filter((m:any)=>m.re!==undefined&&m.text.includes('Dream journal')).length===1,'Reopen duplicated the dream report')
} else {
await until(async()=>Object.keys(await ledger()).length===2)
await ensureAutomation(globalThis.dreamHost)
const id=(await harness.snapshot(BuiltInDoc,root.id,context)).id
const pause=(await harness.snapshot(PauseDoc,root.id,context)).id
assert(Object.keys(await ledger()).length===2,'Repeated ensure duplicated built-ins')
assert((await ledger())[id].name===AUTOMATION_NAME&&(await ledger())[id].condition.above===70,'Compaction trigger')
assert((await ledger())[pause].condition.idleFor===8*3600_000&&(await ledger())[pause].condition.above===50,'Pause trigger')
await harness.commit(async(tx:any)=>Object.assign(await tx.doc(ContextUsageDoc,primary.id),{percent:71,tokens:710,window:1000}),context)
await until(async()=> (await requested()).filter((e:any)=>e.data.automation?.id===id).length===1)
await harness.commit((tx:any)=>globalThis.dreamHost.emit(tx,Fired,{id,fire:1}),context)
await harness.commit((tx:any)=>fired(globalThis.dreamHost,tx,id,1),context)
await harness.waitForIdle(context)
const deliveries=await harness.commit((tx:any)=>tx.scanTasks({kind:'dream.deliver'},100),context)
for(const task of deliveries.items) await harness.waitForTask(task.id,context)
assert((await requested()).length===1,'One fire must add exactly one dream.requested entry')
const request=(await storage.scanSubmissions({conversationId:primary.id},100,undefined,context)).items.find((s:any)=>s.requestId===`dream.requested:${id}:1`)
assert(request?.type==='write','The fire adds a passive write')
await globalThis.dreamService.operate({action:'start'})
await until(async()=> (await requested()).length===2)
for(const args of [{learned:['one'],handoff:'x'},{learned:['a','b','c'],handoff:'x'.repeat(12001)}]) {
let failed=false;try{await globalThis.dreamService.operate({action:'prepare',...args})}catch{failed=true}assert(failed,'Invalid preparation must fail')
}
await globalThis.dreamService.operate({action:'prepare',learned:['I learned one.','I keep two.','I propose three.'],handoff:'First handoff'})
await globalThis.dreamService.operate({action:'prepare',learned:['a','b','c'],handoff:'Replacement handoff'})
await until(async()=> (await entries()).filter((e:any)=>e.kind==='dream.prepared').length===2)
await (await primary.submit({type:'input',content:`CALL ${JSON.stringify({name:'branch',args:{repo:'/src/demo',prompt:'Dream journal pass'}})}`},context)).wait(context)
await until(async()=> (await entries()).some((e:any)=>e.kind==='board.reply'&&e.data.text.includes('Dream journal')))
await harness.waitForIdle(context)
const report=(await entries()).find((e:any)=>e.kind==='board.reply'&&e.data.text.includes('Dream journal'))
const branch=(await harness.conversation(report.data.from,context))!
const reports=(await branch.context(context)).entries.filter((e:any)=>e.kind==='subagents.report')
assert(reports.length===1&&reports[0].data.agent==='dream-pass'&&JSON.stringify(reports).includes('Evidence #1'),`The dream-pass must report its evidence: ${JSON.stringify((await branch.context(context)).entries.filter((e:any)=>['pi.tool-result','subagents.report'].includes(e.kind)))}`)
await operation({action:'pause',id:pause})
await harness.commit(async(tx:any)=>{delete (await tx.doc(PauseDoc,root.id)).id},context)
await ensureAutomation(globalThis.dreamHost)
assert((await harness.snapshot(PauseDoc,root.id,context)).id===pause&&!(await ledger())[pause].enabled,'A lost receipt must adopt the paused automation')
await operation({action:'delete',id})
await ensureAutomation(globalThis.dreamHost)
assert((await harness.snapshot(BuiltInDoc,root.id,context)).deleted&&Object.keys(await ledger()).length===1,'Deletion must persist')
}
} finally {await loader.close();await harness.close(context)}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"private": true,
"type": "module",
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js" },
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js && esbuild dream-test.ts --bundle --format=esm --platform=browser --outfile=dream-test-bundle.js" },
"dependencies": {
"@earendil-works/pi-durable": "1.5.0",
"@earendil-works/pi-ai": "1.5.0",
+131
View File
@@ -0,0 +1,131 @@
//! World-loaded dream built-ins and messages survive a harness reopen.
use sausage::{harness::run_module, world::World};
use std::{path::Path, process::Command};
const BUNDLE: &str = include_str!("../js/dream-test-bundle.js");
fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.output()?;
anyhow::ensure!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().into())
}
fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}
fn prepare(dir: &Path) -> anyhow::Result<()> {
World::open(dir)?;
let disk = dir.join("world");
let repo = disk.join("src/sausage");
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
anyhow::ensure!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd-init build failed"
);
}
anyhow::ensure!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success(),
"world git build failed"
);
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
std::fs::create_dir_all(repo.join("extensions/probe"))?;
std::fs::write(
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.dreamHost=host; globalThis.dreamService=host.need({name:'dream'}); globalThis.dreamSubagents=host.need({name:'subagents'}); globalThis.dreamAutomations=host.need({name:'automations'}); return {name:'probe'}; }",
)?;
std::fs::create_dir_all(repo.join("agents"))?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"},"fast":{"model":"faux/faux-1"}}"#,
)?;
for role in ["ultimate", "coordinator", "worker", "reviewer"] {
let denial = if role == "worker" || role == "reviewer" {
"denyTools: [subagent]\n"
} else {
""
};
std::fs::write(
repo.join(format!("agents/{role}.md")),
format!(
"---\nname: {role}\ndescription: P3 {role}\nisolate: none\n{denial}{}---\nP3 role:{role}\n",
if role == "ultimate" {
"resident: true\n"
} else {
""
}
),
)?;
}
std::fs::copy(
source.join("agents/dream-pass.md"),
repo.join("agents/dream-pass.md"),
)?;
let demo = disk.join("src/demo");
std::fs::create_dir_all(&demo)?;
std::fs::write(demo.join("README.md"), "P3 demo\n")?;
for repo in [&repo, &demo] {
git(repo, &["init", "-b", "main"])?;
git(repo, &["add", "."])?;
git(repo, &["commit", "--no-gpg-sign", "-m", "P3 fixture"])?;
}
Ok(())
}
async fn run(dir: &Path, mode: &str) -> anyhow::Result<()> {
let world = World::open(dir)?;
let exec = sausage::process::exec(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?;
run_module(
world,
exec,
&format!("globalThis.dreamMode={mode:?};\n{BUNDLE}"),
)
.await
}
#[tokio::test(flavor = "current_thread")]
async fn builtins_fire_and_report_across_reopen() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
prepare(dir.path())?;
run(dir.path(), "first").await?;
run(dir.path(), "reopen").await
}