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.
@@ -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 `# FRAIM
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
- const dirPath = path_1.default.join(snapshotDir, dirName);
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
- fs_1.default.writeFileSync(destination, decorateManagedManagerFile(file.content, metadata.backend));
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, or a personal learning file)`);
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.shouldDecorateAsMarkdown = shouldDecorateAsMarkdown;
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, or the org brand descriptor. A single guard
52
- * shared by the git and cloud paths.
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
- * Managed-content decoration prepends a markdown "synced from your org" header.
59
- * It must only touch markdown prepending it to org_brand.json would corrupt
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 shouldDecorateAsMarkdown(relativePath) {
63
- return relativePath.toLowerCase().endsWith('.md');
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
- const dirPath = path_1.default.join(snapshotDir, dirName);
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
- if (!isSafePackPath(file.relativePath))
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
- // #744: only markdown gets the managed-content header; JSON is written verbatim.
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, or org-*.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
- if (!PACK_RELATIVE_PATH.test(a.relativePath)) {
64
- throw new Error(`Invalid org-pack path '${a.relativePath}' (must be context|rules|learnings/<name>.md).`);
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
- exports.FREE_JOBS = new Set([
11
- 'create-architecture',
12
- 'project-scaffolding',
13
- 'blue-sky-brainstorming',
14
- 'codebase-analysis-and-ideation',
15
- 'domain-registration-research',
16
- 'google-workspace-setup',
17
- 'github-org-setup',
18
- 'contribute-to-fraim',
19
- 'file-fraim-issue',
20
- 'praise-fraim',
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-infographic', 'notebooklm-podcast', 'notebooklm-presentation', 'notebooklm-report', 'notebooklm-video-overview'],
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', 'stakeholder-status-reporting', 'weekly-operating-review']),
130
- protectedJobs: ['fully-delegate', 'delivery-governance-review', 'issue-preparation', 'issue-retrospective', 'work-completion', 'stakeholder-status-reporting', 'send-stakeholder-update', 'weekly-operating-review', 'cross-functional-dependency-management', 'portfolio-impact-report'],
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-account-health-review', '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'],
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', 'aws-activate-credits-application', 'community-funding-preparation', 'fundraising-prospect-discovery', 'google-cloud-credits-application', 'investor-pitch-preparation', 'microsoft-azure-credits-application', 'review-funding-preparation', 'invoice-generation', 'business-banking-setup'],
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
- if (isFreeJob(jobName)) {
277
- return null;
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 PROTECTED_JOB_TO_PERSONA.get(jobName) || null;
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 registryPath = (0, path_1.join)(this.workspaceRoot, 'registry', ...normalizedPath.split('/'));
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
- if (!protectedPersonaKey) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.218",
3
+ "version": "2.0.220",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {