@archznn/crewloop-skills 0.4.3 → 0.5.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 +1 -1
- 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 +34 -4
- 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 +274 -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/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 +165 -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/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/types.d.ts +4 -0
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +1 -1
- 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/types.ts +4 -0
- package/skills/orchestrator/SKILL.md +6 -0
|
@@ -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
|
});
|
|
@@ -32,11 +32,19 @@ export class StateStore {
|
|
|
32
32
|
session.tool_counts[event.tool] = (session.tool_counts[event.tool] || 0) + 1;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
if (event.skill) {
|
|
35
|
+
if (event.event_type === 'session_start' && event.skill) {
|
|
36
|
+
session.active_skill = event.skill;
|
|
37
|
+
session.active_confidence = 'explicit';
|
|
38
|
+
} else if (event.skill) {
|
|
36
39
|
session.active_skill = event.skill;
|
|
37
40
|
session.active_confidence = event.event_type === 'skill_change' ? 'explicit' : 'heuristic';
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
if (event.event_type === 'session_end') {
|
|
44
|
+
session.ended_at = event.timestamp;
|
|
45
|
+
}
|
|
46
|
+
session.lifecycle = deriveLifecycle(event, session);
|
|
47
|
+
|
|
40
48
|
session.status = deriveSessionStatus(event);
|
|
41
49
|
|
|
42
50
|
this.sessions.set(event.session_id, session);
|
|
@@ -91,6 +99,7 @@ export class StateStore {
|
|
|
91
99
|
source,
|
|
92
100
|
events: [],
|
|
93
101
|
tool_counts: {},
|
|
102
|
+
lifecycle: 'starting',
|
|
94
103
|
started_at: now,
|
|
95
104
|
last_event_at: now,
|
|
96
105
|
};
|
|
@@ -99,6 +108,16 @@ export class StateStore {
|
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
function deriveLifecycle(event: DashboardEvent, session: Session): 'starting' | 'running' | 'ended' {
|
|
112
|
+
if (event.event_type === 'session_end' || session.ended_at) {
|
|
113
|
+
return 'ended';
|
|
114
|
+
}
|
|
115
|
+
if (event.event_type === 'session_start' && session.events.length <= 1) {
|
|
116
|
+
return 'starting';
|
|
117
|
+
}
|
|
118
|
+
return 'running';
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
function deriveSessionStatus(event: DashboardEvent): EventStatus | undefined {
|
|
103
122
|
switch (event.event_type) {
|
|
104
123
|
case 'session_start':
|
|
@@ -28,10 +28,12 @@ export interface Session {
|
|
|
28
28
|
active_skill?: string;
|
|
29
29
|
active_confidence?: 'explicit' | 'heuristic' | 'unknown';
|
|
30
30
|
status?: EventStatus;
|
|
31
|
+
lifecycle: 'starting' | 'running' | 'ended';
|
|
31
32
|
events: DashboardEvent[];
|
|
32
33
|
tool_counts: Record<string, number>;
|
|
33
34
|
started_at: number;
|
|
34
35
|
last_event_at: number;
|
|
36
|
+
ended_at?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface DashboardState {
|
|
@@ -60,9 +62,11 @@ export interface ClientSession {
|
|
|
60
62
|
skill?: string;
|
|
61
63
|
activeSkill?: ClientActiveSkill;
|
|
62
64
|
status?: EventStatus;
|
|
65
|
+
lifecycle: 'starting' | 'running' | 'ended';
|
|
63
66
|
events: ClientEvent[];
|
|
64
67
|
startTime: number;
|
|
65
68
|
lastActivity: number;
|
|
69
|
+
endedAt?: number;
|
|
66
70
|
toolCounts: Record<string, number>;
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -11,6 +11,12 @@ You are a technical product manager and discovery specialist. Your job is to ext
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## DASHBOARD LIFECYCLE
|
|
15
|
+
|
|
16
|
+
When this skill is loaded at the start of a session, the CrewLoop dashboard should display an active session named `orchestrator`. If the agent supports lifecycle hooks, ensure the first event sent to the dashboard marks `orchestrator` as the active skill.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
14
20
|
### 🚨 MANDATORY: Read Reference & Template Files
|
|
15
21
|
Before taking any action, you MUST read the global conventions in [conventions.md](../../references/conventions.md), the workflow in [workflow.md](../../references/workflow.md), and any local reference files or directories (such as `references/` or `assets/`) if present. Never skip this step or make assumptions about the guidelines.
|
|
16
22
|
|