@rigour-labs/core 5.5.4 → 6.1.0-beta.1

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 (76) hide show
  1. package/dist/context/automatic-index.d.ts +18 -0
  2. package/dist/context/automatic-index.js +142 -0
  3. package/dist/context/automatic-index.test.d.ts +1 -0
  4. package/dist/context/automatic-index.test.js +45 -0
  5. package/dist/context/cache-engine.d.ts +25 -0
  6. package/dist/context/dependency-graph.d.ts +13 -0
  7. package/dist/context/dependency-graph.js +97 -0
  8. package/dist/context/dependency-graph.test.d.ts +1 -0
  9. package/dist/context/dependency-graph.test.js +13 -0
  10. package/dist/context/index-bridge.js +21 -2
  11. package/dist/firewall/adversarial.d.ts +13 -0
  12. package/dist/firewall/adversarial.js +125 -0
  13. package/dist/firewall/arbitration-token.d.ts +8 -0
  14. package/dist/firewall/arbitration-token.js +58 -0
  15. package/dist/firewall/attestation.d.ts +38 -0
  16. package/dist/firewall/attestation.js +197 -0
  17. package/dist/firewall/capability-broker.d.ts +47 -0
  18. package/dist/firewall/capability-broker.js +209 -0
  19. package/dist/firewall/firewall.test.d.ts +1 -0
  20. package/dist/firewall/firewall.test.js +102 -0
  21. package/dist/firewall/index.d.ts +10 -0
  22. package/dist/firewall/index.js +10 -0
  23. package/dist/firewall/mcp-gateway.d.ts +40 -0
  24. package/dist/firewall/mcp-gateway.js +64 -0
  25. package/dist/firewall/policy-hash.d.ts +1 -0
  26. package/dist/firewall/policy-hash.js +5 -0
  27. package/dist/firewall/scope-enforcement.d.ts +26 -0
  28. package/dist/firewall/scope-enforcement.js +155 -0
  29. package/dist/firewall/transaction.d.ts +43 -0
  30. package/dist/firewall/transaction.js +216 -0
  31. package/dist/firewall/typed-command.d.ts +16 -0
  32. package/dist/firewall/typed-command.js +129 -0
  33. package/dist/firewall/types.d.ts +96 -0
  34. package/dist/firewall/types.js +5 -0
  35. package/dist/hooks/checker.d.ts +2 -0
  36. package/dist/hooks/checker.js +25 -1
  37. package/dist/hooks/standalone-checker.js +5 -1
  38. package/dist/hooks/templates.js +3 -0
  39. package/dist/index.d.ts +10 -0
  40. package/dist/index.js +8 -0
  41. package/dist/pattern-index/embeddings.js +10 -3
  42. package/dist/pattern-index/indexer.d.ts +2 -0
  43. package/dist/pattern-index/indexer.js +72 -0
  44. package/dist/services/agent-history.d.ts +76 -0
  45. package/dist/services/agent-history.js +201 -0
  46. package/dist/services/agent-history.test.d.ts +1 -0
  47. package/dist/services/agent-history.test.js +69 -0
  48. package/dist/services/context-telemetry-service.d.ts +21 -0
  49. package/dist/services/context-telemetry-service.js +36 -7
  50. package/dist/services/context-telemetry-service.test.js +13 -0
  51. package/dist/services/engineering-knowledge-graph.d.ts +55 -0
  52. package/dist/services/engineering-knowledge-graph.js +177 -0
  53. package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
  54. package/dist/services/engineering-knowledge-graph.test.js +59 -0
  55. package/dist/services/model-pricing.d.ts +2 -0
  56. package/dist/services/model-pricing.js +2 -0
  57. package/dist/services/system-health.d.ts +34 -0
  58. package/dist/services/system-health.js +53 -0
  59. package/dist/storage/db.js +130 -1
  60. package/dist/storage/index.d.ts +4 -0
  61. package/dist/storage/index.js +2 -0
  62. package/dist/storage/lessons.d.ts +60 -0
  63. package/dist/storage/lessons.js +322 -0
  64. package/dist/storage/lessons.test.d.ts +1 -0
  65. package/dist/storage/lessons.test.js +50 -0
  66. package/dist/storage/local-encryption.d.ts +2 -0
  67. package/dist/storage/local-encryption.js +41 -0
  68. package/dist/storage/team-store.d.ts +75 -0
  69. package/dist/storage/team-store.js +321 -0
  70. package/dist/storage/team-store.test.d.ts +1 -0
  71. package/dist/storage/team-store.test.js +11 -0
  72. package/dist/storage/team-vector-store.d.ts +25 -0
  73. package/dist/storage/team-vector-store.js +183 -0
  74. package/dist/storage/team-vector-store.test.d.ts +1 -0
  75. package/dist/storage/team-vector-store.test.js +42 -0
  76. package/package.json +8 -6
@@ -13,6 +13,8 @@ interface CheckerOptions {
13
13
  files: string[];
14
14
  timeout_ms?: number;
15
15
  block_on_failure?: boolean;
16
+ /** Bound writer identity — required when agent scopes are registered */
17
+ agentId?: string;
16
18
  }
17
19
  /**
18
20
  * Run fast gates on a set of files.
@@ -12,6 +12,7 @@ import path from 'path';
12
12
  import yaml from 'yaml';
13
13
  import { ConfigSchema } from '../types/index.js';
14
14
  import { scanInputForCredentials } from './input-validator.js';
15
+ import { evaluateWriteScope, loadAgentScopesFromDisk } from '../firewall/scope-enforcement.js';
15
16
  const JS_TS_PATTERN = /\.(ts|tsx|js|jsx|mts|mjs)$/;
16
17
  /**
17
18
  * Check if a file matches any ignore pattern from rigour.yml.
@@ -92,14 +93,17 @@ function checkFile(content, relPath, cwd, config) {
92
93
  */
93
94
  export async function runHookChecker(options) {
94
95
  const start = Date.now();
95
- const { cwd, files, timeout_ms = 5000 } = options;
96
+ const { cwd, files, timeout_ms = 5000, agentId } = options;
96
97
  const failures = [];
98
+ let timedOut = false;
97
99
  try {
98
100
  const config = await loadConfig(cwd);
99
101
  const deadline = start + timeout_ms;
100
102
  const ignorePatterns = config.ignore ?? [];
103
+ const agentScopes = await loadAgentScopesFromDisk(cwd);
101
104
  for (const filePath of files) {
102
105
  if (Date.now() > deadline) {
106
+ timedOut = true;
103
107
  break;
104
108
  }
105
109
  const resolved = await resolveFile(filePath, cwd);
@@ -110,9 +114,29 @@ export async function runHookChecker(options) {
110
114
  if (isIgnored(resolved.relPath, ignorePatterns)) {
111
115
  continue;
112
116
  }
117
+ if (agentScopes.length > 0) {
118
+ const scopeEv = evaluateWriteScope(cwd, resolved.relPath, agentScopes, agentId);
119
+ if (scopeEv.decision !== 'allow') {
120
+ failures.push({
121
+ gate: 'agent-scope',
122
+ file: resolved.relPath,
123
+ message: scopeEv.reason,
124
+ severity: 'critical',
125
+ });
126
+ continue;
127
+ }
128
+ }
113
129
  const fileFailures = checkFile(resolved.content, resolved.relPath, cwd, config);
114
130
  failures.push(...fileFailures);
115
131
  }
132
+ if (timedOut) {
133
+ failures.push({
134
+ gate: 'hook-timeout',
135
+ file: '',
136
+ message: `Hook checker exceeded ${timeout_ms}ms before all files were scanned (fail-closed)`,
137
+ severity: 'critical',
138
+ });
139
+ }
116
140
  return {
117
141
  status: failures.length > 0 ? 'fail' : 'pass',
118
142
  failures,
@@ -89,7 +89,11 @@ async function main() {
89
89
  process.stdout.write(EMPTY_RESULT);
90
90
  return;
91
91
  }
92
- const result = await runHookChecker({ cwd, files });
92
+ const result = await runHookChecker({
93
+ cwd,
94
+ files,
95
+ agentId: process.env.RIGOUR_AGENT_ID || undefined,
96
+ });
93
97
  process.stdout.write(JSON.stringify(result));
94
98
  if (result.status === 'fail') {
95
99
  logFailures(result.failures);
@@ -82,6 +82,7 @@ process.stdin.on('end', async () => {
82
82
  const result = await runHookChecker({
83
83
  cwd: process.cwd(),
84
84
  files: [payload.file_path],
85
+ agentId: process.env.RIGOUR_AGENT_ID || undefined,
85
86
  });
86
87
 
87
88
  // Write result to stdout for Cursor to consume
@@ -148,6 +149,7 @@ process.stdin.on('end', async () => {
148
149
  const result = await runHookChecker({
149
150
  cwd: process.cwd(),
150
151
  files: [filePath],
152
+ agentId: process.env.RIGOUR_AGENT_ID || undefined,
151
153
  });
152
154
 
153
155
  if (result.status === 'fail') {
@@ -206,6 +208,7 @@ process.stdin.on('end', async () => {
206
208
  const result = await runHookChecker({
207
209
  cwd: process.cwd(),
208
210
  files: [payload.file_path],
211
+ agentId: process.env.RIGOUR_AGENT_ID || undefined,
209
212
  });
210
213
 
211
214
  if (result.status === 'fail') {
package/dist/index.d.ts CHANGED
@@ -24,6 +24,10 @@ export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, rese
24
24
  export type { RigourDB, CompactResult } from './storage/index.js';
25
25
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
26
26
  export type { ProjectStats } from './storage/index.js';
27
+ export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
28
+ export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './storage/index.js';
29
+ export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
30
+ export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './storage/index.js';
27
31
  export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
28
32
  export type { TemporalDriftReport, ProvenanceStream, MonthlyBucket, WeeklyBucket, DriftDirection } from './services/temporal-drift.js';
29
33
  export { getProvenanceTrends, getQualityTrend } from './services/adaptive-thresholds.js';
@@ -35,5 +39,11 @@ export type { RenderOptions, GateResult } from './services/terminal-renderer.js'
35
39
  export * from './context/cache-engine.js';
36
40
  export * from './context/context-session.js';
37
41
  export * from './context/index-bridge.js';
42
+ export * from './context/automatic-index.js';
43
+ export * from './context/dependency-graph.js';
38
44
  export * from './services/context-telemetry-service.js';
45
+ export * from './services/system-health.js';
46
+ export * from './services/agent-history.js';
47
+ export * from './services/engineering-knowledge-graph.js';
48
+ export * from './firewall/index.js';
39
49
  export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
package/dist/index.js CHANGED
@@ -24,6 +24,8 @@ export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
24
24
  export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
25
25
  // Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
26
26
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
27
+ export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
28
+ export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
27
29
  // Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
28
30
  export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
29
31
  // Adaptive Thresholds (v5 — Z-score + per-provenance trends)
@@ -36,7 +38,13 @@ export { renderScoreGauge, renderSeveritySection, renderGateGrid, renderBrainSta
36
38
  export * from './context/cache-engine.js';
37
39
  export * from './context/context-session.js';
38
40
  export * from './context/index-bridge.js';
41
+ export * from './context/automatic-index.js';
42
+ export * from './context/dependency-graph.js';
39
43
  export * from './services/context-telemetry-service.js';
44
+ export * from './services/system-health.js';
45
+ export * from './services/agent-history.js';
46
+ export * from './services/engineering-knowledge-graph.js';
47
+ export * from './firewall/index.js';
40
48
  export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
41
49
  // Pattern Index is intentionally NOT exported here to prevent
42
50
  // native dependency issues (sharp/transformers) from leaking into
@@ -7,6 +7,7 @@
7
7
  * Singleton for the embedding pipeline to avoid re-loading the model.
8
8
  */
9
9
  let embeddingPipeline = null;
10
+ let embeddingUnavailable = false;
10
11
  /**
11
12
  * Get or initialize the embedding pipeline.
12
13
  */
@@ -21,6 +22,8 @@ async function getPipeline() {
21
22
  return { data: new Float32Array(vector) };
22
23
  };
23
24
  }
25
+ if (embeddingUnavailable)
26
+ return null;
24
27
  if (!embeddingPipeline) {
25
28
  try {
26
29
  // Dynamic import to isolate native dependency issues (like sharp)
@@ -29,8 +32,9 @@ async function getPipeline() {
29
32
  embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
30
33
  }
31
34
  catch (error) {
32
- console.error('Failed to initialize embedding pipeline:', error);
33
- throw error;
35
+ embeddingUnavailable = true;
36
+ console.warn('Semantic enrichment is degraded; structural and text retrieval remain available.');
37
+ return null;
34
38
  }
35
39
  }
36
40
  return embeddingPipeline;
@@ -41,11 +45,14 @@ async function getPipeline() {
41
45
  export async function generateEmbedding(text) {
42
46
  try {
43
47
  const extractor = await getPipeline();
48
+ if (!extractor)
49
+ return [];
44
50
  const output = await extractor(text, { pooling: 'mean', normalize: true });
45
51
  return Array.from(output.data);
46
52
  }
47
53
  catch (error) {
48
- console.warn('Semantic reasoning disabled: Embedding generation failed.', error);
54
+ embeddingUnavailable = true;
55
+ console.warn('Semantic enrichment is degraded; structural and text retrieval remain available.');
49
56
  return [];
50
57
  }
51
58
  }
@@ -15,6 +15,8 @@ export declare class PatternIndexer {
15
15
  constructor(rootDir: string, config?: Partial<PatternIndexConfig>);
16
16
  buildIndex(): Promise<PatternIndex>;
17
17
  updateIndex(existingIndex: PatternIndex): Promise<PatternIndex>;
18
+ updateFiles(existingIndex: PatternIndex, changedFiles: string[]): Promise<PatternIndex>;
19
+ enrichIndex(existingIndex: PatternIndex): Promise<PatternIndex>;
18
20
  private findFiles;
19
21
  private extractPatterns;
20
22
  private getScriptKind;
@@ -12,6 +12,7 @@ import * as fs from 'fs/promises';
12
12
  import * as path from 'path';
13
13
  import { globby } from 'globby';
14
14
  import ts from 'typescript';
15
+ import micromatch from 'micromatch';
15
16
  import { generateEmbedding } from './embeddings.js';
16
17
  import { hashContent } from './indexer-helpers.js';
17
18
  import { extractGoPatterns, extractRustPatterns, extractJVMStylePatterns, extractPythonPatterns, extractGenericCPatterns, } from './indexer-lang.js';
@@ -156,6 +157,77 @@ export class PatternIndexer {
156
157
  files: updatedFiles,
157
158
  };
158
159
  }
160
+ async updateFiles(existingIndex, changedFiles) {
161
+ const startTime = Date.now();
162
+ const root = path.resolve(this.rootDir);
163
+ const changed = new Set(changedFiles.flatMap(file => {
164
+ const absolute = path.resolve(root, file);
165
+ const relative = path.relative(root, absolute);
166
+ if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
167
+ return [];
168
+ return [relative.split(path.sep).join('/')];
169
+ }));
170
+ const patterns = existingIndex.patterns.filter(pattern => !changed.has(pattern.file));
171
+ const files = existingIndex.files.filter(file => !changed.has(file.path));
172
+ for (const relativePath of changed) {
173
+ const absolutePath = path.join(root, relativePath);
174
+ const supported = this.config.extensions.includes(path.extname(relativePath).toLowerCase());
175
+ const included = micromatch.isMatch(relativePath.replace(/\\/g, '/'), this.config.include, { dot: false });
176
+ const excluded = micromatch.isMatch(relativePath.replace(/\\/g, '/'), this.config.exclude, { dot: true });
177
+ if (!supported || !included || excluded)
178
+ continue;
179
+ try {
180
+ const content = await fs.readFile(absolutePath, 'utf-8');
181
+ const filePatterns = await this.extractPatterns(absolutePath, content);
182
+ if (this.config.useEmbeddings) {
183
+ await Promise.all(filePatterns.map(async (pattern) => {
184
+ pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
185
+ }));
186
+ }
187
+ patterns.push(...filePatterns);
188
+ files.push({
189
+ path: relativePath,
190
+ hash: hashContent(content),
191
+ patternCount: filePatterns.length,
192
+ indexedAt: new Date().toISOString(),
193
+ });
194
+ }
195
+ catch (error) {
196
+ if (error?.code !== 'ENOENT')
197
+ throw error;
198
+ }
199
+ }
200
+ return {
201
+ version: INDEX_VERSION,
202
+ lastUpdated: new Date().toISOString(),
203
+ rootDir: this.rootDir,
204
+ patterns,
205
+ files,
206
+ stats: this.calculateStats(patterns, files, Date.now() - startTime),
207
+ };
208
+ }
209
+ async enrichIndex(existingIndex) {
210
+ const startTime = Date.now();
211
+ const patterns = existingIndex.patterns.map(pattern => ({ ...pattern }));
212
+ const pending = patterns.filter(pattern => !pattern.embedding?.length);
213
+ if (pending.length > 0) {
214
+ pending[0].embedding = await generateEmbedding(`${pending[0].name} ${pending[0].type} ${pending[0].description}`);
215
+ if (!pending[0].embedding.length)
216
+ return existingIndex;
217
+ }
218
+ const batchSize = 10;
219
+ for (let i = 1; i < pending.length; i += batchSize) {
220
+ await Promise.all(pending.slice(i, i + batchSize).map(async (pattern) => {
221
+ pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
222
+ }));
223
+ }
224
+ return {
225
+ ...existingIndex,
226
+ lastUpdated: new Date().toISOString(),
227
+ patterns,
228
+ stats: this.calculateStats(patterns, existingIndex.files, Date.now() - startTime),
229
+ };
230
+ }
159
231
  // -------------------------------------------------------------------------
160
232
  // Private helpers
161
233
  // -------------------------------------------------------------------------
@@ -0,0 +1,76 @@
1
+ export type AgentEventOutcome = 'success' | 'error' | 'rejected' | 'unknown';
2
+ export interface AgentGuidance {
3
+ kind: 'context-scope' | 'pattern' | 'memory';
4
+ recommendation: string;
5
+ patternRefs: Array<{
6
+ id: string;
7
+ label: string;
8
+ file?: string;
9
+ }>;
10
+ lessonRefs: Array<{
11
+ id: string;
12
+ label: string;
13
+ visibility?: string;
14
+ }>;
15
+ memoryRefs: Array<{
16
+ id: string;
17
+ label: string;
18
+ }>;
19
+ selectedFiles: string[];
20
+ excludedFileCount: number;
21
+ conflicts: number;
22
+ }
23
+ export interface AgentImpact {
24
+ guidanceCount: number;
25
+ knowledgeItems: number;
26
+ candidateTokens: number;
27
+ returnedTokens: number;
28
+ avoidedTokens: number;
29
+ candidateFiles: number;
30
+ returnedFiles: number;
31
+ excludedFiles: number;
32
+ cacheHits: number;
33
+ recommendations: string[];
34
+ }
35
+ export interface AgentHistoryEvent {
36
+ id: string;
37
+ timestamp: string;
38
+ type: string;
39
+ agentId: string;
40
+ taskId?: string;
41
+ sessionId?: string;
42
+ requestId?: string;
43
+ tool?: string;
44
+ outcome: AgentEventOutcome;
45
+ files: string[];
46
+ summary: string;
47
+ guidance?: AgentGuidance;
48
+ telemetry?: {
49
+ candidateTokens: number;
50
+ returnedTokens: number;
51
+ candidateFiles: number;
52
+ returnedFiles: number;
53
+ cacheStatus: string;
54
+ classification: 'observed' | 'measured estimate' | 'modelled estimate';
55
+ };
56
+ }
57
+ export interface AgentRun {
58
+ id: string;
59
+ agentId: string;
60
+ taskId?: string;
61
+ sessionId?: string;
62
+ startedAt: string;
63
+ endedAt: string;
64
+ status: AgentEventOutcome | 'active';
65
+ eventCount: number;
66
+ files: string[];
67
+ tools: string[];
68
+ impact?: AgentImpact;
69
+ }
70
+ export declare function normalizeAgentEvent(input: unknown, index?: number): AgentHistoryEvent;
71
+ /**
72
+ * Normalizes a page as a unit so a response inherits identity and scope from its
73
+ * matching request. Older event logs often stored those fields only on tool_call.
74
+ */
75
+ export declare function normalizeAgentEvents(inputs: unknown[]): AgentHistoryEvent[];
76
+ export declare function buildAgentRuns(events: AgentHistoryEvent[]): AgentRun[];
@@ -0,0 +1,201 @@
1
+ function text(value) {
2
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
3
+ }
4
+ function validDate(value) {
5
+ const candidate = text(value);
6
+ return candidate && !Number.isNaN(Date.parse(candidate)) ? candidate : new Date(0).toISOString();
7
+ }
8
+ function strings(value) {
9
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.length > 0) : [];
10
+ }
11
+ function number(value) {
12
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
13
+ }
14
+ function refs(value) {
15
+ if (!Array.isArray(value))
16
+ return [];
17
+ return value.flatMap(item => {
18
+ if (!item || typeof item !== 'object')
19
+ return [];
20
+ const record = item;
21
+ const id = text(record.id);
22
+ const label = text(record.label);
23
+ return id && label ? [{ id, label, file: text(record.file), visibility: text(record.visibility) }] : [];
24
+ });
25
+ }
26
+ function eventGuidance(event) {
27
+ const value = event.guidance ?? event._guidance;
28
+ if (!value || typeof value !== 'object')
29
+ return undefined;
30
+ const record = value;
31
+ const recommendation = text(record.recommendation);
32
+ const kind = text(record.kind);
33
+ if (!recommendation || !['context-scope', 'pattern', 'memory'].includes(kind ?? ''))
34
+ return undefined;
35
+ return {
36
+ kind: kind,
37
+ recommendation,
38
+ patternRefs: refs(record.patternRefs),
39
+ lessonRefs: refs(record.lessonRefs),
40
+ memoryRefs: refs(record.memoryRefs),
41
+ selectedFiles: strings(record.selectedFiles),
42
+ excludedFileCount: number(record.excludedFileCount),
43
+ conflicts: number(record.conflicts),
44
+ };
45
+ }
46
+ function eventTelemetry(event) {
47
+ const value = event.telemetry ?? event._telemetry;
48
+ if (!value || typeof value !== 'object')
49
+ return undefined;
50
+ const record = value;
51
+ const classification = text(record.classification);
52
+ return {
53
+ candidateTokens: number(record.candidateTokens),
54
+ returnedTokens: number(record.returnedTokens),
55
+ candidateFiles: number(record.candidateFiles),
56
+ returnedFiles: number(record.returnedFiles),
57
+ cacheStatus: text(record.cacheStatus) ?? 'none',
58
+ classification: classification === 'observed' || classification === 'modelled estimate'
59
+ ? classification
60
+ : 'measured estimate',
61
+ };
62
+ }
63
+ function emptyImpact() {
64
+ return {
65
+ guidanceCount: 0,
66
+ knowledgeItems: 0,
67
+ candidateTokens: 0,
68
+ returnedTokens: 0,
69
+ avoidedTokens: 0,
70
+ candidateFiles: 0,
71
+ returnedFiles: 0,
72
+ excludedFiles: 0,
73
+ cacheHits: 0,
74
+ recommendations: [],
75
+ };
76
+ }
77
+ function eventFiles(event) {
78
+ const args = event.arguments && typeof event.arguments === 'object' ? event.arguments : {};
79
+ const report = event._rigour_report && typeof event._rigour_report === 'object'
80
+ ? event._rigour_report
81
+ : {};
82
+ const failures = Array.isArray(report.failures) ? report.failures : [];
83
+ const reported = failures.flatMap((failure) => failure && typeof failure === 'object'
84
+ ? strings(failure.files)
85
+ : []);
86
+ return [...new Set([
87
+ ...strings(event.files),
88
+ ...strings(args.files),
89
+ ...reported,
90
+ text(event.file),
91
+ text(event.path),
92
+ ].filter((file) => Boolean(file)))];
93
+ }
94
+ function eventOutcome(event) {
95
+ const value = String(event.outcome ?? event.status ?? '').toLowerCase();
96
+ const type = String(event.type ?? '').toLowerCase();
97
+ if (value.includes('reject') || type.includes('reject') || type.includes('deny'))
98
+ return 'rejected';
99
+ if (value.includes('error') || value.includes('fail') || type.includes('error') || type.includes('fail'))
100
+ return 'error';
101
+ if (value.includes('success') || value === 'pass' || type.includes('success') || type.includes('completed'))
102
+ return 'success';
103
+ return 'unknown';
104
+ }
105
+ export function normalizeAgentEvent(input, index = 0) {
106
+ const event = input && typeof input === 'object' ? input : {};
107
+ const type = text(event.type) ?? 'unknown';
108
+ const tool = text(event.tool ?? event.toolName);
109
+ const outcome = eventOutcome(event);
110
+ const summary = text(event.summary ?? event.message ?? event.description)
111
+ ?? `${tool ?? type}${outcome === 'unknown' ? '' : ` · ${outcome}`}`;
112
+ return {
113
+ id: text(event.id) ?? text(event.requestId) ?? `legacy-event-${index + 1}`,
114
+ timestamp: validDate(event.timestamp ?? event.createdAt),
115
+ type,
116
+ agentId: text(event.agentId ?? event.agent_id) ?? 'unknown-agent',
117
+ taskId: text(event.taskId ?? event.task_id),
118
+ sessionId: text(event.sessionId ?? event.session_id),
119
+ requestId: text(event.requestId ?? event.request_id),
120
+ tool,
121
+ outcome,
122
+ files: eventFiles(event),
123
+ summary,
124
+ guidance: eventGuidance(event),
125
+ telemetry: eventTelemetry(event),
126
+ };
127
+ }
128
+ /**
129
+ * Normalizes a page as a unit so a response inherits identity and scope from its
130
+ * matching request. Older event logs often stored those fields only on tool_call.
131
+ */
132
+ export function normalizeAgentEvents(inputs) {
133
+ const requestContext = new Map();
134
+ for (const input of inputs) {
135
+ const event = input && typeof input === 'object' ? input : {};
136
+ const requestId = text(event.requestId ?? event.request_id);
137
+ if (!requestId || String(event.type) !== 'tool_call')
138
+ continue;
139
+ const args = event.arguments && typeof event.arguments === 'object'
140
+ ? event.arguments
141
+ : {};
142
+ requestContext.set(requestId, {
143
+ agentId: event.agentId ?? event.agent_id ?? args.agentId ?? args.agent_id ?? args.agent
144
+ ?? args.fromAgentId ?? args.toAgentId,
145
+ taskId: event.taskId ?? event.task_id ?? args.taskId ?? args.task_id,
146
+ sessionId: event.sessionId ?? event.session_id ?? args.sessionId ?? args.session_id,
147
+ files: event.files ?? args.files ?? args.filesChanged ?? args.filesInScope,
148
+ });
149
+ }
150
+ return inputs.map((input, index) => {
151
+ const event = input && typeof input === 'object' ? input : {};
152
+ const requestId = text(event.requestId ?? event.request_id);
153
+ return normalizeAgentEvent(requestId ? { ...requestContext.get(requestId), ...event } : event, index);
154
+ });
155
+ }
156
+ export function buildAgentRuns(events) {
157
+ const runs = new Map();
158
+ for (const event of [...events].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))) {
159
+ const day = event.timestamp.slice(0, 10);
160
+ const id = event.sessionId ?? event.taskId ?? event.requestId ?? `${event.agentId}:${day}`;
161
+ const current = runs.get(id) ?? {
162
+ id,
163
+ agentId: event.agentId,
164
+ taskId: event.taskId,
165
+ sessionId: event.sessionId,
166
+ startedAt: event.timestamp,
167
+ endedAt: event.timestamp,
168
+ status: 'active',
169
+ eventCount: 0,
170
+ files: [],
171
+ tools: [],
172
+ impact: emptyImpact(),
173
+ };
174
+ current.endedAt = event.timestamp;
175
+ current.eventCount++;
176
+ current.files = [...new Set([...current.files, ...event.files])];
177
+ if (event.tool)
178
+ current.tools = [...new Set([...current.tools, event.tool])];
179
+ if (event.guidance) {
180
+ current.impact ??= emptyImpact();
181
+ const telemetry = event.telemetry;
182
+ current.impact.guidanceCount++;
183
+ current.impact.knowledgeItems += event.guidance.patternRefs.length
184
+ + event.guidance.lessonRefs.length
185
+ + event.guidance.memoryRefs.length;
186
+ current.impact.candidateTokens += telemetry?.candidateTokens ?? 0;
187
+ current.impact.returnedTokens += telemetry?.returnedTokens ?? 0;
188
+ current.impact.avoidedTokens += Math.max(0, (telemetry?.candidateTokens ?? 0) - (telemetry?.returnedTokens ?? 0));
189
+ current.impact.candidateFiles += telemetry?.candidateFiles ?? 0;
190
+ current.impact.returnedFiles += telemetry?.returnedFiles ?? 0;
191
+ current.impact.excludedFiles += event.guidance.excludedFileCount;
192
+ if (telemetry?.cacheStatus && !['none', 'miss'].includes(telemetry.cacheStatus))
193
+ current.impact.cacheHits++;
194
+ current.impact.recommendations = [...new Set([...current.impact.recommendations, event.guidance.recommendation])];
195
+ }
196
+ if (event.outcome !== 'unknown')
197
+ current.status = event.outcome;
198
+ runs.set(id, current);
199
+ }
200
+ return [...runs.values()].sort((a, b) => Date.parse(b.endedAt) - Date.parse(a.endedAt));
201
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildAgentRuns, normalizeAgentEvent, normalizeAgentEvents } from './agent-history.js';
3
+ describe('agent history', () => {
4
+ it('normalizes incomplete historical records without dropping them', () => {
5
+ const event = normalizeAgentEvent({ type: 'tool_error', arguments: { files: ['src/a.ts'] } });
6
+ expect(event.agentId).toBe('unknown-agent');
7
+ expect(event.outcome).toBe('error');
8
+ expect(event.files).toEqual(['src/a.ts']);
9
+ });
10
+ it('groups events into stable runs and retains outcomes', () => {
11
+ const events = [
12
+ normalizeAgentEvent({ id: '1', sessionId: 's1', agentId: 'a1', timestamp: '2026-01-01T00:00:00Z', tool: 'read' }),
13
+ normalizeAgentEvent({ id: '2', sessionId: 's1', agentId: 'a1', timestamp: '2026-01-01T00:01:00Z', type: 'completed', files: ['src/a.ts'] }),
14
+ ];
15
+ expect(buildAgentRuns(events)).toMatchObject([{ id: 's1', eventCount: 2, status: 'success', files: ['src/a.ts'] }]);
16
+ });
17
+ it('normalizes guidance safely and builds an honest impact receipt', () => {
18
+ const events = [normalizeAgentEvent({
19
+ id: 'response-1', requestId: 'request-1', agentId: 'agent-1', timestamp: '2026-01-01T00:00:00Z',
20
+ type: 'tool_response', status: 'success',
21
+ guidance: {
22
+ kind: 'context-scope',
23
+ recommendation: 'Read only the two evidence-backed files.',
24
+ patternRefs: [{ id: 'p1', label: 'withTransaction' }, { bad: true }],
25
+ lessonRefs: [{ id: 'l1', label: 'Bound retries' }],
26
+ selectedFiles: ['src/a.ts', 42],
27
+ excludedFileCount: 18,
28
+ },
29
+ telemetry: {
30
+ candidateTokens: 2400,
31
+ returnedTokens: 400,
32
+ candidateFiles: 20,
33
+ returnedFiles: 2,
34
+ cacheStatus: 'exact-hit',
35
+ classification: 'measured estimate',
36
+ },
37
+ })];
38
+ expect(events[0].guidance?.selectedFiles).toEqual(['src/a.ts']);
39
+ expect(buildAgentRuns(events)[0].impact).toMatchObject({
40
+ guidanceCount: 1,
41
+ knowledgeItems: 2,
42
+ avoidedTokens: 2000,
43
+ excludedFiles: 18,
44
+ cacheHits: 1,
45
+ });
46
+ });
47
+ it('ignores malformed guidance rather than breaking legacy history', () => {
48
+ const event = normalizeAgentEvent({ guidance: { kind: 'context-scope', recommendation: 42 }, telemetry: 'bad' });
49
+ expect(event.guidance).toBeUndefined();
50
+ expect(event.telemetry).toBeUndefined();
51
+ });
52
+ });
53
+ describe('normalizeAgentEvents', () => {
54
+ it('carries request identity into the matching response', () => {
55
+ const events = normalizeAgentEvents([
56
+ {
57
+ type: 'tool_call', requestId: 'request-1', tool: 'rigour_checkpoint',
58
+ timestamp: '2026-09-10T00:00:00Z',
59
+ arguments: { agentId: 'agent-a', taskId: 'task-a', filesChanged: ['src/a.ts'] },
60
+ },
61
+ {
62
+ type: 'tool_response', requestId: 'request-1', tool: 'rigour_checkpoint', status: 'success',
63
+ timestamp: '2026-09-10T00:00:01Z',
64
+ },
65
+ ]);
66
+ expect(events[1]).toMatchObject({ agentId: 'agent-a', taskId: 'task-a', files: ['src/a.ts'] });
67
+ expect(buildAgentRuns(events)).toHaveLength(1);
68
+ });
69
+ });