fraim-framework 2.0.87 → 2.0.88

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.
@@ -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: false
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
  }
@@ -0,0 +1,83 @@
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.DeviceFlowService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class DeviceFlowService {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ /**
14
+ * Start the Device Flow Login
15
+ */
16
+ async login() {
17
+ console.log(chalk_1.default.blue('\nšŸ”— Starting Authentication...'));
18
+ try {
19
+ // 1. Request device and user codes
20
+ const deviceCode = await this.requestDeviceCode();
21
+ console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
22
+ console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
23
+ console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
24
+ console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
25
+ // 2. Poll for the access token
26
+ const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
27
+ console.log(chalk_1.default.green('\nāœ… Authentication Successful!'));
28
+ return token;
29
+ }
30
+ catch (error) {
31
+ console.error(chalk_1.default.red(`\nāŒ Authentication failed: ${error.message}`));
32
+ throw error;
33
+ }
34
+ }
35
+ async requestDeviceCode() {
36
+ const response = await axios_1.default.post(this.config.authUrl, {
37
+ client_id: this.config.clientId,
38
+ scope: this.config.scope
39
+ }, {
40
+ headers: { Accept: 'application/json' }
41
+ });
42
+ return response.data;
43
+ }
44
+ async pollForToken(deviceCode, interval) {
45
+ let currentInterval = interval * 1000;
46
+ return new Promise((resolve, reject) => {
47
+ const poll = async () => {
48
+ try {
49
+ const response = await axios_1.default.post(this.config.tokenUrl, {
50
+ client_id: this.config.clientId,
51
+ device_code: deviceCode,
52
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
53
+ }, {
54
+ headers: { Accept: 'application/json' }
55
+ });
56
+ if (response.data.access_token) {
57
+ resolve(response.data.access_token);
58
+ return;
59
+ }
60
+ if (response.data.error) {
61
+ const error = response.data.error;
62
+ if (error === 'authorization_pending') {
63
+ // Keep polling
64
+ setTimeout(poll, currentInterval);
65
+ }
66
+ else if (error === 'slow_down') {
67
+ currentInterval += 5000;
68
+ setTimeout(poll, currentInterval);
69
+ }
70
+ else {
71
+ reject(new Error(response.data.error_description || error));
72
+ }
73
+ }
74
+ }
75
+ catch (error) {
76
+ reject(error);
77
+ }
78
+ };
79
+ setTimeout(poll, currentInterval);
80
+ });
81
+ }
82
+ }
83
+ exports.DeviceFlowService = DeviceFlowService;
@@ -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, registryPath) {
82
- const overridePath = `.fraim/personalized-employee/${registryPath}`;
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' ? 'job stub' : `${file.type} file`;
95
- const banner = buildSyncedContentBanner(typeLabel, registryPath);
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 workflowFiles = files.filter(f => f.type === 'workflow');
151
- const workflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'workflows');
152
- if (!(0, fs_1.existsSync)(workflowsDir)) {
153
- (0, fs_1.mkdirSync)(workflowsDir, { recursive: true });
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
- // Clean existing workflows
156
- cleanDirectory(workflowsDir);
157
- // Write workflow files
158
- for (const file of workflowFiles) {
159
- const filePath = (0, path_1.join)(workflowsDir, file.path);
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(` + ${file.path}`));
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
- const filePath = (0, path_1.join)(employeeJobsDir, file.path);
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/${file.path}`));
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
- const managerRelativePath = file.path.replace(/^ai-manager\//, '');
193
- const filePath = (0, path_1.join)(managerJobsDir, managerRelativePath);
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/${managerRelativePath}`));
231
+ console.log(chalk_1.default.gray(` + .fraim/ai-manager/jobs/${relPath}`));
200
232
  }
201
- // Sync full skill files to .fraim/ai-employee/skills/
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
- const filePath = (0, path_1.join)(skillsDir, file.path);
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 full rule files to .fraim/ai-employee/rules/
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
- const filePath = (0, path_1.join)(rulesDir, file.path);
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: workflowFiles.length,
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(/^(\d+)-/);
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(/^(\d+)-/);
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(/^(\d+)-/);
63
+ const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
64
64
  if (issueMatch) {
65
65
  return `issue_${issueMatch[1]}`;
66
66
  }