fraim-framework 2.0.153 → 2.0.159

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.
@@ -48,6 +48,7 @@ const git_utils_1 = require("../../core/utils/git-utils");
48
48
  const agent_adapters_1 = require("../utils/agent-adapters");
49
49
  const fraim_gitignore_1 = require("../utils/fraim-gitignore");
50
50
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
51
+ const github_workflow_sync_1 = require("../utils/github-workflow-sync");
51
52
  function resolveExplicitLocalSyncUrl() {
52
53
  const candidates = [
53
54
  process.env.FRAIM_TEST_SERVER_URL,
@@ -124,7 +125,7 @@ const runSync = async (options) => {
124
125
  return;
125
126
  }
126
127
  const projectRoot = options.projectRoot ? path_1.default.resolve(options.projectRoot) : process.cwd();
127
- const config = (0, config_loader_1.loadFraimConfig)();
128
+ const config = (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
128
129
  const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
129
130
  const refreshLocalIgnoreConfig = () => {
130
131
  const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
@@ -168,6 +169,16 @@ const runSync = async (options) => {
168
169
  if (adapterUpdates.length > 0) {
169
170
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
170
171
  }
172
+ if (config.repository?.provider === 'github' && (0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
173
+ const workflowBundle = await (0, github_workflow_sync_1.fetchGitHubWorkflowBundle)({
174
+ remoteUrl: localUrl,
175
+ apiKey: 'local-dev'
176
+ });
177
+ const workflowResult = (0, github_workflow_sync_1.reconcileGitHubWorkflowAssets)({ projectRoot, files: workflowBundle, config });
178
+ for (const line of (0, github_workflow_sync_1.formatGitHubWorkflowSyncSummary)(workflowResult)) {
179
+ console.log(chalk_1.default.green(line));
180
+ }
181
+ }
171
182
  return;
172
183
  }
173
184
  console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
@@ -207,6 +218,16 @@ const runSync = async (options) => {
207
218
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
208
219
  updateVersionInConfig(fraimDir);
209
220
  refreshLocalIgnoreConfig();
221
+ if (config.repository?.provider === 'github' && (0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
222
+ const workflowBundle = await (0, github_workflow_sync_1.fetchGitHubWorkflowBundle)({
223
+ remoteUrl: config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me',
224
+ apiKey
225
+ });
226
+ const workflowResult = (0, github_workflow_sync_1.reconcileGitHubWorkflowAssets)({ projectRoot, files: workflowBundle, config });
227
+ for (const line of (0, github_workflow_sync_1.formatGitHubWorkflowSyncSummary)(workflowResult)) {
228
+ console.log(chalk_1.default.green(line));
229
+ }
230
+ }
210
231
  const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
211
232
  if (adapterUpdates.length > 0) {
212
233
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
@@ -56,10 +56,10 @@ function buildFraimInvocationBody(profile = 'none') {
56
56
  return `Follow this process:
57
57
 
58
58
  ${buildDeferredToolBootstrapSection(profile)}1. **If the user did not specify a FRAIM job or topic**:
59
- Call \`list_fraim_jobs()\` to discover available jobs. Present the results grouped by the categories returned by the server. For each group, list 3-5 of the most relevant jobs with a one-line description.
59
+ If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect \`fraim/personalized-employee/jobs/\` for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
60
60
 
61
61
  2. **Find the match**:
62
- Match the user's request to a FRAIM job from \`list_fraim_jobs()\`. If no job matches, try a likely FRAIM skill with \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` and confirm the match with the user.
62
+ Match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no job matches, try a likely FRAIM skill with \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` and confirm the match with the user.
63
63
 
64
64
  3. **Load the full content**:
65
65
  - For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
@@ -0,0 +1,231 @@
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.GITHUB_WORKFLOW_ASSET_PREFIX = exports.GITHUB_WORKFLOW_HEADER = exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = void 0;
7
+ exports.isGitHubWorkflowAutomationEnabled = isGitHubWorkflowAutomationEnabled;
8
+ exports.decorateManagedGitHubWorkflow = decorateManagedGitHubWorkflow;
9
+ exports.isFraimManagedGitHubWorkflow = isFraimManagedGitHubWorkflow;
10
+ exports.loadGitHubWorkflowManifest = loadGitHubWorkflowManifest;
11
+ exports.loadLocalGitHubWorkflowBundle = loadLocalGitHubWorkflowBundle;
12
+ exports.fetchGitHubWorkflowBundle = fetchGitHubWorkflowBundle;
13
+ exports.reconcileGitHubWorkflowAssets = reconcileGitHubWorkflowAssets;
14
+ exports.formatGitHubWorkflowSyncSummary = formatGitHubWorkflowSyncSummary;
15
+ const axios_1 = __importDefault(require("axios"));
16
+ const crypto_1 = __importDefault(require("crypto"));
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const config_loader_1 = require("../../core/config-loader");
20
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
21
+ exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = path_1.default.join('fraim', 'managed-assets', 'github-workflows.json');
22
+ exports.GITHUB_WORKFLOW_HEADER = '# FRAIM-MANAGED: github-workflow';
23
+ exports.GITHUB_WORKFLOW_ASSET_PREFIX = '# FRAIM-ASSET-ID: ';
24
+ function sha256(content) {
25
+ return `sha256:${crypto_1.default.createHash('sha256').update(content).digest('hex')}`;
26
+ }
27
+ function getManifestPath(projectRoot) {
28
+ return path_1.default.join(projectRoot, exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH);
29
+ }
30
+ function isGitHubWorkflowAutomationEnabled(config) {
31
+ return config?.customizations?.githubWorkflows?.enabled === true;
32
+ }
33
+ function decorateManagedGitHubWorkflow(assetId, content) {
34
+ const normalized = content.replace(/^\uFEFF/, '');
35
+ const assetHeader = `${exports.GITHUB_WORKFLOW_HEADER}\n${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`;
36
+ if (normalized.includes(exports.GITHUB_WORKFLOW_HEADER) && normalized.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`)) {
37
+ return normalized;
38
+ }
39
+ return `${assetHeader}\n${normalized}`;
40
+ }
41
+ function isFraimManagedGitHubWorkflow(content, assetId) {
42
+ if (!content.includes(exports.GITHUB_WORKFLOW_HEADER)) {
43
+ return false;
44
+ }
45
+ if (!assetId) {
46
+ return true;
47
+ }
48
+ return content.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`);
49
+ }
50
+ function loadGitHubWorkflowManifest(projectRoot) {
51
+ const manifestPath = getManifestPath(projectRoot);
52
+ if (!fs_1.default.existsSync(manifestPath)) {
53
+ return {
54
+ version: 1,
55
+ enabled: true,
56
+ provider: 'github',
57
+ managedFiles: []
58
+ };
59
+ }
60
+ return JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
61
+ }
62
+ function writeGitHubWorkflowManifest(projectRoot, manifest) {
63
+ const manifestPath = getManifestPath(projectRoot);
64
+ fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true });
65
+ fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
66
+ }
67
+ function loadLocalGitHubWorkflowBundle(registryRoot) {
68
+ const workflowsRoot = path_1.default.join(registryRoot, 'github', 'workflows');
69
+ if (!fs_1.default.existsSync(workflowsRoot)) {
70
+ return [];
71
+ }
72
+ const files = fs_1.default.readdirSync(workflowsRoot, { withFileTypes: true })
73
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.yml'))
74
+ .map((entry) => {
75
+ const fullPath = path_1.default.join(workflowsRoot, entry.name);
76
+ const content = fs_1.default.readFileSync(fullPath, 'utf8');
77
+ return {
78
+ path: entry.name,
79
+ content,
80
+ type: 'github-workflow',
81
+ digest: sha256(content)
82
+ };
83
+ });
84
+ return files.sort((a, b) => a.path.localeCompare(b.path));
85
+ }
86
+ async function fetchGitHubWorkflowBundle(options) {
87
+ const response = await axios_1.default.get(`${options.remoteUrl}/api/registry/github-workflows`, {
88
+ headers: {
89
+ 'x-api-key': options.apiKey
90
+ },
91
+ timeout: 30000
92
+ });
93
+ return (response.data.files || []);
94
+ }
95
+ function readProjectConfig(projectRoot, config) {
96
+ if (config) {
97
+ return config;
98
+ }
99
+ return (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
100
+ }
101
+ function reconcileGitHubWorkflowAssets(options) {
102
+ const config = readProjectConfig(options.projectRoot, options.config);
103
+ const provider = config.repository?.provider || null;
104
+ if (provider !== 'github') {
105
+ return {
106
+ enabled: false,
107
+ provider,
108
+ skippedReason: 'non-GitHub repo',
109
+ results: []
110
+ };
111
+ }
112
+ if (!isGitHubWorkflowAutomationEnabled(config)) {
113
+ return {
114
+ enabled: false,
115
+ provider,
116
+ skippedReason: 'GitHub workflow automation disabled',
117
+ results: []
118
+ };
119
+ }
120
+ const manifest = loadGitHubWorkflowManifest(options.projectRoot);
121
+ const manifestEntries = new Map(manifest.managedFiles.map((entry) => [entry.path, entry]));
122
+ const workflowsDir = path_1.default.join(options.projectRoot, '.github', 'workflows');
123
+ fs_1.default.mkdirSync(workflowsDir, { recursive: true });
124
+ const results = [];
125
+ const nextManagedFiles = [];
126
+ for (const file of options.files) {
127
+ const assetId = path_1.default.basename(file.path);
128
+ const relativeWorkflowPath = path_1.default.join('.github', 'workflows', file.path).replace(/[\\/]/g, '/');
129
+ const destinationPath = path_1.default.join(options.projectRoot, '.github', 'workflows', file.path);
130
+ const desiredContent = decorateManagedGitHubWorkflow(assetId, file.content);
131
+ const desiredInstalledDigest = sha256(desiredContent);
132
+ const previousEntry = manifestEntries.get(relativeWorkflowPath);
133
+ if (!fs_1.default.existsSync(destinationPath)) {
134
+ fs_1.default.mkdirSync(path_1.default.dirname(destinationPath), { recursive: true });
135
+ fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
136
+ nextManagedFiles.push({
137
+ path: relativeWorkflowPath,
138
+ assetId,
139
+ sourceDigest: file.digest,
140
+ installedDigest: desiredInstalledDigest
141
+ });
142
+ results.push({
143
+ path: relativeWorkflowPath,
144
+ assetId,
145
+ status: 'installed'
146
+ });
147
+ continue;
148
+ }
149
+ const currentContent = fs_1.default.readFileSync(destinationPath, 'utf8');
150
+ const currentDigest = sha256(currentContent);
151
+ const ownedByFraim = isFraimManagedGitHubWorkflow(currentContent, assetId) || Boolean(previousEntry);
152
+ if (!ownedByFraim) {
153
+ if (previousEntry) {
154
+ nextManagedFiles.push(previousEntry);
155
+ }
156
+ results.push({
157
+ path: relativeWorkflowPath,
158
+ assetId,
159
+ status: 'conflict',
160
+ reason: 'same-named file exists without FRAIM ownership marker'
161
+ });
162
+ continue;
163
+ }
164
+ if (currentDigest === desiredInstalledDigest) {
165
+ nextManagedFiles.push({
166
+ path: relativeWorkflowPath,
167
+ assetId,
168
+ sourceDigest: file.digest,
169
+ installedDigest: desiredInstalledDigest
170
+ });
171
+ results.push({
172
+ path: relativeWorkflowPath,
173
+ assetId,
174
+ status: 'already current'
175
+ });
176
+ continue;
177
+ }
178
+ if (previousEntry && currentDigest === previousEntry.installedDigest) {
179
+ fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
180
+ nextManagedFiles.push({
181
+ path: relativeWorkflowPath,
182
+ assetId,
183
+ sourceDigest: file.digest,
184
+ installedDigest: desiredInstalledDigest
185
+ });
186
+ results.push({
187
+ path: relativeWorkflowPath,
188
+ assetId,
189
+ status: 'updated'
190
+ });
191
+ continue;
192
+ }
193
+ nextManagedFiles.push(previousEntry || {
194
+ path: relativeWorkflowPath,
195
+ assetId,
196
+ sourceDigest: file.digest,
197
+ installedDigest: currentDigest
198
+ });
199
+ results.push({
200
+ path: relativeWorkflowPath,
201
+ assetId,
202
+ status: 'conflict',
203
+ reason: previousEntry
204
+ ? 'managed file has local edits that do not match the last installed digest'
205
+ : 'FRAIM-managed header exists without manifest baseline'
206
+ });
207
+ }
208
+ writeGitHubWorkflowManifest(options.projectRoot, {
209
+ version: 1,
210
+ enabled: true,
211
+ provider: 'github',
212
+ managedFiles: nextManagedFiles.sort((a, b) => a.path.localeCompare(b.path))
213
+ });
214
+ return {
215
+ enabled: true,
216
+ provider,
217
+ results
218
+ };
219
+ }
220
+ function formatGitHubWorkflowSyncSummary(result) {
221
+ if (!result.enabled) {
222
+ return [`GitHub workflow reconciliation skipped: ${result.skippedReason || 'not enabled'}`];
223
+ }
224
+ if (result.results.length === 0) {
225
+ return ['GitHub workflow reconciliation: no workflow assets found'];
226
+ }
227
+ return result.results.map((item) => {
228
+ const suffix = item.reason ? ` (${item.reason})` : '';
229
+ return `GitHub workflow ${item.status}: ${item.path}${suffix}`;
230
+ });
231
+ }
@@ -34,7 +34,7 @@ function getManagedAgentBinDirs() {
34
34
  const portableNodeBin = getPortableNodeBinPath();
35
35
  const candidates = process.platform === 'win32'
36
36
  ? [nodeRoot, portableNodeBin]
37
- : [path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
37
+ : [nodeRoot, path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
38
38
  return [...new Set(candidates.filter(Boolean))];
39
39
  }
40
40
  function buildPathWithManagedAgentBins(basePath) {
@@ -22,7 +22,7 @@ function formatModeLabel(mode) {
22
22
  function getModeSpecificNextStep(mode) {
23
23
  switch (mode) {
24
24
  case 'conversational':
25
- return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
25
+ return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable project rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
26
26
  case 'split':
27
27
  return `The agent will create fraim/config.json during onboarding, confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
28
28
  default:
@@ -70,12 +70,15 @@ function buildInitProjectSummary(result) {
70
70
  }
71
71
  function printInitProjectSummary(result) {
72
72
  const summary = buildInitProjectSummary(result);
73
+ const showRepositoryDetails = result.mode !== 'conversational';
73
74
  console.log(chalk_1.default.green(`\n${summary.status}`));
74
75
  console.log(chalk_1.default.blue('Project summary:'));
75
76
  console.log(chalk_1.default.gray(` Mode: ${summary.fields.mode}`));
76
77
  console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
77
- console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
78
- console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
78
+ if (showRepositoryDetails) {
79
+ console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
80
+ console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
81
+ }
79
82
  console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
80
83
  if (summary.fields.createdPaths.length > 0) {
81
84
  console.log(chalk_1.default.gray(` Created: ${summary.fields.createdPaths.join(', ')}`));
@@ -28,7 +28,6 @@ class AIMentor {
28
28
  throw new Error(`Phase "${args.currentPhase}" not found in job "${args.jobName}".`);
29
29
  }
30
30
  }
31
- // Handle different statuses
32
31
  if (args.status === 'starting') {
33
32
  return await this.generateStartingMessage(workflow, args.currentPhase, args.skipIncludes);
34
33
  }
@@ -63,27 +62,17 @@ class AIMentor {
63
62
  buildReportBackFooter(jobName, phaseId, phaseFlow) {
64
63
  if (!phaseFlow)
65
64
  return '';
66
- const base = `seekMentoring({
67
- jobName: "${jobName}",
68
- issueNumber: "<issue_number>",
69
- currentPhase: "${phaseId}",
70
- status: "complete",`;
71
65
  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;
66
+ const completionCall = `seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete" })`;
67
+ if (!onSuccess || typeof onSuccess === 'string') {
68
+ const finalPhaseNote = onSuccess ? '' : ' This is the final phase.';
69
+ return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`${completionCall}\`.${finalPhaseNote}`;
70
+ }
71
+ const validOutcomes = Object.keys(onSuccess)
72
+ .filter((key) => key !== 'default')
73
+ .map((key) => `"${key}"`)
74
+ .join(' | ');
75
+ return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: { issueType: "<outcome>" } })\` with one of: ${validOutcomes}.`;
87
76
  }
88
77
  /** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
89
78
  async getPhaseAuthorityContent() {
@@ -95,18 +84,32 @@ class AIMentor {
95
84
  return '';
96
85
  }
97
86
  }
87
+ getCompactPhaseAuthority() {
88
+ return AIMentor.COMPACT_PHASE_AUTHORITY;
89
+ }
98
90
  async prependPhaseAuthority(message, isPhased) {
99
91
  if (!isPhased)
100
92
  return message;
101
- const block = await this.getPhaseAuthorityContent();
102
- if (!block)
103
- return message;
104
- return `${block}\n\n---\n\n${message}`;
93
+ return `${this.getCompactPhaseAuthority()}\n\n${message}`;
94
+ }
95
+ buildPhasedJobOverview(workflow) {
96
+ const metadataPhases = Object.keys(workflow.metadata.phases || {});
97
+ const initialPhase = workflow.metadata.initialPhase || metadataPhases[0] || Array.from(workflow.phases.keys())[0] || 'starting';
98
+ const overview = workflow.overview.trim();
99
+ return [
100
+ overview,
101
+ '',
102
+ '---',
103
+ '',
104
+ '## Start Here',
105
+ `- Initial phase: \`${initialPhase}\``,
106
+ '- Call `seekMentoring` to load the instructions for this phase.'
107
+ ].join('\n');
105
108
  }
106
109
  async generateStartingMessage(workflow, phaseId, skipIncludes) {
107
110
  const entityType = 'Job';
108
111
  if (workflow.isSimple) {
109
- const message = `🚀 **Starting ${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
112
+ const message = `Starting ${entityType}: ${workflow.metadata.name}\n\n${workflow.overview}`;
110
113
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (starting)`);
111
114
  return {
112
115
  message,
@@ -116,17 +119,14 @@ class AIMentor {
116
119
  }
117
120
  const isVeryFirstCall = phaseId === 'starting';
118
121
  const targetPhase = isVeryFirstCall ? (workflow.metadata.initialPhase || 'starting') : phaseId;
119
- let message = '';
120
- if (isVeryFirstCall && workflow.overview) {
121
- message += `${workflow.overview}\n\n---\n\n`;
122
- }
122
+ let message = `### Current Phase: ${targetPhase}\n\n`;
123
123
  let instructions = workflow.phases.get(targetPhase);
124
124
  if (instructions) {
125
125
  instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
126
- message += `${instructions}`;
126
+ message += instructions;
127
127
  }
128
128
  else {
129
- message += `⚠️ No specific instructions found for phase: ${targetPhase}`;
129
+ message += `No specific instructions found for phase: ${targetPhase}`;
130
130
  }
131
131
  const phaseFlow = workflow.metadata.phases?.[targetPhase];
132
132
  message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
@@ -142,7 +142,7 @@ class AIMentor {
142
142
  async generateCompletionMessage(workflow, phaseId, findings, evidence, skipIncludes) {
143
143
  const entityType = 'Job';
144
144
  if (workflow.isSimple) {
145
- const message = `✅ **${entityType} Complete: ${workflow.metadata.name}**\n\n🎉 Great work! You have completed the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
145
+ const message = `${entityType} Complete: ${workflow.metadata.name}\n\nYou have completed the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
146
146
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (complete)`);
147
147
  return {
148
148
  message,
@@ -158,12 +158,12 @@ class AIMentor {
158
158
  }
159
159
  else {
160
160
  const outcome = findings?.phaseOutcome ?? findings?.issueType ?? evidence?.issueType ?? evidence?.phaseOutcome ?? 'default';
161
- nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess['default'] ?? null;
161
+ nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess.default ?? null;
162
162
  }
163
163
  }
164
164
  let message = '';
165
165
  if (nextPhaseId) {
166
- message += `Great work. Moving to the next phase: **${nextPhaseId}**.\n\n`;
166
+ message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
167
167
  let nextInstructions = workflow.phases.get(nextPhaseId);
168
168
  if (nextInstructions) {
169
169
  nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
@@ -187,7 +187,7 @@ class AIMentor {
187
187
  async generateHelpMessage(workflow, phaseId, status, skipIncludes) {
188
188
  const entityType = 'Job';
189
189
  if (workflow.isSimple) {
190
- const message = `**${entityType}: ${workflow.metadata.name}**\n\n${workflow.overview}`;
190
+ const message = `${entityType}: ${workflow.metadata.name}\n\n${workflow.overview}`;
191
191
  this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (${status})`);
192
192
  return {
193
193
  message,
@@ -214,7 +214,15 @@ class AIMentor {
214
214
  }
215
215
  async getJobOverview(jobName) {
216
216
  const job = await this.getOrLoadJob(jobName);
217
- return job ? { overview: job.overview, isSimple: job.isSimple } : null;
217
+ if (!job)
218
+ return null;
219
+ if (job.isSimple) {
220
+ return { overview: job.overview, isSimple: true };
221
+ }
222
+ return {
223
+ overview: this.buildPhasedJobOverview(job),
224
+ isSimple: false
225
+ };
218
226
  }
219
227
  async getAllJobMetadata() {
220
228
  const items = await this.resolver.listItems('job');
@@ -228,3 +236,4 @@ class AIMentor {
228
236
  }
229
237
  }
230
238
  exports.AIMentor = AIMentor;
239
+ AIMentor.COMPACT_PHASE_AUTHORITY = '**Use only the phases defined in this workflow.**';
@@ -25,6 +25,66 @@ function normalizeCustomerCommunication(config) {
25
25
  deliveryProvider: current.deliveryProvider
26
26
  };
27
27
  }
28
+ function normalizeIntegrations(config) {
29
+ const current = config?.integrations;
30
+ if (!current || typeof current !== 'object')
31
+ return undefined;
32
+ return {
33
+ itsm: current.itsm && typeof current.itsm === 'object'
34
+ ? {
35
+ provider: current.itsm.provider,
36
+ instanceUrl: current.itsm.instanceUrl
37
+ }
38
+ : undefined,
39
+ identity: current.identity && typeof current.identity === 'object'
40
+ ? { provider: current.identity.provider }
41
+ : undefined
42
+ };
43
+ }
44
+ function normalizeAutomation(config) {
45
+ const support = config?.automation?.support;
46
+ if (!support || typeof support !== 'object')
47
+ return undefined;
48
+ return {
49
+ support: {
50
+ startMode: support.startMode,
51
+ defaultDecisionMode: support.defaultDecisionMode,
52
+ contextResolver: support.contextResolver && typeof support.contextResolver === 'object'
53
+ ? {
54
+ scriptPath: support.contextResolver.scriptPath,
55
+ arguments: Array.isArray(support.contextResolver.arguments) ? support.contextResolver.arguments : undefined,
56
+ timeoutMs: support.contextResolver.timeoutMs
57
+ }
58
+ : undefined,
59
+ queue: support.queue && typeof support.queue === 'object'
60
+ ? {
61
+ provider: support.queue.provider,
62
+ table: support.queue.table,
63
+ assignmentGroup: support.queue.assignmentGroup,
64
+ claimField: support.queue.claimField,
65
+ fixturePath: support.queue.fixturePath,
66
+ eligibleStates: Array.isArray(support.queue.eligibleStates) ? support.queue.eligibleStates : undefined,
67
+ pollIntervalSeconds: support.queue.pollIntervalSeconds,
68
+ successState: support.queue.successState,
69
+ failureState: support.queue.failureState,
70
+ closeState: support.queue.closeState
71
+ }
72
+ : undefined,
73
+ requestTypes: support.requestTypes && typeof support.requestTypes === 'object'
74
+ ? support.requestTypes
75
+ : undefined,
76
+ communication: support.communication && typeof support.communication === 'object'
77
+ ? {
78
+ channel: support.communication.channel,
79
+ deliveryMode: support.communication.deliveryMode,
80
+ recipientField: support.communication.recipientField,
81
+ includeTemporaryPassword: support.communication.includeTemporaryPassword,
82
+ messageTemplate: support.communication.messageTemplate
83
+ }
84
+ : undefined
85
+ }
86
+ };
87
+ }
28
88
  function normalizeFraimConfig(config) {
29
89
  // Handle backward compatibility and migration
30
90
  const mergedConfig = {
@@ -70,6 +130,14 @@ function normalizeFraimConfig(config) {
70
130
  if (customerCommunication) {
71
131
  mergedConfig.customerCommunication = customerCommunication;
72
132
  }
133
+ const integrations = normalizeIntegrations(config);
134
+ if (integrations) {
135
+ mergedConfig.integrations = integrations;
136
+ }
137
+ const automation = normalizeAutomation(config);
138
+ if (automation) {
139
+ mergedConfig.automation = automation;
140
+ }
73
141
  return mergedConfig;
74
142
  }
75
143
  /**