fraim 2.0.342 → 2.0.344
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/registry/scripts/artifact-paths.js +108 -0
- package/dist/registry/scripts/learning/completion-state.js +205 -5
- package/dist/registry/scripts/learning-usage/local-runtime.js +217 -0
- package/dist/src/cli/commands/init-project.js +2 -12
- package/dist/src/cli/commands/sync.js +36 -162
- package/dist/src/cli/setup/ide-invocation-surfaces.js +3 -3
- package/dist/src/cli/setup/user-level-sync.js +3 -3
- package/dist/src/cli/utils/agent-adapters.js +12 -12
- package/dist/src/cli/utils/fraim-gitignore.js +28 -3
- package/dist/src/cli/utils/pack-home.js +10 -0
- package/dist/src/cli/utils/project-catalog-cleanup.js +62 -0
- package/dist/src/cli/utils/remote-sync.js +26 -2
- package/dist/src/config/persona-hiring.js +7 -1
- package/dist/src/core/ai-mentor.js +42 -11
- package/dist/src/core/config-loader.js +4 -0
- package/dist/src/core/fraim-config-schema.generated.js +22 -0
- package/dist/src/core/handoff-contracts.js +8 -3
- package/dist/src/core/utils/local-registry-resolver.js +33 -57
- package/dist/src/fraim/db-service.js +127 -118
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +79 -6
- package/dist/src/local-mcp-server/learning-context-builder.js +101 -105
- package/dist/src/local-mcp-server/learning-source-record.js +122 -0
- package/dist/src/local-mcp-server/learning-usage-attestation.js +66 -56
- package/dist/src/local-mcp-server/learning-usage-command.js +5 -4
- package/dist/src/local-mcp-server/stdio-server.js +10 -7
- package/dist/src/local-mcp-server/usage-collector.js +2 -0
- package/dist/src/mcp/tool-schemas.js +5 -1
- package/dist/src/services/mcp-service.js +6 -4
- package/dist/src/services/usage-analytics-service.js +1 -0
- package/package.json +3 -1
|
@@ -45,10 +45,8 @@ const config_loader_1 = require("../../core/config-loader");
|
|
|
45
45
|
const version_utils_1 = require("../utils/version-utils");
|
|
46
46
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
47
47
|
const git_utils_1 = require("../../core/utils/git-utils");
|
|
48
|
-
const
|
|
48
|
+
const project_catalog_cleanup_1 = require("../utils/project-catalog-cleanup");
|
|
49
49
|
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
50
|
-
const user_config_1 = require("../utils/user-config");
|
|
51
|
-
const ide_detector_1 = require("../setup/ide-detector");
|
|
52
50
|
const remote_sync_1 = require("../utils/remote-sync");
|
|
53
51
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
54
52
|
function resolveExplicitLocalSyncUrl() {
|
|
@@ -113,66 +111,6 @@ function removeLegacyVersionFromConfig(fraimDir) {
|
|
|
113
111
|
console.warn(chalk_1.default.yellow('Could not remove legacy version from config.json.'));
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
|
-
function resolveAllowedConfigTypes() {
|
|
117
|
-
const persisted = (0, user_config_1.getInstalledIdes)();
|
|
118
|
-
const liveDetected = [
|
|
119
|
-
...(0, ide_detector_1.detectInstalledIDEs)(),
|
|
120
|
-
...(0, ide_detector_1.detectInstalledIDEs)('cli-runnable')
|
|
121
|
-
].map((ide) => (0, ide_detector_1.getAdapterConfigType)(ide));
|
|
122
|
-
const liveSet = [...new Set(liveDetected)];
|
|
123
|
-
if (!persisted) {
|
|
124
|
-
return liveSet.length > 0 ? liveSet : null;
|
|
125
|
-
}
|
|
126
|
-
const merged = [...new Set([...persisted, ...liveSet])];
|
|
127
|
-
return merged.length > 0 ? merged : null;
|
|
128
|
-
}
|
|
129
|
-
async function cleanupStaleAdapterFiles(projectRoot, allowedConfigTypes) {
|
|
130
|
-
if (!allowedConfigTypes) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
(0, project_fraim_paths_1.assertWorkspaceFraimRoot)(projectRoot);
|
|
134
|
-
const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
135
|
-
const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
|
|
136
|
-
const execFileAsync = promisify(execFile);
|
|
137
|
-
for (const [relPath, configType] of Object.entries((0, agent_adapters_1.getAdapterConfigTypes)())) {
|
|
138
|
-
const configTypes = Array.isArray(configType) ? configType : [configType];
|
|
139
|
-
if (configTypes.includes('standard') || configTypes.some((type) => allowedConfigTypes.includes(type))) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const fullPath = path_1.default.join(projectRoot, relPath);
|
|
143
|
-
if (!fs_1.default.existsSync(fullPath)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const content = fs_1.default.readFileSync(fullPath, 'utf8');
|
|
148
|
-
if (!content.includes(remote_sync_1.SYNCED_CONTENT_BANNER_MARKER)) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
await execFileAsync('git', ['rm', '--cached', relPath], { cwd: projectRoot });
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Not tracked or git unavailable — still clean up from disk and gitignore.
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
(0, fraim_gitignore_1.addToFraimGitignore)(projectRoot, relPath);
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// Best-effort.
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
fs_1.default.unlinkSync(fullPath);
|
|
169
|
-
console.log(chalk_1.default.green(`Removed stale FRAIM adapter file: ${relPath}`));
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Best-effort.
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
114
|
function failSync(mode, message) {
|
|
177
115
|
if (mode === 'throw') {
|
|
178
116
|
throw new Error(message);
|
|
@@ -182,10 +120,13 @@ function failSync(mode, message) {
|
|
|
182
120
|
const runSync = async (options) => {
|
|
183
121
|
const failHard = options.failHard ?? 'exit';
|
|
184
122
|
const projectRoot = options.projectRoot ? path_1.default.resolve(options.projectRoot) : process.cwd();
|
|
123
|
+
const useLocal = options.local || process.env.FRAIM_LOCAL_SYNC === '1';
|
|
185
124
|
// ─── Credential resolution (before project detection) ─────────────────────
|
|
186
125
|
// Order: user-level config → env var → project config (only if in a project)
|
|
187
|
-
const remoteUrl =
|
|
188
|
-
|
|
126
|
+
const remoteUrl = useLocal
|
|
127
|
+
? resolveExplicitLocalSyncUrl() || `http://localhost:${process.env.FRAIM_MCP_PORT || (0, git_utils_1.getPort)()}`
|
|
128
|
+
: process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
129
|
+
let apiKey = useLocal ? 'local-dev' : loadUserApiKey() || process.env.FRAIM_API_KEY;
|
|
189
130
|
const hasProject = (0, project_fraim_paths_1.workspaceFraimExists)(projectRoot);
|
|
190
131
|
if (!apiKey && hasProject) {
|
|
191
132
|
const config = (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
|
|
@@ -272,60 +213,7 @@ const runSync = async (options) => {
|
|
|
272
213
|
}
|
|
273
214
|
catch { /* reporting must never fail a sync */ }
|
|
274
215
|
};
|
|
275
|
-
//
|
|
276
|
-
const useLocal = options.local || process.env.FRAIM_LOCAL_SYNC === '1';
|
|
277
|
-
if (useLocal) {
|
|
278
|
-
console.log(chalk_1.default.blue('Syncing FRAIM jobs from local server...'));
|
|
279
|
-
const localPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : (0, git_utils_1.getPort)();
|
|
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
|
-
}
|
|
286
|
-
const localRegistryFiles = await (0, remote_sync_1.fetchRegistryFiles)(localUrl, 'local-dev');
|
|
287
|
-
const result = await syncFromRemote({
|
|
288
|
-
remoteUrl: localUrl,
|
|
289
|
-
apiKey: 'local-dev',
|
|
290
|
-
projectRoot,
|
|
291
|
-
skipUpdates: true,
|
|
292
|
-
registryFiles: localRegistryFiles
|
|
293
|
-
});
|
|
294
|
-
if (result.success) {
|
|
295
|
-
// Local-dev sync bypasses Layer 1 (machine-level sync) below, but scripts,
|
|
296
|
-
// docs, and job stubs still need to land in ~/.fraim/ or the reported counts
|
|
297
|
-
// are a lie.
|
|
298
|
-
await (0, remote_sync_1.syncScriptsToUserDir)(localRegistryFiles);
|
|
299
|
-
await (0, remote_sync_1.syncDocsToUserDir)(localRegistryFiles);
|
|
300
|
-
await (0, remote_sync_1.syncJobsToUserDir)(localRegistryFiles);
|
|
301
|
-
const { ensureUserLevelDependencies: ensureLocalUserLevelDependencies } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
302
|
-
ensureLocalUserLevelDependencies();
|
|
303
|
-
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`));
|
|
304
|
-
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
305
|
-
removeLegacyVersionFromConfig(fraimDir);
|
|
306
|
-
writeSyncMetadata('local', localUrl);
|
|
307
|
-
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
308
|
-
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
309
|
-
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
310
|
-
}
|
|
311
|
-
if (ignoreUpdate.gitignoreUpdated) {
|
|
312
|
-
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
313
|
-
}
|
|
314
|
-
const allowedLocalTypes = resolveAllowedConfigTypes();
|
|
315
|
-
await cleanupStaleAdapterFiles(projectRoot, allowedLocalTypes);
|
|
316
|
-
const localAdapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedLocalTypes);
|
|
317
|
-
if (localAdapterUpdates.length > 0) {
|
|
318
|
-
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${localAdapterUpdates.join(', ')}`));
|
|
319
|
-
}
|
|
320
|
-
await refreshOrgCache(localUrl, 'local-dev');
|
|
321
|
-
await refreshManagerCache(localUrl, 'local-dev');
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
|
|
325
|
-
console.error(chalk_1.default.yellow('Make sure the FRAIM MCP server is running locally (npm run dev).'));
|
|
326
|
-
failSync(failHard, `Local sync failed: ${result.error}`);
|
|
327
|
-
}
|
|
328
|
-
// ─── Global install notice ────────────────────────────────────────────────
|
|
216
|
+
// Local and remote sources share the same machine-level writers.
|
|
329
217
|
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
330
218
|
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
331
219
|
if (isGlobal && !options.skipUpdates) {
|
|
@@ -337,7 +225,7 @@ const runSync = async (options) => {
|
|
|
337
225
|
// LAYER 1 — Machine-level sync (always runs)
|
|
338
226
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
339
227
|
console.log(chalk_1.default.blue('Syncing machine-level FRAIM content (~/.fraim/)...'));
|
|
340
|
-
// 1a. Fetch registry files
|
|
228
|
+
// 1a. Fetch registry files once for every machine-level writer
|
|
341
229
|
let registryFiles = [];
|
|
342
230
|
try {
|
|
343
231
|
registryFiles = await (0, remote_sync_1.fetchRegistryFiles)(remoteUrl, apiKey);
|
|
@@ -370,6 +258,10 @@ const runSync = async (options) => {
|
|
|
370
258
|
if (machineEmployeeJobsSynced > 0 || machineManagerJobsSynced > 0) {
|
|
371
259
|
console.log(chalk_1.default.green(` Synced ${machineEmployeeJobsSynced} ai-employee jobs and ${machineManagerJobsSynced} ai-manager jobs to ~/.fraim/`));
|
|
372
260
|
}
|
|
261
|
+
const { skillsSynced, rulesSynced } = await (0, remote_sync_1.syncSkillsAndRulesToUserDir)(registryFiles);
|
|
262
|
+
if (skillsSynced > 0 || rulesSynced > 0) {
|
|
263
|
+
console.log(chalk_1.default.green(` Synced ${skillsSynced} skills and ${rulesSynced} rules to ~/.fraim/`));
|
|
264
|
+
}
|
|
373
265
|
// 1c. Refresh org home
|
|
374
266
|
await refreshOrgCache(remoteUrl, apiKey);
|
|
375
267
|
// 1d. Refresh manager home
|
|
@@ -389,45 +281,34 @@ const runSync = async (options) => {
|
|
|
389
281
|
// 1f. Ensure user-level directories and runtime dependencies (Issue #1508:
|
|
390
282
|
// synced scripts like the DOCX review layer require npm deps such as
|
|
391
283
|
// adm-zip; a plain sync must install them, not just create directories)
|
|
392
|
-
const {
|
|
393
|
-
|
|
394
|
-
ensureUserLevelDependencies();
|
|
284
|
+
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../setup/user-level-sync')));
|
|
285
|
+
await syncUserLevelArtifacts();
|
|
395
286
|
// 1g. Write sync metadata
|
|
396
|
-
writeSyncMetadata('remote', remoteUrl);
|
|
287
|
+
writeSyncMetadata(useLocal ? 'local' : 'remote', remoteUrl);
|
|
397
288
|
console.log(chalk_1.default.green('✅ Machine-level sync complete.'));
|
|
398
289
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
399
|
-
//
|
|
290
|
+
// Project maintenance only: no catalog or adapter writes.
|
|
400
291
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
401
|
-
if (options.global)
|
|
402
|
-
console.log(chalk_1.default.gray('--global: skipping project-level sync.'));
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (!hasProject) {
|
|
406
|
-
console.log(chalk_1.default.cyan('No FRAIM project detected at cwd. Machine-level sync complete.'));
|
|
407
|
-
console.log(chalk_1.default.cyan('Run from a project folder or pass --project-root to sync project stubs.'));
|
|
292
|
+
if (options.global || !hasProject)
|
|
408
293
|
return;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (process.env.TEST_MODE === 'true') {
|
|
424
|
-
console.log(chalk_1.default.yellow('TEST_MODE: Continuing without project sync.'));
|
|
294
|
+
if ((0, project_catalog_cleanup_1.hasLegacyProjectCatalogs)(projectRoot)) {
|
|
295
|
+
// An empty/partial response (including TEST_MODE fetch continuation) is
|
|
296
|
+
// not evidence that all generated project content has a replacement.
|
|
297
|
+
const replacementsSynced = machineEmployeeJobsSynced > 0 && machineManagerJobsSynced > 0
|
|
298
|
+
&& skillsSynced > 0 && rulesSynced > 0 && docsSynced > 0;
|
|
299
|
+
if (!replacementsSynced) {
|
|
300
|
+
console.warn(chalk_1.default.yellow('Project catalog cleanup skipped: replacement machine catalogs are incomplete. Existing catalogs and exclusions were retained.'));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
(0, project_catalog_cleanup_1.removeLegacyProjectCatalogs)(projectRoot);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
console.warn(chalk_1.default.yellow(`Project catalog cleanup failed: ${error instanceof Error ? error.message : String(error)}. Remaining catalogs and exclusions were retained; retry sync after resolving the error.`));
|
|
425
308
|
return;
|
|
426
309
|
}
|
|
427
|
-
failSync(failHard, `Project sync failed: ${result.error}`);
|
|
428
310
|
}
|
|
429
|
-
|
|
430
|
-
removeLegacyVersionFromConfig(fraimDir);
|
|
311
|
+
removeLegacyVersionFromConfig((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot));
|
|
431
312
|
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
432
313
|
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
433
314
|
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
@@ -435,21 +316,14 @@ const runSync = async (options) => {
|
|
|
435
316
|
if (ignoreUpdate.gitignoreUpdated) {
|
|
436
317
|
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
437
318
|
}
|
|
438
|
-
const allowedTypes = resolveAllowedConfigTypes();
|
|
439
|
-
await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
|
|
440
|
-
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedTypes);
|
|
441
|
-
if (adapterUpdates.length > 0) {
|
|
442
|
-
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
443
|
-
}
|
|
444
|
-
console.log(chalk_1.default.green('✅ Project-level sync complete.'));
|
|
445
319
|
};
|
|
446
320
|
exports.runSync = runSync;
|
|
447
321
|
exports.syncCommand = new commander_1.Command('sync')
|
|
448
|
-
.description('Sync
|
|
322
|
+
.description('Sync machine-level FRAIM catalogs, scripts, docs and agent integration')
|
|
449
323
|
.option('-f, --force', 'Force sync even if digest matches')
|
|
450
324
|
.option('--skip-updates', 'Skip checking for CLI updates (legacy)')
|
|
451
325
|
.option('--local', 'Sync from local development server (port derived from git branch)')
|
|
452
|
-
.option('--global', 'Sync machine-level FRAIM content only (~/.fraim/), skip project
|
|
453
|
-
.option('--project-root <path>', 'Explicit project root
|
|
454
|
-
.option('--project-adapters', '
|
|
326
|
+
.option('--global', 'Sync machine-level FRAIM content only (~/.fraim/), skip project maintenance')
|
|
327
|
+
.option('--project-root <path>', 'Explicit project root for configuration and local exclusion maintenance')
|
|
328
|
+
.option('--project-adapters', 'Deprecated compatibility option (no effect; use machine-level FRAIM setup)')
|
|
455
329
|
.action(exports.runSync);
|
|
@@ -68,11 +68,11 @@ function buildFraimInvocationBody(profile = 'none') {
|
|
|
68
68
|
${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
69
69
|
Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
|
|
70
70
|
|
|
71
|
-
2. **
|
|
72
|
-
|
|
71
|
+
2. **Discover jobs when activation did not name an exact FRAIM job**:
|
|
72
|
+
Read \`~/.fraim/ai-employee/jobs/INDEX.md\` and \`~/.fraim/ai-manager/jobs/INDEX.md\` first (each lists every job's name, one-line intent, and category) and match the request against them; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning stubs in those machine directories if an index is missing or stale. Also check \`fraim/personalized-employee/jobs/INDEX.md\` (or its stubs, if no index is present) for project overrides or repo-specific jobs. Project baseline catalog directories are not required. If machine files are missing or you cannot inspect local files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
|
|
73
73
|
|
|
74
74
|
3. **Find the match**:
|
|
75
|
-
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the
|
|
75
|
+
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the machine stub catalog, \`fraim/personalized-employee/jobs/\` (project overrides take precedence), or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists, call \`get_fraim_job({ job: "adhoc-prompt" })\` directly and execute it with the user's instructions as the task input — no confirmation question, and do not pick the nearest catalog job.
|
|
76
76
|
|
|
77
77
|
4. **Load the full content**:
|
|
78
78
|
- For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
|
|
@@ -49,9 +49,9 @@ exports.syncUserLevelArtifacts = syncUserLevelArtifacts;
|
|
|
49
49
|
* - scripts/ — locally-executed scripts (synced during any fraim sync)
|
|
50
50
|
* - personalized-employee/ — user-level overrides and learnings
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* Sync writes machine-level job/skill/rule discovery stubs separately. These
|
|
53
|
+
* adapters route agents to those catalogs; the MCP proxy serves full executable
|
|
54
|
+
* instructions through get_fraim_job() and get_fraim_file().
|
|
55
55
|
*
|
|
56
56
|
* Part of: User-Level FRAIM Artifacts with Local Shadow Semantics
|
|
57
57
|
*/
|
|
@@ -52,11 +52,11 @@ function mergeCursorRule(existingContent, managedSection) {
|
|
|
52
52
|
return `${ide_invocation_surfaces_1.CURSOR_MDC_FRONTMATTER}\n\n${mergedBody}\n`;
|
|
53
53
|
}
|
|
54
54
|
function getAdapterFiles(allowedConfigTypes = null) {
|
|
55
|
-
const fraimRoot =
|
|
56
|
-
const employeeJobsPath =
|
|
57
|
-
const managerJobsPath =
|
|
58
|
-
const employeeSkillsPath =
|
|
59
|
-
const employeeRulesPath =
|
|
55
|
+
const fraimRoot = '~/.fraim';
|
|
56
|
+
const employeeJobsPath = `${fraimRoot}/ai-employee/jobs`;
|
|
57
|
+
const managerJobsPath = `${fraimRoot}/ai-manager/jobs`;
|
|
58
|
+
const employeeSkillsPath = `${fraimRoot}/ai-employee/skills`;
|
|
59
|
+
const employeeRulesPath = `${fraimRoot}/ai-employee/rules`;
|
|
60
60
|
const personalizedRootPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee');
|
|
61
61
|
const projectRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/rules/project_rules.md');
|
|
62
62
|
const markdownBody = buildManagedSection(`
|
|
@@ -91,7 +91,7 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
|
|
|
91
91
|
const copilotBody = buildManagedSection(`
|
|
92
92
|
## FRAIM
|
|
93
93
|
|
|
94
|
-
- Use \`${fraimRoot}/\` as the
|
|
94
|
+
- Use \`${fraimRoot}/\` as the machine-wide FRAIM catalog.
|
|
95
95
|
- FRAIM jobs are the primary execution units and should be treated like first-class workflows.
|
|
96
96
|
- FRAIM skills are reusable capabilities jobs compose.
|
|
97
97
|
- FRAIM rules are always-on constraints and conventions.
|
|
@@ -99,19 +99,19 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
|
|
|
99
99
|
- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
|
|
100
100
|
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
101
101
|
- If the user names an exact FRAIM job, fetch that full job directly with FRAIM MCP tools.
|
|
102
|
-
- When FRAIM routing is active but the job is not exact, read
|
|
102
|
+
- When FRAIM routing is active but the job is not exact, read \`${employeeJobsPath}/INDEX.md\` and \`${managerJobsPath}/INDEX.md\` first to identify which job to invoke (falling back to their individual stub files if an index is missing or stale). Also check \`${personalizedRootPath}/jobs/INDEX.md\` or its stubs for project overrides before fetching full content with FRAIM MCP tools.
|
|
103
103
|
- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
|
|
104
104
|
- **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
105
105
|
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
106
106
|
`);
|
|
107
107
|
const fraimReadme = `# FRAIM Catalog
|
|
108
108
|
|
|
109
|
-
This directory
|
|
109
|
+
This directory holds project-specific FRAIM content. The shared discovery catalog lives under \`~/.fraim/\`.
|
|
110
110
|
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
111
|
+
- \`~/.fraim/ai-employee/jobs/\`: employee job stubs (+ \`INDEX.md\`)
|
|
112
|
+
- \`~/.fraim/ai-manager/jobs/\`: manager job stubs (+ \`INDEX.md\`)
|
|
113
|
+
- \`~/.fraim/ai-employee/skills/\`: skill stubs (+ \`INDEX.md\`)
|
|
114
|
+
- \`~/.fraim/ai-employee/rules/\`: rule stubs
|
|
115
115
|
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
116
116
|
|
|
117
117
|
When FRAIM routing is active and no exact FRAIM job is named, read the \`INDEX.md\` in the relevant directory first to discover which FRAIM job, skill, or rule is relevant (falling back to scanning the individual stubs if no index is present), then load the full content through FRAIM MCP tools. If an exact FRAIM job is named, load it directly. For ordinary requests, do not scan this catalog first.
|
|
@@ -7,12 +7,11 @@ exports.ensureFraimSyncedContentLocallyExcluded = exports.removeFraimSyncedConte
|
|
|
7
7
|
exports.addToFraimGitignore = addToFraimGitignore;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const project_catalog_cleanup_1 = require("./project-catalog-cleanup");
|
|
10
12
|
exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
|
|
11
13
|
exports.FRAIM_SYNC_GITIGNORE_END = '# END FRAIM SYNCED CONTENT';
|
|
12
14
|
exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
|
|
13
|
-
'fraim/ai-employee/',
|
|
14
|
-
'fraim/ai-manager/',
|
|
15
|
-
'fraim/docs/',
|
|
16
15
|
'fraim/.sync-metadata.json',
|
|
17
16
|
];
|
|
18
17
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -57,6 +56,24 @@ function resolveGitInfoExcludePath(projectRoot) {
|
|
|
57
56
|
}
|
|
58
57
|
return path_1.default.join(gitDir, 'info', 'exclude');
|
|
59
58
|
}
|
|
59
|
+
// info/exclude belongs to the common Git directory, so one migrated worktree
|
|
60
|
+
// must not expose generated files in another worktree that has not synced yet.
|
|
61
|
+
function retainSharedCatalogExclusions(projectRoot, existing) {
|
|
62
|
+
if (!/^fraim\/(?:ai-employee|ai-manager|docs)\/\r?$/m.test(existing))
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
const listing = (0, child_process_1.execFileSync)('git', ['worktree', 'list', '--porcelain', '-z'], {
|
|
66
|
+
cwd: projectRoot, encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
});
|
|
68
|
+
const roots = listing.split('\0').filter(field => field.startsWith('worktree '))
|
|
69
|
+
.map(field => field.slice('worktree '.length));
|
|
70
|
+
// Unknown worktree state is not evidence that their catalogs are gone.
|
|
71
|
+
return roots.length === 0 || roots.some(root => (0, project_catalog_cleanup_1.hasLegacyProjectCatalogs)(root));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
|
|
61
78
|
const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
|
|
62
79
|
if (!fs_1.default.existsSync(gitignorePath)) {
|
|
@@ -87,6 +104,11 @@ function addToFraimGitignore(projectRoot, entry) {
|
|
|
87
104
|
fs_1.default.writeFileSync(gitignorePath, appended, 'utf8');
|
|
88
105
|
}
|
|
89
106
|
const ensureFraimSyncedContentLocallyExcluded = (projectRoot) => {
|
|
107
|
+
// init-project can call this before sync. Keep both legacy exclusion blocks
|
|
108
|
+
// intact until migration succeeds, including on skipped/failed sync or cleanup.
|
|
109
|
+
if ((0, project_catalog_cleanup_1.hasLegacyProjectCatalogs)(projectRoot)) {
|
|
110
|
+
return { gitignoreUpdated: false, gitInfoExcludeUpdated: false };
|
|
111
|
+
}
|
|
90
112
|
const gitignoreUpdated = (0, exports.removeFraimSyncedContentGitignoreBlock)(projectRoot);
|
|
91
113
|
const excludePath = resolveGitInfoExcludePath(projectRoot);
|
|
92
114
|
if (!excludePath) {
|
|
@@ -99,6 +121,9 @@ const ensureFraimSyncedContentLocallyExcluded = (projectRoot) => {
|
|
|
99
121
|
const existingRaw = fs_1.default.existsSync(excludePath)
|
|
100
122
|
? fs_1.default.readFileSync(excludePath, 'utf8')
|
|
101
123
|
: '';
|
|
124
|
+
if (retainSharedCatalogExclusions(projectRoot, existingRaw)) {
|
|
125
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: false };
|
|
126
|
+
}
|
|
102
127
|
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
103
128
|
const cleaned = normalizeWithoutManagedBlock(existingRaw);
|
|
104
129
|
const managedBlock = buildManagedBlock();
|
|
@@ -35,6 +35,7 @@ exports.ensurePackClone = ensurePackClone;
|
|
|
35
35
|
const child_process_1 = require("child_process");
|
|
36
36
|
const fs_1 = __importDefault(require("fs"));
|
|
37
37
|
const path_1 = __importDefault(require("path"));
|
|
38
|
+
const artifact_paths_1 = require("../../../registry/scripts/artifact-paths");
|
|
38
39
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
39
40
|
const user_config_1 = require("./user-config");
|
|
40
41
|
/**
|
|
@@ -84,10 +85,19 @@ function migrateOrgFlatContent(orgRoot) {
|
|
|
84
85
|
_migratedOrgRoots.add(orgRoot);
|
|
85
86
|
const dest = path_1.default.join(orgRoot, 'personalized-employee');
|
|
86
87
|
try {
|
|
88
|
+
// Root-relative output trees are authoritative, even when their names resemble legacy capabilities.
|
|
89
|
+
const artifacts = (0, artifact_paths_1.resolveArtifactPaths)(orgRoot, dest);
|
|
90
|
+
const protectedDirectories = [artifacts.evidence, artifacts.raw, artifacts.retrospectives, path_1.default.dirname(artifacts.archiveRaw)];
|
|
91
|
+
const contains = (root, target) => {
|
|
92
|
+
const relative = path_1.default.relative(root, target);
|
|
93
|
+
return relative === '' || (relative !== '..' && !relative.startsWith('..' + path_1.default.sep) && !path_1.default.isAbsolute(relative));
|
|
94
|
+
};
|
|
87
95
|
for (const dir of ORG_CONTENT_DIRS) {
|
|
88
96
|
const src = path_1.default.join(orgRoot, dir);
|
|
89
97
|
if (!fs_1.default.existsSync(src))
|
|
90
98
|
continue;
|
|
99
|
+
if (protectedDirectories.some(directory => contains(src, directory) || contains(directory, src)))
|
|
100
|
+
continue;
|
|
91
101
|
// Skip if already under personalized-employee.
|
|
92
102
|
const destDir = path_1.default.join(dest, dir);
|
|
93
103
|
if (fs_1.default.existsSync(destDir))
|
|
@@ -0,0 +1,62 @@
|
|
|
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.hasLegacyProjectCatalogs = hasLegacyProjectCatalogs;
|
|
7
|
+
exports.removeLegacyProjectCatalogs = removeLegacyProjectCatalogs;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
11
|
+
// Only generated catalog roots are eligible; adapters and personalization are not.
|
|
12
|
+
const LEGACY_PROJECT_CATALOGS = ['ai-employee', 'ai-manager', 'docs'];
|
|
13
|
+
function hasLegacyProjectCatalogs(projectRoot) {
|
|
14
|
+
return LEGACY_PROJECT_CATALOGS.some(name => fs_1.default.lstatSync(path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot), name), { throwIfNoEntry: false }) !== undefined);
|
|
15
|
+
}
|
|
16
|
+
function overlaps(left, right) {
|
|
17
|
+
const within = (parent, child) => {
|
|
18
|
+
const rel = path_1.default.relative(parent, child);
|
|
19
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${path_1.default.sep}`) && !path_1.default.isAbsolute(rel));
|
|
20
|
+
};
|
|
21
|
+
return within(left, right) || within(right, left);
|
|
22
|
+
}
|
|
23
|
+
/** Called only after all replacement machine catalogs have been synced. */
|
|
24
|
+
function removeLegacyProjectCatalogs(projectRoot) {
|
|
25
|
+
const project = path_1.default.resolve(projectRoot);
|
|
26
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(project);
|
|
27
|
+
if (fs_1.default.lstatSync(fraimDir).isSymbolicLink()) {
|
|
28
|
+
throw new Error('Refusing catalog cleanup through a linked fraim directory.');
|
|
29
|
+
}
|
|
30
|
+
const assertWorkspacePath = (0, project_fraim_paths_1.createWorkspaceFraimPathAsserter)(project);
|
|
31
|
+
const machineDir = fs_1.default.realpathSync((0, project_fraim_paths_1.getUserFraimDirPath)());
|
|
32
|
+
const targets = LEGACY_PROJECT_CATALOGS.map(name => ({ name, target: path_1.default.resolve(fraimDir, name) }))
|
|
33
|
+
.filter(({ target }) => fs_1.default.lstatSync(target, { throwIfNoEntry: false }) !== undefined);
|
|
34
|
+
const assertPlainTree = (entry) => {
|
|
35
|
+
const stat = fs_1.default.lstatSync(entry);
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
throw new Error(`Refusing catalog cleanup through link: ${entry}`);
|
|
38
|
+
assertWorkspacePath(entry);
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
for (const child of fs_1.default.readdirSync(entry))
|
|
41
|
+
assertPlainTree(path_1.default.join(entry, child));
|
|
42
|
+
}
|
|
43
|
+
else if (!stat.isFile()) {
|
|
44
|
+
throw new Error(`Refusing catalog cleanup of special file: ${entry}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// Preflight every tree before deleting any: links, overlap, or unreadable paths
|
|
48
|
+
// must not turn a partial inspection into destructive cleanup.
|
|
49
|
+
for (const { name, target } of targets) {
|
|
50
|
+
assertPlainTree(target);
|
|
51
|
+
(0, project_fraim_paths_1.assertExactWorkspaceFraimPath)(project, target, name);
|
|
52
|
+
if (!fs_1.default.lstatSync(target).isDirectory() || overlaps(fs_1.default.realpathSync(target), machineDir)) {
|
|
53
|
+
throw new Error(`Refusing catalog cleanup of non-directory or machine-overlapping path: ${target}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const { name, target } of targets) {
|
|
57
|
+
// Recheck immediately before removal; rmSync does not recurse through links.
|
|
58
|
+
assertPlainTree(target);
|
|
59
|
+
(0, project_fraim_paths_1.assertExactWorkspaceFraimPath)(project, target, name);
|
|
60
|
+
fs_1.default.rmSync(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -16,13 +16,13 @@ exports.fetchRegistryFiles = fetchRegistryFiles;
|
|
|
16
16
|
exports.syncScriptsToUserDir = syncScriptsToUserDir;
|
|
17
17
|
exports.syncDocsToUserDir = syncDocsToUserDir;
|
|
18
18
|
exports.syncJobsToUserDir = syncJobsToUserDir;
|
|
19
|
+
exports.syncSkillsAndRulesToUserDir = syncSkillsAndRulesToUserDir;
|
|
19
20
|
exports.syncFromRemote = syncFromRemote;
|
|
20
21
|
const axios_1 = __importDefault(require("axios"));
|
|
21
22
|
const fs_1 = require("fs");
|
|
22
23
|
const path_1 = require("path");
|
|
23
24
|
const chalk_1 = __importDefault(require("chalk"));
|
|
24
25
|
const script_sync_utils_1 = require("./script-sync-utils");
|
|
25
|
-
const fraim_gitignore_1 = require("./fraim-gitignore");
|
|
26
26
|
const capability_index_1 = require("../../core/utils/capability-index");
|
|
27
27
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
28
28
|
const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
|
|
@@ -70,7 +70,9 @@ function shouldLockSyncedContent() {
|
|
|
70
70
|
return !['0', 'false', 'off', 'no'].includes(normalized);
|
|
71
71
|
}
|
|
72
72
|
function getSyncedContentLockTargets(projectRoot) {
|
|
73
|
-
|
|
73
|
+
// This compatibility writer still materializes project catalogs. Their lock
|
|
74
|
+
// scope must not depend on the current machine-only sync exclusion policy.
|
|
75
|
+
return ['fraim/ai-employee/', 'fraim/ai-manager/', 'fraim/docs/']
|
|
74
76
|
.filter((entry) => /[\\/]$/.test(entry))
|
|
75
77
|
.map((entry) => entry.replace(/[\\/]+$/, ''))
|
|
76
78
|
.map((entry) => (0, path_1.join)(projectRoot, entry));
|
|
@@ -288,6 +290,28 @@ async function syncJobsToUserDir(files) {
|
|
|
288
290
|
]);
|
|
289
291
|
return { employeeJobsSynced, managerJobsSynced };
|
|
290
292
|
}
|
|
293
|
+
/** Keep skill and rule discovery beside the machine-level job catalogs. */
|
|
294
|
+
async function syncSkillsAndRulesToUserDir(files) {
|
|
295
|
+
const counts = { skillsSynced: 0, rulesSynced: 0 };
|
|
296
|
+
for (const type of ['skill', 'rule']) {
|
|
297
|
+
const directory = `${type}s`;
|
|
298
|
+
const stubs = files.filter(file => file.type === type).map(file => ({
|
|
299
|
+
...file,
|
|
300
|
+
path: file.path.replace(/\\/g, '/').replace(new RegExp(`^${directory}/`), ''),
|
|
301
|
+
content: applySyncedContentBanner(file),
|
|
302
|
+
}));
|
|
303
|
+
const count = await syncTypedFilesToUserDir(stubs, type, `ai-employee/${directory}`, type);
|
|
304
|
+
if (type === 'skill') {
|
|
305
|
+
counts.skillsSynced = count;
|
|
306
|
+
if (count > 0)
|
|
307
|
+
(0, capability_index_1.writeCapabilityIndex)((0, path_1.join)((0, script_sync_utils_1.getUserFraimDir)(), 'ai-employee', directory), 'skill');
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
counts.rulesSynced = count;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return counts;
|
|
314
|
+
}
|
|
291
315
|
/**
|
|
292
316
|
* Sync jobs and scripts from remote FRAIM server
|
|
293
317
|
*/
|
|
@@ -282,7 +282,13 @@ function buildPersonaAvatarUrl(personaKey) {
|
|
|
282
282
|
return `https://api.dicebear.com/9.x/${avatar.style}/svg?${params.toString()}`;
|
|
283
283
|
}
|
|
284
284
|
function isPersonaHireKey(value) {
|
|
285
|
-
|
|
285
|
+
// Own properties only. `in` also answers true for every Object.prototype member,
|
|
286
|
+
// so 'constructor', 'toString', '__proto__' and friends passed this guard and then
|
|
287
|
+
// indexed the catalog to a prototype member rather than a persona. The guard is the
|
|
288
|
+
// input validation in front of the hire-checkout and entitlement reads, where the
|
|
289
|
+
// value is caller-supplied, and (issue #1925) in front of the employee-record reader,
|
|
290
|
+
// where it is file-supplied.
|
|
291
|
+
return Object.prototype.hasOwnProperty.call(exports.PERSONA_HIRE_CATALOG, value);
|
|
286
292
|
}
|
|
287
293
|
function getPersonaHireAmountCents(personaKey, mode) {
|
|
288
294
|
const persona = exports.PERSONA_HIRE_CATALOG[personaKey];
|