@way_marks/server 1.0.0 → 2.0.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.
@@ -90,377 +90,403 @@ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
90
90
  const DB_PATH = process.env.WAYMARK_DB_PATH
91
91
  || path.join(PROJECT_ROOT, '.waymark', 'waymark.db');
92
92
  const DB_DIR = path.dirname(DB_PATH);
93
- // Ensure database directory exists
94
- if (!fs.existsSync(DB_DIR)) {
95
- fs.mkdirSync(DB_DIR, { recursive: true });
96
- }
97
- const db = new better_sqlite3_1.default(DB_PATH);
98
- // Create table on startup
99
- db.exec(`
100
- CREATE TABLE IF NOT EXISTS action_log (
101
- id INTEGER PRIMARY KEY AUTOINCREMENT,
102
- action_id TEXT UNIQUE NOT NULL,
103
- session_id TEXT NOT NULL,
104
- tool_name TEXT NOT NULL,
105
- target_path TEXT,
106
- input_payload TEXT NOT NULL,
107
- before_snapshot TEXT,
108
- after_snapshot TEXT,
109
- status TEXT NOT NULL DEFAULT 'pending',
110
- error_message TEXT,
111
- stdout TEXT,
112
- stderr TEXT,
113
- rolled_back INTEGER NOT NULL DEFAULT 0,
114
- rolled_back_at TEXT,
115
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
116
- )
117
- `);
118
- // Migrate existing DBs — add stdout/stderr if not present
119
- try {
120
- db.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
121
- }
122
- catch { }
123
- try {
124
- db.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
125
- }
126
- catch { }
127
- // Migrate v2: policy engine columns
128
- try {
129
- db.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
130
- }
131
- catch { }
132
- try {
133
- db.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
134
- }
135
- catch { }
136
- try {
137
- db.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
138
- }
139
- catch { }
140
- // Migrate v3: approval flow columns
141
- try {
142
- db.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
143
- }
144
- catch { }
145
- try {
146
- db.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
147
- }
148
- catch { }
149
- try {
150
- db.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
151
- }
152
- catch { }
153
- try {
154
- db.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
155
- }
156
- catch { }
157
- // Migrate v4: Phase 1 — plan mode logging visibility
158
- try {
159
- db.exec("ALTER TABLE action_log ADD COLUMN event_type TEXT NOT NULL DEFAULT 'execution'");
160
- }
161
- catch { }
162
- try {
163
- db.exec('ALTER TABLE action_log ADD COLUMN observation_context TEXT');
164
- }
165
- catch { }
166
- try {
167
- db.exec('ALTER TABLE action_log ADD COLUMN request_source TEXT DEFAULT \'direct\'');
168
- }
169
- catch { }
170
- // Migrate v5: Phase 5B — CLI action logging (GitHub Copilot CLI wrapper)
171
- try {
172
- db.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
173
- }
174
- catch { }
175
- // Migrate v6: Phase 1 — Session-level rollback
176
- try {
177
- db.exec('ALTER TABLE action_log ADD COLUMN rollback_group TEXT');
178
- }
179
- catch { }
180
- try {
181
- db.exec('ALTER TABLE action_log ADD COLUMN is_reversible INTEGER DEFAULT 1');
182
- }
183
- catch { }
184
- try {
185
- db.exec('ALTER TABLE action_log ADD COLUMN revert_action_id TEXT');
186
- }
187
- catch { }
188
- // Phase 1: Create sessions table
189
- db.exec(`
190
- CREATE TABLE IF NOT EXISTS sessions (
191
- session_id TEXT PRIMARY KEY,
192
- user_id TEXT,
193
- project_id TEXT,
194
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
195
- rolled_back_at DATETIME,
196
- status TEXT NOT NULL DEFAULT 'active'
197
- )
198
- `);
199
- // Indexes for query performance
200
- try {
201
- db.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
202
- }
203
- catch { }
204
- try {
205
- db.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
206
- }
207
- catch { }
208
- try {
209
- db.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
210
- }
211
- catch { }
212
- // Phase 3: Add indexes for pagination and filtering
213
- try {
214
- db.exec('CREATE INDEX IF NOT EXISTS idx_tool_name ON action_log(tool_name)');
215
- }
216
- catch { }
217
- try {
218
- db.exec('CREATE INDEX IF NOT EXISTS idx_created_at ON action_log(created_at DESC)');
219
- }
220
- catch { }
221
- try {
222
- db.exec('CREATE INDEX IF NOT EXISTS idx_status_created ON action_log(status, created_at DESC)');
223
- }
224
- catch { }
225
- // Phase 3: Create archive table (same schema as action_log)
226
- db.exec(`
227
- CREATE TABLE IF NOT EXISTS action_archive (
228
- id INTEGER PRIMARY KEY AUTOINCREMENT,
229
- action_id TEXT UNIQUE NOT NULL,
230
- session_id TEXT NOT NULL,
231
- tool_name TEXT NOT NULL,
232
- target_path TEXT,
233
- input_payload TEXT NOT NULL,
234
- before_snapshot TEXT,
235
- after_snapshot TEXT,
236
- status TEXT NOT NULL DEFAULT 'pending',
237
- error_message TEXT,
238
- stdout TEXT,
239
- stderr TEXT,
240
- rolled_back INTEGER NOT NULL DEFAULT 0,
241
- rolled_back_at TEXT,
242
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
243
- decision TEXT NOT NULL DEFAULT 'allow',
244
- policy_reason TEXT,
245
- matched_rule TEXT,
246
- approved_at TEXT,
247
- approved_by TEXT,
248
- rejected_at TEXT,
249
- rejected_reason TEXT,
250
- event_type TEXT NOT NULL DEFAULT 'execution',
251
- observation_context TEXT,
252
- request_source TEXT DEFAULT 'direct',
253
- source TEXT DEFAULT 'mcp',
254
- rollback_group TEXT,
255
- is_reversible INTEGER DEFAULT 1,
256
- revert_action_id TEXT
257
- )
258
- `);
259
- // Phase 1: Add same columns to archive (migrations)
260
- try {
261
- db.exec('ALTER TABLE action_archive ADD COLUMN rollback_group TEXT');
262
- }
263
- catch { }
264
- try {
265
- db.exec('ALTER TABLE action_archive ADD COLUMN is_reversible INTEGER DEFAULT 1');
266
- }
267
- catch { }
268
- try {
269
- db.exec('ALTER TABLE action_archive ADD COLUMN revert_action_id TEXT');
270
- }
271
- catch { }
272
- // Phase 3: Add indexes on archive table
273
- try {
274
- db.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
275
- }
276
- catch { }
277
- try {
278
- db.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
279
- }
280
- catch { }
281
- // Phase 1: Add indexes for session rollback
282
- try {
283
- db.exec('CREATE INDEX IF NOT EXISTS idx_action_session ON action_log(session_id, status)');
284
- }
285
- catch { }
286
- try {
287
- db.exec('CREATE INDEX IF NOT EXISTS idx_action_rollback_group ON action_log(rollback_group)');
288
- }
289
- catch { }
290
- try {
291
- db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status, created_at DESC)');
292
- }
293
- catch { }
294
- try {
295
- db.exec('CREATE INDEX IF NOT EXISTS idx_archive_session ON action_archive(session_id)');
296
- }
297
- catch { }
298
- // Phase 2: Create team_members table
299
- db.exec(`
300
- CREATE TABLE IF NOT EXISTS team_members (
301
- member_id TEXT PRIMARY KEY,
302
- name TEXT NOT NULL,
303
- email TEXT NOT NULL UNIQUE,
304
- slack_id TEXT,
305
- role TEXT DEFAULT 'approver',
306
- added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
307
- added_by TEXT,
308
- status TEXT DEFAULT 'active'
309
- )
310
- `);
311
- // Phase 2: Create approval_routes table (rules for who approves what)
312
- db.exec(`
313
- CREATE TABLE IF NOT EXISTS approval_routes (
314
- route_id TEXT PRIMARY KEY,
315
- name TEXT NOT NULL,
316
- description TEXT,
317
- condition_type TEXT DEFAULT 'all_sessions',
318
- condition_json TEXT,
319
- required_approvers INTEGER DEFAULT 1,
320
- approver_ids TEXT NOT NULL,
321
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
322
- created_by TEXT,
323
- status TEXT DEFAULT 'active'
324
- )
325
- `);
326
- // Phase 2: Create approval_requests table (pending/completed approvals)
327
- db.exec(`
328
- CREATE TABLE IF NOT EXISTS approval_requests (
329
- request_id TEXT PRIMARY KEY,
330
- session_id TEXT NOT NULL,
331
- route_id TEXT NOT NULL,
332
- triggered_by TEXT NOT NULL,
333
- triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
334
- status TEXT DEFAULT 'pending',
335
- completed_at DATETIME,
336
- approver_ids TEXT NOT NULL,
337
- approved_count INTEGER DEFAULT 0,
338
- rejected_count INTEGER DEFAULT 0,
339
- approval_details TEXT
340
- )
341
- `);
342
- // Phase 2: Create approval_decisions table (audit trail)
343
- db.exec(`
344
- CREATE TABLE IF NOT EXISTS approval_decisions (
345
- decision_id TEXT PRIMARY KEY,
346
- request_id TEXT NOT NULL,
347
- approver_id TEXT NOT NULL,
348
- decision TEXT NOT NULL,
349
- reason TEXT,
350
- decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
351
- )
352
- `);
353
- // Phase 2: Add indexes for team tables
354
- try {
355
- db.exec('CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email)');
356
- }
357
- catch { }
358
- try {
359
- db.exec('CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status)');
360
- }
361
- catch { }
362
- try {
363
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_routes_status ON approval_routes(status)');
364
- }
365
- catch { }
366
- try {
367
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_session ON approval_requests(session_id)');
368
- }
369
- catch { }
370
- try {
371
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status)');
372
- }
373
- catch { }
374
- try {
375
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_triggered ON approval_requests(triggered_at DESC)');
376
- }
377
- catch { }
378
- try {
379
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_request ON approval_decisions(request_id)');
380
- }
381
- catch { }
382
- try {
383
- db.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_approver ON approval_decisions(approver_id)');
384
- }
385
- catch { }
386
- // Phase 3: Create escalation_rules table
387
- db.exec(`
388
- CREATE TABLE IF NOT EXISTS escalation_rules (
389
- rule_id TEXT PRIMARY KEY,
390
- name TEXT NOT NULL,
391
- description TEXT,
392
- timeout_hours INTEGER DEFAULT 24,
393
- escalation_targets TEXT NOT NULL,
394
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
395
- created_by TEXT,
396
- status TEXT DEFAULT 'active'
397
- )
398
- `);
399
- // Phase 3: Create escalation_requests table
400
- db.exec(`
401
- CREATE TABLE IF NOT EXISTS escalation_requests (
402
- request_id TEXT PRIMARY KEY,
403
- approval_request_id TEXT NOT NULL,
404
- session_id TEXT NOT NULL,
405
- escalation_triggered_at DATETIME,
406
- escalation_deadline DATETIME,
407
- escalation_targets TEXT NOT NULL,
408
- status TEXT DEFAULT 'pending',
409
- decided_at DATETIME,
410
- decision TEXT
411
- )
412
- `);
413
- // Phase 3: Create escalation_decisions table
414
- db.exec(`
415
- CREATE TABLE IF NOT EXISTS escalation_decisions (
416
- decision_id TEXT PRIMARY KEY,
417
- escalation_request_id TEXT NOT NULL,
418
- target_id TEXT NOT NULL,
419
- decision TEXT NOT NULL,
420
- reason TEXT,
421
- decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
422
- )
423
- `);
424
- // Phase 3: Add indexes for escalation tables
425
- try {
426
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_rules_status ON escalation_rules(status)');
427
- }
428
- catch { }
429
- try {
430
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_approval ON escalation_requests(approval_request_id)');
431
- }
432
- catch { }
433
- try {
434
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_deadline ON escalation_requests(escalation_deadline)');
435
- }
436
- catch { }
437
- try {
438
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_status ON escalation_requests(status)');
439
- }
440
- catch { }
441
- try {
442
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_request ON escalation_decisions(escalation_request_id)');
443
- }
444
- catch { }
445
- try {
446
- db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_target ON escalation_decisions(target_id)');
447
- }
448
- catch { }
449
- const insertStmt = db.prepare(`
450
- INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
451
- VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
452
- `);
453
- const updateStmt = db.prepare(`
454
- UPDATE action_log
455
- SET status = COALESCE(@status, status),
456
- after_snapshot = COALESCE(@after_snapshot, after_snapshot),
457
- error_message = COALESCE(@error_message, error_message),
458
- stdout = COALESCE(@stdout, stdout),
459
- stderr = COALESCE(@stderr, stderr)
460
- WHERE action_id = @action_id
461
- `);
93
+ // Lazy-load database to allow tests to mock it
94
+ let db = null;
95
+ let initialized = false;
96
+ function initializeSchema(database) {
97
+ if (initialized)
98
+ return;
99
+ initialized = true;
100
+ // Create table on startup
101
+ database.exec(`
102
+ CREATE TABLE IF NOT EXISTS action_log (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ action_id TEXT UNIQUE NOT NULL,
105
+ session_id TEXT NOT NULL,
106
+ tool_name TEXT NOT NULL,
107
+ target_path TEXT,
108
+ input_payload TEXT NOT NULL,
109
+ before_snapshot TEXT,
110
+ after_snapshot TEXT,
111
+ status TEXT NOT NULL DEFAULT 'pending',
112
+ error_message TEXT,
113
+ stdout TEXT,
114
+ stderr TEXT,
115
+ rolled_back INTEGER NOT NULL DEFAULT 0,
116
+ rolled_back_at TEXT,
117
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
118
+ )
119
+ `);
120
+ // Migrate existing DBs add stdout/stderr if not present
121
+ try {
122
+ database.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
123
+ }
124
+ catch { }
125
+ try {
126
+ database.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
127
+ }
128
+ catch { }
129
+ // Migrate v2: policy engine columns
130
+ try {
131
+ database.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
132
+ }
133
+ catch { }
134
+ try {
135
+ database.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
136
+ }
137
+ catch { }
138
+ try {
139
+ database.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
140
+ }
141
+ catch { }
142
+ // Migrate v3: approval flow columns
143
+ try {
144
+ database.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
145
+ }
146
+ catch { }
147
+ try {
148
+ database.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
149
+ }
150
+ catch { }
151
+ try {
152
+ database.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
153
+ }
154
+ catch { }
155
+ try {
156
+ database.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
157
+ }
158
+ catch { }
159
+ // Migrate v4: Phase 1 plan mode logging visibility
160
+ try {
161
+ database.exec("ALTER TABLE action_log ADD COLUMN event_type TEXT NOT NULL DEFAULT 'execution'");
162
+ }
163
+ catch { }
164
+ try {
165
+ database.exec('ALTER TABLE action_log ADD COLUMN observation_context TEXT');
166
+ }
167
+ catch { }
168
+ try {
169
+ database.exec('ALTER TABLE action_log ADD COLUMN request_source TEXT DEFAULT \'direct\'');
170
+ }
171
+ catch { }
172
+ // Migrate v5: Phase 5B CLI action logging (GitHub Copilot CLI wrapper)
173
+ try {
174
+ database.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
175
+ }
176
+ catch { }
177
+ // Migrate v6: Phase 1 Session-level rollback
178
+ try {
179
+ database.exec('ALTER TABLE action_log ADD COLUMN rollback_group TEXT');
180
+ }
181
+ catch { }
182
+ try {
183
+ database.exec('ALTER TABLE action_log ADD COLUMN is_reversible INTEGER DEFAULT 1');
184
+ }
185
+ catch { }
186
+ try {
187
+ database.exec('ALTER TABLE action_log ADD COLUMN revert_action_id TEXT');
188
+ }
189
+ catch { }
190
+ // Phase 1: Create sessions table
191
+ database.exec(`
192
+ CREATE TABLE IF NOT EXISTS sessions (
193
+ session_id TEXT PRIMARY KEY,
194
+ user_id TEXT,
195
+ project_id TEXT,
196
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
197
+ rolled_back_at DATETIME,
198
+ status TEXT NOT NULL DEFAULT 'active'
199
+ )
200
+ `);
201
+ // Indexes for query performance
202
+ try {
203
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
204
+ }
205
+ catch { }
206
+ try {
207
+ database.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
208
+ }
209
+ catch { }
210
+ try {
211
+ database.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
212
+ }
213
+ catch { }
214
+ // Phase 3: Add indexes for pagination and filtering
215
+ try {
216
+ database.exec('CREATE INDEX IF NOT EXISTS idx_tool_name ON action_log(tool_name)');
217
+ }
218
+ catch { }
219
+ try {
220
+ database.exec('CREATE INDEX IF NOT EXISTS idx_created_at ON action_log(created_at DESC)');
221
+ }
222
+ catch { }
223
+ try {
224
+ database.exec('CREATE INDEX IF NOT EXISTS idx_status_created ON action_log(status, created_at DESC)');
225
+ }
226
+ catch { }
227
+ // Phase 3: Create archive table (same schema as action_log)
228
+ database.exec(`
229
+ CREATE TABLE IF NOT EXISTS action_archive (
230
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
231
+ action_id TEXT UNIQUE NOT NULL,
232
+ session_id TEXT NOT NULL,
233
+ tool_name TEXT NOT NULL,
234
+ target_path TEXT,
235
+ input_payload TEXT NOT NULL,
236
+ before_snapshot TEXT,
237
+ after_snapshot TEXT,
238
+ status TEXT NOT NULL DEFAULT 'pending',
239
+ error_message TEXT,
240
+ stdout TEXT,
241
+ stderr TEXT,
242
+ rolled_back INTEGER NOT NULL DEFAULT 0,
243
+ rolled_back_at TEXT,
244
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
245
+ decision TEXT NOT NULL DEFAULT 'allow',
246
+ policy_reason TEXT,
247
+ matched_rule TEXT,
248
+ approved_at TEXT,
249
+ approved_by TEXT,
250
+ rejected_at TEXT,
251
+ rejected_reason TEXT,
252
+ event_type TEXT NOT NULL DEFAULT 'execution',
253
+ observation_context TEXT,
254
+ request_source TEXT DEFAULT 'direct',
255
+ source TEXT DEFAULT 'mcp',
256
+ rollback_group TEXT,
257
+ is_reversible INTEGER DEFAULT 1,
258
+ revert_action_id TEXT
259
+ )
260
+ `);
261
+ // Phase 1: Add same columns to archive (migrations)
262
+ try {
263
+ database.exec('ALTER TABLE action_archive ADD COLUMN rollback_group TEXT');
264
+ }
265
+ catch { }
266
+ try {
267
+ database.exec('ALTER TABLE action_archive ADD COLUMN is_reversible INTEGER DEFAULT 1');
268
+ }
269
+ catch { }
270
+ try {
271
+ database.exec('ALTER TABLE action_archive ADD COLUMN revert_action_id TEXT');
272
+ }
273
+ catch { }
274
+ // Phase 3: Add indexes on archive table
275
+ try {
276
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
277
+ }
278
+ catch { }
279
+ try {
280
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
281
+ }
282
+ catch { }
283
+ // Phase 1: Add indexes for session rollback
284
+ try {
285
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_session ON action_log(session_id, status)');
286
+ }
287
+ catch { }
288
+ try {
289
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_rollback_group ON action_log(rollback_group)');
290
+ }
291
+ catch { }
292
+ try {
293
+ database.exec('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status, created_at DESC)');
294
+ }
295
+ catch { }
296
+ try {
297
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_session ON action_archive(session_id)');
298
+ }
299
+ catch { }
300
+ // Phase 2: Create team_members table
301
+ database.exec(`
302
+ CREATE TABLE IF NOT EXISTS team_members (
303
+ member_id TEXT PRIMARY KEY,
304
+ name TEXT NOT NULL,
305
+ email TEXT NOT NULL UNIQUE,
306
+ slack_id TEXT,
307
+ role TEXT DEFAULT 'approver',
308
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
309
+ added_by TEXT,
310
+ status TEXT DEFAULT 'active'
311
+ )
312
+ `);
313
+ // Phase 2: Create approval_routes table (rules for who approves what)
314
+ database.exec(`
315
+ CREATE TABLE IF NOT EXISTS approval_routes (
316
+ route_id TEXT PRIMARY KEY,
317
+ name TEXT NOT NULL,
318
+ description TEXT,
319
+ condition_type TEXT DEFAULT 'all_sessions',
320
+ condition_json TEXT,
321
+ required_approvers INTEGER DEFAULT 1,
322
+ approver_ids TEXT NOT NULL,
323
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
324
+ created_by TEXT,
325
+ status TEXT DEFAULT 'active'
326
+ )
327
+ `);
328
+ // Phase 2: Create approval_requests table (pending/completed approvals)
329
+ database.exec(`
330
+ CREATE TABLE IF NOT EXISTS approval_requests (
331
+ request_id TEXT PRIMARY KEY,
332
+ session_id TEXT NOT NULL,
333
+ route_id TEXT NOT NULL,
334
+ triggered_by TEXT NOT NULL,
335
+ triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
336
+ status TEXT DEFAULT 'pending',
337
+ completed_at DATETIME,
338
+ approver_ids TEXT NOT NULL,
339
+ approved_count INTEGER DEFAULT 0,
340
+ rejected_count INTEGER DEFAULT 0,
341
+ approval_details TEXT
342
+ )
343
+ `);
344
+ // Phase 2: Create approval_decisions table (audit trail)
345
+ database.exec(`
346
+ CREATE TABLE IF NOT EXISTS approval_decisions (
347
+ decision_id TEXT PRIMARY KEY,
348
+ request_id TEXT NOT NULL,
349
+ approver_id TEXT NOT NULL,
350
+ decision TEXT NOT NULL,
351
+ reason TEXT,
352
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
353
+ )
354
+ `);
355
+ // Phase 2: Add indexes for team tables
356
+ try {
357
+ database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email)');
358
+ }
359
+ catch { }
360
+ try {
361
+ database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status)');
362
+ }
363
+ catch { }
364
+ try {
365
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_routes_status ON approval_routes(status)');
366
+ }
367
+ catch { }
368
+ try {
369
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_session ON approval_requests(session_id)');
370
+ }
371
+ catch { }
372
+ try {
373
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status)');
374
+ }
375
+ catch { }
376
+ try {
377
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_triggered ON approval_requests(triggered_at DESC)');
378
+ }
379
+ catch { }
380
+ try {
381
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_request ON approval_decisions(request_id)');
382
+ }
383
+ catch { }
384
+ try {
385
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_approver ON approval_decisions(approver_id)');
386
+ }
387
+ catch { }
388
+ // Phase 3: Create escalation_rules table
389
+ database.exec(`
390
+ CREATE TABLE IF NOT EXISTS escalation_rules (
391
+ rule_id TEXT PRIMARY KEY,
392
+ name TEXT NOT NULL,
393
+ description TEXT,
394
+ timeout_hours INTEGER DEFAULT 24,
395
+ escalation_targets TEXT NOT NULL,
396
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
397
+ created_by TEXT,
398
+ status TEXT DEFAULT 'active'
399
+ )
400
+ `);
401
+ // Phase 3: Create escalation_requests table
402
+ database.exec(`
403
+ CREATE TABLE IF NOT EXISTS escalation_requests (
404
+ request_id TEXT PRIMARY KEY,
405
+ approval_request_id TEXT NOT NULL,
406
+ session_id TEXT NOT NULL,
407
+ escalation_triggered_at DATETIME,
408
+ escalation_deadline DATETIME,
409
+ escalation_targets TEXT NOT NULL,
410
+ status TEXT DEFAULT 'pending',
411
+ decided_at DATETIME,
412
+ decision TEXT
413
+ )
414
+ `);
415
+ // Phase 3: Create escalation_decisions table
416
+ database.exec(`
417
+ CREATE TABLE IF NOT EXISTS escalation_decisions (
418
+ decision_id TEXT PRIMARY KEY,
419
+ escalation_request_id TEXT NOT NULL,
420
+ target_id TEXT NOT NULL,
421
+ decision TEXT NOT NULL,
422
+ reason TEXT,
423
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
424
+ )
425
+ `);
426
+ // Phase 3: Add indexes for escalation tables
427
+ try {
428
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_rules_status ON escalation_rules(status)');
429
+ }
430
+ catch { }
431
+ try {
432
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_approval ON escalation_requests(approval_request_id)');
433
+ }
434
+ catch { }
435
+ try {
436
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_deadline ON escalation_requests(escalation_deadline)');
437
+ }
438
+ catch { }
439
+ try {
440
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_status ON escalation_requests(status)');
441
+ }
442
+ catch { }
443
+ try {
444
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_request ON escalation_decisions(escalation_request_id)');
445
+ }
446
+ catch { }
447
+ try {
448
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_target ON escalation_decisions(target_id)');
449
+ }
450
+ catch { }
451
+ }
452
+ function getDb() {
453
+ if (!db) {
454
+ // Ensure database directory exists
455
+ if (!fs.existsSync(DB_DIR)) {
456
+ fs.mkdirSync(DB_DIR, { recursive: true });
457
+ }
458
+ db = new better_sqlite3_1.default(DB_PATH);
459
+ initializeSchema(db);
460
+ }
461
+ return db;
462
+ }
463
+ let insertStmt = null;
464
+ let updateStmt = null;
465
+ function getInsertStmt() {
466
+ if (!insertStmt) {
467
+ insertStmt = getDb().prepare(`
468
+ INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
469
+ VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
470
+ `);
471
+ }
472
+ return insertStmt;
473
+ }
474
+ function getUpdateStmt() {
475
+ if (!updateStmt) {
476
+ updateStmt = getDb().prepare(`
477
+ UPDATE action_log
478
+ SET status = COALESCE(@status, status),
479
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot),
480
+ error_message = COALESCE(@error_message, error_message),
481
+ stdout = COALESCE(@stdout, stdout),
482
+ stderr = COALESCE(@stderr, stderr)
483
+ WHERE action_id = @action_id
484
+ `);
485
+ }
486
+ return updateStmt;
487
+ }
462
488
  function insertAction(params) {
463
- insertStmt.run({
489
+ getInsertStmt().run({
464
490
  action_id: params.action_id,
465
491
  session_id: params.session_id,
466
492
  tool_name: params.tool_name,
@@ -478,7 +504,7 @@ function insertAction(params) {
478
504
  });
479
505
  }
480
506
  function updateAction(action_id, params) {
481
- updateStmt.run({
507
+ getUpdateStmt().run({
482
508
  action_id,
483
509
  status: params.status ?? null,
484
510
  after_snapshot: params.after_snapshot ?? null,
@@ -488,24 +514,24 @@ function updateAction(action_id, params) {
488
514
  });
489
515
  }
490
516
  function getActions() {
491
- return db.prepare(`
517
+ return getDb().prepare(`
492
518
  SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
493
519
  `).all();
494
520
  }
495
521
  function getAction(action_id) {
496
- return db.prepare(`
522
+ return getDb().prepare(`
497
523
  SELECT * FROM action_log WHERE action_id = ?
498
524
  `).get(action_id);
499
525
  }
500
526
  function markRolledBack(action_id) {
501
- db.prepare(`
527
+ getDb().prepare(`
502
528
  UPDATE action_log
503
529
  SET rolled_back = 1, rolled_back_at = datetime('now')
504
530
  WHERE action_id = ?
505
531
  `).run(action_id);
506
532
  }
507
533
  function getSessions() {
508
- return db.prepare(`
534
+ return getDb().prepare(`
509
535
  SELECT session_id,
510
536
  COUNT(*) as action_count,
511
537
  MAX(created_at) as latest
@@ -515,7 +541,7 @@ function getSessions() {
515
541
  `).all();
516
542
  }
517
543
  function approveAction(action_id, approved_by, after_snapshot) {
518
- db.prepare(`
544
+ getDb().prepare(`
519
545
  UPDATE action_log
520
546
  SET status = 'success',
521
547
  decision = 'allow',
@@ -526,7 +552,7 @@ function approveAction(action_id, approved_by, after_snapshot) {
526
552
  `).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
527
553
  }
528
554
  function rejectAction(action_id, reason) {
529
- db.prepare(`
555
+ getDb().prepare(`
530
556
  UPDATE action_log
531
557
  SET status = 'rejected',
532
558
  decision = 'rejected',
@@ -536,7 +562,7 @@ function rejectAction(action_id, reason) {
536
562
  `).run({ action_id, reason });
537
563
  }
538
564
  function getPendingCount() {
539
- const row = db.prepare(`
565
+ const row = getDb().prepare(`
540
566
  SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
541
567
  `).get();
542
568
  return row.count;
@@ -560,12 +586,12 @@ function getActionsWithFiltering(filter = {}) {
560
586
  params.search = `%${filter.search}%`;
561
587
  }
562
588
  // Get total count
563
- const countRow = db.prepare(`
589
+ const countRow = getDb().prepare(`
564
590
  SELECT COUNT(*) as count FROM action_log WHERE ${where}
565
591
  `).get(params);
566
592
  const totalCount = countRow.count;
567
593
  // Get paginated results
568
- const actions = db.prepare(`
594
+ const actions = getDb().prepare(`
569
595
  SELECT * FROM action_log WHERE ${where}
570
596
  ORDER BY created_at DESC
571
597
  LIMIT @limit OFFSET @offset
@@ -583,12 +609,12 @@ function archiveOldActions(daysOld = 30) {
583
609
  cutoffDate.setDate(cutoffDate.getDate() - daysOld);
584
610
  const cutoffStr = cutoffDate.toISOString();
585
611
  // Copy old actions to archive
586
- const result = db.prepare(`
612
+ const result = getDb().prepare(`
587
613
  INSERT OR IGNORE INTO action_archive
588
614
  SELECT * FROM action_log WHERE created_at < @cutoff
589
615
  `).run({ cutoff: cutoffStr });
590
616
  // Delete from main table (but keep recent entries)
591
- const deleteStmt = db.prepare(`
617
+ const deleteStmt = getDb().prepare(`
592
618
  DELETE FROM action_log WHERE created_at < @cutoff
593
619
  AND id NOT IN (
594
620
  SELECT id FROM action_log ORDER BY created_at DESC LIMIT 1000
@@ -615,11 +641,11 @@ function getArchivedActionsWithFiltering(filter = {}) {
615
641
  where += ' AND (action_id LIKE @search OR target_path LIKE @search)';
616
642
  params.search = `%${filter.search}%`;
617
643
  }
618
- const countRow = db.prepare(`
644
+ const countRow = getDb().prepare(`
619
645
  SELECT COUNT(*) as count FROM action_archive WHERE ${where}
620
646
  `).get(params);
621
647
  const totalCount = countRow.count;
622
- const actions = db.prepare(`
648
+ const actions = getDb().prepare(`
623
649
  SELECT * FROM action_archive WHERE ${where}
624
650
  ORDER BY created_at DESC
625
651
  LIMIT @limit OFFSET @offset
@@ -637,34 +663,34 @@ function getSummaryStats() {
637
663
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
638
664
  const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
639
665
  const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
640
- const total = db.prepare(`
666
+ const total = getDb().prepare(`
641
667
  SELECT COUNT(*) as count FROM action_log
642
668
  `).get();
643
- const pending = db.prepare(`
669
+ const pending = getDb().prepare(`
644
670
  SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
645
671
  `).get();
646
- const approved = db.prepare(`
672
+ const approved = getDb().prepare(`
647
673
  SELECT COUNT(*) as count FROM action_log WHERE status = 'success' OR decision = 'allow'
648
674
  `).get();
649
- const rejected = db.prepare(`
675
+ const rejected = getDb().prepare(`
650
676
  SELECT COUNT(*) as count FROM action_log WHERE status = 'rejected' OR decision = 'rejected'
651
677
  `).get();
652
- const todayCount = db.prepare(`
678
+ const todayCount = getDb().prepare(`
653
679
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @today
654
680
  `).get({ today });
655
- const weekCount = db.prepare(`
681
+ const weekCount = getDb().prepare(`
656
682
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @weekAgo
657
683
  `).get({ weekAgo });
658
- const monthCount = db.prepare(`
684
+ const monthCount = getDb().prepare(`
659
685
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @monthAgo
660
686
  `).get({ monthAgo });
661
- const topTools = db.prepare(`
687
+ const topTools = getDb().prepare(`
662
688
  SELECT tool_name as tool, COUNT(*) as count FROM action_log
663
689
  GROUP BY tool_name
664
690
  ORDER BY count DESC
665
691
  LIMIT 5
666
692
  `).all();
667
- const topPaths = db.prepare(`
693
+ const topPaths = getDb().prepare(`
668
694
  SELECT target_path as path, COUNT(*) as count FROM action_log
669
695
  WHERE target_path IS NOT NULL
670
696
  GROUP BY target_path
@@ -685,23 +711,23 @@ function getSummaryStats() {
685
711
  }
686
712
  // Phase 1: Session management functions
687
713
  function createSession(session_id, user_id, project_id) {
688
- db.prepare(`
714
+ getDb().prepare(`
689
715
  INSERT OR IGNORE INTO sessions (session_id, user_id, project_id, status)
690
716
  VALUES (?, ?, ?, 'active')
691
717
  `).run(session_id, user_id ?? null, project_id ?? null);
692
718
  }
693
719
  function getSession(session_id) {
694
- return db.prepare(`
720
+ return getDb().prepare(`
695
721
  SELECT * FROM sessions WHERE session_id = ?
696
722
  `).get(session_id);
697
723
  }
698
724
  function getAllSessions() {
699
- return db.prepare(`
725
+ return getDb().prepare(`
700
726
  SELECT * FROM sessions ORDER BY created_at DESC
701
727
  `).all();
702
728
  }
703
729
  function getSessionActions(session_id) {
704
- return db.prepare(`
730
+ return getDb().prepare(`
705
731
  SELECT * FROM action_log
706
732
  WHERE session_id = ?
707
733
  ORDER BY created_at ASC
@@ -710,7 +736,7 @@ function getSessionActions(session_id) {
710
736
  function rollbackSession(session_id) {
711
737
  try {
712
738
  // Start transaction
713
- const transaction = db.transaction(() => {
739
+ const transaction = getDb().transaction(() => {
714
740
  // Get all actions in session
715
741
  const actions = getSessionActions(session_id);
716
742
  if (actions.length === 0) {
@@ -740,7 +766,7 @@ function rollbackSession(session_id) {
740
766
  }
741
767
  }
742
768
  // Mark session as rolled back
743
- db.prepare(`
769
+ getDb().prepare(`
744
770
  UPDATE sessions
745
771
  SET rolled_back_at = datetime('now'), status = 'rolled_back'
746
772
  WHERE session_id = ?
@@ -764,7 +790,7 @@ function rollbackSession(session_id) {
764
790
  }
765
791
  }
766
792
  function markSessionRolledBack(session_id) {
767
- db.prepare(`
793
+ getDb().prepare(`
768
794
  UPDATE sessions
769
795
  SET rolled_back_at = datetime('now'), status = 'rolled_back'
770
796
  WHERE session_id = ?
@@ -772,45 +798,45 @@ function markSessionRolledBack(session_id) {
772
798
  }
773
799
  // Phase 2: Team member functions
774
800
  function addTeamMember(member_id, name, email, added_by, slack_id) {
775
- db.prepare(`
801
+ getDb().prepare(`
776
802
  INSERT OR REPLACE INTO team_members (member_id, name, email, slack_id, added_by, status)
777
803
  VALUES (?, ?, ?, ?, ?, 'active')
778
804
  `).run(member_id, name, email, slack_id ?? null, added_by);
779
805
  }
780
806
  function getTeamMember(member_id) {
781
- return db.prepare(`
807
+ return getDb().prepare(`
782
808
  SELECT * FROM team_members WHERE member_id = ?
783
809
  `).get(member_id);
784
810
  }
785
811
  function getTeamMemberByEmail(email) {
786
- return db.prepare(`
812
+ return getDb().prepare(`
787
813
  SELECT * FROM team_members WHERE email = ?
788
814
  `).get(email);
789
815
  }
790
816
  function getAllTeamMembers() {
791
- return db.prepare(`
817
+ return getDb().prepare(`
792
818
  SELECT * FROM team_members WHERE status = 'active' ORDER BY name ASC
793
819
  `).all();
794
820
  }
795
821
  function removeTeamMember(member_id) {
796
- db.prepare(`
822
+ getDb().prepare(`
797
823
  UPDATE team_members SET status = 'inactive' WHERE member_id = ?
798
824
  `).run(member_id);
799
825
  }
800
826
  // Phase 2: Approval route functions
801
827
  function addApprovalRoute(route_id, name, approver_ids, created_by, description, condition_type, condition_json) {
802
- db.prepare(`
828
+ getDb().prepare(`
803
829
  INSERT OR REPLACE INTO approval_routes (route_id, name, description, condition_type, condition_json, approver_ids, created_by, status)
804
830
  VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
805
831
  `).run(route_id, name, description ?? null, condition_type ?? 'all_sessions', condition_json ?? null, JSON.stringify(approver_ids), created_by);
806
832
  }
807
833
  function getApprovalRoute(route_id) {
808
- return db.prepare(`
834
+ return getDb().prepare(`
809
835
  SELECT * FROM approval_routes WHERE route_id = ?
810
836
  `).get(route_id);
811
837
  }
812
838
  function getAllApprovalRoutes() {
813
- return db.prepare(`
839
+ return getDb().prepare(`
814
840
  SELECT * FROM approval_routes WHERE status = 'active' ORDER BY name ASC
815
841
  `).all();
816
842
  }
@@ -818,49 +844,49 @@ function updateApprovalRoute(route_id, updates) {
818
844
  const current = getApprovalRoute(route_id);
819
845
  if (!current)
820
846
  throw new Error(`Route ${route_id} not found`);
821
- db.prepare(`
847
+ getDb().prepare(`
822
848
  UPDATE approval_routes
823
849
  SET name = ?, description = ?, approver_ids = ?
824
850
  WHERE route_id = ?
825
851
  `).run(updates.name ?? current.name, updates.description ?? current.description, updates.approver_ids ? JSON.stringify(updates.approver_ids) : current.approver_ids, route_id);
826
852
  }
827
853
  function deleteApprovalRoute(route_id) {
828
- db.prepare(`
854
+ getDb().prepare(`
829
855
  UPDATE approval_routes SET status = 'inactive' WHERE route_id = ?
830
856
  `).run(route_id);
831
857
  }
832
858
  // Phase 2: Approval request functions
833
859
  function createApprovalRequest(request_id, session_id, route_id, triggered_by, approver_ids) {
834
- db.prepare(`
860
+ getDb().prepare(`
835
861
  INSERT INTO approval_requests (request_id, session_id, route_id, triggered_by, approver_ids, status)
836
862
  VALUES (?, ?, ?, ?, ?, 'pending')
837
863
  `).run(request_id, session_id, route_id, triggered_by, JSON.stringify(approver_ids));
838
864
  }
839
865
  function getApprovalRequest(request_id) {
840
- return db.prepare(`
866
+ return getDb().prepare(`
841
867
  SELECT * FROM approval_requests WHERE request_id = ?
842
868
  `).get(request_id);
843
869
  }
844
870
  function getSessionApprovalRequests(session_id) {
845
- return db.prepare(`
871
+ return getDb().prepare(`
846
872
  SELECT * FROM approval_requests WHERE session_id = ? ORDER BY triggered_at DESC
847
873
  `).all(session_id);
848
874
  }
849
875
  function getPendingApprovals(approver_id) {
850
876
  if (approver_id) {
851
- return db.prepare(`
877
+ return getDb().prepare(`
852
878
  SELECT * FROM approval_requests
853
879
  WHERE status = 'pending' AND approver_ids LIKE ?
854
880
  ORDER BY triggered_at DESC
855
881
  `).all(`%"${approver_id}"%`);
856
882
  }
857
- return db.prepare(`
883
+ return getDb().prepare(`
858
884
  SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY triggered_at DESC
859
885
  `).all();
860
886
  }
861
887
  function submitApprovalDecision(decision_id, request_id, approver_id, decision, reason) {
862
888
  // Insert decision
863
- db.prepare(`
889
+ getDb().prepare(`
864
890
  INSERT INTO approval_decisions (decision_id, request_id, approver_id, decision, reason)
865
891
  VALUES (?, ?, ?, ?, ?)
866
892
  `).run(decision_id, request_id, approver_id, decision, reason ?? null);
@@ -868,7 +894,7 @@ function submitApprovalDecision(decision_id, request_id, approver_id, decision,
868
894
  const request = getApprovalRequest(request_id);
869
895
  if (!request)
870
896
  throw new Error(`Request ${request_id} not found`);
871
- const decisions = db.prepare(`
897
+ const decisions = getDb().prepare(`
872
898
  SELECT decision FROM approval_decisions WHERE request_id = ?
873
899
  `).all(request_id);
874
900
  const approvedCount = decisions.filter(d => d.decision === 'approve').length;
@@ -882,31 +908,31 @@ function submitApprovalDecision(decision_id, request_id, approver_id, decision,
882
908
  else if (approvedCount >= approverIds.length) {
883
909
  newStatus = 'approved';
884
910
  }
885
- db.prepare(`
911
+ getDb().prepare(`
886
912
  UPDATE approval_requests
887
913
  SET approved_count = ?, rejected_count = ?, status = ?, completed_at = ?
888
914
  WHERE request_id = ?
889
915
  `).run(approvedCount, rejectedCount, newStatus, newStatus !== 'pending' ? new Date().toISOString() : null, request_id);
890
916
  }
891
917
  function getApprovalDecisions(request_id) {
892
- return db.prepare(`
918
+ return getDb().prepare(`
893
919
  SELECT * FROM approval_decisions WHERE request_id = ? ORDER BY decided_at ASC
894
920
  `).all(request_id);
895
921
  }
896
922
  // Phase 3: Escalation rule functions
897
923
  function addEscalationRule(rule_id, name, escalation_targets, created_by, description, timeout_hours) {
898
- db.prepare(`
924
+ getDb().prepare(`
899
925
  INSERT OR REPLACE INTO escalation_rules (rule_id, name, description, timeout_hours, escalation_targets, created_by, status)
900
926
  VALUES (?, ?, ?, ?, ?, ?, 'active')
901
927
  `).run(rule_id, name, description ?? null, timeout_hours ?? 24, JSON.stringify(escalation_targets), created_by);
902
928
  }
903
929
  function getEscalationRule(rule_id) {
904
- return db.prepare(`
930
+ return getDb().prepare(`
905
931
  SELECT * FROM escalation_rules WHERE rule_id = ?
906
932
  `).get(rule_id);
907
933
  }
908
934
  function getAllEscalationRules() {
909
- return db.prepare(`
935
+ return getDb().prepare(`
910
936
  SELECT * FROM escalation_rules WHERE status = 'active' ORDER BY name ASC
911
937
  `).all();
912
938
  }
@@ -914,36 +940,36 @@ function updateEscalationRule(rule_id, updates) {
914
940
  const current = getEscalationRule(rule_id);
915
941
  if (!current)
916
942
  throw new Error(`Rule ${rule_id} not found`);
917
- db.prepare(`
943
+ getDb().prepare(`
918
944
  UPDATE escalation_rules
919
945
  SET name = ?, description = ?, timeout_hours = ?, escalation_targets = ?
920
946
  WHERE rule_id = ?
921
947
  `).run(updates.name ?? current.name, updates.description ?? current.description, updates.timeout_hours ?? current.timeout_hours, updates.escalation_targets ? JSON.stringify(updates.escalation_targets) : current.escalation_targets, rule_id);
922
948
  }
923
949
  function deleteEscalationRule(rule_id) {
924
- db.prepare(`
950
+ getDb().prepare(`
925
951
  UPDATE escalation_rules SET status = 'inactive' WHERE rule_id = ?
926
952
  `).run(rule_id);
927
953
  }
928
954
  // Phase 3: Escalation request functions
929
955
  function createEscalationRequest(request_id, approval_request_id, session_id, escalation_targets, deadline) {
930
- db.prepare(`
956
+ getDb().prepare(`
931
957
  INSERT INTO escalation_requests (request_id, approval_request_id, session_id, escalation_triggered_at, escalation_deadline, escalation_targets, status)
932
958
  VALUES (?, ?, ?, datetime('now'), ?, ?, 'pending')
933
959
  `).run(request_id, approval_request_id, session_id, deadline, JSON.stringify(escalation_targets));
934
960
  }
935
961
  function getEscalationRequest(request_id) {
936
- return db.prepare(`
962
+ return getDb().prepare(`
937
963
  SELECT * FROM escalation_requests WHERE request_id = ?
938
964
  `).get(request_id);
939
965
  }
940
966
  function getPendingEscalations() {
941
- return db.prepare(`
967
+ return getDb().prepare(`
942
968
  SELECT * FROM escalation_requests WHERE status = 'pending' ORDER BY escalation_deadline ASC
943
969
  `).all();
944
970
  }
945
971
  function getStaleApprovals(now) {
946
- return db.prepare(`
972
+ return getDb().prepare(`
947
973
  SELECT DISTINCT ar.request_id as approval_request_id, ar.session_id
948
974
  FROM approval_requests ar
949
975
  WHERE ar.status = 'pending'
@@ -955,7 +981,7 @@ function getStaleApprovals(now) {
955
981
  `).all(now);
956
982
  }
957
983
  function submitEscalationDecision(decision_id, escalation_request_id, target_id, decision, reason) {
958
- db.prepare(`
984
+ getDb().prepare(`
959
985
  INSERT INTO escalation_decisions (decision_id, escalation_request_id, target_id, decision, reason)
960
986
  VALUES (?, ?, ?, ?, ?)
961
987
  `).run(decision_id, escalation_request_id, target_id, decision, reason ?? null);
@@ -963,24 +989,24 @@ function submitEscalationDecision(decision_id, escalation_request_id, target_id,
963
989
  const request = getEscalationRequest(escalation_request_id);
964
990
  if (!request)
965
991
  return;
966
- const decisions = db.prepare(`
992
+ const decisions = getDb().prepare(`
967
993
  SELECT DISTINCT decision FROM escalation_decisions WHERE escalation_request_id = ?
968
994
  `).all(escalation_request_id);
969
995
  const hasBlockDecision = decisions.some(d => d.decision === 'block');
970
996
  const newStatus = hasBlockDecision ? 'blocked' : 'proceeded';
971
- db.prepare(`
997
+ getDb().prepare(`
972
998
  UPDATE escalation_requests
973
999
  SET status = ?, decided_at = datetime('now'), decision = ?
974
1000
  WHERE request_id = ?
975
1001
  `).run(newStatus, newStatus, escalation_request_id);
976
1002
  }
977
1003
  function getEscalationDecisions(escalation_request_id) {
978
- return db.prepare(`
1004
+ return getDb().prepare(`
979
1005
  SELECT * FROM escalation_decisions WHERE escalation_request_id = ? ORDER BY decided_at ASC
980
1006
  `).all(escalation_request_id);
981
1007
  }
982
1008
  function getEscalationHistory(session_id) {
983
- return db.prepare(`
1009
+ return getDb().prepare(`
984
1010
  SELECT * FROM escalation_requests WHERE session_id = ? ORDER BY escalation_triggered_at DESC
985
1011
  `).all(session_id);
986
1012
  }