aegis-bridge 2.15.1 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -157,6 +157,7 @@ export interface CreateSessionRequest {
157
157
  workDir: string;
158
158
  name?: string;
159
159
  prompt?: string;
160
+ prd?: string;
160
161
  resumeSessionId?: string;
161
162
  claudeCommand?: string;
162
163
  env?: Record<string, string>;
@@ -182,6 +183,7 @@ export interface SessionSummary {
182
183
  createdAt: number;
183
184
  lastActivity: number;
184
185
  permissionMode: string;
186
+ prd?: string;
185
187
  }
186
188
  export interface OkResponse {
187
189
  ok: boolean;
@@ -57,6 +57,16 @@ interface SessionLatencyResponse {
57
57
  realtime: SessionLatency | null;
58
58
  aggregated: SessionLatencySummary | null;
59
59
  }
60
+ interface MemoryEntryResponse {
61
+ entry: {
62
+ key: string;
63
+ value: string;
64
+ namespace: string;
65
+ created_at: number;
66
+ updated_at: number;
67
+ expires_at?: number;
68
+ };
69
+ }
60
70
  export declare class AegisClient {
61
71
  private baseUrl;
62
72
  private authToken?;
@@ -103,6 +113,9 @@ export declare class AegisClient {
103
113
  }>;
104
114
  }): Promise<PipelineState>;
105
115
  getSwarm(): Promise<Record<string, unknown>>;
116
+ setMemory(key: string, value: string, ttlSeconds?: number): Promise<MemoryEntryResponse>;
117
+ getMemory(key: string): Promise<MemoryEntryResponse>;
118
+ deleteMemory(key: string): Promise<OkResponse>;
106
119
  }
107
120
  export declare function createMcpServer(aegisPort: number, authToken?: string): McpServer;
108
121
  export declare function startMcpServer(port: number, authToken?: string): Promise<void>;
@@ -175,6 +175,20 @@ export class AegisClient {
175
175
  async getSwarm() {
176
176
  return this.request('/v1/swarm');
177
177
  }
178
+ async setMemory(key, value, ttlSeconds) {
179
+ return this.request('/v1/memory', {
180
+ method: 'POST',
181
+ body: JSON.stringify({ key, value, ttlSeconds }),
182
+ });
183
+ }
184
+ async getMemory(key) {
185
+ return this.request(`/v1/memory/${encodeURIComponent(key)}`);
186
+ }
187
+ async deleteMemory(key) {
188
+ return this.request(`/v1/memory/${encodeURIComponent(key)}`, {
189
+ method: 'DELETE',
190
+ });
191
+ }
178
192
  }
179
193
  function formatToolError(e) {
180
194
  if (e instanceof Error) {
@@ -665,6 +679,59 @@ export function createMcpServer(aegisPort, authToken) {
665
679
  return formatToolError(e);
666
680
  }
667
681
  });
682
+ // ── state_set ──
683
+ server.tool('state_set', 'Set a shared state key/value entry via Aegis memory bridge.', {
684
+ key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
685
+ value: z.string().describe('State payload as string'),
686
+ ttlSeconds: z.number().int().positive().max(86400 * 30).optional().describe('Optional TTL in seconds (max 30 days)'),
687
+ }, async ({ key, value, ttlSeconds }) => {
688
+ try {
689
+ const result = await client.setMemory(key, value, ttlSeconds);
690
+ return {
691
+ content: [{
692
+ type: 'text',
693
+ text: JSON.stringify(result, null, 2),
694
+ }],
695
+ };
696
+ }
697
+ catch (e) {
698
+ return formatToolError(e);
699
+ }
700
+ });
701
+ // ── state_get ──
702
+ server.tool('state_get', 'Get a shared state key/value entry via Aegis memory bridge.', {
703
+ key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
704
+ }, async ({ key }) => {
705
+ try {
706
+ const result = await client.getMemory(key);
707
+ return {
708
+ content: [{
709
+ type: 'text',
710
+ text: JSON.stringify(result, null, 2),
711
+ }],
712
+ };
713
+ }
714
+ catch (e) {
715
+ return formatToolError(e);
716
+ }
717
+ });
718
+ // ── state_delete ──
719
+ server.tool('state_delete', 'Delete a shared state key/value entry via Aegis memory bridge.', {
720
+ key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
721
+ }, async ({ key }) => {
722
+ try {
723
+ const result = await client.deleteMemory(key);
724
+ return {
725
+ content: [{
726
+ type: 'text',
727
+ text: JSON.stringify(result, null, 2),
728
+ }],
729
+ };
730
+ }
731
+ catch (e) {
732
+ return formatToolError(e);
733
+ }
734
+ });
668
735
  // ── MCP Prompts (Issue #443) ────────────────────────────────────────
669
736
  server.prompt('implement_issue', 'Create a session and generate a structured implementation prompt for a GitHub issue.', {
670
737
  issueNumber: z.string().describe('GitHub issue number'),
@@ -0,0 +1,11 @@
1
+ /**
2
+ * path-utils.ts — path helpers shared across session/tmux logic.
3
+ */
4
+ /**
5
+ * Compute the Claude project hash folder from a workDir path.
6
+ *
7
+ * Examples:
8
+ * - /home/user/project -> -home-user-project
9
+ * - D:\\Users\\me\\project -> -d-Users-me-project
10
+ */
11
+ export declare function computeProjectHash(workDir: string): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * path-utils.ts — path helpers shared across session/tmux logic.
3
+ */
4
+ /**
5
+ * Compute the Claude project hash folder from a workDir path.
6
+ *
7
+ * Examples:
8
+ * - /home/user/project -> -home-user-project
9
+ * - D:\\Users\\me\\project -> -d-Users-me-project
10
+ */
11
+ export function computeProjectHash(workDir) {
12
+ const normalized = workDir.replace(/\\/g, '/').trim();
13
+ const withLowerDrive = normalized.replace(/^[A-Za-z]:/, (m) => `${m[0].toLowerCase()}`);
14
+ const segments = withLowerDrive
15
+ .split('/')
16
+ .filter(Boolean)
17
+ .map((segment) => segment.replace(/:/g, '').replace(/\s+/g, '-'));
18
+ if (segments.length === 0)
19
+ return '-';
20
+ return `-${segments.join('-')}`;
21
+ }
@@ -46,7 +46,16 @@ export type PipelineStageStatus = 'pending' | 'running' | 'completed' | 'failed'
46
46
  export interface PipelineState {
47
47
  id: string;
48
48
  name: string;
49
+ currentStage: 'plan' | 'execute' | 'verify' | 'fix' | 'submit' | 'done';
49
50
  status: 'running' | 'completed' | 'failed';
51
+ retryCount: number;
52
+ maxRetries: number;
53
+ stageHistory: Array<{
54
+ stage: string;
55
+ enteredAt: number;
56
+ exitedAt?: number;
57
+ output?: unknown;
58
+ }>;
50
59
  stages: Array<{
51
60
  name: string;
52
61
  status: PipelineStageStatus;
@@ -62,6 +71,7 @@ export declare class PipelineManager {
62
71
  private sessions;
63
72
  private eventBus?;
64
73
  private static readonly PIPELINE_RETRY_MAX_ATTEMPTS;
74
+ private static readonly PIPELINE_FIX_MAX_RETRIES;
65
75
  private pipelines;
66
76
  private pipelineConfigs;
67
77
  private pollInterval;
@@ -78,6 +88,7 @@ export declare class PipelineManager {
78
88
  private advancePipeline;
79
89
  /** Poll running pipelines and advance stages. */
80
90
  private pollPipelines;
91
+ private transitionPipelineStage;
81
92
  /** Detect circular dependencies. Throws if found. */
82
93
  private detectCycles;
83
94
  /** Clean up. */
package/dist/pipeline.js CHANGED
@@ -11,6 +11,7 @@ export class PipelineManager {
11
11
  sessions;
12
12
  eventBus;
13
13
  static PIPELINE_RETRY_MAX_ATTEMPTS = 3;
14
+ static PIPELINE_FIX_MAX_RETRIES = 3;
14
15
  pipelines = new Map();
15
16
  pipelineConfigs = new Map(); // #219: preserve original stage config
16
17
  pollInterval = null;
@@ -72,7 +73,11 @@ export class PipelineManager {
72
73
  const pipeline = {
73
74
  id,
74
75
  name: config.name,
76
+ currentStage: 'plan',
75
77
  status: 'running',
78
+ retryCount: 0,
79
+ maxRetries: PipelineManager.PIPELINE_FIX_MAX_RETRIES,
80
+ stageHistory: [{ stage: 'plan', enteredAt: Date.now() }],
76
81
  stages: config.stages.map(s => ({
77
82
  name: s.name,
78
83
  status: 'pending',
@@ -108,11 +113,14 @@ export class PipelineManager {
108
113
  // If any stage failed, fail the pipeline
109
114
  if (failedStages.length > 0) {
110
115
  pipeline.status = 'failed';
116
+ this.transitionPipelineStage(pipeline, 'fix', { reason: 'stage_failed', failedStages: failedStages.map(s => s.name) });
111
117
  return;
112
118
  }
113
119
  // Check if all stages are completed
114
120
  if (pipeline.stages.every(s => s.status === 'completed')) {
115
121
  pipeline.status = 'completed';
122
+ this.transitionPipelineStage(pipeline, 'submit', { reason: 'all_stages_completed' });
123
+ this.transitionPipelineStage(pipeline, 'done', { status: 'completed' });
116
124
  if (this.eventBus) {
117
125
  this.eventBus.emitEnded(id, 'pipeline_completed');
118
126
  }
@@ -148,13 +156,23 @@ export class PipelineManager {
148
156
  stage.sessionId = session.id;
149
157
  stage.status = 'running';
150
158
  stage.startedAt = Date.now();
159
+ this.transitionPipelineStage(pipeline, 'execute', { stage: stage.name, sessionId: session.id });
151
160
  }
152
161
  catch (e) {
153
162
  stage.status = 'failed';
154
163
  stage.error = getErrorMessage(e);
155
164
  pipeline.status = 'failed';
165
+ this.transitionPipelineStage(pipeline, 'fix', { stage: stage.name, error: stage.error });
156
166
  }
157
167
  }
168
+ const hasRunning = pipeline.stages.some(s => s.status === 'running');
169
+ const hasPending = pipeline.stages.some(s => s.status === 'pending');
170
+ if (hasRunning) {
171
+ this.transitionPipelineStage(pipeline, 'verify', { runningStages: pipeline.stages.filter(s => s.status === 'running').map(s => s.name) });
172
+ }
173
+ else if (hasPending) {
174
+ this.transitionPipelineStage(pipeline, 'plan', { pendingStages: pipeline.stages.filter(s => s.status === 'pending').map(s => s.name) });
175
+ }
158
176
  }
159
177
  /** Poll running pipelines and advance stages. */
160
178
  async pollPipelines() {
@@ -184,6 +202,7 @@ export class PipelineManager {
184
202
  if (session.status === 'idle') {
185
203
  stage.status = 'completed';
186
204
  stage.completedAt = Date.now();
205
+ this.transitionPipelineStage(pipeline, 'verify', { stageCompleted: stage.name });
187
206
  }
188
207
  }
189
208
  // #219: Use stored original config so stage prompt/permissionMode/autoApprove/workDir are preserved
@@ -207,6 +226,25 @@ export class PipelineManager {
207
226
  }
208
227
  }
209
228
  }
229
+ transitionPipelineStage(pipeline, stage, output) {
230
+ if (pipeline.currentStage === stage)
231
+ return;
232
+ const now = Date.now();
233
+ const previous = pipeline.stageHistory[pipeline.stageHistory.length - 1];
234
+ if (previous && previous.exitedAt === undefined) {
235
+ previous.exitedAt = now;
236
+ if (output !== undefined)
237
+ previous.output = output;
238
+ }
239
+ pipeline.currentStage = stage;
240
+ pipeline.stageHistory.push({ stage, enteredAt: now });
241
+ if (stage === 'fix') {
242
+ pipeline.retryCount += 1;
243
+ if (pipeline.retryCount > pipeline.maxRetries) {
244
+ pipeline.status = 'failed';
245
+ }
246
+ }
247
+ }
210
248
  /** Detect circular dependencies. Throws if found. */
211
249
  detectCycles(stages) {
212
250
  const graph = new Map();
package/dist/server.js CHANGED
@@ -349,6 +349,7 @@ const createSessionSchema = z.object({
349
349
  workDir: z.string().min(1),
350
350
  name: z.string().max(200).optional(),
351
351
  prompt: z.string().max(100_000).optional(),
352
+ prd: z.string().max(100_000).optional(),
352
353
  resumeSessionId: z.string().uuid().optional(),
353
354
  claudeCommand: z.string().max(10_000).optional(),
354
355
  env: z.record(z.string(), z.string()).optional(),
@@ -680,7 +681,7 @@ async function createSessionHandler(req, reply) {
680
681
  if (!parsed.success) {
681
682
  return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
682
683
  }
683
- const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
684
+ const { workDir, name, prompt, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
684
685
  if (!workDir)
685
686
  return reply.status(400).send({ error: 'workDir is required' });
686
687
  // Issue #564: Validate installed Claude Code version
@@ -729,7 +730,7 @@ async function createSessionHandler(req, reply) {
729
730
  }
730
731
  }
731
732
  console.time("POST_CREATE_SESSION");
732
- const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
733
+ const session = await sessions.createSession({ workDir: safeWorkDir, name, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
733
734
  console.timeEnd("POST_CREATE_SESSION");
734
735
  console.time("POST_CHANNEL_CREATED");
735
736
  // Issue #625: Track session in metrics so sessionsCreated counter is accurate
package/dist/session.d.ts CHANGED
@@ -40,6 +40,7 @@ export interface SessionInfo {
40
40
  parentId?: string;
41
41
  children?: string[];
42
42
  permissionPolicy?: PermissionPolicy;
43
+ prd?: string;
43
44
  }
44
45
  export interface SessionState {
45
46
  sessions: Record<string, SessionInfo>;
@@ -135,6 +136,7 @@ export declare class SessionManager {
135
136
  createSession(opts: {
136
137
  workDir: string;
137
138
  name?: string;
139
+ prd?: string;
138
140
  resumeSessionId?: string;
139
141
  claudeCommand?: string;
140
142
  env?: Record<string, string>;
@@ -304,6 +306,7 @@ export declare class SessionManager {
304
306
  createdAt: number;
305
307
  lastActivity: number;
306
308
  permissionMode: string;
309
+ prd?: string;
307
310
  }>;
308
311
  /** Paginated transcript read — does NOT advance the session's byteOffset. */
309
312
  readTranscript(id: string, page?: number, limit?: number, roleFilter?: 'user' | 'assistant' | 'system'): Promise<{
package/dist/session.js CHANGED
@@ -21,6 +21,7 @@ import { PermissionRequestManager } from './permission-request-manager.js';
21
21
  import { QuestionManager } from './question-manager.js';
22
22
  import { Mutex } from 'async-mutex';
23
23
  import { maybeInjectFault } from './fault-injection.js';
24
+ import { computeProjectHash } from './path-utils.js';
24
25
  /** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
25
26
  function hydrateSessions(raw) {
26
27
  const sessions = {};
@@ -562,6 +563,7 @@ export class SessionManager {
562
563
  settingsPatched,
563
564
  hookSettingsFile,
564
565
  hookSecret,
566
+ prd: opts.prd,
565
567
  };
566
568
  this.state.sessions[id] = session;
567
569
  this.invalidateSessionsListCache();
@@ -1207,6 +1209,7 @@ export class SessionManager {
1207
1209
  createdAt: session.createdAt,
1208
1210
  lastActivity: session.lastActivity,
1209
1211
  permissionMode: session.permissionMode,
1212
+ prd: session.prd,
1210
1213
  };
1211
1214
  }
1212
1215
  /** Paginated transcript read — does NOT advance the session's byteOffset. */
@@ -1400,7 +1403,7 @@ export class SessionManager {
1400
1403
  }
1401
1404
  /** Attempt filesystem-based discovery for a single session poll tick. */
1402
1405
  async maybeDiscoverFromFilesystem(session, workDir) {
1403
- const projectHash = '-' + workDir.replace(/^\//, '').replace(/\//g, '-');
1406
+ const projectHash = computeProjectHash(workDir);
1404
1407
  const projectDir = join(this.config.claudeProjectsDir, projectHash);
1405
1408
  if (!existsSync(projectDir))
1406
1409
  return false;
package/dist/tmux.js CHANGED
@@ -11,6 +11,7 @@ import { existsSync, readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { homedir, tmpdir } from 'node:os';
13
13
  import { randomBytes } from 'node:crypto';
14
+ import { computeProjectHash } from './path-utils.js';
14
15
  /** Shell-escape a string by wrapping in single quotes and escaping embedded single quotes. */
15
16
  function shellEscape(s) {
16
17
  return `'${s.replace(/'/g, "'\\''")}'`;
@@ -360,7 +361,7 @@ export class TmuxManager {
360
361
  */
361
362
  async archiveStaleSessionFiles(workDir) {
362
363
  // Compute the project hash the same way Claude CLI does
363
- const projectHash = '-' + workDir.replace(/^\//, '').replace(/\//g, '-');
364
+ const projectHash = computeProjectHash(workDir);
364
365
  const projectDir = join(homedir(), '.claude', 'projects', projectHash);
365
366
  if (!existsSync(projectDir))
366
367
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.15.1",
3
+ "version": "2.15.2",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",