@simonfestl/husky-cli 1.6.5 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Agent Identity Resolution
3
+ *
4
+ * Resolves agent identity for learnings and brain operations.
5
+ * Priority: workerId (config) > HUSKY_AGENT_ID (env) > session-based ID (fallback)
6
+ */
7
+ import { getConfig } from '../commands/config.js';
8
+ import { isValidAgentType } from './biz/agent-brain.js';
9
+ import { randomUUID } from 'crypto';
10
+ import * as os from 'os';
11
+ let sessionAgentId;
12
+ /**
13
+ * Get session-based agent ID (created once per CLI session)
14
+ */
15
+ function getSessionAgentId() {
16
+ if (!sessionAgentId) {
17
+ const hostname = os.hostname();
18
+ const timestamp = Date.now().toString(36);
19
+ const random = randomUUID().split('-')[0];
20
+ sessionAgentId = `session-${hostname}-${timestamp}-${random}`;
21
+ }
22
+ return sessionAgentId;
23
+ }
24
+ /**
25
+ * Resolve agent identity from config, env, or generate session ID
26
+ */
27
+ export function resolveAgentIdentity() {
28
+ const config = getConfig();
29
+ // Priority 1: workerId from config (persistent)
30
+ if (config.workerId) {
31
+ const agentType = config.agentType && isValidAgentType(config.agentType)
32
+ ? config.agentType
33
+ : undefined;
34
+ return {
35
+ agentId: config.workerId,
36
+ agentType,
37
+ source: 'config',
38
+ workerId: config.workerId,
39
+ };
40
+ }
41
+ // Priority 2: HUSKY_AGENT_ID from environment
42
+ const envAgentId = process.env.HUSKY_AGENT_ID;
43
+ if (envAgentId) {
44
+ const envAgentType = process.env.HUSKY_AGENT_TYPE;
45
+ const agentType = envAgentType && isValidAgentType(envAgentType)
46
+ ? envAgentType
47
+ : undefined;
48
+ return {
49
+ agentId: envAgentId,
50
+ agentType,
51
+ source: 'env',
52
+ };
53
+ }
54
+ // Priority 3: Session-based ID (fallback)
55
+ const sessionId = getSessionAgentId();
56
+ return {
57
+ agentId: sessionId,
58
+ agentType: undefined,
59
+ source: 'session',
60
+ };
61
+ }
62
+ /**
63
+ * Get agent ID (convenience method)
64
+ */
65
+ export function getAgentId() {
66
+ return resolveAgentIdentity().agentId;
67
+ }
68
+ /**
69
+ * Get agent type (convenience method)
70
+ */
71
+ export function getAgentType() {
72
+ return resolveAgentIdentity().agentType;
73
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker"];
2
2
  export type AgentType = typeof AGENT_TYPES[number];
3
+ export type MemoryVisibility = 'private' | 'team' | 'public';
3
4
  export interface Memory {
4
5
  id: string;
5
6
  agent: string;
@@ -9,6 +10,17 @@ export interface Memory {
9
10
  createdAt: Date;
10
11
  updatedAt: Date;
11
12
  metadata?: Record<string, unknown>;
13
+ visibility?: MemoryVisibility;
14
+ publishedBy?: string;
15
+ publishedAt?: string;
16
+ useCount?: number;
17
+ endorsements?: number;
18
+ recallCount?: number;
19
+ lastRecalledAt?: string;
20
+ boostCount?: number;
21
+ downvoteCount?: number;
22
+ qualityScore?: number;
23
+ status?: 'active' | 'archived' | 'deleted';
12
24
  }
13
25
  export interface RecallResult {
14
26
  memory: Memory;
@@ -32,7 +44,7 @@ export declare class AgentBrain {
32
44
  databaseName: string;
33
45
  };
34
46
  private ensureCollection;
35
- remember(content: string, tags?: string[], metadata?: Record<string, unknown>): Promise<string>;
47
+ remember(content: string, tags?: string[], metadata?: Record<string, unknown>, visibility?: MemoryVisibility, allowPii?: boolean): Promise<string>;
36
48
  recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
37
49
  recallByTags(tags: string[], limit?: number): Promise<Memory[]>;
38
50
  forget(memoryId: string): Promise<void>;
@@ -41,5 +53,55 @@ export declare class AgentBrain {
41
53
  count: number;
42
54
  tags: Record<string, number>;
43
55
  }>;
56
+ /**
57
+ * Publish a memory for sharing
58
+ */
59
+ publish(memoryId: string, visibility: MemoryVisibility): Promise<void>;
60
+ /**
61
+ * Unpublish a memory (set to private)
62
+ */
63
+ unpublish(memoryId: string): Promise<void>;
64
+ /**
65
+ * Recall shared memories from other agents
66
+ */
67
+ recallShared(query: string, limit?: number, minScore?: number, publicOnly?: boolean): Promise<RecallResult[]>;
68
+ /**
69
+ * List shared memories
70
+ */
71
+ listShared(limit?: number, publicOnly?: boolean): Promise<Memory[]>;
72
+ /**
73
+ * Boost a memory (positive feedback)
74
+ */
75
+ boost(memoryId: string): Promise<void>;
76
+ /**
77
+ * Downvote a memory (negative feedback)
78
+ */
79
+ downvote(memoryId: string): Promise<void>;
80
+ /**
81
+ * Get quality metrics for a memory
82
+ */
83
+ getQuality(memoryId: string): Promise<{
84
+ recallCount: number;
85
+ boostCount: number;
86
+ downvoteCount: number;
87
+ qualityScore: number;
88
+ status: string;
89
+ }>;
90
+ /**
91
+ * Calculate quality score based on feedback
92
+ */
93
+ private calculateQualityScore;
94
+ /**
95
+ * Calculate effective score with decay
96
+ */
97
+ private calculateEffectiveScore;
98
+ /**
99
+ * Archive low-quality memories
100
+ */
101
+ cleanup(dryRun?: boolean, threshold?: number, minAgeDays?: number, tags?: string[]): Promise<Memory[]>;
102
+ /**
103
+ * Permanently delete archived memories
104
+ */
105
+ purge(retentionDays?: number): Promise<number>;
44
106
  }
45
107
  export default AgentBrain;
@@ -2,6 +2,7 @@ import { QdrantClient } from './qdrant.js';
2
2
  import { EmbeddingService } from './embeddings.js';
3
3
  import { getConfig } from '../../commands/config.js';
4
4
  import { randomUUID } from 'crypto';
5
+ import { sanitizeForEmbedding } from './pii-filter.js';
5
6
  const MEMORIES_COLLECTION = 'agent-memories';
6
7
  const VECTOR_SIZE = 768;
7
8
  export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker'];
@@ -57,19 +58,42 @@ export class AgentBrain {
57
58
  await this.qdrant.createCollection(MEMORIES_COLLECTION, VECTOR_SIZE);
58
59
  }
59
60
  }
60
- async remember(content, tags = [], metadata) {
61
+ async remember(content, tags = [], metadata, visibility = 'private', allowPii = false) {
61
62
  await this.ensureCollection();
62
- const embedding = await this.embeddings.embed(content);
63
+ // Phase 5: PII Filter (GDPR compliance)
64
+ const sanitizeResult = sanitizeForEmbedding(content, allowPii);
65
+ if (sanitizeResult.redactionCount > 0 && !allowPii) {
66
+ console.warn(` ⚠️ PII removed from memory content (${sanitizeResult.redactedTypes.join(', ')})`);
67
+ }
68
+ // Use sanitized content for embedding (privacy-safe)
69
+ const embedding = await this.embeddings.embed(sanitizeResult.sanitized);
63
70
  const id = randomUUID();
64
71
  const now = new Date().toISOString();
65
72
  await this.qdrant.upsertOne(MEMORIES_COLLECTION, id, embedding, {
66
73
  agent: this.agentId,
67
74
  agentType: this.agentType || 'default',
68
- content,
75
+ content: sanitizeResult.sanitized, // Store sanitized content
69
76
  tags,
70
- metadata: metadata || {},
77
+ metadata: {
78
+ ...(metadata || {}),
79
+ // Store redaction info for transparency
80
+ piiRedacted: sanitizeResult.redactionCount > 0,
81
+ piiTypes: sanitizeResult.redactedTypes,
82
+ },
71
83
  createdAt: now,
72
84
  updatedAt: now,
85
+ // Phase 2: Visibility
86
+ visibility,
87
+ publishedBy: visibility !== 'private' ? this.agentId : undefined,
88
+ publishedAt: visibility !== 'private' ? now : undefined,
89
+ useCount: 0,
90
+ endorsements: 0,
91
+ // Phase 3: Quality
92
+ recallCount: 0,
93
+ boostCount: 0,
94
+ downvoteCount: 0,
95
+ qualityScore: 1.0,
96
+ status: 'active',
73
97
  });
74
98
  return id;
75
99
  }
@@ -195,5 +219,293 @@ export class AgentBrain {
195
219
  tags: tagCounts,
196
220
  };
197
221
  }
222
+ // =========================================================================
223
+ // Phase 2: Cross-Agent Sharing
224
+ // =========================================================================
225
+ /**
226
+ * Publish a memory for sharing
227
+ */
228
+ async publish(memoryId, visibility) {
229
+ await this.ensureCollection();
230
+ const now = new Date().toISOString();
231
+ await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
232
+ visibility,
233
+ publishedBy: this.agentId,
234
+ publishedAt: now,
235
+ updatedAt: now,
236
+ });
237
+ }
238
+ /**
239
+ * Unpublish a memory (set to private)
240
+ */
241
+ async unpublish(memoryId) {
242
+ await this.ensureCollection();
243
+ const now = new Date().toISOString();
244
+ await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
245
+ visibility: 'private',
246
+ publishedBy: undefined,
247
+ publishedAt: undefined,
248
+ updatedAt: now,
249
+ });
250
+ }
251
+ /**
252
+ * Recall shared memories from other agents
253
+ */
254
+ async recallShared(query, limit = 5, minScore = 0.5, publicOnly = false) {
255
+ await this.ensureCollection();
256
+ const queryEmbedding = await this.embeddings.embed(query);
257
+ const filter = {
258
+ must: [
259
+ {
260
+ should: publicOnly
261
+ ? [{ key: 'visibility', match: { value: 'public' } }]
262
+ : [
263
+ { key: 'visibility', match: { value: 'public' } },
264
+ ...(this.agentType
265
+ ? [{ key: 'visibility', match: { value: 'team' } }, { key: 'agentType', match: { value: this.agentType } }]
266
+ : []),
267
+ ],
268
+ },
269
+ { key: 'status', match: { value: 'active' } },
270
+ ],
271
+ };
272
+ const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit * 3, {
273
+ filter,
274
+ scoreThreshold: minScore,
275
+ });
276
+ return results.slice(0, limit).map(r => ({
277
+ memory: {
278
+ id: String(r.id),
279
+ agent: String(r.payload?.agent || ''),
280
+ agentType: String(r.payload?.agentType || ''),
281
+ content: String(r.payload?.content || ''),
282
+ tags: r.payload?.tags || [],
283
+ createdAt: new Date(String(r.payload?.createdAt || new Date().toISOString())),
284
+ updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
285
+ metadata: r.payload?.metadata,
286
+ visibility: r.payload?.visibility || 'private',
287
+ publishedBy: String(r.payload?.publishedBy || ''),
288
+ publishedAt: String(r.payload?.publishedAt || ''),
289
+ useCount: Number(r.payload?.useCount || 0),
290
+ endorsements: Number(r.payload?.endorsements || 0),
291
+ },
292
+ score: r.score,
293
+ }));
294
+ }
295
+ /**
296
+ * List shared memories
297
+ */
298
+ async listShared(limit = 20, publicOnly = false) {
299
+ await this.ensureCollection();
300
+ const filter = {
301
+ must: [
302
+ {
303
+ should: publicOnly
304
+ ? [{ key: 'visibility', match: { value: 'public' } }]
305
+ : [
306
+ { key: 'visibility', match: { value: 'public' } },
307
+ ...(this.agentType
308
+ ? [{ key: 'visibility', match: { value: 'team' } }, { key: 'agentType', match: { value: this.agentType } }]
309
+ : []),
310
+ ],
311
+ },
312
+ { key: 'status', match: { value: 'active' } },
313
+ ],
314
+ };
315
+ const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
316
+ filter,
317
+ limit,
318
+ with_payload: true,
319
+ });
320
+ return results
321
+ .map(r => ({
322
+ id: String(r.id),
323
+ agent: String(r.payload?.agent || ''),
324
+ agentType: String(r.payload?.agentType || ''),
325
+ content: String(r.payload?.content || ''),
326
+ tags: r.payload?.tags || [],
327
+ createdAt: new Date(String(r.payload?.createdAt || new Date().toISOString())),
328
+ updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
329
+ metadata: r.payload?.metadata,
330
+ visibility: r.payload?.visibility || 'private',
331
+ endorsements: Number(r.payload?.endorsements || 0),
332
+ }))
333
+ .sort((a, b) => (b.endorsements || 0) - (a.endorsements || 0));
334
+ }
335
+ // =========================================================================
336
+ // Phase 3: Quality & Decay
337
+ // =========================================================================
338
+ /**
339
+ * Boost a memory (positive feedback)
340
+ */
341
+ async boost(memoryId) {
342
+ await this.ensureCollection();
343
+ const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
344
+ if (!point)
345
+ throw new Error('Memory not found');
346
+ const boostCount = Number(point.payload?.boostCount || 0) + 1;
347
+ const downvoteCount = Number(point.payload?.downvoteCount || 0);
348
+ const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
349
+ await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
350
+ boostCount,
351
+ qualityScore,
352
+ updatedAt: new Date().toISOString(),
353
+ });
354
+ }
355
+ /**
356
+ * Downvote a memory (negative feedback)
357
+ */
358
+ async downvote(memoryId) {
359
+ await this.ensureCollection();
360
+ const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
361
+ if (!point)
362
+ throw new Error('Memory not found');
363
+ const boostCount = Number(point.payload?.boostCount || 0);
364
+ const downvoteCount = Number(point.payload?.downvoteCount || 0) + 1;
365
+ const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
366
+ await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
367
+ downvoteCount,
368
+ qualityScore,
369
+ updatedAt: new Date().toISOString(),
370
+ });
371
+ }
372
+ /**
373
+ * Get quality metrics for a memory
374
+ */
375
+ async getQuality(memoryId) {
376
+ await this.ensureCollection();
377
+ const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
378
+ if (!point)
379
+ throw new Error('Memory not found');
380
+ return {
381
+ recallCount: Number(point.payload?.recallCount || 0),
382
+ boostCount: Number(point.payload?.boostCount || 0),
383
+ downvoteCount: Number(point.payload?.downvoteCount || 0),
384
+ qualityScore: Number(point.payload?.qualityScore || 1.0),
385
+ status: String(point.payload?.status || 'active'),
386
+ };
387
+ }
388
+ /**
389
+ * Calculate quality score based on feedback
390
+ */
391
+ calculateQualityScore(boostCount, downvoteCount) {
392
+ // Feedback adjustment: -0.2 to +0.2
393
+ const feedbackBoost = Math.tanh((boostCount - downvoteCount * 2) * 0.1) * 0.2;
394
+ return Math.max(0.1, Math.min(1.5, 1.0 + feedbackBoost));
395
+ }
396
+ /**
397
+ * Calculate effective score with decay
398
+ */
399
+ calculateEffectiveScore(semanticScore, createdAt, recallCount, boostCount, downvoteCount) {
400
+ const now = Date.now();
401
+ const ageDays = (now - createdAt.getTime()) / (1000 * 60 * 60 * 24);
402
+ // Exponential decay with agent-type-specific half-life
403
+ // Support agents: 14-day half-life (faster decay for time-sensitive support knowledge)
404
+ // Other agents: 30-day half-life (standard decay)
405
+ const halfLifeDays = this.agentType === 'support' ? 14 : 30;
406
+ const decayFactor = Math.pow(0.5, ageDays / halfLifeDays);
407
+ // Recall boost (logarithmic)
408
+ const recallBoost = Math.log2(recallCount + 1) * 0.1;
409
+ // Feedback adjustment
410
+ const feedbackBoost = Math.tanh((boostCount - downvoteCount * 2) * 0.1) * 0.2;
411
+ // Effective score
412
+ return semanticScore * Math.max(0.1, Math.min(1.5, decayFactor + recallBoost + feedbackBoost));
413
+ }
414
+ /**
415
+ * Archive low-quality memories
416
+ */
417
+ async cleanup(dryRun = true, threshold = 0.1, minAgeDays = 90, tags) {
418
+ await this.ensureCollection();
419
+ const filter = {
420
+ must: [
421
+ { key: 'agent', match: { value: this.agentId } },
422
+ { key: 'status', match: { value: 'active' } },
423
+ ],
424
+ };
425
+ if (this.agentType) {
426
+ filter.must.push({ key: 'agentType', match: { value: this.agentType } });
427
+ }
428
+ // Add tag filter if specified
429
+ if (tags && tags.length > 0) {
430
+ filter.must.push({
431
+ should: tags.map(tag => ({
432
+ key: 'tags',
433
+ match: { any: [tag] }
434
+ }))
435
+ });
436
+ }
437
+ const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
438
+ filter,
439
+ limit: 1000,
440
+ with_payload: true,
441
+ });
442
+ const toArchive = [];
443
+ const now = Date.now();
444
+ for (const r of results) {
445
+ const createdAt = new Date(String(r.payload?.createdAt || new Date().toISOString()));
446
+ const ageDays = (now - createdAt.getTime()) / (1000 * 60 * 60 * 24);
447
+ const qualityScore = Number(r.payload?.qualityScore || 1.0);
448
+ const recallCount = Number(r.payload?.recallCount || 0);
449
+ const downvoteCount = Number(r.payload?.downvoteCount || 0);
450
+ const boostCount = Number(r.payload?.boostCount || 0);
451
+ // Cleanup criteria
452
+ const shouldArchive = (qualityScore < threshold && ageDays > minAgeDays) ||
453
+ (downvoteCount > 3 && boostCount === 0) ||
454
+ (recallCount === 0 && ageDays > 180);
455
+ if (shouldArchive) {
456
+ toArchive.push({
457
+ id: String(r.id),
458
+ agent: String(r.payload?.agent || ''),
459
+ agentType: String(r.payload?.agentType || ''),
460
+ content: String(r.payload?.content || ''),
461
+ tags: r.payload?.tags || [],
462
+ createdAt,
463
+ updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
464
+ metadata: r.payload?.metadata,
465
+ qualityScore,
466
+ recallCount,
467
+ downvoteCount,
468
+ boostCount,
469
+ });
470
+ if (!dryRun) {
471
+ await this.qdrant.setPayload(MEMORIES_COLLECTION, String(r.id), {
472
+ status: 'archived',
473
+ updatedAt: new Date().toISOString(),
474
+ });
475
+ }
476
+ }
477
+ }
478
+ return toArchive;
479
+ }
480
+ /**
481
+ * Permanently delete archived memories
482
+ */
483
+ async purge(retentionDays = 365) {
484
+ await this.ensureCollection();
485
+ const filter = {
486
+ must: [
487
+ { key: 'agent', match: { value: this.agentId } },
488
+ { key: 'status', match: { value: 'archived' } },
489
+ ],
490
+ };
491
+ const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
492
+ filter,
493
+ limit: 1000,
494
+ with_payload: true,
495
+ });
496
+ const now = Date.now();
497
+ const toPurge = [];
498
+ for (const r of results) {
499
+ const updatedAt = new Date(String(r.payload?.updatedAt || new Date().toISOString()));
500
+ const daysSinceArchived = (now - updatedAt.getTime()) / (1000 * 60 * 60 * 24);
501
+ if (daysSinceArchived > retentionDays) {
502
+ toPurge.push(r.id);
503
+ }
504
+ }
505
+ if (toPurge.length > 0) {
506
+ await this.qdrant.deletePoints(MEMORIES_COLLECTION, toPurge);
507
+ }
508
+ return toPurge.length;
509
+ }
198
510
  }
199
511
  export default AgentBrain;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Learning Capture Service
3
+ *
4
+ * Automatically captures learnings when a task is completed.
5
+ * Uses LLM (Vertex AI) to extract meaningful learnings from task context.
6
+ */
7
+ import { AgentType } from './agent-brain.js';
8
+ export interface Task {
9
+ id: string;
10
+ title: string;
11
+ description?: string;
12
+ status: string;
13
+ priority?: string;
14
+ projectId?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export interface LearningCaptureInput {
18
+ taskId: string;
19
+ task: Task;
20
+ prUrl?: string;
21
+ explicitLearnings?: string;
22
+ agentId: string;
23
+ agentType?: AgentType;
24
+ }
25
+ export interface LearningResult {
26
+ id: string;
27
+ content: string;
28
+ tags: string[];
29
+ }
30
+ /**
31
+ * Capture learnings from a completed task
32
+ *
33
+ * This function:
34
+ * 1. Generates learnings from task context (or uses explicit learnings)
35
+ * 2. Stores them in the agent's brain
36
+ * 3. Returns the learning IDs
37
+ */
38
+ export declare function captureLearnings(input: LearningCaptureInput): Promise<LearningResult[]>;
39
+ /**
40
+ * Fetch task details from API
41
+ */
42
+ export declare function fetchTask(taskId: string, apiUrl: string, apiKey?: string): Promise<Task | null>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Learning Capture Service
3
+ *
4
+ * Automatically captures learnings when a task is completed.
5
+ * Uses LLM (Vertex AI) to extract meaningful learnings from task context.
6
+ */
7
+ import { AgentBrain } from './agent-brain.js';
8
+ /**
9
+ * Generate learnings from task context using LLM
10
+ */
11
+ async function generateLearningsFromTask(task, prUrl) {
12
+ // For now, return basic task-based learnings
13
+ // TODO: In future, use Vertex AI to extract deeper insights from:
14
+ // - Task description
15
+ // - PR diff/content
16
+ // - Related comments/discussions
17
+ // - Error patterns encountered
18
+ const learnings = [];
19
+ // Basic learning from task completion
20
+ const basicTags = [
21
+ `task:${task.id}`,
22
+ ...(task.projectId ? [`project:${task.projectId}`] : []),
23
+ 'type:completion',
24
+ ];
25
+ // Create a learning from the task
26
+ const taskLearning = {
27
+ content: `Completed task: ${task.title}${task.description ? ` - ${task.description}` : ''}${prUrl ? ` (PR: ${prUrl})` : ''}`,
28
+ tags: basicTags,
29
+ };
30
+ learnings.push(taskLearning);
31
+ return learnings;
32
+ }
33
+ /**
34
+ * Capture learnings from a completed task
35
+ *
36
+ * This function:
37
+ * 1. Generates learnings from task context (or uses explicit learnings)
38
+ * 2. Stores them in the agent's brain
39
+ * 3. Returns the learning IDs
40
+ */
41
+ export async function captureLearnings(input) {
42
+ const { taskId, task, prUrl, explicitLearnings, agentId, agentType } = input;
43
+ // Initialize agent brain
44
+ const brain = new AgentBrain({ agentId, agentType });
45
+ const results = [];
46
+ // If explicit learnings provided, store them directly
47
+ if (explicitLearnings) {
48
+ const tags = [
49
+ `task:${taskId}`,
50
+ ...(task.projectId ? [`project:${task.projectId}`] : []),
51
+ 'type:explicit',
52
+ 'source:manual',
53
+ ];
54
+ const id = await brain.remember(explicitLearnings, tags, {
55
+ taskId,
56
+ taskTitle: task.title,
57
+ prUrl,
58
+ capturedAt: new Date().toISOString(),
59
+ });
60
+ results.push({
61
+ id,
62
+ content: explicitLearnings,
63
+ tags,
64
+ });
65
+ return results;
66
+ }
67
+ // Generate learnings from task context
68
+ try {
69
+ const generatedLearnings = await generateLearningsFromTask(task, prUrl);
70
+ for (const learning of generatedLearnings) {
71
+ const id = await brain.remember(learning.content, learning.tags, {
72
+ taskId,
73
+ taskTitle: task.title,
74
+ prUrl,
75
+ capturedAt: new Date().toISOString(),
76
+ source: 'auto-generated',
77
+ });
78
+ results.push({
79
+ id,
80
+ content: learning.content,
81
+ tags: learning.tags,
82
+ });
83
+ }
84
+ }
85
+ catch (error) {
86
+ console.warn(' ⚠️ Failed to generate learnings:', error instanceof Error ? error.message : error);
87
+ // Don't fail task completion if learning capture fails
88
+ }
89
+ return results;
90
+ }
91
+ /**
92
+ * Fetch task details from API
93
+ */
94
+ export async function fetchTask(taskId, apiUrl, apiKey) {
95
+ try {
96
+ const res = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
97
+ headers: apiKey ? { 'x-api-key': apiKey } : {},
98
+ });
99
+ if (!res.ok) {
100
+ return null;
101
+ }
102
+ return await res.json();
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PII Filter for GDPR-Compliant Embeddings
3
+ *
4
+ * Sanitizes content before embedding to remove personally identifiable information (PII).
5
+ * This enables:
6
+ * - DSGVO/GDPR compliance
7
+ * - Use of US-hosted models (OpenAI, Anthropic US)
8
+ * - Automatic SOP generation from learnings
9
+ * - Unlimited retention (no Löschrecht)
10
+ */
11
+ export interface SanitizeResult {
12
+ sanitized: string;
13
+ redactedTypes: string[];
14
+ redactionCount: number;
15
+ }
16
+ /**
17
+ * Sanitize content by removing PII
18
+ */
19
+ export declare function sanitizeForEmbedding(content: string, allowPii?: boolean): SanitizeResult;
20
+ /**
21
+ * Check if content contains PII
22
+ */
23
+ export declare function containsPII(content: string): boolean;
24
+ /**
25
+ * Preview what would be redacted (for debugging)
26
+ */
27
+ export declare function previewRedaction(content: string): {
28
+ original: string;
29
+ sanitized: string;
30
+ changes: Array<{
31
+ type: string;
32
+ count: number;
33
+ }>;
34
+ };