fraim-framework 2.0.100 → 2.0.102
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 +39 -17
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/init-project.js +6 -2
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/sync.js +12 -0
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/utils/agent-adapters.js +23 -3
- package/dist/src/cli/utils/fraim-gitignore.js +66 -15
- package/dist/src/cli/utils/version-utils.js +10 -7
- package/dist/src/core/config-loader.js +18 -0
- package/dist/src/core/utils/project-fraim-migration.js +12 -0
- package/dist/src/core/utils/workflow-parser.js +5 -3
- package/dist/src/local-mcp-server/stdio-server.js +298 -23
- package/dist/src/local-mcp-server/usage-collector.js +62 -51
- package/index.js +84 -84
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Current "vibe coding" frameworks are great at getting from idea to prototype. Th
|
|
|
8
8
|
|
|
9
9
|
AI agents are like brilliant but inexperienced developers. They need:
|
|
10
10
|
• Clear guardrails to prevent costly mistakes
|
|
11
|
-
• Structured
|
|
11
|
+
• Structured jobs, skills, and rules to avoid chaos
|
|
12
12
|
• Evidence-based validation (not "looks good" claims)
|
|
13
13
|
• Learning systems to improve over time
|
|
14
14
|
• Balance between determinism and creativity
|
|
@@ -23,7 +23,7 @@ FRAIM transforms you from a solo developer into an AI manager orchestrating mult
|
|
|
23
23
|
|
|
24
24
|
**Real Results:**
|
|
25
25
|
• Dramatic reduction in AI-generated code that needs rework
|
|
26
|
-
• Faster delivery through structured
|
|
26
|
+
• Faster delivery through structured jobs
|
|
27
27
|
• Higher test coverage through mandatory evidence collection
|
|
28
28
|
• Zero agent conflicts through phase-based coordination
|
|
29
29
|
|
|
@@ -44,11 +44,11 @@ R - Retrospectives: Continuous learning from experience
|
|
|
44
44
|
|
|
45
45
|
| **Human Development** | **AI Agent Development** | **FRAIM Solution** |
|
|
46
46
|
|----------------------|-------------------------|-------------------|
|
|
47
|
-
| **Code Reviews** | Random quality checks | Structured review
|
|
47
|
+
| **Code Reviews** | Random quality checks | Structured review jobs with evidence requirements |
|
|
48
48
|
| **Testing Standards** | "Looks good" claims | Mandatory test evidence with failure reproduction |
|
|
49
49
|
| **Team Coordination** | Agent conflicts and overlaps | Phase-based isolation with clear handoffs |
|
|
50
50
|
| **Learning Culture** | Repeated mistakes | Retrospective-driven improvement system |
|
|
51
|
-
| **Process Discipline** | Ad-hoc approaches | Proven
|
|
51
|
+
| **Process Discipline** | Ad-hoc approaches | Proven jobs, skills, and deterministic scripts from real projects |
|
|
52
52
|
| **Quality Gates** | Unreliable outcomes | Deterministic validation with rollback capabilities |
|
|
53
53
|
|
|
54
54
|
|
|
@@ -133,7 +133,7 @@ R - Retrospectives: Continuous learning from experience
|
|
|
133
133
|
|
|
134
134
|
#### 🔄 **Merge Requirements & Branch Safety**
|
|
135
135
|
**Problem**: Agents accidentally overwrite master branch or create merge conflicts
|
|
136
|
-
**Solution**: Mandatory rebase
|
|
136
|
+
**Solution**: Mandatory rebase discipline with conflict resolution patterns
|
|
137
137
|
```bash
|
|
138
138
|
# Before: Force pushes that destroy other work
|
|
139
139
|
# After: Rebase-on-master with force-with-lease for safety
|
|
@@ -159,7 +159,7 @@ R - Retrospectives: Continuous learning from experience
|
|
|
159
159
|
## 🚀 **Proven Benefits from Real Projects**
|
|
160
160
|
|
|
161
161
|
- **Dramatic reduction** in AI-generated code that needs rework through evidence-based validation
|
|
162
|
-
- **Faster delivery** through structured
|
|
162
|
+
- **Faster delivery** through structured jobs and clear handoffs
|
|
163
163
|
- **Higher test coverage** through mandatory testing guidelines and evidence collection
|
|
164
164
|
- **Zero agent conflicts** through phase-based isolation and coordination
|
|
165
165
|
- **Complete accountability** - agents fix their own mistakes with evidence
|
|
@@ -248,7 +248,7 @@ fraim setup --key=<your-fraim-key>
|
|
|
248
248
|
|
|
249
249
|
The setup command supports three modes:
|
|
250
250
|
|
|
251
|
-
**Conversational Mode**:
|
|
251
|
+
**Conversational Mode**: FRAIM job guidance only, no platform integration required
|
|
252
252
|
```bash
|
|
253
253
|
fraim setup --key=<your-fraim-key>
|
|
254
254
|
# Select "Conversational Mode" when prompted
|
|
@@ -299,19 +299,41 @@ fraim doctor --test-mcp # Test MCP server connections
|
|
|
299
299
|
fraim doctor # Diagnose configuration issues
|
|
300
300
|
|
|
301
301
|
# Sync and maintenance
|
|
302
|
-
fraim sync # Sync latest
|
|
302
|
+
fraim sync # Sync latest jobs, skills, rules, and templates
|
|
303
303
|
```
|
|
304
304
|
|
|
305
305
|
**💡 Pro Tip**: Use `fraim add-ide` when you install a new IDE after initial setup. It reuses your existing FRAIM and platform tokens, making it much faster than running full setup again.
|
|
306
306
|
|
|
307
|
+
### **Which Job Should I Run?**
|
|
308
|
+
|
|
309
|
+
FRAIM's primary execution unit is a **job**. Jobs define the phased path. Skills and rules support the job; they are not the thing you "run" first.
|
|
310
|
+
|
|
311
|
+
Use these defaults:
|
|
312
|
+
- `feature-specification` when the request is still fuzzy or needs clarified requirements, UX, or acceptance criteria.
|
|
313
|
+
- `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
|
|
314
|
+
- `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
|
|
315
|
+
- `test-execution` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
|
|
316
|
+
- `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
|
|
317
|
+
- `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
|
|
318
|
+
- `implementation-design-review` when you need to verify the code matches the approved technical design.
|
|
319
|
+
- `issue-retrospective` after the work is complete and you want durable learnings captured.
|
|
320
|
+
|
|
321
|
+
Typical path for a larger feature:
|
|
322
|
+
- `feature-specification` -> `technical-design` -> `feature-implementation` -> review job -> `issue-retrospective`
|
|
323
|
+
|
|
324
|
+
Typical path for a small bug fix:
|
|
325
|
+
- `feature-implementation` -> review job if needed -> `issue-retrospective`
|
|
326
|
+
|
|
327
|
+
Once FRAIM is connected in your IDE, ask your agent to `list FRAIM jobs` or name the specific job directly, for example: `Run the feature-implementation job for issue #123`.
|
|
328
|
+
|
|
307
329
|
### **🧩 Personalized Jobs, Skills, and Rules**
|
|
308
330
|
|
|
309
|
-
Project-specific customization now lives under
|
|
331
|
+
Project-specific customization now lives under `fraim/personalized-employee/`.
|
|
310
332
|
|
|
311
333
|
Recommended layout:
|
|
312
334
|
|
|
313
335
|
```text
|
|
314
|
-
|
|
336
|
+
fraim/
|
|
315
337
|
personalized-employee/
|
|
316
338
|
jobs/
|
|
317
339
|
skills/
|
|
@@ -327,12 +349,12 @@ fraim override --copy rules/engineering/architecture-standards.md
|
|
|
327
349
|
```
|
|
328
350
|
|
|
329
351
|
Guidance:
|
|
330
|
-
- Put phased job customizations in
|
|
331
|
-
- Put reusable local capability snippets in
|
|
332
|
-
- Put broad team conventions in
|
|
333
|
-
- Put local deliverable tweaks in
|
|
334
|
-
- Do not edit synced content under
|
|
335
|
-
- Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in
|
|
352
|
+
- Put phased job customizations in `fraim/personalized-employee/jobs/...`
|
|
353
|
+
- Put reusable local capability snippets in `fraim/personalized-employee/skills/...`
|
|
354
|
+
- Put broad team conventions in `fraim/personalized-employee/rules/...`
|
|
355
|
+
- Put local deliverable tweaks in `fraim/personalized-employee/templates/...`
|
|
356
|
+
- Do not edit synced content under `fraim/ai-employee/` or `fraim/ai-manager/`; `fraim sync` will overwrite it
|
|
357
|
+
- Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in `fraim/personalized-employee/`
|
|
336
358
|
|
|
337
359
|
### **🔧 Jira Integration Setup**
|
|
338
360
|
|
|
@@ -382,7 +404,7 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json # For Clau
|
|
|
382
404
|
## 🌟 **Why FRAIM is the Future**
|
|
383
405
|
|
|
384
406
|
### **1. Proven in Production**
|
|
385
|
-
Every rule,
|
|
407
|
+
Every rule, job, skill, and pattern has been tested in real projects. This isn't theoretical—it is battle-tested.
|
|
386
408
|
|
|
387
409
|
### **2. Enterprise Discipline**
|
|
388
410
|
The same rigor you'd apply to managing human developers, applied to AI agents.
|
package/bin/fraim.js
CHANGED
|
@@ -266,8 +266,12 @@ const runInitProject = async () => {
|
|
|
266
266
|
(0, project_bootstrap_1.recordPathStatus)(result, (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(dir), false);
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
270
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
271
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
272
|
+
}
|
|
273
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
274
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
271
275
|
}
|
|
272
276
|
const detection = (0, platform_detection_1.detectPlatformFromGit)();
|
|
273
277
|
if (detection.provider === 'github') {
|
|
@@ -594,7 +594,7 @@ const runSetup = async (options) => {
|
|
|
594
594
|
console.log(chalk_1.default.cyan('\n📝 For future projects:'));
|
|
595
595
|
console.log(chalk_1.default.cyan(' 1. cd into any project directory'));
|
|
596
596
|
console.log(chalk_1.default.cyan(' 2. Run: fraim init-project'));
|
|
597
|
-
console.log(chalk_1.default.cyan(' 3.
|
|
597
|
+
console.log(chalk_1.default.cyan(' 3. Ask your AI agent: "FRAIM was just installed. Read the FRAIM docs, explain what it can do for me, then run project-onboarding."'));
|
|
598
598
|
if (mode === 'integrated') {
|
|
599
599
|
const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
|
|
600
600
|
const unconfiguredProviders = allProviderIds.filter(id => !tokens[id]);
|
|
@@ -46,6 +46,7 @@ const version_utils_1 = require("../utils/version-utils");
|
|
|
46
46
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
47
47
|
const git_utils_1 = require("../../core/utils/git-utils");
|
|
48
48
|
const agent_adapters_1 = require("../utils/agent-adapters");
|
|
49
|
+
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
49
50
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
50
51
|
/**
|
|
51
52
|
* Load API key from user-level config (~/.fraim/config.json)
|
|
@@ -86,6 +87,15 @@ const runSync = async (options) => {
|
|
|
86
87
|
const projectRoot = process.cwd();
|
|
87
88
|
const config = (0, config_loader_1.loadFraimConfig)();
|
|
88
89
|
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
90
|
+
const refreshLocalIgnoreConfig = () => {
|
|
91
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
92
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
93
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
94
|
+
}
|
|
95
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
96
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
89
99
|
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
90
100
|
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
91
101
|
if (isGlobal && !options.skipUpdates) {
|
|
@@ -106,6 +116,7 @@ const runSync = async (options) => {
|
|
|
106
116
|
if (result.success) {
|
|
107
117
|
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
|
|
108
118
|
updateVersionInConfig(fraimDir);
|
|
119
|
+
refreshLocalIgnoreConfig();
|
|
109
120
|
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
|
|
110
121
|
if (adapterUpdates.length > 0) {
|
|
111
122
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
@@ -148,6 +159,7 @@ const runSync = async (options) => {
|
|
|
148
159
|
}
|
|
149
160
|
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
|
|
150
161
|
updateVersionInConfig(fraimDir);
|
|
162
|
+
refreshLocalIgnoreConfig();
|
|
151
163
|
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
|
|
152
164
|
if (adapterUpdates.length > 0) {
|
|
153
165
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
@@ -0,0 +1,83 @@
|
|
|
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.DeviceFlowService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
class DeviceFlowService {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start the Device Flow Login
|
|
15
|
+
*/
|
|
16
|
+
async login() {
|
|
17
|
+
console.log(chalk_1.default.blue('\n🔗 Starting Authentication...'));
|
|
18
|
+
try {
|
|
19
|
+
// 1. Request device and user codes
|
|
20
|
+
const deviceCode = await this.requestDeviceCode();
|
|
21
|
+
console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
|
|
22
|
+
console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
|
|
23
|
+
console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
|
|
24
|
+
console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
|
|
25
|
+
// 2. Poll for the access token
|
|
26
|
+
const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
|
|
27
|
+
console.log(chalk_1.default.green('\n✅ Authentication Successful!'));
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk_1.default.red(`\n❌ Authentication failed: ${error.message}`));
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async requestDeviceCode() {
|
|
36
|
+
const response = await axios_1.default.post(this.config.authUrl, {
|
|
37
|
+
client_id: this.config.clientId,
|
|
38
|
+
scope: this.config.scope
|
|
39
|
+
}, {
|
|
40
|
+
headers: { Accept: 'application/json' }
|
|
41
|
+
});
|
|
42
|
+
return response.data;
|
|
43
|
+
}
|
|
44
|
+
async pollForToken(deviceCode, interval) {
|
|
45
|
+
let currentInterval = interval * 1000;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const poll = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios_1.default.post(this.config.tokenUrl, {
|
|
50
|
+
client_id: this.config.clientId,
|
|
51
|
+
device_code: deviceCode,
|
|
52
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
53
|
+
}, {
|
|
54
|
+
headers: { Accept: 'application/json' }
|
|
55
|
+
});
|
|
56
|
+
if (response.data.access_token) {
|
|
57
|
+
resolve(response.data.access_token);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (response.data.error) {
|
|
61
|
+
const error = response.data.error;
|
|
62
|
+
if (error === 'authorization_pending') {
|
|
63
|
+
// Keep polling
|
|
64
|
+
setTimeout(poll, currentInterval);
|
|
65
|
+
}
|
|
66
|
+
else if (error === 'slow_down') {
|
|
67
|
+
currentInterval += 5000;
|
|
68
|
+
setTimeout(poll, currentInterval);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject(new Error(response.data.error_description || error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
setTimeout(poll, currentInterval);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.DeviceFlowService = DeviceFlowService;
|
|
@@ -10,6 +10,7 @@ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
|
10
10
|
const START_MARKER = '<!-- FRAIM_AGENT_ADAPTER_START -->';
|
|
11
11
|
const END_MARKER = '<!-- FRAIM_AGENT_ADAPTER_END -->';
|
|
12
12
|
const CURSOR_RULE_PATH = path_1.default.join('.cursor', 'rules', 'fraim.mdc');
|
|
13
|
+
const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fraim.md');
|
|
13
14
|
const CURSOR_FRONTMATTER = `---
|
|
14
15
|
description: FRAIM discovery and execution contract
|
|
15
16
|
alwaysApply: true
|
|
@@ -59,9 +60,13 @@ This repository uses FRAIM.
|
|
|
59
60
|
- Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
|
|
60
61
|
- Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
|
|
61
62
|
- Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
|
|
62
|
-
-
|
|
63
|
+
- Before acting on any user request, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections to match the request to the right job.
|
|
64
|
+
- Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
|
|
63
65
|
- For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
|
|
64
66
|
- Read \`${projectRulesPath}\` if it exists before doing work.
|
|
67
|
+
|
|
68
|
+
> [!IMPORTANT]
|
|
69
|
+
> **Job stubs are for discovery only.** When a user @mentions or references any file under \`${employeeJobsPath}/\` or \`${managerJobsPath}/\`, do NOT attempt to execute the job from the stub content. The stub only shows intent and phase names. Always call \`get_fraim_job({ job: "<job-name>" })\` first to get the full phased instructions before doing any work.
|
|
65
70
|
`);
|
|
66
71
|
const cursorManagedBody = buildManagedSection(`
|
|
67
72
|
Use FRAIM as the repo's execution framework.
|
|
@@ -72,6 +77,7 @@ Use FRAIM as the repo's execution framework.
|
|
|
72
77
|
- Rules are always-on constraints.
|
|
73
78
|
- Repo-specific overrides and learnings under \`${personalizedRootPath}/\` take precedence.
|
|
74
79
|
- Choose a relevant job from the stubs, then call \`get_fraim_job(...)\` for the full phased instructions.
|
|
80
|
+
- **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
75
81
|
`);
|
|
76
82
|
const copilotBody = buildManagedSection(`
|
|
77
83
|
## FRAIM
|
|
@@ -82,6 +88,7 @@ Use FRAIM as the repo's execution framework.
|
|
|
82
88
|
- FRAIM rules are always-on constraints and conventions.
|
|
83
89
|
- Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
|
|
84
90
|
- Use the stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
|
|
91
|
+
- **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
85
92
|
`);
|
|
86
93
|
const fraimReadme = `# FRAIM Catalog
|
|
87
94
|
|
|
@@ -94,13 +101,26 @@ This directory is the repository-visible FRAIM surface.
|
|
|
94
101
|
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
95
102
|
|
|
96
103
|
Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools.
|
|
104
|
+
`;
|
|
105
|
+
const claudeCommand = `The user wants to run FRAIM. The requested job or topic is: $ARGUMENTS
|
|
106
|
+
|
|
107
|
+
Follow this process:
|
|
108
|
+
|
|
109
|
+
1. **If no argument was given** (the line above ends with ": "): scan all stub files under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\`. List each by filename and its Intent line. Ask the user which job they want to run, then proceed to step 2.
|
|
110
|
+
|
|
111
|
+
2. **Find the job**: search \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` for a stub whose filename (without \`.md\`) matches or closely resembles the argument. Read the stub's Intent/Outcome to confirm it matches what the user wants.
|
|
112
|
+
|
|
113
|
+
3. **Load the full job**: call \`get_fraim_job({ job: "<matched-job-name>" })\` — never execute from stub content.
|
|
114
|
+
|
|
115
|
+
4. **Execute**: follow the phased instructions returned by \`get_fraim_job\`, using \`seekMentoring\` at phase transitions where indicated.
|
|
97
116
|
`;
|
|
98
117
|
return [
|
|
99
118
|
{ path: 'AGENTS.md', content: markdownBody },
|
|
100
119
|
{ path: 'CLAUDE.md', content: markdownBody },
|
|
101
120
|
{ path: path_1.default.join('.github', 'copilot-instructions.md'), content: copilotBody },
|
|
102
121
|
{ path: CURSOR_RULE_PATH, content: cursorManagedBody },
|
|
103
|
-
{ path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme }
|
|
122
|
+
{ path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme },
|
|
123
|
+
{ path: CLAUDE_FRAIM_COMMAND_PATH, content: claudeCommand }
|
|
104
124
|
];
|
|
105
125
|
}
|
|
106
126
|
function ensureAgentAdapterFiles(projectRoot) {
|
|
@@ -114,7 +134,7 @@ function ensureAgentAdapterFiles(projectRoot) {
|
|
|
114
134
|
const existing = fs_1.default.existsSync(fullPath) ? fs_1.default.readFileSync(fullPath, 'utf8') : '';
|
|
115
135
|
const next = file.path === CURSOR_RULE_PATH
|
|
116
136
|
? mergeCursorRule(existing, file.content)
|
|
117
|
-
: file.path.endsWith('README.md')
|
|
137
|
+
: file.path.endsWith('README.md') || file.path === CLAUDE_FRAIM_COMMAND_PATH
|
|
118
138
|
? file.content
|
|
119
139
|
: mergeManagedSection(existing, file.content);
|
|
120
140
|
if (existing !== next) {
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.ensureFraimSyncedContentLocallyExcluded = exports.removeFraimSyncedContentGitignoreBlock = exports.FRAIM_SYNC_GITIGNORE_ENTRIES = exports.FRAIM_SYNC_GITIGNORE_END = exports.FRAIM_SYNC_GITIGNORE_START = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
|
|
@@ -14,27 +14,78 @@ exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
|
|
|
14
14
|
'fraim/docs/',
|
|
15
15
|
];
|
|
16
16
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const managedBlock = `${exports.FRAIM_SYNC_GITIGNORE_START}\n${exports.FRAIM_SYNC_GITIGNORE_ENTRIES.join('\n')}\n${exports.FRAIM_SYNC_GITIGNORE_END}`;
|
|
25
|
-
const blockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
26
|
-
const withoutManagedBlock = normalized.replace(blockPattern, '\n').trimEnd();
|
|
17
|
+
const managedBlockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
18
|
+
function buildManagedBlock() {
|
|
19
|
+
return `${exports.FRAIM_SYNC_GITIGNORE_START}\n${exports.FRAIM_SYNC_GITIGNORE_ENTRIES.join('\n')}\n${exports.FRAIM_SYNC_GITIGNORE_END}`;
|
|
20
|
+
}
|
|
21
|
+
function normalizeWithoutManagedBlock(raw) {
|
|
22
|
+
const normalized = raw.replace(/\r\n/g, '\n');
|
|
23
|
+
const withoutManagedBlock = normalized.replace(managedBlockPattern, '\n').trimEnd();
|
|
27
24
|
const cleaned = withoutManagedBlock
|
|
28
25
|
.replace(/\n{3,}/g, '\n\n')
|
|
29
26
|
.replace(/^\n+/, '')
|
|
30
27
|
.trimEnd();
|
|
28
|
+
return cleaned;
|
|
29
|
+
}
|
|
30
|
+
function resolveGitInfoExcludePath(projectRoot) {
|
|
31
|
+
const gitPath = path_1.default.join(projectRoot, '.git');
|
|
32
|
+
if (!fs_1.default.existsSync(gitPath)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (fs_1.default.statSync(gitPath).isDirectory()) {
|
|
36
|
+
return path_1.default.join(gitPath, 'info', 'exclude');
|
|
37
|
+
}
|
|
38
|
+
const gitPointer = fs_1.default.readFileSync(gitPath, 'utf8');
|
|
39
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)\s*$/im);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const gitDir = path_1.default.isAbsolute(match[1])
|
|
44
|
+
? match[1]
|
|
45
|
+
: path_1.default.resolve(projectRoot, match[1]);
|
|
46
|
+
return path_1.default.join(gitDir, 'info', 'exclude');
|
|
47
|
+
}
|
|
48
|
+
const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
|
|
49
|
+
const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
|
|
50
|
+
if (!fs_1.default.existsSync(gitignorePath)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const existingRaw = fs_1.default.readFileSync(gitignorePath, 'utf8');
|
|
54
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
55
|
+
const cleaned = normalizeWithoutManagedBlock(existingRaw);
|
|
56
|
+
if (cleaned === normalized.trimEnd()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
60
|
+
const next = cleaned.length > 0 ? `${cleaned}\n` : '';
|
|
61
|
+
fs_1.default.writeFileSync(gitignorePath, next.replace(/\n/g, newline), 'utf8');
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
exports.removeFraimSyncedContentGitignoreBlock = removeFraimSyncedContentGitignoreBlock;
|
|
65
|
+
const ensureFraimSyncedContentLocallyExcluded = (projectRoot) => {
|
|
66
|
+
const gitignoreUpdated = (0, exports.removeFraimSyncedContentGitignoreBlock)(projectRoot);
|
|
67
|
+
const excludePath = resolveGitInfoExcludePath(projectRoot);
|
|
68
|
+
if (!excludePath) {
|
|
69
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: false };
|
|
70
|
+
}
|
|
71
|
+
const infoDir = path_1.default.dirname(excludePath);
|
|
72
|
+
if (!fs_1.default.existsSync(infoDir)) {
|
|
73
|
+
fs_1.default.mkdirSync(infoDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
const existingRaw = fs_1.default.existsSync(excludePath)
|
|
76
|
+
? fs_1.default.readFileSync(excludePath, 'utf8')
|
|
77
|
+
: '';
|
|
78
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
79
|
+
const cleaned = normalizeWithoutManagedBlock(existingRaw);
|
|
80
|
+
const managedBlock = buildManagedBlock();
|
|
31
81
|
const next = cleaned.length > 0
|
|
32
82
|
? `${cleaned}\n\n${managedBlock}\n`
|
|
33
83
|
: `${managedBlock}\n`;
|
|
34
84
|
if (next !== normalized) {
|
|
35
|
-
|
|
36
|
-
|
|
85
|
+
const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
86
|
+
fs_1.default.writeFileSync(excludePath, next.replace(/\n/g, newline), 'utf8');
|
|
87
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: true };
|
|
37
88
|
}
|
|
38
|
-
return false;
|
|
89
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: false };
|
|
39
90
|
};
|
|
40
|
-
exports.
|
|
91
|
+
exports.ensureFraimSyncedContentLocallyExcluded = ensureFraimSyncedContentLocallyExcluded;
|
|
@@ -10,12 +10,11 @@ function getFraimVersion() {
|
|
|
10
10
|
// Try reliable paths to find package.json relative to this file
|
|
11
11
|
// locally: src/cli/utils/version-utils.ts -> package.json is ../../../package.json
|
|
12
12
|
// dist: dist/src/cli/utils/version-utils.js -> package.json is ../../../../package.json
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for (const pkgPath of possiblePaths) {
|
|
13
|
+
// Traverse up from __dirname until we find fraim-framework's package.json.
|
|
14
|
+
// Fixed relative paths break when npx cache layouts differ across OS/npm versions.
|
|
15
|
+
let dir = __dirname;
|
|
16
|
+
for (let i = 0; i < 10; i++) {
|
|
17
|
+
const pkgPath = path_1.default.join(dir, 'package.json');
|
|
19
18
|
if (fs_1.default.existsSync(pkgPath)) {
|
|
20
19
|
try {
|
|
21
20
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -27,6 +26,10 @@ function getFraimVersion() {
|
|
|
27
26
|
// Ignore parsing errors
|
|
28
27
|
}
|
|
29
28
|
}
|
|
29
|
+
const parent = path_1.default.dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
break; // Reached filesystem root
|
|
32
|
+
dir = parent;
|
|
30
33
|
}
|
|
31
|
-
return '
|
|
34
|
+
return 'unknown'; // Do not return a fake version — unknown is safer than misleading
|
|
32
35
|
}
|
|
@@ -11,6 +11,20 @@ exports.getRepositoryInfo = getRepositoryInfo;
|
|
|
11
11
|
const fs_1 = require("fs");
|
|
12
12
|
const types_1 = require("./types");
|
|
13
13
|
const project_fraim_paths_1 = require("./utils/project-fraim-paths");
|
|
14
|
+
function normalizeCustomerCommunication(config) {
|
|
15
|
+
const current = config?.['customer-communication'];
|
|
16
|
+
if (!current || typeof current !== 'object')
|
|
17
|
+
return undefined;
|
|
18
|
+
return {
|
|
19
|
+
productName: current.productName,
|
|
20
|
+
productUrl: current.productUrl,
|
|
21
|
+
senderDisplayName: current.senderDisplayName,
|
|
22
|
+
senderEmail: current.senderEmail,
|
|
23
|
+
senderReplyTo: current.senderReplyTo,
|
|
24
|
+
newsletterAudienceProvider: current.newsletterAudienceProvider,
|
|
25
|
+
deliveryProvider: current.deliveryProvider
|
|
26
|
+
};
|
|
27
|
+
}
|
|
14
28
|
function normalizeFraimConfig(config) {
|
|
15
29
|
// Handle backward compatibility and migration
|
|
16
30
|
const mergedConfig = {
|
|
@@ -52,6 +66,10 @@ function normalizeFraimConfig(config) {
|
|
|
52
66
|
if (config.competitors && typeof config.competitors === 'object') {
|
|
53
67
|
mergedConfig.competitors = config.competitors;
|
|
54
68
|
}
|
|
69
|
+
const customerCommunication = normalizeCustomerCommunication(config);
|
|
70
|
+
if (customerCommunication) {
|
|
71
|
+
mergedConfig.customerCommunication = customerCommunication;
|
|
72
|
+
}
|
|
55
73
|
return mergedConfig;
|
|
56
74
|
}
|
|
57
75
|
/**
|
|
@@ -90,6 +90,11 @@ function migrateLegacyProjectFraimDir(projectRoot = process.cwd()) {
|
|
|
90
90
|
for (const entry of MIGRATABLE_LEGACY_ENTRIES) {
|
|
91
91
|
moveOrMergePath(path_1.default.join(legacyDirPath, entry), path_1.default.join(workspaceDirPath, entry), entry, result);
|
|
92
92
|
}
|
|
93
|
+
// Handle learned-skills -> personalized-employee/skills mapping
|
|
94
|
+
const legacyLearnedSkillsPath = path_1.default.join(legacyDirPath, 'learned-skills');
|
|
95
|
+
if (fs_1.default.existsSync(legacyLearnedSkillsPath)) {
|
|
96
|
+
moveOrMergePath(legacyLearnedSkillsPath, path_1.default.join(workspaceDirPath, 'personalized-employee', 'skills'), 'learned-skills', result);
|
|
97
|
+
}
|
|
93
98
|
for (const entry of PRUNABLE_LEGACY_SYNCED_ENTRIES) {
|
|
94
99
|
const legacyEntryPath = path_1.default.join(legacyDirPath, entry);
|
|
95
100
|
if (!fs_1.default.existsSync(legacyEntryPath)) {
|
|
@@ -98,6 +103,13 @@ function migrateLegacyProjectFraimDir(projectRoot = process.cwd()) {
|
|
|
98
103
|
fs_1.default.rmSync(legacyEntryPath, { recursive: true, force: true });
|
|
99
104
|
result.prunedLegacyPaths.push(entry);
|
|
100
105
|
}
|
|
106
|
+
// Final check for entries that are neither migratable nor prunable
|
|
107
|
+
const remainingEntries = fs_1.default.readdirSync(legacyDirPath);
|
|
108
|
+
const coreEntries = [...MIGRATABLE_LEGACY_ENTRIES, ...PRUNABLE_LEGACY_SYNCED_ENTRIES, 'learned-skills'];
|
|
109
|
+
const unknownEntries = remainingEntries.filter(e => !coreEntries.includes(e));
|
|
110
|
+
if (unknownEntries.length > 0) {
|
|
111
|
+
result.conflictPaths.push(...unknownEntries);
|
|
112
|
+
}
|
|
101
113
|
result.legacyDirReadyForDeletion = result.conflictPaths.length === 0 && isDirectoryEmpty(legacyDirPath);
|
|
102
114
|
return result;
|
|
103
115
|
}
|
|
@@ -5,13 +5,15 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
7
|
static extractMetadataBlock(content) {
|
|
8
|
-
|
|
8
|
+
// Allow leading comments and whitespace before frontmatter
|
|
9
|
+
const frontmatterMatch = content.match(/^[\s\S]*?---\r?\n([\s\S]+?)\r?\n---/);
|
|
9
10
|
if (frontmatterMatch) {
|
|
10
11
|
try {
|
|
12
|
+
const startIndex = frontmatterMatch.index || 0;
|
|
11
13
|
return {
|
|
12
14
|
state: 'valid',
|
|
13
15
|
metadata: JSON.parse(frontmatterMatch[1]),
|
|
14
|
-
bodyStartIndex: frontmatterMatch[0].length
|
|
16
|
+
bodyStartIndex: startIndex + frontmatterMatch[0].length
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
@@ -107,7 +109,7 @@ class WorkflowParser {
|
|
|
107
109
|
overview = contentAfterMetadata;
|
|
108
110
|
}
|
|
109
111
|
const phases = new Map();
|
|
110
|
-
const phaseSections = restOfContent.split(/^##\s+Phase:\s
|
|
112
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s*/im);
|
|
111
113
|
if (!metadata.phases) {
|
|
112
114
|
metadata.phases = {};
|
|
113
115
|
}
|