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.
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-ide.js +1 -1
- package/dist/src/cli/commands/doctor.js +6 -6
- package/dist/src/cli/commands/init-project.js +63 -52
- package/dist/src/cli/commands/list-overridable.js +33 -55
- package/dist/src/cli/commands/list.js +35 -9
- package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
- package/dist/src/cli/commands/override.js +18 -39
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/sync.js +34 -27
- package/dist/src/cli/doctor/check-runner.js +3 -3
- package/dist/src/cli/doctor/checks/global-setup-checks.js +13 -13
- package/dist/src/cli/doctor/checks/project-setup-checks.js +12 -12
- package/dist/src/cli/doctor/checks/scripts-checks.js +2 -2
- package/dist/src/cli/doctor/checks/workflow-checks.js +56 -60
- package/dist/src/cli/doctor/reporters/console-reporter.js +1 -1
- package/dist/src/cli/fraim.js +3 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -1
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +2 -2
- package/dist/src/cli/setup/first-run.js +4 -3
- package/dist/src/cli/utils/agent-adapters.js +126 -0
- package/dist/src/cli/utils/fraim-gitignore.js +15 -21
- package/dist/src/cli/utils/project-bootstrap.js +93 -0
- package/dist/src/cli/utils/remote-sync.js +20 -67
- package/dist/src/core/ai-mentor.js +31 -49
- package/dist/src/core/config-loader.js +57 -62
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/types.js +1 -1
- package/dist/src/core/utils/job-parser.js +176 -0
- package/dist/src/core/utils/local-registry-resolver.js +61 -71
- package/dist/src/core/utils/project-fraim-migration.js +103 -0
- package/dist/src/core/utils/project-fraim-paths.js +38 -0
- package/dist/src/core/utils/stub-generator.js +41 -75
- package/dist/src/core/utils/workflow-parser.js +5 -3
- package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
- package/dist/src/local-mcp-server/stdio-server.js +124 -51
- package/dist/src/local-mcp-server/usage-collector.js +109 -27
- package/index.js +1 -1
- 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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
214
|
-
const managerJobsDir = (0,
|
|
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(` + .
|
|
186
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${relPath}`)}`));
|
|
232
187
|
}
|
|
233
|
-
// Sync skill STUBS to
|
|
188
|
+
// Sync skill STUBS to fraim/ai-employee/skills/
|
|
234
189
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
235
|
-
const skillsDir = (0,
|
|
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
|
|
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/${
|
|
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
|
|
208
|
+
// Sync rule STUBS to fraim/ai-employee/rules/
|
|
254
209
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
255
|
-
const rulesDir = (0,
|
|
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
|
|
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/${
|
|
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
|
|
247
|
+
// Sync docs to fraim/docs/
|
|
293
248
|
const docsFiles = files.filter(f => f.type === 'docs');
|
|
294
|
-
const docsDir = (0,
|
|
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.
|
|
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.
|
|
14
|
+
const workflow = await this.getOrLoadJob(args.jobName);
|
|
15
15
|
if (!workflow) {
|
|
16
|
-
throw new Error(`
|
|
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
|
|
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
|
|
43
|
-
if (this.
|
|
44
|
-
return this.
|
|
42
|
+
async getOrLoadJob(jobType) {
|
|
43
|
+
if (this.jobCache.has(jobType)) {
|
|
44
|
+
return this.jobCache.get(jobType);
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
this.
|
|
49
|
-
return
|
|
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(
|
|
63
|
+
buildReportBackFooter(jobName, phaseId, phaseFlow) {
|
|
64
64
|
if (!phaseFlow)
|
|
65
65
|
return '';
|
|
66
66
|
const base = `seekMentoring({
|
|
67
|
-
|
|
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
|
|
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 (
|
|
122
|
-
message +=
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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 =
|
|
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.
|
|
216
|
+
const job = await this.getOrLoadJob(jobName);
|
|
235
217
|
return job ? { overview: job.overview, isSimple: job.isSimple } : null;
|
|
236
218
|
}
|
|
237
|
-
async
|
|
238
|
-
const items = await this.resolver.listItems('
|
|
239
|
-
const
|
|
219
|
+
async getAllJobMetadata() {
|
|
220
|
+
const items = await this.resolver.listItems('job');
|
|
221
|
+
const jobs = [];
|
|
240
222
|
for (const item of items) {
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
223
|
+
const job = await this.resolver.getJob(item.name);
|
|
224
|
+
if (job)
|
|
225
|
+
jobs.push(job);
|
|
244
226
|
}
|
|
245
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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);
|
|
26
|
-
|
|
27
|
-
|
|
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('
|
|
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(
|
|
76
|
-
console.warn('
|
|
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,
|