fraim 2.0.348 → 2.0.350

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.
@@ -50,6 +50,7 @@ class LocalRegistryResolver {
50
50
  constructor(options) {
51
51
  this.workspaceRoot = options.workspaceRoot;
52
52
  this.effectiveFraimDir = options.effectiveFraimDir;
53
+ this.userLevelOnly = options.userLevelOnly ?? false;
53
54
  this.remoteContentResolver = options.remoteContentResolver;
54
55
  this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
55
56
  this.shouldFilter = options.shouldFilter;
@@ -87,7 +88,14 @@ class LocalRegistryResolver {
87
88
  * Check if a local override exists for the given path
88
89
  */
89
90
  hasLocalOverride(path) {
90
- return (0, fs_1.existsSync)(this.getOverridePath(path));
91
+ return !this.userLevelOnly && (0, fs_1.existsSync)(this.getOverridePath(path));
92
+ }
93
+ /** Whether an effective personalized layer should replace a remote baseline response. */
94
+ hasPersonalizedOverride(path) {
95
+ return this.hasLocalOverride(path)
96
+ || this.readManagerLocalOverlayFile(path) !== null
97
+ || this.readManagerOverlayFile(path) !== null
98
+ || this.readOrgOverlayFile(path) !== null;
91
99
  }
92
100
  /**
93
101
  * Find the actual relative path for a registry item by searching subdirectories.
@@ -108,11 +116,11 @@ class LocalRegistryResolver {
108
116
  // `jobs/<category>/<name>.md`, so the job was reported not found even
109
117
  // though the file existed.
110
118
  const searchRoots = [
111
- this.getFraimPath('personalized-employee', dir),
119
+ ...(!this.userLevelOnly ? [this.getFraimPath('personalized-employee', dir)] : []),
112
120
  (0, path_1.join)(this.managerLocalRoot, dir),
113
121
  (0, path_1.join)(this.managerCacheRoot, dir),
114
122
  (0, path_1.join)(this.orgCacheRoot, dir),
115
- (0, path_1.join)(this.workspaceRoot, 'registry', dir),
123
+ ...(!this.userLevelOnly ? [(0, path_1.join)(this.workspaceRoot, 'registry', dir)] : []),
116
124
  // Installed projects discover categorized capabilities in machine catalogs.
117
125
  this.getSyncedCatalogRoot('ai-employee', dir),
118
126
  ...(type !== 'skills' ? [this.getSyncedCatalogRoot('ai-manager', dir)] : []),
@@ -326,6 +334,8 @@ class LocalRegistryResolver {
326
334
  }
327
335
  }
328
336
  readWorkspaceRegistryFile(path) {
337
+ if (this.userLevelOnly)
338
+ return null;
329
339
  const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
330
340
  const registryRoot = (0, path_1.join)(this.workspaceRoot, 'registry');
331
341
  const registryPath = (0, path_1.join)(registryRoot, ...normalizedPath.split('/'));
@@ -7,6 +7,7 @@ exports.WORKSPACE_SYNCED_CONTENT_DIRS = exports.WORKSPACE_FRAIM_DIRNAME = void 0
7
7
  exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
8
8
  exports.workspaceFraimExists = workspaceFraimExists;
9
9
  exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
10
+ exports.workspaceOnboardingReady = workspaceOnboardingReady;
10
11
  exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
11
12
  exports.assertWorkspaceFraimRoot = assertWorkspaceFraimRoot;
12
13
  exports.assertWorkspaceFraimPath = assertWorkspaceFraimPath;
@@ -75,6 +76,19 @@ function workspaceFraimExists(projectRoot = process.cwd()) {
75
76
  function getWorkspaceConfigPath(projectRoot = process.cwd()) {
76
77
  return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), 'config.json');
77
78
  }
79
+ /** Config presence is readiness; unreadable filesystem state is not absence. */
80
+ function workspaceOnboardingReady(projectRoot) {
81
+ if (!projectRoot)
82
+ return false;
83
+ try {
84
+ (0, fs_1.statSync)(getWorkspaceConfigPath(projectRoot));
85
+ return true;
86
+ }
87
+ catch (error) {
88
+ const code = error.code;
89
+ return code === 'ENOENT' || code === 'ENOTDIR' ? false : null;
90
+ }
91
+ }
78
92
  function getWorkspaceFraimPath(projectRoot = process.cwd(), ...parts) {
79
93
  return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), ...parts);
80
94
  }
@@ -1535,6 +1535,7 @@ function readRejectedProposals(workspaceRoot, userId) {
1535
1535
  return [];
1536
1536
  const out = [];
1537
1537
  const seen = new Set();
1538
+ const projectOwnerPath = (0, owner_identity_1.ownerPath)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot));
1538
1539
  const headingRe = /^## (Rejected|Superseded): (.+)$/m;
1539
1540
  const reasonRe = /^Reason: (.+)$/m;
1540
1541
  const revisitRe = /^Revisit when: (.+)$/m;
@@ -1567,7 +1568,9 @@ function readRejectedProposals(workspaceRoot, userId) {
1567
1568
  let proposal = {};
1568
1569
  if (json) {
1569
1570
  try {
1570
- proposal = JSON.parse(json[1]);
1571
+ const parsed = JSON.parse(json[1]);
1572
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
1573
+ proposal = parsed;
1571
1574
  }
1572
1575
  catch {
1573
1576
  // Keep the human-readable half; the JSON only enriches it.
@@ -1576,12 +1579,24 @@ function readRejectedProposals(workspaceRoot, userId) {
1576
1579
  const decision = (proposal.decision || {});
1577
1580
  const content = (proposal.content || {});
1578
1581
  const trace = (content.trace || {});
1582
+ let targetOwner;
1583
+ try {
1584
+ const owner = proposal.target?.owner;
1585
+ (0, owner_identity_1.validateOwner)(owner);
1586
+ targetOwner = { level: owner.level, path: owner.path };
1587
+ }
1588
+ catch {
1589
+ // Older/damaged targets remain visible as unknown; never infer
1590
+ // their project from the shared history file's location.
1591
+ }
1592
+ if (targetOwner?.level === 'Project' && targetOwner.path !== projectOwnerPath)
1593
+ continue;
1579
1594
  const claimKey = typeof proposal.claimKey === 'string' && proposal.claimKey
1580
1595
  ? proposal.claimKey
1581
1596
  : parts[1] || heading[2];
1582
1597
  // The same claim can appear in more than one configured base. The first
1583
1598
  // base supplies display metadata for that same recorded decision.
1584
- const key = `${claimKey}:${String(decision.at || '')}`;
1599
+ const key = JSON.stringify([claimKey, String(decision.at || ''), targetOwner?.level, targetOwner?.path]);
1585
1600
  if (seen.has(key))
1586
1601
  continue;
1587
1602
  seen.add(key);
@@ -1596,6 +1611,7 @@ function readRejectedProposals(workspaceRoot, userId) {
1596
1611
  rejectedAt: typeof decision.at === 'string' ? decision.at : '',
1597
1612
  ...(typeof trace.recurrences === 'number' ? { recurrencesAtRejection: trace.recurrences } : {}),
1598
1613
  displayPath,
1614
+ ...(targetOwner ? { targetOwner } : {}),
1599
1615
  });
1600
1616
  }
1601
1617
  }
@@ -1657,8 +1657,7 @@ class FraimLocalMCPServer {
1657
1657
  if (request.method !== 'tools/call' || typeof toolName !== 'string') {
1658
1658
  return this.processResponseWithHydration(response, requestSessionId);
1659
1659
  }
1660
- const projectRoot = this.findProjectRoot();
1661
- const resolver = projectRoot ? this.getRegistryResolver(requestSessionId) : null;
1660
+ const resolver = this.getRegistryResolver(requestSessionId);
1662
1661
  let finalizedResponse = response;
1663
1662
  // 1. Resolve top-level override if this tool maps to a specific file.
1664
1663
  if (resolver) {
@@ -1670,14 +1669,14 @@ class FraimLocalMCPServer {
1670
1669
  else if (toolName === 'get_fraim_job')
1671
1670
  registryPath = await resolver.findRegistryPath('jobs', args.job);
1672
1671
  if (registryPath) {
1673
- const localOverride = resolver.hasLocalOverride(registryPath);
1672
+ const localOverride = resolver.hasPersonalizedOverride(registryPath);
1674
1673
  const remoteFailed = Boolean(response.error);
1675
1674
  if (response.error) {
1676
1675
  this.log(`[req:${requestId}] Remote error for ${registryPath}: code=${response.error.code}`);
1677
1676
  }
1678
1677
  if (localOverride || remoteFailed) {
1679
1678
  try {
1680
- // personalized-employee/ takes priority; when remote fails, resolver falls back to synced/cached content
1679
+ // Every configured personalized layer takes priority over the remote baseline.
1681
1680
  const resolved = await resolver.resolveFile(registryPath).catch(() => null);
1682
1681
  if (resolved?.content) {
1683
1682
  const newResponse = {
@@ -1990,75 +1989,60 @@ class FraimLocalMCPServer {
1990
1989
  const { getEffectiveFraimDir } = require('../core/utils/project-fraim-paths');
1991
1990
  const effectiveFraimDir = projectRoot
1992
1991
  ? getEffectiveFraimDir(projectRoot)
1993
- : getEffectiveFraimDir(process.cwd());
1992
+ : (0, project_fraim_paths_1.getUserFraimDirPath)();
1994
1993
  if (effectiveFraimDir) {
1995
1994
  this.log(`📂 Effective FRAIM dir: ${effectiveFraimDir}`);
1996
1995
  }
1997
- if (!projectRoot && !effectiveFraimDir) {
1998
- this.log('⚠️ No project root or user-level FRAIM found, override resolution disabled');
1999
- // Return a resolver that always falls back to remote
2000
- return new local_registry_resolver_1.LocalRegistryResolver({
2001
- workspaceRoot: process.cwd(),
2002
- // Issue #1043 round 2: overlay roots are the layers' homes. There is no
2003
- // separate cache to point at any more.
2004
- managerCacheRoot: (0, pack_home_1.resolvePackHome)('manager').contentRoot,
2005
- orgCacheRoot: (0, pack_home_1.resolvePackHome)('org').contentRoot,
2006
- remoteContentResolver: async (_path) => {
2007
- throw new Error('No project root available');
2008
- },
2009
- shouldFilter: (content) => this.isStub(content)
2010
- });
2011
- }
2012
- else {
2013
- // Determine if we need effectiveFraimDir override.
2014
- // If the effective dir is user-level (~/.fraim/), it's NOT under projectRoot/fraim/,
2015
- // so we pass it as effectiveFraimDir to bypass the getWorkspaceFraimPath logic.
2016
- const workspaceRoot = projectRoot || process.cwd();
2017
- const projectFraimDir = projectRoot ? (0, path_1.join)(projectRoot, 'fraim') : '';
2018
- const needsEffectiveDirOverride = effectiveFraimDir && effectiveFraimDir !== projectFraimDir;
2019
- return new local_registry_resolver_1.LocalRegistryResolver({
2020
- workspaceRoot,
2021
- ...(needsEffectiveDirOverride ? { effectiveFraimDir } : {}),
2022
- // Issue #1043 round 2: overlay roots are the layers' homes.
2023
- managerCacheRoot: (0, pack_home_1.resolvePackHome)('manager').contentRoot,
2024
- orgCacheRoot: (0, pack_home_1.resolvePackHome)('org').contentRoot,
2025
- shouldFilter: (content) => this.isStub(content),
2026
- remoteContentResolver: async (path) => {
2027
- // Fetch parent content from remote for inheritance
2028
- this.log(`🔄 Remote content resolver: fetching ${path}`);
2029
- this.log(`🔄 Fetching raw file content: ${path}`);
2030
- const request = {
2031
- jsonrpc: '2.0',
2032
- id: (0, crypto_1.randomUUID)(),
2033
- method: 'tools/call',
2034
- params: {
2035
- name: 'get_fraim_file',
2036
- arguments: {
2037
- path,
2038
- raw: true
2039
- }
1996
+ // Determine if we need effectiveFraimDir override.
1997
+ // If the effective dir is user-level (~/.fraim/), it's NOT under projectRoot/fraim/,
1998
+ // so we pass it as effectiveFraimDir to bypass the getWorkspaceFraimPath logic.
1999
+ const workspaceRoot = projectRoot || (0, project_fraim_paths_1.getUserFraimDirPath)();
2000
+ const projectFraimDir = projectRoot ? (0, path_1.join)(projectRoot, 'fraim') : '';
2001
+ const needsEffectiveDirOverride = effectiveFraimDir && effectiveFraimDir !== projectFraimDir;
2002
+ return new local_registry_resolver_1.LocalRegistryResolver({
2003
+ workspaceRoot,
2004
+ userLevelOnly: !projectRoot,
2005
+ ...(needsEffectiveDirOverride ? { effectiveFraimDir } : {}),
2006
+ // Issue #1043 round 2: overlay roots are the layers' homes.
2007
+ managerLocalRoot: (0, pack_home_1.resolvePackHome)('manager', { migrateLegacy: false }).contentRoot,
2008
+ managerCacheRoot: (0, pack_home_1.resolvePackHome)('manager', { migrateLegacy: false }).contentRoot,
2009
+ orgCacheRoot: (0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot,
2010
+ shouldFilter: (content) => this.isStub(content),
2011
+ remoteContentResolver: async (path) => {
2012
+ // Fetch parent content from remote for inheritance
2013
+ this.log(`🔄 Remote content resolver: fetching ${path}`);
2014
+ this.log(`🔄 Fetching raw file content: ${path}`);
2015
+ const request = {
2016
+ jsonrpc: '2.0',
2017
+ id: (0, crypto_1.randomUUID)(),
2018
+ method: 'tools/call',
2019
+ params: {
2020
+ name: 'get_fraim_file',
2021
+ arguments: {
2022
+ path,
2023
+ raw: true
2040
2024
  }
2041
- };
2042
- this.applyRequestSessionId(request, requestSessionId);
2043
- const response = await this._doProxyToRemote(request);
2044
- if (response.error) {
2045
- this.logError(`❌ Remote content resolver failed: ${response.error.message}`);
2046
- throw new Error(`Failed to fetch parent: ${response.error.message}`);
2047
2025
  }
2048
- // Extract content from MCP response format
2049
- if (response.result?.content && Array.isArray(response.result.content)) {
2050
- const textContent = response.result.content.find((c) => c.type === 'text');
2051
- if (textContent?.text) {
2052
- this.log(`✅ Remote content resolver: fetched ${textContent.text.length} chars`);
2053
- return textContent.text;
2054
- }
2026
+ };
2027
+ this.applyRequestSessionId(request, requestSessionId);
2028
+ const response = await this._doProxyToRemote(request);
2029
+ if (response.error) {
2030
+ this.logError(`❌ Remote content resolver failed: ${response.error.message}`);
2031
+ throw new Error(`Failed to fetch parent: ${response.error.message}`);
2032
+ }
2033
+ // Extract content from MCP response format
2034
+ if (response.result?.content && Array.isArray(response.result.content)) {
2035
+ const textContent = response.result.content.find((c) => c.type === 'text');
2036
+ if (textContent?.text) {
2037
+ this.log(`✅ Remote content resolver: fetched ${textContent.text.length} chars`);
2038
+ return textContent.text;
2055
2039
  }
2056
- this.logError(`❌ Remote content resolver: no content in response for ${path}`);
2057
- this.logError(`Response: ${JSON.stringify(response, null, 2)}`);
2058
- throw new Error(`No content in remote response for ${path}`);
2059
2040
  }
2060
- });
2061
- }
2041
+ this.logError(`❌ Remote content resolver: no content in response for ${path}`);
2042
+ this.logError(`Response: ${JSON.stringify(response, null, 2)}`);
2043
+ throw new Error(`No content in remote response for ${path}`);
2044
+ }
2045
+ });
2062
2046
  }
2063
2047
  /**
2064
2048
  * Check if content is a "stub" indicating local version should be ignored.
@@ -201,6 +201,7 @@ Example:
201
201
  description: `Get a specific skill, rule, or reference file from the FRAIM registry by path.
202
202
 
203
203
  For running FRAIM jobs, use get_fraim_job instead — do NOT call get_fraim_file for job execution.
204
+ For read-only diagnosis or delivery verification, use get_fraim_file({ path: "jobs/<category>/<job>.md", raw: true }). This does not start a job; the local proxy resolves effective overrides, inheritance, includes, and template values.
204
205
 
205
206
  Examples:
206
207
  - get_fraim_file({ path: "skills/communication/active-listening.md" })
@@ -226,7 +227,7 @@ Examples:
226
227
  name: 'get_fraim_job',
227
228
  description: `Execute a named FRAIM job after explicit FRAIM activation. Call this tool whenever the user asks to run, start, or execute a named FRAIM job. Returns phased instructions — follow each phase and call seekMentoring to advance phases.
228
229
 
229
- Do NOT use get_fraim_file to load job content. Always use get_fraim_job for job execution.
230
+ Use this tool to start job execution, not to inspect candidate jobs. Read-only inspection uses get_fraim_file with the catalog job path and raw: true.
230
231
 
231
232
  Examples:
232
233
  - get_fraim_job({ job: "feature-specification" })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.348",
3
+ "version": "2.0.350",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {