@archznn/crewloop-skills 0.4.3 → 0.6.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.
- package/package.json +3 -2
- package/packages/cli/dist/agents.d.ts +9 -0
- package/packages/cli/dist/agents.d.ts.map +1 -1
- package/packages/cli/dist/agents.js +62 -5
- package/packages/cli/dist/agents.js.map +1 -1
- package/packages/cli/dist/cli.d.ts +1 -0
- package/packages/cli/dist/cli.d.ts.map +1 -1
- package/packages/cli/dist/cli.js +57 -5
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/hooks.d.ts +37 -0
- package/packages/cli/dist/hooks.d.ts.map +1 -0
- package/packages/cli/dist/hooks.js +282 -0
- package/packages/cli/dist/hooks.js.map +1 -0
- package/packages/cli/dist/mcp.d.ts +8 -0
- package/packages/cli/dist/mcp.d.ts.map +1 -1
- package/packages/cli/dist/mcp.js +19 -1
- package/packages/cli/dist/mcp.js.map +1 -1
- package/packages/cli/dist/tests/cli.test.js +21 -0
- package/packages/cli/dist/tests/cli.test.js.map +1 -1
- package/packages/cli/dist/tests/hooks.test.d.ts +2 -0
- package/packages/cli/dist/tests/hooks.test.d.ts.map +1 -0
- package/packages/cli/dist/tests/hooks.test.js +179 -0
- package/packages/cli/dist/tests/hooks.test.js.map +1 -0
- package/packages/cli/dist/tests/mcp.test.js +79 -0
- package/packages/cli/dist/tests/mcp.test.js.map +1 -1
- package/servers/dashboard/bin/crewloop-shim.js +4 -0
- package/servers/dashboard/dist/adapters/codex.d.ts +1 -0
- package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/codex.js +1 -0
- package/servers/dashboard/dist/adapters/codex.js.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts +1 -0
- package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.js +1 -0
- package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts +2 -1
- package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/shim.js +15 -2
- package/servers/dashboard/dist/adapters/shim.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.test.js +43 -0
- package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
- package/servers/dashboard/dist/presenter.d.ts.map +1 -1
- package/servers/dashboard/dist/presenter.js +2 -0
- package/servers/dashboard/dist/presenter.js.map +1 -1
- package/servers/dashboard/dist/presenter.test.js +7 -0
- package/servers/dashboard/dist/presenter.test.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/infer.js +5 -0
- package/servers/dashboard/dist/skills/infer.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.test.js +9 -0
- package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
- package/servers/dashboard/dist/state.d.ts.map +1 -1
- package/servers/dashboard/dist/state.js +19 -1
- package/servers/dashboard/dist/state.js.map +1 -1
- package/servers/dashboard/dist/state.test.js +37 -0
- package/servers/dashboard/dist/state.test.js.map +1 -1
- package/servers/dashboard/dist/tests/shim.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/shim.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/shim.test.js +47 -0
- package/servers/dashboard/dist/tests/shim.test.js.map +1 -0
- package/servers/dashboard/dist/types.d.ts +4 -0
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +3 -2
- package/servers/dashboard/public/app.js +81 -12
- package/servers/dashboard/public/styles.css +174 -19
- package/servers/dashboard/src/adapters/codex.ts +2 -0
- package/servers/dashboard/src/adapters/kimi.ts +2 -0
- package/servers/dashboard/src/adapters/shim.test.ts +64 -1
- package/servers/dashboard/src/adapters/shim.ts +18 -2
- package/servers/dashboard/src/presenter.test.ts +8 -0
- package/servers/dashboard/src/presenter.ts +2 -0
- package/servers/dashboard/src/skills/infer.test.ts +10 -0
- package/servers/dashboard/src/skills/infer.ts +8 -0
- package/servers/dashboard/src/state.test.ts +43 -0
- package/servers/dashboard/src/state.ts +20 -1
- package/servers/dashboard/src/tests/shim.test.ts +45 -0
- package/servers/dashboard/src/types.ts +4 -0
- package/skills/orchestrator/SKILL.md +6 -0
|
@@ -219,27 +219,89 @@
|
|
|
219
219
|
renderActivityGraph(session);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
let lifecycleBadgeEl = null;
|
|
223
|
+
let emptyStateEl = null;
|
|
224
|
+
|
|
225
|
+
function getActiveSkillContent() {
|
|
226
|
+
return activeSkillName.parentElement;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function setChildrenVisibility(visible) {
|
|
230
|
+
const content = getActiveSkillContent();
|
|
231
|
+
if (!content) return;
|
|
232
|
+
Array.from(content.children).forEach((child) => {
|
|
233
|
+
if (child === emptyStateEl) return;
|
|
234
|
+
child.style.display = visible ? '' : 'none';
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderEmptyState() {
|
|
239
|
+
if (!emptyStateEl) {
|
|
240
|
+
emptyStateEl = document.createElement('div');
|
|
241
|
+
emptyStateEl.className = 'empty-state';
|
|
242
|
+
emptyStateEl.innerHTML = `
|
|
243
|
+
<div class="empty-state-icon"><i class="ph ph-monitor-play"></i></div>
|
|
244
|
+
<h2 class="empty-state-title">NO ACTIVE SESSION</h2>
|
|
245
|
+
<p class="empty-state-body">Start an agent session to see it here.</p>
|
|
246
|
+
`;
|
|
247
|
+
getActiveSkillContent()?.appendChild(emptyStateEl);
|
|
248
|
+
}
|
|
249
|
+
emptyStateEl.style.display = 'flex';
|
|
250
|
+
setChildrenVisibility(false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function hideEmptyState() {
|
|
254
|
+
if (emptyStateEl) {
|
|
255
|
+
emptyStateEl.style.display = 'none';
|
|
256
|
+
}
|
|
257
|
+
setChildrenVisibility(true);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderLifecycleBadge(session) {
|
|
261
|
+
if (!lifecycleBadgeEl) {
|
|
262
|
+
lifecycleBadgeEl = document.createElement('span');
|
|
263
|
+
lifecycleBadgeEl.className = 'lifecycle-badge';
|
|
264
|
+
statusBadge.parentNode?.insertBefore(lifecycleBadgeEl, statusBadge.nextSibling);
|
|
265
|
+
}
|
|
266
|
+
const lifecycle = session.lifecycle || 'starting';
|
|
267
|
+
lifecycleBadgeEl.className = 'lifecycle-badge ' + lifecycle;
|
|
268
|
+
lifecycleBadgeEl.innerHTML = `<span class="lifecycle-dot"></span>${lifecycle.toUpperCase()}`;
|
|
269
|
+
lifecycleBadgeEl.style.display = 'inline-flex';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function hideLifecycleBadge() {
|
|
273
|
+
if (lifecycleBadgeEl) {
|
|
274
|
+
lifecycleBadgeEl.style.display = 'none';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
222
278
|
function renderActiveSkill(session) {
|
|
223
279
|
if (!session) {
|
|
224
|
-
|
|
225
|
-
activeSkillIcon.className = 'ph ph-circle';
|
|
280
|
+
renderEmptyState();
|
|
226
281
|
activeStrip.className = 'panel-accent-strip';
|
|
227
|
-
|
|
228
|
-
statusText.textContent = 'IDLE';
|
|
229
|
-
confidenceBadge.textContent = 'unknown';
|
|
230
|
-
activeSkillSource.innerHTML = '<i class="ph ph-monitor"></i><span>waiting for events</span>';
|
|
282
|
+
hideLifecycleBadge();
|
|
231
283
|
return;
|
|
232
284
|
}
|
|
233
285
|
|
|
286
|
+
hideEmptyState();
|
|
287
|
+
|
|
234
288
|
const skill = session.activeSkill || { name: 'UNKNOWN', confidence: 'low' };
|
|
235
289
|
const iconClass = skillIcon(skill.name);
|
|
236
290
|
activeSkillName.textContent = skill.name.toUpperCase();
|
|
237
291
|
activeSkillIcon.className = 'ph ' + iconClass;
|
|
238
292
|
|
|
239
|
-
const
|
|
240
|
-
activeStrip.className = 'panel-accent-strip' +
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
const lifecycle = session.lifecycle || 'starting';
|
|
294
|
+
activeStrip.className = 'panel-accent-strip ' + lifecycle;
|
|
295
|
+
|
|
296
|
+
if (lifecycle === 'ended') {
|
|
297
|
+
statusDot.className = 'status-dot ' + (session.status || '');
|
|
298
|
+
statusText.textContent = session.status ? session.status.toUpperCase() : 'ENDED';
|
|
299
|
+
} else {
|
|
300
|
+
statusDot.className = 'status-dot ' + lifecycle;
|
|
301
|
+
statusText.textContent = lifecycle.toUpperCase();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
renderLifecycleBadge(session);
|
|
243
305
|
confidenceBadge.textContent = skill.confidence || 'low';
|
|
244
306
|
|
|
245
307
|
activeSkillSource.innerHTML = `<i class="ph ph-${sourceIcon(session.source)}"></i><span>${session.source || 'unknown'}</span>`;
|
|
@@ -265,7 +327,8 @@
|
|
|
265
327
|
const events = session.events || [];
|
|
266
328
|
const toolEvents = events.filter((e) => e.event_type === 'tool_start' || e.event_type === 'tool_end');
|
|
267
329
|
toolCount.textContent = String(Math.ceil(toolEvents.length / 2));
|
|
268
|
-
const
|
|
330
|
+
const endTime = session.endedAt || session.lastActivity;
|
|
331
|
+
const dur = endTime && session.startTime ? endTime - session.startTime : 0;
|
|
269
332
|
durationEl.textContent = formatDuration(dur);
|
|
270
333
|
updateEventRate();
|
|
271
334
|
}
|
|
@@ -383,8 +446,14 @@
|
|
|
383
446
|
li.className = 'session-item' + (s.id === state.selectedSessionId ? ' active' : '');
|
|
384
447
|
li.setAttribute('role', 'option');
|
|
385
448
|
li.setAttribute('aria-selected', String(s.id === state.selectedSessionId));
|
|
449
|
+
const duration = s.endedAt
|
|
450
|
+
? `ended after ${formatDuration(s.endedAt - s.startTime)}`
|
|
451
|
+
: formatDuration(Date.now() - s.startTime);
|
|
386
452
|
li.innerHTML = `
|
|
387
|
-
<
|
|
453
|
+
<div class="session-item-main">
|
|
454
|
+
<span class="session-item-id">${truncate(s.id, 16)}</span>
|
|
455
|
+
<span class="session-item-meta">${formatTime(s.startTime)} · ${duration}</span>
|
|
456
|
+
</div>
|
|
388
457
|
<span class="session-item-source">${s.source || 'unknown'}</span>
|
|
389
458
|
`;
|
|
390
459
|
li.addEventListener('click', () => {
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
--warning: #facc15;
|
|
17
17
|
--running: #38bdf8;
|
|
18
18
|
|
|
19
|
+
/* lifecycle tokens */
|
|
20
|
+
--lifecycle-starting: var(--warning);
|
|
21
|
+
--lifecycle-running: var(--running);
|
|
22
|
+
--lifecycle-ended: var(--text-muted);
|
|
23
|
+
--lifecycle-starting-bg: rgba(250, 204, 21, 0.12);
|
|
24
|
+
--lifecycle-running-bg: rgba(56, 189, 248, 0.12);
|
|
25
|
+
--lifecycle-ended-bg: rgba(148, 163, 184, 0.10);
|
|
26
|
+
|
|
19
27
|
--font-display: 'Bebas Neue', 'Oswald', sans-serif;
|
|
20
28
|
--font-body: 'DM Sans', system-ui, sans-serif;
|
|
21
29
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
@@ -237,6 +245,121 @@ html, body {
|
|
|
237
245
|
text-transform: uppercase;
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
.session-item-main {
|
|
249
|
+
display: flex;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
gap: 2px;
|
|
252
|
+
min-width: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.session-item-meta {
|
|
256
|
+
font-size: 0.6875rem;
|
|
257
|
+
color: var(--text-muted);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Lifecycle badge */
|
|
261
|
+
.lifecycle-badge {
|
|
262
|
+
display: inline-flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: 6px;
|
|
265
|
+
padding: 4px 10px;
|
|
266
|
+
border-radius: 999px;
|
|
267
|
+
font-size: 0.75rem;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
line-height: 1;
|
|
270
|
+
letter-spacing: 0.06em;
|
|
271
|
+
text-transform: uppercase;
|
|
272
|
+
border: 1px solid var(--border-default);
|
|
273
|
+
background: var(--bg-elevated);
|
|
274
|
+
color: var(--text-secondary);
|
|
275
|
+
animation: lifecycle-enter 0.2s ease-out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.lifecycle-badge.starting {
|
|
279
|
+
color: var(--lifecycle-starting);
|
|
280
|
+
background: var(--lifecycle-starting-bg);
|
|
281
|
+
border-color: rgba(250, 204, 21, 0.35);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.lifecycle-badge.running {
|
|
285
|
+
color: var(--lifecycle-running);
|
|
286
|
+
background: var(--lifecycle-running-bg);
|
|
287
|
+
border-color: rgba(56, 189, 248, 0.35);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.lifecycle-badge.ended {
|
|
291
|
+
color: var(--lifecycle-ended);
|
|
292
|
+
background: var(--lifecycle-ended-bg);
|
|
293
|
+
border-color: var(--border-default);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@keyframes lifecycle-enter {
|
|
297
|
+
from {
|
|
298
|
+
opacity: 0;
|
|
299
|
+
transform: translateY(-4px);
|
|
300
|
+
}
|
|
301
|
+
to {
|
|
302
|
+
opacity: 1;
|
|
303
|
+
transform: translateY(0);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Empty state */
|
|
308
|
+
.empty-state {
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
gap: 12px;
|
|
314
|
+
padding: 48px 24px;
|
|
315
|
+
text-align: center;
|
|
316
|
+
color: var(--text-secondary);
|
|
317
|
+
animation: empty-state-enter 0.25s ease-out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.empty-state-icon {
|
|
321
|
+
width: 64px;
|
|
322
|
+
height: 64px;
|
|
323
|
+
border-radius: 16px;
|
|
324
|
+
background: var(--bg-elevated);
|
|
325
|
+
border: 1px solid var(--border-default);
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
color: var(--text-muted);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.empty-state-icon i {
|
|
333
|
+
font-size: 2rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.empty-state-title {
|
|
337
|
+
font-family: var(--font-display);
|
|
338
|
+
font-size: 2rem;
|
|
339
|
+
font-weight: 400;
|
|
340
|
+
line-height: 1.1;
|
|
341
|
+
letter-spacing: 0.04em;
|
|
342
|
+
color: var(--text-primary);
|
|
343
|
+
margin: 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.empty-state-body {
|
|
347
|
+
font-size: 0.875rem;
|
|
348
|
+
color: var(--text-secondary);
|
|
349
|
+
margin: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@keyframes empty-state-enter {
|
|
353
|
+
from {
|
|
354
|
+
opacity: 0;
|
|
355
|
+
transform: translateY(8px);
|
|
356
|
+
}
|
|
357
|
+
to {
|
|
358
|
+
opacity: 1;
|
|
359
|
+
transform: translateY(0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
240
363
|
/* Main layout */
|
|
241
364
|
.main {
|
|
242
365
|
display: flex;
|
|
@@ -294,12 +417,24 @@ html, body {
|
|
|
294
417
|
right: 0;
|
|
295
418
|
height: 4px;
|
|
296
419
|
background: var(--text-muted);
|
|
297
|
-
transition: background 0.2s ease;
|
|
420
|
+
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.panel-accent-strip.starting {
|
|
424
|
+
background: var(--lifecycle-starting);
|
|
425
|
+
box-shadow: 0 0 12px var(--lifecycle-starting);
|
|
426
|
+
animation: lifecycle-glow 2s ease-in-out infinite;
|
|
298
427
|
}
|
|
299
428
|
|
|
300
429
|
.panel-accent-strip.running {
|
|
301
|
-
background: var(--
|
|
302
|
-
box-shadow: 0 0
|
|
430
|
+
background: var(--lifecycle-running);
|
|
431
|
+
box-shadow: 0 0 12px var(--lifecycle-running);
|
|
432
|
+
animation: lifecycle-glow 2s ease-in-out infinite;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.panel-accent-strip.ended {
|
|
436
|
+
background: var(--border-strong);
|
|
437
|
+
box-shadow: none;
|
|
303
438
|
}
|
|
304
439
|
|
|
305
440
|
.active-skill-content {
|
|
@@ -363,8 +498,16 @@ html, body {
|
|
|
363
498
|
background: var(--text-muted);
|
|
364
499
|
}
|
|
365
500
|
|
|
501
|
+
.status-dot.starting {
|
|
502
|
+
background: var(--lifecycle-starting);
|
|
503
|
+
}
|
|
504
|
+
|
|
366
505
|
.status-dot.running {
|
|
367
|
-
background: var(--running);
|
|
506
|
+
background: var(--lifecycle-running);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.status-dot.ended {
|
|
510
|
+
background: var(--lifecycle-ended);
|
|
368
511
|
}
|
|
369
512
|
|
|
370
513
|
.status-dot.error {
|
|
@@ -561,31 +704,43 @@ html, body {
|
|
|
561
704
|
}
|
|
562
705
|
}
|
|
563
706
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
transform: scale(1);
|
|
568
|
-
}
|
|
569
|
-
50% {
|
|
570
|
-
opacity: 0.4;
|
|
571
|
-
transform: scale(1.2);
|
|
572
|
-
}
|
|
707
|
+
.status-dot.running,
|
|
708
|
+
.status-dot.starting {
|
|
709
|
+
animation: lifecycle-pulse 1.5s ease-in-out infinite;
|
|
573
710
|
}
|
|
574
711
|
|
|
575
|
-
.
|
|
576
|
-
|
|
712
|
+
.lifecycle-dot {
|
|
713
|
+
width: 6px;
|
|
714
|
+
height: 6px;
|
|
715
|
+
border-radius: 50%;
|
|
716
|
+
background: currentColor;
|
|
577
717
|
}
|
|
578
718
|
|
|
719
|
+
.lifecycle-badge.starting .lifecycle-dot,
|
|
720
|
+
.lifecycle-badge.running .lifecycle-dot {
|
|
721
|
+
animation: lifecycle-pulse 1.5s ease-in-out infinite;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.panel-accent-strip.starting,
|
|
579
725
|
.panel-accent-strip.running {
|
|
580
|
-
animation: glow 2s ease-in-out infinite;
|
|
726
|
+
animation: lifecycle-glow 2s ease-in-out infinite;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
@keyframes lifecycle-pulse {
|
|
730
|
+
0%, 100% {
|
|
731
|
+
opacity: 1;
|
|
732
|
+
}
|
|
733
|
+
50% {
|
|
734
|
+
opacity: 0.5;
|
|
735
|
+
}
|
|
581
736
|
}
|
|
582
737
|
|
|
583
|
-
@keyframes glow {
|
|
738
|
+
@keyframes lifecycle-glow {
|
|
584
739
|
0%, 100% {
|
|
585
|
-
box-shadow: 0 0
|
|
740
|
+
box-shadow: 0 0 8px currentColor;
|
|
586
741
|
}
|
|
587
742
|
50% {
|
|
588
|
-
box-shadow: 0 0 16px
|
|
743
|
+
box-shadow: 0 0 16px currentColor;
|
|
589
744
|
}
|
|
590
745
|
}
|
|
591
746
|
|
|
@@ -19,6 +19,7 @@ export interface CodexHookPayload {
|
|
|
19
19
|
executed?: boolean;
|
|
20
20
|
success?: boolean;
|
|
21
21
|
durationMs?: number;
|
|
22
|
+
skill?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const EVENT_MAP: Record<string, EventType> = {
|
|
@@ -42,6 +43,7 @@ export function normalizeCodex(payload: CodexHookPayload): DashboardEvent | unde
|
|
|
42
43
|
session_id: payload.sessionId || payload.session_id || 'unknown',
|
|
43
44
|
event_type,
|
|
44
45
|
tool: payload.toolName,
|
|
46
|
+
skill: payload.skill,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -9,6 +9,7 @@ export interface KimiHookPayload {
|
|
|
9
9
|
tool_response?: Record<string, unknown>;
|
|
10
10
|
stop_reason?: string;
|
|
11
11
|
usage?: unknown;
|
|
12
|
+
skill?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const EVENT_MAP: Record<string, EventType> = {
|
|
@@ -32,6 +33,7 @@ export function normalizeKimi(payload: KimiHookPayload): DashboardEvent | undefi
|
|
|
32
33
|
session_id: payload.session_id || 'unknown',
|
|
33
34
|
event_type,
|
|
34
35
|
tool: payload.tool_name,
|
|
36
|
+
skill: payload.skill,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { detectSource, buildEvent } from './shim';
|
|
3
|
+
import { detectSource, buildEvent, getDefaultSkill } from './shim';
|
|
4
4
|
import type { AgentSource, DashboardEvent } from '../types';
|
|
5
5
|
|
|
6
6
|
describe('detectSource', () => {
|
|
@@ -20,6 +20,22 @@ describe('detectSource', () => {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
describe('getDefaultSkill', () => {
|
|
24
|
+
it('reads default skill from argv', () => {
|
|
25
|
+
assert.equal(getDefaultSkill(['node', 'shim', 'kimi', '--default-skill', 'orchestrator']), 'orchestrator');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('falls back to env var', () => {
|
|
29
|
+
process.env.CREWLOOP_DEFAULT_SKILL = 'architect';
|
|
30
|
+
assert.equal(getDefaultSkill(['node', 'shim', 'kimi']), 'architect');
|
|
31
|
+
delete process.env.CREWLOOP_DEFAULT_SKILL;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns undefined when not configured', () => {
|
|
35
|
+
assert.equal(getDefaultSkill(['node', 'shim', 'kimi']), undefined);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
23
39
|
describe('buildEvent', () => {
|
|
24
40
|
it('builds Kimi PreToolUse event', () => {
|
|
25
41
|
const event = buildEvent('kimi' as AgentSource, {
|
|
@@ -71,4 +87,51 @@ describe('buildEvent', () => {
|
|
|
71
87
|
});
|
|
72
88
|
assert.equal(event, undefined);
|
|
73
89
|
});
|
|
90
|
+
|
|
91
|
+
it('attaches default skill to session_start events', () => {
|
|
92
|
+
const event = buildEvent(
|
|
93
|
+
'kimi' as AgentSource,
|
|
94
|
+
{ hook_event_name: 'SessionStart', session_id: 'sess-1', cwd: '/project' },
|
|
95
|
+
'orchestrator'
|
|
96
|
+
);
|
|
97
|
+
assert.equal(event?.event_type, 'session_start');
|
|
98
|
+
assert.equal(event?.skill, 'orchestrator');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not attach default skill to tool events', () => {
|
|
102
|
+
const startEvent = buildEvent(
|
|
103
|
+
'kimi' as AgentSource,
|
|
104
|
+
{ hook_event_name: 'PreToolUse', session_id: 'sess-1', cwd: '/project', tool_name: 'Read' },
|
|
105
|
+
'orchestrator'
|
|
106
|
+
);
|
|
107
|
+
assert.equal(startEvent?.event_type, 'tool_start');
|
|
108
|
+
assert.equal(startEvent?.skill, undefined);
|
|
109
|
+
|
|
110
|
+
const endEvent = buildEvent(
|
|
111
|
+
'kimi' as AgentSource,
|
|
112
|
+
{ hook_event_name: 'PostToolUse', session_id: 'sess-1', cwd: '/project', tool_name: 'Read' },
|
|
113
|
+
'orchestrator'
|
|
114
|
+
);
|
|
115
|
+
assert.equal(endEvent?.event_type, 'tool_end');
|
|
116
|
+
assert.equal(endEvent?.skill, undefined);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('forwards explicit payload skill for kimi', () => {
|
|
120
|
+
const event = buildEvent('kimi' as AgentSource, {
|
|
121
|
+
hook_event_name: 'SessionStart',
|
|
122
|
+
session_id: 'sess-1',
|
|
123
|
+
cwd: '/project',
|
|
124
|
+
skill: 'architect',
|
|
125
|
+
});
|
|
126
|
+
assert.equal(event?.skill, 'architect');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('forwards explicit payload skill for codex', () => {
|
|
130
|
+
const event = buildEvent('codex' as AgentSource, {
|
|
131
|
+
sessionId: 'sess-2',
|
|
132
|
+
hook_event_name: 'SessionStart',
|
|
133
|
+
skill: 'engineer',
|
|
134
|
+
});
|
|
135
|
+
assert.equal(event?.skill, 'engineer');
|
|
136
|
+
});
|
|
74
137
|
});
|
|
@@ -6,6 +6,15 @@ import { sanitize } from '../filters/sanitize';
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_SERVER_URL = 'http://127.0.0.1:7890';
|
|
8
8
|
|
|
9
|
+
export function getDefaultSkill(argv: string[]): string | undefined {
|
|
10
|
+
const idx = argv.indexOf('--default-skill');
|
|
11
|
+
if (idx !== -1 && argv[idx + 1]) {
|
|
12
|
+
return argv[idx + 1];
|
|
13
|
+
}
|
|
14
|
+
const env = process.env.CREWLOOP_DEFAULT_SKILL;
|
|
15
|
+
return env || undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export function detectSource(argv: string[]): AgentSource | undefined {
|
|
10
19
|
const arg = argv[2];
|
|
11
20
|
if (arg === 'kimi' || arg === 'codex' || arg === 'opencode' || arg === 'log-watcher') {
|
|
@@ -42,13 +51,18 @@ export function normalizePayload(source: AgentSource, raw: unknown): DashboardEv
|
|
|
42
51
|
|
|
43
52
|
export function buildEvent(
|
|
44
53
|
source: AgentSource,
|
|
45
|
-
raw: Record<string, unknown
|
|
54
|
+
raw: Record<string, unknown>,
|
|
55
|
+
defaultSkill?: string
|
|
46
56
|
): DashboardEvent | undefined {
|
|
47
57
|
const base = normalizePayload(source, raw);
|
|
48
58
|
if (!base) {
|
|
49
59
|
return undefined;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
if (base.event_type === 'session_start' && defaultSkill) {
|
|
63
|
+
base.skill = defaultSkill;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
const isPost = base.event_type === 'tool_end';
|
|
53
67
|
const sanitized = sanitize(
|
|
54
68
|
{
|
|
@@ -100,6 +114,8 @@ export function runShim(): void {
|
|
|
100
114
|
process.exit(1);
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
const defaultSkill = getDefaultSkill(process.argv);
|
|
118
|
+
|
|
103
119
|
let raw = '';
|
|
104
120
|
process.stdin.setEncoding('utf8');
|
|
105
121
|
process.stdin.on('data', (chunk) => {
|
|
@@ -108,7 +124,7 @@ export function runShim(): void {
|
|
|
108
124
|
process.stdin.on('end', () => {
|
|
109
125
|
try {
|
|
110
126
|
const payload = JSON.parse(raw);
|
|
111
|
-
const event = buildEvent(source, payload);
|
|
127
|
+
const event = buildEvent(source, payload, defaultSkill);
|
|
112
128
|
if (event) {
|
|
113
129
|
postEvent(event);
|
|
114
130
|
}
|
|
@@ -14,6 +14,7 @@ function makeSession(overrides: Partial<Session> = {}): Session {
|
|
|
14
14
|
active_skill: 'architect',
|
|
15
15
|
active_confidence: 'explicit',
|
|
16
16
|
status: 'running',
|
|
17
|
+
lifecycle: 'running',
|
|
17
18
|
...overrides,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -28,11 +29,18 @@ describe('presenter', () => {
|
|
|
28
29
|
assert.equal(client.skill, 'architect');
|
|
29
30
|
assert.deepEqual(client.activeSkill, { name: 'architect', confidence: 'explicit' });
|
|
30
31
|
assert.equal(client.status, 'running');
|
|
32
|
+
assert.equal(client.lifecycle, 'running');
|
|
31
33
|
assert.equal(client.startTime, 1000);
|
|
32
34
|
assert.equal(client.lastActivity, 2000);
|
|
33
35
|
assert.deepEqual(client.toolCounts, { Read: 2 });
|
|
34
36
|
});
|
|
35
37
|
|
|
38
|
+
it('includes lifecycle and endedAt', () => {
|
|
39
|
+
const client = presentSession(makeSession({ lifecycle: 'ended', ended_at: 5000 }));
|
|
40
|
+
assert.equal(client.lifecycle, 'ended');
|
|
41
|
+
assert.equal(client.endedAt, 5000);
|
|
42
|
+
});
|
|
43
|
+
|
|
36
44
|
it('omits active skill when not set', () => {
|
|
37
45
|
const client = presentSession(makeSession({ active_skill: undefined }));
|
|
38
46
|
assert.equal(client.activeSkill, undefined);
|
|
@@ -26,9 +26,11 @@ export function presentSession(session: Session): ClientSession {
|
|
|
26
26
|
}
|
|
27
27
|
: undefined,
|
|
28
28
|
status: session.status,
|
|
29
|
+
lifecycle: session.lifecycle,
|
|
29
30
|
events: session.events.map(presentEvent),
|
|
30
31
|
startTime: session.started_at,
|
|
31
32
|
lastActivity: session.last_event_at,
|
|
33
|
+
endedAt: session.ended_at,
|
|
32
34
|
toolCounts: session.tool_counts,
|
|
33
35
|
};
|
|
34
36
|
}
|
|
@@ -9,6 +9,7 @@ function makeSession(overrides: Partial<Session> = {}): Session {
|
|
|
9
9
|
source: 'kimi',
|
|
10
10
|
events: [],
|
|
11
11
|
tool_counts: {},
|
|
12
|
+
lifecycle: 'running',
|
|
12
13
|
started_at: Date.now(),
|
|
13
14
|
last_event_at: Date.now(),
|
|
14
15
|
...overrides,
|
|
@@ -76,6 +77,15 @@ describe('SkillInferenceEngine', () => {
|
|
|
76
77
|
assert.equal(result.confidence, 'heuristic');
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
it('preserves explicit active skill when no new explicit signal arrives', () => {
|
|
81
|
+
const engine = new SkillInferenceEngine(skills);
|
|
82
|
+
const session = makeSession({ active_skill: 'orchestrator', active_confidence: 'explicit' });
|
|
83
|
+
const event = makeEvent({ tool: 'Read', detail: 'README.md' });
|
|
84
|
+
const result = engine.infer(event, session);
|
|
85
|
+
assert.equal(result.skill, 'orchestrator');
|
|
86
|
+
assert.equal(result.confidence, 'explicit');
|
|
87
|
+
});
|
|
88
|
+
|
|
79
89
|
it('returns unknown when nothing matches', () => {
|
|
80
90
|
const engine = new SkillInferenceEngine(skills);
|
|
81
91
|
const event = makeEvent({ tool: 'UnknownTool' });
|
|
@@ -9,6 +9,14 @@ export class SkillInferenceEngine {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
infer(event: DashboardEvent, session: Session): SkillInferenceResult {
|
|
12
|
+
const explicitSignal =
|
|
13
|
+
(event.event_type === 'skill_change' && event.skill) ||
|
|
14
|
+
(event.tool === 'Skill' && event.detail && this.normalizeSkillName(event.detail));
|
|
15
|
+
|
|
16
|
+
if (session.active_confidence === 'explicit' && !explicitSignal) {
|
|
17
|
+
return { skill: session.active_skill, confidence: 'explicit' };
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
if (event.event_type === 'skill_change' && event.skill) {
|
|
13
21
|
return { skill: event.skill, confidence: 'explicit' };
|
|
14
22
|
}
|
|
@@ -15,6 +15,7 @@ function makeEvent(overrides: Partial<DashboardEvent> = {}): DashboardEvent {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
|
|
18
19
|
describe('StateStore', () => {
|
|
19
20
|
it('creates session on first event', () => {
|
|
20
21
|
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
@@ -51,6 +52,14 @@ describe('StateStore', () => {
|
|
|
51
52
|
assert.equal(session.active_confidence, 'explicit');
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
it('sets explicit active skill from session_start event', () => {
|
|
56
|
+
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
57
|
+
store.applyEvent(makeEvent({ skill: 'orchestrator', event_type: 'session_start' }));
|
|
58
|
+
const session = store.getSession('sess-1')!;
|
|
59
|
+
assert.equal(session.active_skill, 'orchestrator');
|
|
60
|
+
assert.equal(session.active_confidence, 'explicit');
|
|
61
|
+
});
|
|
62
|
+
|
|
54
63
|
it('derives running status from tool_start', () => {
|
|
55
64
|
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
56
65
|
store.applyEvent(makeEvent({ event_type: 'tool_start' }));
|
|
@@ -85,4 +94,38 @@ describe('StateStore', () => {
|
|
|
85
94
|
assert.equal(sessions[0].id, 'b');
|
|
86
95
|
assert.equal(sessions[1].id, 'a');
|
|
87
96
|
});
|
|
97
|
+
|
|
98
|
+
it('starts with lifecycle starting on session_start', () => {
|
|
99
|
+
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
100
|
+
store.applyEvent(makeEvent({ event_type: 'session_start' }));
|
|
101
|
+
const session = store.getSession('sess-1')!;
|
|
102
|
+
assert.equal(session.lifecycle, 'starting');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('transitions to running on first tool event', () => {
|
|
106
|
+
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
107
|
+
store.applyEvent(makeEvent({ event_type: 'session_start' }));
|
|
108
|
+
store.applyEvent(makeEvent({ event_type: 'tool_start', tool: 'Read' }));
|
|
109
|
+
const session = store.getSession('sess-1')!;
|
|
110
|
+
assert.equal(session.lifecycle, 'running');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sets lifecycle ended and ended_at on session_end', () => {
|
|
114
|
+
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
115
|
+
store.applyEvent(makeEvent({ event_type: 'session_start' }));
|
|
116
|
+
const endTs = Date.now() + 1000;
|
|
117
|
+
store.applyEvent(makeEvent({ event_type: 'session_end', timestamp: endTs }));
|
|
118
|
+
const session = store.getSession('sess-1')!;
|
|
119
|
+
assert.equal(session.lifecycle, 'ended');
|
|
120
|
+
assert.equal(session.ended_at, endTs);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('keeps session ended after subsequent tool events', () => {
|
|
124
|
+
const store = new StateStore({ maxEventsPerSession: 10, sessionMaxAgeMs: 60000 });
|
|
125
|
+
store.applyEvent(makeEvent({ event_type: 'session_start' }));
|
|
126
|
+
store.applyEvent(makeEvent({ event_type: 'session_end' }));
|
|
127
|
+
store.applyEvent(makeEvent({ event_type: 'tool_start', tool: 'Read' }));
|
|
128
|
+
const session = store.getSession('sess-1')!;
|
|
129
|
+
assert.equal(session.lifecycle, 'ended');
|
|
130
|
+
});
|
|
88
131
|
});
|