aegis-bridge 2.9.3 → 2.10.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.
package/dist/config.d.ts CHANGED
@@ -68,6 +68,15 @@ export interface Config {
68
68
  /** Issue #884: Additional Claude projects directories to search during worktree fanout.
69
69
  * Paths are expanded (~) and checked for existence before searching. */
70
70
  worktreeSiblingDirs: string[];
71
+ /** Issue #740: Verification Protocol — auto run quality gate after session ends.
72
+ * When enabled, Aegis runs tsc + build + test after a Stop hook and emits
73
+ * results via SSE (event: 'verification'). */
74
+ verificationProtocol: {
75
+ /** Auto-run verification when Stop hook fires (default: false). */
76
+ autoVerifyOnStop: boolean;
77
+ /** Run only critical checks: tsc + build (skip slow tests). Default: false = full. */
78
+ criticalOnly: boolean;
79
+ };
71
80
  }
72
81
  /** Compute stall threshold from env var or default (Issue #392).
73
82
  * If CLAUDE_STREAM_IDLE_TIMEOUT_MS is set, uses Math.max(120000, parseInt(val) * 1.5).
package/dist/config.js CHANGED
@@ -49,6 +49,7 @@ const defaults = {
49
49
  worktreeAwareContinuation: false,
50
50
  memoryBridge: { enabled: false },
51
51
  worktreeSiblingDirs: [],
52
+ verificationProtocol: { autoVerifyOnStop: false, criticalOnly: false },
52
53
  };
53
54
  /** Parse CLI args for --config flag */
54
55
  function getConfigPathFromArgv() {
package/dist/events.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * The monitor pushes events; the SSE route consumes them.
7
7
  */
8
8
  export interface SessionSSEEvent {
9
- event: 'status' | 'message' | 'system' | 'approval' | 'ended' | 'heartbeat' | 'stall' | 'dead' | 'hook' | 'subagent_start' | 'subagent_stop';
9
+ event: 'status' | 'message' | 'system' | 'approval' | 'ended' | 'heartbeat' | 'stall' | 'dead' | 'hook' | 'subagent_start' | 'subagent_stop' | 'verification';
10
10
  sessionId: string;
11
11
  timestamp: string;
12
12
  data: Record<string, unknown>;
@@ -15,8 +15,20 @@ export interface SessionSSEEvent {
15
15
  /** Issue #308: Incrementing event ID for Last-Event-ID replay. */
16
16
  id?: number;
17
17
  }
18
+ export interface VerificationResult {
19
+ ok: boolean;
20
+ steps: {
21
+ name: 'tsc' | 'build' | 'test';
22
+ ok: boolean;
23
+ durationMs: number;
24
+ output?: string;
25
+ error?: string;
26
+ }[];
27
+ totalDurationMs: number;
28
+ summary: string;
29
+ }
18
30
  export interface GlobalSSEEvent {
19
- event: 'session_status_change' | 'session_message' | 'session_approval' | 'session_ended' | 'session_created' | 'session_stall' | 'session_dead' | 'session_subagent_start' | 'session_subagent_stop';
31
+ event: 'session_status_change' | 'session_message' | 'session_approval' | 'session_ended' | 'session_created' | 'session_stall' | 'session_dead' | 'session_subagent_start' | 'session_subagent_stop' | 'session_verification';
20
32
  sessionId: string;
21
33
  timestamp: string;
22
34
  data: Record<string, unknown>;
@@ -65,6 +77,8 @@ export declare class SessionEventBus {
65
77
  };
66
78
  /** Emit a status change event. */
67
79
  emitStatus(sessionId: string, status: string, detail: string): void;
80
+ /** Issue #740: Emit a verification result event. */
81
+ emitVerification(sessionId: string, result: VerificationResult): void;
68
82
  /** Emit a message event. */
69
83
  emitMessage(sessionId: string, role: string, text: string, contentType?: string, toolMeta?: {
70
84
  tool_name?: string;
package/dist/events.js CHANGED
@@ -16,6 +16,7 @@ function toGlobalEvent(event) {
16
16
  approval: 'session_approval',
17
17
  ended: 'session_ended',
18
18
  heartbeat: 'session_status_change',
19
+ verification: 'session_verification',
19
20
  stall: 'session_stall',
20
21
  dead: 'session_dead',
21
22
  subagent_start: 'session_subagent_start',
@@ -167,6 +168,15 @@ export class SessionEventBus {
167
168
  data: { status, detail },
168
169
  });
169
170
  }
171
+ /** Issue #740: Emit a verification result event. */
172
+ emitVerification(sessionId, result) {
173
+ this.emit(sessionId, {
174
+ event: 'verification',
175
+ sessionId,
176
+ timestamp: new Date().toISOString(),
177
+ data: result,
178
+ });
179
+ }
170
180
  /** Emit a message event. */
171
181
  emitMessage(sessionId, role, text, contentType, toolMeta) {
172
182
  this.emit(sessionId, {
@@ -23,7 +23,8 @@ function isRecord(value) {
23
23
  return typeof value === 'object' && value !== null && !Array.isArray(value);
24
24
  }
25
25
  function parseSettingsWithFallback(raw) {
26
- const json = JSON.parse(raw);
26
+ // Windows editors may write UTF-8 BOM; strip it so JSON.parse does not fail.
27
+ const json = JSON.parse(raw.replace(/^\uFEFF/, ''));
27
28
  if (!isRecord(json))
28
29
  return undefined;
29
30
  const parsed = ccSettingsSchema.safeParse(json);
@@ -0,0 +1,12 @@
1
+ export type PermissionDecision = 'allow' | 'deny';
2
+ export declare class PermissionRequestManager {
3
+ private pendingPermissions;
4
+ waitForPermissionDecision(sessionId: string, timeoutMs?: number, toolName?: string, prompt?: string): Promise<PermissionDecision>;
5
+ hasPendingPermission(sessionId: string): boolean;
6
+ getPendingPermissionInfo(sessionId: string): {
7
+ toolName?: string;
8
+ prompt?: string;
9
+ } | null;
10
+ resolvePendingPermission(sessionId: string, decision: PermissionDecision): boolean;
11
+ cleanupPendingPermission(sessionId: string): void;
12
+ }
@@ -0,0 +1,36 @@
1
+ export class PermissionRequestManager {
2
+ pendingPermissions = new Map();
3
+ waitForPermissionDecision(sessionId, timeoutMs = 10_000, toolName, prompt) {
4
+ return new Promise((resolve) => {
5
+ const timer = setTimeout(() => {
6
+ this.pendingPermissions.delete(sessionId);
7
+ console.log(`Hooks: PermissionRequest timeout for session ${sessionId} - auto-rejecting`);
8
+ resolve('deny');
9
+ }, timeoutMs);
10
+ this.pendingPermissions.set(sessionId, { resolve, timer, toolName, prompt });
11
+ });
12
+ }
13
+ hasPendingPermission(sessionId) {
14
+ return this.pendingPermissions.has(sessionId);
15
+ }
16
+ getPendingPermissionInfo(sessionId) {
17
+ const pending = this.pendingPermissions.get(sessionId);
18
+ return pending ? { toolName: pending.toolName, prompt: pending.prompt } : null;
19
+ }
20
+ resolvePendingPermission(sessionId, decision) {
21
+ const pending = this.pendingPermissions.get(sessionId);
22
+ if (!pending)
23
+ return false;
24
+ clearTimeout(pending.timer);
25
+ this.pendingPermissions.delete(sessionId);
26
+ pending.resolve(decision);
27
+ return true;
28
+ }
29
+ cleanupPendingPermission(sessionId) {
30
+ const pending = this.pendingPermissions.get(sessionId);
31
+ if (pending) {
32
+ clearTimeout(pending.timer);
33
+ this.pendingPermissions.delete(sessionId);
34
+ }
35
+ }
36
+ }
package/dist/server.js CHANGED
@@ -26,6 +26,7 @@ import { captureScreenshot, isPlaywrightAvailable } from './screenshot.js';
26
26
  import { validateScreenshotUrl, resolveAndCheckIp, buildHostResolverRule } from './ssrf.js';
27
27
  import { validateWorkDir, permissionRuleSchema } from './validation.js';
28
28
  import { SessionEventBus } from './events.js';
29
+ import { runVerification } from './verification.js';
29
30
  import { SSEWriter } from './sse-writer.js';
30
31
  import { SSEConnectionLimiter } from './sse-limiter.js';
31
32
  import { PipelineManager } from './pipeline.js';
@@ -431,7 +432,8 @@ app.get('/v1/sessions/:id/metrics', async (req, reply) => {
431
432
  });
432
433
  // Issue #704: Tool usage endpoints
433
434
  app.get('/v1/sessions/:id/tools', async (req, reply) => {
434
- const session = sessions.getSession(req.params.id);
435
+ const sessionId = req.params.id;
436
+ const session = sessions.getSession(sessionId);
435
437
  if (!session)
436
438
  return reply.status(404).send({ error: 'Session not found' });
437
439
  // Parse JSONL on-demand for tool usage
@@ -472,7 +474,8 @@ app.get('/v1/channels/health', async () => {
472
474
  });
473
475
  // Issue #87: Per-session latency metrics
474
476
  app.get('/v1/sessions/:id/latency', async (req, reply) => {
475
- const session = sessions.getSession(req.params.id);
477
+ const sessionId = req.params.id;
478
+ const session = sessions.getSession(sessionId);
476
479
  if (!session)
477
480
  return reply.status(404).send({ error: 'Session not found' });
478
481
  const realtimeLatency = sessions.getLatencyMetrics(req.params.id);
@@ -678,7 +681,8 @@ app.post('/v1/sessions', createSessionHandler);
678
681
  app.post('/sessions', createSessionHandler);
679
682
  // Get session (Issue #20: includes actionHints for interactive states)
680
683
  async function getSessionHandler(req, reply) {
681
- const session = sessions.getSession(req.params.id);
684
+ const sessionId = req.params.id;
685
+ const session = sessions.getSession(sessionId);
682
686
  if (!session)
683
687
  return reply.status(404).send({ error: 'Session not found' });
684
688
  return addActionHints(session, sessions);
@@ -739,7 +743,8 @@ app.post('/v1/sessions/:id/send', sendMessageHandler);
739
743
  app.post('/sessions/:id/send', sendMessageHandler);
740
744
  // Issue #702: GET children sessions
741
745
  async function getChildrenHandler(req, reply) {
742
- const session = sessions.getSession(req.params.id);
746
+ const sessionId = req.params.id;
747
+ const session = sessions.getSession(sessionId);
743
748
  if (!session)
744
749
  return reply.status(404).send({ error: 'Session not found' });
745
750
  const children = (session.children ?? []).map(id => {
@@ -771,13 +776,15 @@ async function spawnChildHandler(req, reply) {
771
776
  app.post('/v1/sessions/:id/spawn', spawnChildHandler);
772
777
  app.post('/sessions/:id/spawn', spawnChildHandler);
773
778
  async function getPermissionPolicyHandler(req, reply) {
774
- const session = sessions.getSession(req.params.id);
779
+ const sessionId = req.params.id;
780
+ const session = sessions.getSession(sessionId);
775
781
  if (!session)
776
782
  return reply.status(404).send({ error: 'Session not found' });
777
783
  return { permissionPolicy: session.permissionPolicy ?? [] };
778
784
  }
779
785
  async function updatePermissionPolicyHandler(req, reply) {
780
- const session = sessions.getSession(req.params.id);
786
+ const sessionId = req.params.id;
787
+ const session = sessions.getSession(sessionId);
781
788
  if (!session)
782
789
  return reply.status(404).send({ error: 'Session not found' });
783
790
  const policy = req.body ?? [];
@@ -816,7 +823,8 @@ app.post('/v1/sessions/:id/answer', async (req, reply) => {
816
823
  if (!questionId || answer === undefined || answer === null) {
817
824
  return reply.status(400).send({ error: 'questionId and answer are required' });
818
825
  }
819
- const session = sessions.getSession(req.params.id);
826
+ const sessionId = req.params.id;
827
+ const session = sessions.getSession(sessionId);
820
828
  if (!session)
821
829
  return reply.status(404).send({ error: 'Session not found' });
822
830
  const resolved = sessions.submitAnswer(req.params.id, questionId, answer);
@@ -872,7 +880,8 @@ app.delete('/v1/sessions/:id', killSessionHandler);
872
880
  app.delete('/sessions/:id', killSessionHandler);
873
881
  // Capture raw pane
874
882
  async function capturePaneHandler(req, reply) {
875
- const session = sessions.getSession(req.params.id);
883
+ const sessionId = req.params.id;
884
+ const session = sessions.getSession(sessionId);
876
885
  if (!session)
877
886
  return reply.status(404).send({ error: 'Session not found' });
878
887
  const pane = await tmux.capturePane(session.windowId);
@@ -979,7 +988,8 @@ async function screenshotHandler(req, reply) {
979
988
  if (dnsResult.error)
980
989
  return reply.status(400).send({ error: dnsResult.error });
981
990
  // Validate session exists
982
- const session = sessions.getSession(req.params.id);
991
+ const sessionId = req.params.id;
992
+ const session = sessions.getSession(sessionId);
983
993
  if (!session)
984
994
  return reply.status(404).send({ error: 'Session not found' });
985
995
  if (!isPlaywrightAvailable()) {
@@ -1004,7 +1014,8 @@ app.post('/v1/sessions/:id/screenshot', screenshotHandler);
1004
1014
  app.post('/sessions/:id/screenshot', screenshotHandler);
1005
1015
  // SSE event stream (Issue #32)
1006
1016
  app.get('/v1/sessions/:id/events', async (req, reply) => {
1007
- const session = sessions.getSession(req.params.id);
1017
+ const sessionId = req.params.id;
1018
+ const session = sessions.getSession(sessionId);
1008
1019
  if (!session)
1009
1020
  return reply.status(404).send({ error: 'Session not found' });
1010
1021
  const clientIp = req.ip;
@@ -1076,7 +1087,8 @@ app.get('/v1/sessions/:id/events', async (req, reply) => {
1076
1087
  // ── Claude Code Hook Endpoints (Issue #161) ─────────────────────────
1077
1088
  // POST /v1/sessions/:id/hooks/permission — PermissionRequest hook from CC
1078
1089
  app.post('/v1/sessions/:id/hooks/permission', async (req, reply) => {
1079
- const session = sessions.getSession(req.params.id);
1090
+ const sessionId = req.params.id;
1091
+ const session = sessions.getSession(sessionId);
1080
1092
  if (!session)
1081
1093
  return reply.status(404).send({ error: 'Session not found' });
1082
1094
  const parsed = permissionHookSchema.safeParse(req.body);
@@ -1103,7 +1115,8 @@ app.post('/v1/sessions/:id/hooks/permission', async (req, reply) => {
1103
1115
  });
1104
1116
  // POST /v1/sessions/:id/hooks/stop — Stop hook from CC
1105
1117
  app.post('/v1/sessions/:id/hooks/stop', async (req, reply) => {
1106
- const session = sessions.getSession(req.params.id);
1118
+ const sessionId = req.params.id;
1119
+ const session = sessions.getSession(sessionId);
1107
1120
  if (!session)
1108
1121
  return reply.status(404).send({ error: 'Session not found' });
1109
1122
  const parsed = stopHookSchema.safeParse(req.body);
@@ -1155,6 +1168,28 @@ app.post('/v1/sessions/batch', async (req, reply) => {
1155
1168
  const result = await pipelines.batchCreate(specs);
1156
1169
  return reply.status(201).send(result);
1157
1170
  });
1171
+ // Issue #740: Verification Protocol — run quality gate (tsc + build + test) on a session's workDir
1172
+ app.post('/v1/sessions/:id/verify', async (req, reply) => {
1173
+ const sessionId = req.params.id;
1174
+ const session = sessions.getSession(sessionId);
1175
+ if (!session)
1176
+ return reply.status(404).send({ error: 'Session not found' });
1177
+ const { workDir } = session;
1178
+ if (!workDir)
1179
+ return reply.status(400).send({ error: 'Session has no workDir' });
1180
+ const criticalOnly = config.verificationProtocol?.criticalOnly ?? false;
1181
+ eventBus.emitStatus(sessionId, 'working', `Running verification (criticalOnly=${criticalOnly})…`);
1182
+ try {
1183
+ const result = await runVerification(workDir, criticalOnly);
1184
+ eventBus.emitVerification(sessionId, result);
1185
+ const httpStatus = result.ok ? 200 : 422;
1186
+ return reply.status(httpStatus).send(result);
1187
+ }
1188
+ catch (e) {
1189
+ const msg = e instanceof Error ? e.message : String(e);
1190
+ return reply.status(500).send({ ok: false, summary: `Verification error: ${msg}` });
1191
+ }
1192
+ });
1158
1193
  // Pipeline create (Issue #36)
1159
1194
  app.post('/v1/pipelines', async (req, reply) => {
1160
1195
  const parsed = pipelineSchema.safeParse(req.body);
package/dist/session.d.ts CHANGED
@@ -9,6 +9,7 @@ import { type ParsedEntry } from './transcript.js';
9
9
  import { type UIState } from './terminal-parser.js';
10
10
  import type { Config } from './config.js';
11
11
  import { type PermissionPolicy } from './validation.js';
12
+ import { type PermissionDecision } from './permission-request-manager.js';
12
13
  export interface SessionInfo {
13
14
  id: string;
14
15
  windowId: string;
@@ -53,7 +54,7 @@ export interface SessionState {
53
54
  */
54
55
  export declare function detectApprovalMethod(paneText: string): 'numbered' | 'yes';
55
56
  /** Resolves a pending PermissionRequest hook with a decision. */
56
- export type PermissionDecision = 'allow' | 'deny';
57
+ export type { PermissionDecision };
57
58
  export declare class SessionManager {
58
59
  private tmux;
59
60
  private config;
@@ -66,7 +67,7 @@ export declare class SessionManager {
66
67
  private saveQueue;
67
68
  private saveDebounceTimer;
68
69
  private static readonly SAVE_DEBOUNCE_MS;
69
- private pendingPermissions;
70
+ private permissionRequests;
70
71
  private pendingQuestions;
71
72
  private static readonly MAX_CACHE_ENTRIES_PER_SESSION;
72
73
  private parsedEntriesCache;
@@ -247,10 +248,6 @@ export declare class SessionManager {
247
248
  toolName?: string;
248
249
  prompt?: string;
249
250
  } | null;
250
- /**
251
- * Resolve a pending permission. Returns true if there was a pending permission to resolve.
252
- */
253
- private resolvePendingPermission;
254
251
  /** Clean up any pending permission for a session (e.g. on session delete). */
255
252
  cleanupPendingPermission(sessionId: string): void;
256
253
  /**
package/dist/session.js CHANGED
@@ -17,6 +17,7 @@ import { neutralizeBypassPermissions, restoreSettings, cleanOrphanedBackup } fro
17
17
  import { persistedStateSchema } from './validation.js';
18
18
  import { loadContinuationPointers } from './continuation-pointer.js';
19
19
  import { writeHookSettingsFile, cleanupHookSettingsFile, cleanupStaleSessionHooks } from './hook-settings.js';
20
+ import { PermissionRequestManager } from './permission-request-manager.js';
20
21
  import { Mutex } from 'async-mutex';
21
22
  import { maybeInjectFault } from './fault-injection.js';
22
23
  /** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
@@ -61,7 +62,7 @@ export class SessionManager {
61
62
  saveQueue = Promise.resolve(); // #218: serialize concurrent saves
62
63
  saveDebounceTimer = null;
63
64
  static SAVE_DEBOUNCE_MS = 5_000; // #357: debounce offset-only saves
64
- pendingPermissions = new Map();
65
+ permissionRequests = new PermissionRequestManager();
65
66
  pendingQuestions = new Map();
66
67
  // #357: Cache of all parsed JSONL entries per session to avoid re-reading from offset 0
67
68
  // #424: Evict oldest entries when cache exceeds max to prevent unbounded growth
@@ -961,7 +962,7 @@ export class SessionManager {
961
962
  if (!session)
962
963
  throw new Error(`Session ${id} not found`);
963
964
  // Issue #284: Resolve pending hook-based permission first
964
- if (this.resolvePendingPermission(id, 'allow')) {
965
+ if (this.permissionRequests.resolvePendingPermission(id, 'allow')) {
965
966
  session.lastActivity = Date.now();
966
967
  if (session.permissionPromptAt) {
967
968
  session.permissionRespondedAt = Date.now();
@@ -983,7 +984,7 @@ export class SessionManager {
983
984
  if (!session)
984
985
  throw new Error(`Session ${id} not found`);
985
986
  // Issue #284: Resolve pending hook-based permission first
986
- if (this.resolvePendingPermission(id, 'deny')) {
987
+ if (this.permissionRequests.resolvePendingPermission(id, 'deny')) {
987
988
  session.lastActivity = Date.now();
988
989
  if (session.permissionPromptAt) {
989
990
  session.permissionRespondedAt = Date.now();
@@ -1008,43 +1009,19 @@ export class SessionManager {
1008
1009
  * @returns Promise that resolves with the client's decision
1009
1010
  */
1010
1011
  waitForPermissionDecision(sessionId, timeoutMs = 10_000, toolName, prompt) {
1011
- return new Promise((resolve) => {
1012
- const timer = setTimeout(() => {
1013
- this.pendingPermissions.delete(sessionId);
1014
- console.log(`Hooks: PermissionRequest timeout for session ${sessionId} — auto-rejecting`);
1015
- resolve('deny');
1016
- }, timeoutMs);
1017
- this.pendingPermissions.set(sessionId, { resolve, timer, toolName, prompt });
1018
- });
1012
+ return this.permissionRequests.waitForPermissionDecision(sessionId, timeoutMs, toolName, prompt);
1019
1013
  }
1020
1014
  /** Check if a session has a pending permission request. */
1021
1015
  hasPendingPermission(sessionId) {
1022
- return this.pendingPermissions.has(sessionId);
1016
+ return this.permissionRequests.hasPendingPermission(sessionId);
1023
1017
  }
1024
1018
  /** Get info about a pending permission (for API responses). */
1025
1019
  getPendingPermissionInfo(sessionId) {
1026
- const pending = this.pendingPermissions.get(sessionId);
1027
- return pending ? { toolName: pending.toolName, prompt: pending.prompt } : null;
1028
- }
1029
- /**
1030
- * Resolve a pending permission. Returns true if there was a pending permission to resolve.
1031
- */
1032
- resolvePendingPermission(sessionId, decision) {
1033
- const pending = this.pendingPermissions.get(sessionId);
1034
- if (!pending)
1035
- return false;
1036
- clearTimeout(pending.timer);
1037
- this.pendingPermissions.delete(sessionId);
1038
- pending.resolve(decision);
1039
- return true;
1020
+ return this.permissionRequests.getPendingPermissionInfo(sessionId);
1040
1021
  }
1041
1022
  /** Clean up any pending permission for a session (e.g. on session delete). */
1042
1023
  cleanupPendingPermission(sessionId) {
1043
- const pending = this.pendingPermissions.get(sessionId);
1044
- if (pending) {
1045
- clearTimeout(pending.timer);
1046
- this.pendingPermissions.delete(sessionId);
1047
- }
1024
+ this.permissionRequests.cleanupPendingPermission(sessionId);
1048
1025
  }
1049
1026
  /**
1050
1027
  * Issue #336: Store a pending AskUserQuestion and return a promise that
@@ -0,0 +1,2 @@
1
+ import type { VerificationResult } from './events.js';
2
+ export declare function runVerification(workDir: string, criticalOnly?: boolean): Promise<VerificationResult>;
@@ -0,0 +1,72 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { join } from 'path';
4
+ import { statSync } from 'fs';
5
+ const execAsync = promisify(exec);
6
+ async function runCmd(cmd, { cwd, timeoutMs }) {
7
+ try {
8
+ const { stdout, stderr } = await execAsync(cmd, { cwd, timeout: Math.floor(timeoutMs / 1000), killSignal: 'SIGKILL' });
9
+ return { stdout, stderr, exitCode: 0 };
10
+ }
11
+ catch (e) {
12
+ const err = e;
13
+ return { stdout: err.stdout ?? '', stderr: err.stderr ?? '', exitCode: err.code ?? 1 };
14
+ }
15
+ }
16
+ export async function runVerification(workDir, criticalOnly = false) {
17
+ const start = Date.now();
18
+ const hasPackageJson = statSync(join(workDir, 'package.json')).isFile();
19
+ if (!hasPackageJson) {
20
+ return {
21
+ ok: false,
22
+ steps: [],
23
+ totalDurationMs: Date.now() - start,
24
+ summary: 'No package.json found — cannot verify',
25
+ };
26
+ }
27
+ const steps = [];
28
+ const timeoutMs = 120_000;
29
+ // Step 1: tsc
30
+ const tscStart = Date.now();
31
+ const tscResult = await runCmd('npx tsc --noEmit', { cwd: workDir, timeoutMs });
32
+ const tscDuration = Date.now() - tscStart;
33
+ const tscOk = tscResult.exitCode === 0;
34
+ steps.push({
35
+ name: 'tsc',
36
+ ok: tscOk,
37
+ durationMs: tscDuration,
38
+ output: tscResult.stdout.slice(0, 2000),
39
+ error: tscOk ? undefined : (tscResult.stderr || tscResult.stdout).slice(0, 2000),
40
+ });
41
+ // Step 2: build
42
+ const buildStart = Date.now();
43
+ const buildResult = await runCmd('npm run build', { cwd: workDir, timeoutMs });
44
+ const buildDuration = Date.now() - buildStart;
45
+ const buildOk = buildResult.exitCode === 0;
46
+ steps.push({
47
+ name: 'build',
48
+ ok: buildOk,
49
+ durationMs: buildDuration,
50
+ output: buildResult.stdout.slice(0, 2000),
51
+ error: buildOk ? undefined : (buildResult.stderr || buildResult.stdout).slice(0, 2000),
52
+ });
53
+ // Step 3: test (unless criticalOnly)
54
+ let testOk = true;
55
+ if (!criticalOnly) {
56
+ const testStart = Date.now();
57
+ const testResult = await runCmd('npm test', { cwd: workDir, timeoutMs: 180_000 });
58
+ const testDuration = Date.now() - testStart;
59
+ testOk = testResult.exitCode === 0;
60
+ steps.push({ name: 'test', ok: testOk, durationMs: testDuration, output: testResult.stdout.slice(0, 2000), error: testOk ? undefined : (testResult.stderr || testResult.stdout).slice(0, 2000) });
61
+ }
62
+ const ok = tscOk && buildOk && (!criticalOnly || testOk);
63
+ const totalDurationMs = Date.now() - start;
64
+ const summary = ok
65
+ ? `Verification passed: tsc ✅, build ✅${criticalOnly ? '' : ', test ✅'} (${totalDurationMs}ms)`
66
+ : `Verification failed: ${[
67
+ !tscOk ? 'tsc ❌' : '',
68
+ !buildOk ? 'build ❌' : '',
69
+ !criticalOnly && !testOk ? 'test ❌' : '',
70
+ ].filter(Boolean).join(', ')} (${totalDurationMs}ms)`;
71
+ return { ok, steps, totalDurationMs, summary };
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.9.3",
3
+ "version": "2.10.1",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",