crewforth 3.1.0-rc.3 → 3.1.0-rc.4
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 +35 -9
- package/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +3 -3
- package/kit/eval/doctor.sh +7 -7
- package/kit/eval/smoke-test.sh +215 -26
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-bash.sh +61 -5
- package/kit/hooks/guard-schedule.sh +92 -0
- package/kit/hooks/prompt-approval.sh +68 -21
- package/kit/settings.json +11 -0
- package/kit/skills/crew-approve/SKILL.md +26 -0
- package/kit/studio/server/lib/graph.js +27 -4
- package/kit/studio/web/app.js +1 -1
- package/kit/studio/web/canvas.js +66 -13
- package/kit/studio/web/graph-plan.js +106 -1
- package/kit/studio/web/names.js +11 -0
- package/kit/studio/web/style.css +16 -0
- package/kit/studio/web/timeline-plan.js +8 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ import { typeOf, roleName, shownType } from './names.js';
|
|
|
22
22
|
export const SIZE = {
|
|
23
23
|
padX: 24, padY: 16, // the canvas's own margin
|
|
24
24
|
colGap: 56, // between one depth and the next
|
|
25
|
+
waveGap: 148, // between one wave and the next: room for the words on an inferred wire
|
|
25
26
|
rowGap: 12, // between siblings
|
|
26
27
|
session: { w: 200, h: 84 },
|
|
27
28
|
card: { w: 248, h: 64 }, // an agent, and a folded group
|
|
@@ -48,7 +49,7 @@ export const AUTO = {
|
|
|
48
49
|
stackAt: 3,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
export const GROUPINGS = ['run', 'type', 'parent', 'none'];
|
|
52
|
+
export const GROUPINGS = ['run', 'type', 'parent', 'order', 'none'];
|
|
52
53
|
export const DENSITIES = ['auto', 'comfortable', 'compact'];
|
|
53
54
|
|
|
54
55
|
// The handful of things a status means to a reader. `killed` and `stopped` come off the transcript verbatim and
|
|
@@ -227,6 +228,8 @@ function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
|
227
228
|
via,
|
|
228
229
|
depth,
|
|
229
230
|
seq: Math.min(...members.map((m) => seq.get(m.id) ?? 0), Infinity),
|
|
231
|
+
// The group stands where its first member does in the order the session called its agents.
|
|
232
|
+
first: members.filter((m) => m.order != null).sort((a, b) => a.order - b.order)[0] ?? null,
|
|
230
233
|
...extra,
|
|
231
234
|
};
|
|
232
235
|
}
|
|
@@ -248,6 +251,7 @@ function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
|
248
251
|
export function plan(nodes, opts = {}) {
|
|
249
252
|
const group = GROUPINGS.includes(opts.group) ? opts.group : 'run';
|
|
250
253
|
const asked = DENSITIES.includes(opts.density) ? opts.density : 'auto';
|
|
254
|
+
if (group === 'order') return planOrder(nodes, asked, opts);
|
|
251
255
|
const seq = opts.seq ?? sequence(nodes);
|
|
252
256
|
const open = opts.open ?? new Map();
|
|
253
257
|
const pinned = opts.pinned ?? new Map();
|
|
@@ -422,6 +426,107 @@ export function plan(nodes, opts = {}) {
|
|
|
422
426
|
};
|
|
423
427
|
}
|
|
424
428
|
|
|
429
|
+
/* ---------------------------------------------------------------- order --- */
|
|
430
|
+
|
|
431
|
+
// What the wire from an agent to a later wave is called, and what it is: a reading of the order of events.
|
|
432
|
+
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.';
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The same agents by when the session called them.
|
|
436
|
+
*
|
|
437
|
+
* Every agent is called by the session, so the tree is one level deep and says nothing of sequence. Here a
|
|
438
|
+
* column is a WAVE: the agents called in one assistant message, which is what "started together" means. Waves
|
|
439
|
+
* run left to right in the order the messages were written, and a wave's agents stack in the order of the calls.
|
|
440
|
+
* Agents the session's transcript did not call (a workflow's) have no wave and take a last column of their own.
|
|
441
|
+
*
|
|
442
|
+
* Two kinds of wire. The session is wired to the first wave: it called it. A later wave is wired, dotted, from
|
|
443
|
+
* each agent that reported after the wave before it was called and before this one was — an inference from the
|
|
444
|
+
* order of events, drawn as one and labelled as one. A wave nothing reported before has no wire.
|
|
445
|
+
*/
|
|
446
|
+
function planOrder(nodes, asked, opts) {
|
|
447
|
+
const pinned = opts.pinned ?? new Map();
|
|
448
|
+
const session = nodes.find((n) => n.kind === 'session') ?? null;
|
|
449
|
+
const agents = nodes.filter((n) => n.kind === 'agent');
|
|
450
|
+
const called = agents.filter((n) => n.wave != null);
|
|
451
|
+
const waveIds = [...new Set(called.map((n) => n.wave))].sort((a, b) => a - b);
|
|
452
|
+
const byOrder = (a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.id).localeCompare(String(b.id));
|
|
453
|
+
const columns = waveIds.map((w) => ({ wave: w, members: called.filter((n) => n.wave === w).sort(byOrder) }));
|
|
454
|
+
for (const c of columns) c.calledAt = Math.min(...c.members.map((m) => m.calledAt ?? Infinity));
|
|
455
|
+
const rest = agents.filter((n) => n.wave == null)
|
|
456
|
+
.sort((a, b) => (a.startedAt ?? Infinity) - (b.startedAt ?? Infinity) || String(a.id).localeCompare(String(b.id)));
|
|
457
|
+
if (rest.length) columns.push({ wave: null, members: rest, calledAt: Infinity });
|
|
458
|
+
|
|
459
|
+
const lay = (compact) => {
|
|
460
|
+
const sz = compact ? SIZE.chip : SIZE.card;
|
|
461
|
+
const items = [];
|
|
462
|
+
const x0 = SIZE.padX + (session ? SIZE.session.w + SIZE.colGap : 0);
|
|
463
|
+
let height = 0;
|
|
464
|
+
columns.forEach((col, i) => {
|
|
465
|
+
col.members.forEach((n, j) => {
|
|
466
|
+
items.push({
|
|
467
|
+
id: n.id, kind: 'agent', node: n, parent: null, via: null, depth: i + 1, seq: n.order ?? j, compact,
|
|
468
|
+
wave: col.wave, w: sz.w, h: sz.h, x: x0 + i * (sz.w + SIZE.waveGap), y: SIZE.padY + j * (sz.h + SIZE.rowGap),
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
height = Math.max(height, SIZE.padY * 2 + col.members.length * (sz.h + SIZE.rowGap) - SIZE.rowGap);
|
|
472
|
+
});
|
|
473
|
+
const width = x0 + Math.max(0, columns.length * (sz.w + SIZE.waveGap) - SIZE.waveGap) + SIZE.padX;
|
|
474
|
+
return { items, width, height };
|
|
475
|
+
};
|
|
476
|
+
let compact = asked === 'compact';
|
|
477
|
+
let box = lay(compact);
|
|
478
|
+
if (asked === 'auto' && box.height > AUTO.tallest) { compact = true; box = lay(true); }
|
|
479
|
+
const { items } = box;
|
|
480
|
+
let { height } = box;
|
|
481
|
+
|
|
482
|
+
let sessionY = opts.sessionY ?? null;
|
|
483
|
+
if (session) {
|
|
484
|
+
const first = items.filter((it) => it.depth === 1);
|
|
485
|
+
if (sessionY == null) {
|
|
486
|
+
const bottom = first.length ? Math.max(...first.map((it) => it.y + it.h)) : SIZE.padY + SIZE.session.h;
|
|
487
|
+
sessionY = Math.max(SIZE.padY, Math.round((SIZE.padY + bottom) / 2 - SIZE.session.h / 2));
|
|
488
|
+
}
|
|
489
|
+
items.unshift({ id: session.id, kind: 'session', node: session, parent: null, depth: 0, w: SIZE.session.w, h: SIZE.session.h, x: SIZE.padX, y: sessionY });
|
|
490
|
+
height = Math.max(height, sessionY + SIZE.session.h + SIZE.padY);
|
|
491
|
+
}
|
|
492
|
+
for (const it of items) {
|
|
493
|
+
const p = pinned.get(it.id);
|
|
494
|
+
if (p) { it.x = p.x; it.y = p.y; it.pinned = true; }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const at = new Map(items.map((it) => [it.id, it]));
|
|
498
|
+
const edges = [];
|
|
499
|
+
const wire = (from, to, extra) => edges.push({
|
|
500
|
+
key: `${from.id}\u0000${to.id}`, source: from.id, target: to.id,
|
|
501
|
+
from: { x: from.x + from.w, y: from.y + from.h / 2 }, to: { x: to.x, y: to.y + to.h / 2 },
|
|
502
|
+
status: to.node.status ?? 'unknown', state: stateOf(to.node.status), ...extra,
|
|
503
|
+
});
|
|
504
|
+
const waves = columns.filter((c) => c.wave != null);
|
|
505
|
+
if (session && waves.length) for (const m of waves[0].members) wire(at.get(session.id), at.get(m.id), {});
|
|
506
|
+
// An agent's report belongs to the first wave called after it: the one that could have been waiting for it.
|
|
507
|
+
for (let i = 1; i < waves.length; i += 1) {
|
|
508
|
+
const since = waves[i - 1].calledAt;
|
|
509
|
+
const reporters = called.filter((n) => n.reportedAt != null && n.reportedAt > since && n.reportedAt <= waves[i].calledAt
|
|
510
|
+
&& n.wave < waves[i].wave).sort(byOrder);
|
|
511
|
+
waves[i].after = reporters;
|
|
512
|
+
for (const r of reporters) {
|
|
513
|
+
waves[i].members.forEach((m, k) => wire(at.get(r.id), at.get(m.id), {
|
|
514
|
+
kind: 'inferred', state: 'inferred',
|
|
515
|
+
// Said once for a reporter, on the wire to the wave's first agent; the others carry it on hover.
|
|
516
|
+
label: k === 0 ? `after ${shownType(r)} reported` : null,
|
|
517
|
+
title: `after ${shownType(r)} reported\n${INFERRED_NOTE}`,
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
for (const m of waves[i].members) at.get(m.id).after = reporters.map((r) => shownType(r));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
items, edges, width: Math.max(box.width, SIZE.padX * 2), height: Math.max(height, SIZE.padY * 2),
|
|
525
|
+
drawn: agents.length, density: compact ? 'compact' : 'comfortable', group: 'order', stacked: false, aside: false,
|
|
526
|
+
waves: waves.length, sessionY,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
425
530
|
/**
|
|
426
531
|
* Which open groups to fold when a session first opens too tall even as chips: the largest first, until the
|
|
427
532
|
* picture is short enough to read or there is nothing left to fold. Returns the ids to fold; an empty list when
|
package/kit/studio/web/names.js
CHANGED
|
@@ -31,3 +31,14 @@ export const shownType = (n) => roleName(typeOf(n));
|
|
|
31
31
|
|
|
32
32
|
/** What hovering an agent says: the type as it is declared, and its task in full, since both are cut on screen. */
|
|
33
33
|
export const hoverOf = (n) => [n?.agentType ?? null, n?.description ?? null].filter(Boolean).join('\n');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where an agent stands among the ones the session called, as a tag: "#3 · 09:14". The number is the order of
|
|
37
|
+
* the calls; the time is when the call was made, in the viewer's own zone. An agent the session's transcript did
|
|
38
|
+
* not call has no place to state, and gets no tag.
|
|
39
|
+
* @param clock (ms) => "HH:MM"
|
|
40
|
+
*/
|
|
41
|
+
export function orderTag(n, clock = null) {
|
|
42
|
+
if (n?.order == null) return '';
|
|
43
|
+
return `#${n.order}${clock && n.calledAt != null ? ` · ${clock(n.calledAt)}` : ''}`;
|
|
44
|
+
}
|
package/kit/studio/web/style.css
CHANGED
|
@@ -836,6 +836,21 @@ button.tl-lab { cursor: pointer; }
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
/* Working. The brightest and the only one that travels. */
|
|
839
|
+
/* A wire that is an inference, not a call: dotted, still, and it can be hovered for what it means. A working wire
|
|
840
|
+
is dashed and moves; this one is neither. */
|
|
841
|
+
.cv-edge[data-kind="inferred"] {
|
|
842
|
+
stroke: var(--ink-faint);
|
|
843
|
+
stroke-dasharray: 1.5 5;
|
|
844
|
+
pointer-events: stroke;
|
|
845
|
+
}
|
|
846
|
+
.cv-edge-label { font-size: 10px; fill: var(--ink-faint); pointer-events: none; }
|
|
847
|
+
/* "#3 · 09:14": where a card stands in the order the session called its agents. */
|
|
848
|
+
.cv-under { display: flex; align-items: baseline; min-width: 0; }
|
|
849
|
+
.cv-under .cv-task { flex: 1 1 auto; min-width: 0; }
|
|
850
|
+
.cv-order { flex: none; margin-left: 6px; font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
851
|
+
.cv-order[hidden] { display: none; }
|
|
852
|
+
.cv-chip .cv-order, .cv-member .cv-order { margin-left: 0; margin-right: 6px; }
|
|
853
|
+
|
|
839
854
|
.cv-edge[data-state="live"] {
|
|
840
855
|
stroke: var(--status-busy);
|
|
841
856
|
opacity: var(--cv-edge-live);
|
|
@@ -1086,6 +1101,7 @@ button.tl-lab { cursor: pointer; }
|
|
|
1086
1101
|
.cv-legend-line[data-state="live"] { border-top: 1.5px dashed var(--status-busy); }
|
|
1087
1102
|
.cv-legend-line[data-state="waiting"] { border-top: 1.5px dashed var(--status-waiting); }
|
|
1088
1103
|
.cv-legend-line[data-state="failed"] { border-top-color: var(--cv-fail); }
|
|
1104
|
+
.cv-legend-line[data-state="inferred"] { border-top: 1.5px dotted var(--ink-faint); }
|
|
1089
1105
|
.cv-x {
|
|
1090
1106
|
width: 20px; height: 20px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
|
|
1091
1107
|
background: none; border: 0; border-radius: var(--radius-sm); color: var(--ink-dim); cursor: pointer;
|
|
@@ -120,6 +120,8 @@ export function groupsOf(nodes, grouping) {
|
|
|
120
120
|
return { id: `parent:${p.id}`, label: p.kind === 'workflow' ? (p.workflowId ?? p.id) : `under ${shownType(p)}` };
|
|
121
121
|
}
|
|
122
122
|
if (grouping === 'none') return { id: 'all', label: null };
|
|
123
|
+
// `order`: the agents called in one assistant message are a wave, and a wave is a group.
|
|
124
|
+
if (grouping === 'order') return n.wave != null ? { id: `wave:${n.wave}`, label: `Wave ${n.wave}` } : { id: 'wave:none', label: 'Not called by the session' };
|
|
123
125
|
// `run`: a workflow run is a group; everything the session started itself is another.
|
|
124
126
|
if (n.workflow) return { id: `run:${n.workflow}`, label: String(n.workflow) };
|
|
125
127
|
return { id: 'direct', label: 'Session · direct' };
|
|
@@ -135,6 +137,12 @@ export function groupsOf(nodes, grouping) {
|
|
|
135
137
|
for (const g of out) g.members.sort((a, b) => (a.startedAt ?? Infinity) - (b.startedAt ?? Infinity) || String(a.id).localeCompare(String(b.id)));
|
|
136
138
|
// What the session did itself comes first; the rest in the order they began.
|
|
137
139
|
out.sort((a, b) => Number(b.id === 'direct') - Number(a.id === 'direct') || first(a) - first(b) || a.id.localeCompare(b.id));
|
|
140
|
+
// Waves are in the order they were called, which is not always the order their agents first wrote.
|
|
141
|
+
if (grouping === 'order') {
|
|
142
|
+
const wave = (g) => g.members[0].wave ?? Infinity;
|
|
143
|
+
out.sort((a, b) => wave(a) - wave(b));
|
|
144
|
+
for (const g of out) g.members.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
|
145
|
+
}
|
|
138
146
|
return out;
|
|
139
147
|
}
|
|
140
148
|
|
package/package.json
CHANGED