fraim 2.0.311 → 2.0.312
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/setup/ide-invocation-surfaces.js +1 -1
- package/dist/src/cli/setup/user-level-sync.js +4 -0
- package/dist/src/config/persona-capability-bundles.js +12 -1
- package/dist/src/local-mcp-server/stdio-server.js +162 -29
- package/dist/src/mcp/tool-schemas.js +4 -0
- package/package.json +1 -1
|
@@ -72,7 +72,7 @@ ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
|
72
72
|
If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect \`fraim/personalized-employee/jobs/\` for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
|
|
73
73
|
|
|
74
74
|
3. **Find the match**:
|
|
75
|
-
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists,
|
|
75
|
+
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists, ask once: "No catalog job matches. Would you like to run this as an ad-hoc task?" On confirmation, call \`get_fraim_job({ job: "adhoc-prompt" })\` and execute it with the user's instructions as the task input — do not pick the nearest catalog job. Do not ask again if the user already provided instructions.
|
|
76
76
|
|
|
77
77
|
4. **Load the full content**:
|
|
78
78
|
- For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
|
|
@@ -125,6 +125,10 @@ function ensureUserLevelDependencies(userFraimDir) {
|
|
|
125
125
|
if (missing.length === 0) {
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
+
if (process.env.FRAIM_SKIP_USER_LEVEL_DEP_INSTALL === '1') {
|
|
129
|
+
console.log(chalk_1.default.yellow(`TEST_MODE: skipping user-level runtime dependency install (${missing.join(', ')}).`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
128
132
|
console.log(chalk_1.default.blue(`📦 Installing user-level runtime dependencies (${missing.join(', ')})...`));
|
|
129
133
|
try {
|
|
130
134
|
(0, child_process_1.execSync)('npm install --no-audit --no-fund --no-save --no-package-lock --omit=dev', {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PERSONA_CAPABILITY_BUNDLES = exports.FREE_JOBS = exports.GENERIC_WORKER_PERSONA_KEY = void 0;
|
|
3
|
+
exports.UNOWNED_EXEMPT_JOBS = 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;
|
|
@@ -315,6 +315,17 @@ const GENERIC_WORKER_OWNED_JOBS = new Set([
|
|
|
315
315
|
'organization-onboarding',
|
|
316
316
|
'organizational-learning-synthesis',
|
|
317
317
|
]);
|
|
318
|
+
// Jobs intentionally left without a named persona owner. They resolve to null
|
|
319
|
+
// from getProtectedPersonaForJob (runs ungated; Hub attributes them to the
|
|
320
|
+
// DEFAULT_UNASSIGNED_PERSONA_KEY/MANdy — same behavior as today's adhoc runs).
|
|
321
|
+
// validate-job-ownership exempts these from the "every job must have an owner"
|
|
322
|
+
// assertion so the validator still catches accidentally unowned jobs.
|
|
323
|
+
//
|
|
324
|
+
// Issue #1610: adhoc-prompt is a manager-directed fallback job, not tied to any
|
|
325
|
+
// specialist hire, matching the existing no-employee attribution of adhoc runs.
|
|
326
|
+
exports.UNOWNED_EXEMPT_JOBS = new Set([
|
|
327
|
+
'adhoc-prompt',
|
|
328
|
+
]);
|
|
318
329
|
function getPersonaCapabilityBundle(personaKey) {
|
|
319
330
|
return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
|
|
320
331
|
}
|
|
@@ -81,6 +81,7 @@ const skill_include_dedup_js_1 = require("./skill-include-dedup.js");
|
|
|
81
81
|
*/
|
|
82
82
|
class FraimTemplateEngine {
|
|
83
83
|
constructor(opts) {
|
|
84
|
+
this.configLoadAttempted = false;
|
|
84
85
|
this.userEmail = null;
|
|
85
86
|
this.deliveryTemplatesCache = null;
|
|
86
87
|
this.providerTemplatesCache = {};
|
|
@@ -117,6 +118,7 @@ class FraimTemplateEngine {
|
|
|
117
118
|
}
|
|
118
119
|
setConfig(config) {
|
|
119
120
|
this.config = config;
|
|
121
|
+
this.configLoadAttempted = true;
|
|
120
122
|
}
|
|
121
123
|
setUserEmail(email) {
|
|
122
124
|
this.userEmail = email;
|
|
@@ -124,6 +126,57 @@ class FraimTemplateEngine {
|
|
|
124
126
|
getUserEmail() {
|
|
125
127
|
return this.userEmail;
|
|
126
128
|
}
|
|
129
|
+
resolveEmailLazy() {
|
|
130
|
+
if (this.userEmail)
|
|
131
|
+
return this.userEmail;
|
|
132
|
+
try {
|
|
133
|
+
const dir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
134
|
+
const prefsPath = (0, path_1.join)(dir, 'preferences.json');
|
|
135
|
+
if ((0, fs_1.existsSync)(prefsPath)) {
|
|
136
|
+
const prefs = JSON.parse((0, fs_1.readFileSync)(prefsPath, 'utf8'));
|
|
137
|
+
if (typeof prefs.userEmail === 'string' && prefs.userEmail) {
|
|
138
|
+
this.userEmail = prefs.userEmail;
|
|
139
|
+
return this.userEmail;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
resolveMachineInfoLazy() {
|
|
147
|
+
if (this.machineInfo)
|
|
148
|
+
return this.machineInfo;
|
|
149
|
+
try {
|
|
150
|
+
this.machineInfo = {
|
|
151
|
+
hostname: (0, os_1.hostname)(),
|
|
152
|
+
platform: (0, os_1.platform)(),
|
|
153
|
+
memory: (0, os_1.totalmem)(),
|
|
154
|
+
cpus: (0, os_1.cpus)().length
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
return this.machineInfo;
|
|
159
|
+
}
|
|
160
|
+
resolveConfigLazy() {
|
|
161
|
+
if (this.config)
|
|
162
|
+
return this.config;
|
|
163
|
+
// If setConfig() was called explicitly (including with null), respect that — no disk read.
|
|
164
|
+
if (this.configLoadAttempted)
|
|
165
|
+
return null;
|
|
166
|
+
if (!this.projectRoot)
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
const configPath = (0, path_1.join)(this.projectRoot, 'fraim', 'config.json');
|
|
170
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
171
|
+
this.config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
172
|
+
this.configLoadAttempted = true;
|
|
173
|
+
return this.config;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore */ }
|
|
177
|
+
this.configLoadAttempted = true;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
127
180
|
substituteTemplates(content) {
|
|
128
181
|
return this.substituteTemplatesWithNotices(content).content;
|
|
129
182
|
}
|
|
@@ -131,15 +184,16 @@ class FraimTemplateEngine {
|
|
|
131
184
|
let result = content;
|
|
132
185
|
const notices = [];
|
|
133
186
|
const blockingRequirements = [];
|
|
134
|
-
// Substitute {{proxy.user.email}}
|
|
135
|
-
|
|
136
|
-
|
|
187
|
+
// Substitute {{proxy.user.email}} — resolved lazily from preferences.json when fraim_connect was skipped.
|
|
188
|
+
const resolvedEmail = this.resolveEmailLazy();
|
|
189
|
+
if (resolvedEmail) {
|
|
190
|
+
result = result.replace(/\{\{proxy\.user\.email\}\}/g, resolvedEmail);
|
|
137
191
|
}
|
|
138
192
|
// 0. Substitute runtime context tokens: {{agent.*}}, {{machine.*}}, {{repository.*}}
|
|
139
|
-
//
|
|
193
|
+
// Machine info is resolved lazily from the OS when fraim_connect was skipped.
|
|
140
194
|
const contexts = {
|
|
141
195
|
agent: this.agentInfo,
|
|
142
|
-
machine: this.
|
|
196
|
+
machine: this.resolveMachineInfoLazy(),
|
|
143
197
|
repository: this.repoInfo
|
|
144
198
|
};
|
|
145
199
|
result = result.replace(/\{\{(agent|machine|repository)\.([^}]+)\}\}/g, (match, ns, path) => {
|
|
@@ -207,8 +261,9 @@ class FraimTemplateEngine {
|
|
|
207
261
|
return originalMatch;
|
|
208
262
|
}
|
|
209
263
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
264
|
+
const config = this.resolveConfigLazy();
|
|
265
|
+
if (config) {
|
|
266
|
+
const value = (0, object_utils_1.getNestedValue)(config, path);
|
|
212
267
|
if (value !== undefined) {
|
|
213
268
|
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
214
269
|
}
|
|
@@ -791,20 +846,25 @@ class FraimLocalMCPServer {
|
|
|
791
846
|
return;
|
|
792
847
|
}
|
|
793
848
|
if (this.connectSyncInFlight) {
|
|
794
|
-
|
|
849
|
+
this.maybeRefreshOrgCache(String(requestId));
|
|
850
|
+
this.maybeRefreshManagerCache(String(requestId));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const projectRoot = this.findProjectRoot();
|
|
854
|
+
this.latestConnectSyncWarning = null;
|
|
855
|
+
if (!projectRoot || !(0, project_fraim_paths_1.workspaceFraimExists)(projectRoot)) {
|
|
856
|
+
this.maybeRefreshOrgCache(String(requestId));
|
|
857
|
+
this.maybeRefreshManagerCache(String(requestId));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const reason = this.getLocalCatalogSyncReason(projectRoot);
|
|
861
|
+
if (!reason) {
|
|
862
|
+
this.maybeRefreshOrgCache(String(requestId));
|
|
863
|
+
this.maybeRefreshManagerCache(String(requestId));
|
|
795
864
|
return;
|
|
796
865
|
}
|
|
866
|
+
this.log(`[req:${requestId}] Refreshing local FRAIM catalog in background because ${reason}`);
|
|
797
867
|
this.connectSyncInFlight = (async () => {
|
|
798
|
-
const projectRoot = this.findProjectRoot();
|
|
799
|
-
this.latestConnectSyncWarning = null;
|
|
800
|
-
if (!projectRoot || !(0, project_fraim_paths_1.workspaceFraimExists)(projectRoot)) {
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
const reason = this.getLocalCatalogSyncReason(projectRoot);
|
|
804
|
-
if (!reason) {
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
this.log(`[req:${requestId}] Refreshing local FRAIM catalog before fraim_connect because ${reason}`);
|
|
808
868
|
try {
|
|
809
869
|
await this.performLocalCatalogSync(projectRoot);
|
|
810
870
|
this.log(`[req:${requestId}] Local FRAIM catalog refresh complete`);
|
|
@@ -814,13 +874,10 @@ class FraimLocalMCPServer {
|
|
|
814
874
|
this.latestConnectSyncWarning = `Local FRAIM catalog refresh failed: ${message}. Discovery will fall back to remote FRAIM tools until sync succeeds.`;
|
|
815
875
|
this.logError(`[req:${requestId}] ${this.latestConnectSyncWarning}`);
|
|
816
876
|
}
|
|
877
|
+
finally {
|
|
878
|
+
this.connectSyncInFlight = null;
|
|
879
|
+
}
|
|
817
880
|
})();
|
|
818
|
-
try {
|
|
819
|
-
await this.connectSyncInFlight;
|
|
820
|
-
}
|
|
821
|
-
finally {
|
|
822
|
-
this.connectSyncInFlight = null;
|
|
823
|
-
}
|
|
824
881
|
this.maybeRefreshOrgCache(String(requestId));
|
|
825
882
|
this.maybeRefreshManagerCache(String(requestId));
|
|
826
883
|
}
|
|
@@ -905,10 +962,63 @@ class FraimLocalMCPServer {
|
|
|
905
962
|
}
|
|
906
963
|
}
|
|
907
964
|
/**
|
|
908
|
-
*
|
|
965
|
+
* Derive the issue worktree directory `prep-issue.sh` would have created for
|
|
966
|
+
* `issueNumber`, without requiring any caller to supply it (issue #1641
|
|
967
|
+
* independent-review follow-up: an opt-in `workingDirectory` parameter that
|
|
968
|
+
* nothing instructs an agent to pass is a recall-dependent fix, not a
|
|
969
|
+
* mechanical one). `prep-issue.sh` names the worktree deterministically —
|
|
970
|
+
* `registry/scripts/prep-issue.sh`: `WORKTREE_DIR="$REPO_NAME - Issue
|
|
971
|
+
* $ISSUE_NUMBER"`, `WORKTREE_PATH="$PARENT_DIR/$WORKTREE_DIR"` — as a sibling
|
|
972
|
+
* of whatever directory the script was run from, using the repository name
|
|
973
|
+
* from `fraim/config.json`, not the actual directory's basename. This
|
|
974
|
+
* reproduces that exact derivation from state this process already has
|
|
975
|
+
* (`this.workspaceRoot`/`findProjectRoot()` for the parent directory,
|
|
976
|
+
* `this.config.repository.name` for the repo name, `issueNumber` from the
|
|
977
|
+
* `seekMentoring` call itself) and returns the candidate only if it actually
|
|
978
|
+
* exists on disk — never inventing an override for a directory that isn't
|
|
979
|
+
* there. Returns `null` when any required input is missing or the derived
|
|
980
|
+
* candidate does not exist, in which case the caller falls back to an
|
|
981
|
+
* explicit override or the cached default.
|
|
982
|
+
*/
|
|
983
|
+
deriveIssueWorktreeDirectory(issueNumber) {
|
|
984
|
+
if (!issueNumber || typeof issueNumber !== 'string' || !issueNumber.trim())
|
|
985
|
+
return null;
|
|
986
|
+
const repoName = this.config?.repository?.name;
|
|
987
|
+
if (!repoName)
|
|
988
|
+
return null;
|
|
989
|
+
const primaryRoot = this.workspaceRoot || this.findProjectRoot();
|
|
990
|
+
if (!primaryRoot)
|
|
991
|
+
return null;
|
|
992
|
+
try {
|
|
993
|
+
const candidate = (0, path_1.join)((0, path_1.dirname)(primaryRoot), `${repoName} - Issue ${issueNumber}`);
|
|
994
|
+
return (0, fs_1.existsSync)(candidate) ? candidate : null;
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Automatically detect repository information from git.
|
|
1002
|
+
*
|
|
1003
|
+
* `overrideProjectDir` (issue #1641): this server process is long-lived and
|
|
1004
|
+
* its notion of "project root" (`this.workspaceRoot`, from the MCP client's
|
|
1005
|
+
* one-time `roots/list` response, or `process.cwd()` at process start) never
|
|
1006
|
+
* changes for the life of the process. A FRAIM job that moves its actual
|
|
1007
|
+
* work into an issue worktree (a sibling directory created by
|
|
1008
|
+
* `prep-issue.sh`) does so entirely within the calling agent's own shell —
|
|
1009
|
+
* this process is never told. Without an override, `detectRepoInfo()` keeps
|
|
1010
|
+
* reporting the branch of wherever it originally resolved (typically the
|
|
1011
|
+
* primary checkout), not the worktree the agent is actually operating in.
|
|
1012
|
+
* When the caller supplies a real directory (the agent's own known working
|
|
1013
|
+
* directory), that directory is used for this call only: the result is not
|
|
1014
|
+
* written to the process-lifetime `this.repoInfo` cache, so it cannot leak
|
|
1015
|
+
* into unrelated callers that rely on the default project's cached identity.
|
|
909
1016
|
*/
|
|
910
|
-
detectRepoInfo() {
|
|
911
|
-
|
|
1017
|
+
detectRepoInfo(overrideProjectDir) {
|
|
1018
|
+
const useOverride = typeof overrideProjectDir === 'string'
|
|
1019
|
+
&& overrideProjectDir.trim() !== ''
|
|
1020
|
+
&& (0, fs_1.existsSync)(overrideProjectDir);
|
|
1021
|
+
if (!useOverride && this.repoInfo) {
|
|
912
1022
|
return this.repoInfo;
|
|
913
1023
|
}
|
|
914
1024
|
// Ensure config is loaded before trying to detect repo info
|
|
@@ -916,7 +1026,7 @@ class FraimLocalMCPServer {
|
|
|
916
1026
|
this.loadConfig();
|
|
917
1027
|
}
|
|
918
1028
|
try {
|
|
919
|
-
const projectDir = this.findProjectRoot() || process.cwd();
|
|
1029
|
+
const projectDir = useOverride ? overrideProjectDir : (this.findProjectRoot() || process.cwd());
|
|
920
1030
|
// Try to get git remote URL
|
|
921
1031
|
let repoUrl = '';
|
|
922
1032
|
try {
|
|
@@ -998,6 +1108,15 @@ class FraimLocalMCPServer {
|
|
|
998
1108
|
if (owner) {
|
|
999
1109
|
repoInfo.owner = owner;
|
|
1000
1110
|
}
|
|
1111
|
+
if (useOverride) {
|
|
1112
|
+
// One-off lookup for a caller-supplied directory: do not clobber the
|
|
1113
|
+
// process-lifetime cache or the default project's usage identifier.
|
|
1114
|
+
const repoLabel = repoInfo.owner
|
|
1115
|
+
? `${repoInfo.owner}/${repoInfo.name}`
|
|
1116
|
+
: repoInfo.projectPath || repoInfo.name;
|
|
1117
|
+
this.log(`Detected repo info for override directory ${projectDir}: ${repoLabel}`);
|
|
1118
|
+
return repoInfo;
|
|
1119
|
+
}
|
|
1001
1120
|
this.repoInfo = repoInfo;
|
|
1002
1121
|
this.usageCollector.setRepoIdentifier(repoInfo.url || null);
|
|
1003
1122
|
const repoLabel = this.repoInfo.owner
|
|
@@ -2373,7 +2492,21 @@ class FraimLocalMCPServer {
|
|
|
2373
2492
|
// for this purpose only when a defaultBranch reference point
|
|
2374
2493
|
// exists; without it the check cannot classify default-vs-feature
|
|
2375
2494
|
// branch, so it must not apply (repo: null).
|
|
2376
|
-
|
|
2495
|
+
//
|
|
2496
|
+
// Issue #1641: this process's default project directory (cached
|
|
2497
|
+
// workspaceRoot / process.cwd()) does not track a worktree the
|
|
2498
|
+
// agent moved its actual work into. Deriving the expected worktree
|
|
2499
|
+
// path from prep-issue.sh's own naming convention (repo name +
|
|
2500
|
+
// issue number, both already known to this process) is mechanical
|
|
2501
|
+
// and requires no agent action, unlike depending on every job/skill
|
|
2502
|
+
// that reaches submission to remember to pass a hint — an explicit
|
|
2503
|
+
// `workingDirectory` remains available as an escape hatch for a
|
|
2504
|
+
// worktree this derivation cannot find (non-standard layout,
|
|
2505
|
+
// renamed directory), and wins when supplied. detectRepoInfo() does
|
|
2506
|
+
// not cache either path, so neither can affect any other lookup.
|
|
2507
|
+
const explicitWorkingDirectory = typeof args.workingDirectory === 'string' ? args.workingDirectory : undefined;
|
|
2508
|
+
const derivedWorkingDirectory = this.deriveIssueWorktreeDirectory(args.issueNumber);
|
|
2509
|
+
const handoffRepoInfo = this.detectRepoInfo(explicitWorkingDirectory || derivedWorkingDirectory || undefined);
|
|
2377
2510
|
const handoffRepoState = handoffRepoInfo?.defaultBranch
|
|
2378
2511
|
? { currentBranch: handoffRepoInfo.branch || '', defaultBranch: handoffRepoInfo.defaultBranch }
|
|
2379
2512
|
: null;
|
|
@@ -366,6 +366,10 @@ Do not use this tool for other repositories or external project issue trackers.`
|
|
|
366
366
|
type: 'object',
|
|
367
367
|
description: 'Structured evidence or data collected (e.g., prospect counts, test results). Submit phases should put review artifacts in evidence.reviewHandoff rather than printing raw JSON to the user. Retrospective phases may put follow-on job recommendations in evidence.nextJobRecommendations (array of { jobId, label, reason?, contextSummary? }, max 3) so the work surface can offer them as next steps.',
|
|
368
368
|
additionalProperties: true
|
|
369
|
+
},
|
|
370
|
+
workingDirectory: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
description: 'Absolute path to the directory you are actually working in right now, when it differs from this session\'s default project directory — most commonly an issue worktree created by prep-issue.sh. Pass this on submit-phase calls carrying evidence.reviewHandoff so the review-action-completeness check reads the branch of your real worktree instead of a directory this long-lived session cached earlier. Optional; omit when working in the default project directory.'
|
|
369
373
|
}
|
|
370
374
|
},
|
|
371
375
|
required: requiredWithSession(['jobName', 'jobId', 'issueNumber', 'currentPhase', 'status'])
|