export declare const APP2_JS = "\nconst ICONS={\"user\": \"\", \"users\": \"\", \"users-three\": \"\", \"robot\": \"\", \"database\": \"\", \"globe-simple\": \"\", \"plugs-connected\": \"\", \"hammer\": \"\", \"bookmark-simple\": \"\", \"rectangle\": \"\", \"squares-four\": \"\", \"file-text\": \"\", \"table\": \"\", \"envelope\": \"\", \"archive\": \"\", \"scales\": \"\", \"arrow-bend-up-right\": \"\", \"arrow-counter-clockwise\": \"\", \"hourglass\": \"\", \"hand-palm\": \"\", \"hand-pointing\": \"\", \"eye\": \"\", \"stop\": \"\", \"wrench\": \"\", \"books\": \"\", \"list-checks\": \"\", \"list-dashes\": \"\", \"check-square\": \"\", \"briefcase\": \"\", \"question\": \"\", \"note\": \"\", \"puzzle-piece\": \"\", \"rows\": \"\", \"chair\": \"\", \"columns\": \"\", \"share-network\": \"\", \"export\": \"\", \"magnifying-glass\": \"\", \"caret-down\": \"\", \"caret-right\": \"\", \"play\": \"\", \"pencil-simple\": \"\", \"pencil-line\": \"\", \"plus\": \"\", \"minus\": \"\", \"arrows-out-simple\": \"\", \"microphone\": \"\", \"lightning\": \"\", \"check\": \"\", \"x\": \"\", \"link\": \"\", \"tray-arrow-down\": \"\", \"clock\": \"\", \"arrows-left-right\": \"\", \"paper-plane-tilt\": \"\", \"arrow-right\": \"\", \"warning\": \"\", \"storefront\": \"\", \"scroll\": \"\", \"translate\": \"\", \"git-diff\": \"\", \"flask\": \"\", \"monitor\": \"\", \"prohibit\": \"\", \"square\": \"\", \"square-half\": \"\", \"square-half-bottom\": \"\", \"shapes\": \"\", \"gear\": \"\", \"dots-three\": \"\", \"cursor\": \"\", \"hand\": \"\", \"text-align-left\": \"\", \"user-plus\": \"\", \"target\": \"\", \"arrows-in-simple\": \"\", \"arrow-u-up-left\": \"\", \"chats-circle\": \"\", \"speaker-high\": \"\", \"key\": \"\", \"microphone-slash\": \"\", \"brain\": \"\", \"graph\": \"\", \"chat-circle-dots\": \"\", \"binoculars\": \"\", \"timer\": \"\", \"infinity\": \"\", \"dots-three-circle\": \"\", \"arrows-out\": \"\", \"arrows-in\": \"\", \"corners-out\": \"\", \"corners-in\": \"\"};\nfunction icons(root){ (root||document).querySelectorAll('i.ph').forEach(el=>{ const name=[...el.classList].find(c=>c.startsWith('ph-')); const k=name?name.slice(3):''; const p=ICONS[k]||ICONS['rectangle']; const svg=document.createElementNS('http://www.w3.org/2000/svg','svg'); svg.setAttribute('viewBox','0 0 256 256'); svg.setAttribute('class','ic'); svg.innerHTML=p; if(el.hasAttribute('style')) svg.setAttribute('style',el.getAttribute('style')); if(el.title) svg.setAttribute('title',el.title); [...el.classList].filter(c=>!c.startsWith('ph')).forEach(c=>svg.classList.add(c)); el.replaceWith(svg); }); }\nnew MutationObserver(()=>icons()).observe(document.documentElement,{childList:true,subtree:true});\nconst $=(s,r=document)=>r.querySelector(s),$$=(s,r=document)=>[...r.querySelectorAll(s)];\nconst esc=s=>String(s??'').replace(/[&<>\"]/g,c=>({'&':'&','<':'<','>':'>','\"':'"'}[c]));\nconst qs=new URLSearchParams(location.search);\nconst WHOI={person:'ph-user',team:'ph-users',agent:'ph-robot',system:'ph-database',outside:'ph-globe-simple',build:'ph-hammer'};\nconst WHOC={person:'var(--person)',team:'var(--person)',agent:'var(--agent)',system:'var(--system)',outside:'var(--outside)',build:'var(--system)'};\nconst TRIG={hand:['ph-hand-pointing','Started by a person'],ask:['ph-chat-circle-dots','When requested'],event:['ph-tray-arrow-down','When an input arrives'],chain:['ph-link','After the preceding job'],clock:['ph-clock','on a schedule'],watch:['ph-binoculars','when a condition is met'],deadline:['ph-timer','At a deadline'],always:['ph-infinity','Continuous work'],other:['ph-dots-three-circle','Another trigger']};\nconst KIND={look:'Find information',read:'Read and extract',match:'Compare or match',draft:'Draft content',decide:'decide',tell:'Communicate a result',move:'Transfer information',wait:'Wait for a response'};\nconst KI=k=>({decide:'ph-scales',wait:'ph-hourglass',tell:'ph-paper-plane-tilt',read:'ph-file-text',look:'ph-magnifying-glass',match:'ph-arrows-left-right',draft:'ph-pencil-line'})[k]||'ph-squares-four';\nconst SW=['#f0a35e','#4fcb92','#8fa3b8','#d9c9a6','#e07a7a','#b48ce0'];\n/* ===== state ===== */\nconst state={board:null,name:qs.get('board')||(window.__staves?(window.__staves.store.list()[0]||'my-board'):''),sel:null,selKind:'board',multi:new Set(),opened:new Set(),zoom:1,view:'normal',needOnly:false,focus:qs.get('focus')||null,focusStack:[],presence:[],connectMode:null};\nconst live=()=>state.board.jobs.filter(j=>!j.removed&&j.kind!=='ghost');\nconst track=id=>state.board.tracks.find(t=>t.id===id), job=id=>state.board.jobs.find(j=>j.id===id&&!j.removed);\nconst kids=id=>live().filter(j=>j.parent===id).sort((a,b)=>(a.order??0)-(b.order??0));\nconst topJobs=()=>live().filter(j=>!j.parent);\nconst findings=j=>(state.board.findings||[]).filter(f=>f.about===j.id&&f.severity!=='info'&&!(f.rule==='account-missing'&&(j.kind==='outside'||trackOf(j)?.kind==='outside')));\nconst isDraft=j=>track(j.t||j.track)?.kind!=='outside'&&j.kind!=='store'&&j.kind!=='queue'&&!(j.outcome&&j.beneficiary);\n/* Who this card came from. Computed on the server (src/authorship.ts) rather than here, because the\n rule that one confirmed field out of four is still mostly staves' is a judgement, and a judgement\n that lives in two places will eventually disagree with itself. A card with nothing in it is not a\n guess \u2014 it is a blank, and .draft already draws that. */\nconst authorOf=j=>state.board?.authorship?.jobs?.[j.id];\nconst guessed=j=>!!authorOf(j)?.inferred&&!isDraft(j);\nconst trackOf=j=>track(j.track);\n/* ===== io ===== */\nconst fq=()=>state.focus?'&focus='+encodeURIComponent(state.focus):'';\nasync function load(){ state.board=await (await fetch('./board.json?board='+encodeURIComponent(state.name)+fq(),{cache:'no-store'})).json(); render(); }\nasync function op(ops,opts={}){ const response=await fetch('./op?board='+encodeURIComponent(state.name)+(opts.propose?'&propose=1':''),{method:'POST',body:JSON.stringify(ops)}); if(!response.ok)throw new Error(await response.text()||'Could not save changes'); await load(); }\nasync function redo(){ const r=await (await fetch('./redo?board='+encodeURIComponent(state.name),{method:'POST'})).json(); if(!r.redone) toast('Nothing to redo'); await load(); }\nlet lastSeen=null; const FEEDMAP={job:'described',updateJob:'updated',split:'split',collect:'gathered',ask:'asked',answer:'answered',comment:'noted',track:'added',artifact:'named',region:'grouped',setVolume:'set the volume of',setIntent:'set the intent',confirm:'confirmed',issue:'picked up',accept:'accepted',reject:'declined',removeJob:'removed',reorder:'reordered',board:'started'};\nasync function feedTick(){ try{ const es=await (await fetch('./entries?board='+encodeURIComponent(state.name)+(lastSeen?'&since='+lastSeen:''),{cache:'no-store'})).json(); if(!lastSeen){ lastSeen=es.length?es[es.length-1].id:'0'; return; } const agentOps=es.filter(e=>e.by!=='human'); if(agentOps.length){ const ul=$('#feedl'); agentOps.forEach(e=>{ const o=e.op; const push=e.by==='staves'; const what=push?esc((o.comment?.text||'').replace(/^pushed back on /,'on ').split(/(?<=[.!?])\\s/)[0]):(FEEDMAP[o.t]||o.t)+' '+esc(o.job?.name||o.track?.name||o.artifact?.name||o.region?.name||o.question?.text||o.comment?.text||(o.id&&job(o.id)?.name)||''); const li=document.createElement('li'); li.className=push?'push':''; li.innerHTML=''+esc(push?'pushed back':e.by)+''+what+''; ul.prepend(li); }); $('#feedt').textContent=(agentOps.find(e=>e.by!=='staves')?.by||'The agent')+' is describing'; $('#feed').classList.add('show'); clearTimeout(feedTick._t); feedTick._t=setTimeout(()=>{ $('#feed').classList.remove('show'); },8000);\n /* The live stream above is what normally repaints the canvas; this is the fallback for when it\n is down, so a long draft still assembles in front of the person instead of landing finished.\n A sheet or a menu is someone mid-sentence, so the repaint waits for the next tick instead. */\n if(agentOps.some(e=>!['comment','question','answer'].includes(e.op.t))&&!$('#wz').children.length&&!$('#pop').classList.contains('show')){ lastSeen=es[es.length-1].id; await load(); return; } } if(es.length) lastSeen=es[es.length-1].id; }catch(e){} }\nasync function undo(){ const r=await (await fetch('./undo?board='+encodeURIComponent(state.name),{method:'POST'})).json(); if(!r.undone) toast('Nothing to undo'); await load(); }\n/* render() rebuilds the outline, the stage and the canvas, and nothing else. Holding the repaint for\n any focused text field meant the board stopped updating the moment someone put their cursor in the\n composer \u2014 which is exactly when staves is drafting into it, so the work assembled where nobody was\n looking and arrived all at once as a finished thing. Only a field the repaint would actually\n destroy is worth waiting for. */\nconst typingWhereRepaintWouldLand=()=>{ const el=document.activeElement; return !!el&&el.matches&&el.matches('input,textarea')&&!!el.closest('#ol,#stage,#tl,#wz'); };\nfunction liveFeed(){ try{ const es=new EventSource('./events'); es.addEventListener('hello',e=>{ const d=JSON.parse(e.data); presence(d.presence); }); es.addEventListener('change',()=>{ if(!typingWhereRepaintWouldLand()) load(); feedTick(); }); es.addEventListener('presence',e=>presence(JSON.parse(e.data))); es.onerror=()=>{ es.close(); setTimeout(liveFeed,3000); }; }catch(e){} }\nfunction presence(list){ state.presence=list||[]; const el=$('#pres'); el.innerHTML=state.presence.length?state.presence.map(p=>''+esc(p.name)+(p.sampling?' ':'')+'').join(''):''; }\n/* ===== header ===== */\nfunction effortLabel(sc,key){ if(!sc||!Number.isFinite(sc[key]))return 'Not estimated'; const count=sc[key==='humanHours'?'humanJobs':'agentJobs']; if(count===0)return 'No work modeled'; const unknown=sc[key+'Unknown']; if(!Array.isArray(unknown))return 'Not estimated'; if(unknown.length)return 'Incomplete ('+unknown.length+' unspecified)'; return sc[key].toFixed(1); }\nfunction header(){ const b=state.board; const sc=b.scorecard||{}; $('#bname').textContent=(b.baseline||b.base?'Alternative \u00B7 ':'')+(b.title||state.name); for(const [id,key] of [['#m-h','humanHours'],['#m-a','agentHours']]){ const label=effortLabel(sc,key); $(id).textContent=Number.isFinite(Number(label))?label:'\u2014'; $(id).title=label; } $('#m-l').textContent=(b.findings||[]).filter(f=>f.severity!=='info').length; $('#m-d').textContent=topJobs().filter(isDraft).length;\n /* Not a score. It says how much of what is written down came from the person, so that a board\n staves drafted in two minutes cannot pass itself off as their account of their own work. */\n const au=b.authorship; const my=$('#m-y'); if(my){ my.textContent=au&&au.known?au.percent+'%':'\u2014'; my.parentElement.dataset.tip=au&&au.known?au.yours+' of the '+au.known+' things on this board came from you. The rest I made up.':'Nothing is written down yet'; my.parentElement.style.display=au&&au.known?'':'none'; }\n const need=topJobs().filter(j=>findings(j).length||b.questions.some(q=>q.about===j.id&&!q.answer)).length; $('#needn').textContent=need; $('#needn').style.display=need?'grid':'none'; }\n/* ===== outline ===== */\nfunction outline(){ const B=$('#olbody'); let h=''; const b=state.board;\n b.tracks.filter(t=>!t.removed).forEach(t=>{ const jobs=topJobs().filter(j=>j.track===t.id); const o=state.opened.has(t.id); h+='
'+esc(t.name)+''+jobs.length+'
';\n if(o) jobs.forEach(j=>{ const jo=state.opened.has(j.id); const F=findings(j); const ks=kids(j.id); h+='
'+(ks.length?'':'')+''+esc(j.name)+''+(b.questions.some(q=>q.about===j.id&&!q.answer)?'?':'')+(F.length?''+F.length+'':'')+(j.gate?'':'')+'
';\n if(jo) ks.forEach(x=>{ h+='
'+esc(x.name)+''+(x.tools||[]).map(tl=>'').join('')+(findings(x).length?''+findings(x).length+'':'')+'
'; }); }); });\n B.innerHTML=h;\n $$('.row',B).forEach(r=>{ const id=r.dataset.id,k=r.dataset.kind;\n r.onclick=e=>{ if(e.target.closest('.add')){ addMenu(e,id); return; } if(e.target.closest('.tw')&&k!=='task'){ state.opened.has(id)?state.opened.delete(id):state.opened.add(id); outline(); return; } if(e.shiftKey){ state.multi.has(id)?state.multi.delete(id):state.multi.add(id); render(); return; } state.multi.clear(); select(id,k); };\n r.ondblclick=()=>{ if(k!=='task'){ state.opened.has(id)?state.opened.delete(id):state.opened.add(id); outline(); } };\n r.ondragstart=e=>{ e.dataTransfer.setData('text/plain',k+':'+id); }; if(k!=='who') peekOn(r,id);\n r.ondragover=e=>{ e.preventDefault(); const rr=r.getBoundingClientRect(); const after=e.clientY>rr.top+rr.height*.6; r.classList.toggle('drag-after',after); r.classList.toggle('drag-over',!after); };\n r.ondragleave=()=>r.classList.remove('drag-over','drag-after');\n r.ondrop=async e=>{ e.preventDefault(); const after=r.classList.contains('drag-after'); r.classList.remove('drag-over','drag-after'); const dd=e.dataTransfer.getData('text/plain'); const sk=dd.slice(0,dd.indexOf(':')), sid=dd.slice(dd.indexOf(':')+1); await dropOutline(sk,sid,k,id,after); }; }); }\nasync function dropOutline(sk,sid,tk,tid,after){ if(sid===tid) return; const b=state.board;\n if(sk==='task'){ const t=job(sid); if(!t) return; let dest=null,ref=null;\n if(tk==='task'){ const d=job(tid); dest=job(d.parent); ref=d; } else if(tk==='job'){ dest=job(tid); } else if(tk==='who'){ // task onto a person: a new job for them holding this task\n const nid='j-'+Date.now(); await op([{t:'job',job:{id:nid,name:t.name,track:tid,trigger:track(tid).kind==='person'?'hand':'chain',inputs:[],outputs:[],provenance:{source:'human',by:'human'},status:'confirmed'}},{t:'updateJob',id:sid,patch:{parent:nid}}]); state.opened.add(tid); state.opened.add(nid); select(nid,'job'); return; }\n if(!dest) return; const dt=trackOf(dest);\n if(dt.kind==='agent'&&(t.workKind==='decide'||t.gate)){ toast('A decision can\\'t move to an agent \u2014 keep it with a person'); return; }\n const ops=[]; if(t.parent!==dest.id) ops.push({t:'updateJob',id:sid,patch:{parent:dest.id,track:dest.track}}); if(ref) ops.push({t:'reorder',id:sid,[after?'after':'before']:ref.id}); if(ops.length) await op(ops); state.opened.add(dest.id); return; }\n if(sk==='job'){ const j=job(sid); if(!j) return;\n if(tk==='who'){ if(j.track!==tid){ await op([{t:'updateJob',id:sid,patch:{track:tid}}]); toast('Moved to '+track(tid).name+' \u2014 handover checks apply'); } return; }\n if(tk==='job'){ const d=job(tid); const ops=[]; if(d.track!==j.track) ops.push({t:'updateJob',id:sid,patch:{track:d.track}}); ops.push({t:'reorder',id:sid,[after?'after':'before']:tid}); await op(ops); return; }\n if(tk==='task'){ if(kids(sid).length){ toast('This job has tasks of its own \u2014 take them out first'); return; } const d=job(tid); await op([{t:'updateJob',id:sid,patch:{parent:d.parent,track:d.track}},{t:'reorder',id:sid,after:tid}]); return; } } }\n/* ===== stage ===== */\nfunction select(id,k){ state.sel=id; state.selKind=k; if(k==='task'){ const t=job(id); if(t){ state.opened.add(t.track); state.opened.add(t.parent); } } if(k==='job'){ const j=job(id); if(j) state.opened.add(j.track); } render(); }\nfunction stageHandoff(){ const H=$('#sh'),B=$('#sb'); const h=state.sel; const A=job(h.from),Z=job(h.to); const a=state.board.artifacts.find(x=>x.id===h.artifact); if(!A||!Z){ select(null,'board'); return; } $('#stt').textContent='Handoff';\n const ex=(A.exits||[]).find(e=>e.target&&(e.target===Z.id||e.target.toLowerCase()===Z.name.toLowerCase()));\n H.innerHTML='
the boardwhat changes hands

'+esc(a?.name||'something')+'

from '+esc(A.name)+' ('+esc(trackOf(A)?.name||'')+') to '+esc(Z.name)+' ('+esc(trackOf(Z)?.name||'')+')
'+esc(a?.kind||'record')+''+(h.kind==='pull'?'the receiver pulls it when they get to it':h.kind==='admit'?'goes through a decision':'pushed when the sender finishes')+''+esc(Z.triggerNote||TRIG[Z.trigger]?.[1]||'how does the receiver start?')+''+(ex?'only when '+esc(ex.condition)+'':'')+'
';\n B.innerHTML='
'+[['document','ph-file-text','a document'],['data','ph-table','data'],['decision','ph-check-square','a decision'],['message','ph-envelope','a message'],['record','ph-note','a record']].map(([k,i,l])=>''+l+'').join('')+'
'+esc(a?.note||'')+'
'\n +'
'+state.board.handoffs.filter(x=>x.from===h.from&&x.to===h.to).map(x=>{ const ar=state.board.artifacts.find(y=>y.id===x.artifact); return '
'+esc(ar?.name||x.artifact)+''+esc(ar?.kind||'')+'
'; }).join('')+'
';\n $$('.ed[data-art]',H).forEach(el=>{ el.onblur=()=>{ const v=el.textContent.trim(); if(v&&v!==(a?.name||'')) op([{t:'artifact',artifact:{id:h.artifact,name:v,kind:a?.kind||'record'}}]); }; el.onkeydown=e=>{ if(e.key==='Enter'){ e.preventDefault(); el.blur(); } e.stopPropagation(); }; });\n $$('.ed[data-artnote]',B).forEach(el=>{ el.onblur=()=>{ const v=el.textContent.trim(); if(v!==(a?.note||'')) op([{t:'artifact',artifact:{id:h.artifact,name:a?.name||'something',kind:a?.kind||'record',note:v||undefined}}]); }; el.onkeydown=e=>{ e.stopPropagation(); }; });\n msgs('board'); }\nasync function deleteHandoff(){ const h=state.sel; const A=job(h.from),Z=job(h.to); if(!A||!Z) return; const a=state.board.artifacts.find(x=>x.id===h.artifact); if(!(await confirmSheet('Remove this handoff?','\"'+esc(a?.name||'something')+'\" will no longer go from '+esc(A.name)+' to '+esc(Z.name)+'. \u2318Z brings it back.','Remove'))) return; await op([{t:'updateJob',id:A.id,patch:{outputs:(A.outputs||[]).filter(x=>x!==h.artifact)}},{t:'updateJob',id:Z.id,patch:{inputs:(Z.inputs||[]).filter(x=>x!==h.artifact)}}]); select(null,'board'); }\nfunction confirmSheet(title,why,verb){ return new Promise(res=>{ const V=$('#veil'); V.innerHTML='
'+title+'
'+why+'
'; V.classList.add('show'); icons(V); const done=v=>{ closeSheet(); res(v); }; $('#cf-no').onclick=()=>done(false); $('#cf-yes').onclick=()=>done(true); V.onkeydown=e=>{ if(e.key==='Escape') done(false); if(e.key==='Enter'){ e.preventDefault(); done(true); } }; setTimeout(()=>$('#cf-yes').focus(),0); }); }\nfunction stage(){ if(state.selKind==='handoff'&&state.sel){ stageHandoff(); return; } const H=$('#sh'),B=$('#sb'); const b=state.board; const j=(state.selKind==='job'||state.selKind==='task')?job(state.sel):null;\n if(!j){ const tr=state.selKind==='who'?track(state.sel):null; $('#stt').textContent=tr?tr.name:'Board'; H.innerHTML='
'+(state.focus?'the board':'the board')+'

'+esc(tr?tr.name:b.title)+'

'+esc(tr?(tr.meta||''):(b.goal||''))+'
'+(!tr&&b.context?'
'+(b.context.outside?''+esc(b.context.outside)+'':'')+(b.context.stakes||[]).map(x=>''+esc(x)+'').join('')+(b.context.mustNot?''+esc(b.context.mustNot)+'':'')+(b.context.scale?''+esc(b.context.scale)+'':'')+'
':'')+'
'+b.tracks.filter(t=>!t.removed).length+''+topJobs().length+''+(b.findings||[]).filter(f=>f.severity!=='info').length+'
';\n if(topJobs().some(j=>j.boardRef)){ const chosen=b.questions.find(q=>q.id==='q-scope')?.answer; B.innerHTML='
'+(chosen?'You chose '+esc(chosen)+'. The agent picks it up when you say continue with staves. Change it below.':'Which workflow? Pick one to describe in depth, audit them all briefly, or describe all in depth. The agent reads your choice.')+'
'+topJobs().filter(j=>j.boardRef).map(j=>'
'+esc(j.name)+'for '+esc(j.beneficiary||'?')+(j.size?' \u00B7 '+esc(j.size):'')+''+esc((j.rationale||'').replace(/^entry: /,''))+'
'+(state.board.boards?.includes(j.boardRef)?'open':'')+'
').join('')+'
See the system
'; msgs('board'); return; }\n B.innerHTML=topJobs().length?'':'

Nothing drawn yet

Three ways in. Use any of them, in any order.
Let an agent describe the codeone command to run, then: resume staves
Be interviewedstart from the goal; it sketches as you talk
Create a text outlinewho does what, one line each
Draw itadd someone, then right-click their row
'; msgs('board'); return; }\n const t=trackOf(j); const F=findings(j); const isTask=!!j.parent; $('#stt').textContent=isTask?'Task':'Job'; const parent=isTask?job(j.parent):null;\n H.innerHTML='
the board'+esc(t.name)+''+(parent?''+esc(parent.name)+'':'')+'

'+esc(j.name)+''+(j.gate?'':'')+'

'+(isTask?'':'
'+esc(j.outcome||'')+'
')\n +'
'+(isDraft(j)&&!isTask?'Outcome not yet described':'')+(j.beneficiary?'For'+esc(j.beneficiary)+'':'Who needs the result?')+((j.doneWhen||[]).length?''+esc(j.doneWhen[0])+'':'Add completion criteria')+(j.gate?''+esc(j.gate.accountable==='rule'?'a rule':track(j.gate.accountable)?.name||j.gate.accountable||'?')+' decides':'')+(F.length?''+F.length+'':'')+'
'+esc(j.triggerNote||(j.trigger?TRIG[j.trigger]?.[1]||j.trigger:'how does it start?'))+''+(t.kind==='person'||t.kind==='team'||t.kind==='outside'?(j.status==='draft'?'':'Description confirmed'):((j.sources||[]).length?'x.path).join(', '))+'\" style=\"color:'+(state.board.stale?.some(x=>x.job===j.id)?'var(--alert)':'var(--agent)')+'\">x.job===j.id)?'ph-warning':'ph-git-diff')+'\">'+(state.board.stale?.some(x=>x.job===j.id)?'Source changed':'Code references attached')+'':'no source'))+'
';\n const ks=kids(j.id);\n const cards=ks.map((x,i)=>'
'+esc(x.name)+'
'+esc(KIND[x.workKind]||x.workKind||'')+'
'+(x.tools||[]).map(tl=>''+esc(tl.name)+'').join('')+'
'+findings(x).slice(0,1).map(f=>'
'+esc(f.message.replace(/^\"[^\"]*\"[: ]*/,''))+'
').join('')+(state.view==='chairs'?'
'+['person','agent','buy','build'].map(ch=>'').join('')+'
':'')+'
open
'+(i':'')).join('')+'
';\n const qsOpen=b.questions.filter(q=>q.about===j.id);\n const raised=qsOpen.map(q=>'
'+esc(q.text)+'
'+(q.answer?'
'+esc(q.answeredBy||'the agent')+' \u00B7 '+esc(q.answer)+'
':'')+'
'+esc(q.answer?'answered':q.status||'raised')+'
').join('');\n const instr=(j.instructions||[]).map((x,i)=>'
Works from'+esc(x.path)+(x.symbol?' \u00B7 '+esc(x.symbol):'')+'read-only from the repo
'+(x.summary?'
'+esc(x.summary)+'
':'')+(x.text?'
'+esc(x.text)+'
':'
\u2026
')+'
').join('');\n const noticed=findings(j).concat(isTask?[]:kids(j.id).flatMap(k=>findings(k)));\n B.innerHTML=(isTask?'':'
'+cards+'
')+(noticed.length?'
The board noticed'+noticed.length+' \u2014 each one has a fix
'+findingsHtml(noticed,false)+'
':'')+'
Raised'+qsOpen.filter(q=>!q.answer).length+' waiting \u00B7 '+qsOpen.filter(q=>q.answer).length+' answered
'+raised+'
\u21B5
'+instr+(isTask?taskEditor(j):'');\n $$('.ed[data-field]',$('#stage')).forEach(el=>{ const save=async()=>{ const v=el.textContent.trim(); const f=el.dataset.field, id=el.dataset.id; const x=job(id); if(!x) return; if(f==='check'){ if(v){ el.textContent=''; await op([{t:'updateJob',id,patch:{checks:[...(x.checks||[]),{rule:v}]}}]); } return; } if(v===(x[f]||'')) return; await op([{t:'updateJob',id,patch:{[f]:v||undefined}}]); }; el.onblur=save; el.onkeydown=e=>{ if(e.key==='Enter'&&(el.dataset.field==='name'||el.dataset.field==='check')){ e.preventDefault(); el.blur(); } if(e.key==='Escape'){ el.blur(); } e.stopPropagation(); }; });\n $('#raise').onkeydown=async e=>{ if(e.key==='Enter'&&e.target.value.trim()){ const text=e.target.value.trim(); e.target.value=''; await op([{t:'ask',question:{id:'q-'+Date.now(),about:j.id,askedBy:'human',text,status:'raised',at:new Date().toISOString()}}]); toast('Raised \u2014 the agent picks it up on /issues'); } };\n $$('pre[data-src]',B).forEach(async pre=>{ try{ pre.textContent=await (await fetch('./source?path='+encodeURIComponent(pre.dataset.src)+(pre.dataset.sym?'&symbol='+encodeURIComponent(pre.dataset.sym):''))).text(); }catch(e){ pre.textContent='(not readable here \u2014 the agent can attach it)'; } });\n $$('.card[data-t]',B).forEach(c=>{ c.onclick=e=>{ if(e.shiftKey){ state.multi.has(c.dataset.t)?state.multi.delete(c.dataset.t):state.multi.add(c.dataset.t); render(); } else select(c.dataset.t,'task'); }; c.ondragstart=e=>e.dataTransfer.setData('text/plain','task:'+c.dataset.t); c.ondragover=e=>{e.preventDefault();c.style.borderColor='var(--acc)';}; c.ondragleave=()=>c.style.borderColor=''; c.ondrop=async e=>{ e.preventDefault(); c.style.borderColor=''; const dd=e.dataTransfer.getData('text/plain'); const k=dd.slice(0,dd.indexOf(':')), id=dd.slice(dd.indexOf(':')+1); if(k==='task') await dropOutline('task',id,'task',c.dataset.t,true); }; });\n msgs(j.id); }\nasync function addTask(jid){ const j=job(jid); const id=jid+':t'+Date.now(); await op([{t:'job',job:{id,name:'New task',track:j.track,parent:jid,trigger:'chain',inputs:[],outputs:[],provenance:{source:'human',by:'human'},status:'confirmed'}}]); state.opened.add(jid); select(id,'task'); sheetTask(id); }\nasync function splitAt(jid,i){ const ks=kids(jid); const after=ks.slice(i); const produces=after.some(x=>['tell','draft','decide'].includes(x.workKind)||x.outputs?.length); if(!produces){ toast('Nothing changes hands here \u2014 this is still one job'); return; } const j=job(jid); const nid='j-'+Date.now(); await op([{t:'job',job:{id:nid,name:after[0].name,track:j.track,trigger:'chain',inputs:[],outputs:[],provenance:{source:'human',by:'human'},status:'confirmed'}},...after.map(x=>({t:'updateJob',id:x.id,patch:{parent:nid}})),{t:'reorder',id:nid,after:jid}]); select(nid,'job'); sheetJob(nid); }\n/* ===== ask drawer ===== */\nconst FIX={'account-missing':['say what a person gets',j=>sheetJob(j)],'no-tasks':['break it down',j=>addTask(j)],'no-checks':['add a check',j=>{ const k=kids(j)[0]; k?sheetTask(k.id):addTask(j); }],'tool-coverage':['say what the tool leaves out',j=>{ const k=kids(j).find(x=>x.tools?.some(t=>!t.limits))||kids(j)[0]; k?sheetTool(k.id):sheetTool(j); }],'tool-unreachable':['say how an agent reaches it',j=>{ const k=kids(j).find(x=>x.tools?.length)||kids(j)[0]; k?sheetTool(k.id):sheetTool(j); }],'gate-no-owner':['name who answers for it',j=>sheetGate(j)],'dangling-exit':['say where it goes',j=>sheetExit(j)],'parked-no-limit':['put a bound on the wait',j=>sheetJob(j)],'unread-artifact':['connect it to who reads it',j=>{ state.connectMode=j; const A=$('.clip[data-job=\"'+j+'\"]'); if(A){ const r=A.getBoundingClientRect(); startConnect(j,{clientX:r.right,clientY:r.top+r.height/2}); } }],'jargon':['name it as a person would',j=>sheetJob(j)],'name-too-long':['shorten the name',j=>sheetJob(j)],'machinery-at-top':['put it inside a job',j=>toast('Drag it under the job it serves, in the outline')],'handover':['confirm the move',j=>op([{t:'confirm',id:j,by:'human'}])],'stale':['ask the agent what changed',j=>{ select(j,'job'); toggleAsk(true); }]};\nfunction findingsHtml(list,withJob){ return list.map(f=>{ const fx=FIX[f.rule]; return '
'+(withJob?''+esc(job(f.about)?.name||f.about)+' \u00B7 ':'')+esc(f.message.replace(/^\"[^\"]*\"[: ]*/,''))+'
'+(fx?'':'')+'
'; }).join(''); }\nfunction sheetExit(id){ const j=job(id); const ex=(j.exits||[]).find(e=>!e.target||/^(stop|nothing|nowhere|\\?)$/i.test(e.target))||(j.exits||[])[0]; sheet('Define an exception',[['when','When does this happen?','Describe the exception',ex?.condition],['to','What happens next?','a job, a person, or \"stop \u2014 and who is told\"',ex?.target]],v=>op([{t:'updateJob',id,patch:{exits:[...(j.exits||[]).filter(e=>e!==ex),{condition:v.when||ex?.condition||'?',target:v.to||'?'}]}}]),'ph-arrow-bend-up-right'); }\n/* the task editor: everything about one task, editable in place */\nfunction taskEditor(x){ const rec=recommend(x); const tools=(x.tools||[]);\n return '
'+esc(x.rationale||'')+'
'\n +'
'+Object.entries(KIND).map(([k,l])=>''+l+'').join('')+'
'\n +''+tools.map((t,i)=>'
'+esc(t.name)+''+(t.does?'returns '+esc(t.does):'Results not described')+' \u00B7 '+(t.limits?'leaves out '+esc(t.limits):'Limits not described')+'
').join('')+''\n +''+(x.checks||[]).map(c=>'
'+esc(c.rule)+(c.onFail?' \u2014 else '+esc(c.onFail):'')+'
').join('')+'
'\n +'
'+esc(rec.title)+'
'+esc(rec.why)+'
'+(rec.level!=='no'?'':'')+'
Drag this task from the outline or its card onto another job, or onto a row of the timeline to make a job there.
'; }\nfunction recommend(x){ const tools=x.tools||[]; const screen=tools.some(t=>t.reach==='screen'), none=tools.some(t=>t.reach==='none'); const k=x.workKind;\n if(k==='decide'||x.gate) return {level:'no',title:'A person decides here',why:'Decisions stay with someone who answers for them. An agent can prepare it \u2014 the recommendation, the evidence \u2014 but not take it.'};\n if(none) return {level:'no',title:'A tool an agent cannot reach',why:tools.filter(t=>t.reach==='none').map(t=>t.name).join(', ')+' has no way in. Say how it could be reached, or keep this with a person.'};\n if(['look','read','match','draft','move'].includes(k)&&tools.length&&!screen) return {level:'yes',title:'Worth exploring for automation',why:(KIND[k]||k)+' with a reachable tool ('+tools.map(t=>t.name).join(', ')+'). Review the checks and failure handling before assigning it.'};\n if(['look','read','match','draft','move'].includes(k)&&screen) return {level:'partly',title:'Review how the tool would be used',why:tools.filter(t=>t.reach==='screen').map(t=>t.name).join(', ')+' is recorded as interface-only. Check whether reliable automation is possible.'};\n if(['look','read','match','draft','move'].includes(k)) return {level:'partly',title:'More detail would help assess this',why:(KIND[k]||k)+' may be suitable, but no tool is recorded. Describe the tools and required result before assessing automation.'};\n if(k==='tell') return {level:'partly',title:'Decide how much communication to automate',why:'Some messages can be sent automatically; others need human review. Describe which applies here.'};\n if(k==='wait') return {level:'no',title:'Define what ends the wait',why:'Record the expected response, a time limit and what happens if no response arrives.'};\n return {level:'partly',title:'Explore whether an agent could help',why:'Describe the steps, tools and judgement involved. A work type alone cannot establish suitability.'}; }\nasync function makeAgentFor(tid){ const x=job(tid); const parent=job(x.parent); let ag=state.board.tracks.find(t=>t.kind==='agent'&&!t.removed); const ops=[]; if(!ag){ const id='t-agent-'+Date.now().toString(36); ops.push({t:'track',track:{id,name:'An agent for '+x.name.toLowerCase(),kind:'agent',meta:'to be built'}}); ag={id}; }\n const art='a-'+Date.now().toString(36); const jid='j-'+Date.now().toString(36);\n ops.push({t:'artifact',artifact:{id:art,name:x.name.toLowerCase()+' \u2014 done',kind:'record'}});\n ops.push({t:'job',job:{id:jid,name:x.name,track:ag.id,trigger:'chain',inputs:[...(parent?.inputs||[])],outputs:[art],outcome:parent?.outcome,beneficiary:parent?.beneficiary||parent?.name,workKind:x.workKind,tools:x.tools,checks:x.checks,rationale:x.rationale,provenance:{source:'human',by:'human'},status:'draft'}});\n ops.push({t:'removeJob',id:tid});\n if(parent) ops.push({t:'updateJob',id:parent.id,patch:{inputs:[...(parent.inputs||[]),art],gate:parent.gate||{rule:'a person checks what the agent did',accountable:parent.track}}});\n await op(ops); select(jid,'job'); toast('Agent job added to the design with a review step.'); }\nasync function liftToJob(tid){ const x=job(tid); await op([{t:'updateJob',id:tid,patch:{parent:null,trigger:'chain'}}]); select(tid,'job'); sheetJob(tid); }\nfunction sheetToolEdit(id,i){ const x=job(id); const t=(x.tools||[])[i]; sheet(t.name,[['n','What is it?','',t.name],['reach','How can this tool be accessed?',null,t.reach||'api',null,[['api','ph-plugs-connected','Through an API'],['mcp','ph-plugs-connected','Through MCP'],['screen','ph-monitor','Through its interface'],['none','ph-prohibit','No access available']]],['does','What does this tool provide?','',t.does],['left','Limits or uncertainties','',t.limits]],v=>{ const tools=[...(x.tools||[])]; tools[i]={name:v.n||t.name,reach:v.reach||t.reach,does:v.does||undefined,limits:v.left||undefined}; op([{t:'updateJob',id,patch:{tools}}]); },'ph-wrench'); }\nfunction propLine(p){ const o=p.op; if(o.t==='updateJob') return 'change '+esc(job(o.id)?.name||o.id)+': '+Object.entries(o.patch).map(([k,v])=>k+' \u2192 '+esc(typeof v==='string'?v:JSON.stringify(v))).join(' \u00B7 '); if(o.t==='job') return 'add '+esc(o.job.name)+' on '+esc(track(o.job.track)?.name||o.job.track); if(o.t==='split') return 'split '+esc(job(o.id)?.name||o.id)+' into '+o.tasks.map(t=>esc(t.name)).join(', '); if(o.t==='collect') return 'gather '+o.into.length+' into '+esc(o.name)+''; return o.t; }\nfunction msgs(key){ const M=$('#msgs'); const b=state.board; const about=(o)=>o.t==='job'?o.job.id:o.id||(o.comment?.about)||'board'; const ps=(b.proposalsList||[]).filter(p=>key==='board'||about(p.op)===key); const list=b.comments.filter(c=>key==='board'?c.about==='board':c.about===key); $('#askn').textContent=ps.length; $('#askn').style.display=ps.length?'grid':'none';\n const fl=(key==='board'?(b.findings||[]).filter(f=>f.severity!=='info'):findings(job(key)||{id:key}).concat(kids(key).flatMap(k=>findings(k))));\n const head=(state.presence.length?'
connected
'+esc(state.presence.map(p=>p.name).join(', '))+' \u2014 Questions are saved here for the connected assistant.
':'
no agent connected
The board answers about its own findings. For anything else, connect an agent.
')\n +(fl.length?'
the board noticed \u00B7 '+fl.length+'
'+findingsHtml(fl,key==='board')+'
':'');\n M.innerHTML=head+ps.map(p=>'
'+(p.demoted?'merged from elsewhere \u2014 its basis had changed; decide':esc(p.by)+' proposes')+'
'+propLine(p)+'
').join('')\n +list.map(c=>'
'+esc(c.by)+'
'+esc(c.text)+'
').join('')||'
the system
Ask about this, or tell the agent what to change. It replies here.
'; }\nfunction toggleAsk(on){ const a=$('#ask'); a.classList.toggle('open',on==null?!a.classList.contains('open'):on); }\n$('#askin').onkeydown=async e=>{ if(e.key!=='Enter'||!e.target.value.trim()) return; const text=e.target.value.trim(); e.target.value=''; const about=state.sel&&state.selKind!=='who'?state.sel:'board'; const ops=[{t:'comment',comment:{id:'c-'+Date.now(),about,by:'human',text,at:new Date().toISOString()}}];\n if(/warning|finding|wrong|loose|notice|issue|problem|flag/i.test(text)){ const fl=about==='board'?(state.board.findings||[]).filter(f=>f.severity!=='info'):findings(job(about)||{id:about}).concat(kids(about).flatMap(k=>findings(k))); const reply=fl.length?'The board noticed '+fl.length+' thing'+(fl.length>1?'s':'')+' here: '+fl.map(f=>(about==='board'?(job(f.about)?.name||f.about)+' \u2014 ':'')+f.message.replace(/^\"[^\"]*\"[: ]*/,'')).join(' \u00B7 ')+'. Each one above has a fix.':'Nothing is flagged here right now.'; ops.push({t:'comment',comment:{id:'c-'+Date.now()+'s',about,by:'staves',text:reply,at:new Date().toISOString()}}); }\n else if(!state.presence.length) ops.push({t:'comment',comment:{id:'c-'+Date.now()+'s',about,by:'staves',text:'No agent is connected to answer this. It stays on the board; an agent picks it up on /issues when one connects \u2014 or ask about the warnings, which the board answers itself.',at:new Date().toISOString()}});\n await op(ops); };\n/* ===== timeline ===== */\nconst COL=224,X0=20;\n/* Which jobs were already on screen last time the canvas was drawn.\n timeline() rebuilds every clip from scratch on every render, so \"new\" cannot be read off the DOM.\n The first paint of a board seeds this set without animating: opening a board you already described\n should not replay its construction. After that, a job the canvas has not shown before is an\n arrival, and it lands. */\nconst drawn=new Set(); let drawnSeeded=false;\nfunction timeline(){ const el=$('#tracks'); el.innerHTML=''; const b=state.board; const cols=b.columns||{}; const ncol=Math.max(7,...topJobs().map(j=>(cols[j.id]??0)+1));\n if(!drawnSeeded){ topJobs().forEach(j=>drawn.add(j.id)); drawnSeeded=true; }\n const st=document.createElement('div'); st.className='stages'; st.innerHTML='
'; el.appendChild(st);\n b.tracks.filter(t=>!t.removed).forEach(t=>{ const d=document.createElement('div'); d.className='track '+t.kind; d.style.setProperty('--c',WHOC[t.kind]); d.dataset.track=t.id; d.innerHTML='
'+esc(t.name)+'
'+esc(t.meta||'')+'
'; const lane=d.querySelector('.lane'); for(let c=1;cj.track===t.id).forEach(j=>{ const c=document.createElement('div'); const F=findings(j); const col=cols[j.id]??0; c.className='clip'+(t.kind==='outside'?' outside':'')+(j.gate?' gate':'')+(state.sel===j.id?' sel':'')+(state.multi.has(j.id)?' multi':'')+(isDraft(j)?' draft':'')+(guessed(j)?' guess':''); c.style.setProperty('--c',WHOC[t.kind]); c.dataset.job=j.id;\n /* Cards land in the order the work happens, not the order the model emitted them. The column\n is already the flow position, so staggering by it means watching the board draw teaches you\n to read it; staggering by arrival would just be noise moving about. */\n if(!drawn.has(j.id)){ drawn.add(j.id); c.classList.add('arriving'); c.style.setProperty('--in',Math.min(col,9)*90+'ms'); } c.style.left=(X0+col*COL)+'px'; const ks=kids(j.id); const tools=ks.flatMap(x=>x.tools||[]);\n c.innerHTML=(j.trigger?'':'')+''+esc(j.name)+''+(ks.length?''+ks.length+'':'')+(tools.length?''+tools.slice(0,4).map(x=>'').join('')+'':'')+(F.length?''+F.length+'':'')+(j.boardRef?'board \u203A':'')+(b.questions.some(q=>q.about===j.id&&!q.answer)?'?':'')+(guessed(j)?'':'')+''+(state.view==='chairs'?''+['person','agent','buy','build'].map(ch=>{ const held=ks.length?ks.some(x=>(x.chair||(x.workKind==='decide'?'person':t.kind==='agent'?'agent':''))===ch):((j.chair||(t.kind==='agent'?'agent':t.kind==='system'||t.kind==='build'?'build':'person'))===ch); const could=ch==='agent'&&!held&&(ks.length?ks.some(x=>['look','read','match','draft','move'].includes(x.workKind)):false); return ''; }).join('')+'':'');\n c.onclick=e=>{ e.stopPropagation(); if(state.connectMode){ const from=state.connectMode; state.connectMode=null; if(from!==j.id) sheetHandoff(from,j.id); return; } if(e.shiftKey){ state.multi.has(j.id)?state.multi.delete(j.id):state.multi.add(j.id); render(); return; } state.multi.clear(); select(j.id,'job'); };\n c.ondblclick=()=>sheetJob(j.id); c.oncontextmenu=e=>clipMenu(e,j); if(!j.boardRef) peekOn(c,j.id); c.querySelector('.port').onmousedown=ev=>{ ev.stopPropagation(); ev.preventDefault(); startConnect(j.id,ev); };\n // the marker settles the card where it sits, and does not take the selection with it: correcting\n // a guess is a different act from choosing what to work on, and stealing focus conflates them\n const byEl=c.querySelector('.by'); if(byEl) byEl.onclick=ev=>{ ev.stopPropagation(); settlePop(ev,j); };\n lane.appendChild(c); });\n lane.ondragover=e=>{ if(e.dataTransfer.types.includes('text/plain')){ e.preventDefault(); lane.classList.add('hot'); } }; lane.ondragleave=()=>lane.classList.remove('hot');\n lane.ondrop=async e=>{ e.preventDefault(); lane.classList.remove('hot'); const dd=e.dataTransfer.getData('text/plain'); const k=dd.slice(0,dd.indexOf(':')), id=dd.slice(dd.indexOf(':')+1); const onClip=e.target.closest('.clip'); if(k==='task'&&onClip&&onClip.dataset.job!==id){ await dropOutline('task',id,'job',onClip.dataset.job,true); return; } if(k==='task'){ const x=job(id); if(!x) return; if(t.kind==='agent'&&x.workKind==='decide'){ toast('A decision stays with a person'); return; } await op([{t:'updateJob',id,patch:{parent:null,track:t.id,trigger:t.kind==='person'?'hand':'chain'}}]); select(id,'job'); } if(k==='job'){ const j=job(id); if(j&&j.track!==t.id){ await op([{t:'updateJob',id,patch:{track:t.id}}]); toast('Moved to '+t.name+' \u2014 handover checks apply'); } } };\n lane.oncontextmenu=e=>{ if(e.target.closest('.clip')) return; laneMenu(e,t,lane); };\n lane.onmousedown=e=>{ if(e.target!==lane||e.button!==0) return; const r=$('#tls').getBoundingClientRect(); const sx=e.clientX,sy=e.clientY; const mq=document.createElement('div'); mq.className='marq'; $('#tls').appendChild(mq); const mv=ev=>{ const x=Math.min(sx,ev.clientX)-r.left+$('#tls').scrollLeft,y=Math.min(sy,ev.clientY)-r.top+$('#tls').scrollTop; mq.style.left=x+'px'; mq.style.top=y+'px'; mq.style.width=Math.abs(ev.clientX-sx)+'px'; mq.style.height=Math.abs(ev.clientY-sy)+'px'; const box={l:Math.min(sx,ev.clientX),r:Math.max(sx,ev.clientX),t:Math.min(sy,ev.clientY),b:Math.max(sy,ev.clientY)}; state.multi.clear(); $$('.clip').forEach(cl=>{ const bb=cl.getBoundingClientRect(); if(bb.right>box.l&&bb.leftbox.t&&bb.top{ mq.remove(); document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); if(state.multi.size){ state.sel=null; state.justMarqueed=true; outline(); selbar(); } }; document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up); };\n el.appendChild(d);\n { const cs=[...lane.querySelectorAll('.clip')].sort((a,c)=>parseFloat(a.style.left)-parseFloat(c.style.left)); let x=0; cs.forEach(c=>{ const want=parseFloat(c.style.left); const left=Math.max(want,x); c.style.left=left+'px'; x=left+c.offsetWidth+14; }); }\n });\n if(b.diff){ (b.diff.moved||[]).forEach(m=>{ const tr=el.querySelector('.track[data-track=\"'+m.from+'\"] .lane'); if(!tr) return; const g=document.createElement('div'); g.className='clip ghost'; g.style.setProperty('--c',WHOC[track(m.from)?.kind||'person']); g.style.left=(X0+(cols[m.job.id]??0)*COL)+'px'; g.innerHTML=''+esc(m.job.name)+'was here'; tr.appendChild(g); }); (b.diff.removed||[]).forEach(j=>{ const tr=el.querySelector('.track[data-track=\"'+j.track+'\"] .lane'); if(!tr) return; const g=document.createElement('div'); g.className='clip ghost'; g.style.left=(X0+(cols[j.id]??0)*COL)+'px'; g.innerHTML=''+esc(j.name)+'removed'; tr.appendChild(g); }); (b.diff.added||[]).forEach(j=>{ $('.clip[data-job=\"'+j.id+'\"]')?.classList.add('added'); }); }\n const dr=document.createElement('div'); dr.className='drop-row'; dr.innerHTML='
someone
'; dr.onclick=e=>addMenu(e,null); el.appendChild(dr);\n requestAnimationFrame(()=>{ (b.regions||[]).filter(r=>!r.removed).forEach(rg=>{ const els=rg.members.map(id=>$('.clip[data-job=\"'+id+'\"]')).filter(Boolean); if(!els.length) return; const er=el.getBoundingClientRect(); let l=1e9,r2=-1e9,tp=1e9,bt=-1e9; els.forEach(x=>{const bb=x.getBoundingClientRect(); l=Math.min(l,bb.left);r2=Math.max(r2,bb.right);tp=Math.min(tp,bb.top);bt=Math.max(bt,bb.bottom);}); const d=document.createElement('div'); d.className='region'; d.style.setProperty('--c',rg.color||SW[0]); d.style.left=((l-er.left)/state.zoom-10)+'px'; d.style.top=((tp-er.top)/state.zoom-10)+'px'; d.style.width=((r2-l)/state.zoom+20)+'px'; d.style.height=((bt-tp)/state.zoom+20)+'px'; d.innerHTML=''+esc(rg.name)+''; el.appendChild(d); });\n const svg=document.createElementNS('http://www.w3.org/2000/svg','svg'); svg.setAttribute('class','wires'); el.appendChild(svg); (b.handoffs||[]).forEach(h=>{ const A=$('.clip[data-job=\"'+h.from+'\"]'),B=$('.clip[data-job=\"'+h.to+'\"]'); if(!A||!B||!A.offsetParent||!B.offsetParent) return; const r=el.getBoundingClientRect(),ra=A.getBoundingClientRect(),rb=B.getBoundingClientRect(); const ax=(ra.right-r.left)/state.zoom,ay=(ra.top+ra.height/2-r.top)/state.zoom,bx=(rb.left-r.left)/state.zoom,by=(rb.top+rb.height/2-r.top)/state.zoom; const p=document.createElementNS('http://www.w3.org/2000/svg','path'); p.setAttribute('class','w'+(h.kind==='pull'?' pull':h.kind==='admit'?' admit':'')+((h.from===state.sel||h.to===state.sel)?' near':'')+(state.selKind==='handoff'&&state.sel&&state.sel.from===h.from&&state.sel.to===h.to&&state.sel.artifact===h.artifact?' selw':'')); p.setAttribute('d',wirePath(ax,ay,bx,by)); p.onclick=ev=>{ ev.stopPropagation(); state.multi.clear(); state.sel={from:h.from,to:h.to,artifact:h.artifact,kind:h.kind}; state.selKind='handoff'; render(); }; p.onmouseenter=()=>{ const a=state.board.artifacts.find(x=>x.id===h.artifact); const pk=$('#peek'); pk.innerHTML='

'+esc(a?.name||'a handoff')+'

'+esc(job(h.from)?.name||'')+' \u2192 '+esc(job(h.to)?.name||'')+'
'; icons(pk); pk.classList.add('show'); const r=el.getBoundingClientRect(); pk.style.left=Math.min(window.innerWidth-360,r.left+((ax+bx)/2)*state.zoom-160)+'px'; pk.style.top=(r.top+((ay+by)/2)*state.zoom-90)+'px'; }; p.onmouseleave=()=>$('#peek').classList.remove('show'); svg.appendChild(p); }); });\n requestAnimationFrame(minimap);\n el.classList.toggle('dim',(!!state.sel&&state.selKind==='job')||state.needOnly); (b.handoffs||[]).forEach(h=>{ if(h.from===state.sel) $('.clip[data-job=\"'+h.to+'\"]')?.classList.add('near'); if(h.to===state.sel) $('.clip[data-job=\"'+h.from+'\"]')?.classList.add('near'); }); if(state.needOnly) $$('.clip').forEach(c=>{ if(findings(job(c.dataset.job)).length) c.classList.add('near'); }); selbar(); }\nfunction minimap(){ const mm=$('#mm'); const tls=$('#tls'); const tr=$('#tracks'); if(!mm||!tr) return; if(document.body.classList.contains('present')){ mm.style.display='none'; return; } if(!$$('.clip:not(.ghost)',tr).length){ mm.style.display='none'; return; } mm.style.display='block'; const W=tr.scrollWidth*state.zoom||1, H=tr.scrollHeight*state.zoom||1; const sx=220/W, sy=64/H; let h=''; $$('.clip:not(.ghost)',tr).forEach(c=>{ const x=c.offsetLeft*state.zoom+ (c.closest('.track')?.querySelector('.h')?.offsetWidth||0)*state.zoom, y=(c.closest('.track')?.offsetTop||0)*state.zoom; h+=''; }); h+=''; mm.innerHTML=h; mm.onclick=e=>{ const r=mm.getBoundingClientRect(); tls.scrollLeft=(e.clientX-r.left)/sx-tls.clientWidth/2; }; }\n$('#tls').addEventListener('scroll',()=>requestAnimationFrame(minimap));\n/* wires: horizontal tangents; when the target is behind, swing wide below */\nfunction wirePath(ax,ay,bx,by){ const dx=bx-ax; if(dx>=0){ const c=Math.max(48,Math.min(160,dx*0.45)); return 'M'+ax+' '+ay+' C '+(ax+c)+' '+ay+','+(bx-c)+' '+by+','+bx+' '+by; }\n // the target is behind: out to the right, down to a lane below both, back left, up into the in-port\n const c=56, my=Math.max(ay,by)+38; return 'M'+ax+' '+ay+' C '+(ax+c)+' '+ay+','+(ax+c)+' '+my+','+(ax)+' '+my+' L '+(bx)+' '+my+' C '+(bx-c)+' '+my+','+(bx-c)+' '+by+','+bx+' '+by; }\nfunction portXY(clipEl,side){ const el=$('#tracks'); const r=el.getBoundingClientRect(),c=clipEl.getBoundingClientRect(); return [ (side==='in'?c.left:c.right)-r.left, (c.top+c.height/2-r.top) ].map(v=>v/state.zoom); }\nfunction startConnect(fromId,ev){ const el=$('#tracks'); const svg=el.querySelector('svg.wires'); if(!svg) return; const A=$('.clip[data-job=\"'+fromId+'\"]'); if(!A) return; const [ax,ay]=portXY(A,'out'); const p=document.createElementNS('http://www.w3.org/2000/svg','path'); p.setAttribute('class','rubber'); svg.appendChild(p); el.classList.add('connecting'); document.body.style.cursor='crosshair'; let over=null;\n const mv=e=>{ const r=el.getBoundingClientRect(); const mx=(e.clientX-r.left)/state.zoom, my=(e.clientY-r.top)/state.zoom; const t=document.elementFromPoint(e.clientX,e.clientY)?.closest('.clip'); if(over&&over!==t) over.classList.remove('target'); over=(t&&t.dataset.job!==fromId)?t:null; if(over){ over.classList.add('target'); const [bx,by]=portXY(over,'in'); p.setAttribute('d',wirePath(ax,ay,bx,by)); } else p.setAttribute('d',wirePath(ax,ay,mx,my)); };\n const up=e=>{ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); p.remove(); el.classList.remove('connecting'); document.body.style.cursor=''; if(over){ over.classList.remove('target'); const to=over.dataset.job; state.connectMode=null; sheetHandoff(fromId,to); } else { state.connectMode=null; } };\n document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up); if(ev) mv(ev); }\nfunction selbar(){ const b=$('#selbar'); const ids=[...state.multi].filter(id=>job(id)&&!job(id).parent); if(ids.length<2){ b.classList.remove('show'); return; } const els=ids.map(id=>$('.clip[data-job=\"'+id+'\"]')).filter(Boolean); if(!els.length){ b.classList.remove('show'); return; } const r=$('#tls').getBoundingClientRect(); let l=1e9,t=1e9; els.forEach(x=>{const bb=x.getBoundingClientRect(); l=Math.min(l,bb.left); t=Math.min(t,bb.top);}); b.style.left=(l-r.left+$('#tls').scrollLeft)+'px'; b.style.top=(t-r.top+$('#tls').scrollTop-40)+'px';\n const kinds=new Set(ids.map(id=>trackOf(job(id)).kind)); const joint=kinds.has('agent')&&kinds.has('person');\n b.innerHTML=''+(joint?'':'')+''; b.classList.add('show'); }\nfunction jointWork(){ const ids=[...state.multi]; const ag=ids.map(job).find(j=>trackOf(j).kind==='agent'); const pe=ids.map(job).find(j=>trackOf(j).kind==='person'); if(!ag||!pe) return; sheet('Joint work',[['what','What does the agent hand the person?','a recommendation, an assessment, a draft'],['rule','What does the person decide?','in words: \"approve above \u20AC5k\"']],async v=>{ const aid='a-'+Date.now(); await op([{t:'artifact',artifact:{id:aid,name:v.what||'a recommendation',kind:'document'}},{t:'updateJob',id:ag.id,patch:{outputs:[...(ag.outputs||[]),aid]}},{t:'updateJob',id:pe.id,patch:{inputs:[...(pe.inputs||[]),aid],trigger:'hand',gate:{rule:v.rule||'a person decides',accountable:pe.track}}}]); state.multi.clear(); select(pe.id,'job'); },'ph-scales'); }\nasync function deleteJob(id){ const j=job(id); if(!j) return; const ks=kids(id); if(!(await confirmSheet('Delete \"'+esc(j.name)+'\"?',(ks.length?ks.length+' task'+(ks.length>1?'s':'')+' inside go with it. ':'')+'Handoffs to and from it are removed. \u2318Z brings it back.'))) return; await op([{t:'removeJob',id}]); select(null,'board'); }\nasync function removeMulti(){ const n=state.multi.size; if(!(await confirmSheet('Delete '+n+' job'+(n>1?'s':'')+'?','Their tasks go with them. \u2318Z brings them back.'))) return; await op([...state.multi].map(id=>({t:'removeJob',id}))); state.multi.clear(); }\nfunction groupJob(){ const ids=[...state.multi]; const tr=trackOf(job(ids[0])); sheet('Into one job',[['n','Job name','Describe the work in a short phrase'],['for','Who needs the result?','']],async v=>{ const id='j-'+Date.now(); await op([{t:'collect',id,name:v.n||'New job',track:tr.id,into:ids},{t:'updateJob',id,patch:{beneficiary:v.for||undefined}}]); state.multi.clear(); select(id,'job'); },'ph-rectangle'); }\nfunction collectAgent(){ const ids=[...state.multi]; const ag=state.board.tracks.find(t=>t.kind==='agent'&&!t.removed); if(!ag){ toast('Add an agent row first'); return; } sheet('For an agent',[['n','Job name','name the job by its outcome']],async v=>{ const id='j-'+Date.now(); await op([{t:'collect',id,name:v.n||'Agent job',track:ag.id,into:ids}]); state.multi.clear(); select(id,'job'); },'ph-robot'); }\n$('#tls').addEventListener('click',e=>{ if(state.justMarqueed){ state.justMarqueed=false; return; } if(!e.target.closest('.clip')&&!e.target.closest('#selbar')&&!e.target.closest('.rt')&&!e.target.closest('svg.wires path')){ state.multi.clear(); select(null,'board'); } });\nfunction fullTimeline(){ const on=$('#ws').classList.toggle('full'); $('#ws').style.gridTemplateRows=''; requestAnimationFrame(()=>{ timeline(); if(on) fit(); }); }\n/* hover peek: the content of a clip or a row, without opening it */\nfunction peekHtml(j){ const F=findings(j); const ks=kids(j.id); const t=trackOf(j); return '

'+esc(j.name)+'

'+esc(j.outcome||j.rationale||(isDraft(j)?'a sketch \u2014 nothing said yet':''))+'
'+(t?'
'+esc(t.name)+(j.beneficiary?' \u00B7 for '+esc(j.beneficiary)+'':'')+'
':'')+(j.trigger?'
'+esc(j.triggerNote||TRIG[j.trigger]?.[1]||j.trigger)+'
':'')+((j.doneWhen||[]).length?'
'+esc(j.doneWhen[0])+'
':'')+(j.gate?'
'+esc(j.gate.rule)+'
':'')+((j.tools||[]).length?'
'+j.tools.map(x=>esc(x.name)).join(', ')+'
':'')+(ks.length?'
'+ks.slice(0,8).map(k=>''+esc(k.name)+'').join('')+(ks.length>8?'+'+(ks.length-8)+'':'')+'
':'')+(F.length?'
'+esc(F[0].message.replace(/^\"[^\"]*\"[: ]*/,''))+(F.length>1?' \u00B7 +'+(F.length-1):'')+'
':''); }\nlet peekT=null; function peekOn(el,id){ el.addEventListener('mouseenter',e=>{ clearTimeout(peekT); peekT=setTimeout(()=>{ const j=job(id); if(!j||state.connectMode) return; const p=$('#peek'); p.innerHTML=peekHtml(j); icons(p); p.classList.add('show'); const r=el.getBoundingClientRect(); const x=Math.min(window.innerWidth-360,r.left); const y=r.bottom+10+p.offsetHeight>window.innerHeight?r.top-10-p.offsetHeight:r.bottom+10; p.style.left=x+'px'; p.style.top=Math.max(8,y)+'px'; },380); }); el.addEventListener('mouseleave',()=>{ clearTimeout(peekT); $('#peek').classList.remove('show'); }); el.addEventListener('mousedown',()=>{ clearTimeout(peekT); $('#peek').classList.remove('show'); }); }\nfunction zoom(d){ state.zoom=Math.min(1.6,Math.max(.5,state.zoom+d*.1)); $('#tracks').style.transform='scale('+state.zoom+')'; $('#tracks').style.transformOrigin='0 0'; timeline(); }\nfunction fit(){ state.zoom=Math.min(1,($('#tls').clientWidth-40)/$('#tracks').scrollWidth); $('#tracks').style.transform='scale('+state.zoom+')'; $('#tracks').style.transformOrigin='0 0'; timeline(); }\n/* ===== settling a guess, where it sits =====\n Every field staves supplied is offered back one at a time, because \"that's all right\" on four\n fields at once is how a person agrees to something they did not read. The whole-card button is\n there for the cosmetic cases, and it says how many it is agreeing to. */\nfunction settlePop(e,j){ e.preventDefault(); e.stopPropagation(); const a=authorOf(j); if(!a||!a.fields.length) return;\n const p=$('#pop'); p.innerHTML='
I made this upNothing you said settled these. Correct me and I will stop assuming it elsewhere.'\n +a.fields.map(f=>'').join('')\n +'
'\n +'
';\n const settle=async fields=>{ p.classList.remove('show'); await op(fields.map(f=>({t:'confirmField',id:j.id,field:f,by:'human'}))); toast(fields.length>1?fields.length+' settled \u2014 that part of the board is yours now':'Settled \u2014 that is yours now'); };\n $$('button[data-f]',p).forEach(b=>b.onclick=ev=>{ ev.stopPropagation(); settle([b.dataset.f]); });\n $$('button[data-all]',p).forEach(b=>b.onclick=ev=>{ ev.stopPropagation(); settle(a.fields.map(f=>f.field)); });\n $$('button[data-talk]',p).forEach(b=>b.onclick=ev=>{ ev.stopPropagation(); p.classList.remove('show'); select(j.id,'job'); interview(); });\n p.classList.add('show'); p.style.left=Math.min(window.innerWidth-p.offsetWidth-8,e.clientX)+'px'; p.style.top=Math.min(window.innerHeight-p.offsetHeight-8,e.clientY)+'px'; }\n/* ===== menus ===== */\nfunction ctx(e,items){ e.preventDefault(); e.stopPropagation(); const p=$('#pop'); p.innerHTML=items.map(it=>it==='\u2014'?'
':'').join(''); $$('button',p).forEach((b,i)=>{ const it=items.filter(x=>x!=='\u2014')[i]; b.onclick=event=>{ event.stopPropagation(); p.classList.remove('show'); it[2](event); }; }); p.classList.add('show'); p.style.left=Math.min(window.innerWidth-p.offsetWidth-8,e.clientX)+'px'; p.style.top=Math.min(window.innerHeight-p.offsetHeight-8,e.clientY)+'px'; }\nfunction colAt(e,lane){ const r=lane.getBoundingClientRect(); return Math.max(0,Math.round(((e.clientX-r.left)/state.zoom-X0)/COL)); }\nasync function addJobOn(tid,afterId){ const t=track(tid); const id='j-'+Date.now(); const ops=[{t:'job',job:{id,name:'New job',track:tid,trigger:t.kind==='person'||t.kind==='outside'?'hand':'chain',inputs:[],outputs:[],provenance:{source:'human',by:'human'},status:'draft'}}]; if(afterId) ops.push({t:'reorder',id,after:afterId}); await op(ops); state.opened.add(tid); select(id,'job'); sheetJob(id); }\nfunction laneMenu(e,t,lane){ const c=colAt(e,lane); const before=topJobs().filter(j=>j.track===t.id&&(state.board.columns?.[j.id]??0)<=c).pop(); const i=state.board.tracks.indexOf(t);\n ctx(e,[['A job here','ph-rectangle',()=>addJobOn(t.id,before?.id),'J'],['A region from the selection','ph-bookmark-simple',()=>sheetRegion(null)],'\u2014',['Someone above','ph-user-plus',()=>addTrack('person',i)],['Someone below','ph-user-plus',()=>addTrack('person',i+1)],['An agent below','ph-robot',()=>addTrack('agent',i+1)],['A system below','ph-database',()=>addTrack('system',i+1)]]); }\nfunction clipMenu(e,j){ ctx(e,[['Open','ph-arrows-out-simple',()=>select(j.id,'job'),'\u21B5'],['Focus','ph-arrows-in-simple',()=>enterFocus(j.id)],'\u2014',['Add a task','ph-squares-four',()=>addTask(j.id),'T'],['A decision here','ph-scales',()=>sheetGate(j.id),'G'],['Connect to\u2026','ph-arrow-right',()=>{ state.connectMode=j.id; toast('Drag to the job it hands to'); const A=$('.clip[data-job=\"'+j.id+'\"]'); const r=A.getBoundingClientRect(); startConnect(j.id,{clientX:r.right,clientY:r.top+r.height/2}); },'A'],['Raise something','ph-hand-pointing',()=>{select(j.id,'job');setTimeout(()=>$('#raise')?.focus(),50);},'R'],['Talk with Staves','ph-robot',()=>{select(j.id,'job');toggleAsk(true);}],'\u2014',['Delete','ph-x',()=>deleteJob(j.id),'\u232B']]); }\nfunction addMenu(e,pid){ const isTrack=pid&&track(pid); const isJob=pid&&job(pid); const items=isJob?[['A task','ph-squares-four',()=>addTask(pid)],['Decision and accountability','ph-scales',()=>sheetGate(pid)],['Add a tool','ph-wrench',()=>sheetTool(pid)]]:isTrack?[['A job','ph-rectangle',()=>addJobOn(pid)]]:[['A person','ph-user',()=>addTrack('person')],['An agent','ph-robot',()=>addTrack('agent')],['A system','ph-database',()=>addTrack('system')],['Outside','ph-globe-simple',()=>addTrack('outside')]]; ctx(e,items); }\nasync function addTrack(kind,i){ const id='t-'+Date.now(); sheet('Who is this?',[['n','Name or role',''],['m','One line','what they are for']],async v=>{ await op([{t:'track',track:{id,name:v.n||{person:'Someone',agent:'An agent',system:'A system',outside:'Outside'}[kind],kind,meta:v.m||undefined}}]); state.opened.add(id); },WHOI[kind]); }\n/* ===== focus ===== */\nfunction enterFocus(id){ const j=job(id); if(j&&j.boardRef){ location.href='./?board='+encodeURIComponent(j.boardRef); return; }\n state.focusStack.push(state.focus); state.focus=id; state.sel=null; history.replaceState(null,'',location.pathname+'?board='+encodeURIComponent(state.name)+'&focus='+encodeURIComponent(id)); load(); }\nfunction leaveFocus(){ state.focus=state.focusStack.pop()||null; state.sel=null; history.replaceState(null,'',location.pathname+'?board='+encodeURIComponent(state.name)+(state.focus?'&focus='+state.focus:'')); load(); }\n/* ===== sheets ===== */\nlet sheetReturnFocus=null;\nfunction sheet(title,fields,onDone,icon){\n const V=$('#veil');sheetReturnFocus=document.activeElement;\n V.innerHTML='
'+esc(title)+'
'+fields.map(([k,l,hint,val,type,opts],index)=>{\n const id='sheet-field-'+index;\n if(type==='note')return '
'+l+'
';\n const help=hint?'
'+esc(hint)+'
':'';\n const described=hint?' aria-describedby=\"'+id+'-help\"':'';\n if(opts)return '
'+esc(l)+''+help+'
'+opts.map(([v,i,t])=>'').join('')+'
';\n return '
'+help+(type==='area'?'':'')+'
';\n }).join('')+'

';V.classList.add('show');\n const form=$('.sheet',V);let saving=false;$('#sh-x').onclick=()=>{if(!saving)closeSheet();};\n $('.sheet',V).onsubmit=async e=>{e.preventDefault();if(saving)return;const values={};$$('[data-k]',V).forEach(el=>{if(el.type!=='radio'||el.checked)values[el.dataset.k]=el.value;});saving=true;$('#sh-ok').disabled=true;$('#sh-x').disabled=true;$('#sh-ok').textContent='Saving\u2026';$('#sheet-error').textContent='';\n try{await onDone(values);if(form.isConnected)closeSheet();}catch(error){$('#sheet-error').textContent=error.message||'Could not save. Your changes are kept here.';$('#sheet-error').focus();$('#sh-ok').textContent='Retry save';$('#sh-ok').disabled=false;$('#sh-x').disabled=false;}finally{saving=false;}};\n V.onkeydown=e=>{e.stopPropagation();if(e.key==='Escape'){e.preventDefault();if(!saving)closeSheet();}if(e.key==='Tab'){const controls=$$('input,textarea,button,select,[tabindex=\"0\"]',V).filter(x=>!x.disabled);const first=controls[0],last=controls.at(-1);if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}}};\n setTimeout(()=>$('input,textarea,button',V)?.focus(),0);\n}\nfunction closeSheet(){const V=$('#veil');if(V.dataset.saving==='true')return;V.classList.remove('show');V.innerHTML='';V.onkeydown=null;if(sheetReturnFocus?.isConnected)sheetReturnFocus.focus();sheetReturnFocus=null;}\nfunction sheetJob(id){ const j=job(id); sheet(j.name||'This job',[['n','Job name','Describe the work in a short phrase',j.name],['o','What should this achieve?','Describe the result for the person who needs it',j.outcome,'area'],['for','Who needs the result?','',j.beneficiary],['d','How will you know it is complete?','A completion criterion',(j.doneWhen||[])[0]],['trig','When does it start?',null,j.trigger||'hand',null,Object.entries(TRIG).map(([k,[i,l]])=>[k,i,l])],['pr','How many inputs are needed to start?', 'Applies to current inputs: '+((j.inputs||[]).map(id=>state.board.artifacts.find(a=>a.id===id)?.name||id).join(', ')||'none'),j.prerequisites?.kind||'unknown',null,[['unknown','','Not yet known'],['all','','All inputs'],['any','','Any one input'],['conditional','','Depends on a condition']]],['pc','Condition (when conditional)','Describe which inputs are required and when',j.prerequisites?.condition],['tn','Describe the trigger or exceptions','\"when the nightly import finishes\", \"when the balance drops below zero\"',j.triggerNote]],v=>{ if(v.pr==='conditional'&&!v.pc?.trim())throw new Error('Describe the condition for starting.'); if(v.pr==='any'&&!j.inputs.length)throw new Error('Add an input before choosing any one input.'); return op([{t:'updateJob',id,patch:{prerequisites:{kind:v.pr||'unknown',inputs:[...(j.prerequisites?.inputs||j.inputs||[])],...(v.pr==='conditional'?{condition:v.pc.trim()}:{})},name:v.n||j.name,outcome:v.o||undefined,beneficiary:v.for||undefined,doneWhen:v.d?[v.d,...(j.doneWhen||[]).slice(1)]:j.doneWhen,trigger:v.trig||j.trigger,triggerNote:v.tn||undefined}}]); },'ph-rectangle'); }\nfunction sheetTask(id){ const x=job(id); sheet(x.name||'This task',[['n','Task name','as you would tell a new colleague',x.name],['k','Work type (optional)','For example, research, negotiate, monitor or review',KIND[x.workKind]||x.workKind],['how','How is this task carried out?','Include exceptions, judgement and anything still unclear',x.rationale,'area'],['chk','What should be checked?','Describe one check; existing checks are kept',(x.checks||[])[0]?.rule]],v=>op([{t:'updateJob',id,patch:{name:v.n||x.name,workKind:Object.keys(KIND).find(k=>KIND[k]===v.k)||v.k||x.workKind,rationale:v.how||undefined,checks:v.chk?[{...(x.checks||[])[0],rule:v.chk},...(x.checks||[]).slice(1)]:x.checks}}]),KI(x.workKind)); }\nfunction sheetTool(id){ const x=job(id); sheet('Add a tool',[['n','What is it?',''],['reach','How can this tool be accessed?',null,'api',null,[['api','ph-plugs-connected','Through an API'],['mcp','ph-plugs-connected','Through MCP'],['screen','ph-monitor','Through its interface'],['none','ph-prohibit','No access available']]],['does','What does this tool provide?','as a person would say it'],['left','Limits or uncertainties','sections, older records, results past the first page \u2014 or \"unknown\"']],v=>op([{t:'updateJob',id,patch:{tools:[...(x.tools||[]),{name:v.n||'a tool',reach:v.reach||'api',does:v.does||undefined,limits:v.left||undefined}]}}]),'ph-wrench'); }\nfunction sheetGate(id){ const j=job(id); const people=state.board.tracks.filter(t=>!t.removed&&t.kind==='person'); sheet('Decision and accountability',[['rule','What decision needs to be made?','in words',j.gate?.rule],['who','Who is accountable for this decision?',null,j.gate?.accountable||'rule',null,[...people.map(t=>[t.id,'ph-user',t.name]),['rule','ph-scales','Owner not yet assigned']]]],v=>op([{t:'updateJob',id,patch:{gate:{...j.gate,rule:v.rule||'Decision not yet described',accountable:v.who||'rule'}}}]),'ph-scales'); }\nfunction sheetHandoff(a,b){ const A=job(a),B=job(b); sheet('What changes hands',[['name','From '+A.name+' to '+B.name+'\u2026','a document, data, a decision, a message'],['kind','Which is it?',null,'document',null,[['document','ph-file-text','a document'],['data','ph-table','data'],['decision','ph-check-square','a decision'],['message','ph-envelope','a message']]]],async v=>{ const id='a-'+Date.now(); await op([{t:'artifact',artifact:{id,name:v.name||'something',kind:v.kind||'document'}},{t:'updateJob',id:a,patch:{outputs:[...(A.outputs||[]),id]}},{t:'updateJob',id:b,patch:{inputs:[...(B.inputs||[]),id]}}]); select(b,'job'); },'ph-arrow-right'); }\nfunction sheetRegion(id){ const rg=id?state.board.regions.find(r=>r.id===id):null; const members=rg?rg.members:[...state.multi]; sheet(rg?'Edit outcome group':'Group under an outcome',[['n','Title','',rg?.name],['c','Colour',null,rg?.color||SW[(state.board.regions||[]).length%SW.length],null,SW.map(c=>[c,'',''])]],async v=>{ state.multi.clear(); await op([{t:'region',region:{id:rg?.id||'r-'+Date.now(),name:v.n||rg?.name||'Region',color:v.c||rg?.color||SW[0],members}}]); },'ph-bookmark-simple'); }\nfunction sheetSketch(){ sheet('Create a text outline',[['title','Workflow name','How a claim becomes a decision',state.board.title!==state.name?state.board.title:''],['sk','Use Role: Job, then -> for the next job. Put tasks on new lines starting with -. Mark decisions with [gate].',null,'claimant: files a claim\\nanna (adjuster): admit the claim -> assess the claim -> decide the claim [gate]\\n- find the policy\\n- check it is not a duplicate\\nto be built: pay or decline\\nclaimant: receives the outcome','area']],v=>applySketch(v.sk,v.title),'ph-text-align-left'); }\nasync function applySketch(txt,title){ const ops=[]; const tracks=new Map(); let last=null, prevOut=null; const b=state.board; if(title&&title!==b.title) ops.push({t:'board',id:state.name,title});\n txt.split('\\n').map(l=>l.trim()).filter(Boolean).forEach(l=>{ if(l.startsWith('-')){ if(last){ const tid=last+':t'+ops.length; ops.push({t:'job',job:{id:tid,name:l.slice(1).trim(),track:tracks.get(last)||b.tracks[0]?.id,parent:last,trigger:'chain',inputs:[],outputs:[],provenance:{source:'human',by:'human'},status:'confirmed'}}); } return; }\n const m=l.match(/^([^:]+):(.*)$/); if(!m) return; const who=m[1].trim(); const kind=/agent/i.test(who)?'agent':/built|system|service/i.test(who)?'system':/claimant|customer|client|user|shopper|requester/i.test(who)?'outside':'person'; let tid=[...tracks.entries()].find(([k,v])=>k==='who:'+who.toLowerCase())?.[1]; if(!tid){ tid=b.tracks.find(t=>t.name.toLowerCase()===who.toLowerCase())?.id||'t-'+ops.length+'-'+Date.now(); tracks.set('who:'+who.toLowerCase(),tid); if(!b.tracks.some(t=>t.id===tid)) ops.push({t:'track',track:{id:tid,name:who.replace(/\\((.*?)\\)/,'\u00B7 $1'),kind}}); }\n m[2].split('->').map(x=>x.trim()).filter(Boolean).forEach(seg=>{ const gate=/\\[gate\\]/.test(seg); const name=seg.replace(/\\[.*?\\]/g,'').trim(); const id='j-'+ops.length+'-'+Date.now(); const inputs=prevOut?[prevOut]:[]; const out='a-'+ops.length+'-'+Date.now(); ops.push({t:'artifact',artifact:{id:out,name:'from '+name,kind:'record'}}); ops.push({t:'job',job:{id,name:name.charAt(0).toUpperCase()+name.slice(1),track:tid,trigger:kind==='person'||kind==='outside'?'hand':'chain',inputs,outputs:[out],kind:kind==='outside'?'outside':undefined,gate:gate?{rule:'a person decides',accountable:tid}:undefined,provenance:{source:'human',by:'human'},status:'draft'}}); tracks.set(id,tid); prevOut=out; last=id; }); });\n if(!ops.length) return; await op(ops); toast(ops.filter(o=>o.t==='job').length+' jobs sketched'); }\nfunction scorecard(e){ const sc=state.board.scorecard||{}, base=state.board.baseScorecard; const p=$('#scpop'); const rows=[[\"Estimated human hours/week\",'humanHours',true],['Estimated agent hours/week','agentHours',false],['decisions a person makes','humanDecisions',false],['decisions a rule makes','ruleDecisions',true],['rules nobody owns','unownedRules',true],['waits with no bound','unboundedWaits',true],['exits going nowhere','danglingExits',true],['tools an agent can\\'t reach','unreachableTools',true],['outputs nobody checks','uncheckedOutputs',true],['jobs awaiting confirmation','unconfirmed',true]]; const fmt=v=>Number.isInteger(v)?v:(+v).toFixed(1);\n p.innerHTML='
Design estimates and checks'+(base?' \u00B7 '+baselineLabel(state.board):'')+'
'+rows.map(([l,k,lower])=>{ const v=sc[k]??0; const unknown=sc[k+'Unknown']||[]; let d=''; if(base&&!unknown.length&&!(base[k+'Unknown']||[]).length&&(!k.endsWith('Hours')||(!isNaN(Number(effortLabel(sc,k)))&&!isNaN(Number(effortLabel(base,k)))))){ const dd=v-(base[k]??0); if(Math.abs(dd)>=.05){ const good=(dd<0)===lower; d=''+(dd>0?'+':'\u2212')+fmt(Math.abs(dd))+(dd>0?' increase':' decrease')+''; } else d='same'; } return '
'+l+''+(k.endsWith('Hours')?effortLabel(sc,k):fmt(v))+''+d+'
'; }).join('')+(state.board.diff?'
Alternative changes'+diffList(state.board.diff)+'
':'')+'
'+(state.board.baseline?.pinned?'':'')+'
';\n p.classList.add('show'); p.style.left=Math.max(8,Math.min(window.innerWidth-p.offsetWidth-8,e.clientX-140))+'px'; p.style.top=Math.max(8,Math.min(window.innerHeight-p.offsetHeight-8,e.clientY+16))+'px'; e.stopPropagation(); }\ndocument.addEventListener('click',e=>{ if(!e.target.closest('.scpop')&&!e.target.closest('#m-h-w')) $('#scpop').classList.remove('show'); });\nfunction intentSheet(){ $('#scpop').classList.remove('show'); const it=state.board.intent||{}; sheet('What are you trying to improve?',[['primary','Primary measure (optional)',null,it.primary||'labor-hours',null,[['cycle-time','ph-clock','cycle time'],['labor-hours','ph-user',\"people's hours\"],['error-rate','ph-warning','errors'],['cost','ph-storefront','cost'],['throughput','ph-arrows-left-right','throughput'],['risk','ph-scales','risk']]],['target','Say the target in words','e.g. halve the analyst\\'s hours per lookup',it.target],['c','What needs to be protected?','Constraints or tradeoffs',(it.constraints||[])[0]]],v=>op([{t:'setIntent',intent:{primary:v.primary||'labor-hours',target:v.target||undefined,constraints:v.c?[v.c]:undefined}}]),'ph-target'); }\nfunction baselineLabel(b){ return b.baseline?.pinned?'Pinned baseline: '+esc(b.baseline.name):'Legacy unpinned comparison (source can change)'; }\nfunction diffValue(value){ if(value===undefined||value===null)return 'not specified'; if(Array.isArray(value))return value.length?value.map(diffValue).join(', '):'none'; if(typeof value==='object')return Object.entries(value).map(([k,v])=>k+': '+diffValue(v)).join('; '); return String(value); }\nfunction diffList(d){ const lines=[]; (d.added||[]).forEach(j=>lines.push('Added '+esc(j.name))); (d.removed||[]).forEach(j=>lines.push('Removed '+esc(j.name))); (d.moved||[]).forEach(m=>lines.push(esc(m.job.name)+' \u00B7 owner: '+esc(track(m.from)?.name||m.from)+' \u2192 '+esc(track(m.to)?.name||m.to))); (d.reparented||[]).forEach(m=>lines.push(esc(m.job.name)+' \u00B7 parent: '+esc(job(m.from)?.name||m.from||'top level')+' \u2192 '+esc(job(m.to)?.name||m.to||'top level'))); (d.renamed||[]).forEach(m=>lines.push('Name: '+esc(m.from)+' \u2192 '+esc(m.job.name))); (d.changed||[]).forEach(c=>c.fields.forEach(f=>lines.push(esc(c.job.name)+' \u00B7 '+esc(({doneWhen:'completion criteria',perWeek:'instances per week',minutes:'minutes per instance',triggerNote:'trigger details',workKind:'work type'})[f.field]||f.field.replace(/([A-Z])/g,' $1').toLowerCase())+': '+esc(diffValue(f.before))+' \u2192 '+esc(diffValue(f.after))))); (d.tracksAdded||[]).forEach(t=>lines.push('Added track '+esc(t.name))); (d.tracksChanged||[]).forEach(c=>lines.push('Track '+esc(c.before.name)+': '+esc(diffValue(c.before))+' \u2192 '+esc(c.after?diffValue(c.after):'removed'))); (d.artifactsChanged||[]).forEach(c=>lines.push('Artifact '+esc(c.after?.name||c.before?.name)+': '+esc(c.before?diffValue(c.before):'not present')+' \u2192 '+esc(c.after?diffValue(c.after):'removed'))); (d.contextChanged||[]).forEach(c=>lines.push('Board '+esc(c.field.replace(/([A-Z])/g,' $1').toLowerCase())+': '+esc(diffValue(c.before))+' \u2192 '+esc(diffValue(c.after)))); return lines.length?lines.map(line=>'
'+line+'
').join(''):'
No changes yet
'; }\n\nlet alternativeReviewLoading=false;\nfunction alternativeReviewContent(review){\n const value=v=>v===undefined?'Not specified':typeof v==='string'?v:JSON.stringify(v,null,2);\n const cell=v=>'
'+esc(value(v))+'
';\n return '

Accepting updates the intended design on '+esc(review.sourceBoard)+'. It does not implement or deploy changes. Existing implementation reports and execution evidence remain separate.

'+(review.changes.length?review.changes.map(change=>'
'+esc(change.entity)+' \u00B7 '+esc(change.id)+' \u00B7 '+esc(change.field==='*'?'Entire item':change.field)+'
Pinned baseline'+cell(change.before)+'
Alternative'+cell(change.after)+'
').join(''):'

No intended design changes to accept.

')+(review.conflicts.length?'
Reconcile these source changes before accepting'+review.conflicts.map(conflict=>'

'+esc(conflict.entity)+' \u00B7 '+esc(conflict.id)+' \u00B7 '+esc(conflict.field)+' \u2014 current source:

'+cell(conflict.current)).join('')+'
':'')+(review.problems.length?'
Resolve these references before accepting
    '+review.problems.map(problem=>'
  • '+esc(problem)+'
  • ').join('')+'
':'')+'
';\n}\nasync function reviewAlternative(){\n if(alternativeReviewLoading)return;\n alternativeReviewLoading=true;\n const board=state.name;\n $('#scpop').classList.remove('show');\n try{\n const response=await fetch('./alternative-review?board='+encodeURIComponent(board));\n if(!response.ok)throw new Error(await response.text()||'Could not load the design comparison.');\n const review=await response.json();\n if(state.name!==board)return;\n const canAccept=review.canAccept&&review.changes.length>0;\n sheet('Review intended changes',[['review',alternativeReviewContent(review),null,null,'note']],async()=>{\n if(!canAccept)throw new Error('Resolve the comparison before accepting.');\n const result=await fetch('./alternative-review?board='+encodeURIComponent(board),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({basis:review.basis})});\n if(!result.ok)throw new Error((await result.text()||'Could not accept this design.')+' Close this dialog and review again if the design changed.');\n const accepted=await result.json();\n location.href='./?board='+encodeURIComponent(accepted.sourceBoard);\n });\n const form=$('.sheet',$('#veil'));\n form.style.width='min(760px,calc(100vw - 32px))';\n form.style.maxHeight='calc(100dvh - 48px)';\n form.style.display='flex';form.style.flexDirection='column';\n const body=$('.sb',form);body.style.overflowY='auto';body.style.minHeight='0';\n $('#sh-ok').textContent='Accept intended design';$('#sh-ok').disabled=!canAccept;\n $('#sh-x').textContent='Close';\n const submit=form.onsubmit;\n form.onsubmit=async event=>{await submit(event);if(form.isConnected){$('#sh-ok').textContent='Accept intended design';$('#sh-ok').disabled=!canAccept;}};\n }catch(error){toast(error.message||'Could not load the design comparison.');}\n finally{alternativeReviewLoading=false;}\n}\n\nasync function branch(){ $('#scpop').classList.remove('show'); const base=state.name,title=state.board.title; sheet('Create alternative',[['n','Alternative name','a short id, e.g. agent-assesses']],async v=>{ const name=(v.n||'').trim().replace(/[^a-z0-9-]/gi,'-').toLowerCase(); if(!name)throw new Error('Give this alternative a name.'); const response=await fetch('./branch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({base,name,title:title+' \u2014 '+name})}); if(!response.ok)throw new Error(await response.text()||'Could not create alternative'); location.href='./?board='+encodeURIComponent(name); },'ph-git-diff'); }\nfunction patterns(){ const P=[['Ask once, then close','ph-hourglass','Wait up to seven days, then close an unanswered request',async(t)=>{ const id='j-'+Date.now(); await op([{t:'job',job:{id,name:'Ask once, then close',track:t,trigger:'chain',inputs:[],outputs:[],doneWhen:['answered within 7 days, else closed as unanswered'],exits:[{condition:'no answer in 7 days',target:'stop'}],provenance:{source:'human',by:'human'},status:'draft'}}]); select(id,'job'); }],['A person looks at 1 in 10','ph-eye','A person reviews a sample of the results',async(t)=>{ const id='j-'+Date.now(); const person=state.board.tracks.find(x=>x.kind==='person'&&!x.removed); await op([{t:'job',job:{id,name:'Review one in ten',track:t,trigger:'chain',inputs:[],outputs:[],gate:{rule:'a person looks at one in ten',accountable:person?.id||'rule'},provenance:{source:'human',by:'human'},status:'draft'}}]); select(id,'job'); }],['Try 3 times, then tell a person','ph-arrow-counter-clockwise','Retry up to three times, then ask a person to help',async(t)=>{ const id='j-'+Date.now(); const person=state.board.tracks.find(x=>x.kind==='person'&&!x.removed); await op([{t:'job',job:{id,name:'Try, then hand over',track:t,trigger:'chain',inputs:[],outputs:[],loop:{to:id,limit:3,then:'tell '+(person?.name||'a person')},provenance:{source:'human',by:'human'},status:'draft'}}]); select(id,'job'); }]];\n const tracks=state.board.tracks.filter(t=>!t.removed); const V=$('#veil'); V.innerHTML='
Patterns
Choose a track, then a pattern to add as an editable draft.
'+P.map((p,i)=>'
'+p[0]+''+p[2]+'
').join('')+'
'+tracks.map((t,i)=>''+esc(t.name)+'').join('')+'
'; V.classList.add('show');\n $$('#pat-t .opt').forEach(o=>o.onclick=()=>{$$('#pat-t .opt').forEach(x=>x.classList.remove('on'));o.classList.add('on');}); $$('.pat',V).forEach(el=>el.onclick=async()=>{ const t=$('#pat-t .opt.on')?.dataset.v; closeSheet(); await P[Number(el.dataset.i)][3](t); }); }\nfunction help(){ const h=$('#help'); h.innerHTML='

Keys and gestures

'+[['click / shift-click / marquee','select'],['\u21B5','focus on the job'],['R','Ask a question or flag a concern'],['A','connect to\u2026'],['G','a decision here'],['T','add a task'],['\u232B','delete'],['\u2318Z / \u21E7\u2318Z','undo / redo'],['?','this'],['esc','close'],['drag a row in the outline','Move to another job or track'],['drag a task card onto a lane','a new job there'],['drag from a clip\\'s port','what changes hands'],['right-click a lane or a clip','add or edit work']].map(([k,l])=>'
'+l+''+k+'
').join('')+'
'; h.classList.add('show'); h.onclick=e=>{ if(e.target===h) h.classList.remove('show'); }; }\nasync function exportJSON(){ const d=await (await fetch('./export.json?board='+encodeURIComponent(state.name))).json(); const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([JSON.stringify(d,null,2)],{type:'application/json'})); a.download=state.name+'.staves.json'; a.click(); }\nfunction importAny(){ const i=document.createElement('input'); i.type='file'; i.accept='.bpmn,.xml,.json'; i.onchange=async()=>{ const f=i.files[0]; if(!f) return; const text=await f.text(); const format=/\\.(bpmn|xml)$/i.test(f.name)||/<\\w*:?definitions/.test(text)?'bpmn':'json'; const r=await (await fetch('./import?board='+encodeURIComponent(state.name),{method:'POST',body:JSON.stringify({format,text})})).json(); await load(); toast('Imported '+r.ops+' changes from '+f.name+' \u2014 review the imported description on the canvas'); }; i.click(); }\nfunction exportBoard(){ const st=window.__staves?.store; if(!st) return; const txt=st.exportJsonl(state.name); const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([txt],{type:'application/x-ndjson'})); a.download=state.name+'.jsonl'; a.click(); toast('Saved '+state.name+'.jsonl \u2014 put it in .staves/ next to the code'); }\nfunction importBoard(){ const st=window.__staves?.store; if(!st) return; const i=document.createElement('input'); i.type='file'; i.accept='.jsonl'; i.onchange=async()=>{ const f=i.files[0]; if(!f) return; const name=f.name.replace(/\\.jsonl$/,''); st.importJsonl(name,await f.text()); location.search='?board='+encodeURIComponent(name); }; i.click(); }\nasync function chooseScope(v){ const q=state.board.questions.find(x=>x.id==='q-scope'); const ops=q?[{t:'answer',id:'q-scope',answer:v,by:'human'}]:[{t:'ask',question:{id:'q-scope',about:'board',askedBy:'agent',text:'How should staves proceed?',status:'raised',at:new Date().toISOString()}},{t:'answer',id:'q-scope',answer:v,by:'human'}]; await op(ops); toast('Chosen: '+v+' \u2014 in your agent, say: continue with staves'); }\n/* The connect sheet with the real commands lives in the editor shell (design/editor/shell.js). The\n single-file build has no shell, so say where the commands are rather than failing silently. */\nfunction connectHelp(reason){ if(window.stavesOpenConnectSheet) window.stavesOpenConnectSheet({reason:reason||''}); else toast('Run npx @staves/cli init in your project, then npx @staves/cli doctor.'); }\nconst interviewSessions=new Map();\nlet IV={lines:[],cards:[],busy:false,listening:false,speak:false,rec:null};\nfunction interview(scope){ // scope: 'board' | a job id | undefined (from selection)\n let jid=scope; if(!jid){ const j=(state.selKind==='job'||state.selKind==='task')?job(state.sel):null; jid=j?(j.parent||j.id):'board'; }\n const J=jid==='board'?null:job(jid); const el=$('#iv'); const title=J?J.name:state.board.title; const isBoard=!J;\n const sessionKey=state.name+':'+jid;\n if(IV.board && IV.job)interviewSessions.set(IV.board+':'+IV.job,IV);\n if(IV.board!==state.name||IV.job!==jid){IV.listening=false;IV.rec?.stop();IV=interviewSessions.get(sessionKey)||{board:state.name,job:jid,lines:state.board.comments.filter(c=>c.about===jid&&(/^\\[interview\\]/.test(c.text)||c.by==='interviewer')).map(c=>({who:c.by==='interviewer'?'interviewer':'person',text:c.text.replace(/^\\[interview\\]\\s*/,''),id:c.id})),cards:[],busy:false,listening:false,speak:false,rec:null};interviewSessions.set(sessionKey,IV);}\n IV.followLatest=IV.followLatest!==false;\n const canVoice=!!(window.SpeechRecognition||window.webkitSpeechRecognition); const canSpeak=!!window.speechSynthesis;\n const scopes=''+(isBoard?''+esc(title)+'':'the board \u203A '+esc(J.name)+'')+'';\n el.innerHTML='

Interview

about '+scopes+' \u00B7 '\n +'
Conversation options
'\n +'
'\n +'

'+esc(title)+'

'+(isBoard?(state.board.context?.purpose?esc(state.board.context.purpose):'Describe who needs a result and how the work happens. You can refine the resulting draft.'):(J.outcome?esc(J.outcome):'Describe the result for the person who needs it'))+'
'+IV.lines.map(l=>'
'+(l.who==='interviewer'?'Staves':'You')+''+esc(l.text)+'
').join('')+'
'\n +'
'+(canVoice?'':'Voice input is unavailable in this browser. You can type your responses.')+'
'\n +'
Drafted from the conversation
'+cardsHtml()+'
On the board
'+(isBoard?(topJobs().map(x=>'
'+esc(x.name)+'
'+esc(track(x.track)?.name||'')+' \u00B7 go deeper \u203A
').join('')||'
Nothing yet \u2014 describe the flow and it appears here.
'):(kids(jid).map(x=>'
'+esc(x.name)+'
'+esc(KIND[x.workKind]||'')+(x.tools?.length?' \u00B7 '+x.tools.map(t=>esc(t.name)).join(', '):'')+'
').join('')||'
No tasks yet.
'))+'
';\n $('#ivscope').onchange=e=>interview(e.target.value);\n $('#ivin').setAttribute('aria-label','Message to Staves');$('#ivin').value=IV.draft||'';\n $('#ivin').oninput=e=>{IV.draft=e.target.value;};\n $('#ivsend').onclick=()=>{if(IV.listening)finishVoiceTurn();else{const input=$('#ivin');if(!IV.busy&&input.value.trim()){IV.followLatest=true;say(input.value.trim());}}};$('#iv-latest').onclick=()=>scrollInterviewLatest(true);$('#ivscroll').onscroll=()=>{const sc=$('#ivscroll');const last=$('#ivlines')?.lastElementChild;IV.followLatest=!last||(last.getBoundingClientRect().bottom<=sc.getBoundingClientRect().bottom+80&&last.getBoundingClientRect().bottom>=sc.getBoundingClientRect().top-80);};\n $('#ivin').onkeydown=e=>{e.stopPropagation();if(e.key==='Enter'){e.preventDefault();$('#ivsend').click();}};\n el.classList.add('show');icons(el);paintInterviewState();setTimeout(()=>$('#ivin')?.focus(),50);\n}\nfunction paintInterviewState(){if(!$('#ivsend'))return;const mode=IV.busy?'thinking':IV.speaking?'speaking':IV.listening?'listening':'ready';if($('#ivacceptall'))$('#ivacceptall').disabled=IV.busy||!IV.cards.some(c=>!c.gone&&c.ops.length);$('#ivsend').disabled=IV.busy;$('#ivsend').textContent=IV.busy?'Thinking\u2026':IV.listening?'Finish & send':'Send';$('#ivin').disabled=IV.busy;if($('#ivmic')){$('#ivmic').disabled=IV.busy;$('#ivmic').setAttribute('aria-label',IV.listening?'Stop dictation without sending':'Dictate message');$('#ivmic').classList.toggle('on',!!IV.listening);}$('#iveng').textContent=IV.error||({thinking:(globalThis.stavesGenerating?'':'Thinking through your answer\u2026'),speaking:'Staves is speaking',listening:'Listening \u00B7 take your time',ready:IV.engine||'Ready'})[mode];$('#iveng').setAttribute('role','status');$('#ivstate').innerHTML=IV.error?''+esc(IV.error)+' ':IV.voiceError?esc(IV.voiceError):IV.listening?'Listening. Pauses do not send your message. Choose Finish & send when you are ready.':IV.busy?'Your message is saved. Staves is preparing a reply.':IV.speaking?'':'Type or dictate a message, then send it when you are ready.';if(typeof discussionActions==='function')discussionActions();}\nfunction scrollInterviewLatest(force=false){const sc=$('#ivscroll'),last=$('#ivlines')?.lastElementChild;if(!sc||!last)return;if(force||IV.followLatest!==false){const bottom=last.getBoundingClientRect().bottom-sc.getBoundingClientRect().top+sc.scrollTop;sc.scrollTop=Math.max(0,bottom-sc.clientHeight+24);if($('#iv-latest'))$('#iv-latest').hidden=true;}else if($('#iv-latest'))$('#iv-latest').hidden=false;}\nasync function sessionOp(session,ops,preconditions){const response=await fetch('./op?board='+encodeURIComponent(session.board),{method:'POST',body:JSON.stringify(preconditions===undefined?ops:{ops,preconditions})});if(!response.ok)throw new Error(opFailureMessage(response.status,await response.text()));if(state.name===session.board){try{await load();session.refreshError='';}catch(error){session.refreshError='Saved, but the board could not refresh. Reload to view the change.';toast(session.refreshError);}}}\nfunction opFailureMessage(status,body){if(status===409)return 'This suggestion is out of date. Ask Staves to suggest it again.';const text=String(body||'').trim();if(text.startsWith('{')){try{const error=JSON.parse(text).error;return typeof error==='string'&&error?error:'Could not save';}catch{}}return text.split('\\n')[0].replace(/^\\w*Error: /,'')||'Could not save';}\nasync function retryInterview(){if(IV.busy)return;const session=IV;session.busy=true;paintInterviewState();await ask(session.lastSaid,session);}\nasync function stopAndSketch(){const session=IV;if(session.accepting)return;const pending=session.cards.filter(c=>!c.gone&&c.ops.length);session.accepting=true;try{if(pending.length){if(pending.every(c=>c.preconditions!==undefined))await sessionOp(session,pending.flatMap(c=>c.ops),pending.flatMap(c=>c.preconditions));else for(const c of pending){await sessionOp(session,c.ops,c.preconditions);c.gone=true;c.accepted=true;}}pending.forEach(c=>{c.gone=true;c.accepted=true;});if(IV===session){$('#iv').classList.remove('show');stopDictation(session);if(typeof conversationPanel!=='undefined'){conversationOpen=false;conversationPanel.classList.remove('open');}}toast(pending.length?pending.length+' suggestions saved':'No new suggestions to save');}catch(error){toast('Suggestions not saved: '+error.message);}finally{session.accepting=false;}}\nfunction cardEffect(c){const names={gate:'decision and accountability',outcome:'intended result',beneficiary:'who needs the result',doneWhen:'completion criteria',checks:'checks',trigger:'start condition',triggerNote:'start details',tools:'tools',rationale:'task description',name:'name',track:'responsibility',inputs:'inputs',outputs:'outputs'};return (c.ops||[]).map(o=>{if(o.t==='job')return o.job.parent?'New task in \u201C'+(job(o.job.parent)?.name||o.job.parent)+'\u201D':'New job on '+(track(o.job.track)?.name||'its proposed role');if(o.t==='updateJob'){const current=job(o.id);return Object.keys(o.patch).map(field=>(current?.[field]?'Replaces ':'Adds ')+(names[field]||field)+' on \u201C'+(current?.name||o.id)+'\u201D').join('; ');}if(o.t==='track')return 'New role: '+o.track.name;return '';}).filter(Boolean).join(' \u00B7 ');}\nfunction cardAction(c){const created=(c.ops||[]).filter(o=>o.t==='job');if(created.length)return created.every(o=>o.job.parent)?'Add '+(created.length===1?'task':created.length+' tasks'):created.every(o=>!o.job.parent)?'Create '+(created.length===1?'job':created.length+' jobs'):'Add work';if((c.ops||[]).some(o=>o.t==='track'))return 'Add role';if((c.ops||[]).some(o=>o.t==='updateJob'))return 'Update job';if((c.ops||[]).some(o=>o.t==='setContext'))return 'Update workflow';return 'Apply change';}\nasync function revealAccepted(i){if(typeof suspendConversationVoice==='function')suspendConversationVoice();$('#iv').classList.remove('show');if(typeof conversationPanel!=='undefined'){conversationOpen=false;conversationPanel.classList.remove('open');}const c=IV.cards[i];const id=c?.ops.find(o=>o.t==='job')?.job.id||c?.ops.find(o=>o.t==='updateJob')?.id;const target=job(id);if(!target){toast('Change saved. Review the workflow details on the board.');return;}if(target.parent){enterFocus(target.parent);await load();}select(target.id,target.parent?'task':'job');}\nfunction cardsHtml(){ if(!IV.cards.length) return '
Suggestions appear here as you describe the work. Review them before adding them to the board.
'; const ICO={task:'ph-squares-four',job:'ph-rectangle',tool:'ph-wrench',wait:'ph-hourglass',gate:'ph-scales',exit:'ph-arrow-bend-up-right',case:'ph-briefcase',check:'ph-check-square',outcome:'ph-rectangle',beneficiary:'ph-user',doneWhen:'ph-check',context:'ph-target',who:'ph-user-plus'}; return IV.cards.map((c,i)=>'
'+esc(c.name)+''+esc(c.type==='context'?'context \u00B7 applied':c.type==='who'?'who \u00B7 added':IV.job==='board'&&c.type==='task'?'job':c.type==='task'?(KIND[c.kind]||'task'):c.type)+(c.confidence&&c.confidence!=='said'?' \u00B7 '+esc(c.confidence)+'':'')+'
'+'

'+esc(cardEffect(c))+'

'+(c.detail?'
'+esc(c.detail)+'
':'')+'
'+esc(c.quote)+'
'+(c.error?'

Not saved: '+esc(c.error)+'

':'')+(c.warning?'
'+esc(c.warning)+'
':'')+(c.accepted?'
Saved to workflow
':c.gone?'':'
'+(c.ops.length?'':'')+'
')+'
').join(''); }\nfunction notThis(i){const c=IV.cards[i];if(!c||c.pending)return;c.gone=true;$('#ivcards').innerHTML=cardsHtml();icons($('#ivcards'));}\nasync function acceptCard(i){const session=IV,c=session.cards[i];if(!c||!c.ops.length||c.pending)return;c.pending=true;c.error='';if(IV===session)$('#ivcards').innerHTML=cardsHtml();try{await sessionOp(session,c.ops,c.preconditions);c.gone=true;c.accepted=true;toast(session.refreshError||'Saved to workflow \u00B7 Show on board reveals the change');}catch(error){c.error=error.message;toast('Not saved: '+error.message);}finally{c.pending=false;if(IV===session){$('#ivcards').innerHTML=cardsHtml();paintInterviewState();}}}\nasync function say(text){const session=IV;if(session.busy)return;session.busy=true;session.rec?.stop();session.listening=false;session.error='';session.lastSaid=text;const line={who:'person',text,id:'c-'+Date.now(),delivery:'sending'};session.pendingLine=line;session.lines.push(line);session.draft='';if($('#ivin'))$('#ivin').value='';renderLines();paintInterviewState();await ask(text,session);}\nasync function ask(said,session=IV){if(!session.busy)session.busy=true;session.lastSaid=said;session.error='';if(IV===session)paintInterviewState();try{\n const line=session.pendingLine;if(line&&!line.saved){await sessionOp(session,[{t:'comment',comment:{id:line.id,about:session.job,by:'human',text:'[interview] '+line.text,at:new Date().toISOString()}}]);line.saved=true;}\n const response=await fetch('./interview?board='+encodeURIComponent(session.board),{method:'POST',signal:AbortSignal.timeout(125000),body:JSON.stringify({job:session.job,lines:session.lines.map(l=>({who:l.who,text:l.text})),said,suggestions:session.cards.map(c=>({type:c.type,name:c.name,quote:c.quote,state:c.accepted?'accepted':c.gone?'dismissed':'pending',about:c.ops.find(o=>o.t==='updateJob')?.id||c.ops.find(o=>o.t==='job')?.job.parent||'board'})),key:localStorage.getItem('staves:key')||undefined})});if(!response.ok)throw new Error(await response.text()||'Could not get a reply');const r=await response.json();if(r.engine==='none')throw new Error('No model available. Connect a model in Settings, then retry.');\n await sessionOp(session,[{t:'comment',comment:{id:'c-reply-'+Date.now(),about:session.job,by:'interviewer',text:r.reply,at:new Date().toISOString()}}]);session.cards=[...(r.cards||[]).map(c=>({...c})),...session.cards];session.lines.push({who:'interviewer',text:r.reply});if(line)line.delivery='sent';session.pendingLine=null;session.engine=r.via?'Connected to '+r.via:'Staves';if(IV===session&&session.speak&&$('#iv')?.classList.contains('show'))speak(r.reply,()=>{if(IV===session&&session.talk&&!session.busy&&!session.listening&&$('#iv')?.classList.contains('show'))toggleMic();});\n }catch(error){session.error=error.message||'Could not get a reply';if(session.pendingLine)session.pendingLine.delivery='failed';}finally{session.busy=false;if(IV===session){renderLines();if($('#ivcards'))$('#ivcards').innerHTML=cardsHtml();paintInterviewState();}}}\nfunction renderLines(){ const session=IV,follow=session.followLatest!==false; const el=$('#ivlines'); if(!el) return; el.innerHTML=session.lines.map((l,i)=>'
'+(l.who==='interviewer'?'Staves':'You')+''+esc(l.text)+(l.delivery==='failed'?' \u00B7 Response failed':l.delivery==='sending'?' \u00B7 Sending\u2026':'')+'
').join(''); $$('[data-line]',el).forEach(x=>{ x.onblur=async()=>{ const i=+x.dataset.line; const v=x.textContent.trim(); if(v&&v!==session.lines[i].text){ session.lines[i].text=v; if(session.lines[i].id) await sessionOp(session,[{t:'comment',comment:{id:session.lines[i].id,about:session.job,by:'human',text:'[interview] '+v,at:new Date().toISOString()}}]); } }; x.onkeydown=e=>{ if(e.key==='Enter'){ e.preventDefault(); x.blur(); } e.stopPropagation(); }; }); requestAnimationFrame(()=>{if(IV===session)scrollInterviewLatest(follow);}); }\nfunction toggleTalk(){const session=IV;session.talk=!session.talk;session.speak=session.talk;if(!session.talk){stopSpeaking();stopDictation(session);}else if(!session.busy&&!session.listening)toggleMic();paintInterviewState();const button=$('#iv .iv-options button');if(button)button.textContent=session.talk?'Stop voice conversation':'Start voice conversation';}\nfunction stopSpeaking(){window.speechSynthesis?.cancel();IV.speaking=false;paintInterviewState();}\nfunction speak(text,then){const session=IV;if(!window.speechSynthesis){then?.();return;}speechSynthesis.cancel();const utterance=new SpeechSynthesisUtterance(text);utterance.rate=1.02;session.speaking=true;if(IV===session)paintInterviewState();const done=()=>{session.speaking=false;if(IV===session){paintInterviewState();then?.();}};utterance.onend=done;utterance.onerror=()=>{session.speaking=false;if(IV===session)paintInterviewState();};speechSynthesis.speak(utterance);}\nfunction stopDictation(session=IV){session.listening=false;session.rec?.stop();if(IV===session)paintInterviewState();}\nfunction finishVoiceTurn(){const session=IV;if(session.busy)return;const text=($('#ivin')?.value||session.draft||'').trim();stopDictation(session);if(!text){session.voiceError='Nothing recorded yet. Try dictating again or type your message.';paintInterviewState();return;}session.followLatest=true;say(text);}\nfunction toggleMic(){const session=IV;if(session.busy)return;if(session.listening){stopDictation(session);return;}const SR=window.SpeechRecognition||window.webkitSpeechRecognition;if(!SR){session.voiceError='Voice input is unavailable in this browser. You can type instead.';paintInterviewState();return;}stopSpeaking();session.voiceError='';const rec=new SR();rec.lang=navigator.language||'en-US';rec.continuous=true;rec.interimResults=true;let committed=($('#ivin')?.value||session.draft||'').trim();let priorFinal=0;\n rec.onresult=event=>{if(IV!==session||!session.listening)return;let final='',interim='';for(let i=0;ipriorFinal){committed=(committed+' '+final.slice(priorFinal)).trim();priorFinal=final.length;}session.draft=(committed+' '+interim).trim();$('#ivin').value=session.draft;};\n rec.onend=()=>{if(IV===session&&session.listening&&!session.busy){priorFinal=0;try{rec.start();}catch(error){session.listening=false;session.voiceError='Dictation stopped. Your words are kept; send them or start dictating again.';paintInterviewState();}}};\n rec.onerror=event=>{if(event.error==='no-speech'||event.error==='aborted')return;session.listening=false;session.voiceError=({'not-allowed':'Microphone permission is blocked. Allow the microphone in your browser, or type your answer.','audio-capture':'No microphone found. You can type your answer.','network':'Dictation lost its connection. Your words are kept; send them or try again.'})[event.error]||'Dictation could not continue. Your words are kept.';if(IV===session)paintInterviewState();};\n session.rec=rec;session.listening=true;try{rec.start();}catch(error){session.listening=false;session.voiceError='Could not start the microphone: '+error.message;}paintInterviewState();}\nfunction keyGate(samplers){\n $('#iveng').textContent='Connect a model to continue';\n const el=$('#ivlines');if(!el)return;\n el.innerHTML='
StavesUse a model key, or connect a coding assistant that supports sharing its model.
';\n const input=$('#ivin');if(input){input.disabled=true;input.placeholder='Connect a model to start the conversation';}\n if(IV.talk)toggleTalk();\n}\nfunction keySheet(){ sheet('Your model key',[['k','Anthropic API key','Saved in this browser and sent to the Staves server for model requests',localStorage.getItem('staves:key')||'','password']],v=>{ if(v.k) localStorage.setItem('staves:key',v.k); else localStorage.removeItem('staves:key'); toast(v.k?'Key kept in this browser':'Key removed'); if(v.k){ IV.lines=IV.lines.filter(l=>l.who!=='staves'); const inp=$('#ivin'); if(inp){ inp.disabled=false; inp.placeholder='Ask a question or describe the work\u2026'; } if(!IV.lines.length) ask(null); } },'ph-key'); }\nasync function lineToTask(jid,cid){ const c=state.board.comments.find(x=>x.id===cid); const j=job(jid); if(!c||!j) return; const text=c.text.replace(/^\\[interview\\]\\s*/,''); const name=text.split(/[.;,\u2014-]/)[0].trim().slice(0,60); const id=jid+':t'+Date.now(); await op([{t:'job',job:{id,name:name||'Task',track:j.track,parent:jid,trigger:'chain',inputs:[],outputs:[],rationale:text,workKind:/look|find|search|check/i.test(text)?'look':/read|open/i.test(text)?'read':/decide|approve/i.test(text)?'decide':/write|draft/i.test(text)?'draft':/send|tell|email/i.test(text)?'tell':/wait/i.test(text)?'wait':'look',provenance:{source:'human',by:'human'},status:'confirmed'}}]); state.opened.add(jid); interview(); toast('Task made from their words \u2014 open it to say what kind of work it is'); }\nfunction importTranscript(){ const j=(state.selKind==='job'||state.selKind==='task')?job(state.sel):null; const key=j?j.id:'board'; sheet('Paste a transcript',[['tx','One line per turn. \"Name: what they said\". Lines without a name are theirs.',null,'','area']],async v=>{ const ops=v.tx.split('\\n').map(l=>l.trim()).filter(Boolean).map((l,i)=>{ const m=l.match(/^([^:]{1,40}):\\s*(.*)$/); const who=m?m[1].trim():''; const text=m?m[2]:l; const isQ=/^(staves|interviewer|you|me|q)$/i.test(who); return {t:'comment',comment:{id:'c-'+Date.now()+'-'+i,about:key,by:isQ?'interviewer':'human',text:(isQ?'':'[interview] ')+text,at:new Date().toISOString()}}; }); if(ops.length) await op(ops); interview(); },'ph-tray-arrow-down'); $('.sheet textarea').style.minHeight='220px'; }\n/* ===== the opening wizard: the context that guides everything ===== */\nlet WZ={step:0,v:{}};\nfunction wizard(){\n const context=state.board.context||{};\n WZ={step:0,editing:live().length>0||!!state.board.context,v:{...context,title:state.board.title!==state.name?state.board.title:'',purpose:context.purpose||state.board.goal||'',outside:context.outside||'',notes:context.notes||'',mustNot:context.mustNot||'',where:''}};\n wzRender();\n}\nfunction wzRender(){\n const el=$('#wz'),v=WZ.v,contextStep=WZ.editing||WZ.step===1;\n let body='';\n if(!contextStep)body='

Where would you like to begin?

Choose a starting point. You can use the other approaches as you go.

'+[\n ['code','ph-robot','Explore an existing project','Connect your coding assistant to help describe the work the project supports.'],\n ['people','ph-microphone','Talk through the work','Describe what someone needs to achieve and build the picture together.'],\n ['drawn','ph-rectangle','Start on the canvas','Add roles, jobs and connections yourself.']\n ].map(([k,i,title,description])=>'').join('')+'
';\n else body='

'+(WZ.editing?'About this workflow':'What are we exploring?')+'

A little context helps. Use your own words; leave anything you do not know yet blank.

More context optional
';\n const verb=WZ.editing?'Save context':contextStep?({code:'Connect project',people:'Start conversation',drawn:'Open canvas'}[v.where]||'Open canvas'):'Continue';\n el.innerHTML='
'+body+'

'+(!WZ.editing&&contextStep?'':'')+'
';\n el.classList.add('show');icons(el);\n const grab=()=>{for(const [key,id] of [['title','title'],['purpose','purpose'],['outside','outside'],['mustNot','mustnot'],['notes','notes']]){const input=$('#wz-'+id);if(input)v[key]=input.value.trim();}};\n $$('[data-where]',el).forEach(button=>button.onclick=()=>{v.where=button.dataset.where;wzRender();});\n $('#wz-cancel').onclick=()=>el.classList.remove('show');\n if($('#wz-back'))$('#wz-back').onclick=()=>{grab();WZ.step=0;wzRender();};\n $('#wz-next').onclick=async()=>{grab();if(!contextStep){WZ.step=1;wzRender();return;}const button=$('#wz-next');button.disabled=true;try{await wzFinish();}catch(error){$('#wz-error').textContent='Could not save this context. '+error.message;button.disabled=false;}};\n el.onkeydown=e=>{if(e.key==='Escape')el.classList.remove('show');};\n if(contextStep)setTimeout(()=>$('#wz-title')?.focus(),0);\n}\nasync function wzFinish(){\n const v=WZ.v,b=state.board,{title,where,...context}=v;\n const ops=[{t:'setContext',context:{...b.context,...context,...(!WZ.editing&&where?{where}:{})}}];\n if(title!==b.title||v.purpose!==b.goal)ops.unshift({t:'board',id:state.name,title:title||b.title,goal:v.purpose});\n // Describing beneficiaries does not assign them a performer type or create tracks.\n const response=await fetch('./op?board='+encodeURIComponent(state.name),{method:'POST',body:JSON.stringify(ops)});\n if(!response.ok)throw new Error('Please try again.');\n await load();$('#wz').classList.remove('show');localStorage.setItem('staves:wz:'+state.name,'1');\n if(!WZ.editing){if(where==='code')connectHelp('This board starts from the code, which needs a connected agent.');else if(where==='people')interview('board');}\n}\nfunction reflHtml(rs){ if(!rs.length) return '
No concerns were found by these checks. Missing information or real-world behaviour may reveal others.
'; const L={logical:'Structure',functional:'Feasibility',goal:'Human outcome'}; return rs.map(x=>'
'+esc(x.title)+''+L[x.lens]+(x.source==='model'?' \u00B7 model':'')+'
'+esc(x.why)+(x.fix?' \u2192 '+esc(x.fix)+'':'')+'
').join(''); }\nfunction reflectNow(){ showReview(); setTimeout(()=>document.getElementById('rvref')?.scrollIntoView({behavior:'smooth'}),100); }\nasync function reflectDeeper(){ const btn=$('#rvdeeper'); btn.disabled=true; btn.innerHTML='thinking\u2026'; try{ const r=await (await fetch('./reflect?board='+encodeURIComponent(state.name),{method:'POST',body:JSON.stringify({key:localStorage.getItem('staves:key')||undefined})})).json(); $('#rvrefl').innerHTML=reflHtml(r.reflections); $$('#rvrefl [data-go]').forEach(x=>x.onclick=()=>{ $('#rv').classList.remove('show'); const j=job(x.dataset.go); if(j) select(j.id,j.parent?'task':'job'); }); icons($('#rvrefl')); btn.innerHTML=r.via==='rules'?'Connect a model for a deeper review':'Model review complete'; }catch(e){ btn.innerHTML='Review unavailable. Try again.'; } }\nfunction showReview(){ const r=state.board.review; if(!r) return; const el=$('#rv'); const cols=state.board.columns||{}; const ncol=Math.max(1,...topJobs().map(j=>(cols[j.id]??0)+1)); const w=760;\n el.innerHTML='
'+(state.presence.length?'Described by '+esc(state.presence.map(p=>p.name).join(', ')):'The review')+'

'+esc(r.title)+'

'+esc(r.goal||'')+' '+esc(r.summary)+'

'\n +'
'+state.board.tracks.filter(t=>!t.removed).map(t=>'
'+esc(t.name)+'
'+topJobs().filter(j=>j.track===t.id).map(j=>''+esc(j.name)+(findings(j).length?''+findings(j).length+'':'')+'').join('')+'
').join('')+'
'\n +'
'+r.facts.map(f=>'
'+esc(f.value)+'
'+esc(f.label)+'
').join('')+'
'\n +r.sections.map(sec=>'

'+esc(sec.title)+'

'+esc(sec.why)+'
'+sec.items.slice(0,8).map(it=>'
'+esc(it.name)+''+esc(it.text)+'open \u203A
').join('')+(sec.items.length>8?'
\u2026and '+(sec.items.length-8)+' more
':'')+'
').join('')\n +'

Questions about the whole workflow

Structure, feasibility and the human outcome
'+reflHtml(r.reflections||[])+'
'\n +(r.first.length?'

Suggested next steps

'+r.first.map((it,i)=>'
'+(i+1)+''+esc(it.text)+'
').join('')+'
':'')\n +'
';\n $$('[data-go]',el).forEach(x=>x.onclick=()=>{ el.classList.remove('show'); const j=job(x.dataset.go); if(j) select(j.id,j.parent?'task':'job'); }); el.classList.add('show'); icons(el); }\nfunction review(){ showReview(); }\n/* ===== keys ===== */\ndocument.addEventListener('keydown',e=>{ if(e.defaultPrevented||e.target.closest('button,select,[contenteditable],[role=dialog]'))return; if(e.key==='Escape'&&e.target.matches('input,textarea')){ e.target.blur(); const ov=['#help','#wz','#rv','#iv'].find(o=>$(o).classList.contains('show')); if(ov){ $(ov).classList.remove('show'); return; } if($('#veil').classList.contains('show')){ closeSheet(); return; } if($('#ask').classList.contains('open')){ toggleAsk(false); return; } return; } if(e.target.matches('input,textarea')) return; if((e.key==='Backspace'||e.key==='Delete')&&state.selKind==='handoff'){ deleteHandoff(); return; } const j=state.selKind==='job'&&job(state.sel); if((e.metaKey||e.ctrlKey)&&e.key==='z'){ e.preventDefault(); e.shiftKey?redo():undo(); return; } if(e.key==='?'){ help(); return; } if(e.key==='f'&&!e.metaKey&&!e.ctrlKey&&!(state.selKind==='job'&&job(state.sel))){ fullTimeline(); return; } if(e.key==='F'){ fullTimeline(); return; } if(e.key==='Escape'){ if(state.view!=='normal'){ $$('#views .ib')[0].click(); return; } if($('#ws').classList.contains('full')){ fullTimeline(); return; } const overlays=['#help','#wz','#rv','#iv']; const open=overlays.find(o=>$(o).classList.contains('show')); if(open){ $(open).classList.remove('show'); return; } if($('#scpop').classList.contains('show')||$('#pop').classList.contains('show')){ $('#scpop').classList.remove('show'); $('#pop').classList.remove('show'); return; } if($('#veil').classList.contains('show')){ closeSheet(); return; } if($('#ask').classList.contains('open')){ toggleAsk(false); return; } state.multi.clear(); state.connectMode=null; select(null,'board'); return; } if(!j) return; if(e.key==='Enter'){ enterFocus(j.id); } if(e.key==='r'){ e.preventDefault(); $('#raise')?.focus(); } if(e.key==='a'){ const A=$('.clip[data-job=\"'+j.id+'\"]'); if(A){ const r=A.getBoundingClientRect(); toast('Move to the job it hands to and click'); startConnect(j.id,{clientX:r.right,clientY:r.top+r.height/2}); } } if(e.key==='g') sheetGate(j.id); if(e.key==='t') addTask(j.id); if(e.key==='Backspace'||e.key==='Delete') deleteJob(j.id); });\ndocument.addEventListener('click',e=>{ if(!e.target.closest('.pop')) $('#pop').classList.remove('show'); });\nfunction toast(t,action){ const e=$('#toast'); e.textContent=t; e.style.cursor=action?'pointer':''; e.onclick=action?()=>{ e.style.display='none'; action(); }:null; e.style.display='block'; clearTimeout(e._t); e._t=setTimeout(()=>e.style.display='none',action?12000:2400); }\n$$('#views .ib').forEach(b=>b.onclick=()=>{ state.view=b.dataset.v; $$('#views .ib').forEach(x=>x.classList.toggle('on',x===b)); document.body.classList.toggle('present',state.view==='present'); document.body.classList.toggle('chairs',state.view==='chairs'); timeline(); if(state.view==='present') requestAnimationFrame(fit); else { $('#tracks').style.transform=''; state.zoom=1; timeline(); } toast(state.view==='present'?'For stakeholders \u2014 machinery folded, nothing to fix shown. Esc to come back.':state.view==='chairs'?'Who could hold each job: green = holds it, orange = an agent could':'The work'); });\nfunction render(){ header(); outline(); stage(); timeline(); $('#fb').classList.toggle('show',!!state.focus); if(state.focus) $('#fbn').textContent=state.board.title; }\n(()=>{ let y0,h0; $('#handle').onmousedown=e=>{ y0=e.clientY; h0=$('#tl').offsetHeight; document.onmousemove=ev=>{ const h=Math.max(140,Math.min(620,h0-(ev.clientY-y0))); $('#ws').style.gridTemplateRows='44px 1fr 4px '+h+'px'; }; document.onmouseup=()=>{document.onmousemove=null;}; }; })();\nif(window.__staves){ document.getElementById('fileops').style.display='inline-flex'; if(!qs.get('board')){ const boards=window.__staves.store.list(); history.replaceState(null,'',location.pathname+'?board='+encodeURIComponent(boards[0]||'my-board')); } }\nload().then(()=>{ liveFeed(); feedTick(); if(state.name&&!topJobs().length&&!state.board.context&&!localStorage.getItem('staves:wz:'+state.name)){ wizard(); } if(!state.name){ toast('No board yet'); } else if(qs.get('review')==='1'||(topJobs().length&&!topJobs().some(j=>j.status==='confirmed'))) showReview(); });\n"; export declare const APP2_HTML = "staves\n\n
\n
\n \n \n \n \n 0\n 0\n 0\n 0\n \u2014\n \n \n \n \n \n
\n
Outline
\n
\n
Board
The agent is describing
    \n \n
    \n
    \n
    Focused on \u2014 what comes in and goes out is at the edges
    Timeline
    \n
    \n
    \n";