fraim-hub 2.0.217 → 2.0.218

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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AiHubConfiguredAgentStore = void 0;
7
7
  exports.synthesizeDefaultConfiguredAgents = synthesizeDefaultConfiguredAgents;
8
8
  exports.checkConfiguredAgentAvailability = checkConfiguredAgentAvailability;
9
+ exports.checkConfiguredAgentReadiness = checkConfiguredAgentReadiness;
9
10
  exports.projectConfiguredAgent = projectConfiguredAgent;
10
11
  exports.resolveConfiguredAgentForHost = resolveConfiguredAgentForHost;
11
12
  exports.resolveConfiguredAgentEnv = resolveConfiguredAgentEnv;
@@ -78,7 +79,9 @@ function normalizeSetupScript(value) {
78
79
  }
79
80
  function synthesizeDefaultConfiguredAgents(employees) {
80
81
  const timestamp = '1970-01-01T00:00:00.000Z';
81
- return employees.map((employee) => ({
82
+ return employees
83
+ .filter((employee) => employee.available)
84
+ .map((employee) => ({
82
85
  id: `${employee.id}-default`,
83
86
  label: employee.label,
84
87
  description: 'Default local launch for this agent tool.',
@@ -161,6 +164,25 @@ function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
161
164
  }
162
165
  return { id: agent.id, label: agent.label, baseHostId: agent.baseHostId, enabled: agent.enabled, available: reasons.length === 0, reasons, warnings };
163
166
  }
167
+ function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
168
+ const check = checkConfiguredAgentAvailability(agent, employees, env);
169
+ if (!check.available || !agent.setupScript)
170
+ return check;
171
+ try {
172
+ resolveConfiguredAgentEnv(agent);
173
+ return check;
174
+ }
175
+ catch (error) {
176
+ return {
177
+ ...check,
178
+ available: false,
179
+ reasons: [
180
+ ...check.reasons,
181
+ error instanceof Error ? error.message : 'Configured agent setup script failed.',
182
+ ],
183
+ };
184
+ }
185
+ }
164
186
  function projectConfiguredAgent(agent, employees) {
165
187
  const check = checkConfiguredAgentAvailability(agent, employees);
166
188
  return {
@@ -244,7 +266,7 @@ function runSetupScript(setupScript) {
244
266
  env: captureEnv,
245
267
  });
246
268
  if (result.error || result.status !== 0) {
247
- const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
269
+ const detail = result.error?.message || `exit ${result.status}`;
248
270
  throw new Error(`Configured agent setup script failed: ${detail}`);
249
271
  }
250
272
  return filterSafeEnv(parseEnvLines(result.stdout || ''));
@@ -143,10 +143,13 @@ function getProtectedPersonaForHubJob(jobName) {
143
143
  return loadPersonaCapabilityModule()?.getProtectedPersonaForJob(jobName) ?? null;
144
144
  }
145
145
  const GENERIC_WORKER_PERSONA_KEY = 'fraimworker';
146
+ // Unprotected jobs default to mandy so unassigned work surfaces
147
+ // under MANdy's employee row rather than the generic FRAIMworker placeholder.
148
+ const DEFAULT_UNASSIGNED_PERSONA_KEY = 'mandy';
146
149
  function getHubPersonaForJob(jobName) {
147
150
  if (!jobName || jobName === '__freeform__')
148
151
  return null;
149
- return getProtectedPersonaForHubJob(jobName) ?? GENERIC_WORKER_PERSONA_KEY;
152
+ return getProtectedPersonaForHubJob(jobName) ?? DEFAULT_UNASSIGNED_PERSONA_KEY;
150
153
  }
151
154
  const FRAIM_INTERNAL_JOB_IDS = new Set([
152
155
  'contribute-to-fraim',
@@ -3053,7 +3056,7 @@ class AiHubServer {
3053
3056
  const agent = this.configuredAgentsForCurrentMachine(employees).find((entry) => entry.id === req.params.id);
3054
3057
  if (!agent)
3055
3058
  return res.status(404).json({ error: 'Configured agent not found.' });
3056
- return res.json((0, configured_agents_1.checkConfiguredAgentAvailability)(agent, employees));
3059
+ return res.json((0, configured_agents_1.checkConfiguredAgentReadiness)(agent, employees));
3057
3060
  });
3058
3061
  this.app.post('/api/ai-hub/runs', (req, res) => {
3059
3062
  try {
@@ -44,7 +44,6 @@ const fs_1 = __importDefault(require("fs"));
44
44
  const path_1 = __importDefault(require("path"));
45
45
  const ide_detector_1 = require("../setup/ide-detector");
46
46
  const mcp_config_generator_1 = require("../setup/mcp-config-generator");
47
- const codex_local_config_1 = require("../setup/codex-local-config");
48
47
  const claude_code_telemetry_1 = require("../setup/claude-code-telemetry");
49
48
  const script_sync_utils_1 = require("../utils/script-sync-utils");
50
49
  const mcp_server_registry_1 = require("../mcp/mcp-server-registry");
@@ -254,12 +253,6 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
254
253
  });
255
254
  }
256
255
  console.log(chalk_1.default.green(`✅ Updated ${configPath}`));
257
- // Handle IDE-specific local config (e.g., Codex needs project-level config)
258
- if (ide.configType === 'codex') {
259
- const localResult = (0, codex_local_config_1.ensureCodexLocalConfig)(process.cwd());
260
- const status = localResult.created ? 'Created' : localResult.updated ? 'Updated' : 'Verified';
261
- console.log(chalk_1.default.green(` ✅ ${status} local ${ide.name} config: ${localResult.path}`));
262
- }
263
256
  // Enable token telemetry for Claude Code via project-level settings
264
257
  if (ide.configType === 'claude-code') {
265
258
  (0, claude_code_telemetry_1.ensureClaudeCodeTelemetryEnv)();
@@ -4,34 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ensureCodexLocalConfig = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
7
  const path_1 = __importDefault(require("path"));
9
- const mcp_config_generator_1 = require("./mcp-config-generator");
10
- const escapeTomlString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
11
- const buildFraimCwdBlock = (projectRoot) => `
12
- [mcp_servers.fraim]
13
- cwd = "${escapeTomlString(projectRoot)}"
14
- `;
15
8
  const ensureCodexLocalConfig = (projectRoot) => {
16
- const codexDir = path_1.default.join(projectRoot, '.codex');
17
- const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
18
- if (!fs_1.default.existsSync(codexDir)) {
19
- fs_1.default.mkdirSync(codexDir, { recursive: true });
20
- }
21
- const hadExistingFile = fs_1.default.existsSync(codexConfigPath);
22
- const existingContent = hadExistingFile ? fs_1.default.readFileSync(codexConfigPath, 'utf8') : '';
23
- const generatedContent = buildFraimCwdBlock(projectRoot);
24
- const mergeResult = (0, mcp_config_generator_1.mergeTomlMCPServers)(existingContent, generatedContent, ['fraim']);
25
- const normalizedExisting = existingContent.replace(/\r\n/g, '\n');
26
- const normalizedMerged = mergeResult.content.replace(/\r\n/g, '\n');
27
- const shouldWrite = !hadExistingFile || normalizedExisting !== normalizedMerged;
28
- if (shouldWrite) {
29
- fs_1.default.writeFileSync(codexConfigPath, mergeResult.content);
30
- }
31
- return {
32
- path: codexConfigPath,
33
- created: !hadExistingFile,
34
- updated: shouldWrite && hadExistingFile
35
- };
9
+ const codexConfigPath = path_1.default.join(projectRoot, '.codex', 'config.toml');
10
+ // Codex resolves the active workspace from its process cwd. FRAIM's Codex
11
+ // MCP server belongs in machine-level config; project-local partial TOML can
12
+ // shadow a valid profile and break configured Hub launches.
13
+ return { path: codexConfigPath, created: false, updated: false };
36
14
  };
37
15
  exports.ensureCodexLocalConfig = ensureCodexLocalConfig;
@@ -8,10 +8,16 @@ exports.listPersonaCapabilityBundles = listPersonaCapabilityBundles;
8
8
  const persona_hiring_1 = require("./persona-hiring");
9
9
  const persona_catalog_routes_1 = require("../routes/persona-catalog-routes");
10
10
  exports.FREE_JOBS = new Set([
11
- 'create-architecture', 'project-scaffolding',
12
- 'blue-sky-brainstorming', 'codebase-analysis-and-ideation',
13
- 'domain-registration-research', 'google-workspace-setup', 'github-org-setup',
14
- 'contribute-to-fraim', 'file-fraim-issue', 'praise-fraim',
11
+ 'create-architecture',
12
+ 'project-scaffolding',
13
+ 'blue-sky-brainstorming',
14
+ 'codebase-analysis-and-ideation',
15
+ 'domain-registration-research',
16
+ 'google-workspace-setup',
17
+ 'github-org-setup',
18
+ 'contribute-to-fraim',
19
+ 'file-fraim-issue',
20
+ 'praise-fraim',
15
21
  ]);
16
22
  function isFreeJob(jobName) {
17
23
  return exports.FREE_JOBS.has(jobName);
@@ -39,8 +45,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
39
45
  beza: {
40
46
  personaKey: 'beza',
41
47
  bundleId: 'persona-beza-core',
42
- catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', 'business-plan-creation']),
43
- protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', 'community-funding-preparation', 'fundraising-prospect-discovery', 'investor-pitch-preparation', 'review-funding-preparation', 'advisory-board-development', 'advisor-interview', 'advisory-board-selection', 'blue-sky-brainstorming', 'domain-registration-research'],
48
+ catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', 'pricing-strategy-definition']),
49
+ protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', 'advisory-board-development', 'advisor-interview', 'advisory-board-selection', 'pricing-strategy-definition'],
44
50
  protectedAliases: ['business-strategy', 'company-strategy'],
45
51
  defaultHireMode: 'job',
46
52
  lockCopy: 'Hire BeZa to unlock business strategy work for this request.'
@@ -48,8 +54,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
48
54
  pam: {
49
55
  personaKey: 'pam',
50
56
  bundleId: 'persona-pam-core',
51
- catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', 'issue-preparation']),
52
- protectedJobs: ['feature-specification', 'technical-design', 'issue-preparation', 'implementation-design-review', 'issue-retrospective', 'work-completion', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
57
+ catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', 'project-plan-creation']),
58
+ protectedJobs: ['feature-specification', 'technical-design', 'experiment-tracking', 'project-plan-creation', 'implementation-feature-review', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
53
59
  protectedAliases: ['product-management', 'product-spec'],
54
60
  defaultHireMode: 'job',
55
61
  lockCopy: 'Hire PaM to unlock product-management work for this request.'
@@ -57,8 +63,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
57
63
  swen: {
58
64
  personaKey: 'swen',
59
65
  bundleId: 'persona-swen-core',
60
- catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'code-refactoring', 'pr-iteration']),
61
- protectedJobs: ['feature-implementation', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
66
+ catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'technical-design', 'code-refactoring']),
67
+ protectedJobs: ['feature-implementation', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
62
68
  protectedAliases: ['software-engineering', 'implementation'],
63
69
  defaultHireMode: 'job',
64
70
  lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
@@ -67,7 +73,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
67
73
  personaKey: 'qasm',
68
74
  bundleId: 'persona-qasm-core',
69
75
  catalogMetadata: buildCatalogMetadata('qasm', ['test-execution', 'browser-application-validation', 'ui-polish-validation']),
70
- protectedJobs: ['test-execution', 'browser-application-validation', 'ui-polish-validation', 'code-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'broken-windows-detection-and-remediation', 'iterative-quality-improvement', 'implementation-feature-review', 'accessibility-audit', 'api-testing', 'performance-benchmarking'],
76
+ protectedJobs: ['test-execution', 'browser-application-validation', 'ui-polish-validation', 'code-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'broken-windows-detection-and-remediation', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'performance-benchmarking'],
71
77
  protectedAliases: ['qa', 'quality-assurance'],
72
78
  defaultHireMode: 'job',
73
79
  lockCopy: 'Hire QAsm to unlock QA validation for this request.'
@@ -84,8 +90,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
84
90
  gautam: {
85
91
  personaKey: 'gautam',
86
92
  bundleId: 'persona-gautam-core',
87
- catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'ppc-campaign-management']),
88
- protectedJobs: ['pricing-strategy-definition', 'marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck'],
93
+ catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'domain-registration-research']),
94
+ protectedJobs: ['marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'domain-registration-research', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck', 'notebooklm-infographic', 'notebooklm-podcast', 'notebooklm-presentation', 'notebooklm-report', 'notebooklm-video-overview'],
89
95
  protectedAliases: ['gtm', 'marketing'],
90
96
  defaultHireMode: 'job',
91
97
  lockCopy: 'Hire GauTaM to unlock go-to-market and paid media work for this request.'
@@ -111,8 +117,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
111
117
  ashley: {
112
118
  personaKey: 'ashley',
113
119
  bundleId: 'persona-ashley-core',
114
- catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'weekly-operating-review']),
115
- protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'weekly-operating-review', 'send-thank-you-notes', 'send-newsletter', 'analyze-transcript', 'send-stakeholder-update'],
120
+ catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
121
+ protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'send-newsletter', 'send-thank-you-notes', 'analyze-transcript'],
116
122
  protectedAliases: ['executive-assistant', 'operations-assistant'],
117
123
  defaultHireMode: 'job',
118
124
  lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
@@ -120,8 +126,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
120
126
  mandy: {
121
127
  personaKey: 'mandy',
122
128
  bundleId: 'persona-mandy-core',
123
- catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', 'project-plan-creation', 'stakeholder-status-reporting']),
124
- protectedJobs: ['fully-delegate', 'delivery-governance-review', 'project-plan-creation', 'stakeholder-status-reporting', 'experiment-tracking', 'cross-functional-dependency-management', 'portfolio-impact-report'],
129
+ catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', 'stakeholder-status-reporting', 'weekly-operating-review']),
130
+ protectedJobs: ['fully-delegate', 'delivery-governance-review', 'issue-preparation', 'issue-retrospective', 'work-completion', 'stakeholder-status-reporting', 'send-stakeholder-update', 'weekly-operating-review', 'cross-functional-dependency-management', 'portfolio-impact-report'],
125
131
  protectedAliases: ['manager', 'team-lead', 'orchestrator'],
126
132
  defaultHireMode: 'job',
127
133
  lockCopy: 'Hire MANdy to unlock autonomous multi-role orchestration — MANdy plans the job sequence, runs sub-agents in parallel, coaches them through verification loops, and hands back a synthesized DRAFT for your approval.'
@@ -174,8 +180,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
174
180
  casey: {
175
181
  personaKey: 'casey',
176
182
  bundleId: 'persona-casey-core',
177
- catalogMetadata: buildCatalogMetadata('casey', ['support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation']),
178
- protectedJobs: ['crm-account-health-review', 'crm-case-resolution', 'customer-health-review', 'loyalty-program-management', 'survey-campaign-management', 'support-queue-management', 'support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation', 'user-survey-management'],
183
+ catalogMetadata: buildCatalogMetadata('casey', ['update-crm', 'support-case-resolution', 'support-system-operationalization']),
184
+ protectedJobs: ['crm-account-health-review', 'crm-case-resolution', 'customer-health-review', 'loyalty-program-management', 'survey-campaign-management', 'update-crm', 'support-queue-management', 'support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation', 'user-survey-management'],
179
185
  protectedAliases: ['customer-success', 'customer-support', 'csm', 'support'],
180
186
  defaultHireMode: 'job',
181
187
  lockCopy: 'Hire CaSey to unlock customer success and support work for this request.'
@@ -204,8 +210,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
204
210
  mona: {
205
211
  personaKey: 'mona',
206
212
  bundleId: 'persona-mona-core',
207
- catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review']),
208
- protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', 'aws-activate-credits-application', 'google-cloud-credits-application', 'microsoft-azure-credits-application', 'invoice-generation', 'business-banking-setup'],
213
+ catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', 'fundraising-prospect-discovery', 'investor-pitch-preparation']),
214
+ protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', 'aws-activate-credits-application', 'community-funding-preparation', 'fundraising-prospect-discovery', 'google-cloud-credits-application', 'investor-pitch-preparation', 'microsoft-azure-credits-application', 'review-funding-preparation', 'invoice-generation', 'business-banking-setup'],
209
215
  protectedAliases: ['finance', 'financial-modeling', 'fpa', 'bookkeeping'],
210
216
  defaultHireMode: 'job',
211
217
  lockCopy: 'Hire MONa to unlock financial modeling, FP&A, and tax strategy work for this request.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.217",
3
+ "version": "2.0.218",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -90,7 +90,7 @@
90
90
  "dotenv": "^16.4.7",
91
91
  "electron": "^41.2.2",
92
92
  "express": "^5.2.1",
93
- "fraim": "2.0.217",
93
+ "fraim": "2.0.218",
94
94
  "mongodb": "^7.0.0",
95
95
  "node-cron": "4.2.1",
96
96
  "node-edge-tts": "^1.2.10",
@@ -82,6 +82,7 @@ const state = {
82
82
  cpHighlightIndex: -1, // index into cpRows flat list
83
83
  cpRows: [], // [{type:'recent'|'job', job, instructions?}]
84
84
  lastRun: null, // {job, instructions, employeeId} for Cmd+Shift+R
85
+ configuredAgentCheckResults: {},
85
86
  // Issue #540 R10: pending run params captured when hire strip is shown.
86
87
  _hireStripPending: null,
87
88
  };
@@ -1803,9 +1804,13 @@ function getConversationPersona(conv) {
1803
1804
  return personaMap().get(conv.personaKey) || null;
1804
1805
  }
1805
1806
 
1807
+ // Unprotected jobs default to mandy so unassigned work surfaces
1808
+ // under MANdy's employee row rather than the generic FRAIMworker placeholder.
1809
+ const DEFAULT_UNASSIGNED_PERSONA_KEY = 'mandy';
1810
+
1806
1811
  function assignedPersonaKeyForJob(job) {
1807
1812
  if (!job || job.id === '__freeform__') return null;
1808
- return job.requiredPersonaKey != null ? job.requiredPersonaKey : GENERIC_WORKER_PERSONA_KEY;
1813
+ return job.requiredPersonaKey != null ? job.requiredPersonaKey : DEFAULT_UNASSIGNED_PERSONA_KEY;
1809
1814
  }
1810
1815
 
1811
1816
  function getEmployeeStatus(employeeId) {
@@ -6577,6 +6582,10 @@ function renderConfiguredAgentsPanel() {
6577
6582
  return;
6578
6583
  }
6579
6584
  for (const agent of agents) {
6585
+ const latestCheck = state.configuredAgentCheckResults[agent.id] || null;
6586
+ const renderedAvailable = latestCheck ? latestCheck.available !== false : agent.available !== false;
6587
+ const renderedEnabled = agent.enabled !== false;
6588
+ const renderedReasons = latestCheck?.reasons || agent.reasons || [];
6580
6589
  const card = document.createElement('div');
6581
6590
  card.className = 'configured-agent-card';
6582
6591
  card.dataset.testid = 'configured-agent-card';
@@ -6585,8 +6594,8 @@ function renderConfiguredAgentsPanel() {
6585
6594
  const title = document.createElement('strong');
6586
6595
  title.textContent = agent.label;
6587
6596
  const status = document.createElement('span');
6588
- status.className = 'configured-agent-status ' + (agent.available !== false && agent.enabled !== false ? 'ok' : 'warn');
6589
- status.textContent = agent.available !== false && agent.enabled !== false ? 'Ready' : 'Check setup';
6597
+ status.className = 'configured-agent-status ' + (renderedAvailable && renderedEnabled ? 'ok' : 'warn');
6598
+ status.textContent = renderedAvailable && renderedEnabled ? 'Ready' : 'Check setup';
6590
6599
  head.appendChild(title);
6591
6600
  head.appendChild(status);
6592
6601
 
@@ -6599,8 +6608,9 @@ function renderConfiguredAgentsPanel() {
6599
6608
  check.addEventListener('click', async () => {
6600
6609
  try {
6601
6610
  const result = await requestJson(`/api/ai-hub/configured-agents/${encodeURIComponent(agent.id)}/check`, { method: 'POST' });
6611
+ state.configuredAgentCheckResults[agent.id] = result;
6602
6612
  showStatus(result.available ? `${agent.label} is ready.` : `${agent.label}: ${(result.reasons || []).join(' ') || 'Check setup.'}`, !result.available);
6603
- await refreshConfiguredAgents();
6613
+ renderConfiguredAgentsPanel();
6604
6614
  } catch (err) {
6605
6615
  showStatus(err.message || 'Agent check failed.', true);
6606
6616
  }
@@ -6622,6 +6632,7 @@ function renderConfiguredAgentsPanel() {
6622
6632
  del.addEventListener('click', async () => {
6623
6633
  try {
6624
6634
  await requestJson(`/api/ai-hub/configured-agents/${encodeURIComponent(agent.id)}`, { method: 'DELETE' });
6635
+ delete state.configuredAgentCheckResults[agent.id];
6625
6636
  if (state.configuredAgentEditingId === agent.id) state.configuredAgentEditingId = null;
6626
6637
  await refreshConfiguredAgents();
6627
6638
  } catch (err) {
@@ -6641,7 +6652,9 @@ function renderConfiguredAgentsPanel() {
6641
6652
 
6642
6653
  const detail = document.createElement('div');
6643
6654
  detail.className = 'configured-agent-detail';
6644
- detail.textContent = agent.description || ((agent.reasons || []).join(' ') || 'Uses local Hub launch settings.');
6655
+ detail.textContent = (latestCheck && renderedReasons.length)
6656
+ ? renderedReasons.join(' ')
6657
+ : (agent.description || (renderedReasons.join(' ') || 'Uses local Hub launch settings.'));
6645
6658
 
6646
6659
  card.appendChild(head);
6647
6660
  card.appendChild(meta);
@@ -6766,6 +6779,7 @@ function buildConfiguredAgentForm(agent) {
6766
6779
  body: JSON.stringify(body),
6767
6780
  });
6768
6781
  state.configuredAgentEditingId = null;
6782
+ delete state.configuredAgentCheckResults[body.id || agent?.id];
6769
6783
  await refreshConfiguredAgents();
6770
6784
  showStatus(`${body.label || body.id} saved.`);
6771
6785
  } catch (err) {
@@ -6778,6 +6792,10 @@ function buildConfiguredAgentForm(agent) {
6778
6792
  async function refreshConfiguredAgents() {
6779
6793
  const configuredAgents = await requestJson('/api/ai-hub/configured-agents');
6780
6794
  if (state.bootstrap) state.bootstrap.configuredAgents = Array.isArray(configuredAgents) ? configuredAgents : [];
6795
+ const ids = new Set((state.bootstrap?.configuredAgents || []).map((agent) => agent.id));
6796
+ for (const id of Object.keys(state.configuredAgentCheckResults)) {
6797
+ if (!ids.has(id)) delete state.configuredAgentCheckResults[id];
6798
+ }
6781
6799
  renderConfiguredAgentsPanel();
6782
6800
  renderActiveEmployeeSelect(activeConversation());
6783
6801
  }
@@ -7646,12 +7664,18 @@ function tfLoadClientState() {
7646
7664
  }
7647
7665
 
7648
7666
  async function tfSaveProjectsToServer(projects, options) {
7649
- if (!state.projectPath) throw new Error('No active project path available.');
7667
+ // #891: on a true first run there is no recorded/active project (per #866 the
7668
+ // launch directory is never assumed as a project), so state.projectPath is empty.
7669
+ // Creating the FIRST project must still work: fall back to the project path being
7670
+ // created (the folder the manager just chose), passed explicitly by the caller.
7671
+ // state.projectPath stays authoritative whenever a project is already active.
7672
+ const projectPath = (options && options.projectPath) || state.projectPath;
7673
+ if (!projectPath) throw new Error('No active project path available.');
7650
7674
  const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
7651
7675
  const payload = await requestJson('/api/ai-hub/projects', {
7652
7676
  method: 'PUT',
7653
7677
  headers: { 'Content-Type': 'application/json' },
7654
- body: JSON.stringify({ projectPath: state.projectPath, projects: Array.isArray(projects) ? projects : [], reviveRemovedPaths }),
7678
+ body: JSON.stringify({ projectPath, projects: Array.isArray(projects) ? projects : [], reviveRemovedPaths }),
7655
7679
  });
7656
7680
  tfAdoptServerProjects(payload && payload.projects);
7657
7681
  return payload;
@@ -11430,7 +11454,7 @@ async function tfCreateProject() {
11430
11454
  };
11431
11455
  const removedKey = tfCanonicalProjectPath(folderPath);
11432
11456
  try {
11433
- await tfSaveProjectsToServer([...(tf.projects || []), project], { reviveRemovedPaths: [folderPath] });
11457
+ await tfSaveProjectsToServer([...(tf.projects || []), project], { reviveRemovedPaths: [folderPath], projectPath: state.projectPath || folderPath });
11434
11458
  tf.removedPaths.delete(removedKey);
11435
11459
  } catch (error) {
11436
11460
  showStatus('Could not create project: ' + (error && error.message ? error.message : 'request failed.'), true);
@@ -1437,6 +1437,7 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
1437
1437
  padding: 8px 12px;
1438
1438
  width: 100%;
1439
1439
  flex: 1 1 100%;
1440
+ overflow-x: auto;
1440
1441
  }
1441
1442
 
1442
1443
  .thread-surface {
@@ -3039,7 +3040,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
3039
3040
  .ptb-dismiss { font-size: 13px; color: var(--muted); background: none; border: 1px solid var(--line, rgba(0,0,0,.1)); border-radius: 6px; padding: 6px 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
3040
3041
  .ptb-dismiss:hover { color: var(--text); border-color: var(--muted); }
3041
3042
  /* #521: project-onboarding input modal (manager's direction before a run). */
3042
- .obi-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; font: inherit; font-size: 14px; line-height: 1.5; padding: 11px 13px; border: 1px solid var(--line, #e2e2e2); border-radius: 10px; background: var(--surface, #fff); color: var(--ink, #1a1a1a); }
3043
+ .obi-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; font: inherit; font-size: 14px; line-height: 1.5; padding: 11px 13px; border: 1px solid var(--line, #e2e2e2); border-radius: 10px; background: var(--surface, #fff); color: var(--text); }
3043
3044
  .obi-input:focus { outline: none; border-color: var(--accent, #1f6feb); }
3044
3045
  .obi-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; }
3045
3046
  .obi-cancel { font-size: 14px; font-weight: 500; color: var(--muted, #666); background: none; border: none; padding: 10px 14px; cursor: pointer; }
@@ -3623,7 +3624,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
3623
3624
  font-size: 13px;
3624
3625
  font-weight: 600;
3625
3626
  text-align: left;
3626
- color: var(--ink, #1a1a1a);
3627
+ color: var(--text);
3627
3628
  background: var(--surface, #fff);
3628
3629
  border: 1px solid var(--line, #e2e2e2);
3629
3630
  border-radius: 8px;
@@ -3660,7 +3661,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
3660
3661
  padding: 8px 10px;
3661
3662
  font-size: 13px;
3662
3663
  text-align: left;
3663
- color: var(--ink, #1a1a1a);
3664
+ color: var(--text);
3664
3665
  background: transparent;
3665
3666
  border: none;
3666
3667
  border-radius: 7px;