fraim 2.0.217 → 2.0.219
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 +0 -7
- package/dist/src/cli/commands/init-project.js +8 -12
- package/dist/src/cli/commands/org.js +2 -2
- package/dist/src/cli/commands/sync.js +15 -10
- package/dist/src/cli/setup/auto-mcp-setup.js +0 -6
- package/dist/src/cli/setup/codex-local-config.js +5 -27
- package/dist/src/cli/setup/ide-invocation-surfaces.js +7 -2
- package/dist/src/cli/utils/manager-pack-sync.js +24 -13
- package/dist/src/cli/utils/manager-publish.js +3 -1
- package/dist/src/cli/utils/org-pack-sync.js +38 -28
- package/dist/src/cli/utils/org-publish.js +21 -4
- package/dist/src/config/persona-capability-bundles.js +45 -27
- package/dist/src/core/capability-pack.js +85 -0
- package/dist/src/core/manager-pack.js +7 -1
- package/dist/src/core/utils/local-registry-resolver.js +90 -2
- package/dist/src/services/persona-entitlement-service.js +4 -1
- package/package.json +1 -1
|
@@ -44,7 +44,6 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
44
44
|
const path_1 = __importDefault(require("path"));
|
|
45
45
|
const ide_detector_1 = require("../setup/ide-detector");
|
|
46
46
|
const mcp_config_generator_1 = require("../setup/mcp-config-generator");
|
|
47
|
-
const codex_local_config_1 = require("../setup/codex-local-config");
|
|
48
47
|
const claude_code_telemetry_1 = require("../setup/claude-code-telemetry");
|
|
49
48
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
50
49
|
const mcp_server_registry_1 = require("../mcp/mcp-server-registry");
|
|
@@ -254,12 +253,6 @@ const configureIDEMCP = async (ide, fraimKey, tokens, providerConfigs) => {
|
|
|
254
253
|
});
|
|
255
254
|
}
|
|
256
255
|
console.log(chalk_1.default.green(`✅ Updated ${configPath}`));
|
|
257
|
-
// Handle IDE-specific local config (e.g., Codex needs project-level config)
|
|
258
|
-
if (ide.configType === 'codex') {
|
|
259
|
-
const localResult = (0, codex_local_config_1.ensureCodexLocalConfig)(process.cwd());
|
|
260
|
-
const status = localResult.created ? 'Created' : localResult.updated ? 'Updated' : 'Verified';
|
|
261
|
-
console.log(chalk_1.default.green(` ✅ ${status} local ${ide.name} config: ${localResult.path}`));
|
|
262
|
-
}
|
|
263
256
|
// Enable token telemetry for Claude Code via project-level settings
|
|
264
257
|
if (ide.configType === 'claude-code') {
|
|
265
258
|
(0, claude_code_telemetry_1.ensureClaudeCodeTelemetryEnv)();
|
|
@@ -12,7 +12,6 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
12
12
|
const sync_1 = require("./sync");
|
|
13
13
|
const platform_detection_1 = require("../utils/platform-detection");
|
|
14
14
|
const ide_detector_1 = require("../setup/ide-detector");
|
|
15
|
-
const codex_local_config_1 = require("../setup/codex-local-config");
|
|
16
15
|
const claude_code_telemetry_1 = require("../setup/claude-code-telemetry");
|
|
17
16
|
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
18
17
|
const agent_adapters_1 = require("../utils/agent-adapters");
|
|
@@ -173,7 +172,7 @@ const runInitProject = async (options = {}) => {
|
|
|
173
172
|
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
174
173
|
}
|
|
175
174
|
if (!process.env.FRAIM_SKIP_SYNC) {
|
|
176
|
-
await (0, sync_1.runSync)({ projectRoot, failHard });
|
|
175
|
+
await (0, sync_1.runSync)({ projectRoot, failHard, projectAdapters: options.projectAdapters });
|
|
177
176
|
result.syncPerformed = true;
|
|
178
177
|
}
|
|
179
178
|
else {
|
|
@@ -187,24 +186,21 @@ const runInitProject = async (options = {}) => {
|
|
|
187
186
|
if (detectedConfigTypes.length > 0) {
|
|
188
187
|
(0, user_config_1.writeUserFraimConfig)({ installedIdes: detectedConfigTypes });
|
|
189
188
|
}
|
|
190
|
-
const codexAvailable = detectedIdes.some((ide) => ide.configType === 'codex');
|
|
191
|
-
if (codexAvailable) {
|
|
192
|
-
const codexLocalResult = (0, codex_local_config_1.ensureCodexLocalConfig)(projectRoot);
|
|
193
|
-
const status = codexLocalResult.created ? 'Created' : codexLocalResult.updated ? 'Updated' : 'Verified';
|
|
194
|
-
console.log(chalk_1.default.green(`${status} project Codex config at ${codexLocalResult.path}`));
|
|
195
|
-
}
|
|
196
189
|
// Enable token telemetry for Claude Code (user-level, applies to all projects)
|
|
197
190
|
const claudeCodeAvailable = detectedIdes.some((ide) => ide.configType === 'claude-code');
|
|
198
191
|
if (claudeCodeAvailable) {
|
|
199
192
|
(0, claude_code_telemetry_1.ensureClaudeCodeTelemetryEnv)();
|
|
200
193
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
if (options.projectAdapters) {
|
|
195
|
+
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, detectedConfigTypes.length > 0 ? detectedConfigTypes : null);
|
|
196
|
+
if (adapterUpdates.length > 0) {
|
|
197
|
+
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
198
|
+
}
|
|
204
199
|
}
|
|
205
200
|
(0, project_bootstrap_1.printInitProjectSummary)(result);
|
|
206
201
|
};
|
|
207
202
|
exports.runInitProject = runInitProject;
|
|
208
203
|
exports.initProjectCommand = new commander_1.Command('init-project')
|
|
209
204
|
.description('Initialize FRAIM in the current project (requires global setup)')
|
|
210
|
-
.
|
|
205
|
+
.option('--project-adapters', 'Write legacy project-local FRAIM agent adapter files')
|
|
206
|
+
.action((options) => (0, exports.runInitProject)({ projectAdapters: Boolean(options.projectAdapters) }));
|
|
@@ -12,8 +12,8 @@ exports.orgCommand = new commander_1.Command('org')
|
|
|
12
12
|
.description('Manage your shared organization context');
|
|
13
13
|
exports.orgCommand
|
|
14
14
|
.command('publish')
|
|
15
|
-
.description('Publish org context/rules/learnings files to the configured org backend')
|
|
16
|
-
.argument('<files...>', 'Local files to publish: org_context.md, org_rules.md, and/or org-*.md learnings')
|
|
15
|
+
.description('Publish org context/rules/learnings/brand files to the configured org backend')
|
|
16
|
+
.argument('<files...>', 'Local files to publish: org_context.md, org_rules.md, org_brand.json, and/or org-*.md learnings')
|
|
17
17
|
.action(async (files) => {
|
|
18
18
|
try {
|
|
19
19
|
const artifacts = files.map((f) => {
|
|
@@ -288,11 +288,13 @@ const runSync = async (options) => {
|
|
|
288
288
|
removeLegacyVersionFromConfig(fraimDir);
|
|
289
289
|
writeSyncMetadata('local', localUrl);
|
|
290
290
|
refreshLocalIgnoreConfig();
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
if (options.projectAdapters) {
|
|
292
|
+
const allowedTypes = resolveAllowedConfigTypes();
|
|
293
|
+
await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
|
|
294
|
+
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedTypes);
|
|
295
|
+
if (adapterUpdates.length > 0) {
|
|
296
|
+
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
297
|
+
}
|
|
296
298
|
}
|
|
297
299
|
await refreshOrgCache(localUrl, 'local-dev');
|
|
298
300
|
await refreshManagerCache(localUrl, 'local-dev');
|
|
@@ -335,11 +337,13 @@ const runSync = async (options) => {
|
|
|
335
337
|
removeLegacyVersionFromConfig(fraimDir);
|
|
336
338
|
writeSyncMetadata('remote', config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
|
|
337
339
|
refreshLocalIgnoreConfig();
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
if (options.projectAdapters) {
|
|
341
|
+
const allowedTypes = resolveAllowedConfigTypes();
|
|
342
|
+
await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
|
|
343
|
+
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedTypes);
|
|
344
|
+
if (adapterUpdates.length > 0) {
|
|
345
|
+
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
346
|
+
}
|
|
343
347
|
}
|
|
344
348
|
await refreshOrgCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
|
|
345
349
|
await refreshManagerCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
|
|
@@ -351,4 +355,5 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
351
355
|
.option('--skip-updates', 'Skip checking for CLI updates (legacy)')
|
|
352
356
|
.option('--local', 'Sync from local development server (port derived from git branch)')
|
|
353
357
|
.option('--global', 'Sync user-level FRAIM content (~/.fraim/) instead of project')
|
|
358
|
+
.option('--project-adapters', 'Write legacy project-local FRAIM agent adapter files')
|
|
354
359
|
.action(exports.runSync);
|
|
@@ -43,7 +43,6 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
43
43
|
const prompts_1 = __importDefault(require("prompts"));
|
|
44
44
|
const ide_detector_1 = require("./ide-detector");
|
|
45
45
|
const mcp_config_generator_1 = require("./mcp-config-generator");
|
|
46
|
-
const codex_local_config_1 = require("./codex-local-config");
|
|
47
46
|
const promptForIDESelection = async (detectedIDEs) => {
|
|
48
47
|
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
49
48
|
console.log(chalk_1.default.yellow(`\nℹ️ Non-interactive mode: configuring all detected IDEs (${detectedIDEs.length})`));
|
|
@@ -223,11 +222,6 @@ const configureIDEMCP = async (ide, fraimKey) => {
|
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
224
|
console.log(chalk_1.default.green(`✅ Updated ${configPath}`));
|
|
226
|
-
if (ide.configType === 'codex') {
|
|
227
|
-
const localResult = (0, codex_local_config_1.ensureCodexLocalConfig)(process.cwd());
|
|
228
|
-
const status = localResult.created ? 'Created' : localResult.updated ? 'Updated' : 'Verified';
|
|
229
|
-
console.log(chalk_1.default.green(` ✅ ${status} local Codex config: ${localResult.path}`));
|
|
230
|
-
}
|
|
231
225
|
};
|
|
232
226
|
const autoConfigureMCP = async (fraimKey, selectedIDEs) => {
|
|
233
227
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
@@ -4,34 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ensureCodexLocalConfig = void 0;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
7
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const mcp_config_generator_1 = require("./mcp-config-generator");
|
|
10
|
-
const escapeTomlString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
11
|
-
const buildFraimCwdBlock = (projectRoot) => `
|
|
12
|
-
[mcp_servers.fraim]
|
|
13
|
-
cwd = "${escapeTomlString(projectRoot)}"
|
|
14
|
-
`;
|
|
15
8
|
const ensureCodexLocalConfig = (projectRoot) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
const hadExistingFile = fs_1.default.existsSync(codexConfigPath);
|
|
22
|
-
const existingContent = hadExistingFile ? fs_1.default.readFileSync(codexConfigPath, 'utf8') : '';
|
|
23
|
-
const generatedContent = buildFraimCwdBlock(projectRoot);
|
|
24
|
-
const mergeResult = (0, mcp_config_generator_1.mergeTomlMCPServers)(existingContent, generatedContent, ['fraim']);
|
|
25
|
-
const normalizedExisting = existingContent.replace(/\r\n/g, '\n');
|
|
26
|
-
const normalizedMerged = mergeResult.content.replace(/\r\n/g, '\n');
|
|
27
|
-
const shouldWrite = !hadExistingFile || normalizedExisting !== normalizedMerged;
|
|
28
|
-
if (shouldWrite) {
|
|
29
|
-
fs_1.default.writeFileSync(codexConfigPath, mergeResult.content);
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
path: codexConfigPath,
|
|
33
|
-
created: !hadExistingFile,
|
|
34
|
-
updated: shouldWrite && hadExistingFile
|
|
35
|
-
};
|
|
9
|
+
const codexConfigPath = path_1.default.join(projectRoot, '.codex', 'config.toml');
|
|
10
|
+
// Codex resolves the active workspace from its process cwd. FRAIM's Codex
|
|
11
|
+
// MCP server belongs in machine-level config; project-local partial TOML can
|
|
12
|
+
// shadow a valid profile and break configured Hub launches.
|
|
13
|
+
return { path: codexConfigPath, created: false, updated: false };
|
|
36
14
|
};
|
|
37
15
|
exports.ensureCodexLocalConfig = ensureCodexLocalConfig;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
3
|
+
exports.CODEX_SKILL_FRONTMATTER = exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
4
4
|
exports.buildFraimInvocationBody = buildFraimInvocationBody;
|
|
5
5
|
exports.buildClaudeSkillContent = buildClaudeSkillContent;
|
|
6
6
|
exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
|
|
@@ -98,8 +98,13 @@ function buildCursorMentionRuleContent() {
|
|
|
98
98
|
${buildFraimInvocationBody('generic-tool-discovery')}
|
|
99
99
|
`;
|
|
100
100
|
}
|
|
101
|
+
exports.CODEX_SKILL_FRONTMATTER = `---
|
|
102
|
+
name: fraim
|
|
103
|
+
description: Discover and execute FRAIM jobs and skills from Codex.
|
|
104
|
+
---`;
|
|
101
105
|
function buildCodexSkillContent() {
|
|
102
|
-
return
|
|
106
|
+
return `${exports.CODEX_SKILL_FRONTMATTER}
|
|
107
|
+
# FRAIM
|
|
103
108
|
|
|
104
109
|
${buildFraimInvocationBody('codex-tool-search')}`;
|
|
105
110
|
}
|
|
@@ -24,13 +24,14 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
24
24
|
const path_1 = __importDefault(require("path"));
|
|
25
25
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
26
26
|
const manager_pack_1 = require("../../core/manager-pack");
|
|
27
|
+
const capability_pack_1 = require("../../core/capability-pack");
|
|
27
28
|
const git_org_sync_1 = require("./git-org-sync");
|
|
28
29
|
const local_folder_sync_1 = require("./local-folder-sync");
|
|
29
30
|
const user_config_1 = require("./user-config");
|
|
30
31
|
exports.MANAGER_CACHE_DIRNAME = 'manager';
|
|
31
32
|
exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
|
|
32
33
|
exports.MANAGER_CACHE_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
|
|
33
|
-
const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings'];
|
|
34
|
+
const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts'];
|
|
34
35
|
function getManagerCacheDir() {
|
|
35
36
|
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.MANAGER_CACHE_DIRNAME);
|
|
36
37
|
}
|
|
@@ -72,20 +73,23 @@ function decorateManagedManagerFile(content, backend) {
|
|
|
72
73
|
].join('\n');
|
|
73
74
|
return `${marker}\n${normalized}`;
|
|
74
75
|
}
|
|
76
|
+
function collectGitPackFilesRecursive(dir, relBase, files) {
|
|
77
|
+
if (!fs_1.default.existsSync(dir))
|
|
78
|
+
return;
|
|
79
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
80
|
+
const rel = `${relBase}/${entry.name}`;
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
collectGitPackFilesRecursive(path_1.default.join(dir, entry.name), rel, files);
|
|
83
|
+
}
|
|
84
|
+
else if (entry.isFile() && (0, manager_pack_1.isManagerPackRelativePath)(rel)) {
|
|
85
|
+
files.push({ relativePath: rel, content: fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8') });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
75
89
|
function collectGitPackFiles(snapshotDir) {
|
|
76
90
|
const files = [];
|
|
77
91
|
for (const dirName of MANAGER_PACK_DIRS) {
|
|
78
|
-
|
|
79
|
-
if (!fs_1.default.existsSync(dirPath))
|
|
80
|
-
continue;
|
|
81
|
-
for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
|
|
82
|
-
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
83
|
-
continue;
|
|
84
|
-
files.push({
|
|
85
|
-
relativePath: `${dirName}/${entry.name}`,
|
|
86
|
-
content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
|
|
87
|
-
});
|
|
88
|
-
}
|
|
92
|
+
collectGitPackFilesRecursive(path_1.default.join(snapshotDir, dirName), dirName, files);
|
|
89
93
|
}
|
|
90
94
|
return files;
|
|
91
95
|
}
|
|
@@ -110,9 +114,16 @@ function materializeCache(files, metadata) {
|
|
|
110
114
|
for (const file of files) {
|
|
111
115
|
if (!(0, manager_pack_1.isManagerPackRelativePath)(file.relativePath))
|
|
112
116
|
continue;
|
|
117
|
+
// Scripts require a security-review gate (Phase 3). Skip in Phase 1.
|
|
118
|
+
if ((0, capability_pack_1.isCapabilityPackPath)(file.relativePath) && file.relativePath.startsWith('scripts/'))
|
|
119
|
+
continue;
|
|
113
120
|
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
114
121
|
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
115
|
-
|
|
122
|
+
// Only markdown gets the managed-content header; scripts/JSON ride verbatim.
|
|
123
|
+
const content = (0, capability_pack_1.shouldDecorateAsMarkdown)(file.relativePath)
|
|
124
|
+
? decorateManagedManagerFile(file.content, metadata.backend)
|
|
125
|
+
: file.content;
|
|
126
|
+
fs_1.default.writeFileSync(destination, content);
|
|
116
127
|
}
|
|
117
128
|
fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.MANAGER_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
|
|
118
129
|
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
@@ -22,8 +22,10 @@ function managerPackRelativePathFor(filePath) {
|
|
|
22
22
|
const relativePath = (0, manager_pack_1.managerPackRelativePathForFileName)(filePath);
|
|
23
23
|
if (relativePath)
|
|
24
24
|
return relativePath;
|
|
25
|
+
// Capability files (jobs/skills/rules/templates/scripts) carry their pack-relative
|
|
26
|
+
// path verbatim — the caller supplies the full relative path, not just a basename.
|
|
25
27
|
const base = path_1.default.basename(filePath);
|
|
26
|
-
throw new Error(`Cannot infer manager-pack location for '${base}' (expected manager_context.md, manager_rules.md,
|
|
28
|
+
throw new Error(`Cannot infer manager-pack location for '${base}' (expected manager_context.md, manager_rules.md, a personal learning file, or a full capability path like jobs/<cat>/<name>.md)`);
|
|
27
29
|
}
|
|
28
30
|
function gitCompareUrl(gitUrl, branch) {
|
|
29
31
|
const httpUrl = gitUrl.replace(/\.git$/, '').replace(/^git@([^:]+):/, 'https://$1/');
|
|
@@ -3,9 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = void 0;
|
|
6
|
+
exports.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = exports.shouldDecorateAsMarkdown = void 0;
|
|
7
7
|
exports.isSafePackPath = isSafePackPath;
|
|
8
|
-
exports.
|
|
8
|
+
exports.isSafeOrgCapabilityPackPath = isSafeOrgCapabilityPackPath;
|
|
9
9
|
exports.getOrgCacheDir = getOrgCacheDir;
|
|
10
10
|
exports.readOrgCacheMetadata = readOrgCacheMetadata;
|
|
11
11
|
exports.getOrgCacheAgeHours = getOrgCacheAgeHours;
|
|
@@ -29,11 +29,14 @@ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
|
29
29
|
const user_config_1 = require("./user-config");
|
|
30
30
|
const git_org_sync_1 = require("./git-org-sync");
|
|
31
31
|
const local_folder_sync_1 = require("./local-folder-sync");
|
|
32
|
+
const capability_pack_1 = require("../../core/capability-pack");
|
|
33
|
+
var capability_pack_2 = require("../../core/capability-pack");
|
|
34
|
+
Object.defineProperty(exports, "shouldDecorateAsMarkdown", { enumerable: true, get: function () { return capability_pack_2.shouldDecorateAsMarkdown; } });
|
|
32
35
|
exports.ORG_CACHE_DIRNAME = 'org';
|
|
33
36
|
exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
|
|
34
37
|
exports.ORG_CACHE_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
|
|
35
38
|
/** Subdirectories of the org pack that sync into the cache (spec R7.1). */
|
|
36
|
-
const ORG_PACK_DIRS = ['context', 'rules', 'learnings'];
|
|
39
|
+
const ORG_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates'];
|
|
37
40
|
/**
|
|
38
41
|
* Pack files must stay inside the three org pack directories. Applied to
|
|
39
42
|
* every relativePath before it touches the filesystem, so a compromised
|
|
@@ -48,19 +51,24 @@ const SAFE_PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
|
|
|
48
51
|
const ORG_BRAND_PACK_PATH = 'context/org_brand.json';
|
|
49
52
|
/**
|
|
50
53
|
* True when a pack-relative path is safe to materialize: a markdown file inside
|
|
51
|
-
* one of the three org pack dirs,
|
|
52
|
-
*
|
|
54
|
+
* one of the three org pack dirs, the org brand descriptor, or a capability
|
|
55
|
+
* pack path (issue #869 Phase 2). Scripts require a Phase-3 security gate and
|
|
56
|
+
* are blocked here.
|
|
53
57
|
*/
|
|
54
58
|
function isSafePackPath(relativePath) {
|
|
55
59
|
return SAFE_PACK_RELATIVE_PATH.test(relativePath) || relativePath === ORG_BRAND_PACK_PATH;
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* the JSON (#744).
|
|
62
|
+
* Issue #869 Phase 2: true when a path is safe to materialize as an org
|
|
63
|
+
* capability overlay (jobs/skills/rules-nested/templates — no scripts until Phase 3).
|
|
61
64
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
65
|
+
function isSafeOrgCapabilityPackPath(relativePath) {
|
|
66
|
+
if (!(0, capability_pack_1.isCapabilityPackPath)(relativePath))
|
|
67
|
+
return false;
|
|
68
|
+
// Scripts require the Phase-3 triple security gate; block at materialization.
|
|
69
|
+
if ((0, capability_pack_1.isScriptPath)(relativePath))
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
64
72
|
}
|
|
65
73
|
function getOrgCacheDir() {
|
|
66
74
|
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_CACHE_DIRNAME);
|
|
@@ -103,22 +111,23 @@ function decorateManagedOrgFile(content, backend) {
|
|
|
103
111
|
].join('\n');
|
|
104
112
|
return `${marker}\n${normalized}`;
|
|
105
113
|
}
|
|
114
|
+
function collectGitPackFilesRecursive(dir, relBase, files) {
|
|
115
|
+
if (!fs_1.default.existsSync(dir))
|
|
116
|
+
return;
|
|
117
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
+
const rel = `${relBase}/${entry.name}`;
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
collectGitPackFilesRecursive(path_1.default.join(dir, entry.name), rel, files);
|
|
121
|
+
}
|
|
122
|
+
else if (entry.isFile() && (isSafePackPath(rel) || isSafeOrgCapabilityPackPath(rel))) {
|
|
123
|
+
files.push({ relativePath: rel, content: fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8') });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
106
127
|
function collectGitPackFiles(snapshotDir) {
|
|
107
128
|
const files = [];
|
|
108
129
|
for (const dirName of ORG_PACK_DIRS) {
|
|
109
|
-
|
|
110
|
-
if (!fs_1.default.existsSync(dirPath))
|
|
111
|
-
continue;
|
|
112
|
-
for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
|
|
113
|
-
const relativePath = `${dirName}/${entry.name}`;
|
|
114
|
-
// Markdown in any pack dir, plus the org brand descriptor (#744).
|
|
115
|
-
if (!entry.isFile() || !isSafePackPath(relativePath))
|
|
116
|
-
continue;
|
|
117
|
-
files.push({
|
|
118
|
-
relativePath,
|
|
119
|
-
content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
|
|
120
|
-
});
|
|
121
|
-
}
|
|
130
|
+
collectGitPackFilesRecursive(path_1.default.join(snapshotDir, dirName), dirName, files);
|
|
122
131
|
}
|
|
123
132
|
return files;
|
|
124
133
|
}
|
|
@@ -130,7 +139,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
|
|
|
130
139
|
const files = Array.isArray(response.data?.files) ? response.data.files : [];
|
|
131
140
|
return {
|
|
132
141
|
files: files.filter((f) => typeof f?.relativePath === 'string' &&
|
|
133
|
-
isSafePackPath(f.relativePath) &&
|
|
142
|
+
(isSafePackPath(f.relativePath) || isSafeOrgCapabilityPackPath(f.relativePath)) &&
|
|
134
143
|
typeof f?.content === 'string'),
|
|
135
144
|
version: String(response.data?.version ?? '0')
|
|
136
145
|
};
|
|
@@ -142,12 +151,13 @@ function materializeCache(files, metadata) {
|
|
|
142
151
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
143
152
|
fs_1.default.mkdirSync(stagingDir, { recursive: true });
|
|
144
153
|
for (const file of files) {
|
|
145
|
-
|
|
154
|
+
const isSafe = isSafePackPath(file.relativePath) || isSafeOrgCapabilityPackPath(file.relativePath);
|
|
155
|
+
if (!isSafe)
|
|
146
156
|
continue;
|
|
147
157
|
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
148
158
|
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
149
|
-
//
|
|
150
|
-
const content = shouldDecorateAsMarkdown(file.relativePath)
|
|
159
|
+
// Only markdown gets the managed-content header; JSON and future scripts ride verbatim.
|
|
160
|
+
const content = (0, capability_pack_1.shouldDecorateAsMarkdown)(file.relativePath)
|
|
151
161
|
? decorateManagedOrgFile(file.content, metadata.backend)
|
|
152
162
|
: file.content;
|
|
153
163
|
fs_1.default.writeFileSync(destination, content);
|
|
@@ -212,7 +222,7 @@ async function syncOrgCache(options) {
|
|
|
212
222
|
try {
|
|
213
223
|
if (organization.backend === 'local-folder') {
|
|
214
224
|
const localPath = organization.localPath;
|
|
215
|
-
const files = (0, local_folder_sync_1.collectLocalFolderFiles)(localPath, isSafePackPath);
|
|
225
|
+
const files = (0, local_folder_sync_1.collectLocalFolderFiles)(localPath, (rel) => isSafePackPath(rel) || isSafeOrgCapabilityPackPath(rel));
|
|
216
226
|
const metadata = {
|
|
217
227
|
version: (0, local_folder_sync_1.localFolderVersion)(localPath),
|
|
218
228
|
backend: 'local-folder',
|
|
@@ -23,23 +23,38 @@ const os_1 = __importDefault(require("os"));
|
|
|
23
23
|
const path_1 = __importDefault(require("path"));
|
|
24
24
|
const user_config_1 = require("./user-config");
|
|
25
25
|
const local_folder_sync_1 = require("./local-folder-sync");
|
|
26
|
+
const capability_pack_1 = require("../../core/capability-pack");
|
|
26
27
|
const PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
|
|
28
|
+
// #868: the company brand descriptor is JSON stored beside org_context.md. It
|
|
29
|
+
// publishes through the same backends as org markdown but is delivered verbatim
|
|
30
|
+
// (no managed-content header — that decoration is markdown-only, added on sync).
|
|
31
|
+
// Mirrors org-pack-sync.isSafePackPath and fraim-mcp-server.isOrgPackPublishRelativePath.
|
|
32
|
+
const ORG_BRAND_PACK_PATH = 'context/org_brand.json';
|
|
27
33
|
const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
|
|
28
34
|
/**
|
|
29
|
-
* Infer the org-pack location for a local file by its name:
|
|
35
|
+
* Infer the org-pack location for a local file by its name or path:
|
|
30
36
|
* org_context.md -> context/org_context.md
|
|
31
37
|
* org_rules.md -> rules/org_rules.md
|
|
38
|
+
* org_brand.json -> context/org_brand.json
|
|
32
39
|
* org-*.md -> learnings/org-*.md
|
|
40
|
+
* jobs/<cat>/<name>.md | skills/<cat>/<name>.md | rules/<cat>/<name>.md |
|
|
41
|
+
* templates/<cat>/<name>.md -> capability path returned as-is (issue #869 Phase 2)
|
|
33
42
|
*/
|
|
34
43
|
function packRelativePathFor(filePath) {
|
|
44
|
+
const normalized = filePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
45
|
+
// Issue #869: if the caller passes a full capability-pack relative path, return it as-is.
|
|
46
|
+
if ((0, capability_pack_1.isCapabilityPackPath)(normalized))
|
|
47
|
+
return normalized;
|
|
35
48
|
const base = path_1.default.basename(filePath);
|
|
36
49
|
if (base === 'org_context.md')
|
|
37
50
|
return 'context/org_context.md';
|
|
38
51
|
if (base === 'org_rules.md')
|
|
39
52
|
return 'rules/org_rules.md';
|
|
53
|
+
if (base === 'org_brand.json')
|
|
54
|
+
return ORG_BRAND_PACK_PATH;
|
|
40
55
|
if (/^org-[\w.-]*\.md$/.test(base))
|
|
41
56
|
return `learnings/${base}`;
|
|
42
|
-
throw new Error(`Cannot infer org-pack location for '${base}' (expected org_context.md, org_rules.md,
|
|
57
|
+
throw new Error(`Cannot infer org-pack location for '${base}' (expected org_context.md, org_rules.md, org_brand.json, org-*.md, or a capability path like jobs/<cat>/<name>.md)`);
|
|
43
58
|
}
|
|
44
59
|
function gitCompareUrl(gitUrl, branch) {
|
|
45
60
|
const httpUrl = gitUrl.replace(/\.git$/, '').replace(/^git@([^:]+):/, 'https://$1/');
|
|
@@ -60,8 +75,10 @@ async function publishOrgArtifacts(artifacts, opts) {
|
|
|
60
75
|
if (artifacts.length === 0)
|
|
61
76
|
throw new Error('No artifacts to publish.');
|
|
62
77
|
for (const a of artifacts) {
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
const isLegacyPath = PACK_RELATIVE_PATH.test(a.relativePath) || a.relativePath === ORG_BRAND_PACK_PATH;
|
|
79
|
+
const isCapabilityPath = (0, capability_pack_1.isCapabilityPackPath)(a.relativePath);
|
|
80
|
+
if (!isLegacyPath && !isCapabilityPath) {
|
|
81
|
+
throw new Error(`Invalid org-pack path '${a.relativePath}' (must be context|rules|learnings/<name>.md, context/org_brand.json, or a capability path like jobs/<cat>/<name>.md).`);
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
if (organization.backend === 'local-folder') {
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PERSONA_CAPABILITY_BUNDLES = exports.FREE_JOBS = void 0;
|
|
3
|
+
exports.PERSONA_CAPABILITY_BUNDLES = exports.FREE_JOBS = exports.GENERIC_WORKER_PERSONA_KEY = void 0;
|
|
4
4
|
exports.isFreeJob = isFreeJob;
|
|
5
5
|
exports.getPersonaCapabilityBundle = getPersonaCapabilityBundle;
|
|
6
6
|
exports.getProtectedPersonaForJob = getProtectedPersonaForJob;
|
|
7
7
|
exports.listPersonaCapabilityBundles = listPersonaCapabilityBundles;
|
|
8
8
|
const persona_hiring_1 = require("./persona-hiring");
|
|
9
9
|
const persona_catalog_routes_1 = require("../routes/persona-catalog-routes");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
// The Hub-only generic worker identity. It is NOT a hireable persona in
|
|
11
|
+
// PERSONA_HIRE_CATALOG and NOT a PERSONA_CAPABILITY_BUNDLES entry, so it never
|
|
12
|
+
// appears in the persona catalog, pricing, or hire flows. FRAIM-internal jobs
|
|
13
|
+
// that no named specialist owns resolve to this key so the Hub attributes them
|
|
14
|
+
// to FRAIMworker; consumers that gate on ownership must treat it as ungated.
|
|
15
|
+
exports.GENERIC_WORKER_PERSONA_KEY = 'fraimworker';
|
|
16
|
+
// Issue #902: the FREE_JOBS bypass is retired. Every catalog job now resolves
|
|
17
|
+
// through explicit named ownership (see getProtectedPersonaForJob). This set is
|
|
18
|
+
// intentionally empty and isFreeJob() always returns false; both are retained as
|
|
19
|
+
// a stable compatibility surface for existing callers and tests.
|
|
20
|
+
exports.FREE_JOBS = new Set();
|
|
16
21
|
function isFreeJob(jobName) {
|
|
17
22
|
return exports.FREE_JOBS.has(jobName);
|
|
18
23
|
}
|
|
@@ -39,8 +44,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
39
44
|
beza: {
|
|
40
45
|
personaKey: 'beza',
|
|
41
46
|
bundleId: 'persona-beza-core',
|
|
42
|
-
catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', '
|
|
43
|
-
protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', '
|
|
47
|
+
catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', 'pricing-strategy-definition']),
|
|
48
|
+
protectedJobs: ['competitive-analysis', 'review-business-strategy', 'business-plan-creation', 'problem-statement-crystallization', 'business-idea-validation-and-scoping', 'founder-market-fit-analysis', 'advisory-board-development', 'advisor-interview', 'advisory-board-selection', 'pricing-strategy-definition', 'blue-sky-brainstorming'],
|
|
44
49
|
protectedAliases: ['business-strategy', 'company-strategy'],
|
|
45
50
|
defaultHireMode: 'job',
|
|
46
51
|
lockCopy: 'Hire BeZa to unlock business strategy work for this request.'
|
|
@@ -48,8 +53,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
48
53
|
pam: {
|
|
49
54
|
personaKey: 'pam',
|
|
50
55
|
bundleId: 'persona-pam-core',
|
|
51
|
-
catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', '
|
|
52
|
-
protectedJobs: ['feature-specification', 'technical-design', '
|
|
56
|
+
catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', 'project-plan-creation']),
|
|
57
|
+
protectedJobs: ['feature-specification', 'technical-design', 'experiment-tracking', 'project-plan-creation', 'implementation-feature-review', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
|
|
53
58
|
protectedAliases: ['product-management', 'product-spec'],
|
|
54
59
|
defaultHireMode: 'job',
|
|
55
60
|
lockCopy: 'Hire PaM to unlock product-management work for this request.'
|
|
@@ -57,8 +62,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
57
62
|
swen: {
|
|
58
63
|
personaKey: 'swen',
|
|
59
64
|
bundleId: 'persona-swen-core',
|
|
60
|
-
catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', '
|
|
61
|
-
protectedJobs: ['feature-implementation', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
|
|
65
|
+
catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'technical-design', 'code-refactoring']),
|
|
66
|
+
protectedJobs: ['feature-implementation', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
|
|
62
67
|
protectedAliases: ['software-engineering', 'implementation'],
|
|
63
68
|
defaultHireMode: 'job',
|
|
64
69
|
lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
|
|
@@ -67,7 +72,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
67
72
|
personaKey: 'qasm',
|
|
68
73
|
bundleId: 'persona-qasm-core',
|
|
69
74
|
catalogMetadata: buildCatalogMetadata('qasm', ['test-execution', 'browser-application-validation', 'ui-polish-validation']),
|
|
70
|
-
protectedJobs: ['test-execution', 'browser-application-validation', 'ui-polish-validation', 'code-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'broken-windows-detection-and-remediation', 'iterative-quality-improvement', '
|
|
75
|
+
protectedJobs: ['test-execution', 'browser-application-validation', 'ui-polish-validation', 'code-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'broken-windows-detection-and-remediation', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'performance-benchmarking'],
|
|
71
76
|
protectedAliases: ['qa', 'quality-assurance'],
|
|
72
77
|
defaultHireMode: 'job',
|
|
73
78
|
lockCopy: 'Hire QAsm to unlock QA validation for this request.'
|
|
@@ -84,8 +89,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
84
89
|
gautam: {
|
|
85
90
|
personaKey: 'gautam',
|
|
86
91
|
bundleId: 'persona-gautam-core',
|
|
87
|
-
catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', '
|
|
88
|
-
protectedJobs: ['
|
|
92
|
+
catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'domain-registration-research']),
|
|
93
|
+
protectedJobs: ['marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'domain-registration-research', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck', 'notebooklm-content-generation'],
|
|
89
94
|
protectedAliases: ['gtm', 'marketing'],
|
|
90
95
|
defaultHireMode: 'job',
|
|
91
96
|
lockCopy: 'Hire GauTaM to unlock go-to-market and paid media work for this request.'
|
|
@@ -111,8 +116,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
111
116
|
ashley: {
|
|
112
117
|
personaKey: 'ashley',
|
|
113
118
|
bundleId: 'persona-ashley-core',
|
|
114
|
-
catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', '
|
|
115
|
-
protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', '
|
|
119
|
+
catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
|
|
120
|
+
protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'send-newsletter', 'send-thank-you-notes', 'analyze-transcript'],
|
|
116
121
|
protectedAliases: ['executive-assistant', 'operations-assistant'],
|
|
117
122
|
defaultHireMode: 'job',
|
|
118
123
|
lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
|
|
@@ -120,8 +125,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
120
125
|
mandy: {
|
|
121
126
|
personaKey: 'mandy',
|
|
122
127
|
bundleId: 'persona-mandy-core',
|
|
123
|
-
catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', '
|
|
124
|
-
protectedJobs: ['fully-delegate', 'delivery-governance-review', '
|
|
128
|
+
catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', 'issue-preparation', 'operational-reporting']),
|
|
129
|
+
protectedJobs: ['fully-delegate', 'delivery-governance-review', 'issue-preparation', 'issue-retrospective', 'work-completion', 'operational-reporting', 'send-stakeholder-update', 'cross-functional-dependency-management'],
|
|
125
130
|
protectedAliases: ['manager', 'team-lead', 'orchestrator'],
|
|
126
131
|
defaultHireMode: 'job',
|
|
127
132
|
lockCopy: 'Hire MANdy to unlock autonomous multi-role orchestration — MANdy plans the job sequence, runs sub-agents in parallel, coaches them through verification loops, and hands back a synthesized DRAFT for your approval.'
|
|
@@ -174,8 +179,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
174
179
|
casey: {
|
|
175
180
|
personaKey: 'casey',
|
|
176
181
|
bundleId: 'persona-casey-core',
|
|
177
|
-
catalogMetadata: buildCatalogMetadata('casey', ['
|
|
178
|
-
protectedJobs: ['crm-
|
|
182
|
+
catalogMetadata: buildCatalogMetadata('casey', ['update-crm', 'support-case-resolution', 'support-system-operationalization']),
|
|
183
|
+
protectedJobs: ['crm-case-resolution', 'customer-health-review', 'loyalty-program-management', 'survey-campaign-management', 'update-crm', 'support-queue-management', 'support-case-resolution', 'support-sop-operationalization', 'support-system-operationalization', 'support-playbook-evaluation', 'user-survey-management'],
|
|
179
184
|
protectedAliases: ['customer-success', 'customer-support', 'csm', 'support'],
|
|
180
185
|
defaultHireMode: 'job',
|
|
181
186
|
lockCopy: 'Hire CaSey to unlock customer success and support work for this request.'
|
|
@@ -204,8 +209,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
204
209
|
mona: {
|
|
205
210
|
personaKey: 'mona',
|
|
206
211
|
bundleId: 'persona-mona-core',
|
|
207
|
-
catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', '
|
|
208
|
-
protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', '
|
|
212
|
+
catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', 'fundraising-prospect-discovery', 'investor-pitch-preparation']),
|
|
213
|
+
protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', 'cloud-credits-application', 'community-funding-preparation', 'fundraising-prospect-discovery', 'investor-pitch-preparation', 'review-funding-preparation', 'invoice-generation', 'business-banking-setup'],
|
|
209
214
|
protectedAliases: ['finance', 'financial-modeling', 'fpa', 'bookkeeping'],
|
|
210
215
|
defaultHireMode: 'job',
|
|
211
216
|
lockCopy: 'Hire MONa to unlock financial modeling, FP&A, and tax strategy work for this request.'
|
|
@@ -263,14 +268,27 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
|
|
|
263
268
|
PROTECTED_JOB_TO_PERSONA.set(jobName, bundle.personaKey);
|
|
264
269
|
}
|
|
265
270
|
}
|
|
271
|
+
// FRAIM-internal jobs owned by the generic FRAIMworker identity rather than a
|
|
272
|
+
// named specialist persona. They resolve through ownership (never short-circuited
|
|
273
|
+
// as "free") so the Hub attributes them to FRAIMworker, but they are never
|
|
274
|
+
// hire-gated because FRAIMworker is not a purchasable persona.
|
|
275
|
+
const GENERIC_WORKER_OWNED_JOBS = new Set([
|
|
276
|
+
'contribute-to-fraim',
|
|
277
|
+
'file-fraim-issue',
|
|
278
|
+
'praise-fraim',
|
|
279
|
+
]);
|
|
266
280
|
function getPersonaCapabilityBundle(personaKey) {
|
|
267
281
|
return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
|
|
268
282
|
}
|
|
269
283
|
function getProtectedPersonaForJob(jobName) {
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
const protectedPersona = PROTECTED_JOB_TO_PERSONA.get(jobName);
|
|
285
|
+
if (protectedPersona) {
|
|
286
|
+
return protectedPersona;
|
|
287
|
+
}
|
|
288
|
+
if (GENERIC_WORKER_OWNED_JOBS.has(jobName)) {
|
|
289
|
+
return exports.GENERIC_WORKER_PERSONA_KEY;
|
|
272
290
|
}
|
|
273
|
-
return
|
|
291
|
+
return null;
|
|
274
292
|
}
|
|
275
293
|
function listPersonaCapabilityBundles() {
|
|
276
294
|
return Object.values(exports.PERSONA_CAPABILITY_BUNDLES);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared allowlist contract for capability-pack paths (issue #869).
|
|
4
|
+
*
|
|
5
|
+
* A single source of truth for which relative paths are valid capability
|
|
6
|
+
* members in a manager or org pack. All publish validators, sync guards, and
|
|
7
|
+
* resolver overlay checks import from here so the allowlist never drifts.
|
|
8
|
+
*
|
|
9
|
+
* Phase-1 scope: jobs, skills, rules/<category>/…, templates, scripts.
|
|
10
|
+
* (Scripts are accepted by isCapabilityPackPath so the guard is correct from
|
|
11
|
+
* Phase 1; the server-side security gate for scripts is Phase 3 work.)
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.CAPABILITY_DIRS = void 0;
|
|
15
|
+
exports.isCapabilityPackPath = isCapabilityPackPath;
|
|
16
|
+
exports.isScriptPath = isScriptPath;
|
|
17
|
+
exports.shouldDecorateAsMarkdown = shouldDecorateAsMarkdown;
|
|
18
|
+
exports.CAPABILITY_DIRS = ['jobs', 'skills', 'rules', 'templates', 'scripts'];
|
|
19
|
+
/**
|
|
20
|
+
* Returns true when `rel` is a valid capability-pack member path:
|
|
21
|
+
*
|
|
22
|
+
* jobs/<category>/<name>.md
|
|
23
|
+
* skills/<category>/<name>.md
|
|
24
|
+
* templates/<category>/<name>.md
|
|
25
|
+
* rules/<category>/<name>.md (depth ≥ 2 — flat rules/<name>.md is reserved)
|
|
26
|
+
* scripts/<name>.<ext>
|
|
27
|
+
*
|
|
28
|
+
* Rejects: path traversal (../ or \), absolute paths (/), control characters,
|
|
29
|
+
* flat rules/<name>.md (reserved for org_rules.md / manager_rules.md).
|
|
30
|
+
*/
|
|
31
|
+
function isCapabilityPackPath(rel) {
|
|
32
|
+
if (!rel)
|
|
33
|
+
return false;
|
|
34
|
+
// Reject control characters (includes null bytes, newlines, etc.)
|
|
35
|
+
// eslint-disable-next-line no-control-regex
|
|
36
|
+
if (/[\x00-\x1F\x7F]/.test(rel))
|
|
37
|
+
return false;
|
|
38
|
+
// Reject backslashes (Windows-style traversal)
|
|
39
|
+
if (rel.includes('\\'))
|
|
40
|
+
return false;
|
|
41
|
+
// Reject absolute paths
|
|
42
|
+
if (rel.startsWith('/'))
|
|
43
|
+
return false;
|
|
44
|
+
// Reject any path traversal sequence
|
|
45
|
+
const parts = rel.split('/');
|
|
46
|
+
if (parts.some(p => p === '..' || p === '.'))
|
|
47
|
+
return false;
|
|
48
|
+
const [dir, ...rest] = parts;
|
|
49
|
+
// Must be one of the five capability directories
|
|
50
|
+
if (!exports.CAPABILITY_DIRS.includes(dir))
|
|
51
|
+
return false;
|
|
52
|
+
if (dir === 'scripts') {
|
|
53
|
+
// scripts/<name>.<ext> — exactly one path segment with an extension
|
|
54
|
+
if (rest.length !== 1)
|
|
55
|
+
return false;
|
|
56
|
+
const name = rest[0];
|
|
57
|
+
return name.length > 0 && /\.[a-zA-Z]+$/.test(name);
|
|
58
|
+
}
|
|
59
|
+
if (dir === 'rules') {
|
|
60
|
+
// rules/<category>/<name>.md — depth exactly 2 (no flat rules/)
|
|
61
|
+
if (rest.length !== 2)
|
|
62
|
+
return false;
|
|
63
|
+
const [, name] = rest;
|
|
64
|
+
return name.endsWith('.md') && name.length > 3;
|
|
65
|
+
}
|
|
66
|
+
// jobs, skills, templates: <dir>/<category>/<name>.md — depth exactly 2
|
|
67
|
+
if (rest.length !== 2)
|
|
68
|
+
return false;
|
|
69
|
+
const [, name] = rest;
|
|
70
|
+
return name.endsWith('.md') && name.length > 3;
|
|
71
|
+
}
|
|
72
|
+
/** True when the path is a script (executable) capability member. */
|
|
73
|
+
function isScriptPath(rel) {
|
|
74
|
+
if (!isCapabilityPackPath(rel))
|
|
75
|
+
return false;
|
|
76
|
+
return rel.startsWith('scripts/');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* True when a sync pack file should receive the managed-content header on
|
|
80
|
+
* materialization. Only markdown files get the header; scripts and JSON ride
|
|
81
|
+
* verbatim. Shared between manager-pack-sync and org-pack-sync.
|
|
82
|
+
*/
|
|
83
|
+
function shouldDecorateAsMarkdown(relativePath) {
|
|
84
|
+
return relativePath.toLowerCase().endsWith('.md');
|
|
85
|
+
}
|
|
@@ -7,12 +7,13 @@ exports.MANAGER_PACK_RELATIVE_PATH_RE = exports.MANAGER_LEARNING_FILE_RE = expor
|
|
|
7
7
|
exports.isManagerPackRelativePath = isManagerPackRelativePath;
|
|
8
8
|
exports.managerPackRelativePathForFileName = managerPackRelativePathForFileName;
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const capability_pack_1 = require("./capability-pack");
|
|
10
11
|
exports.MANAGER_CONTEXT_RELATIVE_PATH = 'context/manager_context.md';
|
|
11
12
|
exports.MANAGER_RULES_RELATIVE_PATH = 'rules/manager_rules.md';
|
|
12
13
|
exports.MANAGER_LEARNING_FILE_RE = /^(?!org-)[A-Za-z0-9._%+@-]+-(mistake-patterns|preferences|manager-coaching|validated-patterns)\.md$/;
|
|
13
14
|
exports.MANAGER_PACK_RELATIVE_PATH_RE = /^(context\/manager_context\.md|rules\/manager_rules\.md|learnings\/(?!org-)[A-Za-z0-9._%+@-]+-(mistake-patterns|preferences|manager-coaching|validated-patterns)\.md)$/;
|
|
14
15
|
function isManagerPackRelativePath(value) {
|
|
15
|
-
return exports.MANAGER_PACK_RELATIVE_PATH_RE.test(value);
|
|
16
|
+
return exports.MANAGER_PACK_RELATIVE_PATH_RE.test(value) || (0, capability_pack_1.isCapabilityPackPath)(value);
|
|
16
17
|
}
|
|
17
18
|
function managerPackRelativePathForFileName(fileName) {
|
|
18
19
|
const base = path_1.default.basename(fileName);
|
|
@@ -22,5 +23,10 @@ function managerPackRelativePathForFileName(fileName) {
|
|
|
22
23
|
return exports.MANAGER_RULES_RELATIVE_PATH;
|
|
23
24
|
if (exports.MANAGER_LEARNING_FILE_RE.test(base))
|
|
24
25
|
return `learnings/${base}`;
|
|
26
|
+
// Capability files carry their full relative path as-is (e.g. jobs/cat/name.md).
|
|
27
|
+
// callers that have the full relative path should pass it directly to
|
|
28
|
+
// isManagerPackRelativePath; this filename-only lookup cannot reconstruct the
|
|
29
|
+
// category from a bare basename, so return null and let the caller supply the
|
|
30
|
+
// full path.
|
|
25
31
|
return null;
|
|
26
32
|
}
|
|
@@ -53,6 +53,10 @@ class LocalRegistryResolver {
|
|
|
53
53
|
this.remoteContentResolver = options.remoteContentResolver;
|
|
54
54
|
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
55
55
|
this.shouldFilter = options.shouldFilter;
|
|
56
|
+
// Issue #869: default manager cache root to ~/.fraim/manager/
|
|
57
|
+
this.managerCacheRoot = options.managerCacheRoot ?? (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager');
|
|
58
|
+
// Issue #869 Phase 2: default org cache root to ~/.fraim/org/
|
|
59
|
+
this.orgCacheRoot = options.orgCacheRoot ?? (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org');
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
62
|
* Get a path within the effective FRAIM directory.
|
|
@@ -225,9 +229,70 @@ class LocalRegistryResolver {
|
|
|
225
229
|
return null;
|
|
226
230
|
}
|
|
227
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Issue #869: read a capability file from the manager overlay cache
|
|
234
|
+
* (~/.fraim/manager/<type>/…). Returns null if not present or filtered.
|
|
235
|
+
*
|
|
236
|
+
* Security: the cache is written by the sync layer which already validated
|
|
237
|
+
* paths; we defensively resolve and check the canonical path stays inside
|
|
238
|
+
* the cache root to guard against future path-traversal bugs in sync.
|
|
239
|
+
*/
|
|
240
|
+
readManagerOverlayFile(path) {
|
|
241
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
242
|
+
const destination = (0, path_1.join)(this.managerCacheRoot, ...normalized.split('/'));
|
|
243
|
+
// Guard: resolved path must stay within the manager cache root.
|
|
244
|
+
const resolved = (0, path_1.resolve)(destination);
|
|
245
|
+
const cacheResolved = (0, path_1.resolve)(this.managerCacheRoot);
|
|
246
|
+
if (!resolved.startsWith(cacheResolved + path_1.sep) && resolved !== cacheResolved) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
if (!(0, fs_1.existsSync)(destination))
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
const content = (0, fs_1.readFileSync)(destination, 'utf-8');
|
|
253
|
+
if (this.shouldFilter && this.shouldFilter(content))
|
|
254
|
+
return null;
|
|
255
|
+
return content;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Issue #869 Phase 2: read a capability file from the org overlay cache
|
|
263
|
+
* (~/.fraim/org/<type>/…). Returns null if not present or filtered.
|
|
264
|
+
*
|
|
265
|
+
* Precedence: project > manager > org > synced baseline > registry > remote.
|
|
266
|
+
* Security: same canonical-path containment guard as readManagerOverlayFile.
|
|
267
|
+
*/
|
|
268
|
+
readOrgOverlayFile(path) {
|
|
269
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
270
|
+
const destination = (0, path_1.join)(this.orgCacheRoot, ...normalized.split('/'));
|
|
271
|
+
const resolved = (0, path_1.resolve)(destination);
|
|
272
|
+
const cacheResolved = (0, path_1.resolve)(this.orgCacheRoot);
|
|
273
|
+
if (!resolved.startsWith(cacheResolved + path_1.sep) && resolved !== cacheResolved) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (!(0, fs_1.existsSync)(destination))
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
const content = (0, fs_1.readFileSync)(destination, 'utf-8');
|
|
280
|
+
if (this.shouldFilter && this.shouldFilter(content))
|
|
281
|
+
return null;
|
|
282
|
+
return content;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
228
288
|
readWorkspaceRegistryFile(path) {
|
|
229
289
|
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
230
|
-
const
|
|
290
|
+
const registryRoot = (0, path_1.join)(this.workspaceRoot, 'registry');
|
|
291
|
+
const registryPath = (0, path_1.join)(registryRoot, ...normalizedPath.split('/'));
|
|
292
|
+
const resolved = (0, path_1.resolve)(registryPath);
|
|
293
|
+
const rootResolved = (0, path_1.resolve)(registryRoot);
|
|
294
|
+
if (!resolved.startsWith(rootResolved + path_1.sep) && resolved !== rootResolved)
|
|
295
|
+
return null;
|
|
231
296
|
if (!(0, fs_1.existsSync)(registryPath)) {
|
|
232
297
|
return null;
|
|
233
298
|
}
|
|
@@ -386,6 +451,28 @@ class LocalRegistryResolver {
|
|
|
386
451
|
const stripMcpHeader = options.stripMcpHeader ?? false;
|
|
387
452
|
// Check for local override
|
|
388
453
|
if (!this.hasLocalOverride(path)) {
|
|
454
|
+
// Issue #869: manager overlay (precedence 2 — between project override and synced baseline)
|
|
455
|
+
const managerOverlayContent = this.readManagerOverlayFile(path);
|
|
456
|
+
if (managerOverlayContent !== null) {
|
|
457
|
+
return {
|
|
458
|
+
content: managerOverlayContent,
|
|
459
|
+
source: 'local',
|
|
460
|
+
personalized: true,
|
|
461
|
+
inherited: false,
|
|
462
|
+
scope: 'manager'
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
// Issue #869 Phase 2: org overlay (precedence 3 — after manager, before synced baseline)
|
|
466
|
+
const orgOverlayContent = this.readOrgOverlayFile(path);
|
|
467
|
+
if (orgOverlayContent !== null) {
|
|
468
|
+
return {
|
|
469
|
+
content: orgOverlayContent,
|
|
470
|
+
source: 'local',
|
|
471
|
+
personalized: true,
|
|
472
|
+
inherited: false,
|
|
473
|
+
scope: 'org'
|
|
474
|
+
};
|
|
475
|
+
}
|
|
389
476
|
const syncedLocalContent = this.readSyncedLocalFile(path);
|
|
390
477
|
if (syncedLocalContent !== null) {
|
|
391
478
|
// Synced baseline (fraim/ai-employee), not personalized.
|
|
@@ -463,7 +550,8 @@ class LocalRegistryResolver {
|
|
|
463
550
|
source: 'local',
|
|
464
551
|
personalized: true,
|
|
465
552
|
inherited: resolved.imports.length > 0,
|
|
466
|
-
imports: resolved.imports.length > 0 ? resolved.imports : undefined
|
|
553
|
+
imports: resolved.imports.length > 0 ? resolved.imports : undefined,
|
|
554
|
+
scope: 'project'
|
|
467
555
|
};
|
|
468
556
|
// Add metadata comment
|
|
469
557
|
if (includeMetadata) {
|
|
@@ -285,7 +285,10 @@ async function evaluatePersonaAccess(dbService, userId, jobName, apiKey, returnT
|
|
|
285
285
|
.map((entitlement) => entitlement.personaKey)
|
|
286
286
|
.filter((personaKey) => (0, persona_hiring_1.isPersonaHireKey)(personaKey))));
|
|
287
287
|
const protectedPersonaKey = (0, persona_capability_bundles_1.getProtectedPersonaForJob)(jobName);
|
|
288
|
-
|
|
288
|
+
// No protected owner, or a non-hireable owner (the display-only FRAIMworker
|
|
289
|
+
// identity, issue #902), means the job is not hire-gated. Allow it and never
|
|
290
|
+
// look the key up in PERSONA_HIRE_CATALOG, which is undefined for FRAIMworker.
|
|
291
|
+
if (!protectedPersonaKey || !(0, persona_hiring_1.isPersonaHireKey)(protectedPersonaKey)) {
|
|
289
292
|
return {
|
|
290
293
|
allowed: true,
|
|
291
294
|
personaKey: null,
|