@eddacraft/anvil-kindling-integration 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +542 -0
  3. package/dist/adapter.d.ts +49 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +100 -0
  6. package/dist/config.d.ts +89 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +173 -0
  9. package/dist/emitters/action-emitter.d.ts +40 -0
  10. package/dist/emitters/action-emitter.d.ts.map +1 -0
  11. package/dist/emitters/action-emitter.js +52 -0
  12. package/dist/emitters/constraint-emitter.d.ts +32 -0
  13. package/dist/emitters/constraint-emitter.d.ts.map +1 -0
  14. package/dist/emitters/constraint-emitter.js +41 -0
  15. package/dist/emitters/error-emitter.d.ts +33 -0
  16. package/dist/emitters/error-emitter.d.ts.map +1 -0
  17. package/dist/emitters/error-emitter.js +50 -0
  18. package/dist/emitters/gate-emitter.d.ts +37 -0
  19. package/dist/emitters/gate-emitter.d.ts.map +1 -0
  20. package/dist/emitters/gate-emitter.js +53 -0
  21. package/dist/emitters/human-input-emitter.d.ts +30 -0
  22. package/dist/emitters/human-input-emitter.d.ts.map +1 -0
  23. package/dist/emitters/human-input-emitter.js +38 -0
  24. package/dist/emitters/index.d.ts +13 -0
  25. package/dist/emitters/index.d.ts.map +1 -0
  26. package/dist/emitters/index.js +19 -0
  27. package/dist/emitters/plan-emitter.d.ts +75 -0
  28. package/dist/emitters/plan-emitter.d.ts.map +1 -0
  29. package/dist/emitters/plan-emitter.js +116 -0
  30. package/dist/emitters/session-emitter.d.ts +57 -0
  31. package/dist/emitters/session-emitter.d.ts.map +1 -0
  32. package/dist/emitters/session-emitter.js +80 -0
  33. package/dist/index.d.ts +40 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +111 -0
  36. package/dist/kindling-service.d.ts +122 -0
  37. package/dist/kindling-service.d.ts.map +1 -0
  38. package/dist/kindling-service.js +203 -0
  39. package/dist/observation-contract.d.ts +561 -0
  40. package/dist/observation-contract.d.ts.map +1 -0
  41. package/dist/observation-contract.js +391 -0
  42. package/dist/query-contract.d.ts +463 -0
  43. package/dist/query-contract.d.ts.map +1 -0
  44. package/dist/query-contract.js +314 -0
  45. package/dist/query-limits.d.ts +40 -0
  46. package/dist/query-limits.d.ts.map +1 -0
  47. package/dist/query-limits.js +79 -0
  48. package/dist/query-service.d.ts +109 -0
  49. package/dist/query-service.d.ts.map +1 -0
  50. package/dist/query-service.js +140 -0
  51. package/dist/retention.d.ts +79 -0
  52. package/dist/retention.d.ts.map +1 -0
  53. package/dist/retention.js +81 -0
  54. package/dist/sensitive-data-validator.d.ts +47 -0
  55. package/dist/sensitive-data-validator.d.ts.map +1 -0
  56. package/dist/sensitive-data-validator.js +135 -0
  57. package/dist/status.d.ts +104 -0
  58. package/dist/status.d.ts.map +1 -0
  59. package/dist/status.js +136 -0
  60. package/dist/utils/debug.d.ts +9 -0
  61. package/dist/utils/debug.d.ts.map +1 -0
  62. package/dist/utils/debug.js +55 -0
  63. package/package.json +114 -0
  64. package/src/adapter.ts +117 -0
  65. package/src/config.ts +202 -0
  66. package/src/emitters/action-emitter.ts +90 -0
  67. package/src/emitters/constraint-emitter.ts +73 -0
  68. package/src/emitters/error-emitter.ts +86 -0
  69. package/src/emitters/gate-emitter.ts +87 -0
  70. package/src/emitters/human-input-emitter.ts +71 -0
  71. package/src/emitters/index.ts +40 -0
  72. package/src/emitters/plan-emitter.ts +183 -0
  73. package/src/emitters/session-emitter.ts +131 -0
  74. package/src/index.ts +254 -0
  75. package/src/kindling-service.ts +272 -0
  76. package/src/malicious-ai.test.ts +949 -0
  77. package/src/observation-contract.ts +500 -0
  78. package/src/query-contract.ts +389 -0
  79. package/src/query-limits.ts +106 -0
  80. package/src/query-service.ts +217 -0
  81. package/src/retention.ts +153 -0
  82. package/src/sensitive-data-validator.ts +167 -0
  83. package/src/status.ts +221 -0
  84. package/src/utils/debug.ts +65 -0
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@eddacraft/anvil-kindling-integration",
3
+ "version": "0.1.0",
4
+ "description": "Kindling memory integration contracts for Anvil v1",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./query": {
14
+ "types": "./dist/query-contract.d.ts",
15
+ "import": "./dist/query-contract.js"
16
+ },
17
+ "./observation": {
18
+ "types": "./dist/observation-contract.d.ts",
19
+ "import": "./dist/observation-contract.js"
20
+ },
21
+ "./config": {
22
+ "types": "./dist/config.d.ts",
23
+ "import": "./dist/config.js"
24
+ },
25
+ "./service": {
26
+ "types": "./dist/kindling-service.d.ts",
27
+ "import": "./dist/kindling-service.js"
28
+ },
29
+ "./emitters": {
30
+ "types": "./dist/emitters/index.d.ts",
31
+ "import": "./dist/emitters/index.js"
32
+ },
33
+ "./emitters/session": {
34
+ "types": "./dist/emitters/session-emitter.d.ts",
35
+ "import": "./dist/emitters/session-emitter.js"
36
+ },
37
+ "./emitters/gate": {
38
+ "types": "./dist/emitters/gate-emitter.d.ts",
39
+ "import": "./dist/emitters/gate-emitter.js"
40
+ },
41
+ "./emitters/action": {
42
+ "types": "./dist/emitters/action-emitter.d.ts",
43
+ "import": "./dist/emitters/action-emitter.js"
44
+ },
45
+ "./emitters/plan": {
46
+ "types": "./dist/emitters/plan-emitter.d.ts",
47
+ "import": "./dist/emitters/plan-emitter.js"
48
+ },
49
+ "./emitters/human-input": {
50
+ "types": "./dist/emitters/human-input-emitter.d.ts",
51
+ "import": "./dist/emitters/human-input-emitter.js"
52
+ },
53
+ "./emitters/constraint": {
54
+ "types": "./dist/emitters/constraint-emitter.d.ts",
55
+ "import": "./dist/emitters/constraint-emitter.js"
56
+ },
57
+ "./emitters/error": {
58
+ "types": "./dist/emitters/error-emitter.d.ts",
59
+ "import": "./dist/emitters/error-emitter.js"
60
+ },
61
+ "./query-service": {
62
+ "types": "./dist/query-service.d.ts",
63
+ "import": "./dist/query-service.js"
64
+ },
65
+ "./query-limits": {
66
+ "types": "./dist/query-limits.d.ts",
67
+ "import": "./dist/query-limits.js"
68
+ },
69
+ "./sensitive-data": {
70
+ "types": "./dist/sensitive-data-validator.d.ts",
71
+ "import": "./dist/sensitive-data-validator.js"
72
+ },
73
+ "./retention": {
74
+ "types": "./dist/retention.d.ts",
75
+ "import": "./dist/retention.js"
76
+ },
77
+ "./status": {
78
+ "types": "./dist/status.d.ts",
79
+ "import": "./dist/status.js"
80
+ },
81
+ "./adapter": {
82
+ "types": "./dist/adapter.d.ts",
83
+ "import": "./dist/adapter.js"
84
+ }
85
+ },
86
+ "files": [
87
+ "dist",
88
+ "src"
89
+ ],
90
+ "dependencies": {
91
+ "@eddacraft/kindling-core": "0.1.1",
92
+ "zod": "^4.3.6"
93
+ },
94
+ "devDependencies": {
95
+ "@types/node": "^25.2.3",
96
+ "typescript": "^5.9.3",
97
+ "vitest": "^4.0.18"
98
+ },
99
+ "keywords": [
100
+ "anvil",
101
+ "kindling",
102
+ "memory",
103
+ "observability",
104
+ "governance"
105
+ ],
106
+ "license": "PROPRIETARY",
107
+ "scripts": {
108
+ "build": "tsc",
109
+ "test": "vitest",
110
+ "bench": "vitest bench",
111
+ "generate:openapi": "tsx scripts/generate-openapi.ts",
112
+ "lint": "eslint src --ext .ts"
113
+ }
114
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Anvil-Kindling Adapter
3
+ *
4
+ * Maps Anvil's 11 observation kinds (rich, domain-specific schemas)
5
+ * to Kindling core's generic observation model for storage and retrieval.
6
+ *
7
+ * Anvil observation data is serialized to `content` as JSON.
8
+ * The original Anvil kind is preserved in `provenance.anvil_kind`.
9
+ */
10
+
11
+ import { randomUUID } from 'node:crypto';
12
+ import type { KindlingService, Capsule, ID } from '@eddacraft/kindling-core';
13
+ import type { Observation as AnvilObservation } from './observation-contract.js';
14
+ import { OBSERVATION_CONTRACT_VERSION } from './observation-contract.js';
15
+ import { createDebugger } from './utils/debug.js';
16
+
17
+ const debug = createDebugger('kindling');
18
+
19
+ /** Map Anvil observation kinds to Kindling's generic observation kinds */
20
+ const KIND_MAP: Record<AnvilObservation['kind'], string> = {
21
+ session_start: 'message',
22
+ session_end: 'message',
23
+ plan_created: 'message',
24
+ plan_edited: 'message',
25
+ plan_approved: 'message',
26
+ plan_rejected: 'message',
27
+ action_executed: 'command',
28
+ gate_evaluated: 'command',
29
+ constraint_applied: 'message',
30
+ human_input: 'message',
31
+ error: 'error',
32
+ };
33
+
34
+ export interface AnvilKindlingAdapterConfig {
35
+ service: KindlingService;
36
+ /** Repo path for scope isolation */
37
+ repoId?: string;
38
+ }
39
+
40
+ /**
41
+ * Bridges Anvil observation emission to Kindling storage.
42
+ *
43
+ * Usage:
44
+ * ```ts
45
+ * const adapter = new AnvilKindlingAdapter({ service });
46
+ * const capsule = adapter.startSession(sessionId, scopeIds);
47
+ * adapter.emit(observation);
48
+ * adapter.endSession(capsule.id);
49
+ * ```
50
+ */
51
+ export class AnvilKindlingAdapter {
52
+ private service: KindlingService;
53
+ private repoId: string | undefined;
54
+
55
+ constructor(config: AnvilKindlingAdapterConfig) {
56
+ this.service = config.service;
57
+ this.repoId = config.repoId;
58
+ debug('AnvilKindlingAdapter created', { repoId: config.repoId });
59
+ }
60
+
61
+ /**
62
+ * Open a Kindling capsule for an Anvil session.
63
+ * Call this when a CLI command starts.
64
+ */
65
+ startSession(sessionId: string, intent: string): Capsule {
66
+ debug('starting session', { sessionId, intent });
67
+ return this.service.openCapsule({
68
+ type: 'session',
69
+ intent,
70
+ scopeIds: {
71
+ sessionId,
72
+ repoId: this.repoId,
73
+ },
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Close the capsule for a session.
79
+ * Call this when a CLI command ends.
80
+ */
81
+ endSession(capsuleId: ID, summaryContent?: string): Capsule {
82
+ debug('ending session', { capsuleId });
83
+ return this.service.closeCapsule(capsuleId, {
84
+ generateSummary: !!summaryContent,
85
+ summaryContent,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Emit an Anvil observation to Kindling.
91
+ * The rich Anvil schema is serialized to content; the original kind
92
+ * is preserved in provenance for filtering.
93
+ */
94
+ emit(observation: AnvilObservation, capsuleId?: ID): void {
95
+ debug('emitting observation via adapter', { kind: observation.kind, capsuleId });
96
+ const kindlingObs = {
97
+ id: randomUUID(),
98
+ kind: KIND_MAP[observation.kind] as 'message' | 'command' | 'error',
99
+ content: JSON.stringify(observation),
100
+ provenance: {
101
+ anvil_kind: observation.kind,
102
+ anvil_contract_version: OBSERVATION_CONTRACT_VERSION,
103
+ },
104
+ ts: Date.now(),
105
+ scopeIds: {
106
+ sessionId: observation.session_id,
107
+ repoId: this.repoId,
108
+ },
109
+ redacted: false,
110
+ };
111
+
112
+ this.service.appendObservation(kindlingObs, {
113
+ capsuleId,
114
+ validate: true,
115
+ });
116
+ }
117
+ }
package/src/config.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Kindling Configuration Schema (KINDLING-002)
3
+ *
4
+ * Defines configuration for the Kindling integration layer.
5
+ * Configuration is read from `.anvilrc` or `anvil.config.json` in the project root.
6
+ *
7
+ * @see kindling-service.ts for how config is consumed
8
+ */
9
+
10
+ import { z } from 'zod';
11
+ import { readFileSync, existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ // =============================================================================
15
+ // Configuration Schema
16
+ // =============================================================================
17
+
18
+ /**
19
+ * Capture flags: which observation kinds to record
20
+ */
21
+ export const CaptureConfigSchema = z.object({
22
+ sessions: z.boolean().default(true).describe('Record session start/end'),
23
+ gates: z.boolean().default(true).describe('Record gate evaluations'),
24
+ actions: z.boolean().default(true).describe('Record action executions'),
25
+ plans: z.boolean().default(true).describe('Record plan lifecycle events'),
26
+ human_inputs: z.boolean().default(true).describe('Record human inputs'),
27
+ constraints: z.boolean().default(true).describe('Record constraint applications'),
28
+ errors: z.boolean().default(true).describe('Record errors'),
29
+ });
30
+
31
+ export type CaptureConfig = z.infer<typeof CaptureConfigSchema>;
32
+
33
+ /**
34
+ * Retention policy
35
+ */
36
+ export const RetentionConfigSchema = z.object({
37
+ days: z.number().int().positive().default(90).describe('Days to retain observations'),
38
+ auto_prune: z.boolean().default(false).describe('Automatically prune on session start'),
39
+ });
40
+
41
+ export type RetentionConfig = z.infer<typeof RetentionConfigSchema>;
42
+
43
+ /**
44
+ * Query limit defaults
45
+ */
46
+ export const QueryLimitConfigSchema = z.object({
47
+ max_results: z
48
+ .number()
49
+ .int()
50
+ .positive()
51
+ .max(1000)
52
+ .default(100)
53
+ .describe('Default max results per query'),
54
+ max_payload_bytes: z
55
+ .number()
56
+ .int()
57
+ .positive()
58
+ .max(10 * 1024 * 1024)
59
+ .default(1024 * 1024)
60
+ .describe('Default max payload size in bytes'),
61
+ });
62
+
63
+ export type QueryLimitConfig = z.infer<typeof QueryLimitConfigSchema>;
64
+
65
+ /**
66
+ * Full Kindling configuration
67
+ */
68
+ export const KindlingConfigSchema = z.object({
69
+ enabled: z.boolean().default(false).describe('Whether Kindling recording is active'),
70
+ database_path: z
71
+ .string()
72
+ .default('.anvil/kindling.db')
73
+ .describe('Path to the SQLite database (relative to project root)'),
74
+ retention: RetentionConfigSchema.default(() => ({
75
+ days: 90,
76
+ auto_prune: false,
77
+ })),
78
+ capture: CaptureConfigSchema.default(() => ({
79
+ sessions: true,
80
+ gates: true,
81
+ actions: true,
82
+ plans: true,
83
+ human_inputs: true,
84
+ constraints: true,
85
+ errors: true,
86
+ })),
87
+ query_limits: QueryLimitConfigSchema.default(() => ({
88
+ max_results: 100,
89
+ max_payload_bytes: 1024 * 1024,
90
+ })),
91
+ });
92
+
93
+ export type KindlingConfig = z.infer<typeof KindlingConfigSchema>;
94
+
95
+ // =============================================================================
96
+ // Default Configuration
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Default configuration: disabled, sensible defaults for everything else
101
+ */
102
+ export const DEFAULT_KINDLING_CONFIG: KindlingConfig = KindlingConfigSchema.parse({});
103
+
104
+ // =============================================================================
105
+ // Configuration Loading
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Known configuration file names, checked in order
110
+ */
111
+ const CONFIG_FILE_NAMES = ['.anvilrc', 'anvil.config.json'] as const;
112
+
113
+ /**
114
+ * Load Kindling configuration from the project root.
115
+ *
116
+ * Searches for `.anvilrc` or `anvil.config.json` in the given directory.
117
+ * Expects a JSON file with an optional `kindling` key containing the config.
118
+ *
119
+ * Returns the default (disabled) config if no file is found or if the
120
+ * `kindling` key is absent.
121
+ *
122
+ * @param projectRoot - Absolute path to the project root directory
123
+ * @returns Parsed and validated KindlingConfig
124
+ */
125
+ export function loadKindlingConfig(projectRoot: string): KindlingConfig {
126
+ for (const fileName of CONFIG_FILE_NAMES) {
127
+ const filePath = join(projectRoot, fileName);
128
+
129
+ if (!existsSync(filePath)) {
130
+ continue;
131
+ }
132
+
133
+ try {
134
+ const raw = readFileSync(filePath, 'utf-8');
135
+ const parsed: unknown = JSON.parse(raw);
136
+
137
+ if (parsed === null || typeof parsed !== 'object') {
138
+ continue;
139
+ }
140
+
141
+ const record = parsed as Record<string, unknown>;
142
+
143
+ if (!('kindling' in record) || record['kindling'] === undefined) {
144
+ // Config file exists but has no kindling section
145
+ return DEFAULT_KINDLING_CONFIG;
146
+ }
147
+
148
+ const result = KindlingConfigSchema.safeParse(record['kindling']);
149
+
150
+ if (result.success) {
151
+ return result.data;
152
+ }
153
+
154
+ // Invalid config shape -- fall back to defaults rather than crashing
155
+ // The caller (service layer) will operate in disabled mode
156
+ return DEFAULT_KINDLING_CONFIG;
157
+ } catch {
158
+ // JSON parse error or read error -- fall back to defaults
159
+ return DEFAULT_KINDLING_CONFIG;
160
+ }
161
+ }
162
+
163
+ // No config file found
164
+ return DEFAULT_KINDLING_CONFIG;
165
+ }
166
+
167
+ /**
168
+ * Check whether a specific observation kind should be captured based on config.
169
+ *
170
+ * @param config - Kindling configuration
171
+ * @param kind - Observation kind string
172
+ * @returns true if the observation should be captured
173
+ */
174
+ export function shouldCapture(config: KindlingConfig, kind: string): boolean {
175
+ if (!config.enabled) {
176
+ return false;
177
+ }
178
+
179
+ switch (kind) {
180
+ case 'session_start':
181
+ case 'session_end':
182
+ return config.capture.sessions;
183
+ case 'gate_evaluated':
184
+ return config.capture.gates;
185
+ case 'action_executed':
186
+ return config.capture.actions;
187
+ case 'plan_created':
188
+ case 'plan_edited':
189
+ case 'plan_approved':
190
+ case 'plan_rejected':
191
+ return config.capture.plans;
192
+ case 'human_input':
193
+ return config.capture.human_inputs;
194
+ case 'constraint_applied':
195
+ return config.capture.constraints;
196
+ case 'error':
197
+ return config.capture.errors;
198
+ default:
199
+ // Unknown kinds are captured by default when enabled
200
+ return true;
201
+ }
202
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Action Emitter (KINDLING-005)
3
+ *
4
+ * Emits action_executed observations when commands, tool invocations,
5
+ * file operations, or diff applications occur.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import type { KindlingService } from '../kindling-service.js';
10
+ import type { ActionExecutedObservation } from '../observation-contract.js';
11
+ import { createDebugger } from '../utils/debug.js';
12
+
13
+ const debug = createDebugger('kindling');
14
+
15
+ // =============================================================================
16
+ // Input Types
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Action execution details to be recorded
21
+ */
22
+ export interface ActionDetails {
23
+ session_id: string;
24
+ action_type: 'command' | 'tool_invocation' | 'file_write' | 'file_delete' | 'diff_apply';
25
+ details: {
26
+ command?: string;
27
+ tool_name?: string;
28
+ file_paths?: string[];
29
+ diff_summary?: {
30
+ additions: number;
31
+ deletions: number;
32
+ files_changed: number;
33
+ };
34
+ working_directory: string;
35
+ environment_target?: string;
36
+ };
37
+ governed_by_gate_id?: string;
38
+ governed_by_plan_id?: string;
39
+ outcome: 'success' | 'failure' | 'partial';
40
+ exit_code?: number;
41
+ duration_ms: number;
42
+ }
43
+
44
+ // =============================================================================
45
+ // Emitter
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Emit an action_executed observation.
50
+ *
51
+ * @param service - KindlingService instance
52
+ * @param actionDetails - Action execution details
53
+ * @returns The generated action_id
54
+ */
55
+ export function emitActionExecuted(service: KindlingService, actionDetails: ActionDetails): string {
56
+ const actionId = randomUUID();
57
+ debug('emitting action_executed', {
58
+ actionId,
59
+ actionType: actionDetails.action_type,
60
+ outcome: actionDetails.outcome,
61
+ });
62
+
63
+ const observation: ActionExecutedObservation = {
64
+ kind: 'action_executed',
65
+ session_id: actionDetails.session_id,
66
+ timestamp: new Date().toISOString(),
67
+ action_id: actionId,
68
+ action_type: actionDetails.action_type,
69
+ details: {
70
+ command: actionDetails.details.command,
71
+ tool_name: actionDetails.details.tool_name,
72
+ file_paths: actionDetails.details.file_paths,
73
+ diff_summary: actionDetails.details.diff_summary,
74
+ working_directory: actionDetails.details.working_directory,
75
+ environment_target: actionDetails.details.environment_target,
76
+ },
77
+ governed_by_gate_id: actionDetails.governed_by_gate_id,
78
+ governed_by_plan_id: actionDetails.governed_by_plan_id,
79
+ outcome: actionDetails.outcome,
80
+ exit_code: actionDetails.exit_code,
81
+ duration_ms: actionDetails.duration_ms,
82
+ };
83
+
84
+ // Fire-and-forget
85
+ service.emit(observation).catch(() => {
86
+ // Silently swallow
87
+ });
88
+
89
+ return actionId;
90
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Constraint Emitter (KINDLING-007b)
3
+ *
4
+ * Emits constraint_applied observations when Anvil prevents an action
5
+ * due to policy, scope, environment, or approval requirements.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import type { KindlingService } from '../kindling-service.js';
10
+ import type { ConstraintAppliedObservation } from '../observation-contract.js';
11
+
12
+ // =============================================================================
13
+ // Input Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Constraint application details to be recorded
18
+ */
19
+ export interface ConstraintDetails {
20
+ session_id: string;
21
+ constraint_type: 'policy' | 'rule' | 'scope' | 'environment' | 'approval_required';
22
+ prevented_action: {
23
+ action_type: string;
24
+ action_target?: string;
25
+ };
26
+ reason: string;
27
+ scope?: string;
28
+ environment?: string;
29
+ options_available?: string[];
30
+ options_allowed?: string[];
31
+ }
32
+
33
+ // =============================================================================
34
+ // Emitter
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Emit a constraint_applied observation.
39
+ *
40
+ * @param service - KindlingService instance
41
+ * @param constraint - Constraint application details
42
+ * @returns The generated constraint_id
43
+ */
44
+ export function emitConstraintApplied(
45
+ service: KindlingService,
46
+ constraint: ConstraintDetails
47
+ ): string {
48
+ const constraintId = randomUUID();
49
+
50
+ const observation: ConstraintAppliedObservation = {
51
+ kind: 'constraint_applied',
52
+ session_id: constraint.session_id,
53
+ timestamp: new Date().toISOString(),
54
+ constraint_id: constraintId,
55
+ constraint_type: constraint.constraint_type,
56
+ prevented_action: {
57
+ action_type: constraint.prevented_action.action_type,
58
+ action_target: constraint.prevented_action.action_target,
59
+ },
60
+ reason: constraint.reason,
61
+ scope: constraint.scope,
62
+ environment: constraint.environment,
63
+ options_available: constraint.options_available,
64
+ options_allowed: constraint.options_allowed,
65
+ };
66
+
67
+ // Fire-and-forget
68
+ service.emit(observation).catch(() => {
69
+ // Silently swallow
70
+ });
71
+
72
+ return constraintId;
73
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Error Emitter (KINDLING-008)
3
+ *
4
+ * Emits error observations when failures occur.
5
+ * "Errors are not noise, they are data."
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import type { KindlingService } from '../kindling-service.js';
10
+ import type { ErrorObservation } from '../observation-contract.js';
11
+ import { createDebugger } from '../utils/debug.js';
12
+
13
+ const debug = createDebugger('kindling');
14
+
15
+ // =============================================================================
16
+ // Input Types
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Error details to be recorded
21
+ */
22
+ export interface ErrorDetails {
23
+ session_id: string;
24
+ error_type:
25
+ | 'command_failure'
26
+ | 'tool_error'
27
+ | 'aborted_execution'
28
+ | 'partial_state'
29
+ | 'validation_failure';
30
+ context: {
31
+ component: string;
32
+ action_id?: string;
33
+ gate_id?: string;
34
+ };
35
+ error_message: string;
36
+ error_code?: string;
37
+ exit_code?: number;
38
+ recoverable: boolean;
39
+ partial_state_description?: string;
40
+ }
41
+
42
+ // =============================================================================
43
+ // Emitter
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Emit an error observation.
48
+ *
49
+ * @param service - KindlingService instance
50
+ * @param errorDetails - Error details
51
+ * @returns The generated error_id
52
+ */
53
+ export function emitError(service: KindlingService, errorDetails: ErrorDetails): string {
54
+ const errorId = randomUUID();
55
+ debug('emitting error observation', {
56
+ errorId,
57
+ errorType: errorDetails.error_type,
58
+ component: errorDetails.context.component,
59
+ recoverable: errorDetails.recoverable,
60
+ });
61
+
62
+ const observation: ErrorObservation = {
63
+ kind: 'error',
64
+ session_id: errorDetails.session_id,
65
+ timestamp: new Date().toISOString(),
66
+ error_id: errorId,
67
+ error_type: errorDetails.error_type,
68
+ context: {
69
+ component: errorDetails.context.component,
70
+ action_id: errorDetails.context.action_id,
71
+ gate_id: errorDetails.context.gate_id,
72
+ },
73
+ error_message: errorDetails.error_message,
74
+ error_code: errorDetails.error_code,
75
+ exit_code: errorDetails.exit_code,
76
+ recoverable: errorDetails.recoverable,
77
+ partial_state_description: errorDetails.partial_state_description,
78
+ };
79
+
80
+ // Fire-and-forget
81
+ service.emit(observation).catch(() => {
82
+ // Silently swallow
83
+ });
84
+
85
+ return errorId;
86
+ }