fraim 2.0.304 → 2.0.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -313,9 +313,8 @@ Use these defaults:
313
313
  - `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
314
314
  - `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
315
315
  - `test-authoring` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
316
- - `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
317
- - `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
318
- - `implementation-design-review` when you need to verify the code matches the approved technical design.
316
+ - `iterative-quality-improvement` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
317
+ - `independent-implementation-review` when you need an independent sign-off that the delivered behavior matches both the approved technical design and the feature spec.
319
318
  - `issue-retrospective` after the work is complete and you want durable learnings captured.
320
319
 
321
320
  Typical path for a larger feature:
@@ -293,8 +293,11 @@ const runSync = async (options) => {
293
293
  });
294
294
  if (result.success) {
295
295
  // Local-dev sync bypasses Layer 1 (machine-level sync) below, but scripts
296
- // still need to land in ~/.fraim/scripts/ or the reported count is a lie.
296
+ // and docs still need to land in ~/.fraim/ or the reported counts are a lie.
297
297
  await (0, remote_sync_1.syncScriptsToUserDir)(localRegistryFiles);
298
+ await (0, remote_sync_1.syncDocsToUserDir)(localRegistryFiles);
299
+ const { ensureUserLevelDependencies: ensureLocalUserLevelDependencies } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
300
+ ensureLocalUserLevelDependencies();
298
301
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
299
302
  const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
300
303
  removeLegacyVersionFromConfig(fraimDir);
@@ -354,6 +357,12 @@ const runSync = async (options) => {
354
357
  if (scriptsSynced > 0) {
355
358
  console.log(chalk_1.default.green(` Synced ${scriptsSynced} scripts to ~/.fraim/scripts/`));
356
359
  }
360
+ // 1b2. Sync docs to ~/.fraim/docs/ (Issue #1508: machine-level docs, e.g.
361
+ // TROUBLESHOOTING.md, must exist on disk the same way scripts do)
362
+ const docsSynced = await (0, remote_sync_1.syncDocsToUserDir)(registryFiles);
363
+ if (docsSynced > 0) {
364
+ console.log(chalk_1.default.green(` Synced ${docsSynced} docs to ~/.fraim/docs/`));
365
+ }
357
366
  // 1c. Refresh org home
358
367
  await refreshOrgCache(remoteUrl, apiKey);
359
368
  // 1d. Refresh manager home
@@ -361,9 +370,12 @@ const runSync = async (options) => {
361
370
  // 1e. Refresh MCP proxy launcher
362
371
  const { ensureFraimMcpLatestLauncher } = await Promise.resolve().then(() => __importStar(require('../mcp/fraim-mcp-latest-launcher')));
363
372
  ensureFraimMcpLatestLauncher();
364
- // 1f. Ensure user-level directories
365
- const { ensureUserLevelDirectories } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
373
+ // 1f. Ensure user-level directories and runtime dependencies (Issue #1508:
374
+ // synced scripts like the DOCX review layer require npm deps such as
375
+ // adm-zip; a plain sync must install them, not just create directories)
376
+ const { ensureUserLevelDirectories, ensureUserLevelDependencies } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
366
377
  ensureUserLevelDirectories();
378
+ ensureUserLevelDependencies();
367
379
  // 1g. Write sync metadata
368
380
  writeSyncMetadata('remote', remoteUrl);
369
381
  console.log(chalk_1.default.green('✅ Machine-level sync complete.'));
@@ -12,8 +12,73 @@ exports.getScriptsChecks = getScriptsChecks;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const os_1 = __importDefault(require("os"));
15
+ const module_1 = __importDefault(require("module"));
15
16
  const child_process_1 = require("child_process");
16
17
  const SCRIPTS_DIR = path_1.default.join(os_1.default.homedir(), '.fraim', 'scripts');
18
+ // Conservative static-require extractor: only matches literal
19
+ // require('name') / require("name") calls, not dynamic requires or imports.
20
+ const STATIC_REQUIRE_PATTERN = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
21
+ /**
22
+ * Recursively list .js/.cjs files under a directory.
23
+ */
24
+ function listNodeScripts(dir) {
25
+ const results = [];
26
+ let entries;
27
+ try {
28
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
29
+ }
30
+ catch {
31
+ return results;
32
+ }
33
+ for (const entry of entries) {
34
+ const fullPath = path_1.default.join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ // Skip vendored dependency trees. A script that ships its own bundled
37
+ // library (e.g. ~/.fraim/scripts/lib/node_modules/@scope/pkg/...) is
38
+ // not itself a FRAIM script requiring dependency resolution — it IS a
39
+ // resolved dependency. Recursing into it produces false-positive
40
+ // warnings about that library's own devDependency-only files (e.g. a
41
+ // benchmark/test file requiring a module never meant to be installed
42
+ // under ~/.fraim/node_modules). Confirmed against a real machine: see
43
+ // issue #1508 manual validation.
44
+ if (entry.name === 'node_modules')
45
+ continue;
46
+ results.push(...listNodeScripts(fullPath));
47
+ }
48
+ else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
49
+ results.push(fullPath);
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+ /**
55
+ * Extract external (non-relative, non-absolute, non-builtin) module names
56
+ * statically required by a script, using its own directory for resolution.
57
+ */
58
+ function extractExternalRequires(scriptPath) {
59
+ let content;
60
+ try {
61
+ content = fs_1.default.readFileSync(scriptPath, 'utf8');
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ const modules = new Set();
67
+ let match;
68
+ STATIC_REQUIRE_PATTERN.lastIndex = 0;
69
+ while ((match = STATIC_REQUIRE_PATTERN.exec(content)) !== null) {
70
+ const moduleName = match[1];
71
+ if (moduleName.startsWith('.') ||
72
+ moduleName.startsWith('/') ||
73
+ /^[a-zA-Z]:[\\/]/.test(moduleName) ||
74
+ moduleName.startsWith('node:') ||
75
+ module_1.default.builtinModules.includes(moduleName)) {
76
+ continue;
77
+ }
78
+ modules.add(moduleName);
79
+ }
80
+ return Array.from(modules);
81
+ }
17
82
  /**
18
83
  * Check if scripts directory exists
19
84
  */
@@ -211,6 +276,73 @@ function checkPythonAvailability() {
211
276
  }
212
277
  };
213
278
  }
279
+ /**
280
+ * Check that synced Node scripts can resolve their external runtime
281
+ * dependencies (e.g. `require('adm-zip')`) from their own directory.
282
+ *
283
+ * Distinguishes delivery (the script file exists) from executability (the
284
+ * script's require statements actually resolve). Issue #1508: `doctor`
285
+ * reported the DOCX review scripts healthy while all six threw
286
+ * `Cannot find module 'adm-zip'` at require time.
287
+ *
288
+ * Does not execute the scripts — only statically parses `require(...)`
289
+ * calls and resolves them the way Node would from the script's directory.
290
+ */
291
+ function checkNodeScriptDependencies() {
292
+ return {
293
+ name: 'Node script dependencies available',
294
+ category: 'scripts',
295
+ critical: false,
296
+ run: async () => {
297
+ if (!fs_1.default.existsSync(SCRIPTS_DIR)) {
298
+ return {
299
+ status: 'passed',
300
+ message: 'No scripts synced — Node dependency check skipped'
301
+ };
302
+ }
303
+ const scripts = listNodeScripts(SCRIPTS_DIR);
304
+ if (scripts.length === 0) {
305
+ return {
306
+ status: 'passed',
307
+ message: 'No Node scripts synced — Node dependency check skipped'
308
+ };
309
+ }
310
+ const unresolved = {};
311
+ for (const scriptPath of scripts) {
312
+ const externalModules = extractExternalRequires(scriptPath);
313
+ const relativeScriptPath = path_1.default.relative(SCRIPTS_DIR, scriptPath);
314
+ for (const moduleName of externalModules) {
315
+ try {
316
+ require.resolve(moduleName, { paths: [path_1.default.dirname(scriptPath)] });
317
+ }
318
+ catch {
319
+ if (!unresolved[moduleName])
320
+ unresolved[moduleName] = [];
321
+ unresolved[moduleName].push(relativeScriptPath);
322
+ }
323
+ }
324
+ }
325
+ const unresolvedModules = Object.keys(unresolved);
326
+ if (unresolvedModules.length === 0) {
327
+ return {
328
+ status: 'passed',
329
+ message: `All Node script dependencies resolve (${scripts.length} scripts checked)`,
330
+ details: { scriptCount: scripts.length }
331
+ };
332
+ }
333
+ const summary = unresolvedModules
334
+ .map((moduleName) => `${moduleName} (${unresolved[moduleName].join(', ')})`)
335
+ .join('; ');
336
+ return {
337
+ status: 'warning',
338
+ message: `Unresolved Node script dependencies: ${summary}`,
339
+ suggestion: 'Run fraim sync to install user-level runtime dependencies.',
340
+ command: 'fraim sync',
341
+ details: { unresolved }
342
+ };
343
+ }
344
+ };
345
+ }
214
346
  /**
215
347
  * Get all scripts checks
216
348
  */
@@ -219,6 +351,7 @@ function getScriptsChecks() {
219
351
  checkScriptsDirectoryExists(),
220
352
  checkScriptsSynced(),
221
353
  checkScriptsExecutable(),
354
+ checkNodeScriptDependencies(),
222
355
  checkPythonAvailability()
223
356
  ];
224
357
  }
@@ -69,6 +69,7 @@ const script_sync_utils_1 = require("../utils/script-sync-utils");
69
69
  */
70
70
  const USER_LEVEL_RUNTIME_DEPS = {
71
71
  'node-edge-tts': '*', // used by scripts/author-audio.js
72
+ 'adm-zip': '^0.6.0', // used by scripts/communication/*.js (DOCX review layer) — matches root/packages/* manifests
72
73
  };
73
74
  /**
74
75
  * Ensure the user-level FRAIM directory structure exists.
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.SYNCED_CONTENT_BANNER_MARKER = void 0;
15
15
  exports.fetchRegistryFiles = fetchRegistryFiles;
16
16
  exports.syncScriptsToUserDir = syncScriptsToUserDir;
17
+ exports.syncDocsToUserDir = syncDocsToUserDir;
17
18
  exports.syncFromRemote = syncFromRemote;
18
19
  const axios_1 = __importDefault(require("axios"));
19
20
  const fs_1 = require("fs");
@@ -188,32 +189,51 @@ async function fetchRegistryFiles(remoteUrl, apiKey) {
188
189
  return response.data.files || [];
189
190
  }
190
191
  /**
191
- * Sync script files to the user-level ~/.fraim/scripts/ directory.
192
- * Extracted from syncFromRemote so it can run without a project root.
192
+ * Sync a registry file subset (filtered by `type`) into a machine-level
193
+ * `~/.fraim/<subdir>/` directory: clean that directory, then write each
194
+ * matching file. Shared by syncScriptsToUserDir and syncDocsToUserDir so the
195
+ * clean-then-write and path-safety logic exists in exactly one place.
193
196
  */
194
- async function syncScriptsToUserDir(files) {
195
- const scriptFiles = files.filter(f => f.type === 'script');
196
- if (scriptFiles.length === 0)
197
+ async function syncTypedFilesToUserDir(files, type, subdir, label) {
198
+ const matchingFiles = files.filter(f => f.type === type);
199
+ if (matchingFiles.length === 0)
197
200
  return 0;
198
201
  const userDir = (0, script_sync_utils_1.getUserFraimDir)();
199
- const scriptsDir = (0, path_1.join)(userDir, 'scripts');
200
- if (!(0, fs_1.existsSync)(scriptsDir)) {
201
- (0, fs_1.mkdirSync)(scriptsDir, { recursive: true });
202
+ const targetDir = (0, path_1.join)(userDir, subdir);
203
+ if (!(0, fs_1.existsSync)(targetDir)) {
204
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
202
205
  }
203
- cleanDirectory(scriptsDir, (candidatePath) => {
204
- if ((0, path_1.resolve)(candidatePath) !== (0, path_1.resolve)(scriptsDir)) {
205
- assertPathInsideDirectory(scriptsDir, candidatePath, 'script directory');
206
+ cleanDirectory(targetDir, (candidatePath) => {
207
+ if ((0, path_1.resolve)(candidatePath) !== (0, path_1.resolve)(targetDir)) {
208
+ assertPathInsideDirectory(targetDir, candidatePath, `${label} directory`);
206
209
  }
207
210
  });
208
- for (const file of scriptFiles) {
209
- const { filePath } = resolveUserRegistryFile(scriptsDir, file.path, 'script file');
211
+ for (const file of matchingFiles) {
212
+ const { filePath } = resolveUserRegistryFile(targetDir, file.path, `${label} file`);
210
213
  const fileDir = (0, path_1.dirname)(filePath);
211
214
  if (!(0, fs_1.existsSync)(fileDir)) {
212
215
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
213
216
  }
214
217
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
215
218
  }
216
- return scriptFiles.length;
219
+ return matchingFiles.length;
220
+ }
221
+ /**
222
+ * Sync script files to the user-level ~/.fraim/scripts/ directory.
223
+ * Extracted from syncFromRemote so it can run without a project root.
224
+ */
225
+ async function syncScriptsToUserDir(files) {
226
+ return syncTypedFilesToUserDir(files, 'script', 'scripts', 'script');
227
+ }
228
+ /**
229
+ * Sync docs files to the user-level ~/.fraim/docs/ directory.
230
+ * Mirrors syncScriptsToUserDir so machine-level docs (e.g. TROUBLESHOOTING.md)
231
+ * are materialized on disk the same way machine-level scripts are.
232
+ * Issue #1508: agent guidance promises ~/.fraim/docs/TROUBLESHOOTING.md exists
233
+ * after sync, but only project-level fraim/docs/ was ever written.
234
+ */
235
+ async function syncDocsToUserDir(files) {
236
+ return syncTypedFilesToUserDir(files, 'docs', 'docs', 'docs');
217
237
  }
218
238
  /**
219
239
  * Sync jobs and scripts from remote FRAIM server
@@ -53,8 +53,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
53
53
  pam: {
54
54
  personaKey: 'pam',
55
55
  bundleId: 'persona-pam-core',
56
- catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'project-plan-creation', 'implementation-feature-review']),
57
- protectedJobs: ['feature-specification', 'send-newsletter', 'send-thank-you-notes', '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'],
56
+ catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'project-plan-creation', 'independent-implementation-review']),
57
+ protectedJobs: ['feature-specification', 'send-newsletter', 'send-thank-you-notes', 'experiment-tracking', 'project-plan-creation', 'independent-implementation-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'],
58
58
  protectedAliases: ['product-management', 'product-spec'],
59
59
  defaultHireMode: 'job',
60
60
  lockCopy: 'Hire PaM to unlock product-management work for this request.'
@@ -63,7 +63,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
63
63
  personaKey: 'swen',
64
64
  bundleId: 'persona-swen-core',
65
65
  catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'technical-design', 'code-refactoring']),
66
- protectedJobs: ['feature-implementation', 'technical-design', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', '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', 'application-replication-workflow'],
66
+ protectedJobs: ['feature-implementation', 'technical-design', 'code-refactoring', 'large-refactor-plan', 'pr-iteration', 'mobile-app-development', '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', 'application-replication-workflow'],
67
67
  protectedAliases: ['software-engineering', 'implementation'],
68
68
  defaultHireMode: 'job',
69
69
  lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
@@ -71,8 +71,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
71
71
  qasm: {
72
72
  personaKey: 'qasm',
73
73
  bundleId: 'persona-qasm-core',
74
- catalogMetadata: buildCatalogMetadata('qasm', ['test-authoring', 'browser-application-validation', 'ui-polish-validation']),
75
- protectedJobs: ['test-authoring', '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'],
74
+ catalogMetadata: buildCatalogMetadata('qasm', ['test-authoring', 'iterative-quality-improvement', 'ui-polish-validation']),
75
+ protectedJobs: ['test-authoring', 'ui-polish-validation', 'codebase-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'performance-benchmarking'],
76
76
  protectedAliases: ['qa', 'quality-assurance'],
77
77
  defaultHireMode: 'job',
78
78
  lockCopy: 'Hire QAsm to unlock QA validation for this request.'
@@ -107,11 +107,11 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
107
107
  sekhar: {
108
108
  personaKey: 'sekhar',
109
109
  bundleId: 'persona-sekhar-core',
110
- catalogMetadata: buildCatalogMetadata('sekhar', ['security-review', 'ai-native-security-setup', 'security-findings-command-center']),
111
- protectedJobs: ['security-review', 'ai-native-security-setup', 'security-findings-command-center', 'vulnerability-triage-and-remediation', 'compliance-review', 'compliance-continuous-monitoring', 'compliance-policy-authoring', 'compliance-questionnaire-response', 'compliance-requirements-detection', 'generate-audit-evidence', 'regulation-evidence-management', 'soc2-evidence-management'],
110
+ catalogMetadata: buildCatalogMetadata('sekhar', ['security-review', 'security-baseline-setup', 'vulnerability-triage-and-remediation']),
111
+ protectedJobs: ['security-review', 'security-baseline-setup', 'vulnerability-triage-and-remediation', 'compliance-review', 'compliance-continuous-monitoring', 'compliance-policy-authoring', 'compliance-questionnaire-response', 'compliance-requirements-detection', 'generate-audit-evidence', 'regulation-evidence-management', 'soc2-evidence-management'],
112
112
  protectedAliases: ['security', 'appsec'],
113
113
  defaultHireMode: 'job',
114
- lockCopy: 'Hire SEChar to unlock security setup, review, and findings-command work for this request.'
114
+ lockCopy: 'Hire SEChar to unlock security setup, review, and remediation work for this request.'
115
115
  },
116
116
  travis: {
117
117
  personaKey: 'travis',
@@ -38,7 +38,7 @@ exports.QUALITY_REGISTRY = {
38
38
  'business-plan-creation': { stage: 'business-strategy', enforced: false },
39
39
  'branding-quality-audit': { stage: 'branding', enforced: true, telemetryKind: 'score' },
40
40
  // Product Quality
41
- 'code-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
41
+ 'codebase-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
42
42
  // UI/UX Quality
43
43
  'ui-quality-assessment': { stage: 'ui-ux-quality', enforced: true, telemetryKind: 'score' },
44
44
  // Test Quality
@@ -151,7 +151,7 @@ const QUALITY_SCORE_DIMENSIONS = {
151
151
  'identityExpressiveness',
152
152
  'governanceReadiness'
153
153
  ],
154
- 'code-quality-assessment': [
154
+ 'codebase-quality-assessment': [
155
155
  'typeSafety',
156
156
  'errorHandling',
157
157
  'architecture',
@@ -5,7 +5,7 @@ const crypto_1 = require("crypto");
5
5
  const STAGE_JOBS = [
6
6
  { name: 'brainstorming', category: 'brainstorming', jobs: ['idea-exploration', 'opportunity-mapping'] },
7
7
  { name: 'customer-development', category: 'customer-development', jobs: ['interview-preparation', 'process-interview-notes', 'review-customer-development'] },
8
- { name: 'product-building', category: 'product-building', jobs: ['feature-specification', 'technical-design', 'feature-implementation', 'pr-iteration'] },
8
+ { name: 'product-building', category: 'product-building', jobs: ['feature-specification', 'technical-design', 'feature-implementation'] },
9
9
  { name: 'fundraising', category: 'fundraising', jobs: ['investor-outreach', 'deck-iteration'] },
10
10
  { name: 'gtm', category: 'gtm', jobs: ['gtm-plan', 'channel-tests'] },
11
11
  { name: 'marketing', category: 'marketing', jobs: ['brand-creation', 'whitepaper-thought-leadership'] },
@@ -103,7 +103,7 @@ async function seedDemoDataForUser(dbService, userId) {
103
103
  { category: 'customer-development', jobName: 'process-interview-notes' },
104
104
  { category: 'business-strategy', jobName: 'review-business-strategy' },
105
105
  { category: 'branding', jobName: 'branding-quality-audit' },
106
- { category: 'product-quality', jobName: 'code-quality-assessment' },
106
+ { category: 'product-quality', jobName: 'codebase-quality-assessment' },
107
107
  { category: 'ui-ux-quality', jobName: 'ui-quality-assessment' },
108
108
  { category: 'test-quality', jobName: 'test-quality-assessment' },
109
109
  { category: 'security', jobName: 'security-review' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.304",
3
+ "version": "2.0.306",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
58
- const whatHappened = document.createElement('pre');
58
+ const whatHappened = document.createElement('div');
59
59
  whatHappened.className = 'what-happened';
60
60
  whatHappened.setAttribute('data-testid', 'error-what-happened');
61
61
  whatHappened.textContent = visible;
@@ -584,6 +584,10 @@
584
584
  const existing = modal.querySelector('[data-testid="error-frame"]');
585
585
  if (existing) existing.remove();
586
586
  if (!window.FraimErrorFrame || typeof window.FraimErrorFrame.render !== 'function') return;
587
+ const status = modal.querySelector('[data-testid="agent-install-status"]');
588
+ if (status) status.remove();
589
+ const actions = modal.querySelector('.install-actions');
590
+ if (actions) actions.remove();
587
591
  const frame = window.FraimErrorFrame.render({
588
592
  whatTried: `We tried to set up ${opt.label}.`,
589
593
  whatHappened: message,
@@ -598,10 +602,9 @@
598
602
  } else if (action.id === 'alternative') {
599
603
  closeButton.click();
600
604
  } else if (action.id === 'manual') {
601
- const status = modal.querySelector('[data-testid="agent-install-status"]');
602
- if (status) {
603
- status.textContent = 'Run npx fraim add-ide for manual setup, then return here when your local agent is ready.';
604
- status.removeAttribute('data-tone');
605
+ const happened = modal.querySelector('[data-testid="error-what-happened"]');
606
+ if (happened) {
607
+ happened.textContent = 'Run npx fraim add-ide for manual setup, then return here when your local agent is ready.';
605
608
  }
606
609
  }
607
610
  });
@@ -357,15 +357,16 @@ body {
357
357
  padding: 10px 12px;
358
358
  }
359
359
  .error-frame .what-happened {
360
- background: #0d1410;
361
- color: #f1c0c0;
362
- font-family: "JetBrains Mono", "Cascadia Code", Consolas, monospace;
363
- font-size: 12px;
360
+ background: #fff;
361
+ color: var(--text);
362
+ font-size: 14px;
363
+ line-height: 1.45;
364
364
  padding: 10px 12px;
365
365
  border-radius: 8px;
366
+ border: 1px solid #f0c9c9;
366
367
  max-height: 88px;
367
368
  overflow: auto;
368
- white-space: pre;
369
+ white-space: pre-wrap;
369
370
  margin: 0;
370
371
  }
371
372
  .error-frame .show-full {