fraim-framework 2.0.95 → 2.0.97

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.
Files changed (40) hide show
  1. package/bin/fraim.js +1 -1
  2. package/dist/src/cli/commands/add-ide.js +1 -1
  3. package/dist/src/cli/commands/doctor.js +6 -6
  4. package/dist/src/cli/commands/init-project.js +63 -52
  5. package/dist/src/cli/commands/list-overridable.js +33 -55
  6. package/dist/src/cli/commands/list.js +35 -9
  7. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  8. package/dist/src/cli/commands/override.js +18 -39
  9. package/dist/src/cli/commands/setup.js +1 -1
  10. package/dist/src/cli/commands/sync.js +34 -27
  11. package/dist/src/cli/doctor/check-runner.js +3 -3
  12. package/dist/src/cli/doctor/checks/global-setup-checks.js +13 -13
  13. package/dist/src/cli/doctor/checks/project-setup-checks.js +12 -12
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +2 -2
  15. package/dist/src/cli/doctor/checks/workflow-checks.js +56 -60
  16. package/dist/src/cli/doctor/reporters/console-reporter.js +1 -1
  17. package/dist/src/cli/fraim.js +3 -1
  18. package/dist/src/cli/mcp/mcp-server-registry.js +1 -1
  19. package/dist/src/cli/services/device-flow-service.js +83 -0
  20. package/dist/src/cli/setup/auto-mcp-setup.js +2 -2
  21. package/dist/src/cli/setup/first-run.js +4 -3
  22. package/dist/src/cli/utils/agent-adapters.js +126 -0
  23. package/dist/src/cli/utils/fraim-gitignore.js +15 -21
  24. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  25. package/dist/src/cli/utils/remote-sync.js +20 -67
  26. package/dist/src/core/ai-mentor.js +31 -49
  27. package/dist/src/core/config-loader.js +57 -62
  28. package/dist/src/core/config-writer.js +75 -0
  29. package/dist/src/core/types.js +1 -1
  30. package/dist/src/core/utils/job-parser.js +176 -0
  31. package/dist/src/core/utils/local-registry-resolver.js +61 -71
  32. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  33. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  34. package/dist/src/core/utils/stub-generator.js +41 -75
  35. package/dist/src/core/utils/workflow-parser.js +5 -3
  36. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  37. package/dist/src/local-mcp-server/stdio-server.js +124 -51
  38. package/dist/src/local-mcp-server/usage-collector.js +109 -27
  39. package/index.js +1 -1
  40. package/package.json +3 -4
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createInitProjectResult = createInitProjectResult;
7
+ exports.recordPathStatus = recordPathStatus;
8
+ exports.buildInitProjectSummary = buildInitProjectSummary;
9
+ exports.printInitProjectSummary = printInitProjectSummary;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ function formatModeLabel(mode) {
12
+ switch (mode) {
13
+ case 'conversational':
14
+ return 'Conversational';
15
+ case 'split':
16
+ return 'Split';
17
+ default:
18
+ return 'Integrated';
19
+ }
20
+ }
21
+ function getModeSpecificNextStep(mode) {
22
+ switch (mode) {
23
+ case 'conversational':
24
+ return 'The agent will focus on project context, validation commands, and durable repo rules.';
25
+ case 'split':
26
+ return 'The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details.';
27
+ default:
28
+ return 'The agent will review the detected repo setup, then ask only for the highest-value missing project details.';
29
+ }
30
+ }
31
+ function createInitProjectResult(projectName, mode) {
32
+ return {
33
+ mode,
34
+ projectName,
35
+ repositoryDetected: false,
36
+ issueTrackingDetected: false,
37
+ createdPaths: [],
38
+ reusedPaths: [],
39
+ warnings: [],
40
+ syncPerformed: false,
41
+ bootstrapNeeded: false
42
+ };
43
+ }
44
+ function recordPathStatus(result, relativePath, created) {
45
+ if (created) {
46
+ result.createdPaths.push(relativePath);
47
+ return;
48
+ }
49
+ result.reusedPaths.push(relativePath);
50
+ }
51
+ function buildInitProjectSummary(result) {
52
+ return {
53
+ status: 'FRAIM project initialized.',
54
+ fields: {
55
+ mode: formatModeLabel(result.mode),
56
+ project: result.projectName,
57
+ repositoryDetection: result.repositoryDetected ? 'detected' : 'not detected',
58
+ issueTracking: result.issueTrackingDetected ? 'detected' : 'not detected',
59
+ sync: result.syncPerformed ? 'completed' : 'skipped for this run',
60
+ createdPaths: [...result.createdPaths],
61
+ reusedPaths: [...result.reusedPaths]
62
+ },
63
+ warnings: [...result.warnings],
64
+ nextStep: {
65
+ prompt: 'Tell your AI agent "Onboard this project".',
66
+ explanation: getModeSpecificNextStep(result.mode)
67
+ }
68
+ };
69
+ }
70
+ function printInitProjectSummary(result) {
71
+ const summary = buildInitProjectSummary(result);
72
+ console.log(chalk_1.default.green(`\n${summary.status}`));
73
+ console.log(chalk_1.default.blue('Project summary:'));
74
+ console.log(chalk_1.default.gray(` Mode: ${summary.fields.mode}`));
75
+ console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
76
+ console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
77
+ console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
78
+ console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
79
+ if (summary.fields.createdPaths.length > 0) {
80
+ console.log(chalk_1.default.gray(` Created: ${summary.fields.createdPaths.join(', ')}`));
81
+ }
82
+ if (summary.fields.reusedPaths.length > 0) {
83
+ console.log(chalk_1.default.gray(` Reused: ${summary.fields.reusedPaths.join(', ')}`));
84
+ }
85
+ if (summary.warnings.length > 0) {
86
+ console.log(chalk_1.default.yellow('\nWarnings:'));
87
+ summary.warnings.forEach((warning) => {
88
+ console.log(chalk_1.default.yellow(` - ${warning}`));
89
+ });
90
+ }
91
+ console.log(chalk_1.default.cyan(`\nNext step: ${summary.nextStep.prompt}`));
92
+ console.log(chalk_1.default.gray(summary.nextStep.explanation));
93
+ }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Remote Registry Sync
4
4
  *
5
- * Fetches workflows and scripts from the remote FRAIM server
5
+ * Fetches jobs and scripts from the remote FRAIM server
6
6
  * instead of bundling them in the npm package.
7
7
  *
8
8
  * Issue: #83 - Minimize client package by fetching registry remotely
@@ -18,6 +18,7 @@ const path_1 = require("path");
18
18
  const chalk_1 = __importDefault(require("chalk"));
19
19
  const script_sync_utils_1 = require("./script-sync-utils");
20
20
  const fraim_gitignore_1 = require("./fraim-gitignore");
21
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
21
22
  const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
22
23
  const SYNCED_CONTENT_BANNER_MARKER = '<!-- FRAIM_SYNC_MANAGED_CONTENT -->';
23
24
  function shouldLockSyncedContent() {
@@ -93,7 +94,7 @@ function applySyncedContentBanner(file) {
93
94
  return insertAfterFrontmatter(file.content, banner);
94
95
  }
95
96
  /**
96
- * Sync workflows and scripts from remote FRAIM server
97
+ * Sync jobs and scripts from remote FRAIM server
97
98
  */
98
99
  async function syncFromRemote(options) {
99
100
  const remoteUrl = options.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
@@ -101,7 +102,6 @@ async function syncFromRemote(options) {
101
102
  if (!apiKey) {
102
103
  return {
103
104
  success: false,
104
- workflowsSynced: 0,
105
105
  employeeJobsSynced: 0,
106
106
  managerJobsSynced: 0,
107
107
  skillsSynced: 0,
@@ -126,7 +126,6 @@ async function syncFromRemote(options) {
126
126
  console.log(chalk_1.default.yellow('⚠️ No files received from remote server'));
127
127
  return {
128
128
  success: false,
129
- workflowsSynced: 0,
130
129
  employeeJobsSynced: 0,
131
130
  managerJobsSynced: 0,
132
131
  skillsSynced: 0,
@@ -143,55 +142,11 @@ async function syncFromRemote(options) {
143
142
  setFileWriteLockRecursively(target, false);
144
143
  }
145
144
  }
146
- // Sync workflows to role-specific folders under .fraim
147
- const allWorkflowFiles = files.filter(f => f.type === 'workflow');
148
- const managerWorkflowFiles = allWorkflowFiles.filter(f => f.path.startsWith('ai-manager/'));
149
- const employeeWorkflowFiles = allWorkflowFiles.filter(f => !f.path.startsWith('ai-manager/'));
150
- // Write employee workflows
151
- const employeeWorkflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'workflows');
152
- if (!(0, fs_1.existsSync)(employeeWorkflowsDir)) {
153
- (0, fs_1.mkdirSync)(employeeWorkflowsDir, { recursive: true });
154
- }
155
- cleanDirectory(employeeWorkflowsDir);
156
- for (const file of employeeWorkflowFiles) {
157
- // Strip "workflows/" prefix and "ai-employee/" role prefix for cleaner local layout
158
- let relPath = file.path;
159
- if (relPath.startsWith('workflows/'))
160
- relPath = relPath.substring('workflows/'.length);
161
- relPath = relPath.replace(/^ai-employee\//, '');
162
- const filePath = (0, path_1.join)(employeeWorkflowsDir, relPath);
163
- const fileDir = (0, path_1.dirname)(filePath);
164
- if (!(0, fs_1.existsSync)(fileDir)) {
165
- (0, fs_1.mkdirSync)(fileDir, { recursive: true });
166
- }
167
- (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
168
- console.log(chalk_1.default.gray(` + .fraim/ai-employee/workflows/${relPath}`));
169
- }
170
- // Write manager workflows
171
- const managerWorkflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'workflows');
172
- if (!(0, fs_1.existsSync)(managerWorkflowsDir)) {
173
- (0, fs_1.mkdirSync)(managerWorkflowsDir, { recursive: true });
174
- }
175
- cleanDirectory(managerWorkflowsDir);
176
- for (const file of managerWorkflowFiles) {
177
- // Strip "workflows/" prefix and "ai-manager/" role prefix
178
- let relPath = file.path;
179
- if (relPath.startsWith('workflows/'))
180
- relPath = relPath.substring('workflows/'.length);
181
- relPath = relPath.replace(/^ai-manager\//, '');
182
- const filePath = (0, path_1.join)(managerWorkflowsDir, relPath);
183
- const fileDir = (0, path_1.dirname)(filePath);
184
- if (!(0, fs_1.existsSync)(fileDir)) {
185
- (0, fs_1.mkdirSync)(fileDir, { recursive: true });
186
- }
187
- (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
188
- console.log(chalk_1.default.gray(` + .fraim/ai-manager/workflows/${relPath}`));
189
- }
190
- // Sync job stubs to role-specific folders under .fraim
145
+ // Sync job stubs to role-specific folders under fraim/
191
146
  const allJobFiles = files.filter(f => f.type === 'job');
192
147
  const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
193
148
  const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
194
- const employeeJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'jobs');
149
+ const employeeJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'jobs');
195
150
  if (!(0, fs_1.existsSync)(employeeJobsDir)) {
196
151
  (0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
197
152
  }
@@ -208,10 +163,10 @@ async function syncFromRemote(options) {
208
163
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
209
164
  }
210
165
  (0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
211
- console.log(chalk_1.default.gray(` + ai-employee/jobs/${relPath}`));
166
+ console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${relPath}`)}`));
212
167
  }
213
- // Sync ai-manager job stubs to .fraim/ai-manager/jobs/
214
- const managerJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'jobs');
168
+ // Sync ai-manager job stubs to fraim/ai-manager/jobs/
169
+ const managerJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-manager', 'jobs');
215
170
  if (!(0, fs_1.existsSync)(managerJobsDir)) {
216
171
  (0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
217
172
  }
@@ -228,17 +183,17 @@ async function syncFromRemote(options) {
228
183
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
229
184
  }
230
185
  (0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
231
- console.log(chalk_1.default.gray(` + .fraim/ai-manager/jobs/${relPath}`));
186
+ console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${relPath}`)}`));
232
187
  }
233
- // Sync skill STUBS to .fraim/ai-employee/skills/
188
+ // Sync skill STUBS to fraim/ai-employee/skills/
234
189
  const skillFiles = files.filter(f => f.type === 'skill');
235
- const skillsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'skills');
190
+ const skillsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'skills');
236
191
  if (!(0, fs_1.existsSync)(skillsDir)) {
237
192
  (0, fs_1.mkdirSync)(skillsDir, { recursive: true });
238
193
  }
239
194
  cleanDirectory(skillsDir);
240
195
  for (const file of skillFiles) {
241
- // Strip "skills/" prefix to avoid redundant nesting in .fraim/ai-employee/skills/
196
+ // Strip "skills/" prefix to avoid redundant nesting in fraim/ai-employee/skills/
242
197
  let relPath = file.path;
243
198
  if (relPath.startsWith('skills/'))
244
199
  relPath = relPath.substring('skills/'.length);
@@ -248,17 +203,17 @@ async function syncFromRemote(options) {
248
203
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
249
204
  }
250
205
  (0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
251
- console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path} (stub)`));
206
+ console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/skills/${relPath}`)} (stub)`));
252
207
  }
253
- // Sync rule STUBS to .fraim/ai-employee/rules/
208
+ // Sync rule STUBS to fraim/ai-employee/rules/
254
209
  const ruleFiles = files.filter(f => f.type === 'rule');
255
- const rulesDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'rules');
210
+ const rulesDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'rules');
256
211
  if (!(0, fs_1.existsSync)(rulesDir)) {
257
212
  (0, fs_1.mkdirSync)(rulesDir, { recursive: true });
258
213
  }
259
214
  cleanDirectory(rulesDir);
260
215
  for (const file of ruleFiles) {
261
- // Strip "rules/" prefix to avoid redundant nesting in .fraim/ai-employee/rules/
216
+ // Strip "rules/" prefix to avoid redundant nesting in fraim/ai-employee/rules/
262
217
  let relPath = file.path;
263
218
  if (relPath.startsWith('rules/'))
264
219
  relPath = relPath.substring('rules/'.length);
@@ -268,7 +223,7 @@ async function syncFromRemote(options) {
268
223
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
269
224
  }
270
225
  (0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
271
- console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path} (stub)`));
226
+ console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/rules/${relPath}`)} (stub)`));
272
227
  }
273
228
  // Sync scripts to user directory
274
229
  const scriptFiles = files.filter(f => f.type === 'script');
@@ -289,9 +244,9 @@ async function syncFromRemote(options) {
289
244
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
290
245
  console.log(chalk_1.default.gray(` + ${file.path}`));
291
246
  }
292
- // Sync docs to .fraim/docs/
247
+ // Sync docs to fraim/docs/
293
248
  const docsFiles = files.filter(f => f.type === 'docs');
294
- const docsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'docs');
249
+ const docsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'docs');
295
250
  if (!(0, fs_1.existsSync)(docsDir)) {
296
251
  (0, fs_1.mkdirSync)(docsDir, { recursive: true });
297
252
  }
@@ -303,7 +258,7 @@ async function syncFromRemote(options) {
303
258
  (0, fs_1.mkdirSync)(fileDir, { recursive: true });
304
259
  }
305
260
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
306
- console.log(chalk_1.default.gray(` + docs/${file.path}`));
261
+ console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`docs/${file.path}`)}`));
307
262
  }
308
263
  if (shouldLockSyncedContent()) {
309
264
  for (const target of lockTargets) {
@@ -313,7 +268,6 @@ async function syncFromRemote(options) {
313
268
  }
314
269
  return {
315
270
  success: true,
316
- workflowsSynced: allWorkflowFiles.length,
317
271
  employeeJobsSynced: jobFiles.length,
318
272
  managerJobsSynced: managerJobFiles.length,
319
273
  skillsSynced: skillFiles.length,
@@ -326,7 +280,6 @@ async function syncFromRemote(options) {
326
280
  console.error(chalk_1.default.red(`❌ Remote sync failed: ${error.message}`));
327
281
  return {
328
282
  success: false,
329
- workflowsSynced: 0,
330
283
  employeeJobsSynced: 0,
331
284
  managerJobsSynced: 0,
332
285
  skillsSynced: 0,
@@ -4,16 +4,16 @@ exports.AIMentor = void 0;
4
4
  const include_resolver_1 = require("./utils/include-resolver");
5
5
  class AIMentor {
6
6
  constructor(resolver) {
7
- this.workflowCache = new Map();
7
+ this.jobCache = new Map();
8
8
  this.resolver = resolver;
9
9
  }
10
10
  /**
11
11
  * Handle mentoring/coaching request from agent
12
12
  */
13
13
  async handleMentoringRequest(args) {
14
- const workflow = await this.getOrLoadWorkflow(args.workflowType);
14
+ const workflow = await this.getOrLoadJob(args.jobName);
15
15
  if (!workflow) {
16
- throw new Error(`Workflow "${args.workflowType}" not found or invalid.`);
16
+ throw new Error(`Job "${args.jobName}" not found or invalid.`);
17
17
  }
18
18
  const validStatus = ['starting', 'complete', 'incomplete', 'failure'].includes(args.status);
19
19
  if (!validStatus) {
@@ -25,7 +25,7 @@ class AIMentor {
25
25
  const hasMetadata = !!phases[args.currentPhase];
26
26
  const hasMarkdown = workflow.phases.has(args.currentPhase);
27
27
  if (!hasMetadata && !hasMarkdown && args.currentPhase !== 'starting') {
28
- throw new Error(`Phase "${args.currentPhase}" not found in workflow "${args.workflowType}".`);
28
+ throw new Error(`Phase "${args.currentPhase}" not found in job "${args.jobName}".`);
29
29
  }
30
30
  }
31
31
  // Handle different statuses
@@ -39,14 +39,14 @@ class AIMentor {
39
39
  return await this.generateHelpMessage(workflow, args.currentPhase, args.status, args.skipIncludes);
40
40
  }
41
41
  }
42
- async getOrLoadWorkflow(workflowType, preferredType) {
43
- if (this.workflowCache.has(workflowType)) {
44
- return this.workflowCache.get(workflowType);
42
+ async getOrLoadJob(jobType) {
43
+ if (this.jobCache.has(jobType)) {
44
+ return this.jobCache.get(jobType);
45
45
  }
46
- const workflow = await this.resolver.getWorkflow(workflowType, preferredType);
47
- if (workflow) {
48
- this.workflowCache.set(workflowType, workflow);
49
- return workflow;
46
+ const job = await this.resolver.getJob(jobType);
47
+ if (job) {
48
+ this.jobCache.set(jobType, job);
49
+ return job;
50
50
  }
51
51
  return null;
52
52
  }
@@ -60,11 +60,11 @@ class AIMentor {
60
60
  const unique = Array.from(new Set(matches));
61
61
  throw new Error(`Unresolved include directives in ${context}: ${unique.join(', ')}`);
62
62
  }
63
- buildReportBackFooter(workflowType, phaseId, phaseFlow) {
63
+ buildReportBackFooter(jobName, phaseId, phaseFlow) {
64
64
  if (!phaseFlow)
65
65
  return '';
66
66
  const base = `seekMentoring({
67
- workflowType: "${workflowType}",
67
+ jobName: "${jobName}",
68
68
  issueNumber: "<issue_number>",
69
69
  currentPhase: "${phaseId}",
70
70
  status: "complete",`;
@@ -104,8 +104,7 @@ class AIMentor {
104
104
  return `${block}\n\n---\n\n${message}`;
105
105
  }
106
106
  async generateStartingMessage(workflow, phaseId, skipIncludes) {
107
- const isJob = workflow.metadata.type === 'job';
108
- const entityType = isJob ? 'Job' : 'Workflow';
107
+ const entityType = 'Job';
109
108
  if (workflow.isSimple) {
110
109
  const message = `🚀 **Starting ${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
111
110
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (starting)`);
@@ -118,11 +117,8 @@ class AIMentor {
118
117
  const isVeryFirstCall = phaseId === 'starting';
119
118
  const targetPhase = isVeryFirstCall ? (workflow.metadata.initialPhase || 'starting') : phaseId;
120
119
  let message = '';
121
- if (!isJob) {
122
- message += `🚀 **Starting Workflow: ${workflow.metadata.name}**\n\n`;
123
- if (isVeryFirstCall) {
124
- message += `${workflow.overview}\n\n---\n\n`;
125
- }
120
+ if (isVeryFirstCall && workflow.overview) {
121
+ message += `${workflow.overview}\n\n---\n\n`;
126
122
  }
127
123
  let instructions = workflow.phases.get(targetPhase);
128
124
  if (instructions) {
@@ -132,10 +128,8 @@ class AIMentor {
132
128
  else {
133
129
  message += `⚠️ No specific instructions found for phase: ${targetPhase}`;
134
130
  }
135
- if (isJob) {
136
- const phaseFlow = workflow.metadata.phases?.[targetPhase];
137
- message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
138
- }
131
+ const phaseFlow = workflow.metadata.phases?.[targetPhase];
132
+ message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
139
133
  if (!skipIncludes) {
140
134
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name}:${targetPhase} (starting)`);
141
135
  }
@@ -146,8 +140,7 @@ class AIMentor {
146
140
  };
147
141
  }
148
142
  async generateCompletionMessage(workflow, phaseId, findings, evidence, skipIncludes) {
149
- const isJob = workflow.metadata.type === 'job';
150
- const entityType = isJob ? 'Job' : 'Workflow';
143
+ const entityType = 'Job';
151
144
  if (workflow.isSimple) {
152
145
  const message = `✅ **${entityType} Complete: ${workflow.metadata.name}**\n\n🎉 Great work! You have completed the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
153
146
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (complete)`);
@@ -170,21 +163,14 @@ class AIMentor {
170
163
  }
171
164
  let message = '';
172
165
  if (nextPhaseId) {
173
- if (isJob) {
174
- message += `Great work. Moving to the next phase: **${nextPhaseId}**.\n\n`;
175
- }
176
- else {
177
- message += `✅ **Phase Complete: ${phaseId}**\n\nMoving to the next phase: **${nextPhaseId}**.\n\n`;
178
- }
166
+ message += `Great work. Moving to the next phase: **${nextPhaseId}**.\n\n`;
179
167
  let nextInstructions = workflow.phases.get(nextPhaseId);
180
168
  if (nextInstructions) {
181
169
  nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
182
170
  message += nextInstructions;
183
171
  }
184
- if (isJob) {
185
- const nextPhaseFlow = workflow.metadata.phases?.[nextPhaseId];
186
- message += this.buildReportBackFooter(workflow.metadata.name, nextPhaseId, nextPhaseFlow);
187
- }
172
+ const nextPhaseFlow = workflow.metadata.phases?.[nextPhaseId];
173
+ message += this.buildReportBackFooter(workflow.metadata.name, nextPhaseId, nextPhaseFlow);
188
174
  }
189
175
  else {
190
176
  message += `🎉 **${entityType} Accomplished!** You have completed all phases of the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
@@ -199,7 +185,7 @@ class AIMentor {
199
185
  };
200
186
  }
201
187
  async generateHelpMessage(workflow, phaseId, status, skipIncludes) {
202
- const entityType = workflow.metadata.type === 'job' ? 'Job' : 'Workflow';
188
+ const entityType = 'Job';
203
189
  if (workflow.isSimple) {
204
190
  const message = `**${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
205
191
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (${status})`);
@@ -226,23 +212,19 @@ class AIMentor {
226
212
  status
227
213
  };
228
214
  }
229
- async getWorkflowOverview(workflowType) {
230
- const workflow = await this.getOrLoadWorkflow(workflowType, 'workflow');
231
- return workflow ? { overview: workflow.overview, isSimple: workflow.isSimple } : null;
232
- }
233
215
  async getJobOverview(jobName) {
234
- const job = await this.getOrLoadWorkflow(jobName, 'job');
216
+ const job = await this.getOrLoadJob(jobName);
235
217
  return job ? { overview: job.overview, isSimple: job.isSimple } : null;
236
218
  }
237
- async getAllWorkflowMetadata() {
238
- const items = await this.resolver.listItems('workflow');
239
- const workflows = [];
219
+ async getAllJobMetadata() {
220
+ const items = await this.resolver.listItems('job');
221
+ const jobs = [];
240
222
  for (const item of items) {
241
- const wf = await this.resolver.getWorkflow(item.name, 'workflow');
242
- if (wf)
243
- workflows.push(wf);
223
+ const job = await this.resolver.getJob(item.name);
224
+ if (job)
225
+ jobs.push(job);
244
226
  }
245
- return workflows;
227
+ return jobs;
246
228
  }
247
229
  }
248
230
  exports.AIMentor = AIMentor;
@@ -1,85 +1,85 @@
1
1
  "use strict";
2
2
  /**
3
3
  * FRAIM Configuration Loader
4
- * Loads and validates .fraim/config.json with fallback to defaults
4
+ * Loads and validates workspace FRAIM config with fallback to defaults.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.normalizeFraimConfig = normalizeFraimConfig;
7
8
  exports.loadFraimConfig = loadFraimConfig;
8
9
  exports.getConfigValue = getConfigValue;
9
10
  exports.getRepositoryInfo = getRepositoryInfo;
10
11
  const fs_1 = require("fs");
11
- const path_1 = require("path");
12
12
  const types_1 = require("./types");
13
+ const project_fraim_paths_1 = require("./utils/project-fraim-paths");
14
+ function normalizeFraimConfig(config) {
15
+ // Handle backward compatibility and migration
16
+ const mergedConfig = {
17
+ version: config.version || types_1.DEFAULT_FRAIM_CONFIG.version,
18
+ project: {
19
+ ...types_1.DEFAULT_FRAIM_CONFIG.project,
20
+ ...(config.project || {})
21
+ },
22
+ repository: {
23
+ ...types_1.DEFAULT_FRAIM_CONFIG.repository,
24
+ ...(config.repository || {}),
25
+ // Migrate from old git config if repository is missing
26
+ ...((!config.repository && config.git) ? {
27
+ provider: config.git.provider || 'github',
28
+ owner: config.git.repoOwner,
29
+ name: config.git.repoName,
30
+ defaultBranch: config.git.defaultBranch || 'main'
31
+ } : {})
32
+ },
33
+ customizations: {
34
+ ...types_1.DEFAULT_FRAIM_CONFIG.customizations,
35
+ ...(config.customizations || {})
36
+ }
37
+ };
38
+ if (config.customizations?.postCleanupHook || config.customizations?.cleanupCommand) {
39
+ if (!mergedConfig.customizations)
40
+ mergedConfig.customizations = {};
41
+ mergedConfig.customizations.postCleanupHook = config.customizations.postCleanupHook || config.customizations.cleanupCommand;
42
+ }
43
+ if (config.issueTracking && typeof config.issueTracking === 'object') {
44
+ mergedConfig.issueTracking = config.issueTracking;
45
+ }
46
+ if (config.compliance) {
47
+ mergedConfig.compliance = config.compliance;
48
+ }
49
+ if (config.learning) {
50
+ mergedConfig.learning = config.learning;
51
+ }
52
+ if (config.competitors && typeof config.competitors === 'object') {
53
+ mergedConfig.competitors = config.competitors;
54
+ }
55
+ return mergedConfig;
56
+ }
13
57
  /**
14
- * Load FRAIM configuration from .fraim/config.json
15
- * Falls back to defaults if file doesn't exist
58
+ * Load FRAIM configuration from the workspace FRAIM config file.
59
+ * Falls back to defaults if the file doesn't exist.
16
60
  */
17
- function loadFraimConfig() {
18
- const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
61
+ function loadFraimConfig(configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(process.cwd())) {
62
+ const displayPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json');
19
63
  if (!(0, fs_1.existsSync)(configPath)) {
20
- console.log('📋 No .fraim/config.json found, using defaults');
64
+ console.log(`No ${displayPath} found, using defaults`);
21
65
  return { ...types_1.DEFAULT_FRAIM_CONFIG };
22
66
  }
23
67
  try {
24
68
  const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
25
- const config = JSON.parse(configContent); // Use any for backward compatibility
26
- // Handle backward compatibility and migration
27
- const mergedConfig = {
28
- version: config.version || types_1.DEFAULT_FRAIM_CONFIG.version,
29
- project: {
30
- ...types_1.DEFAULT_FRAIM_CONFIG.project,
31
- ...(config.project || {})
32
- },
33
- repository: {
34
- ...types_1.DEFAULT_FRAIM_CONFIG.repository,
35
- ...(config.repository || {}),
36
- // Migrate from old git config if repository is missing
37
- ...((!config.repository && config.git) ? {
38
- provider: config.git.provider || 'github',
39
- owner: config.git.repoOwner,
40
- name: config.git.repoName,
41
- defaultBranch: config.git.defaultBranch || 'main'
42
- } : {})
43
- },
44
- customizations: {
45
- ...types_1.DEFAULT_FRAIM_CONFIG.customizations,
46
- ...(config.customizations || {})
47
- }
48
- };
49
- if (config.customizations?.postCleanupHook || config.customizations?.cleanupCommand) {
50
- if (!mergedConfig.customizations)
51
- mergedConfig.customizations = {};
52
- mergedConfig.customizations.postCleanupHook = config.customizations.postCleanupHook || config.customizations.cleanupCommand;
53
- }
54
- if (config.issueTracking && typeof config.issueTracking === 'object') {
55
- mergedConfig.issueTracking = config.issueTracking;
56
- }
57
- // Add optional workflow-driven fields only if they exist in the config
58
- if (config.compliance) {
59
- mergedConfig.compliance = config.compliance;
60
- }
61
- if (config.learning) {
62
- mergedConfig.learning = config.learning;
63
- }
64
- if (config.competitors && typeof config.competitors === 'object') {
65
- mergedConfig.competitors = config.competitors;
66
- }
67
- console.log(`📋 Loaded FRAIM config from .fraim/config.json (version ${mergedConfig.version})`);
68
- // Warn about deprecated git config
69
+ const config = JSON.parse(configContent);
70
+ const mergedConfig = normalizeFraimConfig(config);
71
+ console.log(`Loaded FRAIM config from ${displayPath} (version ${mergedConfig.version})`);
69
72
  if (config.git && !config.repository) {
70
- console.warn('⚠️ Deprecated: "git" config detected. Consider migrating to "repository" config.');
73
+ console.warn('Deprecated: "git" config detected. Consider migrating to "repository" config.');
71
74
  }
72
75
  return mergedConfig;
73
76
  }
74
77
  catch (error) {
75
- console.warn(`⚠️ Failed to load .fraim/config.json: ${error instanceof Error ? error.message : 'Unknown error'}`);
76
- console.warn(' Using default configuration');
78
+ console.warn(`Failed to load ${displayPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
79
+ console.warn('Using default configuration');
77
80
  return { ...types_1.DEFAULT_FRAIM_CONFIG };
78
81
  }
79
82
  }
80
- /**
81
- * Get configuration value by path (e.g., "project.name", "repository.owner")
82
- */
83
83
  function getConfigValue(config, path) {
84
84
  const parts = path.split('.');
85
85
  let value = config;
@@ -93,11 +93,7 @@ function getConfigValue(config, path) {
93
93
  }
94
94
  return value;
95
95
  }
96
- /**
97
- * Get repository info with backward compatibility
98
- */
99
96
  function getRepositoryInfo(config) {
100
- // Prefer new repository config
101
97
  if (config.repository) {
102
98
  return {
103
99
  owner: config.repository.owner || config.repository.organization || config.repository.namespace,
@@ -106,7 +102,6 @@ function getRepositoryInfo(config) {
106
102
  defaultBranch: config.repository.defaultBranch
107
103
  };
108
104
  }
109
- // Fall back to old git config
110
105
  if (config.git) {
111
106
  return {
112
107
  owner: config.git.repoOwner,