fraim-hub 2.0.287 → 2.0.289

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.
@@ -82,6 +82,22 @@ function parseSeekMentoringSignal(line) {
82
82
  return sig;
83
83
  }
84
84
  }
85
+ // Copilot CLI shape (issue #1399): { type: 'tool.execution_start', data: {
86
+ // mcpServerName, mcpToolName, arguments } }. Live-verified via a direct
87
+ // spike against the installed CLI under --output-format json (see
88
+ // docs/rfcs/1399-copilot-host-chattiness-technical-design.md) — Copilot
89
+ // reports the MCP server and tool name as separate, unambiguous fields
90
+ // rather than a qualifier string, so this reads data.mcpToolName directly
91
+ // instead of going through canonicalToolName's __/./: splitting.
92
+ if (obj.type === 'tool.execution_start' && typeof obj.data === 'object' && obj.data !== null) {
93
+ const data = obj.data;
94
+ if (isFraimTool(data.mcpToolName, 'seekMentoring')) {
95
+ const args = normalizeToolArgs(data.arguments) || undefined;
96
+ const sig = extractSignalFromArgs(args);
97
+ if (sig)
98
+ return sig;
99
+ }
100
+ }
85
101
  // Claude Code shape: tool_use blocks live inside parsed.message.content.
86
102
  // The name is MCP-prefixed (e.g. 'mcp__fraim__seekMentoring').
87
103
  const candidates = [obj];
@@ -133,6 +149,16 @@ function parseFraimJobLoadSignal(line) {
133
149
  return sig;
134
150
  }
135
151
  }
152
+ // Copilot CLI shape (issue #1399): see parseSeekMentoringSignal's Copilot
153
+ // block above for the field-shape rationale.
154
+ if (obj.type === 'tool.execution_start' && typeof obj.data === 'object' && obj.data !== null) {
155
+ const data = obj.data;
156
+ if (isFraimTool(data.mcpToolName, 'get_fraim_job')) {
157
+ const sig = readFraimJobFromArgs(data.arguments);
158
+ if (sig)
159
+ return sig;
160
+ }
161
+ }
136
162
  // Claude Code shape: tool_use blocks live inside message.content with
137
163
  // an MCP-prefixed name such as mcp__fraim__get_fraim_job.
138
164
  const candidates = [obj];
@@ -289,6 +315,14 @@ function parseAgentIdentitySignal(line) {
289
315
  return readAgentFromArgs(normalizeToolArgs(item.arguments) || undefined);
290
316
  }
291
317
  }
318
+ // Copilot CLI shape (issue #1399): see parseSeekMentoringSignal's Copilot
319
+ // block for the field-shape rationale.
320
+ if (obj.type === 'tool.execution_start' && typeof obj.data === 'object' && obj.data !== null) {
321
+ const data = obj.data;
322
+ if (isFraimTool(data.mcpToolName, 'fraim_connect')) {
323
+ return readAgentFromArgs(normalizeToolArgs(data.arguments) || undefined);
324
+ }
325
+ }
292
326
  // Claude Code shape.
293
327
  const candidates = [obj];
294
328
  if (typeof obj.message === 'object' && obj.message !== null) {
@@ -1434,7 +1468,12 @@ function buildStartPlan(hostId, message, sessionId) {
1434
1468
  const browser = sharedBrowserHostConfig('copilot');
1435
1469
  return {
1436
1470
  command: COPILOT_BINARY,
1437
- args: ['--yolo', ...browser.args],
1471
+ // Issue #1399: request Copilot's structured JSONL output mode
1472
+ // (one JSON object per line) instead of its default human-
1473
+ // readable terminal rendering, so parseHostLine can classify
1474
+ // tool calls/results/deltas separately from genuine reply text
1475
+ // instead of pattern-matching rendered chrome characters.
1476
+ args: ['--yolo', '--output-format', 'json', ...browser.args],
1438
1477
  stdin: transformHeadlessFraimMessage(message, 'start'),
1439
1478
  env: browser.env,
1440
1479
  };
@@ -1495,7 +1534,8 @@ function buildContinuePlan(hostId, sessionId, message) {
1495
1534
  const browser = sharedBrowserHostConfig('copilot');
1496
1535
  return {
1497
1536
  command: COPILOT_BINARY,
1498
- args: ['--yolo', '--resume', sessionId, ...browser.args],
1537
+ // Issue #1399: see buildStartPlan's copilot branch for rationale.
1538
+ args: ['--yolo', '--output-format', 'json', '--resume', sessionId, ...browser.args],
1499
1539
  stdin: transformHeadlessFraimMessage(message, 'continue'),
1500
1540
  env: browser.env,
1501
1541
  };
@@ -1562,9 +1602,10 @@ function buildDirectStartPlan(hostId, message, sessionId) {
1562
1602
  }
1563
1603
  if (hostId === 'copilot') {
1564
1604
  // Direct (A/B) mode for Copilot: headless, no FRAIM MCP wiring.
1605
+ // Issue #1399: --output-format json for the same reason as buildStartPlan.
1565
1606
  return {
1566
1607
  command: COPILOT_BINARY,
1567
- args: ['--yolo'],
1608
+ args: ['--yolo', '--output-format', 'json'],
1568
1609
  stdin: DIRECT_PREAMBLE + message,
1569
1610
  };
1570
1611
  }
@@ -1608,9 +1649,10 @@ function buildDirectContinuePlan(hostId, sessionId, message) {
1608
1649
  }
1609
1650
  if (hostId === 'copilot') {
1610
1651
  // Direct continue mode for Copilot: resume session, no FRAIM MCP wiring.
1652
+ // Issue #1399: --output-format json for the same reason as buildStartPlan.
1611
1653
  return {
1612
1654
  command: COPILOT_BINARY,
1613
- args: ['--yolo', '--resume', sessionId],
1655
+ args: ['--yolo', '--output-format', 'json', '--resume', sessionId],
1614
1656
  stdin: DIRECT_PREAMBLE + message,
1615
1657
  };
1616
1658
  }
@@ -1723,31 +1765,69 @@ function parseHostLine(hostId, line) {
1723
1765
  if (hostId === 'antigravity') {
1724
1766
  return withSignal({ raw: trimmed });
1725
1767
  }
1726
- // GitHub Copilot CLI output: JSON stream where each event carries a `type`
1727
- // field. Known event shapes (from the agentic CLI stream):
1728
- // { "type": "session.started", "session_id": "..." } — session id
1729
- // { "type": "message", "role": "assistant", "content": "..." } reply text
1730
- // { "type": "turn.completed", "usage": { ... } } — token usage (same shape as Codex)
1731
- // For any JSON event not matching the above, signal scanning (seekMentoring,
1732
- // agent identity) still runs because withSignal is applied to every parsed result.
1733
- // Non-JSON lines from Copilot are treated as plain-text employee messages.
1768
+ // GitHub Copilot CLI output (issue #1399): Hub invokes Copilot with
1769
+ // --output-format json (see buildStartPlan's copilot branch), which
1770
+ // reports every event as one JSON object per line. Two shapes from the
1771
+ // original #531 integration are kept as defensive fallbacks never
1772
+ // confirmed against real traffic, but harmless to keep recognizing if a
1773
+ // future CLI version or mode emits them:
1774
+ // { "type": "session.started", "session_id": "..." }
1775
+ // { "type": "message", "role": "assistant", "content": "..." }
1776
+ // The real vocabulary below was captured via a live spike against the
1777
+ // installed CLI binary (see "Spike Findings" in
1778
+ // docs/rfcs/1399-copilot-host-chattiness-technical-design.md):
1779
+ // { "type": "assistant.message", "data": { "content": "...", "toolRequests": [...] } }
1780
+ // — the genuine reply. data.content is the message text; toolRequests
1781
+ // is metadata about an about-to-run tool call, not display text.
1782
+ // { "type": "result", "sessionId": "...", "exitCode": 0, "usage": {...} }
1783
+ // — terminal turn/session summary. This is the only place sessionId
1784
+ // was observed in a single-shot run (no early session-id event fired).
1785
+ // { "type": "tool.execution_start" | "tool.execution_complete", "data": {...} },
1786
+ // { "type": "model.tool_execution", "data": {...} }
1787
+ // — tool invocation and result. Diagnostic only, never a message.
1788
+ // seekMentoring/get_fraim_job/fraim_connect signal extraction (via
1789
+ // withSignal below) reads the call arguments off tool.execution_start's
1790
+ // data.arguments/data.mcpServerName/data.mcpToolName.
1791
+ // { "type": "assistant.message_delta" | "assistant.tool_call_delta", ...,
1792
+ // "ephemeral": true }
1793
+ // — streaming deltas. The CLI's own schema marks these ephemeral, an
1794
+ // explicit signal (not a heuristic) that they are safe to drop.
1795
+ // session.*/model.*/user.message/assistant.turn_*/assistant.idle
1796
+ // — setup, bookkeeping, and input-echo diagnostics.
1797
+ // Any other/unrecognized type, and any line that fails JSON.parse entirely,
1798
+ // fails CLOSED (raw only) — matching Codex's and Claude's existing
1799
+ // fail-closed default. This replaces the prior fail-OPEN catch-all that
1800
+ // promoted arbitrary unrecognized Copilot text (in practice, ~80% of a
1801
+ // real run's lines — the CLI's own tool-call/result terminal rendering) to
1802
+ // a manager-visible message.
1734
1803
  if (hostId === 'copilot') {
1804
+ let parsed;
1735
1805
  try {
1736
- const parsed = JSON.parse(trimmed);
1737
- if (parsed.type === 'session.started' && typeof parsed.session_id === 'string' && parsed.session_id.length > 0) {
1738
- return withSignal({ sessionId: parsed.session_id, raw: trimmed });
1739
- }
1740
- if (parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string') {
1741
- return withSignal({ message: parsed.content, raw: trimmed });
1742
- }
1743
- // All other JSON events: apply signal scanning and surface as raw.
1744
- return withSignal({ raw: trimmed });
1806
+ parsed = JSON.parse(trimmed);
1745
1807
  }
1746
1808
  catch {
1747
- // Non-JSON line from Copilot: treat as a plain-text employee message,
1748
- // same pattern as Gemini CLI's non-JSON output.
1749
- return withSignal({ message: trimmed, raw: trimmed });
1809
+ return withSignal({ raw: trimmed });
1810
+ }
1811
+ if (!parsed || typeof parsed !== 'object')
1812
+ return withSignal({ raw: trimmed });
1813
+ if (parsed.type === 'session.started' && typeof parsed.session_id === 'string' && parsed.session_id.length > 0) {
1814
+ return withSignal({ sessionId: parsed.session_id, raw: trimmed });
1815
+ }
1816
+ if (parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string') {
1817
+ return withSignal({ message: parsed.content, raw: trimmed });
1818
+ }
1819
+ if (parsed.type === 'assistant.message') {
1820
+ const content = typeof parsed.data?.content === 'string' ? parsed.data.content : undefined;
1821
+ return content ? withSignal({ message: content, raw: trimmed }) : withSignal({ raw: trimmed });
1750
1822
  }
1823
+ if (parsed.type === 'result') {
1824
+ const sessionId = typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0 ? parsed.sessionId : undefined;
1825
+ return withSignal({ sessionId, raw: trimmed });
1826
+ }
1827
+ // tool.execution_start/complete, model.tool_execution, the *_delta
1828
+ // events, and session/model/user/turn bookkeeping all fall here:
1829
+ // diagnostic-only, but still signal-scanned by withSignal above.
1830
+ return withSignal({ raw: trimmed });
1751
1831
  }
1752
1832
  try {
1753
1833
  const parsed = JSON.parse(trimmed);
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HubConnectorStatusStore = exports.HostConfigStore = exports.DeploymentStore = void 0;
40
+ exports.selfHealBrokenManagedAgents = selfHealBrokenManagedAgents;
40
41
  exports.configureFraimForHubAgent = configureFraimForHubAgent;
41
42
  exports.hubCommandVersion = hubCommandVersion;
42
43
  exports.buildOpenFileInvocation = buildOpenFileInvocation;
@@ -1699,6 +1700,35 @@ function hubAgentOption(hubId) {
1699
1700
  const frId = HUB_TO_FIRST_RUN_ID[hubId];
1700
1701
  return frId ? types_1.FIRST_RUN_AGENT_OPTIONS.find((o) => o.id === frId) : undefined;
1701
1702
  }
1703
+ const defaultSelfHealDeps = {
1704
+ checkHealth: agent_cli_health_checks_1.checkAgentCliHealthByCommand,
1705
+ install: (option, systemPath) => (0, managed_agent_install_1.installManagedAgent)(option, systemPath, { runProcess: hubRunProcess, commandVersion: hubCommandVersion }),
1706
+ invalidateCache: hosts_1.invalidateEmployeeDetectionCache,
1707
+ };
1708
+ async function selfHealBrokenManagedAgents(deps = defaultSelfHealDeps) {
1709
+ for (const hubId of ['claude', 'codex']) {
1710
+ try {
1711
+ const health = await deps.checkHealth(hubId);
1712
+ if (!health || health.status !== 'error')
1713
+ continue;
1714
+ const details = health.details;
1715
+ if (!details?.managedPath || details.managedVersion)
1716
+ continue;
1717
+ const option = hubAgentOption(hubId);
1718
+ if (!option || !option.installPackage)
1719
+ continue;
1720
+ console.warn(`[ai-hub] self-heal: ${option.label}'s FRAIM-managed install is broken (${health.message}) — attempting automatic reinstall`);
1721
+ const systemPath = (0, managed_agent_paths_1.stripManagedAgentBinDirsFromPath)(process.env.PATH);
1722
+ await deps.install({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath);
1723
+ deps.invalidateCache();
1724
+ const verified = await deps.checkHealth(hubId);
1725
+ console.warn(`[ai-hub] self-heal: ${option.label} reinstall ${verified?.status === 'error' ? 'did NOT fix it — manual reinstall needed' : 'succeeded'}`);
1726
+ }
1727
+ catch (error) {
1728
+ console.warn(`[ai-hub] self-heal check failed for ${hubId}:`, error instanceof Error ? error.message : String(error));
1729
+ }
1730
+ }
1731
+ }
1702
1732
  /**
1703
1733
  * Issue #747: after the Hub installs an agent CLI, run the `add-ide` command for that agent so the
1704
1734
  * FRAIM MCP (plus slash commands / rules) is wired into its config and its first run works.
@@ -2699,6 +2729,17 @@ class AiHubServer {
2699
2729
  void (0, hosts_1.detectEmployeesAsync)({ force: true }).catch((error) => {
2700
2730
  console.warn('[ai-hub] agent availability priming failed:', error?.message || error);
2701
2731
  });
2732
+ // Issue #1412: fire-and-forget self-heal of any broken FRAIM-managed CLI
2733
+ // install (e.g. an interrupted Claude Code/Codex update left a
2734
+ // non-executable launcher). Never blocks or fails startup. Skipped in
2735
+ // NODE_ENV=test so the test suite's many short-lived Hub instances never
2736
+ // trigger a real `npm install -g`, matching this file's existing
2737
+ // NODE_ENV-gated test-safety pattern (see hubCommandVersion/hubRunProcess).
2738
+ if (process.env.NODE_ENV !== 'test') {
2739
+ void selfHealBrokenManagedAgents().catch((error) => {
2740
+ console.warn('[ai-hub] self-heal pass failed:', error?.message || error);
2741
+ });
2742
+ }
2702
2743
  // Issue #578: rehydrate active scheduled deployments from disk. Test and preview
2703
2744
  // servers that inject a fake host but not a deployment store should not run the
2704
2745
  // user's real scheduled deployments from the default store.
@@ -6083,6 +6124,13 @@ class AiHubServer {
6083
6124
  if (reviewApprovalSystemEventText)
6084
6125
  current.events.push((0, hosts_1.createHubEvent)('system', reviewApprovalSystemEventText));
6085
6126
  current.events.push((0, hosts_1.createHubEvent)('system', 'No resumable session - starting a fresh agent turn from conversation context.'));
6127
+ // Issue #1373 (Defect 2): nextJobRecommendations can only be stale
6128
+ // here — it is only ever set at retrospective (issue #848), and a
6129
+ // job that just received coaching has not reached retrospective
6130
+ // again yet. Clear it at send-time so a coached Done job stops
6131
+ // offering next-job recommendations immediately, not only once the
6132
+ // resumed session's next seekMentoring signal arrives.
6133
+ current.nextJobRecommendations = null;
6086
6134
  });
6087
6135
  const startedFresh = this.runRegistry.get(run.id);
6088
6136
  if (startedFresh)
@@ -6139,6 +6187,9 @@ class AiHubServer {
6139
6187
  current.messages.push((0, hosts_1.createHubMessage)('manager', prepared.display || message, deliveryStatus));
6140
6188
  if (reviewApprovalSystemEventText)
6141
6189
  current.events.push((0, hosts_1.createHubEvent)('system', reviewApprovalSystemEventText));
6190
+ // Issue #1373 (Defect 2): see the no-session branch above for why
6191
+ // this can only be stale at coaching-send time.
6192
+ current.nextJobRecommendations = null;
6142
6193
  });
6143
6194
  const started = this.runRegistry.get(run.id);
6144
6195
  if (started)
@@ -6281,18 +6332,24 @@ class AiHubServer {
6281
6332
  messages: persistedConversation ? persistedMessagesForRun(persistedConversation) : [],
6282
6333
  events: resumeEvents,
6283
6334
  eventLogRefs: persistedConversation?.eventLogRefs || [],
6284
- // Only carry phase state forward when the prior run was interrupted mid-job
6285
- // (status !== 'completed'). A completed conversation is a finished run;
6286
- // resuming the session starts a fresh job, so the tracker must be blank.
6287
- currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
6288
- phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
6289
- phaseVisits: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseVisits || []) : [],
6335
+ // A manager can issue new instructions after a job was marked done.
6336
+ // That correction stays in the same conversation/job identity and keeps
6337
+ // its phase history so the tracker can show the job moving back to an
6338
+ // earlier phase instead of pretending this is unrelated fresh work.
6339
+ currentPhase: persistedRun?.currentPhase || null,
6340
+ phaseHistory: persistedRun?.phaseHistory || [],
6341
+ phaseVisits: persistedRun?.phaseVisits || [],
6290
6342
  totals: persistedRun?.totals || emptyTotals(),
6291
6343
  lastStatusChangeAt: now,
6292
- runDiscriminant: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
6344
+ runDiscriminant: persistedRun?.runDiscriminant || undefined,
6293
6345
  // Issue #1357: consult custom persona owner before falling back to catalog.
6294
6346
  personaKey: getCustomPersonaForJob(projectPath, jobId) ?? getHubPersonaForJob(jobId),
6295
6347
  continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
6348
+ // Issue #1373 (Defect 2): nextJobRecommendations are only ever set at
6349
+ // retrospective/completion (issue #848); a resumed run has not reached
6350
+ // retrospective, so this is always null here. Explicit for readability
6351
+ // and to make the invariant visible next to the Defect 1 fix above.
6352
+ nextJobRecommendations: null,
6296
6353
  };
6297
6354
  host_session_state_1.hostSessionState.applySession(run, { configuredAgentId: configuredAgent.id, baseHostId: hostId }, sessionId, { sourceRunId: run.id, status: resolvedHostSession?.status || 'suspect' });
6298
6355
  // Continue-turn message (FRAIM invocation for the job + instructions) plus
@@ -97,6 +97,27 @@ async function runAgentCliHealthCheck(cli) {
97
97
  : managedPath === ambientPath
98
98
  ? ambientVersion
99
99
  : probeVersion(managedPath);
100
+ // Issue #1412: a resolved file that fails to execute at all (e.g. an
101
+ // interrupted update left a non-executable fallback launcher) is a harder
102
+ // failure than drift — `getSystemCommandPath()` only proves the file
103
+ // exists, not that it can run. Without this check, a single broken install
104
+ // with nothing else on PATH to disagree with (ambientPath === managedPath,
105
+ // both versions null) falls straight through the mismatch check below and
106
+ // reads as "consistent". Check this before computing npm-global/drift
107
+ // details that don't matter once the CLI can't run at all.
108
+ const brokenPath = ambientPath && !ambientVersion
109
+ ? ambientPath
110
+ : managedPath && !managedVersion
111
+ ? managedPath
112
+ : null;
113
+ if (brokenPath) {
114
+ return {
115
+ status: 'error',
116
+ message: `${cli.label} is installed at ${brokenPath} but failed to run (\`${cli.command} --version\` produced no output). An interrupted or partial update may have left a non-functional launcher.`,
117
+ suggestion: `Reinstall ${cli.label}, then run "${cli.command} --version" again to confirm it's fixed.`,
118
+ details: { ambientPath, ambientVersion, managedPath, managedVersion },
119
+ };
120
+ }
100
121
  const npmGlobalBinDirs = (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)();
101
122
  const npmGlobalPath = npmGlobalBinDirs.length > 0
102
123
  ? (0, command_resolution_1.getSystemCommandPath)(cli.command, npmGlobalBinDirs.join(path_1.default.delimiter))
@@ -64,13 +64,17 @@ function buildSearchQuery(roleKey) {
64
64
  const profile = getHumanManagerProfile(roleKey);
65
65
  return `${profile.keywords.join(' OR ')} AND ${AI_MANAGER_QUERY_TERMS}`;
66
66
  }
67
+ function indefiniteArticle(phrase) {
68
+ return /^[aeiou]/i.test(phrase.trim()) ? 'an' : 'a';
69
+ }
67
70
  function buildJobDescription(roleKey) {
68
71
  const profile = getHumanManagerProfile(roleKey);
69
72
  const role = persona_hiring_1.PERSONA_HIRE_CATALOG[roleKey]?.role ?? 'AI Employee';
73
+ const article = indefiniteArticle(role);
70
74
  return [
71
- `${profile.humanTitle} — manager for an ${role}`,
75
+ `${profile.humanTitle} — manager for ${article} ${role}`,
72
76
  '',
73
- `You will manage an ${role} (an autonomous AI agent) and the humans around it, owning the outcomes it ships.`,
77
+ `You will manage ${article} ${role} (an autonomous AI agent) and the humans around it, owning the outcomes it ships.`,
74
78
  '',
75
79
  "What you'll do:",
76
80
  `- Write crisp specifications and acceptance criteria the ${role} can execute against.`,
@@ -278,7 +278,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
278
278
  ],
279
279
  protectedAliases: ['ai-engineering', 'agent-engineering', 'ai-agents'],
280
280
  defaultHireMode: 'job',
281
- lockCopy: 'Hire AIda to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
281
+ lockCopy: 'Hire AIDa to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
282
282
  }
283
283
  };
284
284
  const PROTECTED_JOB_TO_PERSONA = new Map();
@@ -20,8 +20,8 @@ exports.getPersonaHireAmountCents = getPersonaHireAmountCents;
20
20
  */
21
21
  exports.PERSONA_HIRE_CATALOG = {
22
22
  aida: {
23
- displayName: 'AIda',
24
- role: 'AI Engineer',
23
+ displayName: 'AIDa',
24
+ role: 'AI Developer',
25
25
  emoji: '\u{1F9E0}',
26
26
  gradient: 'linear-gradient(135deg, #4f46e5 0%, #06b6d4 50%, #10b981 100%)',
27
27
  blurb: 'Designs production AI agents, connects them to tools and data, and proves their behavior with evals before deployment.',
@@ -30,7 +30,7 @@ exports.PERSONA_HIRE_CATALOG = {
30
30
  },
31
31
  swen: {
32
32
  displayName: 'SWEn',
33
- role: 'AI Software Engineer',
33
+ role: 'Software Engineer',
34
34
  emoji: '💻',
35
35
  gradient: 'linear-gradient(135deg, #2563eb 0%, #06b6d4 100%)',
36
36
  blurb: 'Implements features, refactors code, and drives PR iteration to merge, reviewing design and code as a senior would.',
@@ -39,7 +39,7 @@ exports.PERSONA_HIRE_CATALOG = {
39
39
  },
40
40
  qasm: {
41
41
  displayName: 'QAsm',
42
- role: 'AI QA Engineer',
42
+ role: 'QA Engineer',
43
43
  emoji: '🛡️',
44
44
  gradient: 'linear-gradient(135deg, #10b981 0%, #14b8a6 100%)',
45
45
  blurb: 'Runs tests, assesses code and test quality, polishes UI, and drives bug bashes with evidence.',
@@ -48,7 +48,7 @@ exports.PERSONA_HIRE_CATALOG = {
48
48
  },
49
49
  sekhar: {
50
50
  displayName: 'SEChar',
51
- role: 'AI Security Engineer',
51
+ role: 'Security Engineer',
52
52
  emoji: '🔒',
53
53
  gradient: 'linear-gradient(135deg, #ef4444 0%, #f43f5e 100%)',
54
54
  blurb: 'Sets up AI-native security baselines, runs the findings command center, reviews changes for risk, and drives remediation to closure.',
@@ -57,7 +57,7 @@ exports.PERSONA_HIRE_CATALOG = {
57
57
  },
58
58
  sreya: {
59
59
  displayName: 'SREya',
60
- role: 'AI Site Reliability Engineer',
60
+ role: 'Site Reliability Engineer',
61
61
  emoji: '☁️',
62
62
  gradient: 'linear-gradient(135deg, #2563eb 0%, #10b981 100%)',
63
63
  blurb: 'Manages deployments, monitors uptime, optimizes cloud cost, and keeps infrastructure resilient and observable.',
@@ -66,7 +66,7 @@ exports.PERSONA_HIRE_CATALOG = {
66
66
  },
67
67
  sade: {
68
68
  displayName: 'SADE',
69
- role: 'AI Salesforce Developer',
69
+ role: 'Salesforce Developer',
70
70
  emoji: '☁️',
71
71
  gradient: 'linear-gradient(135deg, #0284c7 0%, #0369a1 100%)',
72
72
  blurb: 'Deploys Salesforce configuration from ServiceNow tickets, builds reports and dashboards, creates Flows from business requirements, audits org health, and manages users and data at scale.',
@@ -75,7 +75,7 @@ exports.PERSONA_HIRE_CATALOG = {
75
75
  },
76
76
  pam: {
77
77
  displayName: 'PaM',
78
- role: 'AI Product Manager',
78
+ role: 'Product Manager',
79
79
  emoji: '📋',
80
80
  gradient: 'linear-gradient(135deg, #8b5cf6 0%, #d946ef 100%)',
81
81
  blurb: 'Owns specs, PRDs, technical design, issue prep, and the path from idea to shippable artifact.',
@@ -84,7 +84,7 @@ exports.PERSONA_HIRE_CATALOG = {
84
84
  },
85
85
  huxley: {
86
86
  displayName: 'hUXley',
87
- role: 'AI UX / Brand Designer',
87
+ role: 'UX / Brand Designer',
88
88
  emoji: '🎨',
89
89
  gradient: 'linear-gradient(135deg, #ec4899 0%, #f472b6 100%)',
90
90
  blurb: 'Builds design systems, prototypes polished user-facing surfaces, and carries brand decisions into shipped product experiences.',
@@ -93,7 +93,7 @@ exports.PERSONA_HIRE_CATALOG = {
93
93
  },
94
94
  gautam: {
95
95
  displayName: 'GauTaM',
96
- role: 'AI GTM & Marketing Manager',
96
+ role: 'GTM & Marketing Manager',
97
97
  emoji: '📣',
98
98
  gradient: 'linear-gradient(135deg, #f97316 0%, #f59e0b 100%)',
99
99
  blurb: 'Defines marketing strategy, ships content, runs launches, and owns the brand voice in market.',
@@ -102,7 +102,7 @@ exports.PERSONA_HIRE_CATALOG = {
102
102
  },
103
103
  sam: {
104
104
  displayName: 'SAM',
105
- role: 'AI Sales Account Manager',
105
+ role: 'Sales Account Manager',
106
106
  emoji: '📈',
107
107
  gradient: 'linear-gradient(135deg, #059669 0%, #0d9488 100%)',
108
108
  blurb: 'Surfaces stalled deals, computes pipeline health, and drafts account-specific re-engagement proposals for your top at-risk opportunities.',
@@ -111,7 +111,7 @@ exports.PERSONA_HIRE_CATALOG = {
111
111
  },
112
112
  casey: {
113
113
  displayName: 'CaSey',
114
- role: 'AI Customer Success + Support',
114
+ role: 'Customer Success + Support',
115
115
  emoji: '💬',
116
116
  gradient: 'linear-gradient(135deg, #db2777 0%, #9333ea 100%)',
117
117
  blurb: 'Scores account churn risk and expansion potential, generates prioritized action plans for CSMs, triages support cases, and routes L1 cases through automated resolution.',
@@ -120,7 +120,7 @@ exports.PERSONA_HIRE_CATALOG = {
120
120
  },
121
121
  mona: {
122
122
  displayName: 'MONa',
123
- role: 'AI Finance Manager',
123
+ role: 'Finance Manager',
124
124
  emoji: '💰',
125
125
  gradient: 'linear-gradient(135deg, #10b981 0%, #f59e0b 100%)',
126
126
  blurb: 'Models revenue, tracks unit economics, builds financial forecasts, and owns the metrics that drive growth decisions.',
@@ -129,7 +129,7 @@ exports.PERSONA_HIRE_CATALOG = {
129
129
  },
130
130
  hari: {
131
131
  displayName: 'HaRi',
132
- role: 'AI HR Manager',
132
+ role: 'HR Manager',
133
133
  emoji: '👥',
134
134
  gradient: 'linear-gradient(135deg, #0d9488 0%, #059669 100%)',
135
135
  blurb: 'Manages onboarding, performance reviews, benefits analysis, payroll coordination, and HR business-partner advisory.',
@@ -138,7 +138,7 @@ exports.PERSONA_HIRE_CATALOG = {
138
138
  },
139
139
  ricardo: {
140
140
  displayName: 'RECardo',
141
- role: 'AI Recruiter',
141
+ role: 'Recruiter',
142
142
  emoji: '🤝',
143
143
  gradient: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
144
144
  blurb: 'Sources candidates, writes job descriptions, screens pipelines, and manages the hiring loop end-to-end.',
@@ -147,7 +147,7 @@ exports.PERSONA_HIRE_CATALOG = {
147
147
  },
148
148
  cela: {
149
149
  displayName: 'CELiA',
150
- role: 'AI Legal Counsel',
150
+ role: 'Legal Counsel',
151
151
  emoji: '⚖️',
152
152
  gradient: 'linear-gradient(135deg, #475569 0%, #6366f1 100%)',
153
153
  blurb: 'Drafts and reviews contracts, NDAs, patents, trademarks, and the SaaS legal stack.',
@@ -156,7 +156,7 @@ exports.PERSONA_HIRE_CATALOG = {
156
156
  },
157
157
  procella: {
158
158
  displayName: 'PROCella',
159
- role: 'AI Procurement Manager',
159
+ role: 'Procurement Manager',
160
160
  emoji: '📦',
161
161
  gradient: 'linear-gradient(135deg, #0f766e 0%, #7c3aed 100%)',
162
162
  blurb: 'Frames procurement strategy, sources suppliers, runs RFx packages, evaluates responses, and keeps purchases acceptance-gated.',
@@ -165,7 +165,7 @@ exports.PERSONA_HIRE_CATALOG = {
165
165
  },
166
166
  banke: {
167
167
  displayName: 'BANKe',
168
- role: 'AI Banking KYC Employee',
168
+ role: 'Banking KYC Employee',
169
169
  emoji: '\u{1F3E6}',
170
170
  gradient: 'linear-gradient(135deg, #0f766e 0%, #2563eb 100%)',
171
171
  blurb: 'Runs banking KYC cases with consent-aware evidence capture, decision receipts, and stable audit handoff artifacts.',
@@ -174,7 +174,7 @@ exports.PERSONA_HIRE_CATALOG = {
174
174
  },
175
175
  auditya: {
176
176
  displayName: 'AUDITya',
177
- role: 'AI Banking Auditor',
177
+ role: 'Banking Auditor',
178
178
  emoji: '\u{1F50E}',
179
179
  gradient: 'linear-gradient(135deg, #7c3aed 0%, #0f172a 100%)',
180
180
  blurb: 'Audits AI banking work by tracing decisions back to evidence, receipts, exceptions, and reusable audit reports.',
@@ -183,7 +183,7 @@ exports.PERSONA_HIRE_CATALOG = {
183
183
  },
184
184
  deidre: {
185
185
  displayName: 'DEIdre',
186
- role: 'AI Inclusion Leader',
186
+ role: 'Inclusion Leader',
187
187
  emoji: '🌍',
188
188
  gradient: 'linear-gradient(135deg, #9333ea 0%, #d946ef 100%)',
189
189
  blurb: 'Audits equity gaps, designs bias-aware AI governance, builds ERG toolkits, and creates inclusion-fluency programs.',
@@ -192,7 +192,7 @@ exports.PERSONA_HIRE_CATALOG = {
192
192
  },
193
193
  careena: {
194
194
  displayName: 'CAREEna',
195
- role: 'AI Career Coach',
195
+ role: 'Career Coach',
196
196
  emoji: '🎓',
197
197
  gradient: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)',
198
198
  blurb: 'Runs the candidate-side search loop: role sourcing, application execution, networking, interview prep, and close-stage offer strategy.',
@@ -201,7 +201,7 @@ exports.PERSONA_HIRE_CATALOG = {
201
201
  },
202
202
  ashley: {
203
203
  displayName: 'AshLey',
204
- role: 'AI Executive Assistant',
204
+ role: 'Executive Assistant',
205
205
  emoji: '📅',
206
206
  gradient: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
207
207
  blurb: 'Owns executive coordination, weekly operating reviews, and portfolio reporting across the workforce.',
@@ -210,7 +210,7 @@ exports.PERSONA_HIRE_CATALOG = {
210
210
  },
211
211
  mandy: {
212
212
  displayName: 'MANdy',
213
- role: 'AI Manager',
213
+ role: 'Manager',
214
214
  emoji: '🎯',
215
215
  gradient: 'linear-gradient(135deg, #7c3aed 0%, #4338ca 100%)',
216
216
  blurb: 'Plans the job sequence, runs sub-agents in parallel, coaches them through verification loops, and hands back a synthesized DRAFT for your approval.',
@@ -219,7 +219,7 @@ exports.PERSONA_HIRE_CATALOG = {
219
219
  },
220
220
  beza: {
221
221
  displayName: 'BeZa',
222
- role: 'AI Business Strategist',
222
+ role: 'Business Strategist',
223
223
  emoji: '🧭',
224
224
  gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
225
225
  blurb: 'Turns ideas into structured business plans, validates founder-market fit, and pressure-tests strategy.',
@@ -228,7 +228,7 @@ exports.PERSONA_HIRE_CATALOG = {
228
228
  },
229
229
  maestro: {
230
230
  displayName: 'MAESTRO',
231
- role: 'Full-Brained AI Employee',
231
+ role: 'Full-Brained Employee',
232
232
  emoji: '★',
233
233
  gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%)',
234
234
  blurb: 'One AI employee who can take a job from any function and ship it back with evidence. You set the direction. You sign off on what ships. Maestro does the work.',
@@ -260,7 +260,7 @@ exports.PERSONA_AVATAR_CATALOG = {
260
260
  procella: { seed: 'PROCELLA-procurement', bg: 'ccfbf1', style: 'notionists' },
261
261
  banke: { seed: 'BANKe-banking-kyc', bg: 'ccfbf1', style: 'notionists' },
262
262
  auditya: { seed: 'AUDITya-banking-audit', bg: 'e9d5ff', style: 'notionists' },
263
- aida: { seed: 'AIda-ai-engineer', bg: 'c7d2fe', style: 'notionists' },
263
+ aida: { seed: 'AIDa-ai-developer', bg: 'c7d2fe', style: 'notionists' },
264
264
  };
265
265
  function buildPersonaAvatarUrl(personaKey) {
266
266
  const avatar = exports.PERSONA_AVATAR_CATALOG[personaKey];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.287",
3
+ "version": "2.0.289",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "author": "Sid Mathur <sid.mathur@gmail.com>",
6
6
  "homepage": "https://github.com/mathursrus/FRAIM#readme",
@@ -210,7 +210,7 @@
210
210
  "electron-updater": "^6.8.9",
211
211
  "express": "^5.2.1",
212
212
  "extract-zip": "^2.0.1",
213
- "fraim": "2.0.287",
213
+ "fraim": "2.0.289",
214
214
  "mongodb": "^7.0.0",
215
215
  "node-cron": "4.2.1",
216
216
  "node-edge-tts": "^1.2.10",