fraim-framework 2.0.60 → 2.0.63
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/bin/fraim-mcp.js +25 -9
- package/dist/src/cli/commands/init-project.js +98 -26
- package/dist/src/cli/commands/init.js +81 -23
- package/dist/src/cli/commands/setup.js +275 -47
- package/dist/src/local-mcp-server/stdio-server.js +507 -30
- package/dist/src/utils/enforcement-utils.js +239 -0
- package/dist/src/utils/git-utils.js +0 -27
- package/dist/src/utils/platform-detection.js +1 -1
- package/dist/src/utils/script-sync-utils.js +6 -1
- package/dist/src/utils/validate-workflows.js +101 -0
- package/dist/src/utils/workflow-parser.js +41 -9
- package/index.js +1 -1
- package/package.json +4 -3
- package/bin/fraim.js +0 -23
- package/dist/src/cli/commands/mcp.js +0 -65
- package/dist/src/fraim/issue-tracking/ado-provider.js +0 -304
- package/dist/src/fraim/issue-tracking/factory.js +0 -63
- package/dist/src/fraim/issue-tracking/github-provider.js +0 -200
- package/dist/src/fraim/issue-tracking/types.js +0 -7
- package/dist/src/fraim/issue-tracking-config.js +0 -83
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Enforcement Utilities for FRAIM
|
|
4
|
+
*
|
|
5
|
+
* Provides deterministic enforcement of:
|
|
6
|
+
* - Working style (PR vs Conversation)
|
|
7
|
+
* - Feedback tracking requirements
|
|
8
|
+
* - User config initialization
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.initializeUserConfig = initializeUserConfig;
|
|
12
|
+
exports.loadUserConfig = loadUserConfig;
|
|
13
|
+
exports.getEnforcementContext = getEnforcementContext;
|
|
14
|
+
exports.isCommitAllowed = isCommitAllowed;
|
|
15
|
+
exports.checkUnaddressedFeedback = checkUnaddressedFeedback;
|
|
16
|
+
exports.generateCommitInstructions = generateCommitInstructions;
|
|
17
|
+
exports.generateFeedbackInstructions = generateFeedbackInstructions;
|
|
18
|
+
const fs_1 = require("fs");
|
|
19
|
+
const path_1 = require("path");
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const script_sync_utils_js_1 = require("./script-sync-utils.js");
|
|
22
|
+
const git_utils_js_1 = require("./git-utils.js");
|
|
23
|
+
/**
|
|
24
|
+
* Initialize user config with default values if it doesn't exist
|
|
25
|
+
*/
|
|
26
|
+
function initializeUserConfig() {
|
|
27
|
+
try {
|
|
28
|
+
const userConfigPath = (0, path_1.join)((0, script_sync_utils_js_1.getUserFraimDir)(), 'config.json');
|
|
29
|
+
if (!(0, fs_1.existsSync)(userConfigPath)) {
|
|
30
|
+
const defaultConfig = {
|
|
31
|
+
workingStyle: 'PR' // Default to PR mode
|
|
32
|
+
};
|
|
33
|
+
// Ensure directory exists
|
|
34
|
+
const userFraimDir = (0, script_sync_utils_js_1.getUserFraimDir)();
|
|
35
|
+
if (!(0, fs_1.existsSync)(userFraimDir)) {
|
|
36
|
+
const { mkdirSync } = require('fs');
|
|
37
|
+
mkdirSync(userFraimDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
(0, fs_1.writeFileSync)(userConfigPath, JSON.stringify(defaultConfig, null, 2));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.warn('⚠️ Failed to initialize user config:', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load user config from ~/.fraim/config.json
|
|
48
|
+
*/
|
|
49
|
+
function loadUserConfig() {
|
|
50
|
+
try {
|
|
51
|
+
const userConfigPath = (0, path_1.join)((0, script_sync_utils_js_1.getUserFraimDir)(), 'config.json');
|
|
52
|
+
if ((0, fs_1.existsSync)(userConfigPath)) {
|
|
53
|
+
const content = (0, fs_1.readFileSync)(userConfigPath, 'utf8');
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn('⚠️ Failed to load user config:', error);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get enforcement context (working style, branch info)
|
|
64
|
+
*/
|
|
65
|
+
function getEnforcementContext(issueNumber, projectRoot) {
|
|
66
|
+
const userConfig = loadUserConfig();
|
|
67
|
+
const workingStyle = userConfig?.workingStyle || 'PR'; // Default to PR
|
|
68
|
+
let currentBranch = null;
|
|
69
|
+
let defaultBranch = null;
|
|
70
|
+
try {
|
|
71
|
+
currentBranch = (0, git_utils_js_1.getCurrentGitBranch)();
|
|
72
|
+
// Try to get default branch
|
|
73
|
+
try {
|
|
74
|
+
const remoteHead = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
75
|
+
timeout: 2000,
|
|
76
|
+
stdio: 'pipe'
|
|
77
|
+
}).toString().trim();
|
|
78
|
+
const match = remoteHead.match(/refs\/remotes\/origin\/(.+)$/);
|
|
79
|
+
if (match) {
|
|
80
|
+
defaultBranch = match[1];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
defaultBranch = 'main'; // Fallback
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Not a git repo or git command failed
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
workingStyle,
|
|
92
|
+
currentBranch,
|
|
93
|
+
defaultBranch,
|
|
94
|
+
issueNumber,
|
|
95
|
+
projectRoot
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if agent commit is allowed based on working style
|
|
100
|
+
* Note: In Conversation mode, agent NEVER commits (user commits manually)
|
|
101
|
+
*/
|
|
102
|
+
function isCommitAllowed(context) {
|
|
103
|
+
// If Conversation style, agent NEVER commits (user commits manually)
|
|
104
|
+
if (context.workingStyle === 'Conversation') {
|
|
105
|
+
return {
|
|
106
|
+
allowed: false,
|
|
107
|
+
reason: 'Working style is "Conversation" - agent does not commit. User commits manually.'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// PR style: commits only allowed on feature branches, not main/master
|
|
111
|
+
if (!context.currentBranch) {
|
|
112
|
+
return {
|
|
113
|
+
allowed: false,
|
|
114
|
+
reason: 'Not in a git repository or unable to determine current branch'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const defaultBranch = context.defaultBranch || 'main';
|
|
118
|
+
const isDefaultBranch = context.currentBranch === defaultBranch ||
|
|
119
|
+
context.currentBranch === 'master' ||
|
|
120
|
+
context.currentBranch === 'main';
|
|
121
|
+
if (isDefaultBranch) {
|
|
122
|
+
return {
|
|
123
|
+
allowed: false,
|
|
124
|
+
reason: `Working style is "PR" but you are on ${context.currentBranch} branch. Create a feature branch first.`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { allowed: true };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check for unaddressed feedback files
|
|
131
|
+
*/
|
|
132
|
+
function checkUnaddressedFeedback(issueNumber, projectRoot) {
|
|
133
|
+
const feedbackFiles = [];
|
|
134
|
+
if (!projectRoot) {
|
|
135
|
+
return { hasUnaddressed: false, feedbackFiles: [] };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const evidenceDir = (0, path_1.join)(projectRoot, 'docs', 'evidence');
|
|
139
|
+
if (!(0, fs_1.existsSync)(evidenceDir)) {
|
|
140
|
+
return { hasUnaddressed: false, feedbackFiles: [] };
|
|
141
|
+
}
|
|
142
|
+
const files = (0, fs_1.readdirSync)(evidenceDir);
|
|
143
|
+
const issueFeedbackPattern = new RegExp(`^${issueNumber}-.*-feedback\\.md$`);
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
if (issueFeedbackPattern.test(file)) {
|
|
146
|
+
const filePath = (0, path_1.join)(evidenceDir, file);
|
|
147
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
148
|
+
// Check if feedback is addressed (look for "Status: Addressed" or similar markers)
|
|
149
|
+
// Simple heuristic: if file contains "Status: Addressed" or "✅ Addressed", consider it addressed
|
|
150
|
+
// If it contains "Status: Unaddressed", it's definitely unaddressed
|
|
151
|
+
const hasUnaddressed = /status:\s*unaddressed/i.test(content);
|
|
152
|
+
const isAddressed = /status:\s*addressed|✅\s*addressed/i.test(content);
|
|
153
|
+
// If explicitly unaddressed, or if no addressed markers found at all, consider unaddressed
|
|
154
|
+
// Note: If file has both, "Unaddressed" takes precedence
|
|
155
|
+
if (hasUnaddressed || (!hasUnaddressed && !isAddressed)) {
|
|
156
|
+
feedbackFiles.push(file);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.warn('⚠️ Failed to check feedback files:', error);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
hasUnaddressed: feedbackFiles.length > 0,
|
|
166
|
+
feedbackFiles
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Generate commit behavior instructions based on working style
|
|
171
|
+
*/
|
|
172
|
+
function generateCommitInstructions(context) {
|
|
173
|
+
// Check commit permission first (handles both Conversation and PR modes)
|
|
174
|
+
const commitCheck = isCommitAllowed(context);
|
|
175
|
+
if (context.workingStyle === 'Conversation') {
|
|
176
|
+
// Conversation mode: Agent never commits, user commits manually
|
|
177
|
+
return `\n\n**⚠️ Working Style: Conversation**\n\n` +
|
|
178
|
+
`You are in "Conversation" mode. **DO NOT commit changes**. The user will commit manually.\n\n` +
|
|
179
|
+
`**Branch Strategy**:\n` +
|
|
180
|
+
`- Work directly on ${context.defaultBranch || 'main/master'} branch\n` +
|
|
181
|
+
`- **Skip branch creation**: No need to run prep-issue.sh or create feature branches\n` +
|
|
182
|
+
`- **Skip PR creation**: Work is committed directly to ${context.defaultBranch || 'main/master'}\n\n` +
|
|
183
|
+
`**Commit Behavior**:\n` +
|
|
184
|
+
`- Make changes as requested\n` +
|
|
185
|
+
`- Do not use git commit tools\n` +
|
|
186
|
+
`- Show diff to user: "I've made these changes: [show diff]"\n` +
|
|
187
|
+
`- Suggest commit message if helpful\n` +
|
|
188
|
+
`- Wait for user to review and commit manually\n\n` +
|
|
189
|
+
`**Outcome Statements**: When documenting completion, use:\n` +
|
|
190
|
+
`- "All work ready for user review and commit" (instead of "committed to feature branch")\n` +
|
|
191
|
+
`- "Changes prepared on ${context.defaultBranch || 'main/master'} branch"\n`;
|
|
192
|
+
}
|
|
193
|
+
// PR style: Check if commits are allowed
|
|
194
|
+
if (!commitCheck.allowed) {
|
|
195
|
+
// Blocked: either on main/master or not in git repo
|
|
196
|
+
return `\n\n**⚠️ Working Style: PR**\n\n` +
|
|
197
|
+
`**BLOCKED**: ${commitCheck.reason}\n\n` +
|
|
198
|
+
`**Action Required**:\n` +
|
|
199
|
+
`1. Run prep-issue script: \`~/.fraim/scripts/prep-issue.sh ${context.issueNumber || 'ISSUE_NUMBER'}\`\n` +
|
|
200
|
+
` OR create feature branch manually: \`git checkout -b feature/${context.issueNumber || 'issue'}-description\`\n` +
|
|
201
|
+
`2. Push branch: \`git push -u origin feature/${context.issueNumber || 'issue'}-description\`\n` +
|
|
202
|
+
`3. Then proceed with your work\n` +
|
|
203
|
+
`**DO NOT commit to ${context.currentBranch || 'main/master'} branch.**\n`;
|
|
204
|
+
}
|
|
205
|
+
// PR mode on feature branch: Commits allowed
|
|
206
|
+
return `\n\n**✅ Working Style: PR**\n\n` +
|
|
207
|
+
`You are in "PR" mode. Commits are allowed on feature branch: **${context.currentBranch}**\n\n` +
|
|
208
|
+
`**Branch Setup**:\n` +
|
|
209
|
+
`- If branch doesn't exist, run: \`~/.fraim/scripts/prep-issue.sh ${context.issueNumber || 'ISSUE_NUMBER'}\`\n` +
|
|
210
|
+
`- Confirm you're on feature branch: \`git branch --show-current\`\n\n` +
|
|
211
|
+
`**Commit Behavior**:\n` +
|
|
212
|
+
`- Create commits on this branch as you complete work\n` +
|
|
213
|
+
`- Commit message format: \`{workflow_type}({issue_number}): {description}\`\n` +
|
|
214
|
+
`- Push branch: \`git push origin ${context.currentBranch}\`\n\n` +
|
|
215
|
+
`**PR Management**:\n` +
|
|
216
|
+
`- Create PR when ready for review\n` +
|
|
217
|
+
`- **Verify PR exists** before marking work complete\n` +
|
|
218
|
+
`- Link PR to issue and add evidence document link\n\n` +
|
|
219
|
+
`**Outcome Statements**: When documenting completion, use:\n` +
|
|
220
|
+
`- "All work committed and pushed to feature branch: ${context.currentBranch}"\n`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Generate feedback check instructions
|
|
224
|
+
*/
|
|
225
|
+
function generateFeedbackInstructions(issueNumber, projectRoot) {
|
|
226
|
+
const feedbackCheck = checkUnaddressedFeedback(issueNumber, projectRoot);
|
|
227
|
+
if (!feedbackCheck.hasUnaddressed) {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
return `\n\n**⚠️ Unaddressed Feedback Detected**\n\n` +
|
|
231
|
+
`The following feedback files need to be addressed before proceeding:\n` +
|
|
232
|
+
feedbackCheck.feedbackFiles.map(f => `- \`docs/evidence/${f}\``).join('\n') +
|
|
233
|
+
`\n\n**Action Required**:\n` +
|
|
234
|
+
`1. Review each feedback file\n` +
|
|
235
|
+
`2. Address all feedback items\n` +
|
|
236
|
+
`3. Mark feedback as addressed in the file\n` +
|
|
237
|
+
`4. Then continue with this phase\n` +
|
|
238
|
+
`\n**DO NOT proceed until all feedback is addressed.**\n`;
|
|
239
|
+
}
|
|
@@ -5,7 +5,6 @@ exports.determineDatabaseName = determineDatabaseName;
|
|
|
5
5
|
exports.getCurrentGitBranch = getCurrentGitBranch;
|
|
6
6
|
exports.determineSchema = determineSchema;
|
|
7
7
|
exports.getDefaultBranch = getDefaultBranch;
|
|
8
|
-
exports.getGitRemoteInfo = getGitRemoteInfo;
|
|
9
8
|
const child_process_1 = require("child_process");
|
|
10
9
|
/**
|
|
11
10
|
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
@@ -94,29 +93,3 @@ function getDefaultBranch() {
|
|
|
94
93
|
// Default fallback
|
|
95
94
|
return 'main';
|
|
96
95
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Gets the GitHub remote info (owner and repo name)
|
|
99
|
-
*/
|
|
100
|
-
function getGitRemoteInfo() {
|
|
101
|
-
try {
|
|
102
|
-
const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin', {
|
|
103
|
-
timeout: 2000, // 2 second timeout
|
|
104
|
-
stdio: 'pipe'
|
|
105
|
-
}).toString().trim();
|
|
106
|
-
// Match both HTTPS and SSH formats
|
|
107
|
-
// HTTPS: https://github.com/owner/repo.git OR https://github.com/owner/repo
|
|
108
|
-
// SSH: git@github.com:owner/repo.git OR git@github.com:owner/repo
|
|
109
|
-
const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
110
|
-
if (match) {
|
|
111
|
-
return {
|
|
112
|
-
owner: match[1],
|
|
113
|
-
repo: match[2],
|
|
114
|
-
defaultBranch: getDefaultBranch()
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch (e) {
|
|
119
|
-
// Silently fail if not a git repo or no origin
|
|
120
|
-
}
|
|
121
|
-
return { owner: null, repo: null };
|
|
122
|
-
}
|
|
@@ -138,7 +138,7 @@ function extractAdoInfo(url) {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
// visualstudio.com format
|
|
141
|
-
match = url.match(/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
141
|
+
match = url.match(/https?:\/\/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
142
142
|
if (match) {
|
|
143
143
|
return {
|
|
144
144
|
provider: 'ado',
|
|
@@ -31,7 +31,7 @@ function getUserScriptsDir() {
|
|
|
31
31
|
return path_1.default.join(getUserFraimDir(), 'scripts');
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Ensure the user-level FRAIM directories exist
|
|
34
|
+
* Ensure the user-level FRAIM directories exist and initialize user config
|
|
35
35
|
*/
|
|
36
36
|
function ensureUserFraimDirectories() {
|
|
37
37
|
const userFraimDir = getUserFraimDir();
|
|
@@ -42,6 +42,11 @@ function ensureUserFraimDirectories() {
|
|
|
42
42
|
if (!fs_1.default.existsSync(userScriptsDir)) {
|
|
43
43
|
fs_1.default.mkdirSync(userScriptsDir, { recursive: true });
|
|
44
44
|
}
|
|
45
|
+
// Initialize user config (~/.fraim/config.json) if it doesn't exist
|
|
46
|
+
const userConfigPath = path_1.default.join(userFraimDir, 'config.json');
|
|
47
|
+
if (!fs_1.default.existsSync(userConfigPath)) {
|
|
48
|
+
fs_1.default.writeFileSync(userConfigPath, JSON.stringify({ workingStyle: 'PR' }, null, 2));
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* Get all script files from the registry (recursively)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateAllWorkflows = validateAllWorkflows;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const workflow_parser_js_1 = require("./workflow-parser.js");
|
|
7
|
+
/**
|
|
8
|
+
* Validate all workflows in the registry
|
|
9
|
+
*/
|
|
10
|
+
function validateAllWorkflows(registryPath) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
const workflowFiles = [];
|
|
13
|
+
function findWorkflows(dir) {
|
|
14
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
15
|
+
return;
|
|
16
|
+
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
findWorkflows(fullPath);
|
|
21
|
+
}
|
|
22
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
23
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
24
|
+
if (content.includes('"initialPhase"')) {
|
|
25
|
+
workflowFiles.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
findWorkflows(registryPath);
|
|
31
|
+
console.log(`🔍 Validating ${workflowFiles.length} workflows...`);
|
|
32
|
+
for (const filePath of workflowFiles) {
|
|
33
|
+
const relativePath = filePath.replace(registryPath, '');
|
|
34
|
+
try {
|
|
35
|
+
const wf = workflow_parser_js_1.WorkflowParser.parse(filePath);
|
|
36
|
+
if (!wf) {
|
|
37
|
+
errors.push(`❌ ${relativePath}: Failed to parse. Ensure JSON metadata is present and valid.`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const { metadata, phases } = wf;
|
|
41
|
+
// 1. Initial Phase check
|
|
42
|
+
if (!phases.has(metadata.initialPhase)) {
|
|
43
|
+
errors.push(`❌ ${relativePath}: initialPhase "${metadata.initialPhase}" does not exist in phases.`);
|
|
44
|
+
}
|
|
45
|
+
// 2. Validate Phase Flow
|
|
46
|
+
for (const [phaseId, flow] of Object.entries(metadata.phases)) {
|
|
47
|
+
// Check if phase exists in markdown
|
|
48
|
+
if (!phases.has(phaseId)) {
|
|
49
|
+
errors.push(`❌ ${relativePath}: Phase "${phaseId}" defined in metadata but missing in markdown.`);
|
|
50
|
+
}
|
|
51
|
+
// Check Success transition
|
|
52
|
+
if (flow.onSuccess && flow.onSuccess !== 'null') {
|
|
53
|
+
if (typeof flow.onSuccess === 'string') {
|
|
54
|
+
if (!phases.has(flow.onSuccess)) {
|
|
55
|
+
errors.push(`❌ ${relativePath}: Phase "${phaseId}" transition onSuccess -> "${flow.onSuccess}" target not found.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Validate each branch of conditional success
|
|
60
|
+
for (const [branch, target] of Object.entries(flow.onSuccess)) {
|
|
61
|
+
if (target && target !== 'null' && !phases.has(target)) {
|
|
62
|
+
errors.push(`❌ ${relativePath}: Phase "${phaseId}" conditional transition onSuccess[${branch}] -> "${target}" target not found.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Check Failure transition
|
|
68
|
+
if (flow.onFailure && !phases.has(flow.onFailure)) {
|
|
69
|
+
errors.push(`❌ ${relativePath}: Phase "${phaseId}" transition onFailure -> "${flow.onFailure}" target not found.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 3. Check for orphaned phases in markdown
|
|
73
|
+
for (const phaseId of phases.keys()) {
|
|
74
|
+
if (!metadata.phases[phaseId]) {
|
|
75
|
+
errors.push(`⚠️ ${relativePath}: Phase "${phaseId}" exists in markdown but is NOT defined in JSON metadata.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
errors.push(`❌ ${relativePath}: Unexpected error: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
valid: errors.filter(e => e.startsWith('❌')).length === 0,
|
|
85
|
+
errors
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Run if called directly
|
|
89
|
+
if (process.argv[1]?.includes('validate-workflows')) {
|
|
90
|
+
const registryPath = process.argv[2] || (0, path_1.join)(process.cwd(), 'registry');
|
|
91
|
+
const result = validateAllWorkflows(registryPath);
|
|
92
|
+
result.errors.forEach(err => console.error(err));
|
|
93
|
+
if (result.valid) {
|
|
94
|
+
console.log('✅ All workflows validated successfully.');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error('❌ Workflow validation failed.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -2,18 +2,33 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkflowParser = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
5
6
|
class WorkflowParser {
|
|
6
7
|
/**
|
|
7
8
|
* Parse a workflow markdown file into a structured definition
|
|
9
|
+
* Supports two formats:
|
|
10
|
+
* 1. Phase-based workflows with JSON frontmatter (e.g., implement, spec)
|
|
11
|
+
* 2. Simple workflows without frontmatter (e.g., bootstrap workflows)
|
|
8
12
|
*/
|
|
9
13
|
static parse(filePath) {
|
|
10
14
|
if (!(0, fs_1.existsSync)(filePath))
|
|
11
15
|
return null;
|
|
12
16
|
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
13
|
-
//
|
|
17
|
+
// Try to extract JSON Metadata (frontmatter)
|
|
14
18
|
const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
15
|
-
if (
|
|
16
|
-
|
|
19
|
+
if (metadataMatch) {
|
|
20
|
+
// Phase-based workflow with frontmatter
|
|
21
|
+
return this.parsePhaseBasedWorkflow(filePath, content, metadataMatch);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Simple workflow without frontmatter
|
|
25
|
+
return this.parseSimpleWorkflow(filePath, content);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a phase-based workflow with JSON frontmatter
|
|
30
|
+
*/
|
|
31
|
+
static parsePhaseBasedWorkflow(filePath, content, metadataMatch) {
|
|
17
32
|
let metadata;
|
|
18
33
|
try {
|
|
19
34
|
metadata = JSON.parse(metadataMatch[1]);
|
|
@@ -22,7 +37,7 @@ class WorkflowParser {
|
|
|
22
37
|
console.error(`❌ Failed to parse JSON metadata in ${filePath}:`, e);
|
|
23
38
|
return null;
|
|
24
39
|
}
|
|
25
|
-
//
|
|
40
|
+
// Extract Overview (Content after metadata but before first phase header)
|
|
26
41
|
const contentAfterMetadata = content.substring(metadataMatch[0].length).trim();
|
|
27
42
|
const firstPhaseIndex = contentAfterMetadata.search(/^##\s+Phase:/m);
|
|
28
43
|
let overview = '';
|
|
@@ -34,7 +49,7 @@ class WorkflowParser {
|
|
|
34
49
|
else {
|
|
35
50
|
overview = contentAfterMetadata;
|
|
36
51
|
}
|
|
37
|
-
//
|
|
52
|
+
// Extract Phases (id -> content)
|
|
38
53
|
const phases = new Map();
|
|
39
54
|
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
40
55
|
// Skip the first part (empty or overview overlap)
|
|
@@ -44,15 +59,32 @@ class WorkflowParser {
|
|
|
44
59
|
const firstLine = sectionLines[0].trim();
|
|
45
60
|
// Extract phase ID (slug before any (Phase X) or space)
|
|
46
61
|
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
47
|
-
//
|
|
48
|
-
// But usually, the agent wants the whole section including the Phase ID header.
|
|
49
|
-
// We'll store the whole section but prepend the header because split removed it.
|
|
62
|
+
// Store the whole section with header
|
|
50
63
|
phases.set(id, `## Phase: ${section.trim()}`);
|
|
51
64
|
}
|
|
52
65
|
return {
|
|
53
66
|
metadata,
|
|
54
67
|
overview,
|
|
55
|
-
phases
|
|
68
|
+
phases,
|
|
69
|
+
isSimple: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a simple workflow without frontmatter (bootstrap-style)
|
|
74
|
+
*/
|
|
75
|
+
static parseSimpleWorkflow(filePath, content) {
|
|
76
|
+
// Extract workflow name from filename
|
|
77
|
+
const workflowName = (0, path_1.basename)(filePath, '.md');
|
|
78
|
+
// For simple workflows, the entire content is the overview
|
|
79
|
+
// No phases, just execution steps
|
|
80
|
+
const metadata = {
|
|
81
|
+
name: workflowName
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
metadata,
|
|
85
|
+
overview: content.trim(),
|
|
86
|
+
phases: new Map(),
|
|
87
|
+
isSimple: true
|
|
56
88
|
};
|
|
57
89
|
}
|
|
58
90
|
/**
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.63",
|
|
4
4
|
"description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"release": "npm version patch && npm publish",
|
|
26
26
|
"test-smoke-ci": "tsx --test tests/test-genericization.ts tests/test-cli.ts tests/test-stub-registry.ts tests/test-sync-stubs.ts",
|
|
27
27
|
"test-all-ci": "tsx --test tests/test-*.ts",
|
|
28
|
-
"validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows",
|
|
29
|
-
"validate:workflows": "tsx scripts/validate-workflows.ts"
|
|
28
|
+
"validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:platform-agnostic",
|
|
29
|
+
"validate:workflows": "tsx scripts/validate-workflows.ts",
|
|
30
|
+
"validate:platform-agnostic": "tsx scripts/validate-platform-agnostic.ts"
|
|
30
31
|
},
|
|
31
32
|
"repository": {
|
|
32
33
|
"type": "git",
|
package/bin/fraim.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FRAIM Framework CLI Entry Point
|
|
5
|
-
*
|
|
6
|
-
* This file delegates to the compiled TypeScript implementation.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
// In production/installed package, code is in dist/
|
|
11
|
-
require('../dist/src/cli/fraim.js');
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error.code === 'MODULE_NOT_FOUND') {
|
|
14
|
-
// In development (local clone), we might use tsx if dist is missing
|
|
15
|
-
// But typically we should just tell user to build.
|
|
16
|
-
console.error('❌ Could not find FRAIM CLI implementation.');
|
|
17
|
-
console.error(' If you are running from source, please run "npm run build" first.');
|
|
18
|
-
console.error(` Error details: ${error.message}`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
} else {
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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.mcpCommand = void 0;
|
|
7
|
-
const commander_1 = require("commander");
|
|
8
|
-
const server_1 = require("../../local-mcp-server/server");
|
|
9
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
-
exports.mcpCommand = new commander_1.Command('mcp')
|
|
11
|
-
.description('Start the local FRAIM MCP server')
|
|
12
|
-
.option('-p, --port <port>', 'Port to run the server on', '3003')
|
|
13
|
-
.option('--remote-only', 'Use remote server only (skip local proxy)', false)
|
|
14
|
-
.action(async (options) => {
|
|
15
|
-
try {
|
|
16
|
-
if (options.remoteOnly) {
|
|
17
|
-
console.log(chalk_1.default.yellow('⚠️ Remote-only mode not yet implemented'));
|
|
18
|
-
console.log(chalk_1.default.blue('ℹ️ Starting local MCP server instead...'));
|
|
19
|
-
}
|
|
20
|
-
const port = parseInt(options.port);
|
|
21
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
22
|
-
console.error(chalk_1.default.red('❌ Invalid port number. Must be between 1 and 65535.'));
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
console.log(chalk_1.default.blue('🚀 Starting FRAIM Local MCP Server...'));
|
|
26
|
-
console.log(chalk_1.default.gray(`📡 Port: ${port}`));
|
|
27
|
-
console.log(chalk_1.default.gray(`🔄 Auto-updates: Every 5 minutes`));
|
|
28
|
-
console.log(chalk_1.default.gray(`📁 Cache: ~/.fraim/cache`));
|
|
29
|
-
console.log('');
|
|
30
|
-
// Set the port in environment
|
|
31
|
-
process.env.FRAIM_LOCAL_MCP_PORT = port.toString();
|
|
32
|
-
// Start the server
|
|
33
|
-
await (0, server_1.startLocalMCPServer)();
|
|
34
|
-
// Server is now running, show configuration instructions
|
|
35
|
-
console.log('');
|
|
36
|
-
console.log(chalk_1.default.green('✅ FRAIM Local MCP Server is running!'));
|
|
37
|
-
console.log('');
|
|
38
|
-
console.log(chalk_1.default.bold('📋 Agent Configuration:'));
|
|
39
|
-
console.log('');
|
|
40
|
-
console.log(chalk_1.default.gray('Add this to your agent\'s MCP configuration:'));
|
|
41
|
-
console.log('');
|
|
42
|
-
console.log(chalk_1.default.cyan(JSON.stringify({
|
|
43
|
-
"mcpServers": {
|
|
44
|
-
"fraim": {
|
|
45
|
-
"command": "fraim",
|
|
46
|
-
"args": ["mcp"],
|
|
47
|
-
"env": {}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}, null, 2)));
|
|
51
|
-
console.log('');
|
|
52
|
-
console.log(chalk_1.default.bold('🔗 Endpoints:'));
|
|
53
|
-
console.log(chalk_1.default.gray(` MCP: http://localhost:${port}/mcp`));
|
|
54
|
-
console.log(chalk_1.default.gray(` Health: http://localhost:${port}/health`));
|
|
55
|
-
console.log('');
|
|
56
|
-
console.log(chalk_1.default.bold('🛑 To stop the server:'));
|
|
57
|
-
console.log(chalk_1.default.gray(' Press Ctrl+C'));
|
|
58
|
-
console.log('');
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
console.error(chalk_1.default.red('❌ Failed to start Local MCP Server:'));
|
|
62
|
-
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
});
|