fraim-framework 2.0.87 → 2.0.89
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 +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +72 -32
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +62 -55
- package/dist/src/core/utils/workflow-parser.js +103 -46
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +27 -6
- package/package.json +14 -5
|
@@ -51,7 +51,15 @@ const LOCAL_PROVIDERS = [
|
|
|
51
51
|
capabilities: ['code', 'issues', 'integrated'],
|
|
52
52
|
docsUrl: 'https://docs.microsoft.com/azure/devops',
|
|
53
53
|
setupInstructions: 'Create a Personal Access Token in Azure DevOps',
|
|
54
|
-
hasAdditionalConfig:
|
|
54
|
+
hasAdditionalConfig: true,
|
|
55
|
+
mcpServer: {
|
|
56
|
+
type: 'stdio',
|
|
57
|
+
command: 'npx',
|
|
58
|
+
args: ['-y', '@azure-devops/mcp', '{config.organization}', '--authentication', 'envvar'],
|
|
59
|
+
envTemplate: {
|
|
60
|
+
ADO_MCP_AUTH_TOKEN: '{token}'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
},
|
|
56
64
|
{
|
|
57
65
|
id: 'jira',
|
|
@@ -73,6 +81,16 @@ const LOCAL_PROVIDERS = [
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
];
|
|
84
|
+
const ADO_CONFIG_REQUIREMENTS = [
|
|
85
|
+
{
|
|
86
|
+
key: 'organization',
|
|
87
|
+
displayName: 'Azure DevOps Organization',
|
|
88
|
+
description: 'Your Azure DevOps organization name (e.g., contoso)',
|
|
89
|
+
required: true,
|
|
90
|
+
type: 'string',
|
|
91
|
+
cliOptionName: 'organization'
|
|
92
|
+
}
|
|
93
|
+
];
|
|
76
94
|
const JIRA_CONFIG_REQUIREMENTS = [
|
|
77
95
|
{
|
|
78
96
|
key: 'baseUrl',
|
|
@@ -111,6 +129,9 @@ function getLocalProviderSetupInstructions(providerId) {
|
|
|
111
129
|
return provider?.setupInstructions || '';
|
|
112
130
|
}
|
|
113
131
|
function getLocalProviderConfigRequirements(providerId) {
|
|
132
|
+
if (providerId === 'ado') {
|
|
133
|
+
return ADO_CONFIG_REQUIREMENTS;
|
|
134
|
+
}
|
|
114
135
|
if (providerId === 'jira') {
|
|
115
136
|
return JIRA_CONFIG_REQUIREMENTS;
|
|
116
137
|
}
|
|
@@ -60,6 +60,10 @@ async function promptForProviders(client, preselectedIds) {
|
|
|
60
60
|
value: provider.id,
|
|
61
61
|
selected: preselectedIds?.includes(provider.id) ?? provider.id === defaultProviderId
|
|
62
62
|
}));
|
|
63
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
64
|
+
console.log(chalk_1.default.yellow(`\nℹ️ Non-interactive mode: defaulting to ${integratedProviders[0]?.displayName || 'first available provider'}`));
|
|
65
|
+
return [defaultProviderId];
|
|
66
|
+
}
|
|
63
67
|
const response = await (0, prompts_1.default)({
|
|
64
68
|
type: 'multiselect',
|
|
65
69
|
name: 'providers',
|
|
@@ -109,6 +113,10 @@ async function promptForSingleProvider(client, purpose, availableIds) {
|
|
|
109
113
|
title: provider.displayName,
|
|
110
114
|
value: provider.id
|
|
111
115
|
}));
|
|
116
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
117
|
+
console.log(chalk_1.default.yellow(`\nℹ️ Non-interactive mode: defaulting to ${providers[0]?.displayName || 'first available provider'}`));
|
|
118
|
+
return defaultProviderId;
|
|
119
|
+
}
|
|
112
120
|
const response = await (0, prompts_1.default)({
|
|
113
121
|
type: 'select',
|
|
114
122
|
name: 'provider',
|
|
@@ -145,6 +153,9 @@ async function promptForProviderToken(client, providerId) {
|
|
|
145
153
|
}
|
|
146
154
|
console.log(chalk_1.default.blue(`\n🔧 ${provider.displayName} Integration Setup`));
|
|
147
155
|
console.log(`FRAIM requires a ${provider.displayName} token for integration.\n`);
|
|
156
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
157
|
+
throw new Error(`Non-interactive mode: ${provider.displayName} token is missing and cannot prompt.`);
|
|
158
|
+
}
|
|
148
159
|
const hasToken = await (0, prompts_1.default)({
|
|
149
160
|
type: 'confirm',
|
|
150
161
|
name: 'hasToken',
|
|
@@ -152,6 +163,27 @@ async function promptForProviderToken(client, providerId) {
|
|
|
152
163
|
initial: false
|
|
153
164
|
});
|
|
154
165
|
if (!hasToken.hasToken) {
|
|
166
|
+
if (providerId === 'github') {
|
|
167
|
+
const loginChoice = await (0, prompts_1.default)({
|
|
168
|
+
type: 'confirm',
|
|
169
|
+
name: 'login',
|
|
170
|
+
message: `Would you like to login to ${provider.displayName} now using your browser? (Recommended)`,
|
|
171
|
+
initial: true
|
|
172
|
+
});
|
|
173
|
+
if (loginChoice.login) {
|
|
174
|
+
const { DeviceFlowService } = await Promise.resolve().then(() => __importStar(require('../internal/device-flow-service')));
|
|
175
|
+
if (!provider.deviceFlowConfig) {
|
|
176
|
+
throw new Error(`Device flow configuration not found for provider: ${providerId}`);
|
|
177
|
+
}
|
|
178
|
+
const deviceFlow = new DeviceFlowService(provider.deviceFlowConfig);
|
|
179
|
+
try {
|
|
180
|
+
return await deviceFlow.login();
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
console.log(chalk_1.default.yellow('\nBrowser login failed or was cancelled. Fallback to manual token entry.'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
155
187
|
console.log(chalk_1.default.yellow(`\n📝 To create a ${provider.displayName} token:`));
|
|
156
188
|
console.log(chalk_1.default.gray(` ${provider.setupInstructions}`));
|
|
157
189
|
console.log(chalk_1.default.gray(` Visit: ${provider.docsUrl}\n`));
|
|
@@ -225,6 +257,13 @@ async function promptForProviderConfig(client, providerId) {
|
|
|
225
257
|
console.log(`Additional configuration required for ${provider.displayName}.\n`);
|
|
226
258
|
const config = {};
|
|
227
259
|
for (const req of requirements) {
|
|
260
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
261
|
+
if (req.required) {
|
|
262
|
+
throw new Error(`Non-interactive mode: Required configuration "${req.displayName}" for ${provider.displayName} is missing.`);
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk_1.default.yellow(`\nℹ️ Non-interactive mode: skipping optional configuration "${req.displayName}"`));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
228
267
|
const response = await (0, prompts_1.default)({
|
|
229
268
|
type: req.type === 'email' ? 'text' : req.type === 'url' ? 'text' : 'text',
|
|
230
269
|
name: 'value',
|
|
@@ -78,21 +78,18 @@ function insertAfterFrontmatter(content, banner) {
|
|
|
78
78
|
const body = normalized.slice(frontmatter.length);
|
|
79
79
|
return `${frontmatter}${banner}${body}`;
|
|
80
80
|
}
|
|
81
|
-
function buildSyncedContentBanner(typeLabel
|
|
82
|
-
|
|
83
|
-
return `${SYNCED_CONTENT_BANNER_MARKER}
|
|
84
|
-
> [!IMPORTANT]
|
|
85
|
-
> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.
|
|
86
|
-
> Do not edit this file.
|
|
87
|
-
`;
|
|
81
|
+
function buildSyncedContentBanner(typeLabel) {
|
|
82
|
+
return `${SYNCED_CONTENT_BANNER_MARKER}\r\n> [!IMPORTANT]\r\n> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.\r\n> Do not edit this file.\r\n`;
|
|
88
83
|
}
|
|
89
84
|
function applySyncedContentBanner(file) {
|
|
90
85
|
const registryPath = getBannerRegistryPath(file);
|
|
91
86
|
if (!registryPath) {
|
|
92
87
|
return file.content;
|
|
93
88
|
}
|
|
94
|
-
const typeLabel = file.type === 'job'
|
|
95
|
-
|
|
89
|
+
const typeLabel = file.type === 'job' || file.type === 'skill' || file.type === 'rule'
|
|
90
|
+
? `${file.type} stub`
|
|
91
|
+
: `${file.type} file`;
|
|
92
|
+
const banner = buildSyncedContentBanner(typeLabel);
|
|
96
93
|
return insertAfterFrontmatter(file.content, banner);
|
|
97
94
|
}
|
|
98
95
|
/**
|
|
@@ -146,23 +143,49 @@ async function syncFromRemote(options) {
|
|
|
146
143
|
setFileWriteLockRecursively(target, false);
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
|
-
// Sync workflows
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
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
154
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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);
|
|
160
163
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
161
164
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
162
165
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
163
166
|
}
|
|
164
167
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
165
|
-
console.log(chalk_1.default.gray(` +
|
|
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}`));
|
|
166
189
|
}
|
|
167
190
|
// Sync job stubs to role-specific folders under .fraim
|
|
168
191
|
const allJobFiles = files.filter(f => f.type === 'job');
|
|
@@ -174,13 +197,18 @@ async function syncFromRemote(options) {
|
|
|
174
197
|
}
|
|
175
198
|
cleanDirectory(employeeJobsDir);
|
|
176
199
|
for (const file of jobFiles) {
|
|
177
|
-
|
|
200
|
+
// Strip "jobs/" prefix and "ai-employee/" role prefix
|
|
201
|
+
let relPath = file.path;
|
|
202
|
+
if (relPath.startsWith('jobs/'))
|
|
203
|
+
relPath = relPath.substring('jobs/'.length);
|
|
204
|
+
relPath = relPath.replace(/^ai-employee\//, '');
|
|
205
|
+
const filePath = (0, path_1.join)(employeeJobsDir, relPath);
|
|
178
206
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
179
207
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
180
208
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
181
209
|
}
|
|
182
210
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
183
|
-
console.log(chalk_1.default.gray(` + ai-employee/jobs/${
|
|
211
|
+
console.log(chalk_1.default.gray(` + ai-employee/jobs/${relPath}`));
|
|
184
212
|
}
|
|
185
213
|
// Sync ai-manager job stubs to .fraim/ai-manager/jobs/
|
|
186
214
|
const managerJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'jobs');
|
|
@@ -189,16 +217,20 @@ async function syncFromRemote(options) {
|
|
|
189
217
|
}
|
|
190
218
|
cleanDirectory(managerJobsDir);
|
|
191
219
|
for (const file of managerJobFiles) {
|
|
192
|
-
|
|
193
|
-
|
|
220
|
+
// Strip "jobs/" prefix and "ai-manager/" role prefix
|
|
221
|
+
let relPath = file.path;
|
|
222
|
+
if (relPath.startsWith('jobs/'))
|
|
223
|
+
relPath = relPath.substring('jobs/'.length);
|
|
224
|
+
relPath = relPath.replace(/^ai-manager\//, '');
|
|
225
|
+
const filePath = (0, path_1.join)(managerJobsDir, relPath);
|
|
194
226
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
195
227
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
196
228
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
197
229
|
}
|
|
198
230
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
199
|
-
console.log(chalk_1.default.gray(` + ai-manager/jobs/${
|
|
231
|
+
console.log(chalk_1.default.gray(` + .fraim/ai-manager/jobs/${relPath}`));
|
|
200
232
|
}
|
|
201
|
-
// Sync
|
|
233
|
+
// Sync skill STUBS to .fraim/ai-employee/skills/
|
|
202
234
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
203
235
|
const skillsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'skills');
|
|
204
236
|
if (!(0, fs_1.existsSync)(skillsDir)) {
|
|
@@ -206,15 +238,19 @@ async function syncFromRemote(options) {
|
|
|
206
238
|
}
|
|
207
239
|
cleanDirectory(skillsDir);
|
|
208
240
|
for (const file of skillFiles) {
|
|
209
|
-
|
|
241
|
+
// Strip "skills/" prefix to avoid redundant nesting in .fraim/ai-employee/skills/
|
|
242
|
+
let relPath = file.path;
|
|
243
|
+
if (relPath.startsWith('skills/'))
|
|
244
|
+
relPath = relPath.substring('skills/'.length);
|
|
245
|
+
const filePath = (0, path_1.join)(skillsDir, relPath);
|
|
210
246
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
211
247
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
212
248
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
213
249
|
}
|
|
214
250
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
215
|
-
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path}`));
|
|
251
|
+
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path} (stub)`));
|
|
216
252
|
}
|
|
217
|
-
// Sync
|
|
253
|
+
// Sync rule STUBS to .fraim/ai-employee/rules/
|
|
218
254
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
219
255
|
const rulesDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'rules');
|
|
220
256
|
if (!(0, fs_1.existsSync)(rulesDir)) {
|
|
@@ -222,13 +258,17 @@ async function syncFromRemote(options) {
|
|
|
222
258
|
}
|
|
223
259
|
cleanDirectory(rulesDir);
|
|
224
260
|
for (const file of ruleFiles) {
|
|
225
|
-
|
|
261
|
+
// Strip "rules/" prefix to avoid redundant nesting in .fraim/ai-employee/rules/
|
|
262
|
+
let relPath = file.path;
|
|
263
|
+
if (relPath.startsWith('rules/'))
|
|
264
|
+
relPath = relPath.substring('rules/'.length);
|
|
265
|
+
const filePath = (0, path_1.join)(rulesDir, relPath);
|
|
226
266
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
227
267
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
228
268
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
229
269
|
}
|
|
230
270
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
231
|
-
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path}`));
|
|
271
|
+
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path} (stub)`));
|
|
232
272
|
}
|
|
233
273
|
// Sync scripts to user directory
|
|
234
274
|
const scriptFiles = files.filter(f => f.type === 'script');
|
|
@@ -273,7 +313,7 @@ async function syncFromRemote(options) {
|
|
|
273
313
|
}
|
|
274
314
|
return {
|
|
275
315
|
success: true,
|
|
276
|
-
workflowsSynced:
|
|
316
|
+
workflowsSynced: allWorkflowFiles.length,
|
|
277
317
|
employeeJobsSynced: jobFiles.length,
|
|
278
318
|
managerJobsSynced: managerJobFiles.length,
|
|
279
319
|
skillsSynced: skillFiles.length,
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AIMentor = void 0;
|
|
4
|
+
const include_resolver_1 = require("./utils/include-resolver");
|
|
5
|
+
class AIMentor {
|
|
6
|
+
constructor(resolver) {
|
|
7
|
+
this.workflowCache = new Map();
|
|
8
|
+
this.resolver = resolver;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Handle mentoring/coaching request from agent
|
|
12
|
+
*/
|
|
13
|
+
async handleMentoringRequest(args) {
|
|
14
|
+
const workflow = await this.getOrLoadWorkflow(args.workflowType);
|
|
15
|
+
if (!workflow) {
|
|
16
|
+
throw new Error(`Workflow "${args.workflowType}" not found or invalid.`);
|
|
17
|
+
}
|
|
18
|
+
const validStatus = ['starting', 'complete', 'incomplete', 'failure'].includes(args.status);
|
|
19
|
+
if (!validStatus) {
|
|
20
|
+
throw new Error(`Invalid status: ${args.status}. Must be one of: starting, complete, incomplete, failure.`);
|
|
21
|
+
}
|
|
22
|
+
// For simple workflows, skip phase validation
|
|
23
|
+
if (!workflow.isSimple) {
|
|
24
|
+
const phases = workflow.metadata.phases || {};
|
|
25
|
+
const hasMetadata = !!phases[args.currentPhase];
|
|
26
|
+
const hasMarkdown = workflow.phases.has(args.currentPhase);
|
|
27
|
+
if (!hasMetadata && !hasMarkdown && args.currentPhase !== 'starting') {
|
|
28
|
+
throw new Error(`Phase "${args.currentPhase}" not found in workflow "${args.workflowType}".`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Handle different statuses
|
|
32
|
+
if (args.status === 'starting') {
|
|
33
|
+
return await this.generateStartingMessage(workflow, args.currentPhase, args.skipIncludes);
|
|
34
|
+
}
|
|
35
|
+
else if (args.status === 'complete') {
|
|
36
|
+
return await this.generateCompletionMessage(workflow, args.currentPhase, args.findings, args.evidence, args.skipIncludes);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return await this.generateHelpMessage(workflow, args.currentPhase, args.status, args.skipIncludes);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async getOrLoadWorkflow(workflowType, preferredType) {
|
|
43
|
+
if (this.workflowCache.has(workflowType)) {
|
|
44
|
+
return this.workflowCache.get(workflowType);
|
|
45
|
+
}
|
|
46
|
+
const workflow = await this.resolver.getWorkflow(workflowType, preferredType);
|
|
47
|
+
if (workflow) {
|
|
48
|
+
this.workflowCache.set(workflowType, workflow);
|
|
49
|
+
return workflow;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
async resolveIncludes(content, basePath) {
|
|
54
|
+
return (0, include_resolver_1.resolveIncludes)(content, this.resolver, basePath);
|
|
55
|
+
}
|
|
56
|
+
assertNoUnresolvedIncludes(content, context) {
|
|
57
|
+
const matches = content.match(/\{\{include:[^}]+\}\}/g);
|
|
58
|
+
if (!matches || matches.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
const unique = Array.from(new Set(matches));
|
|
61
|
+
throw new Error(`Unresolved include directives in ${context}: ${unique.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
buildReportBackFooter(workflowType, phaseId, phaseFlow) {
|
|
64
|
+
if (!phaseFlow)
|
|
65
|
+
return '';
|
|
66
|
+
const base = `seekMentoring({
|
|
67
|
+
workflowType: "${workflowType}",
|
|
68
|
+
issueNumber: "<issue_number>",
|
|
69
|
+
currentPhase: "${phaseId}",
|
|
70
|
+
status: "complete",`;
|
|
71
|
+
const onSuccess = phaseFlow.onSuccess;
|
|
72
|
+
let successBlock;
|
|
73
|
+
if (!onSuccess) {
|
|
74
|
+
successBlock = `\n\n---\n\n> **⚑ Phase Complete — Report Back**\n> When you have finished all steps above, call:\n> \`\`\`javascript\n> ${base}\n> // This is the final phase.\n> })\n> \`\`\``;
|
|
75
|
+
}
|
|
76
|
+
else if (typeof onSuccess === 'string') {
|
|
77
|
+
successBlock = `\n\n---\n\n> **⚑ Phase Complete — Report Back**\n> When you have finished all steps above, call:\n> \`\`\`javascript\n> ${base}\n> })\n> \`\`\``;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const validOutcomes = Object.keys(onSuccess)
|
|
81
|
+
.filter(k => k !== 'default')
|
|
82
|
+
.map(k => `"${k}"`)
|
|
83
|
+
.join(' | ');
|
|
84
|
+
successBlock = `\n\n---\n\n> **⚑ Phase Complete — Report Back**\n> The next phase depends on your outcome. Set \`findings.issueType\` (or \`findings.phaseOutcome\`) to one of: ${validOutcomes}\n>\n> Then call:\n> \`\`\`javascript\n> ${base}\n> findings: { issueType: "<Outcome>" }\n> })\n> \`\`\``;
|
|
85
|
+
}
|
|
86
|
+
return successBlock;
|
|
87
|
+
}
|
|
88
|
+
/** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
|
|
89
|
+
async getPhaseAuthorityContent() {
|
|
90
|
+
try {
|
|
91
|
+
const content = await this.resolver.getFile('orchestration/phase-authority.md');
|
|
92
|
+
return content?.trim() || '';
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async prependPhaseAuthority(message, isPhased) {
|
|
99
|
+
if (!isPhased)
|
|
100
|
+
return message;
|
|
101
|
+
const block = await this.getPhaseAuthorityContent();
|
|
102
|
+
if (!block)
|
|
103
|
+
return message;
|
|
104
|
+
return `${block}\n\n---\n\n${message}`;
|
|
105
|
+
}
|
|
106
|
+
async generateStartingMessage(workflow, phaseId, skipIncludes) {
|
|
107
|
+
const isJob = workflow.metadata.type === 'job';
|
|
108
|
+
const entityType = isJob ? 'Job' : 'Workflow';
|
|
109
|
+
if (workflow.isSimple) {
|
|
110
|
+
const message = `🚀 **Starting ${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
|
|
111
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (starting)`);
|
|
112
|
+
return {
|
|
113
|
+
message,
|
|
114
|
+
nextPhase: null,
|
|
115
|
+
status: 'starting'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const isVeryFirstCall = phaseId === 'starting';
|
|
119
|
+
const targetPhase = isVeryFirstCall ? (workflow.metadata.initialPhase || 'starting') : phaseId;
|
|
120
|
+
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
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let instructions = workflow.phases.get(targetPhase);
|
|
128
|
+
if (instructions) {
|
|
129
|
+
instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
|
|
130
|
+
message += `${instructions}`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
message += `⚠️ No specific instructions found for phase: ${targetPhase}`;
|
|
134
|
+
}
|
|
135
|
+
if (isJob) {
|
|
136
|
+
const phaseFlow = workflow.metadata.phases?.[targetPhase];
|
|
137
|
+
message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
|
|
138
|
+
}
|
|
139
|
+
if (!skipIncludes) {
|
|
140
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name}:${targetPhase} (starting)`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
message: await this.prependPhaseAuthority(message, true),
|
|
144
|
+
nextPhase: targetPhase,
|
|
145
|
+
status: 'starting'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async generateCompletionMessage(workflow, phaseId, findings, evidence, skipIncludes) {
|
|
149
|
+
const isJob = workflow.metadata.type === 'job';
|
|
150
|
+
const entityType = isJob ? 'Job' : 'Workflow';
|
|
151
|
+
if (workflow.isSimple) {
|
|
152
|
+
const message = `✅ **${entityType} Complete: ${workflow.metadata.name}**\n\n🎉 Great work! You have completed the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
|
|
153
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (complete)`);
|
|
154
|
+
return {
|
|
155
|
+
message,
|
|
156
|
+
nextPhase: null,
|
|
157
|
+
status: 'complete'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const phaseFlow = workflow.metadata.phases?.[phaseId];
|
|
161
|
+
let nextPhaseId = null;
|
|
162
|
+
if (phaseFlow && phaseFlow.onSuccess) {
|
|
163
|
+
if (typeof phaseFlow.onSuccess === 'string') {
|
|
164
|
+
nextPhaseId = phaseFlow.onSuccess;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const outcome = findings?.phaseOutcome ?? findings?.issueType ?? evidence?.issueType ?? evidence?.phaseOutcome ?? 'default';
|
|
168
|
+
nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess['default'] ?? null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let message = '';
|
|
172
|
+
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
|
+
}
|
|
179
|
+
let nextInstructions = workflow.phases.get(nextPhaseId);
|
|
180
|
+
if (nextInstructions) {
|
|
181
|
+
nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
|
|
182
|
+
message += nextInstructions;
|
|
183
|
+
}
|
|
184
|
+
if (isJob) {
|
|
185
|
+
const nextPhaseFlow = workflow.metadata.phases?.[nextPhaseId];
|
|
186
|
+
message += this.buildReportBackFooter(workflow.metadata.name, nextPhaseId, nextPhaseFlow);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
message += `🎉 **${entityType} Accomplished!** You have completed all phases of the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
|
|
191
|
+
}
|
|
192
|
+
if (!skipIncludes) {
|
|
193
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name}:${phaseId} (complete)`);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
message: await this.prependPhaseAuthority(message, true),
|
|
197
|
+
nextPhase: nextPhaseId,
|
|
198
|
+
status: 'complete'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async generateHelpMessage(workflow, phaseId, status, skipIncludes) {
|
|
202
|
+
const entityType = workflow.metadata.type === 'job' ? 'Job' : 'Workflow';
|
|
203
|
+
if (workflow.isSimple) {
|
|
204
|
+
const message = `**${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
|
|
205
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (${status})`);
|
|
206
|
+
return {
|
|
207
|
+
message,
|
|
208
|
+
nextPhase: null,
|
|
209
|
+
status
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const phaseMeta = workflow.metadata.phases?.[phaseId];
|
|
213
|
+
const targetPhaseId = status === 'failure' ? (phaseMeta?.onFailure || phaseId) : phaseId;
|
|
214
|
+
let message = `### Current Phase: ${targetPhaseId}\n\n`;
|
|
215
|
+
let instructions = workflow.phases.get(targetPhaseId);
|
|
216
|
+
if (instructions) {
|
|
217
|
+
instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
|
|
218
|
+
message += instructions;
|
|
219
|
+
}
|
|
220
|
+
if (!skipIncludes) {
|
|
221
|
+
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name}:${targetPhaseId} (${status})`);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
message: await this.prependPhaseAuthority(message, true),
|
|
225
|
+
nextPhase: targetPhaseId,
|
|
226
|
+
status
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async getWorkflowOverview(workflowType) {
|
|
230
|
+
const workflow = await this.getOrLoadWorkflow(workflowType, 'workflow');
|
|
231
|
+
return workflow ? { overview: workflow.overview, isSimple: workflow.isSimple } : null;
|
|
232
|
+
}
|
|
233
|
+
async getJobOverview(jobName) {
|
|
234
|
+
const job = await this.getOrLoadWorkflow(jobName, 'job');
|
|
235
|
+
return job ? { overview: job.overview, isSimple: job.isSimple } : null;
|
|
236
|
+
}
|
|
237
|
+
async getAllWorkflowMetadata() {
|
|
238
|
+
const items = await this.resolver.listItems('workflow');
|
|
239
|
+
const workflows = [];
|
|
240
|
+
for (const item of items) {
|
|
241
|
+
const wf = await this.resolver.getWorkflow(item.name, 'workflow');
|
|
242
|
+
if (wf)
|
|
243
|
+
workflows.push(wf);
|
|
244
|
+
}
|
|
245
|
+
return workflows;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.AIMentor = AIMentor;
|
|
@@ -12,9 +12,9 @@ const child_process_1 = require("child_process");
|
|
|
12
12
|
*/
|
|
13
13
|
function getPort() {
|
|
14
14
|
try {
|
|
15
|
-
const branchName = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
16
|
-
// Match issue-123 or 123-feature-name
|
|
17
|
-
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(
|
|
15
|
+
const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
16
|
+
// Match issue-123 or 123-feature-name or feature/123-name
|
|
17
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
18
18
|
if (issueMatch) {
|
|
19
19
|
const issueNum = parseInt(issueMatch[1], 10);
|
|
20
20
|
// Ensure port is in a safe range (10000-65535)
|
|
@@ -31,8 +31,8 @@ function getPort() {
|
|
|
31
31
|
*/
|
|
32
32
|
function determineDatabaseName() {
|
|
33
33
|
try {
|
|
34
|
-
const branchName = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
35
|
-
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(
|
|
34
|
+
const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
35
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
36
36
|
if (issueMatch) {
|
|
37
37
|
return `fraim_issue_${issueMatch[1]}`;
|
|
38
38
|
}
|
|
@@ -60,7 +60,7 @@ function getCurrentGitBranch() {
|
|
|
60
60
|
* Determines the database schema prefix based on the branch
|
|
61
61
|
*/
|
|
62
62
|
function determineSchema(branchName) {
|
|
63
|
-
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(
|
|
63
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
64
64
|
if (issueMatch) {
|
|
65
65
|
return `issue_${issueMatch[1]}`;
|
|
66
66
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.MAX_INCLUDE_PASSES = void 0;
|
|
13
13
|
exports.resolveIncludesWithIndex = resolveIncludesWithIndex;
|
|
14
|
+
exports.resolveIncludes = resolveIncludes;
|
|
14
15
|
const fs_1 = require("fs");
|
|
15
16
|
/** Maximum resolution passes to prevent infinite loops from circular includes */
|
|
16
17
|
exports.MAX_INCLUDE_PASSES = 10;
|
|
@@ -45,3 +46,47 @@ function resolveIncludesWithIndex(content, fileIndex) {
|
|
|
45
46
|
}
|
|
46
47
|
return result;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Asynchronously resolve {{include:path}} directives in content using a RegistryResolver.
|
|
51
|
+
*
|
|
52
|
+
* @param content - Raw content that may contain {{include:path}} directives
|
|
53
|
+
* @param resolver - RegistryResolver instance
|
|
54
|
+
* @returns Content with all resolvable includes inlined
|
|
55
|
+
*/
|
|
56
|
+
async function resolveIncludes(content, resolver, basePath) {
|
|
57
|
+
let result = content;
|
|
58
|
+
let pass = 0;
|
|
59
|
+
while (result.includes('{{include:') && pass < exports.MAX_INCLUDE_PASSES) {
|
|
60
|
+
// Collect all unique includes in this pass
|
|
61
|
+
const matches = result.match(/\{\{include:([^}]+)\}\}/g);
|
|
62
|
+
if (!matches)
|
|
63
|
+
break;
|
|
64
|
+
const uniqueMatches = Array.from(new Set(matches));
|
|
65
|
+
for (const match of uniqueMatches) {
|
|
66
|
+
const filePath = match.match(/\{\{include:([^}]+)\}\}/)[1].trim();
|
|
67
|
+
let targetPath = filePath;
|
|
68
|
+
if (filePath.startsWith('./') && basePath) {
|
|
69
|
+
// Resolve relative to the directory of the current file
|
|
70
|
+
const dir = basePath.includes('/') ? basePath.substring(0, basePath.lastIndexOf('/')) : '';
|
|
71
|
+
targetPath = dir ? `${dir}/${filePath.substring(2)}` : filePath.substring(2);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const fileContent = await resolver.getFile(targetPath);
|
|
75
|
+
if (fileContent !== null) {
|
|
76
|
+
// Recursively resolve includes in the newly fetched content
|
|
77
|
+
const resolvedContent = await resolveIncludes(fileContent, resolver, targetPath);
|
|
78
|
+
// Replace all occurrences of this specific include
|
|
79
|
+
result = result.split(match).join(resolvedContent);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn(`⚠️ Include file not found via resolver: ${targetPath} (original: ${filePath}, base: ${basePath})`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(`❌ Failed to resolve include via resolver: ${targetPath}`, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pass++;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|