fraim-framework 2.0.185 → 2.0.187

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.
@@ -227,6 +227,9 @@ function safeHttpUrl(value) {
227
227
  }
228
228
  // ─── Issue #578: Deployment + Host stores ─────────────────────────────────────
229
229
  const VALID_EMPLOYEE_IDS = ['codex', 'claude', 'gemini', 'copilot'];
230
+ function startSessionSeedForHost(hostId, runId) {
231
+ return hostId === 'gemini' ? undefined : runId;
232
+ }
230
233
  class DeploymentStore {
231
234
  constructor(filePath) {
232
235
  this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-deployments.json');
@@ -587,6 +590,42 @@ function emptyTotals() {
587
590
  },
588
591
  };
589
592
  }
593
+ const PHASE_STATUSES = new Set(['starting', 'complete', 'incomplete', 'failure']);
594
+ function normalizePersistedPhaseHistory(value) {
595
+ if (!Array.isArray(value))
596
+ return [];
597
+ return value
598
+ .map((entry) => {
599
+ if (!entry || typeof entry !== 'object')
600
+ return null;
601
+ const raw = entry;
602
+ if (typeof raw.phaseId !== 'string' || !raw.phaseId)
603
+ return null;
604
+ const latestStatus = typeof raw.latestStatus === 'string' && PHASE_STATUSES.has(raw.latestStatus)
605
+ ? raw.latestStatus
606
+ : null;
607
+ return {
608
+ phaseId: raw.phaseId,
609
+ enteredAt: typeof raw.enteredAt === 'string' ? raw.enteredAt : new Date().toISOString(),
610
+ latestStatus,
611
+ latestText: typeof raw.latestText === 'string' ? raw.latestText : null,
612
+ };
613
+ })
614
+ .filter((entry) => Boolean(entry));
615
+ }
616
+ function readPersistedRunProjection(conversation) {
617
+ const rawRun = conversation?.run;
618
+ if (!rawRun || typeof rawRun !== 'object')
619
+ return null;
620
+ const value = rawRun;
621
+ return {
622
+ currentPhase: typeof value.currentPhase === 'string' ? value.currentPhase : null,
623
+ phaseHistory: normalizePersistedPhaseHistory(value.phaseHistory),
624
+ stages: Array.isArray(value.stages) ? value.stages : [],
625
+ totals: value.totals && typeof value.totals === 'object' ? value.totals : null,
626
+ runDiscriminant: typeof value.runDiscriminant === 'string' ? value.runDiscriminant : null,
627
+ };
628
+ }
590
629
  // Issue #347 — apply per-turn usage from the host stream into the
591
630
  // run's tokenTotals. Idempotent on the same turn (we replace, not add)
592
631
  // because Codex's `turn.completed.usage` is cumulative, not delta.
@@ -766,20 +805,17 @@ function deriveStages(run, projectPath) {
766
805
  }
767
806
  return declaredPath.map((phase, index) => {
768
807
  let state;
769
- if (currentIndex < 0) {
770
- state = 'upcoming';
771
- }
772
- else if (index < currentIndex) {
773
- state = 'done';
774
- }
775
- else if (index === currentIndex) {
808
+ const entry = historyMap.get(phase.id);
809
+ if (index === currentIndex) {
776
810
  // If the agent has already reported this phase as 'complete', advance
777
811
  // its visual state to 'done' so the tracker doesn't look frozen while
778
812
  // waiting for the agent to start the next phase (e.g. after
779
813
  // implement-submission completes but before address-feedback starts).
780
- const entry = historyMap.get(phase.id);
781
814
  state = entry?.latestStatus === 'complete' ? 'done' : 'current';
782
815
  }
816
+ else if (entry?.latestStatus === 'complete' || (currentIndex >= 0 && index < currentIndex && entry)) {
817
+ state = 'done';
818
+ }
783
819
  else {
784
820
  state = 'upcoming';
785
821
  }
@@ -1399,6 +1435,7 @@ class AiHubServer {
1399
1435
  }
1400
1436
  conversationRecordFromRun(run) {
1401
1437
  const lastUpdatedAt = run.updatedAt || new Date().toISOString();
1438
+ const stages = deriveStages(run, run.projectPath);
1402
1439
  return {
1403
1440
  id: run.conversationId || run.id,
1404
1441
  projectPath: path_1.default.resolve(run.projectPath),
@@ -1433,11 +1470,27 @@ class AiHubServer {
1433
1470
  compareRunId: run.compareRunId || null,
1434
1471
  // Issue #578: preserve trigger source so the UI can render the chip.
1435
1472
  sourceTrigger: run.sourceTrigger,
1473
+ // Issue #708: carry the invocation scope so the record lands in (and is keyed to)
1474
+ // the right bucket. Falls back to the legacy client `invokedArea` when present.
1475
+ scope: run.scope
1476
+ ?? run.invokedArea
1477
+ ?? 'project',
1478
+ run: {
1479
+ stages,
1480
+ currentPhase: run.currentPhase || null,
1481
+ phaseHistory: run.phaseHistory || [],
1482
+ totals: run.totals || null,
1483
+ runDiscriminant: run.runDiscriminant || null,
1484
+ },
1436
1485
  };
1437
1486
  }
1438
1487
  persistRunConversation(run, activeId) {
1439
1488
  try {
1440
- this.conversationStore.upsertConversation(run.projectPath, this.conversationRecordFromRun(run), activeId);
1489
+ // Issue #708: route the record to its scope bucket (manager/company runs get a
1490
+ // project-independent home); project runs continue to key by project path.
1491
+ const record = this.conversationRecordFromRun(run);
1492
+ const key = (0, conversation_store_1.conversationScopeKey)(record.scope, run.projectPath);
1493
+ this.conversationStore.upsertConversation(key, record, activeId);
1441
1494
  }
1442
1495
  catch (error) {
1443
1496
  console.warn('[ai-hub] conversation store write failed:', error instanceof Error ? error.message : error);
@@ -1544,8 +1597,10 @@ class AiHubServer {
1544
1597
  }
1545
1598
  if (event.agentIdentity)
1546
1599
  applyAgentIdentitySignal(current, event.agentIdentity);
1600
+ if (event.fraimJob)
1601
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
1547
1602
  if (event.seekMentoring)
1548
- applySeekMentoringSignal(current, event.seekMentoring);
1603
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
1549
1604
  if (event.usage)
1550
1605
  applyUsageSignal(current, event.usage);
1551
1606
  });
@@ -1567,7 +1622,7 @@ class AiHubServer {
1567
1622
  }
1568
1623
  this.runRegistry.dispose(childRun.id);
1569
1624
  },
1570
- }, childRun.sessionId);
1625
+ }, startSessionSeedForHost(managerRun.hostId, childRun.id));
1571
1626
  this.runRegistry.attachChildIfRunning(childRun.id, child);
1572
1627
  }
1573
1628
  notifyManagerOfDelegatedChild(managerRunId, childRun) {
@@ -1691,8 +1746,10 @@ class AiHubServer {
1691
1746
  }
1692
1747
  if (event.agentIdentity)
1693
1748
  applyAgentIdentitySignal(current, event.agentIdentity);
1749
+ if (event.fraimJob)
1750
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
1694
1751
  if (event.seekMentoring)
1695
- applySeekMentoringSignal(current, event.seekMentoring);
1752
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
1696
1753
  if (event.usage)
1697
1754
  applyUsageSignal(current, event.usage);
1698
1755
  });
@@ -1764,14 +1821,48 @@ class AiHubServer {
1764
1821
  };
1765
1822
  }
1766
1823
  resolveHubJob(projectPath, jobId) {
1824
+ if (!jobId || jobId === '__freeform__')
1825
+ return null;
1767
1826
  const employeeJob = (0, catalog_1.discoverEmployeeJobs)(projectPath).find((job) => job.id === jobId);
1768
- if (employeeJob)
1769
- return { id: employeeJob.id, stubPath: employeeJob.stubPath };
1827
+ if (employeeJob) {
1828
+ return {
1829
+ id: employeeJob.id,
1830
+ title: employeeJob.title,
1831
+ stubPath: employeeJob.stubPath,
1832
+ personaKey: employeeJob.requiredPersonaKey ?? getProtectedPersonaForHubJob(employeeJob.id),
1833
+ };
1834
+ }
1770
1835
  const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
1771
- if (managerTemplate)
1772
- return { id: managerTemplate.id, stubPath: managerTemplate.stubPath };
1836
+ if (managerTemplate) {
1837
+ return {
1838
+ id: managerTemplate.id,
1839
+ title: managerTemplate.title,
1840
+ stubPath: managerTemplate.stubPath,
1841
+ personaKey: getProtectedPersonaForHubJob(managerTemplate.id),
1842
+ };
1843
+ }
1773
1844
  return null;
1774
1845
  }
1846
+ applySeekMentoringSignalToRun(run, signal) {
1847
+ this.maybePromoteFreeformRunToJob(run, signal.jobId || signal.jobName);
1848
+ applySeekMentoringSignal(run, signal);
1849
+ }
1850
+ applyFraimJobSignalToRun(run, signal) {
1851
+ this.maybePromoteFreeformRunToJob(run, signal.jobId);
1852
+ }
1853
+ maybePromoteFreeformRunToJob(run, signalJobId) {
1854
+ const normalizedJobId = (signalJobId || '').trim().toLowerCase();
1855
+ if (run.jobId !== '__freeform__' || !normalizedJobId)
1856
+ return;
1857
+ const metadata = this.resolveHubJob(run.projectPath, normalizedJobId);
1858
+ if (!metadata)
1859
+ return;
1860
+ run.jobId = metadata.id;
1861
+ run.jobTitle = metadata.title;
1862
+ run.personaKey = metadata.personaKey;
1863
+ run.updatedAt = new Date().toISOString();
1864
+ run.events.push((0, hosts_1.createHubEvent)('system', `Recognized FRAIM job ${metadata.id} from structured telemetry.`));
1865
+ }
1775
1866
  // Lightweight markdown → .docx. Shared by the GET (file path) and POST (inline
1776
1867
  // content) export routes so a conversational deliverable with no on-disk file
1777
1868
  // can still be downloaded for Word annotation.
@@ -1979,6 +2070,19 @@ class AiHubServer {
1979
2070
  }
1980
2071
  // Read API key from header — query-param API keys are prohibited (§3.14)
1981
2072
  const apiKey = typeof req.headers['x-fraim-api-key'] === 'string' ? req.headers['x-fraim-api-key'] : undefined;
2073
+ // #719: a bare reload (no projectPath query) must land on the last-recorded
2074
+ // workspace project, not the launch folder. The bootstrap saves its resolved
2075
+ // current path back into the projects list, so defaulting to the launch
2076
+ // folder would resurrect a removed launch-folder project on every reload
2077
+ // (tfSwitchProjectFolder already documents the recorded folder as "the
2078
+ // default on next load"). The launch folder stays the fallback when nothing
2079
+ // is recorded or the recorded folder no longer exists.
2080
+ if (!projectPath) {
2081
+ const recorded = this.preferencesStore.load(this.projectPath).projectPath;
2082
+ if (recorded && path_1.default.resolve(recorded) !== path_1.default.resolve(this.projectPath) && fs_1.default.existsSync(recorded)) {
2083
+ projectPath = recorded;
2084
+ }
2085
+ }
1982
2086
  res.json(await this.bootstrapResponse(projectPath || this.projectPath, apiKey));
1983
2087
  });
1984
2088
  // Issue #512 (S3, R14) — Brain summary as a standalone route, returning the
@@ -2064,25 +2168,32 @@ class AiHubServer {
2064
2168
  await this.remoteGateway.removeManagerTeam(apiKey, personaKey);
2065
2169
  return res.status(204).end();
2066
2170
  });
2171
+ // Issue #708: manager/company scopes resolve to a project-independent sentinel bucket
2172
+ // key (which must bypass path/dir resolution); everything else keys by project path.
2173
+ const scopeParam = (raw) => (raw === 'manager' || raw === 'company') ? raw : undefined;
2067
2174
  this.app.get('/api/ai-hub/conversations', (req, res) => {
2068
- const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2069
- ? path_1.default.resolve(req.query.projectPath)
2070
- : this.projectPath;
2071
- const loaded = this.conversationStore.loadProject(projectPath);
2072
- return res.json({ projectPath, ...loaded, source: 'disk' });
2175
+ const scope = scopeParam(req.query.scope);
2176
+ const key = scope
2177
+ ? (0, conversation_store_1.conversationScopeKey)(scope, '')
2178
+ : (typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2179
+ ? path_1.default.resolve(req.query.projectPath)
2180
+ : this.projectPath);
2181
+ const loaded = this.conversationStore.loadProject(key);
2182
+ return res.json({ projectPath: key, scope: scope ?? 'project', ...loaded, source: 'disk' });
2073
2183
  });
2074
2184
  this.app.put('/api/ai-hub/conversations', (req, res) => {
2075
2185
  try {
2076
2186
  const body = (req.body ?? {});
2077
- const projectPath = ensureDirectoryPath(body.projectPath || this.projectPath);
2187
+ const scope = scopeParam(body.scope);
2188
+ const key = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
2078
2189
  if (!Array.isArray(body.conversations)) {
2079
2190
  return res.status(400).json({ error: 'conversations array required' });
2080
2191
  }
2081
- const saved = this.conversationStore.replaceProject(projectPath, {
2192
+ const saved = this.conversationStore.replaceProject(key, {
2082
2193
  activeId: body.activeId ?? null,
2083
2194
  conversations: body.conversations,
2084
2195
  });
2085
- return res.json({ projectPath, ...saved, source: 'disk' });
2196
+ return res.json({ projectPath: key, scope: scope ?? 'project', ...saved, source: 'disk' });
2086
2197
  }
2087
2198
  catch (error) {
2088
2199
  return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not persist conversations.' });
@@ -2091,7 +2202,8 @@ class AiHubServer {
2091
2202
  this.app.patch('/api/ai-hub/conversations/:conversationId', (req, res) => {
2092
2203
  try {
2093
2204
  const body = (req.body ?? {});
2094
- const projectPath = ensureDirectoryPath(body.projectPath || this.projectPath);
2205
+ const scope = scopeParam(body.scope);
2206
+ const projectPath = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
2095
2207
  const saved = this.conversationStore.patchConversation(projectPath, req.params.conversationId, body);
2096
2208
  if (body.activeId !== undefined) {
2097
2209
  const withActive = this.conversationStore.replaceProject(projectPath, { ...saved, activeId: body.activeId });
@@ -2123,6 +2235,46 @@ class AiHubServer {
2123
2235
  return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not persist projects.' });
2124
2236
  }
2125
2237
  });
2238
+ // Issue #719: DELETE /api/ai-hub/projects/:id — remove a project from the Hub.
2239
+ // PUT is a merge by design and cannot express removal, so removal gets its own
2240
+ // route (same convention as /schedules/:id, /webhooks/:id, /hosts/:id). Deletes
2241
+ // every server-side derivation layer — preferences entry, conversation-store
2242
+ // KEY, project-scoped deployments — so the project cannot resurrect (R3/R6/R9).
2243
+ // Never touches anything under the project's folderPath (R8).
2244
+ this.app.delete('/api/ai-hub/projects/:id', (req, res) => {
2245
+ const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2246
+ ? path_1.default.resolve(req.query.projectPath)
2247
+ : this.projectPath;
2248
+ const known = this.knownProjects(projectPath);
2249
+ const entry = known.find((project) => project.id === req.params.id);
2250
+ if (!entry)
2251
+ return res.status(404).json({ error: 'Project not found.' });
2252
+ // The current projectPath is unconditionally re-injected on every load, so
2253
+ // deleting it would resurrect on the next request — reject as a sequencing
2254
+ // error; the client switches workspaces before deleting (R5/D4).
2255
+ if (sameDirectoryPath(entry.folderPath, projectPath)) {
2256
+ return res.status(409).json({ error: 'Cannot remove the current project. Switch to another project first.' });
2257
+ }
2258
+ // A live schedule/webhook would fire later, write a conversation under the
2259
+ // removed folderPath, and resurrect the project in the background (R6).
2260
+ for (const deployment of this.deploymentStore.load()) {
2261
+ if (!deploymentBelongsToProject(deployment, entry.folderPath, this.projectPath))
2262
+ continue;
2263
+ const task = this.cronHandles.get(deployment.id);
2264
+ if (task) {
2265
+ task.stop();
2266
+ this.cronHandles.delete(deployment.id);
2267
+ }
2268
+ this.deploymentStore.delete(deployment.id);
2269
+ }
2270
+ // Delete the conversation-store KEY — an empty list would still re-derive
2271
+ // the project via listProjectPaths (R9).
2272
+ this.conversationStore.removeProject(entry.folderPath);
2273
+ // Persist the filtered list directly: saveKnownProjects unions with the
2274
+ // derived list by design and cannot express removal.
2275
+ const projects = this.preferencesStore.saveProjects(projectPath, known.filter((project) => project.id !== entry.id));
2276
+ return res.json({ ok: true, projects });
2277
+ });
2126
2278
  this.app.post('/api/ai-hub/api-key', (req, res) => {
2127
2279
  const { apiKey } = req.body;
2128
2280
  if (!apiKey || typeof apiKey !== 'string')
@@ -2355,11 +2507,15 @@ class AiHubServer {
2355
2507
  throw new Error('Choose a FRAIM job before starting a run.');
2356
2508
  }
2357
2509
  const startTimestamp = new Date().toISOString();
2510
+ const jobMetadata = this.resolveHubJob(projectPath, jobId);
2511
+ const fallbackJobTitle = typeof req.body.jobTitle === 'string' && req.body.jobTitle.trim()
2512
+ ? req.body.jobTitle.trim()
2513
+ : jobId;
2358
2514
  const run = {
2359
2515
  id: (0, crypto_1.randomUUID)(),
2360
2516
  conversationId: typeof req.body.conversationId === 'string' && req.body.conversationId.trim() ? req.body.conversationId.trim() : undefined,
2361
2517
  conversationTitle: typeof req.body.conversationTitle === 'string' && req.body.conversationTitle.trim() ? req.body.conversationTitle.trim() : undefined,
2362
- jobTitle: typeof req.body.jobTitle === 'string' && req.body.jobTitle.trim() ? req.body.jobTitle.trim() : jobId,
2518
+ jobTitle: jobMetadata?.title || fallbackJobTitle,
2363
2519
  jobId,
2364
2520
  hostId,
2365
2521
  projectPath,
@@ -2373,7 +2529,7 @@ class AiHubServer {
2373
2529
  phaseHistory: [],
2374
2530
  totals: emptyTotals(),
2375
2531
  lastStatusChangeAt: startTimestamp,
2376
- personaKey: getProtectedPersonaForHubJob(jobId),
2532
+ personaKey: jobMetadata?.personaKey ?? getProtectedPersonaForHubJob(jobId),
2377
2533
  // Issue #442: mark this as the FRAIM side of an A/B pair when applicable.
2378
2534
  ...(compareMode === 'ab' ? { runRole: 'fraim' } : {}),
2379
2535
  // #0: trigger source — defaults to 'manager' when not provided by the caller.
@@ -2426,8 +2582,10 @@ class AiHubServer {
2426
2582
  }
2427
2583
  if (event.agentIdentity)
2428
2584
  applyAgentIdentitySignal(current, event.agentIdentity);
2585
+ if (event.fraimJob)
2586
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
2429
2587
  if (event.seekMentoring)
2430
- applySeekMentoringSignal(current, event.seekMentoring);
2588
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
2431
2589
  if (event.usage)
2432
2590
  applyUsageSignal(current, event.usage);
2433
2591
  });
@@ -2460,7 +2618,7 @@ class AiHubServer {
2460
2618
  if (latest)
2461
2619
  this.drainPendingDelegatedReviews(latest);
2462
2620
  },
2463
- }, run.sessionId);
2621
+ }, startSessionSeedForHost(hostId, run.id));
2464
2622
  this.runRegistry.attachChildIfRunning(run.id, child);
2465
2623
  // Issue #442: spawn the Direct run via startDirectRun so CliHostRuntime
2466
2624
  // uses buildDirectStartPlan (--strict-mcp-config, raw stdin) rather than
@@ -2489,7 +2647,7 @@ class AiHubServer {
2489
2647
  });
2490
2648
  this.runRegistry.dispose(directId);
2491
2649
  },
2492
- }, directRun.sessionId);
2650
+ }, startSessionSeedForHost(hostId, directRun.id));
2493
2651
  this.runRegistry.attachChildIfRunning(directRun.id, directChild);
2494
2652
  }
2495
2653
  const existingPreferences = this.preferencesStore.load(projectPath);
@@ -2615,8 +2773,10 @@ class AiHubServer {
2615
2773
  }
2616
2774
  if (event.agentIdentity)
2617
2775
  applyAgentIdentitySignal(current, event.agentIdentity);
2776
+ if (event.fraimJob)
2777
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
2618
2778
  if (event.seekMentoring)
2619
- applySeekMentoringSignal(current, event.seekMentoring);
2779
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
2620
2780
  if (event.usage)
2621
2781
  applyUsageSignal(current, event.usage);
2622
2782
  });
@@ -2677,16 +2837,27 @@ class AiHubServer {
2677
2837
  if (!employee?.available) {
2678
2838
  throw new Error(`${employee?.label || 'Selected employee'} is not available on this machine.`);
2679
2839
  }
2840
+ const conversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
2841
+ ? body.conversationId.trim()
2842
+ : undefined;
2843
+ const persistedConversation = conversationId
2844
+ ? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === conversationId)
2845
+ : undefined;
2846
+ const persistedRun = readPersistedRunProjection(persistedConversation);
2680
2847
  const now = new Date().toISOString();
2681
2848
  const run = {
2682
2849
  id: (0, crypto_1.randomUUID)(),
2683
- conversationId: typeof body.conversationId === 'string' && body.conversationId.trim() ? body.conversationId.trim() : undefined,
2850
+ conversationId,
2684
2851
  conversationTitle: typeof body.conversationTitle === 'string' && body.conversationTitle.trim() ? body.conversationTitle.trim() : undefined,
2685
2852
  jobTitle: typeof body.jobTitle === 'string' && body.jobTitle.trim() ? body.jobTitle.trim() : jobId,
2686
2853
  jobId, hostId, projectPath, status: 'running', sessionId,
2687
2854
  createdAt: now, updatedAt: now, messages: [],
2688
2855
  events: [(0, hosts_1.createHubEvent)('system', `Resuming ${hostId} session ${sessionId} in ${projectPath}`)],
2689
- currentPhase: null, phaseHistory: [], totals: emptyTotals(), lastStatusChangeAt: now,
2856
+ currentPhase: persistedRun?.currentPhase || null,
2857
+ phaseHistory: persistedRun?.phaseHistory || [],
2858
+ totals: persistedRun?.totals || emptyTotals(),
2859
+ lastStatusChangeAt: now,
2860
+ runDiscriminant: persistedRun?.runDiscriminant || undefined,
2690
2861
  personaKey: getProtectedPersonaForHubJob(jobId),
2691
2862
  };
2692
2863
  // Continue-turn message (FRAIM invocation for the job + instructions) plus
@@ -2710,8 +2881,10 @@ class AiHubServer {
2710
2881
  }
2711
2882
  if (event.agentIdentity)
2712
2883
  applyAgentIdentitySignal(current, event.agentIdentity);
2884
+ if (event.fraimJob)
2885
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
2713
2886
  if (event.seekMentoring)
2714
- applySeekMentoringSignal(current, event.seekMentoring);
2887
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
2715
2888
  if (event.usage)
2716
2889
  applyUsageSignal(current, event.usage);
2717
2890
  });
@@ -3139,8 +3312,10 @@ class AiHubServer {
3139
3312
  current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
3140
3313
  if (event.agentIdentity)
3141
3314
  applyAgentIdentitySignal(current, event.agentIdentity);
3315
+ if (event.fraimJob)
3316
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
3142
3317
  if (event.seekMentoring)
3143
- applySeekMentoringSignal(current, event.seekMentoring);
3318
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
3144
3319
  if (event.usage)
3145
3320
  applyUsageSignal(current, event.usage);
3146
3321
  });
@@ -3153,7 +3328,7 @@ class AiHubServer {
3153
3328
  });
3154
3329
  this.runRegistry.dispose(run.id);
3155
3330
  },
3156
- }, run.sessionId);
3331
+ }, startSessionSeedForHost(hostId, run.id));
3157
3332
  // Update the registry entry with the real child process handle.
3158
3333
  this.runRegistry.attachChildIfRunning(run.id, child);
3159
3334
  return res.json({ runId: run.id, status: 'started', employee: employeeId, job: jobName });
@@ -3259,8 +3434,10 @@ class AiHubServer {
3259
3434
  }
3260
3435
  if (event.agentIdentity)
3261
3436
  applyAgentIdentitySignal(current, event.agentIdentity);
3437
+ if (event.fraimJob)
3438
+ this.applyFraimJobSignalToRun(current, event.fraimJob);
3262
3439
  if (event.seekMentoring)
3263
- applySeekMentoringSignal(current, event.seekMentoring);
3440
+ this.applySeekMentoringSignalToRun(current, event.seekMentoring);
3264
3441
  if (event.usage)
3265
3442
  applyUsageSignal(current, event.usage);
3266
3443
  });
@@ -3280,7 +3457,7 @@ class AiHubServer {
3280
3457
  this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
3281
3458
  this.runRegistry.dispose(run.id);
3282
3459
  },
3283
- });
3460
+ }, startSessionSeedForHost(deployment.hostId, run.id));
3284
3461
  this.runRegistry.create(run, child);
3285
3462
  this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = run.id; });
3286
3463
  return run;
@@ -281,6 +281,7 @@ const listSupportedIDEs = () => {
281
281
  console.log(chalk_1.default.yellow(' Example: fraim add-ide --ide claude-code'));
282
282
  console.log(chalk_1.default.yellow(' Anthropic aliases: claude, claude-code, claude-desktop, claude-cowork'));
283
283
  console.log(chalk_1.default.yellow(' Gemini aliases: gemini, gemini-cli, gemini cli'));
284
+ console.log(chalk_1.default.yellow(' GitHub Copilot CLI aliases: copilot, copilot-cli, github copilot cli'));
284
285
  };
285
286
  const promptForIDESelection = async (availableIDEs, tokens) => {
286
287
  console.log(chalk_1.default.green(`✅ Found ${availableIDEs.length} IDEs that can be configured:\n`));
@@ -385,7 +386,7 @@ const runAddIDE = async (options) => {
385
386
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
386
387
  if (detectedIDEs.length === 0) {
387
388
  console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected on your system.'));
388
- console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
389
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, GitHub Copilot CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
389
390
  console.log(chalk_1.default.blue('\n💡 Install an IDE and run this command again.'));
390
391
  return;
391
392
  }
@@ -455,7 +456,7 @@ const runAddIDE = async (options) => {
455
456
  exports.runAddIDE = runAddIDE;
456
457
  exports.addIDECommand = new commander_1.Command('add-ide')
457
458
  .description('Add FRAIM configuration to additional IDEs')
458
- .option('--ide <name>', 'Configure specific IDE (claude, claude-code, claude-desktop, claude-cowork, antigravity, gemini, gemini-cli, kiro, cursor, vscode, codex, grok, windsurf)')
459
+ .option('--ide <name>', 'Configure specific IDE (claude, claude-code, claude-desktop, claude-cowork, antigravity, gemini, gemini-cli, copilot, copilot-cli, kiro, cursor, vscode, codex, grok, windsurf)')
459
460
  .option('--all', 'Configure all detected IDEs')
460
461
  .option('--list', 'List all supported IDEs and their detection status')
461
462
  .action(exports.runAddIDE);
@@ -52,6 +52,10 @@ const forEachInstalledIDE = async (callback) => {
52
52
  const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
53
53
  const urlOnlyEntry = { type: 'http', url: mcpUrl };
54
54
  await forEachInstalledIDE(async (ide, configPath) => {
55
+ if (provider === 'github' && ide.configType === 'copilot-cli') {
56
+ console.log(chalk_1.default.gray(`Skipped ${ide.name} GitHub MCP entry (built into Copilot CLI)`));
57
+ return;
58
+ }
55
59
  if (ide.configFormat === 'json') {
56
60
  let ideConfig = {};
57
61
  if (fs_1.default.existsSync(configPath)) {
@@ -61,7 +65,9 @@ const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
61
65
  if (!ideConfig[serversKey]) {
62
66
  ideConfig[serversKey] = {};
63
67
  }
64
- ideConfig[serversKey][provider] = urlOnlyEntry;
68
+ ideConfig[serversKey][provider] = ide.configType === 'copilot-cli'
69
+ ? { ...urlOnlyEntry, tools: ['*'] }
70
+ : urlOnlyEntry;
65
71
  fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
66
72
  console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
67
73
  }
@@ -135,7 +135,8 @@ async function cleanupStaleAdapterFiles(projectRoot, allowedConfigTypes) {
135
135
  const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
136
136
  const execFileAsync = promisify(execFile);
137
137
  for (const [relPath, configType] of Object.entries((0, agent_adapters_1.getAdapterConfigTypes)())) {
138
- if (configType === 'standard' || allowedConfigTypes.includes(configType)) {
138
+ const configTypes = Array.isArray(configType) ? configType : [configType];
139
+ if (configTypes.includes('standard') || configTypes.some((type) => allowedConfigTypes.includes(type))) {
139
140
  continue;
140
141
  }
141
142
  const fullPath = path_1.default.join(projectRoot, relPath);
@@ -2,7 +2,7 @@
2
2
  // IDE Format Adapters - transform logical server structure to IDE-specific formats
3
3
  // Uses the centralized registry to determine server types
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.IDE_FORMATS = exports.GrokFormat = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
5
+ exports.IDE_FORMATS = exports.GrokFormat = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.CopilotCliFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
6
6
  exports.getIDEFormat = getIDEFormat;
7
7
  const mcp_server_registry_1 = require("./mcp-server-registry");
8
8
  const provider_registry_1 = require("../providers/provider-registry");
@@ -120,6 +120,40 @@ class GeminiCliFormat {
120
120
  }
121
121
  }
122
122
  exports.GeminiCliFormat = GeminiCliFormat;
123
+ class CopilotCliFormat {
124
+ constructor() {
125
+ this.name = 'copilot-cli';
126
+ }
127
+ transform(servers) {
128
+ const mcpServers = {};
129
+ for (const [key, server] of servers) {
130
+ // GitHub MCP is built into Copilot CLI. Avoid a duplicate user-configured
131
+ // github entry and let Copilot handle its native GitHub auth path.
132
+ if (key === 'github') {
133
+ continue;
134
+ }
135
+ if (server.url) {
136
+ mcpServers[key] = {
137
+ type: server.type === 'sse' ? 'sse' : 'http',
138
+ url: server.url,
139
+ ...(server.headers && { headers: server.headers }),
140
+ tools: ['*']
141
+ };
142
+ }
143
+ else {
144
+ mcpServers[key] = {
145
+ type: 'stdio',
146
+ command: server.command,
147
+ ...(server.args && { args: server.args }),
148
+ ...(server.env && { env: server.env }),
149
+ tools: ['*']
150
+ };
151
+ }
152
+ }
153
+ return { mcpServers };
154
+ }
155
+ }
156
+ exports.CopilotCliFormat = CopilotCliFormat;
123
157
  // Claude Desktop format (excludes provider servers - Issue #132)
124
158
  class ClaudeFormat {
125
159
  constructor() {
@@ -267,6 +301,7 @@ exports.IDE_FORMATS = {
267
301
  kiro: new KiroFormat(),
268
302
  vscode: new VSCodeFormat(),
269
303
  'gemini-cli': new GeminiCliFormat(),
304
+ 'copilot-cli': new CopilotCliFormat(),
270
305
  claude: new ClaudeFormat(),
271
306
  'claude-code': new ClaudeCodeFormat(),
272
307
  windsurf: new WindsurfFormat(),
@@ -233,7 +233,7 @@ const autoConfigureMCP = async (fraimKey, selectedIDEs) => {
233
233
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
234
234
  if (detectedIDEs.length === 0 && (!selectedIDEs || selectedIDEs.length === 0)) {
235
235
  console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
236
- console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
236
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, GitHub Copilot CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
237
237
  console.log(chalk_1.default.blue('\n💡 You can install an IDE and run setup again later.'));
238
238
  console.log(chalk_1.default.gray(' Or continue with manual MCP configuration.'));
239
239
  if (process.env.FRAIM_NON_INTERACTIVE) {