crewforth 3.5.0-rc.1 → 3.5.0-rc.2
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/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/kit/CLAUDE.md +1 -1
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/smoke-test.sh +60 -2
- package/kit/hooks/prompt-approval.sh +5 -5
- package/kit/skills/crew-plan/SKILL.md +2 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/spec-planning/SKILL.md +9 -2
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +21 -18
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +33 -29
- package/kit/studio/web/map.js +74 -73
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +1 -1
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
// 2. Nothing here reads the zoom. Positions are a function of the nodes and the viewer's choices, never of the
|
|
15
15
|
// view, so zooming cannot move a card.
|
|
16
16
|
// 3. Colour is not decided here at all. A box carries its status; the stylesheet turns status into colour.
|
|
17
|
+
//
|
|
18
|
+
// TWO SIDES
|
|
19
|
+
//
|
|
20
|
+
// In a window with room (`opts.sides`) the session stands in the middle: the agents that build — backend, frontend,
|
|
21
|
+
// database, test, devops, performance — grow to its left, mirrored, and everything else grows to its right. One
|
|
22
|
+
// column of a dozen agents beside an empty half of the pane is then two columns of six. A side is decided by an
|
|
23
|
+
// agent's type, never by how many are on either side, so an agent that arrives does not move one already drawn.
|
|
17
24
|
|
|
18
25
|
import { typeOf, roleName, shownType } from './names.js';
|
|
26
|
+
import { t } from './i18n.js';
|
|
19
27
|
|
|
20
28
|
/* ------------------------------------------------------------- measures --- */
|
|
21
29
|
|
|
@@ -80,6 +88,15 @@ export const stateOf = (status) => STATE[status] ?? 'unknown';
|
|
|
80
88
|
|
|
81
89
|
const SESSION = 'session';
|
|
82
90
|
|
|
91
|
+
/** The agents that build the product. Under `sides` they stand to the left of the session; the rest to its right. */
|
|
92
|
+
export const BUILDERS = new Set([
|
|
93
|
+
'crew-backend-expert', 'crew-frontend-expert', 'crew-database-expert',
|
|
94
|
+
'crew-test-expert', 'crew-devops-expert', 'crew-performance-expert',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** -1 for the left of the session, 1 for its right. A workflow run, an unknown type and every other agent: right. */
|
|
98
|
+
export const sideOf = (type) => (BUILDERS.has(type) ? -1 : 1);
|
|
99
|
+
|
|
83
100
|
/* --------------------------------------------------------------- groups --- */
|
|
84
101
|
|
|
85
102
|
const byArrival = (seq) => (a, b) => (seq.get(a.id) ?? 0) - (seq.get(b.id) ?? 0);
|
|
@@ -214,7 +231,7 @@ function itemsOf(nodes, { group, density, seq, stack, aside }) {
|
|
|
214
231
|
if (n.kind === 'workflow' && group !== 'none') {
|
|
215
232
|
const members = kids.get(n.id) ?? [];
|
|
216
233
|
const g = groupItem(n.id, 'run', members, anchor, via, depth, seq, {
|
|
217
|
-
label: n.workflowId ?? 'workflow run', node: n,
|
|
234
|
+
label: n.workflowId ?? t('workflow run'), node: n,
|
|
218
235
|
});
|
|
219
236
|
g.seq = seq.get(n.id) ?? g.seq;
|
|
220
237
|
items.push(g);
|
|
@@ -295,6 +312,8 @@ export const sessionHeight = (n) => SIZE.session.h + (n?.backgroundNow?.length ?
|
|
|
295
312
|
* the reference window's when it is not given
|
|
296
313
|
* @param opts.aside true when an open type group puts its agents in the next column instead of growing
|
|
297
314
|
* downward; a narrow window leaves it off
|
|
315
|
+
* @param opts.sides true to put the agents that build to the left of the session and the rest to its right
|
|
316
|
+
* (see TWO SIDES above); a narrow window leaves it off and the tree reads left to right
|
|
298
317
|
* @returns { items, edges, width, height, drawn, density, sessionY }
|
|
299
318
|
*/
|
|
300
319
|
export function plan(nodes, opts = {}) {
|
|
@@ -333,6 +352,17 @@ export function plan(nodes, opts = {}) {
|
|
|
333
352
|
};
|
|
334
353
|
|
|
335
354
|
const G = SIZE.group;
|
|
355
|
+
const sides = Boolean(opts.sides);
|
|
356
|
+
|
|
357
|
+
/** The side an item grows on: 0 for the session, and what the top of its branch is for everything under it. */
|
|
358
|
+
const sideOfItem = (it) => {
|
|
359
|
+
if (!sides || it.kind === 'session') return 0;
|
|
360
|
+
let top = it;
|
|
361
|
+
for (let guard = 0; top.parent && top.parent.kind !== 'session' && guard < 64; guard += 1) top = top.parent;
|
|
362
|
+
if (top.kind === 'agent') return sideOf(typeOf(top.node));
|
|
363
|
+
if (top.kind === 'group' && top.type === 'type') return sideOf(top.real);
|
|
364
|
+
return 1;
|
|
365
|
+
};
|
|
336
366
|
|
|
337
367
|
/** Size and place everything drawn, at one density. Returns the box the picture needs. */
|
|
338
368
|
const lay = (drawnItems, compact) => {
|
|
@@ -372,13 +402,18 @@ export function plan(nodes, opts = {}) {
|
|
|
372
402
|
}));
|
|
373
403
|
}
|
|
374
404
|
|
|
375
|
-
// Columns: one per depth, as wide as the widest thing in it.
|
|
376
|
-
|
|
405
|
+
// Columns: one per depth, as wide as the widest thing in it. Under `sides` a column is a depth on one side,
|
|
406
|
+
// keyed by the depth with the side's sign, so the left side's columns come first, outermost first.
|
|
407
|
+
for (const it of drawnItems) { it.side = sideOfItem(it); it.col = it.side < 0 ? -it.depth : it.depth; }
|
|
408
|
+
const cols = [...new Set(drawnItems.map((it) => it.col))].sort((a, b) => a - b);
|
|
377
409
|
const colX = new Map();
|
|
410
|
+
const colW = new Map();
|
|
378
411
|
let x = SIZE.padX;
|
|
379
|
-
for (const
|
|
380
|
-
|
|
381
|
-
|
|
412
|
+
for (const k of cols) {
|
|
413
|
+
const w = Math.max(...drawnItems.filter((it) => it.col === k).map((it) => it.w));
|
|
414
|
+
colX.set(k, x);
|
|
415
|
+
colW.set(k, w);
|
|
416
|
+
x += w + SIZE.colGap;
|
|
382
417
|
}
|
|
383
418
|
const width = x - SIZE.colGap + SIZE.padX;
|
|
384
419
|
|
|
@@ -392,12 +427,14 @@ export function plan(nodes, opts = {}) {
|
|
|
392
427
|
return Math.max(SIZE.padY, cy - it.h / 2);
|
|
393
428
|
};
|
|
394
429
|
let height = 0;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
430
|
+
// Nearest the session first, so a parent has its place before its children ask for it.
|
|
431
|
+
for (const k of cols.slice().sort((a, b) => Math.abs(a) - Math.abs(b) || a - b)) {
|
|
432
|
+
if (k === 0) continue;
|
|
433
|
+
const col = drawnItems.filter((it) => it.col === k).sort((a, b) => a.seq - b.seq || String(a.id).localeCompare(String(b.id)));
|
|
398
434
|
let cursor = SIZE.padY;
|
|
399
435
|
for (const it of col) {
|
|
400
|
-
|
|
436
|
+
// On the left a column's boxes line up on their right edge, the one their wire comes into.
|
|
437
|
+
it.x = it.side < 0 ? colX.get(k) + colW.get(k) - it.w : colX.get(k);
|
|
401
438
|
it.y = Math.max(cursor, anchorY(it));
|
|
402
439
|
cursor = it.y + it.h + SIZE.rowGap;
|
|
403
440
|
}
|
|
@@ -449,15 +486,17 @@ export function plan(nodes, opts = {}) {
|
|
|
449
486
|
if (p) { it.x = p.x; it.y = p.y; it.pinned = true; }
|
|
450
487
|
}
|
|
451
488
|
|
|
452
|
-
// Wires: out of the right edge of what spawned a thing, into its left edge.
|
|
489
|
+
// Wires: out of the right edge of what spawned a thing, into its left edge. On the left side, mirrored: out of
|
|
490
|
+
// the left edge, into the right one, and `dir` says so to whoever draws the curve.
|
|
453
491
|
const edges = [];
|
|
454
492
|
for (const it of drawnItems) {
|
|
455
493
|
const p = it.parent;
|
|
456
494
|
if (!p) continue;
|
|
457
495
|
const cell = cellOf(it);
|
|
496
|
+
const left = it.side < 0;
|
|
458
497
|
const from = cell
|
|
459
|
-
? { x: p.x + cell.x + cell.w, y: p.y + cell.y + cell.h / 2 }
|
|
460
|
-
: { x: p.x + p.w, y: p.y + p.h / 2 };
|
|
498
|
+
? { x: p.x + cell.x + (left ? 0 : cell.w), y: p.y + cell.y + cell.h / 2 }
|
|
499
|
+
: { x: p.x + (left ? 0 : p.w), y: p.y + p.h / 2 };
|
|
461
500
|
const status = it.kind === 'group' ? it.status : (it.node.status ?? 'unknown');
|
|
462
501
|
// A group's wire says whether the group is still working. What went wrong inside it is said by its bar, its
|
|
463
502
|
// count and the member itself; a red wire into twelve agents because one failed would say the wrong thing
|
|
@@ -468,7 +507,8 @@ export function plan(nodes, opts = {}) {
|
|
|
468
507
|
source: it.via ?? p.id,
|
|
469
508
|
target: it.id,
|
|
470
509
|
from,
|
|
471
|
-
to: { x: it.x, y: it.y + it.h / 2 },
|
|
510
|
+
to: { x: it.x + (left ? it.w : 0), y: it.y + it.h / 2 },
|
|
511
|
+
dir: left ? -1 : 1,
|
|
472
512
|
status,
|
|
473
513
|
state,
|
|
474
514
|
});
|
|
@@ -476,14 +516,14 @@ export function plan(nodes, opts = {}) {
|
|
|
476
516
|
|
|
477
517
|
return {
|
|
478
518
|
items: drawnItems, edges, width: Math.max(width, SIZE.padX * 2), height: Math.max(height, SIZE.padY * 2),
|
|
479
|
-
drawn, density, group, stacked, aside: Boolean(aside), sessionY: sessionY ?? null,
|
|
519
|
+
drawn, density, group, stacked, aside: Boolean(aside), sides, sessionY: sessionY ?? null,
|
|
480
520
|
};
|
|
481
521
|
}
|
|
482
522
|
|
|
483
523
|
/* ---------------------------------------------------------------- order --- */
|
|
484
524
|
|
|
485
525
|
// What the wire from an agent to a later wave is called, and what it is: a reading of the order of events.
|
|
486
|
-
export const INFERRED_NOTE = 'Inferred from the order of events: this wave was called after that agent reported. It is not a call from one agent to another; the session calls every agent.';
|
|
526
|
+
export const INFERRED_NOTE = t('Inferred from the order of events: this wave was called after that agent reported. It is not a call from one agent to another; the session calls every agent.');
|
|
487
527
|
|
|
488
528
|
/**
|
|
489
529
|
* The same agents by when the session called them.
|
|
@@ -568,8 +608,8 @@ function planOrder(nodes, asked, opts, tallest = AUTO.tallest) {
|
|
|
568
608
|
waves[i].members.forEach((m, k) => wire(at.get(r.id), at.get(m.id), {
|
|
569
609
|
kind: 'inferred', state: 'inferred',
|
|
570
610
|
// Said once for a reporter, on the wire to the wave's first agent; the others carry it on hover.
|
|
571
|
-
label: k === 0 ?
|
|
572
|
-
title:
|
|
611
|
+
label: k === 0 ? t('after {type} reported', { type: shownType(r) }) : null,
|
|
612
|
+
title: `${t('after {type} reported', { type: shownType(r) })}\n${INFERRED_NOTE}`,
|
|
573
613
|
}));
|
|
574
614
|
}
|
|
575
615
|
for (const m of waves[i].members) at.get(m.id).after = reporters.map((r) => shownType(r));
|
|
@@ -577,7 +617,7 @@ function planOrder(nodes, asked, opts, tallest = AUTO.tallest) {
|
|
|
577
617
|
|
|
578
618
|
return {
|
|
579
619
|
items, edges, width: Math.max(box.width, SIZE.padX * 2), height: Math.max(height, SIZE.padY * 2),
|
|
580
|
-
drawn: agents.length, density: compact ? 'compact' : 'comfortable', group: 'order', stacked: false, aside: false,
|
|
620
|
+
drawn: agents.length, density: compact ? 'compact' : 'comfortable', group: 'order', stacked: false, aside: false, sides: false,
|
|
581
621
|
waves: waves.length, sessionY,
|
|
582
622
|
};
|
|
583
623
|
}
|
|
@@ -659,14 +699,14 @@ export function attention(nodes, waiting = new Set()) {
|
|
|
659
699
|
const needs = agents.filter((n) => waiting.has(n.id)).sort((a, b) => when(b) - when(a));
|
|
660
700
|
const failed = agents.filter((n) => !waiting.has(n.id) && stateOf(n.status) === 'failed').sort((a, b) => when(b) - when(a));
|
|
661
701
|
return [
|
|
662
|
-
...needs.map((n) => ({ id: n.id, tone: 'waiting', name: shownType(n), real: typeOf(n), says: 'waiting for you' })),
|
|
702
|
+
...needs.map((n) => ({ id: n.id, tone: 'waiting', name: shownType(n), real: typeOf(n), says: t('waiting for you') })),
|
|
663
703
|
...failed.map((n) => ({
|
|
664
704
|
id: n.id,
|
|
665
705
|
tone: 'fail',
|
|
666
706
|
name: shownType(n),
|
|
667
707
|
real: typeOf(n),
|
|
668
|
-
says: [n.status, n.errors ?
|
|
669
|
-
n.workflow ?
|
|
708
|
+
says: [({ failed: t('failed'), killed: t('killed'), stopped: t('stopped') })[n.status] ?? n.status, n.errors ? (n.errors === 1 ? t('{n} error', { n: 1 }) : t('{n} errors', { n: n.errors })) : null,
|
|
709
|
+
n.workflow ? t('in {name}', { name: n.workflow }) : null].filter(Boolean).join(' · '),
|
|
670
710
|
})),
|
|
671
711
|
];
|
|
672
712
|
}
|