fraim 2.0.280 → 2.0.283
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/README.md +467 -467
- package/bin/fraim.js +12 -12
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +266 -182
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -3
- package/dist/src/cli/setup/ide-invocation-surfaces.js +64 -64
- package/dist/src/cli/utils/agent-adapters.js +61 -61
- package/dist/src/core/ai-mentor.js +35 -0
- package/dist/src/core/handoff-contracts.js +73 -46
- package/dist/src/core/resolve-phase-edge.js +33 -0
- package/dist/src/core/utils/stub-generator.js +53 -53
- package/dist/src/first-run/server.js +5 -1
- package/dist/src/first-run/session-service.js +48 -12
- package/dist/src/fraim/issues.js +4 -4
- package/dist/src/local-mcp-server/stdio-server.js +43 -4
- package/dist/src/mcp/tool-schemas.js +40 -40
- package/dist/src/middleware/telemetry.js +21 -21
- package/dist/src/services/email-service.js +623 -623
- package/dist/src/services/installer-service.js +22 -22
- package/index.js +83 -83
- package/package.json +59 -59
- package/public/first-run/error-frame.js +100 -100
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +747 -742
- package/public/first-run/styles.css +929 -929
- package/dist/src/cli/commands/learning-usage.js +0 -412
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
- package/dist/src/services/email-service-clean.js +0 -782
|
@@ -50,71 +50,71 @@ function generateJobStub(jobName, _jobPath, intent, outcome, steps, displayName)
|
|
|
50
50
|
const metadata = displayName
|
|
51
51
|
? `---\n${JSON.stringify({ name: jobName, displayName }, null, 2)}\n---\n`
|
|
52
52
|
: '';
|
|
53
|
-
return `${STUB_MARKER}
|
|
54
|
-
${metadata}
|
|
55
|
-
# FRAIM Job: ${jobName}
|
|
56
|
-
|
|
57
|
-
## Intent
|
|
58
|
-
${intent}
|
|
59
|
-
|
|
60
|
-
## Outcome
|
|
61
|
-
${outcome}
|
|
62
|
-
|
|
63
|
-
## Steps
|
|
64
|
-
${steps}
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
> [!IMPORTANT]
|
|
69
|
-
> **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
|
|
70
|
-
> This stub is for discoverability only. The actual job has multiple phases with
|
|
71
|
-
> detailed steps, validation criteria, and required skills.
|
|
72
|
-
>
|
|
73
|
-
> To get full phase-by-phase instructions, call:
|
|
74
|
-
> \`get_fraim_job({ job: "${jobName}" })\`
|
|
75
|
-
>
|
|
76
|
-
> Then follow all phases using \`seekMentoring\` at each phase transition.
|
|
53
|
+
return `${STUB_MARKER}
|
|
54
|
+
${metadata}
|
|
55
|
+
# FRAIM Job: ${jobName}
|
|
56
|
+
|
|
57
|
+
## Intent
|
|
58
|
+
${intent}
|
|
59
|
+
|
|
60
|
+
## Outcome
|
|
61
|
+
${outcome}
|
|
62
|
+
|
|
63
|
+
## Steps
|
|
64
|
+
${steps}
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
> [!IMPORTANT]
|
|
69
|
+
> **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
|
|
70
|
+
> This stub is for discoverability only. The actual job has multiple phases with
|
|
71
|
+
> detailed steps, validation criteria, and required skills.
|
|
72
|
+
>
|
|
73
|
+
> To get full phase-by-phase instructions, call:
|
|
74
|
+
> \`get_fraim_job({ job: "${jobName}" })\`
|
|
75
|
+
>
|
|
76
|
+
> Then follow all phases using \`seekMentoring\` at each phase transition.
|
|
77
77
|
`;
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Generates a lightweight markdown stub for a skill.
|
|
81
81
|
*/
|
|
82
82
|
function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
|
|
83
|
-
return `${STUB_MARKER}
|
|
84
|
-
# FRAIM Skill: ${skillName}
|
|
85
|
-
|
|
86
|
-
## Skill Input
|
|
87
|
-
${skillInput}
|
|
88
|
-
|
|
89
|
-
## Skill Output
|
|
90
|
-
${skillOutput}
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
> [!IMPORTANT]
|
|
95
|
-
> **For AI Agents:** This is a discoverability stub for the skill.
|
|
96
|
-
> All execution details must be fetched from MCP before use.
|
|
97
|
-
> To retrieve the complete skill instructions, call:
|
|
98
|
-
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
83
|
+
return `${STUB_MARKER}
|
|
84
|
+
# FRAIM Skill: ${skillName}
|
|
85
|
+
|
|
86
|
+
## Skill Input
|
|
87
|
+
${skillInput}
|
|
88
|
+
|
|
89
|
+
## Skill Output
|
|
90
|
+
${skillOutput}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
> [!IMPORTANT]
|
|
95
|
+
> **For AI Agents:** This is a discoverability stub for the skill.
|
|
96
|
+
> All execution details must be fetched from MCP before use.
|
|
97
|
+
> To retrieve the complete skill instructions, call:
|
|
98
|
+
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
99
99
|
`;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Generates a lightweight markdown stub for a rule.
|
|
103
103
|
*/
|
|
104
104
|
function generateRuleStub(ruleName, rulePath, intent) {
|
|
105
|
-
return `${STUB_MARKER}
|
|
106
|
-
# FRAIM Rule: ${ruleName}
|
|
107
|
-
|
|
108
|
-
## Intent
|
|
109
|
-
${intent}
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
> [!IMPORTANT]
|
|
114
|
-
> **For AI Agents:** This is a discoverability stub for the rule.
|
|
115
|
-
> All rule details must be fetched from MCP before use.
|
|
116
|
-
> To retrieve the complete rule instructions, call:
|
|
117
|
-
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
105
|
+
return `${STUB_MARKER}
|
|
106
|
+
# FRAIM Rule: ${ruleName}
|
|
107
|
+
|
|
108
|
+
## Intent
|
|
109
|
+
${intent}
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
> [!IMPORTANT]
|
|
114
|
+
> **For AI Agents:** This is a discoverability stub for the rule.
|
|
115
|
+
> All rule details must be fetched from MCP before use.
|
|
116
|
+
> To retrieve the complete rule instructions, call:
|
|
117
|
+
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
118
118
|
`;
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
@@ -121,6 +121,7 @@ class FirstRunServer {
|
|
|
121
121
|
constructor(options) {
|
|
122
122
|
this.app = (0, express_1.default)();
|
|
123
123
|
this.sessionService = options.sessionService;
|
|
124
|
+
this.onOpenHub = options.onOpenHub;
|
|
124
125
|
this.finishPromise = new Promise((resolve) => {
|
|
125
126
|
this.finishResolver = resolve;
|
|
126
127
|
});
|
|
@@ -299,13 +300,16 @@ class FirstRunServer {
|
|
|
299
300
|
// a durable launcher binary that survives independently.
|
|
300
301
|
this.app.post('/api/first-run/open-hub', async (_req, res) => {
|
|
301
302
|
try {
|
|
302
|
-
|
|
303
|
+
let result = await this.sessionService.openHub();
|
|
303
304
|
// Write the next-prompt artifact as a side effect of opening the
|
|
304
305
|
// Hub so the client doesn't need a separate /finish call. We
|
|
305
306
|
// intentionally do NOT resolve the finishPromise - see /finish
|
|
306
307
|
// handler comment above.
|
|
307
308
|
if (result.ok) {
|
|
308
309
|
this.sessionService.finish();
|
|
310
|
+
if (this.onOpenHub) {
|
|
311
|
+
result = { ...result, ...(await this.onOpenHub()) };
|
|
312
|
+
}
|
|
309
313
|
}
|
|
310
314
|
return res.json(result);
|
|
311
315
|
}
|
|
@@ -48,6 +48,7 @@ const ide_detector_1 = require("../cli/setup/ide-detector");
|
|
|
48
48
|
const ide_global_integration_1 = require("../cli/setup/ide-global-integration");
|
|
49
49
|
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
50
50
|
const setup_1 = require("../cli/commands/setup");
|
|
51
|
+
const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
|
|
51
52
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
52
53
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
53
54
|
const managed_agent_install_1 = require("../cli/utils/managed-agent-install");
|
|
@@ -118,13 +119,18 @@ function buildPersistShellPathWindowsCommand(bins) {
|
|
|
118
119
|
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
119
120
|
].join('; ');
|
|
120
121
|
}
|
|
121
|
-
function persistShellPath() {
|
|
122
|
+
function persistShellPath(includePackagedFraimBin = false) {
|
|
122
123
|
const marker = '# FRAIM managed binaries';
|
|
123
124
|
const legacyExportLine = 'export PATH="$HOME/.fraim/node/bin:$PATH"';
|
|
124
|
-
const exportLine =
|
|
125
|
+
const exportLine = includePackagedFraimBin
|
|
126
|
+
? 'export PATH="$PATH:$HOME/.fraim/bin:$HOME/.fraim/node/bin"'
|
|
127
|
+
: 'export PATH="$PATH:$HOME/.fraim/node/bin"';
|
|
125
128
|
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
126
129
|
if (process.platform === 'win32') {
|
|
127
|
-
const
|
|
130
|
+
const bins = includePackagedFraimBin
|
|
131
|
+
? [path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'bin'), ...(0, managed_agent_paths_1.getManagedAgentBinDirs)()]
|
|
132
|
+
: (0, managed_agent_paths_1.getManagedAgentBinDirs)();
|
|
133
|
+
const psCmd = buildPersistShellPathWindowsCommand(bins);
|
|
128
134
|
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
@@ -146,8 +152,12 @@ function persistShellPath() {
|
|
|
146
152
|
fs_1.default.writeFileSync(profile, content.replace(legacyExportLine, exportLine), 'utf8');
|
|
147
153
|
continue;
|
|
148
154
|
}
|
|
149
|
-
if (content.includes(marker))
|
|
155
|
+
if (content.includes(marker)) {
|
|
156
|
+
if (includePackagedFraimBin && !content.includes('$HOME/.fraim/bin')) {
|
|
157
|
+
fs_1.default.writeFileSync(profile, content.replace(exportLine.replace(':$HOME/.fraim/bin', ''), exportLine), 'utf8');
|
|
158
|
+
}
|
|
150
159
|
continue;
|
|
160
|
+
}
|
|
151
161
|
fs_1.default.appendFileSync(profile, stanza, 'utf8');
|
|
152
162
|
}
|
|
153
163
|
catch {
|
|
@@ -246,6 +256,7 @@ class FirstRunSessionService {
|
|
|
246
256
|
// wizard prompts the user to paste their key (see setKey / needsKey).
|
|
247
257
|
this.key = options.key || '';
|
|
248
258
|
this.headless = options.headless === true;
|
|
259
|
+
this.embeddedDesktop = options.embeddedDesktop === true;
|
|
249
260
|
this.fakeMode = getFakeStateMode();
|
|
250
261
|
this.fakeStderr =
|
|
251
262
|
process.env.FRAIM_FIRST_RUN_FAKE_STDERR ??
|
|
@@ -310,10 +321,10 @@ class FirstRunSessionService {
|
|
|
310
321
|
}
|
|
311
322
|
// Real detection — populate row statuses from `command -v` style probes.
|
|
312
323
|
const nodeRow = this.getRow('node');
|
|
313
|
-
const nodeVer = commandVersion('node');
|
|
324
|
+
const nodeVer = this.embeddedDesktop ? process.version : commandVersion('node');
|
|
314
325
|
if (nodeVer) {
|
|
315
326
|
nodeRow.status = 'ok';
|
|
316
|
-
nodeRow.verb = `${nodeVer} detected`;
|
|
327
|
+
nodeRow.verb = this.embeddedDesktop ? `${nodeVer} bundled with FRAIM` : `${nodeVer} detected`;
|
|
317
328
|
}
|
|
318
329
|
else {
|
|
319
330
|
nodeRow.status = 'pending';
|
|
@@ -330,7 +341,7 @@ class FirstRunSessionService {
|
|
|
330
341
|
gitRow.verb = 'optional — only needed for code delivery workflows';
|
|
331
342
|
}
|
|
332
343
|
const fraimRow = this.getRow('fraim');
|
|
333
|
-
if (commandVersion('npx') !== null) {
|
|
344
|
+
if (this.embeddedDesktop || commandVersion('npx') !== null) {
|
|
334
345
|
// Only mark ok if setup has previously completed (config.json is written after autoConfigureMCP).
|
|
335
346
|
// Without this, the wizard skips runFraimRow entirely and autoConfigureMCP never runs for IDEs.
|
|
336
347
|
const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
|
|
@@ -342,7 +353,7 @@ class FirstRunSessionService {
|
|
|
342
353
|
fraimRow.status = 'pending';
|
|
343
354
|
fraimRow.verb = "we'll configure your IDEs";
|
|
344
355
|
}
|
|
345
|
-
persistShellPath();
|
|
356
|
+
persistShellPath(this.embeddedDesktop);
|
|
346
357
|
}
|
|
347
358
|
else {
|
|
348
359
|
fraimRow.status = 'pending';
|
|
@@ -570,6 +581,12 @@ class FirstRunSessionService {
|
|
|
570
581
|
}
|
|
571
582
|
async runNodeRow() {
|
|
572
583
|
const row = this.getRow('node');
|
|
584
|
+
if (this.embeddedDesktop) {
|
|
585
|
+
row.status = 'ok';
|
|
586
|
+
row.verb = `${process.version} bundled with FRAIM`;
|
|
587
|
+
this.persist();
|
|
588
|
+
return this.respond(`FRAIM includes its own runtime (${process.version}).`, true);
|
|
589
|
+
}
|
|
573
590
|
// Node is bootstrapped by the install template before the wizard starts.
|
|
574
591
|
// If node is missing here something is severely wrong; fall through to error.
|
|
575
592
|
const ver = commandVersion('node');
|
|
@@ -631,7 +648,10 @@ class FirstRunSessionService {
|
|
|
631
648
|
return this.respond('FRAIM configured.', true);
|
|
632
649
|
}
|
|
633
650
|
try {
|
|
634
|
-
if (
|
|
651
|
+
if (this.embeddedDesktop) {
|
|
652
|
+
(0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
653
|
+
}
|
|
654
|
+
else if (!forceConfigure && !commandVersion('fraim')) {
|
|
635
655
|
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
636
656
|
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
637
657
|
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
@@ -645,7 +665,7 @@ class FirstRunSessionService {
|
|
|
645
665
|
this.persist();
|
|
646
666
|
await runProcess('npm', ['install', '-g', 'fraim@latest'], { npm_config_prefix: prefix });
|
|
647
667
|
}
|
|
648
|
-
persistShellPath();
|
|
668
|
+
persistShellPath(this.embeddedDesktop);
|
|
649
669
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
650
670
|
if (detectedIDEs.length > 0) {
|
|
651
671
|
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, detectedIDEs.map((ide) => ide.name));
|
|
@@ -660,7 +680,9 @@ class FirstRunSessionService {
|
|
|
660
680
|
await installSlashCommands();
|
|
661
681
|
await installGlobalRules();
|
|
662
682
|
row.status = 'ok';
|
|
663
|
-
row.verb =
|
|
683
|
+
row.verb = this.embeddedDesktop
|
|
684
|
+
? 'Ready — bundled with FRAIM Hub.'
|
|
685
|
+
: 'Ready — open a new terminal before running fraim commands.';
|
|
664
686
|
delete row.streamOutput;
|
|
665
687
|
const surfaces = this.getConfiguredAgentSurfaces();
|
|
666
688
|
this.state.setupResult = {
|
|
@@ -671,7 +693,9 @@ class FirstRunSessionService {
|
|
|
671
693
|
completedAt: new Date().toISOString(),
|
|
672
694
|
};
|
|
673
695
|
this.persist();
|
|
674
|
-
return this.respond(
|
|
696
|
+
return this.respond(this.embeddedDesktop
|
|
697
|
+
? 'FRAIM and FRAIM Hub are ready in this installed application.'
|
|
698
|
+
: 'FRAIM is ready. Open a new terminal window so your PATH update takes effect before running fraim commands.', true);
|
|
675
699
|
}
|
|
676
700
|
catch (error) {
|
|
677
701
|
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -931,6 +955,18 @@ class FirstRunSessionService {
|
|
|
931
955
|
message: "No AI agent (Claude Code or Codex) found on this machine. Let's install one first.",
|
|
932
956
|
};
|
|
933
957
|
}
|
|
958
|
+
if (this.embeddedDesktop) {
|
|
959
|
+
(0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
960
|
+
if (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)())) {
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
needsAgentSetup: true,
|
|
964
|
+
message: 'FRAIM CLI was not installed into the packaged desktop profile. Re-run FRAIM setup before opening Hub.',
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
appendInstallLog('hub-opened embedded-desktop');
|
|
968
|
+
return { ok: true, desktop: true, message: 'Opening FRAIM Hub in this app.' };
|
|
969
|
+
}
|
|
934
970
|
try {
|
|
935
971
|
const npxBinary = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
936
972
|
// Issue #866 R1/R2: launch the FRAIM Hub desktop (Electron) app, not a
|
package/dist/src/fraim/issues.js
CHANGED
|
@@ -45,8 +45,8 @@ async function fileFraimIssue(params) {
|
|
|
45
45
|
if (!token) {
|
|
46
46
|
return {
|
|
47
47
|
success: false,
|
|
48
|
-
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
49
|
-
|
|
48
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
49
|
+
|
|
50
50
|
Please set GITHUB_TOKEN environment variable to file issues in ${owner}/${repo}.`
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -102,8 +102,8 @@ async function listFraimIssuesByReporter(params) {
|
|
|
102
102
|
if (!token) {
|
|
103
103
|
return {
|
|
104
104
|
success: false,
|
|
105
|
-
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
106
|
-
|
|
105
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
106
|
+
|
|
107
107
|
Please set GITHUB_TOKEN environment variable to query issues in ${FRAIM_REPO_OWNER}/${FRAIM_REPO_NAME}.`
|
|
108
108
|
};
|
|
109
109
|
}
|
|
@@ -2209,19 +2209,58 @@ class FraimLocalMCPServer {
|
|
|
2209
2209
|
// nextJobRecommendations, and delegationLedger evidence fields.
|
|
2210
2210
|
// The Hub cannot render review bars, next-job chips, or the
|
|
2211
2211
|
// delegation board when these fields are absent.
|
|
2212
|
+
const handoffPhaseMap = await mentor.getJobPhaseMap(args.jobName);
|
|
2212
2213
|
const handoffErrors = (0, handoff_contracts_1.validateHandoffContracts)({
|
|
2213
2214
|
jobName: args.jobName,
|
|
2214
2215
|
currentPhase: args.currentPhase,
|
|
2215
2216
|
status: args.status,
|
|
2216
2217
|
evidence: args.evidence,
|
|
2218
|
+
findings: args.findings,
|
|
2219
|
+
phases: handoffPhaseMap,
|
|
2217
2220
|
});
|
|
2218
2221
|
if (handoffErrors.length > 0) {
|
|
2219
2222
|
const missingField = handoffErrors[0].includes('reviewHandoff') ? 'reviewHandoff'
|
|
2220
2223
|
: handoffErrors[0].includes('nextJobRecommendations') ? 'nextJobRecommendations'
|
|
2221
|
-
: '
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2224
|
+
: handoffErrors[0].includes('evidence.approved') ? 'approved'
|
|
2225
|
+
: 'delegationLedger';
|
|
2226
|
+
// Content-scoped applicability (issue #1318 follow-up on #1276): not every
|
|
2227
|
+
// job's submission phase promises evidence.reviewHandoff. Jobs built on
|
|
2228
|
+
// rich-review-artifact-contract.md, author-docx.md, or reporting-standards.md
|
|
2229
|
+
// (analysis reports, DOCX deliverables, operational setup jobs) use a
|
|
2230
|
+
// different, legitimate completion contract and never instruct the field.
|
|
2231
|
+
// Enforcing the gate on them would reject a call their own phase text never
|
|
2232
|
+
// told the agent to satisfy — a genuine stuck-agent risk, not a hypothetical
|
|
2233
|
+
// one (grep-verified against the registry: ~16 of 183 reviewable jobs). This
|
|
2234
|
+
// is not a violation at all when the phase never promised the field, so it is
|
|
2235
|
+
// skipped entirely rather than logged. Escape hatch for the promised case:
|
|
2236
|
+
// FRAIM_HANDOFF_ENFORCEMENT_MODE=off disables rejection (still logs);
|
|
2237
|
+
// =warn logs without rejecting; default is unconditional rejection, matching
|
|
2238
|
+
// how the other three handoff contracts already behave.
|
|
2239
|
+
const isReviewHandoffField = missingField === 'reviewHandoff';
|
|
2240
|
+
const reviewHandoffApplies = !isReviewHandoffField
|
|
2241
|
+
|| await mentor.phasePromisesReviewHandoff(args.jobName, args.currentPhase);
|
|
2242
|
+
// Escape hatch scoped to reviewHandoff only: the other three contracts
|
|
2243
|
+
// (nextJobRecommendations, delegationLedger, approved) have shipped
|
|
2244
|
+
// unconditional, uncapped enforcement since #916/#1157 with no reported
|
|
2245
|
+
// issue, so they keep that behavior — FRAIM_HANDOFF_ENFORCEMENT_MODE never
|
|
2246
|
+
// relaxes them.
|
|
2247
|
+
const enforcementMode = isReviewHandoffField
|
|
2248
|
+
? (process.env.FRAIM_HANDOFF_ENFORCEMENT_MODE || 'enforce')
|
|
2249
|
+
: 'enforce';
|
|
2250
|
+
if (!reviewHandoffApplies) {
|
|
2251
|
+
// Not gated: this job's submission phase never promised reviewHandoff.
|
|
2252
|
+
}
|
|
2253
|
+
else if (enforcementMode === 'off') {
|
|
2254
|
+
this.log(`⚠️ [reviewHandoff enforcement: off] seekMentoring for ${args.jobName}:${args.currentPhase} would be rejected: ${handoffErrors.join('; ')}`);
|
|
2255
|
+
}
|
|
2256
|
+
else if (enforcementMode === 'warn') {
|
|
2257
|
+
this.log(`⚠️ [reviewHandoff enforcement: warn-only] seekMentoring for ${args.jobName}:${args.currentPhase} would be rejected: ${handoffErrors.join('; ')}`);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
this.log(`⚠️ Handoff contract enforcement rejected seekMentoring for ${args.jobName}:${args.currentPhase}: ${handoffErrors.join('; ')}`);
|
|
2261
|
+
const rejection = (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors);
|
|
2262
|
+
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
|
|
2263
|
+
}
|
|
2225
2264
|
}
|
|
2226
2265
|
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
|
|
2227
2266
|
}
|
|
@@ -42,19 +42,19 @@ const getToolDefinitions = (options = {}) => {
|
|
|
42
42
|
? ['sessionId', ...fields]
|
|
43
43
|
: fields;
|
|
44
44
|
const fraimConnectDescription = isLocalProxySurface
|
|
45
|
-
? `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
46
|
-
|
|
47
|
-
Example:
|
|
48
|
-
{
|
|
49
|
-
"agent": {"name": "Claude", "model": "claude-3.5-sonnet"}
|
|
45
|
+
? `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
{
|
|
49
|
+
"agent": {"name": "Claude", "model": "claude-3.5-sonnet"}
|
|
50
50
|
}`
|
|
51
|
-
: `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
52
|
-
|
|
53
|
-
Hosted marketplace clients may call this with only agent information. If machine or repository context is omitted, FRAIM creates a hosted marketplace session context automatically.
|
|
54
|
-
|
|
55
|
-
Example:
|
|
56
|
-
{
|
|
57
|
-
"agent": {"name": "ChatGPT", "model": "gpt-5"}
|
|
51
|
+
: `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
52
|
+
|
|
53
|
+
Hosted marketplace clients may call this with only agent information. If machine or repository context is omitted, FRAIM creates a hosted marketplace session context automatically.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
{
|
|
57
|
+
"agent": {"name": "ChatGPT", "model": "gpt-5"}
|
|
58
58
|
}`;
|
|
59
59
|
return [
|
|
60
60
|
{
|
|
@@ -198,12 +198,12 @@ Example:
|
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
name: 'get_fraim_file',
|
|
201
|
-
description: `Get a specific skill, rule, or reference file from the FRAIM registry by path.
|
|
202
|
-
|
|
203
|
-
For running FRAIM jobs, use get_fraim_job instead — do NOT call get_fraim_file for job execution.
|
|
204
|
-
|
|
205
|
-
Examples:
|
|
206
|
-
- get_fraim_file({ path: "skills/communication/active-listening.md" })
|
|
201
|
+
description: `Get a specific skill, rule, or reference file from the FRAIM registry by path.
|
|
202
|
+
|
|
203
|
+
For running FRAIM jobs, use get_fraim_job instead — do NOT call get_fraim_file for job execution.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
- get_fraim_file({ path: "skills/communication/active-listening.md" })
|
|
207
207
|
- get_fraim_file({ path: "rules/local-development.md" })`,
|
|
208
208
|
annotations: readOnlyClosedWorldAnnotations,
|
|
209
209
|
inputSchema: {
|
|
@@ -224,13 +224,13 @@ Examples:
|
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
name: 'get_fraim_job',
|
|
227
|
-
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
|
-
Do NOT use get_fraim_file to load job content. Always use get_fraim_job for job execution.
|
|
230
|
-
|
|
231
|
-
Examples:
|
|
232
|
-
- get_fraim_job({ job: "feature-specification" })
|
|
233
|
-
- get_fraim_job({ job: "technical-design" })
|
|
227
|
+
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
|
+
Do NOT use get_fraim_file to load job content. Always use get_fraim_job for job execution.
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
- get_fraim_job({ job: "feature-specification" })
|
|
233
|
+
- get_fraim_job({ job: "technical-design" })
|
|
234
234
|
- get_fraim_job({ job: "feature-implementation" })`,
|
|
235
235
|
annotations: readOnlyClosedWorldAnnotations,
|
|
236
236
|
inputSchema: {
|
|
@@ -263,11 +263,11 @@ Examples:
|
|
|
263
263
|
},
|
|
264
264
|
{
|
|
265
265
|
name: 'file_fraim_github_issue',
|
|
266
|
-
description: `Create a GitHub issue in the FRAIM repository.
|
|
267
|
-
|
|
268
|
-
Use this tool when you need to report a bug, request a feature from FRAIM. Do not use this tool to file issues in other repositories.
|
|
269
|
-
Supports dry-run mode to preview the operation.
|
|
270
|
-
|
|
266
|
+
description: `Create a GitHub issue in the FRAIM repository.
|
|
267
|
+
|
|
268
|
+
Use this tool when you need to report a bug, request a feature from FRAIM. Do not use this tool to file issues in other repositories.
|
|
269
|
+
Supports dry-run mode to preview the operation.
|
|
270
|
+
|
|
271
271
|
This tool accepts text only. If visual evidence is needed, upload images to a stable shared HTTPS URL first and include those URLs in the issue body as markdown images or plain links. Do not include local file paths or base64-encoded image data in the issue body.`,
|
|
272
272
|
annotations: writeOpenWorldAnnotations,
|
|
273
273
|
inputSchema: {
|
|
@@ -297,16 +297,16 @@ This tool accepts text only. If visual evidence is needed, upload images to a st
|
|
|
297
297
|
},
|
|
298
298
|
{
|
|
299
299
|
name: 'list_my_fraim_github_issues',
|
|
300
|
-
description: `List issues the current FRAIM user filed into the FRAIM GitHub repository.
|
|
301
|
-
|
|
302
|
-
This tool is the read-side companion to file_fraim_github_issue. Use it when you need a simple list of issues filed by the current FRAIM user into the FRAIM repository.
|
|
303
|
-
|
|
304
|
-
Returns only:
|
|
305
|
-
- issue number
|
|
306
|
-
- title
|
|
307
|
-
- status
|
|
308
|
-
- created date
|
|
309
|
-
|
|
300
|
+
description: `List issues the current FRAIM user filed into the FRAIM GitHub repository.
|
|
301
|
+
|
|
302
|
+
This tool is the read-side companion to file_fraim_github_issue. Use it when you need a simple list of issues filed by the current FRAIM user into the FRAIM repository.
|
|
303
|
+
|
|
304
|
+
Returns only:
|
|
305
|
+
- issue number
|
|
306
|
+
- title
|
|
307
|
+
- status
|
|
308
|
+
- created date
|
|
309
|
+
|
|
310
310
|
Do not use this tool for other repositories or external project issue trackers.`,
|
|
311
311
|
annotations: readOnlyOpenWorldAnnotations,
|
|
312
312
|
inputSchema: {
|
|
@@ -84,9 +84,9 @@ class TelemetryMiddleware {
|
|
|
84
84
|
jsonrpc: '2.0',
|
|
85
85
|
error: {
|
|
86
86
|
code: -32600,
|
|
87
|
-
message: `Missing Session ID. REQUIRED ACTION:
|
|
88
|
-
1. Call 'fraim_connect' first
|
|
89
|
-
2. Capture the returned sessionId
|
|
87
|
+
message: `Missing Session ID. REQUIRED ACTION:
|
|
88
|
+
1. Call 'fraim_connect' first
|
|
89
|
+
2. Capture the returned sessionId
|
|
90
90
|
3. Include that sessionId on every subsequent FRAIM tool call (params.sessionId or params.arguments.sessionId).`
|
|
91
91
|
},
|
|
92
92
|
id: req.body?.id || null
|
|
@@ -97,24 +97,24 @@ class TelemetryMiddleware {
|
|
|
97
97
|
if (!isActive) {
|
|
98
98
|
const sessionStartGuidance = isProxiedAgentCall
|
|
99
99
|
? `Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first, then include the returned sessionId on every subsequent FRAIM tool call.`
|
|
100
|
-
: `Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first with these parameters:
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
"agent": {
|
|
104
|
-
"name": "YourAgentName",
|
|
105
|
-
"model": "your-model-name"
|
|
106
|
-
},
|
|
107
|
-
"machine": {
|
|
108
|
-
"hostname": "use os.hostname()",
|
|
109
|
-
"platform": "use process.platform"
|
|
110
|
-
},
|
|
111
|
-
"repo": {
|
|
112
|
-
"url": "git remote URL",
|
|
113
|
-
"owner": "repo owner",
|
|
114
|
-
"name": "repo name"
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
100
|
+
: `Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first with these parameters:
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
"agent": {
|
|
104
|
+
"name": "YourAgentName",
|
|
105
|
+
"model": "your-model-name"
|
|
106
|
+
},
|
|
107
|
+
"machine": {
|
|
108
|
+
"hostname": "use os.hostname()",
|
|
109
|
+
"platform": "use process.platform"
|
|
110
|
+
},
|
|
111
|
+
"repo": {
|
|
112
|
+
"url": "git remote URL",
|
|
113
|
+
"owner": "repo owner",
|
|
114
|
+
"name": "repo name"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
118
|
After successful fraim_connect, all other FRAIM tools will work. This is required for telemetry and session management.`;
|
|
119
119
|
return res.status(400).json({
|
|
120
120
|
jsonrpc: '2.0',
|