fraim-framework 2.0.154 → 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.
- package/dist/src/ai-hub/hosts.js +135 -8
- package/dist/src/ai-hub/server.js +201 -1
- package/dist/src/cli/commands/init-project.js +46 -34
- package/dist/src/cli/commands/sync.js +22 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +2 -2
- package/dist/src/cli/utils/github-workflow-sync.js +231 -0
- package/dist/src/cli/utils/managed-agent-paths.js +1 -1
- package/dist/src/cli/utils/project-bootstrap.js +6 -3
- package/dist/src/core/ai-mentor.js +46 -37
- package/dist/src/core/config-loader.js +68 -0
- package/dist/src/core/fraim-config-schema.generated.js +267 -1
- package/dist/src/core/utils/fraim-labels.js +182 -0
- package/dist/src/core/utils/git-utils.js +22 -1
- package/dist/src/core/utils/project-fraim-paths.js +58 -0
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +77 -52
- package/dist/src/local-mcp-server/stdio-server.js +212 -13
- package/package.json +5 -2
- package/public/ai-hub/index.html +271 -229
- package/public/ai-hub/script.js +879 -527
- package/public/ai-hub/styles.css +877 -694
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +667 -667
|
@@ -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
|
-
|
|
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()
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
73
|
-
if (!onSuccess) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 =
|
|
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 +=
|
|
126
|
+
message += instructions;
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
|
-
message +=
|
|
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 =
|
|
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
|
|
161
|
+
nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess.default ?? null;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
let message = '';
|
|
165
165
|
if (nextPhaseId) {
|
|
166
|
-
message += `
|
|
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 =
|
|
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
|
-
|
|
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
|
/**
|