fraim 2.0.270 → 2.0.272
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/cli/commands/add-ide.js +37 -132
- package/dist/src/cli/commands/add-provider.js +32 -268
- package/dist/src/cli/commands/learning-usage.js +412 -0
- package/dist/src/cli/commands/login.js +5 -5
- package/dist/src/cli/commands/setup.js +15 -52
- package/dist/src/cli/commands/sync.js +111 -80
- package/dist/src/cli/fraim.js +1 -42
- package/dist/src/cli/mcp/ide-formats.js +1 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +3 -3
- package/dist/src/cli/providers/local-provider-registry.js +4 -4
- package/dist/src/cli/setup/auto-mcp-setup.js +4 -13
- package/dist/src/cli/utils/remote-sync.js +41 -25
- package/dist/src/core/ai-mentor.js +27 -14
- package/dist/src/core/config-loader.js +48 -3
- package/dist/src/core/fraim-config-schema.generated.js +18 -0
- package/dist/src/core/handoff-contracts.js +37 -1
- package/dist/src/core/job-phases.js +2 -14
- package/dist/src/core/resolve-phase-edge.js +75 -0
- package/dist/src/core/types.js +7 -1
- package/dist/src/core/utils/git-utils.js +24 -14
- package/dist/src/core/utils/project-fraim-paths.js +16 -1
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +8 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +448 -95
- package/dist/src/local-mcp-server/learning-firing-parser.js +247 -0
- package/dist/src/local-mcp-server/learning-usage-analysis.js +347 -0
- package/dist/src/local-mcp-server/learning-usage-attestation.js +191 -0
- package/dist/src/local-mcp-server/learning-usage-command.js +408 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
- package/dist/src/local-mcp-server/learning-usage-store.js +417 -0
- package/dist/src/local-mcp-server/stdio-server.js +43 -0
- package/dist/src/services/provider-service.js +4 -4
- package/package.json +1 -1
|
@@ -181,46 +181,34 @@ function failSync(mode, message) {
|
|
|
181
181
|
}
|
|
182
182
|
const runSync = async (options) => {
|
|
183
183
|
const failHard = options.failHard ?? 'exit';
|
|
184
|
-
// Handle --global flag: sync to user-level ~/.fraim/ instead of project
|
|
185
|
-
if (options.global) {
|
|
186
|
-
console.log(chalk_1.default.blue('Syncing FRAIM content to user-level directory (~/.fraim/)...'));
|
|
187
|
-
try {
|
|
188
|
-
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
189
|
-
await syncUserLevelArtifacts();
|
|
190
|
-
console.log(chalk_1.default.green('\n✅ User-level FRAIM content sync complete.'));
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
console.error(chalk_1.default.red(`User-level sync failed: ${error.message}`));
|
|
194
|
-
failSync(failHard, `User-level sync failed: ${error.message}`);
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
184
|
const projectRoot = options.projectRoot ? path_1.default.resolve(options.projectRoot) : process.cwd();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
185
|
+
// ─── Credential resolution (before project detection) ─────────────────────
|
|
186
|
+
// Order: user-level config → env var → project config (only if in a project)
|
|
187
|
+
const remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
188
|
+
let apiKey = loadUserApiKey() || process.env.FRAIM_API_KEY;
|
|
189
|
+
const hasProject = (0, project_fraim_paths_1.workspaceFraimExists)(projectRoot);
|
|
190
|
+
if (!apiKey && hasProject) {
|
|
191
|
+
const config = (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
|
|
192
|
+
apiKey = config.apiKey;
|
|
193
|
+
}
|
|
194
|
+
if (!apiKey) {
|
|
195
|
+
if (process.env.TEST_MODE === 'true') {
|
|
196
|
+
console.log(chalk_1.default.yellow('TEST_MODE: No API key configured. Using test placeholder key.'));
|
|
197
|
+
apiKey = 'test-mode-key';
|
|
205
198
|
}
|
|
206
|
-
|
|
207
|
-
console.
|
|
199
|
+
else {
|
|
200
|
+
console.error(chalk_1.default.red('No API key configured. Cannot sync.'));
|
|
201
|
+
console.error(chalk_1.default.yellow('Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json'));
|
|
202
|
+
console.error(chalk_1.default.yellow('Or use --local to sync from a locally running FRAIM server.'));
|
|
203
|
+
failSync(failHard, 'No API key configured.');
|
|
208
204
|
}
|
|
209
|
-
}
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
// served stale with its age (R2.3, R4.3).
|
|
213
|
-
const refreshOrgCache = async (remoteUrl, apiKey) => {
|
|
214
|
-
// Org sync is a network round-trip to the org backend. In automated
|
|
215
|
-
// tests there is no org configured and no server to reach, so skip it
|
|
216
|
-
// entirely rather than emit warnings or attempt a real request. Local
|
|
217
|
-
// dev (--local / FRAIM_LOCAL_SYNC) passes the loopback URL + 'local-dev'
|
|
218
|
-
// key below, which syncOrgCache treats as "no cloud org" unless a git
|
|
219
|
-
// backend is configured, so it degrades cleanly without this guard.
|
|
205
|
+
}
|
|
206
|
+
// ─── Closures shared between layers ───────────────────────────────────────
|
|
207
|
+
const refreshOrgCache = async (url, key) => {
|
|
220
208
|
if (process.env.TEST_MODE === 'true')
|
|
221
209
|
return;
|
|
222
210
|
const { syncOrgCache } = await Promise.resolve().then(() => __importStar(require('../utils/org-pack-sync')));
|
|
223
|
-
const outcome = await syncOrgCache({ remoteUrl, apiKey });
|
|
211
|
+
const outcome = await syncOrgCache({ remoteUrl: url, apiKey: key });
|
|
224
212
|
if (outcome.status === 'synced') {
|
|
225
213
|
console.log(chalk_1.default.green(`Org context synced (${outcome.metadata.backend}, version ${outcome.metadata.version.slice(0, 12)})`));
|
|
226
214
|
}
|
|
@@ -230,8 +218,6 @@ const runSync = async (options) => {
|
|
|
230
218
|
else if (outcome.status === 'absent') {
|
|
231
219
|
console.log(chalk_1.default.yellow(`Org context not synced: ${outcome.error}`));
|
|
232
220
|
}
|
|
233
|
-
// Migrate stranded content from the legacy standard path to contentRoot,
|
|
234
|
-
// then report any files that still could not be moved.
|
|
235
221
|
try {
|
|
236
222
|
const { migrateStrandedContent, findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
|
|
237
223
|
migrateStrandedContent('org');
|
|
@@ -247,10 +233,6 @@ const runSync = async (options) => {
|
|
|
247
233
|
}
|
|
248
234
|
}
|
|
249
235
|
catch { /* reporting must never fail a sync */ }
|
|
250
|
-
// 'disabled' (no org configured) stays silent.
|
|
251
|
-
// R8.1: one-time publish offer for legacy machine-local org files. The
|
|
252
|
-
// publish itself runs through organization-onboarding (propose-and-approve);
|
|
253
|
-
// sync only surfaces the offer and never moves files on its own (R8.2).
|
|
254
236
|
if (outcome.status !== 'disabled') {
|
|
255
237
|
const { detectLegacyOrgArtifacts } = await Promise.resolve().then(() => __importStar(require('../utils/org-migration')));
|
|
256
238
|
const legacy = detectLegacyOrgArtifacts();
|
|
@@ -260,11 +242,11 @@ const runSync = async (options) => {
|
|
|
260
242
|
}
|
|
261
243
|
}
|
|
262
244
|
};
|
|
263
|
-
const refreshManagerCache = async (
|
|
245
|
+
const refreshManagerCache = async (url, key) => {
|
|
264
246
|
if (process.env.TEST_MODE === 'true')
|
|
265
247
|
return;
|
|
266
248
|
const { syncManagerCache } = await Promise.resolve().then(() => __importStar(require('../utils/manager-pack-sync')));
|
|
267
|
-
const outcome = await syncManagerCache({ remoteUrl, apiKey });
|
|
249
|
+
const outcome = await syncManagerCache({ remoteUrl: url, apiKey: key });
|
|
268
250
|
if (outcome.status === 'synced') {
|
|
269
251
|
console.log(chalk_1.default.green(`Manager context synced (${outcome.metadata.backend}, version ${outcome.metadata.version.slice(0, 12)})`));
|
|
270
252
|
}
|
|
@@ -274,8 +256,6 @@ const runSync = async (options) => {
|
|
|
274
256
|
else if (outcome.status === 'absent') {
|
|
275
257
|
console.log(chalk_1.default.yellow(`Manager context not synced: ${outcome.error}`));
|
|
276
258
|
}
|
|
277
|
-
// Migrate stranded content from the legacy standard path to contentRoot,
|
|
278
|
-
// then report any files that still could not be moved.
|
|
279
259
|
try {
|
|
280
260
|
const { migrateStrandedContent, findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
|
|
281
261
|
migrateStrandedContent('manager');
|
|
@@ -292,25 +272,17 @@ const runSync = async (options) => {
|
|
|
292
272
|
}
|
|
293
273
|
catch { /* reporting must never fail a sync */ }
|
|
294
274
|
};
|
|
295
|
-
|
|
296
|
-
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
297
|
-
if (isGlobal && !options.skipUpdates) {
|
|
298
|
-
console.log(chalk_1.default.yellow('You are running a global installation of FRAIM.'));
|
|
299
|
-
console.log(chalk_1.default.gray('Updates are not automatic in this mode.'));
|
|
300
|
-
console.log(chalk_1.default.cyan('Recommended: Use "npx fraim@latest sync" instead.\n'));
|
|
301
|
-
}
|
|
302
|
-
const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
|
|
303
|
-
// Allow `FRAIM_LOCAL_SYNC=1` to flip into local-mode without needing
|
|
304
|
-
// the --local CLI flag. The FRE's runProjectRow path doesn't surface
|
|
305
|
-
// a --local flag, but devs validating the FRE locally need a way to
|
|
306
|
-
// point sync at their localhost MCP server. With this env var set,
|
|
307
|
-
// any caller (including the FRE) routes through the local sync path
|
|
308
|
-
// exactly as if the user had passed --local.
|
|
275
|
+
// ─── Local sync shortcut (dev mode) ───────────────────────────────────────
|
|
309
276
|
const useLocal = options.local || process.env.FRAIM_LOCAL_SYNC === '1';
|
|
310
277
|
if (useLocal) {
|
|
311
278
|
console.log(chalk_1.default.blue('Syncing FRAIM jobs from local server...'));
|
|
312
279
|
const localPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : (0, git_utils_1.getPort)();
|
|
313
280
|
const localUrl = resolveExplicitLocalSyncUrl() || `http://localhost:${localPort}`;
|
|
281
|
+
const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
|
|
282
|
+
if (!hasProject) {
|
|
283
|
+
console.error(chalk_1.default.red('Local sync requires a FRAIM project directory (fraim/ must exist).'));
|
|
284
|
+
failSync(failHard, 'Local sync requires a project directory.');
|
|
285
|
+
}
|
|
314
286
|
const result = await syncFromRemote({
|
|
315
287
|
remoteUrl: localUrl,
|
|
316
288
|
apiKey: 'local-dev',
|
|
@@ -319,9 +291,16 @@ const runSync = async (options) => {
|
|
|
319
291
|
});
|
|
320
292
|
if (result.success) {
|
|
321
293
|
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 local server`));
|
|
294
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
322
295
|
removeLegacyVersionFromConfig(fraimDir);
|
|
323
296
|
writeSyncMetadata('local', localUrl);
|
|
324
|
-
|
|
297
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
298
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
299
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
300
|
+
}
|
|
301
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
302
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
303
|
+
}
|
|
325
304
|
if (options.projectAdapters) {
|
|
326
305
|
const allowedTypes = resolveAllowedConfigTypes();
|
|
327
306
|
await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
|
|
@@ -338,39 +317,91 @@ const runSync = async (options) => {
|
|
|
338
317
|
console.error(chalk_1.default.yellow('Make sure the FRAIM MCP server is running locally (npm run dev).'));
|
|
339
318
|
failSync(failHard, `Local sync failed: ${result.error}`);
|
|
340
319
|
}
|
|
341
|
-
|
|
342
|
-
|
|
320
|
+
// ─── Global install notice ────────────────────────────────────────────────
|
|
321
|
+
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
322
|
+
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
323
|
+
if (isGlobal && !options.skipUpdates) {
|
|
324
|
+
console.log(chalk_1.default.yellow('You are running a global installation of FRAIM.'));
|
|
325
|
+
console.log(chalk_1.default.gray('Updates are not automatic in this mode.'));
|
|
326
|
+
console.log(chalk_1.default.cyan('Recommended: Use "npx fraim@latest sync" instead.\n'));
|
|
327
|
+
}
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
+
// LAYER 1 — Machine-level sync (always runs)
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
+
console.log(chalk_1.default.blue('Syncing machine-level FRAIM content (~/.fraim/)...'));
|
|
332
|
+
// 1a. Fetch registry files (single API call shared with Layer 2)
|
|
333
|
+
let registryFiles = [];
|
|
334
|
+
try {
|
|
335
|
+
registryFiles = await (0, remote_sync_1.fetchRegistryFiles)(remoteUrl, apiKey);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
343
338
|
if (process.env.TEST_MODE === 'true') {
|
|
344
|
-
console.log(chalk_1.default.yellow('TEST_MODE:
|
|
345
|
-
apiKey = 'test-mode-key';
|
|
339
|
+
console.log(chalk_1.default.yellow('TEST_MODE: Registry fetch failed (server may be unavailable). Continuing.'));
|
|
346
340
|
}
|
|
347
341
|
else {
|
|
348
|
-
console.error(chalk_1.default.red(
|
|
349
|
-
console.error(chalk_1.default.yellow(
|
|
350
|
-
|
|
351
|
-
failSync(failHard, 'No API key configured.');
|
|
342
|
+
console.error(chalk_1.default.red(`Registry sync failed: ${error.message}`));
|
|
343
|
+
console.error(chalk_1.default.yellow('Check your API key and network connection.'));
|
|
344
|
+
failSync(failHard, `Registry sync failed: ${error.message}`);
|
|
352
345
|
}
|
|
353
346
|
}
|
|
354
|
-
|
|
347
|
+
// 1b. Sync scripts to ~/.fraim/scripts/
|
|
348
|
+
const scriptsSynced = await (0, remote_sync_1.syncScriptsToUserDir)(registryFiles);
|
|
349
|
+
if (scriptsSynced > 0) {
|
|
350
|
+
console.log(chalk_1.default.green(` Synced ${scriptsSynced} scripts to ~/.fraim/scripts/`));
|
|
351
|
+
}
|
|
352
|
+
// 1c. Refresh org home
|
|
353
|
+
await refreshOrgCache(remoteUrl, apiKey);
|
|
354
|
+
// 1d. Refresh manager home
|
|
355
|
+
await refreshManagerCache(remoteUrl, apiKey);
|
|
356
|
+
// 1e. Refresh MCP proxy launcher
|
|
357
|
+
const { ensureFraimMcpLatestLauncher } = await Promise.resolve().then(() => __importStar(require('../mcp/fraim-mcp-latest-launcher')));
|
|
358
|
+
ensureFraimMcpLatestLauncher();
|
|
359
|
+
// 1f. Ensure user-level directories
|
|
360
|
+
const { ensureUserLevelDirectories } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
361
|
+
ensureUserLevelDirectories();
|
|
362
|
+
// 1g. Write sync metadata
|
|
363
|
+
writeSyncMetadata('remote', remoteUrl);
|
|
364
|
+
console.log(chalk_1.default.green('✅ Machine-level sync complete.'));
|
|
365
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
366
|
+
// LAYER 2 — Project-level sync (conditional)
|
|
367
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
368
|
+
if (options.global) {
|
|
369
|
+
console.log(chalk_1.default.gray('--global: skipping project-level sync.'));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!hasProject) {
|
|
373
|
+
console.log(chalk_1.default.cyan('No FRAIM project detected at cwd. Machine-level sync complete.'));
|
|
374
|
+
console.log(chalk_1.default.cyan('Run from a project folder or pass --project-root to sync project stubs.'));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk_1.default.blue('Syncing project-level FRAIM content (fraim/)...'));
|
|
378
|
+
const { syncFromRemote } = await Promise.resolve().then(() => __importStar(require('../utils/remote-sync')));
|
|
379
|
+
const config = (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
|
|
380
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
355
381
|
const result = await syncFromRemote({
|
|
356
|
-
remoteUrl: config.remoteUrl,
|
|
382
|
+
remoteUrl: config.remoteUrl || remoteUrl,
|
|
357
383
|
apiKey,
|
|
358
384
|
projectRoot,
|
|
359
|
-
skipUpdates: options.skipUpdates || false
|
|
385
|
+
skipUpdates: options.skipUpdates || false,
|
|
386
|
+
registryFiles
|
|
360
387
|
});
|
|
361
388
|
if (!result.success) {
|
|
362
|
-
console.error(chalk_1.default.red(`
|
|
363
|
-
console.error(chalk_1.default.yellow('Check your API key and network connection.'));
|
|
389
|
+
console.error(chalk_1.default.red(`Project sync failed: ${result.error}`));
|
|
364
390
|
if (process.env.TEST_MODE === 'true') {
|
|
365
|
-
console.log(chalk_1.default.yellow('TEST_MODE: Continuing without
|
|
391
|
+
console.log(chalk_1.default.yellow('TEST_MODE: Continuing without project sync.'));
|
|
366
392
|
return;
|
|
367
393
|
}
|
|
368
|
-
failSync(failHard, `
|
|
394
|
+
failSync(failHard, `Project sync failed: ${result.error}`);
|
|
369
395
|
}
|
|
370
|
-
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules,
|
|
396
|
+
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, and ${result.docsSynced} docs`));
|
|
371
397
|
removeLegacyVersionFromConfig(fraimDir);
|
|
372
|
-
|
|
373
|
-
|
|
398
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
399
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
400
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
401
|
+
}
|
|
402
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
403
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
404
|
+
}
|
|
374
405
|
if (options.projectAdapters) {
|
|
375
406
|
const allowedTypes = resolveAllowedConfigTypes();
|
|
376
407
|
await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
|
|
@@ -379,8 +410,7 @@ const runSync = async (options) => {
|
|
|
379
410
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
380
411
|
}
|
|
381
412
|
}
|
|
382
|
-
|
|
383
|
-
await refreshManagerCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
|
|
413
|
+
console.log(chalk_1.default.green('✅ Project-level sync complete.'));
|
|
384
414
|
};
|
|
385
415
|
exports.runSync = runSync;
|
|
386
416
|
exports.syncCommand = new commander_1.Command('sync')
|
|
@@ -388,6 +418,7 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
388
418
|
.option('-f, --force', 'Force sync even if digest matches')
|
|
389
419
|
.option('--skip-updates', 'Skip checking for CLI updates (legacy)')
|
|
390
420
|
.option('--local', 'Sync from local development server (port derived from git branch)')
|
|
391
|
-
.option('--global', 'Sync
|
|
421
|
+
.option('--global', 'Sync machine-level FRAIM content only (~/.fraim/), skip project stubs')
|
|
422
|
+
.option('--project-root <path>', 'Explicit project root (enables project-level sync from any cwd)')
|
|
392
423
|
.option('--project-adapters', 'Write legacy project-local FRAIM agent adapter files')
|
|
393
424
|
.action(exports.runSync);
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -1,38 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
5
|
};
|
|
@@ -101,12 +68,4 @@ program.addCommand(workspace_config_1.workspaceConfigCommand);
|
|
|
101
68
|
program.addCommand(org_1.orgCommand);
|
|
102
69
|
program.addCommand(manager_1.managerCommand);
|
|
103
70
|
program.addCommand(cleanup_artifacts_1.cleanupArtifactsCommand);
|
|
104
|
-
|
|
105
|
-
(async () => {
|
|
106
|
-
// Import the initialization promise from setup command
|
|
107
|
-
const { setupCommandInitialization } = await Promise.resolve().then(() => __importStar(require('./commands/setup')));
|
|
108
|
-
if (setupCommandInitialization) {
|
|
109
|
-
await setupCommandInitialization;
|
|
110
|
-
}
|
|
111
|
-
program.parse(process.argv);
|
|
112
|
-
})();
|
|
71
|
+
program.parse(process.argv);
|
|
@@ -257,7 +257,7 @@ class CodexFormat {
|
|
|
257
257
|
const escapedUrl = this.escapeToml(server.url);
|
|
258
258
|
sections.push(`[mcp_servers.${key}]`);
|
|
259
259
|
sections.push(`url = "${escapedUrl}"`);
|
|
260
|
-
//
|
|
260
|
+
// URL-only provider metadata serializes without a synthesized Authorization header.
|
|
261
261
|
if (authHeader) {
|
|
262
262
|
sections.push(`http_headers = { Authorization = "${this.escapeToml(authHeader)}" }`);
|
|
263
263
|
}
|
|
@@ -81,9 +81,9 @@ async function buildProviderMCPServer(providerId, token, config) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Serialize an HTTP MCP server from explicit provider metadata.
|
|
85
|
+
* A missing authHeaderTemplate produces a URL-only shape; it does not establish
|
|
86
|
+
* authentication compatibility for any agent host.
|
|
87
87
|
*/
|
|
88
88
|
function buildHTTPServer(mcpConfig, token) {
|
|
89
89
|
if (!mcpConfig.url) {
|
|
@@ -22,7 +22,7 @@ const LOCAL_PROVIDERS = [
|
|
|
22
22
|
description: 'GitHub repository and issue management',
|
|
23
23
|
capabilities: ['code', 'issues', 'integrated'],
|
|
24
24
|
docsUrl: 'https://docs.github.com',
|
|
25
|
-
setupInstructions: '
|
|
25
|
+
setupInstructions: 'Ask your agent to use the FRAIM connect-mcp skill and follow current GitHub and host guidance',
|
|
26
26
|
hasAdditionalConfig: false,
|
|
27
27
|
mcpServer: {
|
|
28
28
|
type: 'http',
|
|
@@ -35,7 +35,7 @@ const LOCAL_PROVIDERS = [
|
|
|
35
35
|
description: 'GitLab repository and issue management',
|
|
36
36
|
capabilities: ['code', 'issues', 'integrated'],
|
|
37
37
|
docsUrl: 'https://docs.gitlab.com',
|
|
38
|
-
setupInstructions: '
|
|
38
|
+
setupInstructions: 'Ask your agent to use the FRAIM connect-mcp skill and follow current GitLab and host guidance',
|
|
39
39
|
hasAdditionalConfig: false,
|
|
40
40
|
mcpServer: {
|
|
41
41
|
type: 'http',
|
|
@@ -49,7 +49,7 @@ const LOCAL_PROVIDERS = [
|
|
|
49
49
|
description: 'Azure DevOps repository and issue management',
|
|
50
50
|
capabilities: ['code', 'issues', 'integrated'],
|
|
51
51
|
docsUrl: 'https://docs.microsoft.com/azure/devops',
|
|
52
|
-
setupInstructions: '
|
|
52
|
+
setupInstructions: 'Ask your agent to use the FRAIM connect-mcp skill and follow current Azure DevOps and host guidance',
|
|
53
53
|
hasAdditionalConfig: true,
|
|
54
54
|
mcpServer: {
|
|
55
55
|
type: 'stdio',
|
|
@@ -66,7 +66,7 @@ const LOCAL_PROVIDERS = [
|
|
|
66
66
|
description: 'Jira issue tracking and project management',
|
|
67
67
|
capabilities: ['issues'],
|
|
68
68
|
docsUrl: 'https://support.atlassian.com/jira',
|
|
69
|
-
setupInstructions: '
|
|
69
|
+
setupInstructions: 'Ask your agent to use the FRAIM connect-mcp skill and follow current Jira and host guidance',
|
|
70
70
|
hasAdditionalConfig: true,
|
|
71
71
|
mcpServer: {
|
|
72
72
|
type: 'stdio',
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.autoConfigureMCP = exports.validateSetupResults = exports.
|
|
39
|
+
exports.autoConfigureMCP = exports.validateSetupResults = exports.promptForIDESelection = void 0;
|
|
40
40
|
const fs_1 = __importDefault(require("fs"));
|
|
41
41
|
const path_1 = __importDefault(require("path"));
|
|
42
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -60,7 +60,7 @@ const promptForIDESelection = async (detectedIDEs) => {
|
|
|
60
60
|
console.log(chalk_1.default.gray(' • fraim (required for FRAIM jobs)'));
|
|
61
61
|
console.log(chalk_1.default.gray(' • git (version control integration)'));
|
|
62
62
|
console.log(chalk_1.default.gray(' • playwright (browser automation)'));
|
|
63
|
-
console.log(chalk_1.default.blue('\n💡
|
|
63
|
+
console.log(chalk_1.default.blue('\n💡 Ask your agent to use the FRAIM connect-mcp skill for GitHub, GitLab, Jira, or other tools.'));
|
|
64
64
|
console.log(chalk_1.default.yellow('\n💡 Existing MCP servers will be preserved - only missing servers will be added.'));
|
|
65
65
|
const response = await (0, prompts_1.default)({
|
|
66
66
|
type: 'text',
|
|
@@ -87,15 +87,6 @@ const promptForIDESelection = async (detectedIDEs) => {
|
|
|
87
87
|
return selectedIndices.map((i) => detectedIDEs[i]).filter((ide) => ide !== undefined);
|
|
88
88
|
};
|
|
89
89
|
exports.promptForIDESelection = promptForIDESelection;
|
|
90
|
-
// Re-export promptForProviderToken for backward compatibility
|
|
91
|
-
// This maintains the same interface but uses the generic system
|
|
92
|
-
const promptForGitHubToken = async () => {
|
|
93
|
-
const { promptForProviderToken } = await Promise.resolve().then(() => __importStar(require('./provider-prompts')));
|
|
94
|
-
const { getProviderClient } = await Promise.resolve().then(() => __importStar(require('../api/get-provider-client')));
|
|
95
|
-
const client = getProviderClient();
|
|
96
|
-
return promptForProviderToken(client, 'github');
|
|
97
|
-
};
|
|
98
|
-
exports.promptForGitHubToken = promptForGitHubToken;
|
|
99
90
|
const backupConfig = (configPath) => {
|
|
100
91
|
if (fs_1.default.existsSync(configPath)) {
|
|
101
92
|
const backupPath = `${configPath}.fraim-backup-${Date.now()}`;
|
|
@@ -188,8 +179,8 @@ const configureIDEMCP = async (ide, fraimKey) => {
|
|
|
188
179
|
const addedServers = [];
|
|
189
180
|
const updatedServers = [];
|
|
190
181
|
const skippedServers = [];
|
|
191
|
-
//
|
|
192
|
-
const alwaysUpdateServers = new Set(['fraim'
|
|
182
|
+
// FRAIM owns its own base server entry. Third-party entries are preserved.
|
|
183
|
+
const alwaysUpdateServers = new Set(['fraim']);
|
|
193
184
|
for (const [serverName, serverConfig] of Object.entries(newMCPServers)) {
|
|
194
185
|
if (!existingMCPServers[serverName]) {
|
|
195
186
|
mergedMCPServers[serverName] = serverConfig;
|
|
@@ -12,6 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.SYNCED_CONTENT_BANNER_MARKER = void 0;
|
|
15
|
+
exports.fetchRegistryFiles = fetchRegistryFiles;
|
|
16
|
+
exports.syncScriptsToUserDir = syncScriptsToUserDir;
|
|
15
17
|
exports.syncFromRemote = syncFromRemote;
|
|
16
18
|
const axios_1 = __importDefault(require("axios"));
|
|
17
19
|
const fs_1 = require("fs");
|
|
@@ -177,6 +179,42 @@ function applySyncedContentBanner(file) {
|
|
|
177
179
|
const banner = buildSyncedContentBanner(typeLabel);
|
|
178
180
|
return insertAfterFrontmatter(file.content, banner);
|
|
179
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Fetch all registry files from the remote FRAIM server.
|
|
184
|
+
* Extracted so callers can partition the result (scripts vs project stubs).
|
|
185
|
+
*/
|
|
186
|
+
async function fetchRegistryFiles(remoteUrl, apiKey) {
|
|
187
|
+
const response = await fetchRegistrySync(remoteUrl, apiKey);
|
|
188
|
+
return response.data.files || [];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Sync script files to the user-level ~/.fraim/scripts/ directory.
|
|
192
|
+
* Extracted from syncFromRemote so it can run without a project root.
|
|
193
|
+
*/
|
|
194
|
+
async function syncScriptsToUserDir(files) {
|
|
195
|
+
const scriptFiles = files.filter(f => f.type === 'script');
|
|
196
|
+
if (scriptFiles.length === 0)
|
|
197
|
+
return 0;
|
|
198
|
+
const userDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
199
|
+
const scriptsDir = (0, path_1.join)(userDir, 'scripts');
|
|
200
|
+
if (!(0, fs_1.existsSync)(scriptsDir)) {
|
|
201
|
+
(0, fs_1.mkdirSync)(scriptsDir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
cleanDirectory(scriptsDir, (candidatePath) => {
|
|
204
|
+
if ((0, path_1.resolve)(candidatePath) !== (0, path_1.resolve)(scriptsDir)) {
|
|
205
|
+
assertPathInsideDirectory(scriptsDir, candidatePath, 'script directory');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
for (const file of scriptFiles) {
|
|
209
|
+
const { filePath } = resolveUserRegistryFile(scriptsDir, file.path, 'script file');
|
|
210
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
211
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
212
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
215
|
+
}
|
|
216
|
+
return scriptFiles.length;
|
|
217
|
+
}
|
|
180
218
|
/**
|
|
181
219
|
* Sync jobs and scripts from remote FRAIM server
|
|
182
220
|
*/
|
|
@@ -200,9 +238,7 @@ async function syncFromRemote(options) {
|
|
|
200
238
|
const assertWorkspacePath = (0, project_fraim_paths_1.createWorkspaceFraimPathAsserter)(options.projectRoot);
|
|
201
239
|
console.log(chalk_1.default.blue('🔄 Syncing from remote FRAIM server...'));
|
|
202
240
|
console.log(chalk_1.default.gray(` Remote: ${remoteUrl}`));
|
|
203
|
-
|
|
204
|
-
const response = await fetchRegistrySync(remoteUrl, apiKey);
|
|
205
|
-
const files = response.data.files || [];
|
|
241
|
+
const files = options.registryFiles || await fetchRegistryFiles(remoteUrl, apiKey);
|
|
206
242
|
if (!files || files.length === 0) {
|
|
207
243
|
console.log(chalk_1.default.yellow('⚠️ No files received from remote server'));
|
|
208
244
|
return {
|
|
@@ -297,29 +333,9 @@ async function syncFromRemote(options) {
|
|
|
297
333
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
298
334
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/rules/${relativePath}`)} (stub)`));
|
|
299
335
|
}
|
|
300
|
-
//
|
|
336
|
+
// Scripts are synced by machine-level layer (syncScriptsToUserDir).
|
|
337
|
+
// Only count them for the result.
|
|
301
338
|
const scriptFiles = files.filter(f => f.type === 'script');
|
|
302
|
-
const userDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
303
|
-
const scriptsDir = (0, path_1.join)(userDir, 'scripts');
|
|
304
|
-
if (!(0, fs_1.existsSync)(scriptsDir)) {
|
|
305
|
-
(0, fs_1.mkdirSync)(scriptsDir, { recursive: true });
|
|
306
|
-
}
|
|
307
|
-
// Clean existing scripts
|
|
308
|
-
cleanDirectory(scriptsDir, (candidatePath) => {
|
|
309
|
-
if ((0, path_1.resolve)(candidatePath) !== (0, path_1.resolve)(scriptsDir)) {
|
|
310
|
-
assertPathInsideDirectory(scriptsDir, candidatePath, 'script directory');
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
// Write script files
|
|
314
|
-
for (const file of scriptFiles) {
|
|
315
|
-
const { filePath, relativePath } = resolveUserRegistryFile(scriptsDir, file.path, 'script file');
|
|
316
|
-
const fileDir = (0, path_1.dirname)(filePath);
|
|
317
|
-
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
318
|
-
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
319
|
-
}
|
|
320
|
-
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
321
|
-
console.log(chalk_1.default.gray(` + ${relativePath}`));
|
|
322
|
-
}
|
|
323
339
|
// Sync docs to fraim/docs/
|
|
324
340
|
const docsFiles = files.filter(f => f.type === 'docs');
|
|
325
341
|
const docsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'docs');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AIMentor = void 0;
|
|
4
4
|
const include_resolver_1 = require("./utils/include-resolver");
|
|
5
|
+
const resolve_phase_edge_1 = require("./resolve-phase-edge");
|
|
5
6
|
class AIMentor {
|
|
6
7
|
constructor(resolver, skillDedup) {
|
|
7
8
|
this.jobCache = new Map();
|
|
@@ -36,7 +37,10 @@ class AIMentor {
|
|
|
36
37
|
return await this.generateCompletionMessage(workflow, args.currentPhase, args.findings, args.evidence, args.skipIncludes);
|
|
37
38
|
}
|
|
38
39
|
else {
|
|
39
|
-
|
|
40
|
+
// Issue #1123: findings/evidence must reach the failure path too. The
|
|
41
|
+
// MCP layer has always forwarded them for every status; only this hop
|
|
42
|
+
// dropped them, which is why a failure edge could not be discriminated.
|
|
43
|
+
return await this.generateHelpMessage(workflow, args.currentPhase, args.status, args.skipIncludes, args.findings, args.evidence);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
async getOrLoadJob(jobType) {
|
|
@@ -65,15 +69,21 @@ class AIMentor {
|
|
|
65
69
|
return '';
|
|
66
70
|
const onSuccess = phaseFlow.onSuccess;
|
|
67
71
|
const completionCall = `seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete" })`;
|
|
72
|
+
// Issue #1123: a routing map nobody knows how to trigger is decorative,
|
|
73
|
+
// which is the defect recorded as #1135. Whichever edge carries a map, the
|
|
74
|
+
// footer has to name its outcomes so the agent can actually supply one.
|
|
75
|
+
const failureOutcomes = (0, resolve_phase_edge_1.discriminantKeys)(phaseFlow.onFailure);
|
|
76
|
+
const failureNote = failureOutcomes.length > 0
|
|
77
|
+
? `\nIf this phase needs to loop back, call the same tool with \`status: "failure"\`, and set \`findings.phaseOutcome\` to one of: ${failureOutcomes.map((key) => `"${key}"`).join(' | ')} when one applies. Omit it otherwise.`
|
|
78
|
+
: '';
|
|
68
79
|
if (!onSuccess || typeof onSuccess === 'string') {
|
|
69
80
|
const finalPhaseNote = onSuccess ? '' : ' This is the final phase.';
|
|
70
|
-
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`${completionCall}\`.${finalPhaseNote}`;
|
|
81
|
+
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`${completionCall}\`.${finalPhaseNote}${failureNote}`;
|
|
71
82
|
}
|
|
72
|
-
const validOutcomes =
|
|
73
|
-
.filter((key) => key !== 'default')
|
|
83
|
+
const validOutcomes = (0, resolve_phase_edge_1.discriminantKeys)(onSuccess)
|
|
74
84
|
.map((key) => `"${key}"`)
|
|
75
85
|
.join(' | ');
|
|
76
|
-
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}
|
|
86
|
+
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}.${failureNote}`;
|
|
77
87
|
}
|
|
78
88
|
/** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
|
|
79
89
|
async getPhaseAuthorityContent() {
|
|
@@ -158,13 +168,9 @@ class AIMentor {
|
|
|
158
168
|
const phaseFlow = workflow.metadata.phases?.[phaseId];
|
|
159
169
|
let nextPhaseId = null;
|
|
160
170
|
if (phaseFlow && phaseFlow.onSuccess) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
else {
|
|
165
|
-
const outcome = findings?.phaseOutcome ?? findings?.issueType ?? evidence?.issueType ?? evidence?.phaseOutcome ?? 'default';
|
|
166
|
-
nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess.default ?? null;
|
|
167
|
-
}
|
|
171
|
+
// Issue #1123: resolved through the shared authority so the success and
|
|
172
|
+
// failure paths cannot read the discriminant differently.
|
|
173
|
+
nextPhaseId = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseFlow.onSuccess, (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence));
|
|
168
174
|
}
|
|
169
175
|
let message = '';
|
|
170
176
|
if (nextPhaseId) {
|
|
@@ -189,7 +195,7 @@ class AIMentor {
|
|
|
189
195
|
status: 'complete'
|
|
190
196
|
};
|
|
191
197
|
}
|
|
192
|
-
async generateHelpMessage(workflow, phaseId, status, skipIncludes) {
|
|
198
|
+
async generateHelpMessage(workflow, phaseId, status, skipIncludes, findings, evidence) {
|
|
193
199
|
const entityType = 'Job';
|
|
194
200
|
if (workflow.isSimple) {
|
|
195
201
|
const message = `${entityType}: ${workflow.metadata.name}\n\n${workflow.overview}`;
|
|
@@ -201,7 +207,14 @@ class AIMentor {
|
|
|
201
207
|
};
|
|
202
208
|
}
|
|
203
209
|
const phaseMeta = workflow.metadata.phases?.[phaseId];
|
|
204
|
-
|
|
210
|
+
// Issue #1123: the failure edge may be a discriminant map. Resolving to
|
|
211
|
+
// null (terminal, malformed, or a map with no `default` and no match)
|
|
212
|
+
// falls back to self-retry, which is what the previous `|| phaseId` did
|
|
213
|
+
// for an absent edge. The old expression could not do this: an object is
|
|
214
|
+
// truthy, so it was returned as the target and `phases.get()` then missed.
|
|
215
|
+
const targetPhaseId = status === 'failure'
|
|
216
|
+
? ((0, resolve_phase_edge_1.resolvePhaseEdge)(phaseMeta?.onFailure, (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence)) || phaseId)
|
|
217
|
+
: phaseId;
|
|
205
218
|
let message = `### Current Phase: ${targetPhaseId}\n\n`;
|
|
206
219
|
let instructions = workflow.phases.get(targetPhaseId);
|
|
207
220
|
if (instructions) {
|