fraim 2.0.218 → 2.0.220
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/org.js +2 -2
- 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 +34 -22
- 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
|
@@ -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) => {
|
|
@@ -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,24 +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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
]);
|
|
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();
|
|
22
21
|
function isFreeJob(jobName) {
|
|
23
22
|
return exports.FREE_JOBS.has(jobName);
|
|
24
23
|
}
|
|
@@ -46,7 +45,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
46
45
|
personaKey: 'beza',
|
|
47
46
|
bundleId: 'persona-beza-core',
|
|
48
47
|
catalogMetadata: buildCatalogMetadata('beza', ['competitive-analysis', 'review-business-strategy', 'pricing-strategy-definition']),
|
|
49
|
-
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'],
|
|
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'],
|
|
50
49
|
protectedAliases: ['business-strategy', 'company-strategy'],
|
|
51
50
|
defaultHireMode: 'job',
|
|
52
51
|
lockCopy: 'Hire BeZa to unlock business strategy work for this request.'
|
|
@@ -91,7 +90,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
91
90
|
personaKey: 'gautam',
|
|
92
91
|
bundleId: 'persona-gautam-core',
|
|
93
92
|
catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'domain-registration-research']),
|
|
94
|
-
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-
|
|
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'],
|
|
95
94
|
protectedAliases: ['gtm', 'marketing'],
|
|
96
95
|
defaultHireMode: 'job',
|
|
97
96
|
lockCopy: 'Hire GauTaM to unlock go-to-market and paid media work for this request.'
|
|
@@ -126,8 +125,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
126
125
|
mandy: {
|
|
127
126
|
personaKey: 'mandy',
|
|
128
127
|
bundleId: 'persona-mandy-core',
|
|
129
|
-
catalogMetadata: buildCatalogMetadata('mandy', ['fully-delegate', '
|
|
130
|
-
protectedJobs: ['fully-delegate', 'delivery-governance-review', 'issue-preparation', 'issue-retrospective', 'work-completion', '
|
|
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'],
|
|
131
130
|
protectedAliases: ['manager', 'team-lead', 'orchestrator'],
|
|
132
131
|
defaultHireMode: 'job',
|
|
133
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.'
|
|
@@ -181,7 +180,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
181
180
|
personaKey: 'casey',
|
|
182
181
|
bundleId: 'persona-casey-core',
|
|
183
182
|
catalogMetadata: buildCatalogMetadata('casey', ['update-crm', 'support-case-resolution', 'support-system-operationalization']),
|
|
184
|
-
protectedJobs: ['crm-
|
|
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'],
|
|
185
184
|
protectedAliases: ['customer-success', 'customer-support', 'csm', 'support'],
|
|
186
185
|
defaultHireMode: 'job',
|
|
187
186
|
lockCopy: 'Hire CaSey to unlock customer success and support work for this request.'
|
|
@@ -211,7 +210,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
211
210
|
personaKey: 'mona',
|
|
212
211
|
bundleId: 'persona-mona-core',
|
|
213
212
|
catalogMetadata: buildCatalogMetadata('mona', ['financial-analysis', 'fundraising-prospect-discovery', 'investor-pitch-preparation']),
|
|
214
|
-
protectedJobs: ['financial-analysis', 'fpa-and-forecasting', 'monthly-close-review', 'tax-strategy-planning', '
|
|
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'],
|
|
215
214
|
protectedAliases: ['finance', 'financial-modeling', 'fpa', 'bookkeeping'],
|
|
216
215
|
defaultHireMode: 'job',
|
|
217
216
|
lockCopy: 'Hire MONa to unlock financial modeling, FP&A, and tax strategy work for this request.'
|
|
@@ -269,14 +268,27 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
|
|
|
269
268
|
PROTECTED_JOB_TO_PERSONA.set(jobName, bundle.personaKey);
|
|
270
269
|
}
|
|
271
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
|
+
]);
|
|
272
280
|
function getPersonaCapabilityBundle(personaKey) {
|
|
273
281
|
return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
|
|
274
282
|
}
|
|
275
283
|
function getProtectedPersonaForJob(jobName) {
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
278
290
|
}
|
|
279
|
-
return
|
|
291
|
+
return null;
|
|
280
292
|
}
|
|
281
293
|
function listPersonaCapabilityBundles() {
|
|
282
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,
|