groove-dev 0.27.75 → 0.27.78

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.
Files changed (97) hide show
  1. package/MOE_TRAINING_PIPELINE.md +216 -12
  2. package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
  3. package/moe-training/client/consent.js +96 -0
  4. package/moe-training/client/envelope-builder.js +56 -0
  5. package/moe-training/client/index.js +10 -0
  6. package/moe-training/client/parsers/claude-code.js +110 -0
  7. package/moe-training/client/parsers/codex.js +80 -0
  8. package/moe-training/client/parsers/gemini.js +80 -0
  9. package/moe-training/client/parsers/grok.js +16 -0
  10. package/moe-training/client/parsers/index.js +20 -0
  11. package/moe-training/client/scrubber.js +126 -0
  12. package/moe-training/client/session-attestation.js +114 -0
  13. package/moe-training/client/step-classifier.js +51 -0
  14. package/moe-training/client/trajectory-capture.js +227 -0
  15. package/moe-training/client/transmission-queue.js +93 -0
  16. package/moe-training/package-lock.json +1266 -0
  17. package/moe-training/package.json +20 -0
  18. package/moe-training/server/enrichment.js +24 -0
  19. package/moe-training/server/index.js +119 -0
  20. package/moe-training/server/ledger.js +110 -0
  21. package/moe-training/server/routes/ingest.js +96 -0
  22. package/moe-training/server/routes/sessions.js +43 -0
  23. package/moe-training/server/routes/stats.js +31 -0
  24. package/moe-training/server/scoring.js +63 -0
  25. package/moe-training/server/session-registry.js +156 -0
  26. package/moe-training/server/stats.js +129 -0
  27. package/moe-training/server/stitcher.js +69 -0
  28. package/moe-training/server/storage.js +147 -0
  29. package/moe-training/server/verifier.js +102 -0
  30. package/moe-training/shared/constants.js +30 -0
  31. package/moe-training/shared/crypto.js +45 -0
  32. package/moe-training/shared/envelope-schema.js +220 -0
  33. package/moe-training/test/client/consent.test.js +121 -0
  34. package/moe-training/test/client/envelope-builder.test.js +107 -0
  35. package/moe-training/test/client/parsers/claude-code.test.js +119 -0
  36. package/moe-training/test/client/parsers/codex.test.js +83 -0
  37. package/moe-training/test/client/parsers/gemini.test.js +99 -0
  38. package/moe-training/test/client/scrubber.test.js +133 -0
  39. package/moe-training/test/client/session-attestation-security.test.js +95 -0
  40. package/moe-training/test/client/step-classifier.test.js +88 -0
  41. package/moe-training/test/integration/handshake.test.js +260 -0
  42. package/moe-training/test/server/ingest-security.test.js +166 -0
  43. package/moe-training/test/server/ledger.test.js +131 -0
  44. package/moe-training/test/server/scoring.test.js +242 -0
  45. package/moe-training/test/server/session-registry.test.js +125 -0
  46. package/moe-training/test/server/stitcher.test.js +157 -0
  47. package/moe-training/test/server/verifier.test.js +232 -0
  48. package/moe-training/test/shared/crypto.test.js +87 -0
  49. package/moe-training/test/shared/envelope-schema.test.js +351 -0
  50. package/node_modules/@groove-dev/cli/package.json +1 -1
  51. package/node_modules/@groove-dev/daemon/package.json +1 -1
  52. package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
  53. package/node_modules/@groove-dev/daemon/src/api.js +77 -0
  54. package/node_modules/@groove-dev/daemon/src/index.js +61 -0
  55. package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
  56. package/node_modules/@groove-dev/daemon/src/preview.js +14 -0
  57. package/node_modules/@groove-dev/daemon/src/process.js +203 -1
  58. package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
  59. package/node_modules/@groove-dev/daemon/src/state.js +20 -1
  60. package/node_modules/@groove-dev/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
  61. package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +1 -0
  62. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  63. package/node_modules/@groove-dev/gui/package.json +1 -1
  64. package/node_modules/@groove-dev/gui/src/app.css +12 -0
  65. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +32 -27
  66. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +26 -24
  67. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +34 -6
  68. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +19 -4
  69. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  70. package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
  71. package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
  72. package/package.json +1 -1
  73. package/packages/cli/package.json +1 -1
  74. package/packages/daemon/package.json +1 -1
  75. package/packages/daemon/src/agent-loop.js +48 -5
  76. package/packages/daemon/src/api.js +77 -0
  77. package/packages/daemon/src/index.js +61 -0
  78. package/packages/daemon/src/journalist.js +64 -21
  79. package/packages/daemon/src/preview.js +14 -0
  80. package/packages/daemon/src/process.js +203 -1
  81. package/packages/daemon/src/providers/grok.js +15 -0
  82. package/packages/daemon/src/state.js +20 -1
  83. package/packages/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
  84. package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
  85. package/packages/gui/dist/index.html +2 -2
  86. package/packages/gui/package.json +1 -1
  87. package/packages/gui/src/app.css +12 -0
  88. package/packages/gui/src/components/chat/chat-input.jsx +32 -27
  89. package/packages/gui/src/components/chat/chat-messages.jsx +26 -24
  90. package/packages/gui/src/components/preview/preview-toolbar.jsx +34 -6
  91. package/packages/gui/src/components/preview/preview-workspace.jsx +19 -4
  92. package/packages/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  93. package/packages/gui/src/stores/groove.js +32 -0
  94. package/packages/gui/src/views/settings.jsx +167 -1
  95. package/welcome.png +0 -0
  96. package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +0 -1
  97. package/packages/gui/dist/assets/index-CVzz6zyb.css +0 -1
@@ -0,0 +1,220 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { SUPPORTED_PROVIDERS, MODEL_TIERS } from './constants.js';
4
+
5
+ export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination'];
6
+
7
+ const VALID_MODEL_ENGINES = Object.keys(MODEL_TIERS);
8
+ const VALID_COMPLEXITIES = ['light', 'medium', 'heavy'];
9
+ const VALID_OUTCOME_STATUSES = ['SUCCESS', 'CRASH', 'KILLED'];
10
+ const MAX_STEPS_PER_ENVELOPE = 500;
11
+ const MAX_STEP_CONTENT_LENGTH = 10_000;
12
+ const MAX_TOKEN_COUNT = 100_000;
13
+ const MAX_STEP_NUMBER = 50_000;
14
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
15
+ const ONE_HOUR_MS = 60 * 60 * 1000;
16
+ const HEX_32 = /^[0-9a-f]{32}$/;
17
+ const HEX_64 = /^[0-9a-f]{64}$/;
18
+ const MAX_OUTCOME_NUMERIC = 50_000;
19
+
20
+ export class EnvelopeBuilder {
21
+ constructor() {
22
+ this._steps = [];
23
+ }
24
+
25
+ addStep(step) {
26
+ this._steps.push(step);
27
+ return this;
28
+ }
29
+
30
+ build(envelopeId, sessionId, chunkSequence, contributorId, metadata) {
31
+ return {
32
+ envelope_id: envelopeId,
33
+ session_id: sessionId,
34
+ chunk_sequence: chunkSequence,
35
+ contributor_id: contributorId,
36
+ attestation: { session_hmac: '', sequence: 0, app_version_hash: '' },
37
+ metadata,
38
+ trajectory_log: [...this._steps],
39
+ };
40
+ }
41
+
42
+ static buildSessionClose(envelopeId, sessionId, outcome) {
43
+ return {
44
+ envelope_id: envelopeId,
45
+ session_id: sessionId,
46
+ type: 'SESSION_CLOSE',
47
+ attestation: { session_hmac: '', sequence: 0, app_version_hash: '' },
48
+ outcome,
49
+ };
50
+ }
51
+ }
52
+
53
+ export function validateEnvelope(envelope) {
54
+ const errors = [];
55
+
56
+ if (!envelope) {
57
+ return { valid: false, errors: ['Envelope is null or undefined'] };
58
+ }
59
+
60
+ if (envelope.type === 'SESSION_CLOSE') {
61
+ return validateSessionClose(envelope);
62
+ }
63
+
64
+ if (!envelope.session_id || typeof envelope.session_id !== 'string') {
65
+ errors.push('Missing or invalid session_id');
66
+ }
67
+ if (typeof envelope.chunk_sequence !== 'number') {
68
+ errors.push('Missing or invalid chunk_sequence');
69
+ }
70
+
71
+ // contributor_id: must be 32-char hex (UUID without dashes)
72
+ if (!envelope.contributor_id || typeof envelope.contributor_id !== 'string') {
73
+ errors.push('Missing or invalid contributor_id');
74
+ } else if (!HEX_32.test(envelope.contributor_id)) {
75
+ errors.push('contributor_id must be a 32-character hex string');
76
+ }
77
+
78
+ // Attestation validation
79
+ if (!envelope.attestation || typeof envelope.attestation !== 'object') {
80
+ errors.push('Missing attestation');
81
+ } else {
82
+ if (typeof envelope.attestation.session_hmac !== 'string' || !HEX_64.test(envelope.attestation.session_hmac)) {
83
+ errors.push('attestation.session_hmac must be exactly 64 hex characters');
84
+ }
85
+ if (typeof envelope.attestation.sequence !== 'number' || !Number.isInteger(envelope.attestation.sequence) || envelope.attestation.sequence < 0 || envelope.attestation.sequence > 1_000_000) {
86
+ errors.push('attestation.sequence must be a non-negative integer, max 1000000');
87
+ }
88
+ if (typeof envelope.attestation.app_version_hash !== 'string' || !HEX_64.test(envelope.attestation.app_version_hash)) {
89
+ errors.push('attestation.app_version_hash must be exactly 64 hex characters');
90
+ }
91
+ }
92
+
93
+ // Metadata validation
94
+ if (!envelope.metadata || typeof envelope.metadata !== 'object') {
95
+ errors.push('Missing metadata');
96
+ } else {
97
+ const m = envelope.metadata;
98
+ if (!m.provider || !SUPPORTED_PROVIDERS.includes(m.provider)) {
99
+ errors.push(`metadata.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}`);
100
+ }
101
+ if (!m.model_engine || !VALID_MODEL_ENGINES.includes(m.model_engine)) {
102
+ errors.push(`metadata.model_engine must be one of: ${VALID_MODEL_ENGINES.join(', ')}`);
103
+ }
104
+ if (!m.agent_role || typeof m.agent_role !== 'string' || m.agent_role.length > 50) {
105
+ errors.push('metadata.agent_role must be a string, max 50 characters');
106
+ }
107
+ if (!m.agent_id || typeof m.agent_id !== 'string' || m.agent_id.length > 100) {
108
+ errors.push('metadata.agent_id must be a string, max 100 characters');
109
+ }
110
+ if (m.team_size !== undefined && m.team_size !== null) {
111
+ if (!Number.isInteger(m.team_size) || m.team_size < 1 || m.team_size > 50) {
112
+ errors.push('metadata.team_size must be an integer 1-50');
113
+ }
114
+ }
115
+ if (m.task_complexity !== undefined && m.task_complexity !== null) {
116
+ if (!VALID_COMPLEXITIES.includes(m.task_complexity)) {
117
+ errors.push('metadata.task_complexity must be light, medium, or heavy');
118
+ }
119
+ }
120
+ if (m.groove_version !== undefined && m.groove_version !== null) {
121
+ if (typeof m.groove_version !== 'string' || m.groove_version.length > 20) {
122
+ errors.push('metadata.groove_version must be a string, max 20 characters');
123
+ }
124
+ }
125
+ // session_quality is ignored from client — server derives quality
126
+ }
127
+
128
+ // Trajectory log validation
129
+ if (!Array.isArray(envelope.trajectory_log)) {
130
+ errors.push('Missing or invalid trajectory_log');
131
+ } else {
132
+ if (envelope.trajectory_log.length > MAX_STEPS_PER_ENVELOPE) {
133
+ errors.push(`trajectory_log exceeds maximum of ${MAX_STEPS_PER_ENVELOPE} steps (got ${envelope.trajectory_log.length})`);
134
+ }
135
+
136
+ const now = Date.now();
137
+ const sevenDaysAgo = now - SEVEN_DAYS_MS;
138
+ const futureLimit = now + ONE_HOUR_MS;
139
+
140
+ for (let i = 0; i < Math.min(envelope.trajectory_log.length, MAX_STEPS_PER_ENVELOPE); i++) {
141
+ const step = envelope.trajectory_log[i];
142
+ if (!STEP_TYPES.includes(step.type)) {
143
+ errors.push(`Invalid step type "${step.type}" at index ${i}`);
144
+ }
145
+ if (typeof step.step !== 'number' || !Number.isInteger(step.step) || step.step < 0 || step.step > MAX_STEP_NUMBER) {
146
+ errors.push(`step.step must be a non-negative integer, max ${MAX_STEP_NUMBER} at index ${i}`);
147
+ }
148
+ if (typeof step.timestamp !== 'number') {
149
+ errors.push(`Missing timestamp at index ${i}`);
150
+ } else {
151
+ const tsMs = step.timestamp < 1e12 ? step.timestamp * 1000 : step.timestamp;
152
+ if (tsMs < sevenDaysAgo || tsMs > futureLimit) {
153
+ errors.push(`Timestamp out of range at index ${i} (must be within last 7 days, max 1 hour in future)`);
154
+ }
155
+ }
156
+ if (step.content !== undefined && typeof step.content === 'string' && step.content.length > MAX_STEP_CONTENT_LENGTH) {
157
+ errors.push(`step.content exceeds ${MAX_STEP_CONTENT_LENGTH} characters at index ${i}`);
158
+ }
159
+ if (step.token_count !== undefined) {
160
+ if (typeof step.token_count !== 'number' || step.token_count < 0 || step.token_count > MAX_TOKEN_COUNT) {
161
+ errors.push(`step.token_count must be 0-${MAX_TOKEN_COUNT} at index ${i}`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return { valid: errors.length === 0, errors };
168
+ }
169
+
170
+ function validateSessionClose(envelope) {
171
+ const errors = [];
172
+
173
+ if (!envelope.session_id || typeof envelope.session_id !== 'string') {
174
+ errors.push('Missing or invalid session_id');
175
+ }
176
+ if (envelope.type !== 'SESSION_CLOSE') {
177
+ errors.push('Invalid type for SESSION_CLOSE envelope');
178
+ }
179
+
180
+ // Attestation validation
181
+ if (!envelope.attestation || typeof envelope.attestation !== 'object') {
182
+ errors.push('Missing attestation');
183
+ } else {
184
+ if (typeof envelope.attestation.session_hmac !== 'string' || !HEX_64.test(envelope.attestation.session_hmac)) {
185
+ errors.push('attestation.session_hmac must be exactly 64 hex characters');
186
+ }
187
+ if (typeof envelope.attestation.sequence !== 'number' || !Number.isInteger(envelope.attestation.sequence) || envelope.attestation.sequence < 0 || envelope.attestation.sequence > 1_000_000) {
188
+ errors.push('attestation.sequence must be a non-negative integer, max 1000000');
189
+ }
190
+ if (typeof envelope.attestation.app_version_hash !== 'string' || !HEX_64.test(envelope.attestation.app_version_hash)) {
191
+ errors.push('attestation.app_version_hash must be exactly 64 hex characters');
192
+ }
193
+ }
194
+
195
+ // Outcome validation
196
+ if (!envelope.outcome || typeof envelope.outcome !== 'object') {
197
+ errors.push('Missing outcome');
198
+ } else {
199
+ if (!VALID_OUTCOME_STATUSES.includes(envelope.outcome.status)) {
200
+ errors.push(`outcome.status must be one of: ${VALID_OUTCOME_STATUSES.join(', ')}`);
201
+ }
202
+ if (typeof envelope.outcome.total_steps !== 'number' || !Number.isInteger(envelope.outcome.total_steps) || envelope.outcome.total_steps < 0 || envelope.outcome.total_steps > MAX_OUTCOME_NUMERIC) {
203
+ errors.push('Missing or invalid outcome.total_steps');
204
+ }
205
+ if (typeof envelope.outcome.total_chunks !== 'number' || !Number.isInteger(envelope.outcome.total_chunks) || envelope.outcome.total_chunks < 0 || envelope.outcome.total_chunks > MAX_OUTCOME_NUMERIC) {
206
+ errors.push('Missing or invalid outcome.total_chunks');
207
+ }
208
+ const numericFields = ['user_interventions', 'total_tokens', 'duration_seconds', 'files_modified', 'errors_encountered', 'errors_recovered', 'coordination_events'];
209
+ for (const field of numericFields) {
210
+ if (envelope.outcome[field] !== undefined) {
211
+ const v = envelope.outcome[field];
212
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v > MAX_OUTCOME_NUMERIC) {
213
+ errors.push(`outcome.${field} must be a non-negative integer, max ${MAX_OUTCOME_NUMERIC}`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ return { valid: errors.length === 0, errors };
220
+ }
@@ -0,0 +1,121 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { describe, it, beforeEach, afterEach } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { tmpdir } from 'node:os';
8
+ import { ConsentManager } from '../../client/consent.js';
9
+
10
+ describe('ConsentManager', () => {
11
+ let tmpDir, dbPath, manager;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = mkdtempSync(join(tmpdir(), 'consent-test-'));
15
+ dbPath = join(tmpDir, 'consent.db');
16
+ manager = new ConsentManager(dbPath);
17
+ });
18
+
19
+ afterEach(() => {
20
+ manager.close();
21
+ rmSync(tmpDir, { recursive: true });
22
+ });
23
+
24
+ it('opt in then isOptedIn returns true', () => {
25
+ manager.recordConsent('user1', true, '1.0');
26
+ assert.equal(manager.isOptedIn('user1'), true);
27
+ });
28
+
29
+ it('opt out then isOptedIn returns false', () => {
30
+ manager.recordConsent('user1', true, '1.0');
31
+ manager.revokeConsent('user1');
32
+ assert.equal(manager.isOptedIn('user1'), false);
33
+ });
34
+
35
+ it('no consent record returns false', () => {
36
+ assert.equal(manager.isOptedIn('nonexistent'), false);
37
+ });
38
+
39
+ it('version mismatch returns false', () => {
40
+ manager.recordConsent('user1', true, '0.9');
41
+ assert.equal(manager.isOptedIn('user1'), false);
42
+ });
43
+
44
+ it('getOptedInCount counts correctly', () => {
45
+ manager.recordConsent('user1', true, '1.0');
46
+ manager.recordConsent('user2', true, '1.0');
47
+ manager.recordConsent('user3', false, '1.0');
48
+ assert.equal(manager.getOptedInCount(), 2);
49
+ });
50
+
51
+ it('getConsentHistory returns all records', () => {
52
+ manager.recordConsent('user1', true, '1.0', { source: 'ui' });
53
+ manager.revokeConsent('user1');
54
+ const history = manager.getConsentHistory('user1');
55
+ assert.equal(history.length, 2);
56
+ assert.equal(history[0].opted_in, true);
57
+ assert.deepEqual(history[0].metadata, { source: 'ui' });
58
+ assert.equal(history[1].opted_in, false);
59
+ });
60
+ });
61
+
62
+ describe('ConsentManager.getOrCreateUserId', () => {
63
+ let tmpDir, userIdPath;
64
+
65
+ beforeEach(() => {
66
+ tmpDir = mkdtempSync(join(tmpdir(), 'userid-test-'));
67
+ userIdPath = join(tmpDir, 'user_id');
68
+ });
69
+
70
+ afterEach(() => {
71
+ rmSync(tmpDir, { recursive: true });
72
+ });
73
+
74
+ it('creates user_id file and returns id', () => {
75
+ const id = ConsentManager.getOrCreateUserId(userIdPath);
76
+ assert.ok(id.length > 0);
77
+ assert.ok(existsSync(userIdPath));
78
+ });
79
+
80
+ it('returns same id on second call', () => {
81
+ const id1 = ConsentManager.getOrCreateUserId(userIdPath);
82
+ const id2 = ConsentManager.getOrCreateUserId(userIdPath);
83
+ assert.equal(id1, id2);
84
+ });
85
+ });
86
+
87
+ describe('ConsentManager.isCaptureEnabled', () => {
88
+ let tmpDir;
89
+
90
+ beforeEach(() => {
91
+ tmpDir = mkdtempSync(join(tmpdir(), 'capture-test-'));
92
+ });
93
+
94
+ afterEach(() => {
95
+ rmSync(tmpDir, { recursive: true });
96
+ });
97
+
98
+ it('returns false when no user_id file exists', () => {
99
+ const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'c.db'));
100
+ assert.equal(result, false);
101
+ });
102
+
103
+ it('returns false when user not opted in', () => {
104
+ const uidPath = join(tmpDir, 'user_id');
105
+ const uid = ConsentManager.getOrCreateUserId(uidPath);
106
+ const dbPath = join(tmpDir, 'consent.db');
107
+ const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
108
+ assert.equal(result, false);
109
+ });
110
+
111
+ it('returns true when user is opted in', () => {
112
+ const uidPath = join(tmpDir, 'user_id');
113
+ const uid = ConsentManager.getOrCreateUserId(uidPath);
114
+ const dbPath = join(tmpDir, 'consent.db');
115
+ const mgr = new ConsentManager(dbPath);
116
+ mgr.recordConsent(uid, true, '1.0');
117
+ mgr.close();
118
+ const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
119
+ assert.equal(result, true);
120
+ });
121
+ });
@@ -0,0 +1,107 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { EnvelopeBuilder } from '../../client/envelope-builder.js';
6
+ import { CHUNK_SIZE } from '../../shared/constants.js';
7
+
8
+ const metadata = {
9
+ model_engine: 'claude-opus-4-6',
10
+ provider: 'claude-code',
11
+ agent_role: 'backend',
12
+ agent_id: 'backend-1',
13
+ task_complexity: 'medium',
14
+ team_size: 2,
15
+ session_quality: 80,
16
+ groove_version: '0.27.0',
17
+ };
18
+
19
+ describe('EnvelopeBuilder', () => {
20
+ it('adding steps below CHUNK_SIZE returns null', () => {
21
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
22
+ for (let i = 0; i < CHUNK_SIZE - 1; i++) {
23
+ const result = builder.addStep({ step: i + 1, type: 'thought', timestamp: Date.now() / 1000 });
24
+ assert.equal(result, null);
25
+ }
26
+ });
27
+
28
+ it('adding step that hits CHUNK_SIZE returns envelope', () => {
29
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
30
+ let envelope = null;
31
+ for (let i = 0; i < CHUNK_SIZE; i++) {
32
+ envelope = builder.addStep({ step: i + 1, type: 'thought', timestamp: Date.now() / 1000 });
33
+ }
34
+ assert.ok(envelope);
35
+ assert.equal(envelope.trajectory_log.length, CHUNK_SIZE);
36
+ assert.equal(envelope.session_id, 'sess_1');
37
+ assert.equal(envelope.contributor_id, 'user_1');
38
+ assert.ok(envelope.envelope_id.startsWith('env_'));
39
+ assert.equal(envelope.chunk_sequence, 0);
40
+ });
41
+
42
+ it('flush returns remaining steps', () => {
43
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
44
+ builder.addStep({ step: 1, type: 'thought', timestamp: 123 });
45
+ builder.addStep({ step: 2, type: 'action', timestamp: 124 });
46
+ const envelope = builder.flush();
47
+ assert.ok(envelope);
48
+ assert.equal(envelope.trajectory_log.length, 2);
49
+ });
50
+
51
+ it('flush returns null when buffer is empty', () => {
52
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
53
+ assert.equal(builder.flush(), null);
54
+ });
55
+
56
+ it('SESSION_CLOSE includes outcome data', () => {
57
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
58
+ const outcome = {
59
+ status: 'SUCCESS',
60
+ user_interventions: 1,
61
+ total_steps: 50,
62
+ total_chunks: 1,
63
+ total_tokens: 5000,
64
+ duration_seconds: 120,
65
+ files_modified: 3,
66
+ errors_encountered: 1,
67
+ errors_recovered: 1,
68
+ coordination_events: 0,
69
+ };
70
+ const close = builder.buildSessionClose(outcome);
71
+ assert.ok(close.envelope_id.startsWith('env_'));
72
+ assert.equal(close.session_id, 'sess_1');
73
+ assert.equal(close.type, 'SESSION_CLOSE');
74
+ assert.deepEqual(close.outcome, outcome);
75
+ });
76
+
77
+ it('truncates step content at 10000 characters', () => {
78
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
79
+ const longContent = 'x'.repeat(15_000);
80
+ builder.addStep({ step: 1, type: 'thought', timestamp: 123, content: longContent });
81
+ const envelope = builder.flush();
82
+ assert.equal(envelope.trajectory_log[0].content.length, 10_000);
83
+ });
84
+
85
+ it('caps token_count at 100000', () => {
86
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
87
+ builder.addStep({ step: 1, type: 'thought', timestamp: 123, token_count: 999_999 });
88
+ const envelope = builder.flush();
89
+ assert.equal(envelope.trajectory_log[0].token_count, 100_000);
90
+ });
91
+
92
+ it('chunk sequence increments correctly', () => {
93
+ const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
94
+ let first = null;
95
+ let second = null;
96
+
97
+ for (let i = 0; i < CHUNK_SIZE; i++) {
98
+ first = builder.addStep({ step: i + 1, type: 'thought', timestamp: Date.now() / 1000 }) || first;
99
+ }
100
+ for (let i = 0; i < CHUNK_SIZE; i++) {
101
+ second = builder.addStep({ step: CHUNK_SIZE + i + 1, type: 'action', timestamp: Date.now() / 1000 }) || second;
102
+ }
103
+
104
+ assert.equal(first.chunk_sequence, 0);
105
+ assert.equal(second.chunk_sequence, 1);
106
+ });
107
+ });
@@ -0,0 +1,119 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { ClaudeCodeParser } from '../../../client/parsers/claude-code.js';
6
+
7
+ describe('ClaudeCodeParser', () => {
8
+ it('parses assistant message as thought', () => {
9
+ const parser = new ClaudeCodeParser();
10
+ const event = {
11
+ type: 'assistant',
12
+ message: {
13
+ content: [{ type: 'text', text: 'I need to fix the bug' }],
14
+ usage: { input_tokens: 100, output_tokens: 50 },
15
+ },
16
+ };
17
+ const result = parser.parseEvent(event);
18
+ assert.equal(result.type, 'thought');
19
+ assert.equal(result.content, 'I need to fix the bug');
20
+ assert.equal(result.token_count, 50);
21
+ });
22
+
23
+ it('parses tool_use block as action', () => {
24
+ const parser = new ClaudeCodeParser();
25
+ const event = {
26
+ type: 'assistant',
27
+ message: {
28
+ content: [{ type: 'tool_use', id: 'tu_1', name: 'Read', input: { file_path: '/test.js' } }],
29
+ usage: { output_tokens: 10 },
30
+ },
31
+ };
32
+ const result = parser.parseEvent(event);
33
+ assert.equal(result.type, 'action');
34
+ assert.equal(result.tool, 'Read');
35
+ assert.deepEqual(result.arguments, { file_path: '/test.js' });
36
+ });
37
+
38
+ it('parses tool_result as observation', () => {
39
+ const parser = new ClaudeCodeParser();
40
+ // First register a tool use
41
+ parser.parseEvent({
42
+ type: 'assistant',
43
+ message: {
44
+ content: [{ type: 'tool_use', id: 'tu_2', name: 'Read', input: {} }],
45
+ usage: {},
46
+ },
47
+ });
48
+ // Then parse result
49
+ const event = {
50
+ type: 'assistant',
51
+ message: {
52
+ content: [{ type: 'tool_result', tool_use_id: 'tu_2', content: 'file contents here', is_error: false }],
53
+ usage: {},
54
+ },
55
+ };
56
+ const result = parser.parseEvent(event);
57
+ assert.equal(result.type, 'observation');
58
+ assert.equal(result.content, 'file contents here');
59
+ assert.equal(result.is_error, false);
60
+ });
61
+
62
+ it('parses error tool_result as error', () => {
63
+ const parser = new ClaudeCodeParser();
64
+ const event = {
65
+ type: 'assistant',
66
+ message: {
67
+ content: [{ type: 'tool_result', tool_use_id: 'tu_3', content: 'Permission denied', is_error: true }],
68
+ usage: {},
69
+ },
70
+ };
71
+ const result = parser.parseEvent(event);
72
+ assert.equal(result.type, 'error');
73
+ assert.equal(result.is_error, true);
74
+ });
75
+
76
+ it('parses result event as resolution', () => {
77
+ const parser = new ClaudeCodeParser();
78
+ const event = { type: 'result', result: 'Task completed successfully', total_tokens_used: 500 };
79
+ const result = parser.parseEvent(event);
80
+ assert.equal(result.type, 'resolution');
81
+ assert.equal(result.content, 'Task completed successfully');
82
+ assert.equal(result.token_count, 500);
83
+ });
84
+
85
+ it('extracts tokens from assistant event', () => {
86
+ const parser = new ClaudeCodeParser();
87
+ const event = {
88
+ type: 'assistant',
89
+ message: {
90
+ usage: {
91
+ input_tokens: 100,
92
+ output_tokens: 50,
93
+ cache_read_input_tokens: 30,
94
+ cache_creation_input_tokens: 10,
95
+ },
96
+ },
97
+ };
98
+ const tokens = parser.extractTokens(event);
99
+ assert.deepEqual(tokens, { input: 100, output: 50, cacheRead: 30, cacheCreation: 10 });
100
+ });
101
+
102
+ it('extracts session id', () => {
103
+ const parser = new ClaudeCodeParser();
104
+ assert.equal(parser.extractSessionId({ session_id: 'sess-abc' }), 'sess-abc');
105
+ assert.equal(parser.extractSessionId({}), null);
106
+ });
107
+
108
+ it('extracts model from assistant event', () => {
109
+ const parser = new ClaudeCodeParser();
110
+ assert.equal(parser.extractModel({ type: 'assistant', message: { model: 'claude-opus-4-6' } }), 'claude-opus-4-6');
111
+ assert.equal(parser.extractModel({ type: 'result' }), null);
112
+ });
113
+
114
+ it('returns null for unknown event types', () => {
115
+ const parser = new ClaudeCodeParser();
116
+ assert.equal(parser.parseEvent({ type: 'system' }), null);
117
+ assert.equal(parser.parseEvent(null), null);
118
+ });
119
+ });
@@ -0,0 +1,83 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { CodexParser } from '../../../client/parsers/codex.js';
6
+
7
+ describe('CodexParser', () => {
8
+ it('parses item.started agent_message as thought', () => {
9
+ const parser = new CodexParser();
10
+ const result = parser.parseEvent({
11
+ type: 'item.started',
12
+ item: { type: 'agent_message', text: 'Let me analyze this' },
13
+ });
14
+ assert.equal(result.type, 'thought');
15
+ assert.equal(result.content, 'Let me analyze this');
16
+ });
17
+
18
+ it('parses item.started command_execution as action', () => {
19
+ const parser = new CodexParser();
20
+ const result = parser.parseEvent({
21
+ type: 'item.started',
22
+ item: { type: 'command_execution', command: 'ls -la' },
23
+ });
24
+ assert.equal(result.type, 'action');
25
+ assert.equal(result.tool, 'command_execution');
26
+ assert.deepEqual(result.arguments, { command: 'ls -la' });
27
+ });
28
+
29
+ it('parses item.started file_edit as action', () => {
30
+ const parser = new CodexParser();
31
+ const result = parser.parseEvent({
32
+ type: 'item.started',
33
+ item: { type: 'file_edit', path: '/src/app.js' },
34
+ });
35
+ assert.equal(result.type, 'action');
36
+ assert.equal(result.tool, 'file_edit');
37
+ });
38
+
39
+ it('parses item.completed command_execution as observation', () => {
40
+ const parser = new CodexParser();
41
+ const result = parser.parseEvent({
42
+ type: 'item.completed',
43
+ item: { type: 'command_execution', aggregated_output: 'file1\nfile2', exit_code: 0 },
44
+ });
45
+ assert.equal(result.type, 'observation');
46
+ assert.equal(result.content, 'file1\nfile2');
47
+ });
48
+
49
+ it('parses item.completed with error exit code as error', () => {
50
+ const parser = new CodexParser();
51
+ const result = parser.parseEvent({
52
+ type: 'item.completed',
53
+ item: { type: 'command_execution', aggregated_output: 'ENOENT', exit_code: 1 },
54
+ });
55
+ assert.equal(result.type, 'error');
56
+ });
57
+
58
+ it('parses turn.completed as resolution', () => {
59
+ const parser = new CodexParser();
60
+ const result = parser.parseEvent({ type: 'turn.completed' });
61
+ assert.equal(result.type, 'resolution');
62
+ });
63
+
64
+ it('extracts tokens from usage', () => {
65
+ const parser = new CodexParser();
66
+ const tokens = parser.extractTokens({
67
+ type: 'item.completed',
68
+ usage: { input_tokens: 200, output_tokens: 100 },
69
+ });
70
+ assert.deepEqual(tokens, { input: 200, output: 100, cacheRead: 0, cacheCreation: 0 });
71
+ });
72
+
73
+ it('extracts session id from thread.started', () => {
74
+ const parser = new CodexParser();
75
+ assert.equal(parser.extractSessionId({ type: 'thread.started', thread_id: 'th_123' }), 'th_123');
76
+ assert.equal(parser.extractSessionId({ type: 'item.started' }), null);
77
+ });
78
+
79
+ it('returns null for unknown event types', () => {
80
+ const parser = new CodexParser();
81
+ assert.equal(parser.parseEvent({ type: 'unknown.event' }), null);
82
+ });
83
+ });