trantor 0.17.13 → 0.17.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/catchup.mjs +56 -0
- package/bin/cli.mjs +2 -0
- package/bin/crew-runner.mjs +6 -1
- package/bin/crew.sh +5 -2
- package/hooks/handoff-now.mjs +21 -0
- package/hooks/heartbeat.mjs +53 -6
- package/hooks/lib/handoff.mjs +225 -0
- package/hooks/precompact.mjs +23 -95
- package/hooks/sessionstart.mjs +49 -9
- package/hub.mjs +174 -10
- package/lib/project.mjs +46 -0
- package/mcp.mjs +17 -6
- package/package.json +3 -2
- package/ui.html +131 -61
package/ui.html
CHANGED
|
@@ -107,9 +107,14 @@ main:not(.learn-open) .learn-body{display:none}
|
|
|
107
107
|
.sortmode:hover{color:var(--mut)}
|
|
108
108
|
.proj.idle .proj-h{opacity:.55;cursor:pointer}
|
|
109
109
|
/* flow (DAG) view */
|
|
110
|
-
|
|
111
|
-
.
|
|
112
|
-
.
|
|
110
|
+
/* view switcher — a clearly-labelled segmented control so it doesn't disappear next to the model chips */
|
|
111
|
+
.vtog{display:inline-flex;align-items:stretch;margin-left:12px;border:1px solid #34465f;border-radius:11px;overflow:hidden;background:#0c121e;flex:none}
|
|
112
|
+
.vtog .vlbl{display:flex;align-items:center;font-size:8.5px;font-weight:800;letter-spacing:.09em;color:var(--mut);padding:0 8px;text-transform:uppercase;background:#131c2c;border-right:1px solid #34465f}
|
|
113
|
+
.vbtn{font-size:10.5px;font-weight:800;letter-spacing:.04em;padding:4px 13px;border:0;border-left:1px solid #1f2839;background:transparent;color:#9fb0c4;cursor:pointer}
|
|
114
|
+
.vbtn:first-of-type{border-left:0}
|
|
115
|
+
.vbtn:hover{background:#172339;color:var(--tx)}
|
|
116
|
+
.vbtn.on{color:#04110e;background:var(--grn)}
|
|
117
|
+
.vbtn.on:hover{background:var(--grn);color:#04110e}
|
|
113
118
|
.flowwrap{padding:14px 16px;overflow:auto}
|
|
114
119
|
.flowwrap svg{display:block}
|
|
115
120
|
.fnode{cursor:pointer}
|
|
@@ -165,6 +170,34 @@ main:not(.learn-open) .learn-body{display:none}
|
|
|
165
170
|
.tlgl svg{flex:none}
|
|
166
171
|
.tlwrap .fhint{position:absolute;right:10px;bottom:6px;z-index:3}
|
|
167
172
|
.tlnode{cursor:pointer}
|
|
173
|
+
/* FLOW v2 = horizontal orchestrator-rooted flowchart TREE. A spine runs left→right; each phase is a
|
|
174
|
+
◇ plan node that fans OUT (drawn edges) to its crew/LLM card nodes, then converges INTO a ◆ integrate
|
|
175
|
+
node, which connects to the next phase. Agent is a node property, not the axis. Native horizontal scroll. */
|
|
176
|
+
.pflowwrap{background:#0b1019;border-top:1px solid var(--line)}
|
|
177
|
+
.finfo{background:#1a2030;border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:11px;color:var(--mut);margin:10px 14px 0}
|
|
178
|
+
.pflowwrap.gflow{position:relative;padding-bottom:22px}
|
|
179
|
+
.gscroll{overflow-x:auto;overflow-y:hidden;padding:4px 0 2px}
|
|
180
|
+
.gscroll svg{display:block}
|
|
181
|
+
.gedge{fill:none;stroke:#33405c;stroke-width:1.6;opacity:.8}
|
|
182
|
+
.gedge.done{stroke:var(--grn2);opacity:.5}
|
|
183
|
+
.gspine{fill:none;stroke:#5a6b86;stroke-width:2.5}
|
|
184
|
+
.gnode{cursor:default}
|
|
185
|
+
.gnode[data-id]{cursor:pointer}
|
|
186
|
+
.gnbox{fill:#101827;stroke:rgba(255,255,255,.12);stroke-width:1}
|
|
187
|
+
.gnode[data-id]:hover .gnbox{stroke:rgba(255,255,255,.55);fill:#162032}
|
|
188
|
+
.gnode.orch .gnbox{fill:#0e1730;stroke:#2c3a55;stroke-dasharray:4 3}
|
|
189
|
+
.gnode.failed .gnbox,.gnode.blocked .gnbox{stroke:var(--red)}
|
|
190
|
+
.gntext{fill:#eaf1fa;font-size:11px;font-family:ui-sans-serif,system-ui;pointer-events:none}
|
|
191
|
+
.gnsub{fill:var(--mut);font-size:9.5px;font-family:ui-sans-serif,system-ui;pointer-events:none}
|
|
192
|
+
.pfband{fill:#0c1320;opacity:.45}
|
|
193
|
+
.pfband.alt{fill:#0e1626;opacity:.55}
|
|
194
|
+
.pfbandtop{opacity:.12}
|
|
195
|
+
.pfbandtop.done{fill:var(--grn2)}.pfbandtop.active{fill:#f0b24b}.pfbandtop.failed,.pfbandtop.blocked{fill:var(--red)}.pfbandtop.planned{fill:#46566f}
|
|
196
|
+
.pflabel{font-size:16px;font-weight:800;font-family:ui-sans-serif,system-ui}
|
|
197
|
+
.pflabel.done{fill:var(--grn2)}.pflabel.active{fill:#f0b24b}.pflabel.failed{fill:var(--red)}.pflabel.planned,.pflabel.blocked{fill:#aeb9c8}
|
|
198
|
+
.pfcount{fill:var(--mut);font-size:10.5px;font-family:ui-sans-serif,system-ui}
|
|
199
|
+
.pfsub{fill:#9fb0c4;font-size:10.5px;font-family:ui-sans-serif,system-ui}
|
|
200
|
+
.pflowwrap .fhint{position:absolute;right:12px;bottom:5px}
|
|
168
201
|
/* card detail modal — the full story of one card: status journey + the agent's own bus reports */
|
|
169
202
|
.cmodal{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(4,7,12,.62)}
|
|
170
203
|
.cmpanel{background:var(--panel);border:1px solid var(--line);border-radius:14px;width:min(760px,93vw);max-height:84vh;display:flex;flex-direction:column;box-shadow:0 20px 64px rgba(0,0,0,.55)}
|
|
@@ -309,6 +342,7 @@ econ();setInterval(econ,15000);
|
|
|
309
342
|
function poolOf(session){const b=brandOf(session);const k=b==='anthropic'?'claude':b==='openai'?'codex':b==='moonshot'?'kimi':b;return POOLS[k]||'';}
|
|
310
343
|
const VIEWS = JSON.parse(localStorage.getItem("abViews") || "{}");
|
|
311
344
|
let HISTORY = {};
|
|
345
|
+
let PHASES = {};
|
|
312
346
|
function setView(proj, v){ VIEWS[proj] = v; localStorage.setItem("abViews", JSON.stringify(VIEWS)); render(); }
|
|
313
347
|
function toggleIdle(name){ if(!name)return; const s=new Set(JSON.parse(localStorage.getItem("abIdleOpen")||"[]")); s.has(name)?s.delete(name):s.add(name); localStorage.setItem("abIdleOpen",JSON.stringify([...s])); render(); }
|
|
314
348
|
let armedDel=null,armedTs=0; // pending ✕ confirmation (project name + when it was armed)
|
|
@@ -320,71 +354,104 @@ function moveProj(name,dir){
|
|
|
320
354
|
[names[i],names[j]]=[names[j],names[i]];
|
|
321
355
|
localStorage.setItem("abOrder",JSON.stringify(names)); render();
|
|
322
356
|
}
|
|
323
|
-
// FLOW =
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
357
|
+
// FLOW v2 = the project's TRUE shape: an orchestrator-rooted phase flowchart. The orchestrator is the
|
|
358
|
+
// spine (the left rail); each PHASE blooms — ◇ plan → the crew fans out to its cards → ◆ converge —
|
|
359
|
+
// then the next phase begins. Agent identity is a node badge, not the axis. Phases come from the hub
|
|
360
|
+
// (/phases: title-prefix + time-cluster; deps-sparse boards fall back to time + a visible notice).
|
|
361
|
+
// Each crew node keeps its full /card detail on click.
|
|
328
362
|
function flowHTML(pt, proj){
|
|
329
|
-
|
|
330
|
-
|
|
363
|
+
const data = PHASES[proj];
|
|
364
|
+
if (!data) return '<div class="pflowwrap"><div class="empty">loading flow…</div></div>';
|
|
365
|
+
if (!data.phases || !data.phases.length) return '<div class="pflowwrap"><div class="empty">no cards yet</div></div>';
|
|
331
366
|
const SCOL = { todo:'#3a4458', doing:'#4a90d9', testing:'#f59e0b', failed:'#ef6a6a', blocked:'#ef6a6a', done:'#14b8a6' };
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
367
|
+
const NW = 168, NH = 34, RG = 11, CG = 64, PGAP = 76, MT = 64, MB = 22;
|
|
368
|
+
const stackH = n => n*NH + Math.max(0,n-1)*RG;
|
|
369
|
+
// Per-phase layout: cards spread by their INTRA-PHASE dependency depth. Independent cards share
|
|
370
|
+
// column 0 (parallel, hanging off the plan node); a card that depends on another lands one column
|
|
371
|
+
// to the right of its parent (a branch/chain). Leaves (no dependents) converge into integrate.
|
|
372
|
+
const layouts = data.phases.map(ph => {
|
|
373
|
+
const work = [...ph.crew.map(n => ({ ...n, _orch:false })), ...ph.orchestrators.map(n => ({ ...n, _orch:true }))];
|
|
374
|
+
const cards = work.length ? work : [{ id:0, title:'(no cards)', status:ph.status, agent:'', synthetic:true, deps:[] }];
|
|
375
|
+
const idSet = new Set(cards.map(c => c.id)); const byId = {}; cards.forEach(c => byId[c.id] = c);
|
|
376
|
+
const parentsOf = c => (c.deps || []).filter(d => idSet.has(d) && d !== c.id);
|
|
377
|
+
const dmemo = {}; const depth = (c, seen) => {
|
|
378
|
+
if (dmemo[c.id] != null) return dmemo[c.id];
|
|
379
|
+
seen = seen || new Set(); if (seen.has(c.id)) return 0; seen.add(c.id);
|
|
380
|
+
const ps = parentsOf(c); const d = ps.length ? Math.max(...ps.map(p => depth(byId[p], seen))) + 1 : 0;
|
|
381
|
+
return (dmemo[c.id] = d);
|
|
382
|
+
};
|
|
383
|
+
cards.forEach(c => c._d = depth(c));
|
|
384
|
+
const maxDepth = Math.max(0, ...cards.map(c => c._d));
|
|
385
|
+
const cols = []; for (let d = 0; d <= maxDepth; d++) cols[d] = cards.filter(c => c._d === d);
|
|
386
|
+
const hasChild = c => cards.some(o => parentsOf(o).includes(c.id));
|
|
387
|
+
return { ph, cards, byId, parentsOf, hasChild, maxDepth, cols };
|
|
336
388
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
389
|
+
const maxColCount = Math.max(1, ...layouts.flatMap(L => L.cols.map(c => c.length)));
|
|
390
|
+
const Y0 = MT + stackH(maxColCount)/2; // shared horizontal spine midline
|
|
391
|
+
const totalH = MT + stackH(maxColCount) + MB;
|
|
392
|
+
const gnode = (x, y, title, status, id, orch, agent) => {
|
|
393
|
+
const stripe = SCOL[status] || '#3a4458';
|
|
394
|
+
return `<g class="gnode ${status}${orch?' orch':''}"${id?` data-id="${id}"`:''}>`
|
|
395
|
+
+ `<rect class="gnbox" x="${x}" y="${y}" width="${NW}" height="${NH}" rx="8"/>`
|
|
396
|
+
+ `<rect x="${x}" y="${y}" width="4" height="${NH}" rx="2" fill="${stripe}"/>`
|
|
397
|
+
+ `<text class="gntext" x="${x+11}" y="${y+(agent?15:23)}">${esc(title.slice(0,24))}${title.length>24?'…':''}</text>`
|
|
398
|
+
+ (agent?`<text class="gnsub" x="${x+11}" y="${y+29}">@${esc(agent)}</text>`:'')
|
|
399
|
+
+ `<title>${esc(title)}${agent?' · @'+esc(agent):''} — ${status}</title></g>`;
|
|
400
|
+
};
|
|
401
|
+
const gedge = (x1,y1,x2,y2,done) => { const mx=(x1+x2)/2;
|
|
402
|
+
return `<path class="gedge${done?' done':''}" d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}"/>`; };
|
|
403
|
+
let x = 26, svg = '', bands = '', prevIntRight = null, pi = 0;
|
|
404
|
+
for (const L of layouts){
|
|
405
|
+
const { ph, cards, byId, parentsOf, hasChild, maxDepth, cols } = L;
|
|
406
|
+
const planX = x, planY = Y0 - NH/2;
|
|
407
|
+
const colX = d => planX + (d+1)*(NW+CG);
|
|
408
|
+
const intX = colX(maxDepth + 1), intY = Y0 - NH/2;
|
|
409
|
+
// per-phase background band (alternating) so each phase reads as one region; collected separately
|
|
410
|
+
// so all bands paint BEHIND the edges/nodes. The header sits inside the band's top strip.
|
|
411
|
+
const bx = planX - 16, bw = (intX + NW + 16) - bx;
|
|
412
|
+
bands += `<rect class="pfband ${pi%2?'alt':''}" x="${bx}" y="2" width="${bw}" height="${totalH-4}" rx="10"/>`
|
|
413
|
+
+ `<rect class="pfbandtop ${ph.status}" x="${bx}" y="2" width="${bw}" height="${MT-12}" rx="10"/>`;
|
|
414
|
+
// header: phase label + the GOAL (explicit) or derived THEME — "what this phase is about"
|
|
415
|
+
const desc = (ph.goal || ph.theme || '').slice(0, Math.max(28, Math.floor(bw/7)));
|
|
416
|
+
svg += `<text class="pflabel ${ph.status}" x="${planX}" y="${MT-38}">${esc(ph.label)}</text>`
|
|
417
|
+
+ `<text class="pfcount" x="${planX}" y="${MT-38}" dx="${(esc(ph.label).length*9)+12}">${ph.status} · ${ph.total} card${ph.total>1?'s':''}</text>`
|
|
418
|
+
+ `<text class="pfsub" x="${planX}" y="${MT-21}">${esc(desc)}</text>`;
|
|
419
|
+
if (prevIntRight != null) svg += `<path class="gspine" marker-end="url(#garr)" d="M${prevIntRight},${Y0} L${planX},${Y0}"/>`;
|
|
420
|
+
svg += gnode(planX, planY, '◇ '+ph.label+' · plan', ph.status, null, true, '');
|
|
421
|
+
// position every card: x by dep-depth column, y stacked + centered on the spine within its column
|
|
422
|
+
const pos = {};
|
|
423
|
+
cols.forEach((colCards, d) => {
|
|
424
|
+
const startY = Y0 - stackH(colCards.length)/2;
|
|
425
|
+
colCards.forEach((c, k) => { pos[c.id] = { x: colX(d), y: startY + k*(NH+RG) }; });
|
|
426
|
+
});
|
|
427
|
+
for (const c of cards){
|
|
428
|
+
const p = pos[c.id], parents = parentsOf(c);
|
|
429
|
+
if (parents.length) for (const pid of parents){ const pp = pos[pid]; if (pp) svg += gedge(pp.x+NW, pp.y+NH/2, p.x, p.y+NH/2, byId[pid].status==='done'); }
|
|
430
|
+
else svg += gedge(planX+NW, planY+NH/2, p.x, p.y+NH/2, ph.status==='done'); // root → plan
|
|
431
|
+
svg += gnode(p.x, p.y, (c.id?'#'+c.id+' ':'')+c.title, c.status, c.synthetic?null:c.id, !!c._orch, c.agent||'');
|
|
432
|
+
if (!hasChild(c)) svg += gedge(p.x+NW, p.y+NH/2, intX, intY+NH/2, c.status==='done'); // leaf → integrate
|
|
368
433
|
}
|
|
369
|
-
svg +=
|
|
370
|
-
|
|
371
|
-
|
|
434
|
+
svg += gnode(intX, intY, '◆ integrate', ph.status, null, true, '');
|
|
435
|
+
prevIntRight = intX + NW;
|
|
436
|
+
x = intX + NW + PGAP;
|
|
437
|
+
pi++;
|
|
372
438
|
}
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
+ `<div class="
|
|
379
|
-
+ gut + `<div class="fhint">build order → · scroll left/right</div></div>`;
|
|
439
|
+
const totalW = x + 10;
|
|
440
|
+
const defs = `<defs><marker id="garr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#46566f"/></marker></defs>`;
|
|
441
|
+
const notice = data.sparse ? `<div class="finfo">⚠ inferred phases — few cards carry explicit tags (P1/P5…), so they're grouped by time + title. Prefix cards (e.g. "P6 …") for sharper phases.</div>` : '';
|
|
442
|
+
return `<div class="pflowwrap gflow" data-proj="${esc(proj)}">${notice}`
|
|
443
|
+
+ `<div class="gscroll" data-proj="${esc(proj)}"><svg width="${totalW}" height="${totalH}">${defs}${bands}${svg}</svg></div>`
|
|
444
|
+
+ `<div class="fhint">orchestrator → crew → integrate · phase by phase · scroll left/right →</div></div>`;
|
|
380
445
|
}
|
|
446
|
+
const GSCROLL = {};
|
|
381
447
|
function wireTimeline(el){
|
|
382
|
-
|
|
448
|
+
// FLOW v2 graph: click a card node to open its full /card detail; preserve horizontal scroll per project.
|
|
449
|
+
el.querySelectorAll('.gscroll').forEach(s => {
|
|
383
450
|
const proj = s.dataset.proj;
|
|
384
|
-
if (
|
|
385
|
-
s.onscroll = () => {
|
|
451
|
+
if (GSCROLL[proj] != null) s.scrollLeft = GSCROLL[proj];
|
|
452
|
+
s.onscroll = () => { GSCROLL[proj] = s.scrollLeft; };
|
|
386
453
|
});
|
|
387
|
-
el.querySelectorAll('.
|
|
454
|
+
el.querySelectorAll('.gnode[data-id]').forEach(n => n.onclick = () => openCard(+n.dataset.id));
|
|
388
455
|
}
|
|
389
456
|
// Card detail modal: one card's FULL story — its status journey interleaved with the agent's own bus
|
|
390
457
|
// reports (what it did, why, how). Click any card in the FLOW timeline to open it.
|
|
@@ -531,7 +598,10 @@ async function render(){
|
|
|
531
598
|
HISTORY[p.project] = h.events || [];
|
|
532
599
|
} catch(e) {}
|
|
533
600
|
});
|
|
534
|
-
|
|
601
|
+
const phaseP = projects.filter(p => VIEWS[p.project] === "flow").map(async p => {
|
|
602
|
+
try { PHASES[p.project] = await (await fetch(`/phases?project=${encodeURIComponent(p.project)}`)).json(); } catch(e) {}
|
|
603
|
+
});
|
|
604
|
+
await Promise.all([...histP, ...phaseP]);
|
|
535
605
|
|
|
536
606
|
// hub was reset (server empty but feed shows history) -> clear the stale client-side feed
|
|
537
607
|
if(!msgs.length&&$('#feed').childElementCount>0){$('#feed').innerHTML='';nmsg=0;}
|
|
@@ -586,7 +656,7 @@ async function render(){
|
|
|
586
656
|
const ph=p.phase||'';
|
|
587
657
|
const brief=p.brief?`<span class="brief">${esc(p.brief)}</span>`:`<span class="brief dim">— no brief yet · an agent sets it with relay_project_brief</span>`;
|
|
588
658
|
const view = VIEWS[p.project] || "board";
|
|
589
|
-
const vtog = `<div class="vtog"><button class="vbtn ${view==="board"?"on":""}" data-proj="${esc(p.project)}" data-view="board">BOARD</button><button class="vbtn ${view==="flow"?"on":""}" data-proj="${esc(p.project)}" data-view="flow">FLOW</button><button class="vbtn ${view==="timeline"?"on":""}" data-proj="${esc(p.project)}" data-view="timeline">TIMELINE</button></div>`;
|
|
659
|
+
const vtog = `<div class="vtog"><span class="vlbl">view</span><button class="vbtn ${view==="board"?"on":""}" data-proj="${esc(p.project)}" data-view="board">BOARD</button><button class="vbtn ${view==="flow"?"on":""}" data-proj="${esc(p.project)}" data-view="flow">FLOW</button><button class="vbtn ${view==="timeline"?"on":""}" data-proj="${esc(p.project)}" data-view="timeline">TIMELINE</button></div>`;
|
|
590
660
|
const ctl=`<span class="mv"><b class="mvup" data-proj="${esc(p.project)}" title="move up">▲</b><b class="mvdn" data-proj="${esc(p.project)}" title="move down">▼</b></span>${p.idle===true?`<span class="pdel" data-proj="${esc(p.project)}" title="forget this project (cards, peers, brief) — it returns if an agent registers it again">✕</span>`:''}`;
|
|
591
661
|
|
|
592
662
|
return `<div class="proj${p.idle===true?' idle':''}" data-projname="${esc(p.project)}"${p.idle===true?` data-idleproj="${esc(p.project)}"`:''}>`+
|