codehere 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/application/use-cases/generate-final-report.js +86 -191
  2. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  3. package/dist/index.js +10 -6
  4. package/dist/infrastructure/cost/budget-config.js +1 -1
  5. package/dist/infrastructure/cost/spend-aggregator.js +1 -1
  6. package/dist/infrastructure/hooks/claude-sdk-hook.js +7 -1
  7. package/dist/infrastructure/orchestration/agents-config-seed.js +1 -1
  8. package/dist/infrastructure/orchestration/aider-adapter.js +4 -1
  9. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  10. package/dist/infrastructure/orchestration/claude-code-adapter.js +5 -1
  11. package/dist/infrastructure/orchestration/codex-adapter.js +4 -9
  12. package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
  13. package/dist/infrastructure/orchestration/failover-config.js +1 -1
  14. package/dist/infrastructure/orchestration/generic-cli-adapter.js +2 -2
  15. package/dist/infrastructure/orchestration/graphify-context.js +18 -13
  16. package/dist/infrastructure/orchestration/memory-injector.js +50 -2
  17. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +25 -25
  18. package/dist/infrastructure/orchestration/progress-aggregator.js +41 -6
  19. package/dist/infrastructure/orchestration/session-manager.js +18 -0
  20. package/dist/infrastructure/orchestration/session-persistence.js +2 -2
  21. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  22. package/dist/infrastructure/protocols/mcp-server-impl.js +66 -2
  23. package/dist/infrastructure/protocols/ui-page-v2.js +260 -40
  24. package/dist/infrastructure/protocols/verify-page.js +42 -0
  25. package/dist/infrastructure/storage/project-thread-mirror.js +1 -1
  26. package/dist/infrastructure/storage/project-thread-reader.js +1 -1
  27. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  28. package/dist/presentation/cli/commands/init-command.js +66 -55
  29. package/package.json +2 -1
@@ -662,14 +662,31 @@ const SHARED_STYLES = `
662
662
  .audit-line b { color: var(--fg-90); font-weight: 600; }
663
663
  .audit-line code { background: hsl(0 0% 9%); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
664
664
 
665
+ /* project-memory nudge · same quiet family as .audit-line (trust present-
666
+ but-quiet); .on = the post-click confirmation state */
667
+ .mem-nudge {
668
+ font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
669
+ padding: 6px 12px; margin: 8px 0;
670
+ background: rgba(0,0,0,0.2);
671
+ border-left: 2px solid var(--border); border-radius: 3px;
672
+ line-height: 1.55;
673
+ }
674
+ .mem-nudge .mem-btn {
675
+ background: none; border: 1px solid var(--border); border-radius: 4px;
676
+ color: var(--fg-70); font-family: var(--mono); font-size: 10.5px;
677
+ padding: 2px 8px; cursor: pointer; margin-left: 4px;
678
+ }
679
+ .mem-nudge .mem-btn:hover { border-color: var(--accent); color: var(--fg); }
680
+ .mem-nudge.on { border-left-color: rgba(40,200,64,0.4); color: var(--fg-70); }
681
+
665
682
  /* auditor persona · audit-chain + verifier + hook-badges amplified,
666
683
  summary muted (per demo v12 line 2462: "amplify audit-lines +
667
684
  hook-badges + trust-grades") */
668
- body.auditor .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
669
- body.auditor .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
670
- body.auditor .hook-badge { transform: scale(1.05); }
671
- body.auditor .file-chip .trust { font-weight: 700; padding: 2px 7px; }
672
- body.auditor .panel:has(p) { opacity: 0.7; }
685
+ body[data-persona="auditor"] .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
686
+ body[data-persona="auditor"] .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
687
+ body[data-persona="auditor"] .hook-badge { transform: scale(1.05); }
688
+ body[data-persona="auditor"] .file-chip .trust { font-weight: 700; padding: 2px 7px; }
689
+ body[data-persona="auditor"] .panel:has(p) { opacity: 0.7; }
673
690
 
674
691
  /* SHAPE: velocity · hide tool-call cards, summary prose, tokens, verifier · keep outcome + files */
675
692
  body.shape-velocity .tc-card,
@@ -683,6 +700,13 @@ const SHARED_STYLES = `
683
700
  body.shape-security .verifier-panel { box-shadow: 0 0 0 2px rgba(255,95,87,0.18); padding: 14px; }
684
701
  body.shape-security .hook-block-card { box-shadow: 0 0 0 2px rgba(255,95,87,0.3); }
685
702
 
703
+ /* Reshape shapes recompose the goal room too (slice 4): "just what shipped"
704
+ * / velocity hide per-turn meta; the audit/security shape amplifies the
705
+ * room's audit line. Reuses the existing body.shape-* + .p-eng/.audit-line. */
706
+ body.shape-ship-only #task-list .card-row2 .p-eng,
707
+ body.shape-velocity #task-list .card-row2 .p-eng { display: none !important; }
708
+ body.shape-security #task-list .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; font-size: 11.5px; padding: 8px 14px; }
709
+
686
710
  /* v0.8 S2.5 · Settings drawer · ported from demo v12 lines 178-192 + A-09
687
711
  * in the backlog. Right-side off-canvas panel · holds Context Sources +
688
712
  * future section-visibility toggles. */
@@ -936,6 +960,29 @@ const SHARED_STYLES = `
936
960
  box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
937
961
  }
938
962
 
963
+ /* Goal-first home · each project (goal) is a group; its tasks live inside.
964
+ * The home is organized by what you're working toward, not by status. */
965
+ .goal-group { margin: 0 0 22px; }
966
+ .goal-head {
967
+ display: flex; align-items: baseline; gap: 10px;
968
+ padding: 0 0 8px; margin: 20px 0 10px;
969
+ border-bottom: 1px solid var(--border);
970
+ }
971
+ .goal-group:first-child .goal-head { margin-top: 0; }
972
+ .goal-title { font-size: 15px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
973
+ .goal-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-left: auto; }
974
+ .goal-head { cursor: pointer; }
975
+ .goal-head:hover .goal-title { color: var(--accent); }
976
+ .goal-head:hover .goal-meta { color: var(--fg-70); }
977
+
978
+ /* The goal room · a single goal opened to its multi-agent turns, in order. */
979
+ .room-head { margin: 4px 0 14px; }
980
+ .room-back { background: transparent; border: 0; color: var(--fg-50); font-family: var(--mono); font-size: 11px; cursor: pointer; padding: 0; margin-bottom: 12px; }
981
+ .room-back:hover { color: var(--fg); }
982
+ .room-title { font-size: 20px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
983
+ .room-sub { font-family: var(--mono); font-size: 11px; color: var(--fg-40); margin-top: 5px; }
984
+ .turns-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; color: var(--fg-40); text-transform: uppercase; margin: 18px 0 12px; }
985
+
939
986
  /* Task card · glass surface · matches demo v12 .glass-card pattern.
940
987
  * Faint white-on-dark fill + 1px low-contrast border · sits on top of
941
988
  * the ambient dot-grid. Hover lifts 2px with a bigger shadow + inner
@@ -1515,8 +1562,9 @@ const SHARED_STYLES = `
1515
1562
  .input-ta::placeholder { color: var(--fg-40); }
1516
1563
  .input-ta:disabled { color: var(--fg-30); }
1517
1564
  .input-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-40); white-space: nowrap; display: inline-flex; gap: 8px; align-items: center; }
1518
- .input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; }
1565
+ .input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; background: transparent; border: none; font: inherit; -webkit-appearance: none; appearance: none; }
1519
1566
  .input-meta .pick-agent:hover { background: rgba(255,255,255,0.05); color: var(--fg); }
1567
+ .input-meta select.pick-agent option { background: var(--bg-2, #1a1a1f); color: var(--fg); }
1520
1568
  .input-meta .cost-est { color: var(--allow); padding: 2px 6px; border-radius: 3px; background: rgba(40,200,64,0.06); }
1521
1569
  .send-btn {
1522
1570
  background: var(--accent); color: #000; border: none; border-radius: 6px;
@@ -1536,7 +1584,11 @@ const SHARED_STYLES = `
1536
1584
  `;
1537
1585
  const APP_SCRIPT = `
1538
1586
  const _cached = {};
1539
- let _filter = 'all', _detail = null, _offline = false;
1587
+ let _filter = 'all', _detail = null, _room = null, _offline = false, _autoOpened = false;
1588
+ // Project-memory state for the open room · null = unknown (fetch in flight),
1589
+ // false = agents forget this project (show the nudge), true = memory already
1590
+ // on (stay quiet), 'just-on' = turned on from the nudge this visit (confirm).
1591
+ let _roomMemory = null;
1540
1592
  const ICONS = { queued:'\\u23F3', running:'\\u25CF', paused:'\\u23F8', blocked:'\\uD83D\\uDD12', done:'\\u2713', failed:'\\u2715', crashed:'!', killed:'\\u2298' };
1541
1593
  const STAT_ORDER = ['running','queued','paused','blocked','done','failed','crashed','killed'];
1542
1594
  const FILTERS = { all: s=>true, running: s=>['running','queued','paused','blocked'].includes(s.status), done: s=>['done','completed'].includes(s.status), failed: s=>['failed','crashed','killed','error'].includes(s.status) };
@@ -1668,12 +1720,14 @@ function codeCard(path, snippet, lang) {
1668
1720
  }
1669
1721
  function allS(d) { const sp=d.spawned||d.sessions||[], pe=d.persisted||[], seen=new Set(sp.map(s=>s.sessionId)); return [...sp,...pe.filter(p=>!seen.has(p.sessionId))]; }
1670
1722
  function cardActions(s) {
1723
+ // Only 'kill' is wired (POST /api/task/:id/kill). Pause/Resume/Restart had NO
1724
+ // backend endpoint — they rendered as live buttons that silently no-op'd (R11
1725
+ // honest-state lie, 2026-06-03). Removed until real process control lands; Kill
1726
+ // and Cancel both map to the wired kill, so every rendered control here works.
1671
1727
  const m={
1672
- running:'<button class="card-btn pause" onclick="event.stopPropagation();a(\\'pause\\',\\''+s+'\\')">Pause</button><button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
1673
- paused:'<button class="card-btn" onclick="event.stopPropagation();a(\\'resume\\',\\''+s+'\\')">Resume</button><button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
1728
+ running:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
1729
+ paused:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
1674
1730
  queued:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
1675
- crashed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
1676
- failed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
1677
1731
  blocked:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
1678
1732
  };
1679
1733
  return m[mapped(s)]||'';
@@ -1691,11 +1745,101 @@ function mapped(s) { return s==='queued'?'running':s==='blocked'?'paused':aStat(
1691
1745
  function findS(id) { const s=allS(_cached.tasks||{}); return s.find(x=>x.sessionId===id)||null; }
1692
1746
 
1693
1747
  function render() {
1694
- if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; renderL(); } }
1748
+ if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; if(_room)renderRoom(); else renderL(); } }
1749
+ else if(_room) renderRoom();
1695
1750
  else renderL();
1696
1751
  upCnt(); upBud(); upSq(); upWk(); upIdx();
1697
1752
  }
1698
1753
 
1754
+ // The goal room — one goal opened to its tasks as a chronological thread of
1755
+ // agent turns. Renders into #task-list (same surface as the goals list).
1756
+ function openRoom(enc){
1757
+ _room=decodeURIComponent(enc||''); _detail=null;
1758
+ // Project-memory status for the nudge — fetched once per entry from the
1759
+ // daemon (the SAME readiness the injector acts on, not a guess).
1760
+ _roomMemory=null;
1761
+ (function(forCwd){
1762
+ fetch('/api/memory/status?cwd='+encodeURIComponent(forCwd)).then(x=>x.json()).then(j=>{
1763
+ if(_room===forCwd){ _roomMemory=!!j.ready; render(); }
1764
+ }).catch(()=>{});
1765
+ })(_room);
1766
+ document.getElementById('task-list-view').style.display='';
1767
+ document.getElementById('task-detail-view').style.display='none';
1768
+ document.body.classList.remove('detail-open');
1769
+ // Hand-off affordance: inside a room the composer targets THIS goal (cwd is
1770
+ // pinned in submit) and the picker defaults to whoever worked it last —
1771
+ // continuing is zero clicks, handing to a different agent is one visible
1772
+ // click. Set once on entry (not in renderRoom, which re-runs every poll and
1773
+ // would stomp the user's choice).
1774
+ const ta=document.getElementById('main-ta');
1775
+ if(ta) ta.placeholder='Add the next task in this goal\\u2026 any agent picks up where the last one left off';
1776
+ const ap=document.getElementById('agent-pick');
1777
+ if(ap){
1778
+ const turns=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'')&&(x.cwd||'')===_room&&x.agent).sort((a,b)=>(a.startedAt||0)-(b.startedAt||0));
1779
+ const raw=turns.length?String(turns[turns.length-1].agent):'';
1780
+ ap.value=['claude-code','codex','aider','gemini','opencode'].indexOf(raw)>=0?raw:'auto';
1781
+ }
1782
+ render();
1783
+ }
1784
+ function closeRoom(){
1785
+ _room=null; _roomMemory=null;
1786
+ const ta=document.getElementById('main-ta');
1787
+ if(ta) ta.placeholder='Describe a task or ask a question...';
1788
+ const ap=document.getElementById('agent-pick'); if(ap) ap.value='auto';
1789
+ render();
1790
+ }
1791
+ async function initRoomMemory(btn){
1792
+ if(!_room) return;
1793
+ if(btn){ btn.disabled=true; btn.textContent='turning on\\u2026'; }
1794
+ try{
1795
+ const r=await fetch('/api/memory/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cwd:_room})});
1796
+ if(r.ok){ _roomMemory='just-on'; render(); }
1797
+ else { const e=await r.json().catch(()=>({})); al('Could not turn on memory: '+(e.error||e.reason||'unknown')); if(btn){btn.disabled=false;btn.textContent='turn on project memory';} }
1798
+ }catch(e){ al('Network error: '+e.message); if(btn){btn.disabled=false;btn.textContent='turn on project memory';} }
1799
+ }
1800
+ function renderRoom(){
1801
+ const c=document.getElementById('task-list');
1802
+ const _hs=document.getElementById('host-sessions-strip'); if(_hs){ _hs.style.display='none'; }
1803
+ const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));
1804
+ const cwd=_room;
1805
+ const items=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'') && (x.cwd||'')===cwd);
1806
+ items.sort((a,b)=>(a.startedAt||0)-(b.startedAt||0)); // turns in the order they happened
1807
+ const projects=_cached.projects||[];
1808
+ const p=projects.find(x=>x.cwd===cwd);
1809
+ const title=(p&&p.title)||(cwd?(cwd.split('/').filter(Boolean).pop()||'Goal'):'Goal');
1810
+ const agents=[...new Set(items.map(x=>x.agent).filter(Boolean))];
1811
+ let html='<div class="room-head"><button class="room-back" onclick="closeRoom()">\\u2190 all goals</button>';
1812
+ html+='<div class="room-title">'+esc(title)+'</div>';
1813
+ // Persona-adaptive (direction B): everyone sees the plain-English outcome;
1814
+ // the technical path is engineer-only (.p-eng · hidden for PM/designer via
1815
+ // the existing body[data-persona] CSS — recomposes live, no re-render).
1816
+ const _done=items.filter(x=>x.status==='completed'||x.status==='done').length;
1817
+ const _outcome=items.length?(_done===items.length?('\\u2713 all '+items.length+' shipped'):(_done+' of '+items.length+' shipped')):'no tasks yet';
1818
+ html+='<div class="room-sub">'+esc(_outcome)+(agents.length?' \\u00b7 '+agents.map(esc).join(', '):'')+'<span class="p-eng"> \\u00b7 '+esc(cwd||'')+'</span></div></div>';
1819
+ if(items.length){
1820
+ html+='<div class="turns-label">How your agents worked this goal, in order</div>';
1821
+ for(const t of items) html+=cardH(t);
1822
+ // Auditor signal · subtle for everyone, amplified for the Auditor persona
1823
+ // via body[data-persona="auditor"] .audit-line. Honest: real step count,
1824
+ // and every step IS in the local hash-chained audit log.
1825
+ const _stepsTotal=items.reduce((n,t)=>n+((t.steps||[]).length),0);
1826
+ html+='<div class="audit-line allow" style="margin-top:14px">&#128274; every step in this goal is in the local audit chain \\u00b7 <b>'+_stepsTotal+' steps</b> \\u00b7 verify \\u21bb</div>';
1827
+ } else {
1828
+ html+='<div class="empty-state"><h2>No tasks in this goal yet.</h2></div>';
1829
+ }
1830
+ // Project-memory nudge (stranger-walk finding 2026-06-06): web-only users
1831
+ // never run codehere-init, so agents silently get NO memory of the
1832
+ // project. Honest + quiet: rendered ONLY when the daemon says memory is
1833
+ // off; the confirmation says "from the next task" because injection
1834
+ // happens at spawn — no fake instant effect.
1835
+ if(_roomMemory===false){
1836
+ html+='<div class="mem-nudge">&#129504; agents forget this project between runs \\u00b7 <button class="mem-btn" onclick="initRoomMemory(this)">turn on project memory</button></div>';
1837
+ } else if(_roomMemory==='just-on'){
1838
+ html+='<div class="mem-nudge on">\\u2713 project memory is on \\u2014 agents will remember this project from the next task</div>';
1839
+ }
1840
+ c.innerHTML=html;
1841
+ }
1842
+
1699
1843
  // C-01 · first-run indexing banner · auto-show when session count < 3 +
1700
1844
  // only once per browser session (sessionStorage tracks the dismissal).
1701
1845
  function upIdx(){
@@ -1773,39 +1917,66 @@ function fmtGap(ms){
1773
1917
 
1774
1918
  function renderL() {
1775
1919
  const c=document.getElementById('task-list'), all=allS(_cached.tasks||{});
1920
+ const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));
1776
1921
  const fl=all.filter(FILTERS[_filter]||FILTERS.all);
1777
1922
  let html='';
1778
- // Pin demo-shape sessions (id prefix 'demo-') to the very top — they
1779
- // exist to demonstrate what the product does · always relevant.
1923
+ // Demo-shape sessions (id prefix 'demo-') render AFTER the goals — they
1924
+ // demonstrate the product, but the home leads with YOUR goals (spec:
1925
+ // "Home is a list of your goals"; demos were burying it).
1780
1926
  const demo=fl.filter(x=>/^demo-/.test(x.sessionId||''));
1781
1927
  const rest=fl.filter(x=>!/^demo-/.test(x.sessionId||''));
1782
- if(demo.length){
1783
- html+='<div class="section-label featured">Featured</div>';
1784
- for(const t of demo) html+=cardH(t);
1785
- }
1786
- for(const g of STAT_ORDER){
1787
- const items=rest.filter(x=>aStat(x.status)===g||(g==='running'&&['running','queued'].includes(x.status))||(g==='failed'&&['crashed','killed','error'].includes(x.status)));
1788
- if(!items.length)continue;
1789
- const label={running:'Running',queued:'Running',paused:'Paused',blocked:'Blocked',done:'Done',failed:'Failed',crashed:'Failed',killed:'Failed'}[g]||g;
1790
- html+='<div class="section-label">'+label+'</div>';
1928
+ // Goal-first home: group the rest by project (the goal/room), not by status.
1929
+ // cwd → project title (from /api/projects); fall back to the folder name.
1930
+ const projects=_cached.projects||[];
1931
+ const titleFor=cwd=>{
1932
+ if(!cwd) return 'Ungrouped';
1933
+ const p=projects.find(x=>x.cwd===cwd);
1934
+ if(p&&p.title) return p.title;
1935
+ const parts=String(cwd).split('/').filter(Boolean);
1936
+ return parts.length?parts[parts.length-1]:'Ungrouped';
1937
+ };
1938
+ const groups=new Map();
1939
+ for(const t of rest){
1940
+ const key=t.cwd||'ungrouped';
1941
+ if(!groups.has(key)) groups.set(key,{cwd:t.cwd,items:[]});
1942
+ groups.get(key).items.push(t);
1943
+ }
1944
+ const lastTs=g=>g.items.reduce((m,x)=>Math.max(m,x.startedAt||0),0);
1945
+ const ordered=[...groups.values()].sort((a,b)=>lastTs(b)-lastTs(a));
1946
+ let prevGroupTs=null;
1947
+ for(const g of ordered){
1948
+ // Time-gap separator BETWEEN goals (ordered by recency) — a big gap means
1949
+ // these goals are from a different stretch of work ("last week").
1950
+ const gts=lastTs(g);
1951
+ if(prevGroupTs!==null && gts>0 && prevGroupTs>0){
1952
+ const gg=Math.abs(prevGroupTs-gts);
1953
+ if(gg>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gg)+'</span><span class="line"></span></div>'; }
1954
+ }
1955
+ prevGroupTs=gts;
1956
+ g.items.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
1957
+ const running=g.items.filter(x=>['running','queued','paused','blocked'].includes(x.status)).length;
1958
+ const meta=g.items.length+' task'+(g.items.length===1?'':'s')+(running?' · '+running+' running':'');
1959
+ html+='<div class="goal-group"><div class="goal-head" data-goal-cwd="'+encodeURIComponent(g.cwd||'')+'" onclick="openRoom(\\''+encodeURIComponent(g.cwd||'')+'\\')"><span class="goal-title">'+esc(titleFor(g.cwd))+'</span><span class="goal-meta">'+esc(meta)+' \\u00b7 open \\u2192</span></div>';
1960
+ // Time-gap separator within a goal · a big gap between consecutive tasks
1961
+ // in the same goal is meaningful ("picked this back up 3 days later").
1791
1962
  let prevTs=null;
1792
- for(const t of items){
1793
- // F-04 · time-gap separator · inject before this card when there's a
1794
- // > 2h gap between its startedAt and the previous card's. Skips for
1795
- // 'running' (still alive, no gap) and the first card (no predecessor).
1796
- const ts = t.startedAt || 0;
1797
- if(g!=='running' && prevTs!==null && ts>0 && prevTs>0){
1963
+ for(const t of g.items){
1964
+ const ts=t.startedAt||0;
1965
+ if(prevTs!==null && ts>0 && prevTs>0){
1798
1966
  const gap=Math.abs(prevTs-ts);
1799
- if(gap > 2*3600*1000){
1800
- html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>';
1801
- }
1967
+ if(gap>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>'; }
1802
1968
  }
1803
1969
  html+=cardH(t);
1804
1970
  prevTs=ts;
1805
1971
  }
1972
+ html+='</div>';
1973
+ }
1974
+ if(demo.length){
1975
+ html+='<div class="section-label featured">Featured</div>';
1976
+ for(const t of demo) html+=cardH(t);
1806
1977
  }
1807
1978
  if(!html){
1808
- const h={all:'No tasks yet. Type a task below to get started.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
1979
+ const h={all:'No goals yet. Type a task below to start one.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
1809
1980
  html='<div class="empty-state"><h2>'+(h[_filter]||'No tasks found.')+'</h2></div>';
1810
1981
  }
1811
1982
  c.innerHTML=html;
@@ -1814,6 +1985,8 @@ function renderL() {
1814
1985
  function renderHostSessions(data){
1815
1986
  const strip=document.getElementById('host-sessions-strip');
1816
1987
  if(!strip)return;
1988
+ // The observed-sessions strip belongs to the goals home, not inside a room.
1989
+ if(_room){ strip.style.display='none'; strip.innerHTML=''; return; }
1817
1990
  const sessions=(data&&data.sessions)||[];
1818
1991
  if(!sessions.length){ strip.style.display='none'; strip.innerHTML=''; return; }
1819
1992
  function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
@@ -1880,7 +2053,7 @@ function cardH(t) {
1880
2053
  const stalledHtml = t.stalledReason ? '<div class="card-stalled" title="the watchdog stopped this task because the agent went silent">'+he(t.stalledReason)+'</div>' : '';
1881
2054
  return '<div class="task-card '+agentClass+pinClass+'" onclick="showD(\\''+t.sessionId+'\\')">'
1882
2055
  +'<div class="card-row1">'+statIcon(s)+'<span class="card-title" title="'+displayPrompt(t.prompt).replace(/"/g,'&quot;').slice(0,400)+'">'+title(t.prompt)+'</span>'+tagHtml(t.tags||[])+agentChip+'</div>'
1883
- +'<div class="card-row2">'+metaParts.join('')+verB(t.verification)+outcomeHtml+'</div>'
2056
+ +'<div class="card-row2"><span class="p-eng">'+metaParts.join('')+'</span>'+verB(t.verification)+outcomeHtml+'</div>'
1884
2057
  +stalledHtml
1885
2058
  +(cardActions(t.sessionId)?'<div class="card-row3">'+cardActions(t.sessionId)+'</div>':'')+'</div>';
1886
2059
  }
@@ -2401,6 +2574,12 @@ function renderD(t) {
2401
2574
  // Empty finalReport object or null/empty summary => honest "unavailable" outcome,
2402
2575
  // not a green check. Composer must offer a re-spawn affordance for these cases.
2403
2576
  const hasReportText = !!(t.finalReport && typeof t.finalReport.summary === 'string' && t.finalReport.summary.trim());
2577
+ // §G R8 — green ✓ "Final Report" ONLY for a verified live-agent summary
2578
+ // (source === 'agent-summary'). Every other source — deterministic fallback,
2579
+ // error-recovery, mock synthesis — renders an honest ⚠, never a fake ✓.
2580
+ // POSITIVE allowlist (not deny-one) so a new/unknown source can't sneak a ✓.
2581
+ const reportSource = (t.finalReport && t.finalReport.source) || '';
2582
+ const isVerifiedAgentReport = reportSource === 'agent-summary';
2404
2583
  const isTerminal = ['completed','done','killed','failed','error','crashed'].includes(t.status);
2405
2584
  // The headline "N files changed" number. Normally derived from the actual
2406
2585
  // artifact list, so claim === rendered count, always. The optional
@@ -2415,8 +2594,13 @@ function renderD(t) {
2415
2594
  } else {
2416
2595
  oHtml+=(claimedFiles?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+claimedFiles+' file'+(claimedFiles===1?'':'s')+' changed</div>':'');
2417
2596
  oHtml+='<div class="outcome-item"><span class="'+(nStep>0?'check':'pending')+'">'+(nStep>0?ms(true):'\\u2022')+'</span> '+nStep+' step'+(nStep===1?'':'s')+'</div>';
2418
- if(hasReportText) {
2597
+ if(hasReportText && isVerifiedAgentReport) {
2419
2598
  oHtml+='<div class="outcome-item"><span class="check">'+ms(true)+'</span> Final Report</div>';
2599
+ } else if(hasReportText) {
2600
+ const _rl = reportSource==='deterministic' ? 'No written report \\u2014 recorded facts only'
2601
+ : reportSource==='error-recovery' ? 'Final Report \\u2014 task did not complete'
2602
+ : 'Final Report \\u2014 unverified (not a live-agent summary)';
2603
+ oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> '+_rl+'</div>';
2420
2604
  } else if(isTerminal) {
2421
2605
  oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> Final Report unavailable</div>';
2422
2606
  }
@@ -3190,9 +3374,28 @@ function al(m){const e=document.getElementById('toast');if(e){e.textContent=m;e.
3190
3374
  async function fetchAll() {
3191
3375
  const sk=document.getElementById('skeleton');if(sk){const p=sk.parentNode;if(p)p.removeChild(sk);}
3192
3376
  try {
3193
- const [t,r]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json())]);
3194
- _cached.tasks=t;_cached.readiness=r;
3377
+ const [t,r,pj]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json()),fetch('/api/projects').then(x=>x.json()).catch(()=>({projects:[]}))]);
3378
+ _cached.tasks=t;_cached.readiness=r;_cached.projects=(pj&&pj.projects)||[];
3195
3379
  if(t.pricing)_cached.pricing=t.pricing;
3380
+ // Auto-open the most relevant thread on first load (running > most-recent) —
3381
+ // landing on a bare list "did nothing" feel (Ega 2026-06-02). Once only, and
3382
+ // only if the user hasn't already navigated; Back still returns to the list.
3383
+ if(!_autoOpened){
3384
+ const _all=allS(t||{});
3385
+ if(_all.length){
3386
+ _autoOpened=true;
3387
+ if(_detail===null){
3388
+ // Demos never hijack first load — auto-open is for YOUR work
3389
+ // (a seeded demo stuck on "running" was stealing it · 2026-06-06 walk).
3390
+ const _real=_all.filter(x=>!/^demo-/.test(x.sessionId||''));
3391
+ const _act=_real.filter(x=>['running','queued','paused','blocked'].includes(x.status));
3392
+ const _pool=_act.length?_act:_real.slice();
3393
+ _pool.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
3394
+ const _pick=_pool[0];
3395
+ if(_pick&&_pick.sessionId){ showD(_pick.sessionId); }
3396
+ }
3397
+ }
3398
+ }
3196
3399
  render();
3197
3400
  }catch(e){console.error('fetch error',e);}
3198
3401
  try {
@@ -3221,7 +3424,20 @@ async function submit(ta,btn,taskId){
3221
3424
  // which is the disabled-composer bug's mirror image: composer enabled but
3222
3425
  // routed wrong.
3223
3426
  const url = taskId ? '/api/task/'+taskId+'/message' : '/api/task';
3224
- const body = taskId ? JSON.stringify({text:p}) : JSON.stringify({prompt:p});
3427
+ let body;
3428
+ if(taskId){ body=JSON.stringify({text:p}); }
3429
+ else {
3430
+ // Room-aware spawn: typing while a goal room is open adds the task TO
3431
+ // that goal (cwd pinned to the room) — before this, the room composer
3432
+ // silently spawned OUTSIDE the room, the same routing lie as the
3433
+ // follow-up bug above. Agent comes from the visible picker; 'auto'
3434
+ // omits it so the server's conductor default applies unchanged.
3435
+ const payload={prompt:p};
3436
+ if(_room) payload.cwd=_room;
3437
+ const ap=document.getElementById('agent-pick');
3438
+ if(ap&&ap.value&&ap.value!=='auto') payload.agent=ap.value;
3439
+ body=JSON.stringify(payload);
3440
+ }
3225
3441
  const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body});
3226
3442
  if(!r.ok){const e=await r.json();al('Failed: '+(e.error||'unknown'));btn.disabled=false;ta.disabled=false;_submitting=false;}else{
3227
3443
  ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;_submitting=false;ta.focus();},1200);
@@ -3946,6 +4162,7 @@ export const TASK_HTML_V2 = `<!doctype html>
3946
4162
  <option value="pm">PM view</option>
3947
4163
  <option value="designer">Designer view</option>
3948
4164
  <option value="eng">Engineer view</option>
4165
+ <option value="auditor">Auditor view</option>
3949
4166
  </select>
3950
4167
  <button class="density-toggle" id="density-toggle" title="Toggle compact / standard density">&#8862; Compact</button>
3951
4168
  <button class="reshape-btn" id="reshape-open" title="Describe the view you want · agent composes from primitives">⌘ Reshape view</button>
@@ -3997,7 +4214,6 @@ export const TASK_HTML_V2 = `<!doctype html>
3997
4214
 
3998
4215
  <div class="content">
3999
4216
  <div id="task-list-view" class="content-inner">
4000
- <div id="host-sessions-strip" style="display:none"></div>
4001
4217
  <div id="task-list">
4002
4218
  <div class="skeleton" id="skeleton">
4003
4219
  <div class="skeleton-card"></div>
@@ -4005,6 +4221,10 @@ export const TASK_HTML_V2 = `<!doctype html>
4005
4221
  <div class="skeleton-card"></div>
4006
4222
  </div>
4007
4223
  </div>
4224
+ <!-- Observed host sessions live BELOW the goals: the spec's home is
4225
+ "a list of your goals" — ambient observation must not bury the
4226
+ thesis surface (found burying it during the 2026-06-06 walk). -->
4227
+ <div id="host-sessions-strip" style="display:none"></div>
4008
4228
  </div>
4009
4229
  <div id="task-detail-view" style="display:none" class="content-inner">
4010
4230
  <div id="detail-view"></div>
@@ -4021,7 +4241,7 @@ export const TASK_HTML_V2 = `<!doctype html>
4021
4241
  <div class="input-area">
4022
4242
  <div class="input-bar">
4023
4243
  <textarea class="input-ta composer-bar" id="main-ta" placeholder="Describe a task or ask a question..." rows="1"></textarea>
4024
- <div class="input-meta composer-meta"><span class="pick-agent" title="Auto-pick · best agent for this task">↳ Auto</span><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
4244
+ <div class="input-meta composer-meta"><select class="pick-agent" id="agent-pick" title="Who does this task — Auto picks the best fit" aria-label="Pick the agent for this task"><option value="auto">↳ Auto</option><option value="claude-code">claude-code</option><option value="codex">codex</option><option value="aider">aider</option><option value="gemini">gemini</option><option value="opencode">opencode</option></select><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
4025
4245
  <button class="send-btn composer-send" id="main-btn">&#8593;</button>
4026
4246
  </div>
4027
4247
  <div class="kbd-hint">
@@ -298,6 +298,23 @@ export const VERIFY_HTML = `<!doctype html>
298
298
  { id:"F-TASK-CARD", name:"Task card", surface:"list", sel:".task-card", status:"shipped", tier:"v0.7" },
299
299
  { id:"F-TIME-GAP-PILL", name:"Brand pill + relative-time sub", surface:"list", sel:".time-gap", status:"shipped", tier:"v0.7" },
300
300
  { id:"F-WORKTREE-ALERT", name:"Worktree alert", surface:"list", sel:".worktree-alert", status:"partial", tier:"v0.7" },
301
+ /* goal-first home + goal room (v1 spec · shared-room hand-off). Goal groups
302
+ render from REAL non-demo tasks (same data dependency as F-TIME-GAP-PILL);
303
+ room features are observed by driving the REAL openRoom() on the first
304
+ goal-head's data-goal-cwd, then closeRoom() — same pattern as showD(). */
305
+ { id:"F-GOAL-GROUP", name:"Goal-first home · tasks grouped by goal", surface:"list", sel:".goal-group", status:"shipped", tier:"v1" },
306
+ { id:"F-GOAL-HEAD-OPEN", name:"Goal head opens its room (carries cwd)", surface:"list", sel:".goal-head[data-goal-cwd]", status:"shipped", tier:"v1" },
307
+ { id:"F-AGENT-PICK", name:"Composer agent picker (Auto + 5 agents)", surface:"list", sel:"select#agent-pick", status:"shipped", tier:"v1" },
308
+ /* room selectors are scoped to #task-list: the hidden detail view's DOM
309
+ persists (display:none still matches querySelectorAll) and also has an
310
+ .audit-line — unscoped, a dead room would false-OBSERVE off it. */
311
+ { id:"F-ROOM-HEAD", name:"Goal room header (title + outcome sub)", surface:"room", sel:"#task-list .room-head .room-title", status:"shipped", tier:"v1" },
312
+ { id:"F-ROOM-BACK", name:"Room back-to-all-goals affordance", surface:"room", sel:"#task-list .room-back", status:"shipped", tier:"v1" },
313
+ { id:"F-ROOM-TURNS", name:"Turns in order (how agents worked the goal)", surface:"room", sel:"#task-list .turns-label", status:"partial", tier:"v1" },
314
+ { id:"F-ROOM-AUDIT", name:"Room audit line (steps in local chain)", surface:"room", sel:"#task-list .audit-line", status:"partial", tier:"v1" },
315
+ /* partial by nature: renders ONLY when the daemon reports project memory
316
+ OFF for the opened room (initialized projects correctly show nothing). */
317
+ { id:"F-ROOM-MEM-NUDGE", name:"Project-memory init nudge (turn on memory)", surface:"room", sel:"#task-list .mem-nudge", status:"partial", tier:"v1" },
301
318
  { id:"M-OVERLAY-LINEAR", name:"Overlay-renderer Linear", surface:"list", sel:".or-overlay", status:"shipped", tier:"v0.9" },
302
319
  { id:"M-PRIMITIVES-NOTE", name:"Primitives note", surface:"list", sel:".primitives-note .prims .p", status:"shipped", tier:"v0.8" },
303
320
  { id:"M-RESHAPE-AGENT-COMPOSE", name:"Agent-compose animation", surface:"list", sel:".agent-compose .line", status:"shipped", tier:"v0.8" },
@@ -651,6 +668,31 @@ export const VERIFY_HTML = `<!doctype html>
651
668
  });
652
669
  });
653
670
  }, Promise.resolve());
671
+ }).then(function(){
672
+ // room-surface features · drive the REAL openRoom() (same pattern as
673
+ // showD above) on the first goal-head's data-goal-cwd, observe, close.
674
+ // No goal-head on the home = no real tasks; the room genuinely cannot
675
+ // be opened, so room features go MISSING/PARTIAL-GAP per their status —
676
+ // honest, and F-GOAL-GROUP fails alongside so the cause is visible.
677
+ var room = FEATURES.filter(function(f){ return f.surface==="room" && f.status!=="roadmap" && f.status!=="future"; });
678
+ if (!room.length) return;
679
+ var w0 = frameWin();
680
+ var gh = frameDoc().querySelector(".goal-head[data-goal-cwd]");
681
+ var check = function(){
682
+ var d = frameDoc();
683
+ room.forEach(function(f){
684
+ var present = d.querySelectorAll(f.sel).length > 0;
685
+ var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
686
+ covResults.push(r); paintCov(r);
687
+ });
688
+ };
689
+ if (!gh){ check(); return; }
690
+ try { w0.openRoom(gh.getAttribute("data-goal-cwd")); } catch(e){}
691
+ return sleep(200).then(function(){
692
+ check();
693
+ try { if (w0.closeRoom) w0.closeRoom(); } catch(e){}
694
+ return sleep(120);
695
+ });
654
696
  }).then(function(){
655
697
  // roadmap / future: SKIP (correctly absent), no observation
656
698
  FEATURES.filter(function(f){ return f.status==="roadmap" || f.status==="future"; }).forEach(function(f){
@@ -31,7 +31,7 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
31
31
  import { homedir } from 'os';
32
32
  import { join } from 'path';
33
33
  import { createHash } from 'crypto';
34
- const PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
34
+ const PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
35
35
  /** Match migration module's defaults. Same regex set kept in sync. */
36
36
  const TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
37
37
  function isTempDir(cwd) {
@@ -21,7 +21,7 @@
21
21
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
22
22
  import { join } from 'path';
23
23
  import { homedir } from 'os';
24
- const DEFAULT_PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
24
+ const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
25
25
  /** List all known projects with summary stats. Cheap; no full task content. */
26
26
  export function listProjects(options) {
27
27
  const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
@@ -24,7 +24,7 @@ import { existsSync, readdirSync, readFileSync, appendFileSync, statSync } from
24
24
  import { join } from 'path';
25
25
  import { homedir } from 'os';
26
26
  import { generateThreadTitle } from '../../application/use-cases/generate-thread-title.js';
27
- const DEFAULT_PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
27
+ const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
28
28
  function scanThread(slug, file) {
29
29
  let title = null;
30
30
  let firstPrompt = null;