fraim 2.0.154 → 2.0.160
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 +1 -1
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/desktop-main.js +225 -50
- package/dist/src/ai-hub/hosts.js +135 -8
- package/dist/src/ai-hub/manager-turns.js +38 -0
- package/dist/src/ai-hub/office-sideload.js +138 -0
- package/dist/src/ai-hub/openclaw-bridge.js +239 -0
- package/dist/src/ai-hub/server.js +479 -48
- package/dist/src/ai-hub/word-sideload.js +95 -0
- package/dist/src/cli/commands/add-ide.js +9 -0
- package/dist/src/cli/commands/init-project.js +46 -34
- package/dist/src/cli/commands/login.js +1 -2
- package/dist/src/cli/commands/setup.js +0 -2
- package/dist/src/cli/commands/sync.js +41 -11
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
- package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +2 -2
- package/dist/src/cli/utils/fraim-gitignore.js +11 -0
- package/dist/src/cli/utils/github-workflow-sync.js +231 -0
- package/dist/src/cli/utils/managed-agent-paths.js +1 -1
- package/dist/src/cli/utils/project-bootstrap.js +6 -3
- package/dist/src/cli/utils/remote-sync.js +1 -1
- package/dist/src/core/ai-mentor.js +46 -37
- package/dist/src/core/config-loader.js +69 -2
- package/dist/src/core/fraim-config-schema.generated.js +267 -6
- package/dist/src/core/types.js +0 -1
- package/dist/src/core/utils/fraim-labels.js +182 -0
- package/dist/src/core/utils/git-utils.js +22 -1
- package/dist/src/core/utils/project-fraim-paths.js +58 -0
- package/dist/src/first-run/session-service.js +3 -3
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +77 -52
- package/dist/src/local-mcp-server/stdio-server.js +212 -13
- package/package.json +6 -2
- package/public/ai-hub/index.html +289 -229
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +235 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/script.js +1155 -586
- package/public/ai-hub/styles.css +1226 -722
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +667 -667
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getFraimMcpLatestLauncherPath = getFraimMcpLatestLauncherPath;
|
|
7
|
+
exports.ensureFraimMcpLatestLauncher = ensureFraimMcpLatestLauncher;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const LAUNCHER_VERSION = 1;
|
|
12
|
+
const launcherSource = `#!/usr/bin/env node
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const LAUNCHER_VERSION = ${LAUNCHER_VERSION};
|
|
19
|
+
const USER_DIR = process.env.FRAIM_USER_DIR || path.join(os.homedir(), '.fraim');
|
|
20
|
+
const STATE_DIR = path.join(USER_DIR, 'mcp-launcher');
|
|
21
|
+
const STATE_PATH = path.join(STATE_DIR, 'latest.json');
|
|
22
|
+
const CACHE_ROOT = path.join(USER_DIR, 'npm-cache', 'mcp');
|
|
23
|
+
|
|
24
|
+
const commandName = (name) => process.platform === 'win32' ? name + '.cmd' : name;
|
|
25
|
+
const npmCommand = commandName('npm');
|
|
26
|
+
const npxCommand = commandName('npx');
|
|
27
|
+
const cliArgs = process.argv.slice(2).length > 0 ? process.argv.slice(2) : ['mcp'];
|
|
28
|
+
|
|
29
|
+
function quoteCmdArg(arg) {
|
|
30
|
+
const value = String(arg);
|
|
31
|
+
return /[\\s"&|<>^]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runCommand(command, args, options) {
|
|
35
|
+
if (process.platform !== 'win32') {
|
|
36
|
+
return spawnSync(command, args, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', [command, ...args.map(quoteCmdArg)].join(' ')], options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureDir(dir) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readState() {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeState(state) {
|
|
55
|
+
ensureDir(STATE_DIR);
|
|
56
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify({ ...state, launcherVersion: LAUNCHER_VERSION, updatedAt: new Date().toISOString() }, null, 2));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveLatest(packageName) {
|
|
60
|
+
const result = runCommand(npmCommand, ['view', packageName, 'version', '--silent'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
env: process.env
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const version = String(result.stdout || '').trim();
|
|
70
|
+
return /^\\d+\\.\\d+\\.\\d+/.test(version) ? version : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolvePackage() {
|
|
74
|
+
const fraimVersion = resolveLatest('fraim');
|
|
75
|
+
if (fraimVersion) {
|
|
76
|
+
const state = { packageName: 'fraim', binName: 'fraim', version: fraimVersion, fromCache: false };
|
|
77
|
+
writeState(state);
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cached = readState();
|
|
82
|
+
if (cached && cached.packageName === 'fraim' && cached.binName === 'fraim' && cached.version) {
|
|
83
|
+
console.error('[fraim-mcp-launcher] Could not resolve npm latest; using cached fraim@' + cached.version + '.');
|
|
84
|
+
return { packageName: 'fraim', binName: 'fraim', version: cached.version, fromCache: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.error('[fraim-mcp-launcher] Could not resolve fraim latest from npm and no cached fraim version exists.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runPackage(plan, cacheDir) {
|
|
92
|
+
ensureDir(cacheDir);
|
|
93
|
+
const env = { ...process.env, npm_config_cache: cacheDir };
|
|
94
|
+
const result = runCommand(npxCommand, ['--yes', '--package', plan.packageName + '@' + plan.version, plan.binName, ...cliArgs], {
|
|
95
|
+
stdio: 'inherit',
|
|
96
|
+
env
|
|
97
|
+
});
|
|
98
|
+
return typeof result.status === 'number' ? result.status : 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const plan = resolvePackage();
|
|
102
|
+
const versionCacheDir = path.join(CACHE_ROOT, plan.packageName + '-' + plan.version.replace(/[^a-zA-Z0-9._-]/g, '_'));
|
|
103
|
+
let status = runPackage(plan, versionCacheDir);
|
|
104
|
+
|
|
105
|
+
if (status !== 0 && process.platform === 'win32') {
|
|
106
|
+
const retryCache = path.join(CACHE_ROOT, 'retry-' + Date.now() + '-' + process.pid);
|
|
107
|
+
console.error('[fraim-mcp-launcher] Package execution failed; retrying once with a fresh npm cache.');
|
|
108
|
+
status = runPackage(plan, retryCache);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.exit(status);
|
|
112
|
+
`;
|
|
113
|
+
function getFraimMcpLatestLauncherPath() {
|
|
114
|
+
return path_1.default.join(process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim'), 'bin', 'fraim-mcp-latest.js');
|
|
115
|
+
}
|
|
116
|
+
function ensureFraimMcpLatestLauncher() {
|
|
117
|
+
const launcherPath = getFraimMcpLatestLauncherPath();
|
|
118
|
+
const launcherDir = path_1.default.dirname(launcherPath);
|
|
119
|
+
fs_1.default.mkdirSync(launcherDir, { recursive: true });
|
|
120
|
+
if (!fs_1.default.existsSync(launcherPath) || fs_1.default.readFileSync(launcherPath, 'utf8') !== launcherSource) {
|
|
121
|
+
fs_1.default.writeFileSync(launcherPath, launcherSource, 'utf8');
|
|
122
|
+
if (process.platform !== 'win32') {
|
|
123
|
+
try {
|
|
124
|
+
fs_1.default.chmodSync(launcherPath, 0o755);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Best effort only. IDE configs invoke this through node, so chmod is not required.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
command: process.execPath,
|
|
133
|
+
args: [launcherPath],
|
|
134
|
+
path: launcherPath
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.isHTTPServer = isHTTPServer;
|
|
|
10
10
|
exports.buildAllBaseServers = buildAllBaseServers;
|
|
11
11
|
const provider_registry_1 = require("../providers/provider-registry");
|
|
12
12
|
const command_resolution_1 = require("./command-resolution");
|
|
13
|
+
const fraim_mcp_latest_launcher_1 = require("./fraim-mcp-latest-launcher");
|
|
13
14
|
exports.BASE_MCP_SERVERS = [
|
|
14
15
|
{
|
|
15
16
|
id: 'git',
|
|
@@ -33,16 +34,19 @@ exports.BASE_MCP_SERVERS = [
|
|
|
33
34
|
id: 'fraim',
|
|
34
35
|
name: 'FRAIM',
|
|
35
36
|
description: 'FRAIM job orchestration and mentoring',
|
|
36
|
-
buildServer: (fraimKey) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
buildServer: (fraimKey) => {
|
|
38
|
+
const launcher = (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
39
|
+
return {
|
|
40
|
+
command: launcher.command,
|
|
41
|
+
args: launcher.args,
|
|
42
|
+
env: {
|
|
43
|
+
// Include API key for IDE configs (Codex, VSCode, etc.)
|
|
44
|
+
// The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
|
|
45
|
+
FRAIM_API_KEY: fraimKey,
|
|
46
|
+
FRAIM_REMOTE_URL: 'https://fraim.wellnessatwork.me'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
];
|
|
48
52
|
// ============================================================================
|
|
@@ -338,7 +338,7 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
|
|
|
338
338
|
console.log(chalk_1.default.blue('\n🎯 Next steps:'));
|
|
339
339
|
console.log(chalk_1.default.cyan(' 1. Restart your configured IDEs'));
|
|
340
340
|
console.log(chalk_1.default.cyan(' 2. Go to any project directory'));
|
|
341
|
-
console.log(chalk_1.default.cyan(' 3. Run: npx fraim
|
|
341
|
+
console.log(chalk_1.default.cyan(' 3. Run: npx fraim@latest init-project'));
|
|
342
342
|
console.log(chalk_1.default.cyan(' 4. Tell your AI agent: "Onboard this project"'));
|
|
343
343
|
}
|
|
344
344
|
};
|
|
@@ -56,10 +56,10 @@ function buildFraimInvocationBody(profile = 'none') {
|
|
|
56
56
|
return `Follow this process:
|
|
57
57
|
|
|
58
58
|
${buildDeferredToolBootstrapSection(profile)}1. **If the user did not specify a FRAIM job or topic**:
|
|
59
|
-
|
|
59
|
+
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.
|
|
60
60
|
|
|
61
61
|
2. **Find the match**:
|
|
62
|
-
Match the user's request to a FRAIM job from \`list_fraim_jobs()
|
|
62
|
+
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 job matches, try a likely FRAIM skill with \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` and confirm the match with the user.
|
|
63
63
|
|
|
64
64
|
3. **Load the full content**:
|
|
65
65
|
- For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
|
|
@@ -12,6 +12,7 @@ exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
|
|
|
12
12
|
'fraim/ai-employee/',
|
|
13
13
|
'fraim/ai-manager/',
|
|
14
14
|
'fraim/docs/',
|
|
15
|
+
'fraim/.sync-metadata.json',
|
|
15
16
|
];
|
|
16
17
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
18
|
const managedBlockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
@@ -43,6 +44,16 @@ function resolveGitInfoExcludePath(projectRoot) {
|
|
|
43
44
|
const gitDir = path_1.default.isAbsolute(match[1])
|
|
44
45
|
? match[1]
|
|
45
46
|
: path_1.default.resolve(projectRoot, match[1]);
|
|
47
|
+
const commonDirPath = path_1.default.join(gitDir, 'commondir');
|
|
48
|
+
if (fs_1.default.existsSync(commonDirPath)) {
|
|
49
|
+
const commonDirRaw = fs_1.default.readFileSync(commonDirPath, 'utf8').trim();
|
|
50
|
+
if (commonDirRaw.length > 0) {
|
|
51
|
+
const commonDir = path_1.default.isAbsolute(commonDirRaw)
|
|
52
|
+
? commonDirRaw
|
|
53
|
+
: path_1.default.resolve(gitDir, commonDirRaw);
|
|
54
|
+
return path_1.default.join(commonDir, 'info', 'exclude');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
return path_1.default.join(gitDir, 'info', 'exclude');
|
|
47
58
|
}
|
|
48
59
|
const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GITHUB_WORKFLOW_ASSET_PREFIX = exports.GITHUB_WORKFLOW_HEADER = exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = void 0;
|
|
7
|
+
exports.isGitHubWorkflowAutomationEnabled = isGitHubWorkflowAutomationEnabled;
|
|
8
|
+
exports.decorateManagedGitHubWorkflow = decorateManagedGitHubWorkflow;
|
|
9
|
+
exports.isFraimManagedGitHubWorkflow = isFraimManagedGitHubWorkflow;
|
|
10
|
+
exports.loadGitHubWorkflowManifest = loadGitHubWorkflowManifest;
|
|
11
|
+
exports.loadLocalGitHubWorkflowBundle = loadLocalGitHubWorkflowBundle;
|
|
12
|
+
exports.fetchGitHubWorkflowBundle = fetchGitHubWorkflowBundle;
|
|
13
|
+
exports.reconcileGitHubWorkflowAssets = reconcileGitHubWorkflowAssets;
|
|
14
|
+
exports.formatGitHubWorkflowSyncSummary = formatGitHubWorkflowSyncSummary;
|
|
15
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const config_loader_1 = require("../../core/config-loader");
|
|
20
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
21
|
+
exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = path_1.default.join('fraim', 'managed-assets', 'github-workflows.json');
|
|
22
|
+
exports.GITHUB_WORKFLOW_HEADER = '# FRAIM-MANAGED: github-workflow';
|
|
23
|
+
exports.GITHUB_WORKFLOW_ASSET_PREFIX = '# FRAIM-ASSET-ID: ';
|
|
24
|
+
function sha256(content) {
|
|
25
|
+
return `sha256:${crypto_1.default.createHash('sha256').update(content).digest('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
function getManifestPath(projectRoot) {
|
|
28
|
+
return path_1.default.join(projectRoot, exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH);
|
|
29
|
+
}
|
|
30
|
+
function isGitHubWorkflowAutomationEnabled(config) {
|
|
31
|
+
return config?.customizations?.githubWorkflows?.enabled === true;
|
|
32
|
+
}
|
|
33
|
+
function decorateManagedGitHubWorkflow(assetId, content) {
|
|
34
|
+
const normalized = content.replace(/^\uFEFF/, '');
|
|
35
|
+
const assetHeader = `${exports.GITHUB_WORKFLOW_HEADER}\n${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`;
|
|
36
|
+
if (normalized.includes(exports.GITHUB_WORKFLOW_HEADER) && normalized.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`)) {
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
return `${assetHeader}\n${normalized}`;
|
|
40
|
+
}
|
|
41
|
+
function isFraimManagedGitHubWorkflow(content, assetId) {
|
|
42
|
+
if (!content.includes(exports.GITHUB_WORKFLOW_HEADER)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!assetId) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return content.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`);
|
|
49
|
+
}
|
|
50
|
+
function loadGitHubWorkflowManifest(projectRoot) {
|
|
51
|
+
const manifestPath = getManifestPath(projectRoot);
|
|
52
|
+
if (!fs_1.default.existsSync(manifestPath)) {
|
|
53
|
+
return {
|
|
54
|
+
version: 1,
|
|
55
|
+
enabled: true,
|
|
56
|
+
provider: 'github',
|
|
57
|
+
managedFiles: []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
61
|
+
}
|
|
62
|
+
function writeGitHubWorkflowManifest(projectRoot, manifest) {
|
|
63
|
+
const manifestPath = getManifestPath(projectRoot);
|
|
64
|
+
fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true });
|
|
65
|
+
fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
function loadLocalGitHubWorkflowBundle(registryRoot) {
|
|
68
|
+
const workflowsRoot = path_1.default.join(registryRoot, 'github', 'workflows');
|
|
69
|
+
if (!fs_1.default.existsSync(workflowsRoot)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const files = fs_1.default.readdirSync(workflowsRoot, { withFileTypes: true })
|
|
73
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.yml'))
|
|
74
|
+
.map((entry) => {
|
|
75
|
+
const fullPath = path_1.default.join(workflowsRoot, entry.name);
|
|
76
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf8');
|
|
77
|
+
return {
|
|
78
|
+
path: entry.name,
|
|
79
|
+
content,
|
|
80
|
+
type: 'github-workflow',
|
|
81
|
+
digest: sha256(content)
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
85
|
+
}
|
|
86
|
+
async function fetchGitHubWorkflowBundle(options) {
|
|
87
|
+
const response = await axios_1.default.get(`${options.remoteUrl}/api/registry/github-workflows`, {
|
|
88
|
+
headers: {
|
|
89
|
+
'x-api-key': options.apiKey
|
|
90
|
+
},
|
|
91
|
+
timeout: 30000
|
|
92
|
+
});
|
|
93
|
+
return (response.data.files || []);
|
|
94
|
+
}
|
|
95
|
+
function readProjectConfig(projectRoot, config) {
|
|
96
|
+
if (config) {
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
return (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
|
|
100
|
+
}
|
|
101
|
+
function reconcileGitHubWorkflowAssets(options) {
|
|
102
|
+
const config = readProjectConfig(options.projectRoot, options.config);
|
|
103
|
+
const provider = config.repository?.provider || null;
|
|
104
|
+
if (provider !== 'github') {
|
|
105
|
+
return {
|
|
106
|
+
enabled: false,
|
|
107
|
+
provider,
|
|
108
|
+
skippedReason: 'non-GitHub repo',
|
|
109
|
+
results: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!isGitHubWorkflowAutomationEnabled(config)) {
|
|
113
|
+
return {
|
|
114
|
+
enabled: false,
|
|
115
|
+
provider,
|
|
116
|
+
skippedReason: 'GitHub workflow automation disabled',
|
|
117
|
+
results: []
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const manifest = loadGitHubWorkflowManifest(options.projectRoot);
|
|
121
|
+
const manifestEntries = new Map(manifest.managedFiles.map((entry) => [entry.path, entry]));
|
|
122
|
+
const workflowsDir = path_1.default.join(options.projectRoot, '.github', 'workflows');
|
|
123
|
+
fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
124
|
+
const results = [];
|
|
125
|
+
const nextManagedFiles = [];
|
|
126
|
+
for (const file of options.files) {
|
|
127
|
+
const assetId = path_1.default.basename(file.path);
|
|
128
|
+
const relativeWorkflowPath = path_1.default.join('.github', 'workflows', file.path).replace(/[\\/]/g, '/');
|
|
129
|
+
const destinationPath = path_1.default.join(options.projectRoot, '.github', 'workflows', file.path);
|
|
130
|
+
const desiredContent = decorateManagedGitHubWorkflow(assetId, file.content);
|
|
131
|
+
const desiredInstalledDigest = sha256(desiredContent);
|
|
132
|
+
const previousEntry = manifestEntries.get(relativeWorkflowPath);
|
|
133
|
+
if (!fs_1.default.existsSync(destinationPath)) {
|
|
134
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destinationPath), { recursive: true });
|
|
135
|
+
fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
|
|
136
|
+
nextManagedFiles.push({
|
|
137
|
+
path: relativeWorkflowPath,
|
|
138
|
+
assetId,
|
|
139
|
+
sourceDigest: file.digest,
|
|
140
|
+
installedDigest: desiredInstalledDigest
|
|
141
|
+
});
|
|
142
|
+
results.push({
|
|
143
|
+
path: relativeWorkflowPath,
|
|
144
|
+
assetId,
|
|
145
|
+
status: 'installed'
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const currentContent = fs_1.default.readFileSync(destinationPath, 'utf8');
|
|
150
|
+
const currentDigest = sha256(currentContent);
|
|
151
|
+
const ownedByFraim = isFraimManagedGitHubWorkflow(currentContent, assetId) || Boolean(previousEntry);
|
|
152
|
+
if (!ownedByFraim) {
|
|
153
|
+
if (previousEntry) {
|
|
154
|
+
nextManagedFiles.push(previousEntry);
|
|
155
|
+
}
|
|
156
|
+
results.push({
|
|
157
|
+
path: relativeWorkflowPath,
|
|
158
|
+
assetId,
|
|
159
|
+
status: 'conflict',
|
|
160
|
+
reason: 'same-named file exists without FRAIM ownership marker'
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (currentDigest === desiredInstalledDigest) {
|
|
165
|
+
nextManagedFiles.push({
|
|
166
|
+
path: relativeWorkflowPath,
|
|
167
|
+
assetId,
|
|
168
|
+
sourceDigest: file.digest,
|
|
169
|
+
installedDigest: desiredInstalledDigest
|
|
170
|
+
});
|
|
171
|
+
results.push({
|
|
172
|
+
path: relativeWorkflowPath,
|
|
173
|
+
assetId,
|
|
174
|
+
status: 'already current'
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (previousEntry && currentDigest === previousEntry.installedDigest) {
|
|
179
|
+
fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
|
|
180
|
+
nextManagedFiles.push({
|
|
181
|
+
path: relativeWorkflowPath,
|
|
182
|
+
assetId,
|
|
183
|
+
sourceDigest: file.digest,
|
|
184
|
+
installedDigest: desiredInstalledDigest
|
|
185
|
+
});
|
|
186
|
+
results.push({
|
|
187
|
+
path: relativeWorkflowPath,
|
|
188
|
+
assetId,
|
|
189
|
+
status: 'updated'
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
nextManagedFiles.push(previousEntry || {
|
|
194
|
+
path: relativeWorkflowPath,
|
|
195
|
+
assetId,
|
|
196
|
+
sourceDigest: file.digest,
|
|
197
|
+
installedDigest: currentDigest
|
|
198
|
+
});
|
|
199
|
+
results.push({
|
|
200
|
+
path: relativeWorkflowPath,
|
|
201
|
+
assetId,
|
|
202
|
+
status: 'conflict',
|
|
203
|
+
reason: previousEntry
|
|
204
|
+
? 'managed file has local edits that do not match the last installed digest'
|
|
205
|
+
: 'FRAIM-managed header exists without manifest baseline'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
writeGitHubWorkflowManifest(options.projectRoot, {
|
|
209
|
+
version: 1,
|
|
210
|
+
enabled: true,
|
|
211
|
+
provider: 'github',
|
|
212
|
+
managedFiles: nextManagedFiles.sort((a, b) => a.path.localeCompare(b.path))
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
enabled: true,
|
|
216
|
+
provider,
|
|
217
|
+
results
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function formatGitHubWorkflowSyncSummary(result) {
|
|
221
|
+
if (!result.enabled) {
|
|
222
|
+
return [`GitHub workflow reconciliation skipped: ${result.skippedReason || 'not enabled'}`];
|
|
223
|
+
}
|
|
224
|
+
if (result.results.length === 0) {
|
|
225
|
+
return ['GitHub workflow reconciliation: no workflow assets found'];
|
|
226
|
+
}
|
|
227
|
+
return result.results.map((item) => {
|
|
228
|
+
const suffix = item.reason ? ` (${item.reason})` : '';
|
|
229
|
+
return `GitHub workflow ${item.status}: ${item.path}${suffix}`;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -34,7 +34,7 @@ function getManagedAgentBinDirs() {
|
|
|
34
34
|
const portableNodeBin = getPortableNodeBinPath();
|
|
35
35
|
const candidates = process.platform === 'win32'
|
|
36
36
|
? [nodeRoot, portableNodeBin]
|
|
37
|
-
: [path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
|
|
37
|
+
: [nodeRoot, path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
|
|
38
38
|
return [...new Set(candidates.filter(Boolean))];
|
|
39
39
|
}
|
|
40
40
|
function buildPathWithManagedAgentBins(basePath) {
|
|
@@ -22,7 +22,7 @@ function formatModeLabel(mode) {
|
|
|
22
22
|
function getModeSpecificNextStep(mode) {
|
|
23
23
|
switch (mode) {
|
|
24
24
|
case 'conversational':
|
|
25
|
-
return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable
|
|
25
|
+
return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable project rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
26
26
|
case 'split':
|
|
27
27
|
return `The agent will create fraim/config.json during onboarding, confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
28
28
|
default:
|
|
@@ -70,12 +70,15 @@ function buildInitProjectSummary(result) {
|
|
|
70
70
|
}
|
|
71
71
|
function printInitProjectSummary(result) {
|
|
72
72
|
const summary = buildInitProjectSummary(result);
|
|
73
|
+
const showRepositoryDetails = result.mode !== 'conversational';
|
|
73
74
|
console.log(chalk_1.default.green(`\n${summary.status}`));
|
|
74
75
|
console.log(chalk_1.default.blue('Project summary:'));
|
|
75
76
|
console.log(chalk_1.default.gray(` Mode: ${summary.fields.mode}`));
|
|
76
77
|
console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
if (showRepositoryDetails) {
|
|
79
|
+
console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
|
|
80
|
+
console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
|
|
81
|
+
}
|
|
79
82
|
console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
|
|
80
83
|
if (summary.fields.createdPaths.length > 0) {
|
|
81
84
|
console.log(chalk_1.default.gray(` Created: ${summary.fields.createdPaths.join(', ')}`));
|
|
@@ -31,8 +31,8 @@ function shouldLockSyncedContent() {
|
|
|
31
31
|
}
|
|
32
32
|
function getSyncedContentLockTargets(projectRoot) {
|
|
33
33
|
return fraim_gitignore_1.FRAIM_SYNC_GITIGNORE_ENTRIES
|
|
34
|
+
.filter((entry) => /[\\/]$/.test(entry))
|
|
34
35
|
.map((entry) => entry.replace(/[\\/]+$/, ''))
|
|
35
|
-
.filter((entry) => entry.length > 0)
|
|
36
36
|
.map((entry) => (0, path_1.join)(projectRoot, entry));
|
|
37
37
|
}
|
|
38
38
|
function setFileWriteLockRecursively(dirPath, readOnly) {
|
|
@@ -28,7 +28,6 @@ class AIMentor {
|
|
|
28
28
|
throw new Error(`Phase "${args.currentPhase}" not found in job "${args.jobName}".`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// Handle different statuses
|
|
32
31
|
if (args.status === 'starting') {
|
|
33
32
|
return await this.generateStartingMessage(workflow, args.currentPhase, args.skipIncludes);
|
|
34
33
|
}
|
|
@@ -63,27 +62,17 @@ class AIMentor {
|
|
|
63
62
|
buildReportBackFooter(jobName, phaseId, phaseFlow) {
|
|
64
63
|
if (!phaseFlow)
|
|
65
64
|
return '';
|
|
66
|
-
const base = `seekMentoring({
|
|
67
|
-
jobName: "${jobName}",
|
|
68
|
-
issueNumber: "<issue_number>",
|
|
69
|
-
currentPhase: "${phaseId}",
|
|
70
|
-
status: "complete",`;
|
|
71
65
|
const onSuccess = phaseFlow.onSuccess;
|
|
72
|
-
|
|
73
|
-
if (!onSuccess) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.map(k => `"${k}"`)
|
|
83
|
-
.join(' | ');
|
|
84
|
-
successBlock = `\n\n---\n\n> **⚑ Phase Complete — Report Back**\n> The next phase depends on your outcome. Set \`findings.issueType\` (or \`findings.phaseOutcome\`) to one of: ${validOutcomes}\n>\n> Then call:\n> \`\`\`javascript\n> ${base}\n> findings: { issueType: "<Outcome>" }\n> })\n> \`\`\``;
|
|
85
|
-
}
|
|
86
|
-
return successBlock;
|
|
66
|
+
const completionCall = `seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete" })`;
|
|
67
|
+
if (!onSuccess || typeof onSuccess === 'string') {
|
|
68
|
+
const finalPhaseNote = onSuccess ? '' : ' This is the final phase.';
|
|
69
|
+
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`${completionCall}\`.${finalPhaseNote}`;
|
|
70
|
+
}
|
|
71
|
+
const validOutcomes = Object.keys(onSuccess)
|
|
72
|
+
.filter((key) => key !== 'default')
|
|
73
|
+
.map((key) => `"${key}"`)
|
|
74
|
+
.join(' | ');
|
|
75
|
+
return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: { issueType: "<outcome>" } })\` with one of: ${validOutcomes}.`;
|
|
87
76
|
}
|
|
88
77
|
/** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
|
|
89
78
|
async getPhaseAuthorityContent() {
|
|
@@ -95,18 +84,32 @@ class AIMentor {
|
|
|
95
84
|
return '';
|
|
96
85
|
}
|
|
97
86
|
}
|
|
87
|
+
getCompactPhaseAuthority() {
|
|
88
|
+
return AIMentor.COMPACT_PHASE_AUTHORITY;
|
|
89
|
+
}
|
|
98
90
|
async prependPhaseAuthority(message, isPhased) {
|
|
99
91
|
if (!isPhased)
|
|
100
92
|
return message;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
return `${this.getCompactPhaseAuthority()}\n\n${message}`;
|
|
94
|
+
}
|
|
95
|
+
buildPhasedJobOverview(workflow) {
|
|
96
|
+
const metadataPhases = Object.keys(workflow.metadata.phases || {});
|
|
97
|
+
const initialPhase = workflow.metadata.initialPhase || metadataPhases[0] || Array.from(workflow.phases.keys())[0] || 'starting';
|
|
98
|
+
const overview = workflow.overview.trim();
|
|
99
|
+
return [
|
|
100
|
+
overview,
|
|
101
|
+
'',
|
|
102
|
+
'---',
|
|
103
|
+
'',
|
|
104
|
+
'## Start Here',
|
|
105
|
+
`- Initial phase: \`${initialPhase}\``,
|
|
106
|
+
'- Call `seekMentoring` to load the instructions for this phase.'
|
|
107
|
+
].join('\n');
|
|
105
108
|
}
|
|
106
109
|
async generateStartingMessage(workflow, phaseId, skipIncludes) {
|
|
107
110
|
const entityType = 'Job';
|
|
108
111
|
if (workflow.isSimple) {
|
|
109
|
-
const message =
|
|
112
|
+
const message = `Starting ${entityType}: ${workflow.metadata.name}\n\n${workflow.overview}`;
|
|
110
113
|
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (starting)`);
|
|
111
114
|
return {
|
|
112
115
|
message,
|
|
@@ -116,17 +119,14 @@ class AIMentor {
|
|
|
116
119
|
}
|
|
117
120
|
const isVeryFirstCall = phaseId === 'starting';
|
|
118
121
|
const targetPhase = isVeryFirstCall ? (workflow.metadata.initialPhase || 'starting') : phaseId;
|
|
119
|
-
let message =
|
|
120
|
-
if (isVeryFirstCall && workflow.overview) {
|
|
121
|
-
message += `${workflow.overview}\n\n---\n\n`;
|
|
122
|
-
}
|
|
122
|
+
let message = `### Current Phase: ${targetPhase}\n\n`;
|
|
123
123
|
let instructions = workflow.phases.get(targetPhase);
|
|
124
124
|
if (instructions) {
|
|
125
125
|
instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
|
|
126
|
-
message +=
|
|
126
|
+
message += instructions;
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
|
-
message +=
|
|
129
|
+
message += `No specific instructions found for phase: ${targetPhase}`;
|
|
130
130
|
}
|
|
131
131
|
const phaseFlow = workflow.metadata.phases?.[targetPhase];
|
|
132
132
|
message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
|
|
@@ -142,7 +142,7 @@ class AIMentor {
|
|
|
142
142
|
async generateCompletionMessage(workflow, phaseId, findings, evidence, skipIncludes) {
|
|
143
143
|
const entityType = 'Job';
|
|
144
144
|
if (workflow.isSimple) {
|
|
145
|
-
const message =
|
|
145
|
+
const message = `${entityType} Complete: ${workflow.metadata.name}\n\nYou have completed the ${workflow.metadata.name} ${entityType.toLowerCase()}.`;
|
|
146
146
|
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (complete)`);
|
|
147
147
|
return {
|
|
148
148
|
message,
|
|
@@ -158,12 +158,12 @@ class AIMentor {
|
|
|
158
158
|
}
|
|
159
159
|
else {
|
|
160
160
|
const outcome = findings?.phaseOutcome ?? findings?.issueType ?? evidence?.issueType ?? evidence?.phaseOutcome ?? 'default';
|
|
161
|
-
nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess
|
|
161
|
+
nextPhaseId = phaseFlow.onSuccess[outcome] ?? phaseFlow.onSuccess.default ?? null;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
let message = '';
|
|
165
165
|
if (nextPhaseId) {
|
|
166
|
-
message += `
|
|
166
|
+
message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
|
|
167
167
|
let nextInstructions = workflow.phases.get(nextPhaseId);
|
|
168
168
|
if (nextInstructions) {
|
|
169
169
|
nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
|
|
@@ -187,7 +187,7 @@ class AIMentor {
|
|
|
187
187
|
async generateHelpMessage(workflow, phaseId, status, skipIncludes) {
|
|
188
188
|
const entityType = 'Job';
|
|
189
189
|
if (workflow.isSimple) {
|
|
190
|
-
const message =
|
|
190
|
+
const message = `${entityType}: ${workflow.metadata.name}\n\n${workflow.overview}`;
|
|
191
191
|
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name} (${status})`);
|
|
192
192
|
return {
|
|
193
193
|
message,
|
|
@@ -214,7 +214,15 @@ class AIMentor {
|
|
|
214
214
|
}
|
|
215
215
|
async getJobOverview(jobName) {
|
|
216
216
|
const job = await this.getOrLoadJob(jobName);
|
|
217
|
-
|
|
217
|
+
if (!job)
|
|
218
|
+
return null;
|
|
219
|
+
if (job.isSimple) {
|
|
220
|
+
return { overview: job.overview, isSimple: true };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
overview: this.buildPhasedJobOverview(job),
|
|
224
|
+
isSimple: false
|
|
225
|
+
};
|
|
218
226
|
}
|
|
219
227
|
async getAllJobMetadata() {
|
|
220
228
|
const items = await this.resolver.listItems('job');
|
|
@@ -228,3 +236,4 @@ class AIMentor {
|
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
exports.AIMentor = AIMentor;
|
|
239
|
+
AIMentor.COMPACT_PHASE_AUTHORITY = '**Use only the phases defined in this workflow.**';
|