fraim-framework 2.0.95 → 2.0.97

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.
Files changed (40) hide show
  1. package/bin/fraim.js +1 -1
  2. package/dist/src/cli/commands/add-ide.js +1 -1
  3. package/dist/src/cli/commands/doctor.js +6 -6
  4. package/dist/src/cli/commands/init-project.js +63 -52
  5. package/dist/src/cli/commands/list-overridable.js +33 -55
  6. package/dist/src/cli/commands/list.js +35 -9
  7. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  8. package/dist/src/cli/commands/override.js +18 -39
  9. package/dist/src/cli/commands/setup.js +1 -1
  10. package/dist/src/cli/commands/sync.js +34 -27
  11. package/dist/src/cli/doctor/check-runner.js +3 -3
  12. package/dist/src/cli/doctor/checks/global-setup-checks.js +13 -13
  13. package/dist/src/cli/doctor/checks/project-setup-checks.js +12 -12
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +2 -2
  15. package/dist/src/cli/doctor/checks/workflow-checks.js +56 -60
  16. package/dist/src/cli/doctor/reporters/console-reporter.js +1 -1
  17. package/dist/src/cli/fraim.js +3 -1
  18. package/dist/src/cli/mcp/mcp-server-registry.js +1 -1
  19. package/dist/src/cli/services/device-flow-service.js +83 -0
  20. package/dist/src/cli/setup/auto-mcp-setup.js +2 -2
  21. package/dist/src/cli/setup/first-run.js +4 -3
  22. package/dist/src/cli/utils/agent-adapters.js +126 -0
  23. package/dist/src/cli/utils/fraim-gitignore.js +15 -21
  24. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  25. package/dist/src/cli/utils/remote-sync.js +20 -67
  26. package/dist/src/core/ai-mentor.js +31 -49
  27. package/dist/src/core/config-loader.js +57 -62
  28. package/dist/src/core/config-writer.js +75 -0
  29. package/dist/src/core/types.js +1 -1
  30. package/dist/src/core/utils/job-parser.js +176 -0
  31. package/dist/src/core/utils/local-registry-resolver.js +61 -71
  32. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  33. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  34. package/dist/src/core/utils/stub-generator.js +41 -75
  35. package/dist/src/core/utils/workflow-parser.js +5 -3
  36. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  37. package/dist/src/local-mcp-server/stdio-server.js +124 -51
  38. package/dist/src/local-mcp-server/usage-collector.js +109 -27
  39. package/index.js +1 -1
  40. package/package.json +3 -4
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateWorkflowStub = generateWorkflowStub;
4
- exports.parseRegistryWorkflow = parseRegistryWorkflow;
5
3
  exports.generateJobStub = generateJobStub;
6
4
  exports.generateSkillStub = generateSkillStub;
7
5
  exports.generateRuleStub = generateRuleStub;
@@ -46,54 +44,22 @@ function extractLeadParagraph(content) {
46
44
  return paragraphLines.join(' ').trim();
47
45
  }
48
46
  /**
49
- * Generates a lightweight markdown stub for a workflow.
50
- * These stubs are committed to the user's repo for discoverability.
47
+ * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
51
48
  */
52
- function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
53
- return `${STUB_MARKER}
54
- # FRAIM Workflow: ${workflowName}
55
-
56
- > [!IMPORTANT]
57
- > This is a **FRAIM-managed workflow stub**.
58
- > To load the full context (rules, templates, and execution steps), ask your AI agent to:
59
- > \`@fraim get_fraim_workflow("${workflowName}")\`
60
- >
61
- > DO NOT EXECUTE.
49
+ function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
50
+ return `${STUB_MARKER}
51
+ # FRAIM Job: ${jobName}
62
52
 
63
53
  ## Intent
64
54
  ${intent}
65
- `;
66
- }
67
- /**
68
- * Parses a workflow file from the registry to extract its intent and principles for the stub.
69
- */
70
- function parseRegistryWorkflow(content) {
71
- // Case-insensitive, handles different line endings, optional following header or EOF
72
- const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
73
- const principlesMatch = content.match(/##\s*principles\s+([\s\S]*?)(?=\n##|$)/i);
74
- const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
75
- const principles = principlesMatch
76
- ? principlesMatch[1].trim().split(/\r?\n/).map(l => l.replace(/^- /, '').trim()).filter(l => l)
77
- : [];
78
- return { intent, principles };
79
- }
80
- /**
81
- * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
82
- */
83
- function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
84
- return `${STUB_MARKER}
85
- # FRAIM Job: ${jobName}
86
-
87
- ## Intent
88
- ${intent}
89
-
90
- ## Outcome
91
- ${outcome}
92
-
93
- ## Steps
94
- ${steps}
95
-
96
- ---
55
+
56
+ ## Outcome
57
+ ${outcome}
58
+
59
+ ## Steps
60
+ ${steps}
61
+
62
+ ---
97
63
 
98
64
  > [!IMPORTANT]
99
65
  > **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
@@ -110,41 +76,41 @@ ${steps}
110
76
  * Generates a lightweight markdown stub for a skill.
111
77
  */
112
78
  function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
113
- return `${STUB_MARKER}
114
- # FRAIM Skill: ${skillName}
115
-
116
- ## Skill Input
117
- ${skillInput}
118
-
119
- ## Skill Output
120
- ${skillOutput}
121
-
122
- ---
123
-
124
- > [!IMPORTANT]
125
- > **For AI Agents:** This is a discoverability stub for the skill.
126
- > All execution details must be fetched from MCP before use.
127
- > To retrieve the complete skill instructions, call:
128
- > \`get_fraim_file({ path: "skills/${skillPath}" })\`
79
+ return `${STUB_MARKER}
80
+ # FRAIM Skill: ${skillName}
81
+
82
+ ## Skill Input
83
+ ${skillInput}
84
+
85
+ ## Skill Output
86
+ ${skillOutput}
87
+
88
+ ---
89
+
90
+ > [!IMPORTANT]
91
+ > **For AI Agents:** This is a discoverability stub for the skill.
92
+ > All execution details must be fetched from MCP before use.
93
+ > To retrieve the complete skill instructions, call:
94
+ > \`get_fraim_file({ path: "skills/${skillPath}" })\`
129
95
  `;
130
96
  }
131
97
  /**
132
98
  * Generates a lightweight markdown stub for a rule.
133
99
  */
134
100
  function generateRuleStub(ruleName, rulePath, intent) {
135
- return `${STUB_MARKER}
136
- # FRAIM Rule: ${ruleName}
137
-
138
- ## Intent
139
- ${intent}
140
-
141
- ---
142
-
143
- > [!IMPORTANT]
144
- > **For AI Agents:** This is a discoverability stub for the rule.
145
- > All rule details must be fetched from MCP before use.
146
- > To retrieve the complete rule instructions, call:
147
- > \`get_fraim_file({ path: "rules/${rulePath}" })\`
101
+ return `${STUB_MARKER}
102
+ # FRAIM Rule: ${ruleName}
103
+
104
+ ## Intent
105
+ ${intent}
106
+
107
+ ---
108
+
109
+ > [!IMPORTANT]
110
+ > **For AI Agents:** This is a discoverability stub for the rule.
111
+ > All rule details must be fetched from MCP before use.
112
+ > To retrieve the complete rule instructions, call:
113
+ > \`get_fraim_file({ path: "rules/${rulePath}" })\`
148
114
  `;
149
115
  }
150
116
  /**
@@ -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
- const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
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+/m);
112
+ const phaseSections = restOfContent.split(/^##\s+Phase:\s*/im);
111
113
  if (!metadata.phases) {
112
114
  metadata.phases = {};
113
115
  }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ /**
3
+ * LearningContextBuilder builds the learning context section for a user.
4
+ *
5
+ * Lives in the local proxy so learning files are always read from the correct
6
+ * workspace root on the user's machine.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.buildLearningContextSection = buildLearningContextSection;
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
12
+ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
13
+ const LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
14
+ const DEFAULT_THRESHOLD = 3.0;
15
+ function getLearningsBase(workspaceRoot) {
16
+ return (0, path_1.join)(workspaceRoot, LEARNINGS_REL);
17
+ }
18
+ function getScoreThreshold(workspaceRoot) {
19
+ try {
20
+ const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
21
+ if ((0, fs_1.existsSync)(configPath)) {
22
+ const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
23
+ const t = config?.learning?.scoreThreshold;
24
+ if (typeof t === 'number' && t > 0)
25
+ return t;
26
+ }
27
+ }
28
+ catch {
29
+ // Fall through to default.
30
+ }
31
+ return DEFAULT_THRESHOLD;
32
+ }
33
+ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType) {
34
+ const baseScore = severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
35
+ const halfLife = fileType === 'mistake-patterns' ? 90 : 180;
36
+ let daysSinceLastSeen = 0;
37
+ try {
38
+ const lastSeen = new Date(lastSeenDate);
39
+ const now = new Date();
40
+ daysSinceLastSeen = Math.max(0, (now.getTime() - lastSeen.getTime()) / (1000 * 60 * 60 * 24));
41
+ }
42
+ catch {
43
+ // Treat as fresh.
44
+ }
45
+ const decay = Math.pow(0.5, daysSinceLastSeen / halfLife);
46
+ const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
47
+ return baseScore * decay * recurrenceBoost;
48
+ }
49
+ function countMistakePatternEntries(filePath, threshold) {
50
+ if (!(0, fs_1.existsSync)(filePath))
51
+ return { active: 0, dormant: 0 };
52
+ try {
53
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
54
+ const lines = content.split('\n');
55
+ let active = 0;
56
+ let dormant = 0;
57
+ let inEntry = false;
58
+ let currentSeverity = null;
59
+ let currentLastSeen = '';
60
+ let currentRecurrences = 1;
61
+ const processCurrentEntry = () => {
62
+ if (!currentSeverity)
63
+ return;
64
+ const score = computeEffectiveScore(currentSeverity, currentLastSeen, currentRecurrences, 'mistake-patterns');
65
+ if (score >= threshold)
66
+ active++;
67
+ else
68
+ dormant++;
69
+ };
70
+ for (const line of lines) {
71
+ const headerMatch = line.match(/^## \[(P-HIGH|P-MED|P-LOW)\]/);
72
+ if (headerMatch) {
73
+ processCurrentEntry();
74
+ inEntry = true;
75
+ currentSeverity = headerMatch[1];
76
+ currentLastSeen = '';
77
+ currentRecurrences = 1;
78
+ continue;
79
+ }
80
+ if (!inEntry)
81
+ continue;
82
+ const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
83
+ if (lastSeenMatch) {
84
+ currentLastSeen = lastSeenMatch[1].trim();
85
+ continue;
86
+ }
87
+ const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
88
+ if (recurrenceMatch) {
89
+ currentRecurrences = parseInt(recurrenceMatch[1], 10);
90
+ }
91
+ }
92
+ processCurrentEntry();
93
+ return { active, dormant };
94
+ }
95
+ catch {
96
+ return { active: 0, dormant: 0 };
97
+ }
98
+ }
99
+ function readFrontmatter(content) {
100
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
101
+ if (!match)
102
+ return {};
103
+ const frontmatter = {};
104
+ for (const line of match[1].split(/\r?\n/)) {
105
+ const separator = line.indexOf(':');
106
+ if (separator === -1)
107
+ continue;
108
+ const key = line.slice(0, separator).trim();
109
+ const value = line.slice(separator + 1).trim();
110
+ if (key) {
111
+ frontmatter[key] = value;
112
+ }
113
+ }
114
+ return frontmatter;
115
+ }
116
+ function isUnsynthesizedRetrospective(filePath) {
117
+ try {
118
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
119
+ const frontmatter = readFrontmatter(content);
120
+ const synthesized = frontmatter.synthesized?.toLowerCase();
121
+ return synthesized !== 'true' && synthesized !== 'yes';
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ function buildLearningContextSection(workspaceRoot, userId, forJob) {
128
+ const learningsBase = getLearningsBase(workspaceRoot);
129
+ const threshold = getScoreThreshold(workspaceRoot);
130
+ const l2MistakePath = (0, path_1.join)(learningsBase, 'org-mistake-patterns.md');
131
+ const l2PrefPath = (0, path_1.join)(learningsBase, 'org-preferences.md');
132
+ const l2CoachPath = (0, path_1.join)(learningsBase, 'org-manager-coaching.md');
133
+ const l2MistakePresent = (0, fs_1.existsSync)(l2MistakePath);
134
+ const l2PrefPresent = (0, fs_1.existsSync)(l2PrefPath);
135
+ const l2CoachPresent = (0, fs_1.existsSync)(l2CoachPath);
136
+ const l2MistakeCounts = l2MistakePresent ? countMistakePatternEntries(l2MistakePath, threshold) : null;
137
+ const l1MistakePath = (0, path_1.join)(learningsBase, `${userId}-mistake-patterns.md`);
138
+ const l1PrefPath = (0, path_1.join)(learningsBase, `${userId}-preferences.md`);
139
+ const l1CoachPath = (0, path_1.join)(learningsBase, `${userId}-manager-coaching.md`);
140
+ const l1MistakePresent = (0, fs_1.existsSync)(l1MistakePath);
141
+ const l1PrefPresent = (0, fs_1.existsSync)(l1PrefPath);
142
+ const l1CoachPresent = (0, fs_1.existsSync)(l1CoachPath);
143
+ const l1MistakeCounts = l1MistakePresent ? countMistakePatternEntries(l1MistakePath, threshold) : null;
144
+ let l0CoachingCount = 0;
145
+ const rawPath = (0, path_1.join)(learningsBase, 'raw');
146
+ if ((0, fs_1.existsSync)(rawPath)) {
147
+ try {
148
+ l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${userId}-`)).length;
149
+ }
150
+ catch {
151
+ // Ignore read failures.
152
+ }
153
+ }
154
+ let l0RetroCount = 0;
155
+ const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
156
+ if ((0, fs_1.existsSync)(retrospectivesPath)) {
157
+ try {
158
+ l0RetroCount = (0, fs_1.readdirSync)(retrospectivesPath)
159
+ .filter(f => f.startsWith(`${userId}-`) && f.endsWith('.md'))
160
+ .filter(f => isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, f))).length;
161
+ }
162
+ catch {
163
+ // Ignore read failures.
164
+ }
165
+ }
166
+ const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent;
167
+ const hasL1 = l1MistakePresent || l1PrefPresent || l1CoachPresent;
168
+ const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
169
+ if (!hasContent)
170
+ return '';
171
+ let section = forJob
172
+ ? '\n\n## Learning Context for This Job\n\n'
173
+ : '\n\n## Learning Context\n\n';
174
+ if (hasL2) {
175
+ section += '### L2 - Org patterns\n';
176
+ if (l2MistakePresent)
177
+ section += `\`${LEARNINGS_REL}/org-mistake-patterns.md\` (entries above score threshold)\n`;
178
+ if (l2PrefPresent)
179
+ section += `\`${LEARNINGS_REL}/org-preferences.md\` (all entries)\n`;
180
+ if (l2CoachPresent)
181
+ section += `\`${LEARNINGS_REL}/org-manager-coaching.md\` (all entries)\n`;
182
+ if (l2MistakeCounts && l2MistakeCounts.dormant > 0) {
183
+ section += `Dormant: ${l2MistakeCounts.dormant} org pattern${l2MistakeCounts.dormant !== 1 ? 's' : ''} below threshold\n`;
184
+ }
185
+ section += '\n';
186
+ }
187
+ if (hasL1) {
188
+ section += '### L1 - Your patterns\n';
189
+ if (l1PrefPresent)
190
+ section += `\`${LEARNINGS_REL}/${userId}-preferences.md\` (all entries)\n`;
191
+ if (l1CoachPresent)
192
+ section += `\`${LEARNINGS_REL}/${userId}-manager-coaching.md\` (all entries)\n`;
193
+ if (l1MistakePresent)
194
+ section += `\`${LEARNINGS_REL}/${userId}-mistake-patterns.md\` (entries above score threshold)\n`;
195
+ if (l1MistakeCounts && l1MistakeCounts.dormant > 0) {
196
+ section += `Dormant: ${l1MistakeCounts.dormant} personal pattern${l1MistakeCounts.dormant !== 1 ? 's' : ''} below threshold\n`;
197
+ }
198
+ section += '\n';
199
+ }
200
+ if (l0CoachingCount > 0 || l0RetroCount > 0) {
201
+ section += '### L0 - Your unprocessed signals\n';
202
+ if (l0CoachingCount > 0) {
203
+ section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${LEARNINGS_REL}/raw/${userId}-*\`\n`;
204
+ }
205
+ if (l0RetroCount > 0) {
206
+ section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${userId}-*\` with \`synthesized: false\` or missing\n`;
207
+ }
208
+ section += '\n';
209
+ }
210
+ const totalL0 = l0CoachingCount + l0RetroCount;
211
+ if (forJob) {
212
+ if (hasL2 || hasL1) {
213
+ section += 'Use the relevant patterns, preferences, and coaching signals in this job.\n';
214
+ }
215
+ if (totalL0 >= 5) {
216
+ section += '\n';
217
+ section += `Warning: ${totalL0} unprocessed signals pending. Consider running \`end-of-day-debrief\` before starting today's work.\n`;
218
+ }
219
+ }
220
+ else {
221
+ section += 'Use this synthesized learning context throughout the session.\n';
222
+ if (totalL0 >= 5) {
223
+ section += '\n';
224
+ section += `Warning: synthesis overdue with ${totalL0} unprocessed signals.\n`;
225
+ section += 'Run `end-of-day-debrief` before starting today\'s work.\n';
226
+ }
227
+ }
228
+ return section;
229
+ }