fraim 2.0.280 → 2.0.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +467 -467
- package/bin/fraim.js +12 -12
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +266 -182
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -3
- package/dist/src/cli/setup/ide-invocation-surfaces.js +64 -64
- package/dist/src/cli/utils/agent-adapters.js +61 -61
- package/dist/src/core/ai-mentor.js +35 -0
- package/dist/src/core/handoff-contracts.js +73 -46
- package/dist/src/core/resolve-phase-edge.js +33 -0
- package/dist/src/core/utils/stub-generator.js +53 -53
- package/dist/src/first-run/server.js +5 -1
- package/dist/src/first-run/session-service.js +48 -12
- package/dist/src/fraim/issues.js +4 -4
- package/dist/src/local-mcp-server/stdio-server.js +43 -4
- package/dist/src/mcp/tool-schemas.js +40 -40
- package/dist/src/middleware/telemetry.js +21 -21
- package/dist/src/services/email-service.js +623 -623
- package/dist/src/services/installer-service.js +22 -22
- package/index.js +83 -83
- package/package.json +59 -59
- package/public/first-run/error-frame.js +100 -100
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +747 -742
- package/public/first-run/styles.css +929 -929
- package/dist/src/cli/commands/learning-usage.js +0 -412
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
- package/dist/src/services/email-service-clean.js +0 -782
|
@@ -14,9 +14,9 @@ exports.buildAntigravityCommandContent = buildAntigravityCommandContent;
|
|
|
14
14
|
exports.buildGeminiCommandContent = buildGeminiCommandContent;
|
|
15
15
|
exports.describeInvocationSurface = describeInvocationSurface;
|
|
16
16
|
exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
|
|
17
|
-
exports.CURSOR_MDC_FRONTMATTER = `---
|
|
18
|
-
description: FRAIM discovery and execution contract
|
|
19
|
-
alwaysApply: true
|
|
17
|
+
exports.CURSOR_MDC_FRONTMATTER = `---
|
|
18
|
+
description: FRAIM discovery and execution contract
|
|
19
|
+
alwaysApply: true
|
|
20
20
|
---`;
|
|
21
21
|
exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
|
|
22
22
|
'fraim_connect',
|
|
@@ -38,112 +38,112 @@ function buildDeferredToolBootstrapSection(profile) {
|
|
|
38
38
|
const preloadList = exports.FRAIM_DEFERRED_TOOL_PRELOAD.map((toolName) => `\`${toolName}\``).join(', ');
|
|
39
39
|
switch (profile) {
|
|
40
40
|
case 'claude-toolsearch':
|
|
41
|
-
return `0. **Preload deferred FRAIM tools when needed**:
|
|
42
|
-
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
|
|
43
|
-
- Do the preload as one batched discovery step, not one search per tool.
|
|
44
|
-
|
|
41
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
42
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
|
|
43
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
44
|
+
|
|
45
45
|
`;
|
|
46
46
|
case 'codex-tool-search':
|
|
47
|
-
return `0. **Preload deferred FRAIM tools when needed**:
|
|
48
|
-
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
|
|
49
|
-
- Do the preload as one batched discovery step, not one search per tool.
|
|
50
|
-
|
|
47
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
48
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
|
|
49
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
50
|
+
|
|
51
51
|
`;
|
|
52
52
|
case 'generic-tool-discovery':
|
|
53
|
-
return `0. **Preload deferred FRAIM tools when needed**:
|
|
54
|
-
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
|
|
55
|
-
- Do the preload as one batched discovery step, not one search per tool.
|
|
56
|
-
|
|
53
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
54
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
|
|
55
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
56
|
+
|
|
57
57
|
`;
|
|
58
58
|
default:
|
|
59
59
|
return '';
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
function buildFraimInvocationBody(profile = 'none') {
|
|
63
|
-
return `Follow this process:
|
|
64
|
-
|
|
65
|
-
${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
66
|
-
Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
|
|
67
|
-
|
|
68
|
-
2. **If the user did not specify a FRAIM job or topic after activation**:
|
|
69
|
-
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.
|
|
70
|
-
|
|
71
|
-
3. **Find the match**:
|
|
72
|
-
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, 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 exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification. Do not pick the nearest catalog job.
|
|
73
|
-
|
|
74
|
-
4. **Load the full content**:
|
|
75
|
-
- For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
|
|
76
|
-
- For skills, use the content returned by \`get_fraim_file(...)\`.
|
|
77
|
-
|
|
78
|
-
5. **Execute**:
|
|
79
|
-
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
80
|
-
- For skills, apply the skill steps directly to the user's current context.
|
|
81
|
-
- ${exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
63
|
+
return `Follow this process:
|
|
64
|
+
|
|
65
|
+
${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
66
|
+
Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
|
|
67
|
+
|
|
68
|
+
2. **If the user did not specify a FRAIM job or topic after activation**:
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
3. **Find the match**:
|
|
72
|
+
If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, 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 exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification. Do not pick the nearest catalog job.
|
|
73
|
+
|
|
74
|
+
4. **Load the full content**:
|
|
75
|
+
- For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
|
|
76
|
+
- For skills, use the content returned by \`get_fraim_file(...)\`.
|
|
77
|
+
|
|
78
|
+
5. **Execute**:
|
|
79
|
+
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
80
|
+
- For skills, apply the skill steps directly to the user's current context.
|
|
81
|
+
- ${exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
82
82
|
`;
|
|
83
83
|
}
|
|
84
84
|
exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
|
|
85
85
|
function buildClaudeSkillContent() {
|
|
86
|
-
return `# FRAIM
|
|
87
|
-
|
|
86
|
+
return `# FRAIM
|
|
87
|
+
|
|
88
88
|
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
89
89
|
}
|
|
90
90
|
function buildClaudeCommandShimContent() {
|
|
91
|
-
return `# FRAIM Compatibility Command
|
|
92
|
-
|
|
93
|
-
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
94
|
-
|
|
91
|
+
return `# FRAIM Compatibility Command
|
|
92
|
+
|
|
93
|
+
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
94
|
+
|
|
95
95
|
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
96
96
|
}
|
|
97
97
|
function buildClaudeSlashCommandContent() {
|
|
98
98
|
return buildClaudeCommandShimContent();
|
|
99
99
|
}
|
|
100
100
|
function buildCursorMentionRuleContent() {
|
|
101
|
-
return `${exports.CURSOR_MDC_FRONTMATTER}
|
|
102
|
-
|
|
103
|
-
# FRAIM
|
|
104
|
-
|
|
105
|
-
${buildFraimInvocationBody('generic-tool-discovery')}
|
|
101
|
+
return `${exports.CURSOR_MDC_FRONTMATTER}
|
|
102
|
+
|
|
103
|
+
# FRAIM
|
|
104
|
+
|
|
105
|
+
${buildFraimInvocationBody('generic-tool-discovery')}
|
|
106
106
|
`;
|
|
107
107
|
}
|
|
108
|
-
exports.CODEX_SKILL_FRONTMATTER = `---
|
|
109
|
-
name: fraim
|
|
110
|
-
description: Discover and execute FRAIM jobs and skills from Codex.
|
|
108
|
+
exports.CODEX_SKILL_FRONTMATTER = `---
|
|
109
|
+
name: fraim
|
|
110
|
+
description: Discover and execute FRAIM jobs and skills from Codex.
|
|
111
111
|
---`;
|
|
112
112
|
function buildCodexSkillContent() {
|
|
113
|
-
return `${exports.CODEX_SKILL_FRONTMATTER}
|
|
114
|
-
# FRAIM
|
|
115
|
-
|
|
113
|
+
return `${exports.CODEX_SKILL_FRONTMATTER}
|
|
114
|
+
# FRAIM
|
|
115
|
+
|
|
116
116
|
${buildFraimInvocationBody('codex-tool-search')}`;
|
|
117
117
|
}
|
|
118
118
|
function buildGrokSkillContent() {
|
|
119
|
-
return `# FRAIM
|
|
120
|
-
|
|
119
|
+
return `# FRAIM
|
|
120
|
+
|
|
121
121
|
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
122
122
|
}
|
|
123
123
|
function buildWindsurfCommandContent() {
|
|
124
|
-
return `# FRAIM
|
|
125
|
-
|
|
124
|
+
return `# FRAIM
|
|
125
|
+
|
|
126
126
|
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
127
127
|
}
|
|
128
128
|
function buildKiroCommandContent() {
|
|
129
|
-
return `# FRAIM
|
|
130
|
-
|
|
129
|
+
return `# FRAIM
|
|
130
|
+
|
|
131
131
|
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
132
132
|
}
|
|
133
133
|
function escapeTomlMultiline(value) {
|
|
134
134
|
return value.replace(/"""/g, '\\"""');
|
|
135
135
|
}
|
|
136
136
|
function buildAntigravityCommandContent() {
|
|
137
|
-
return `# FRAIM
|
|
138
|
-
|
|
137
|
+
return `# FRAIM
|
|
138
|
+
|
|
139
139
|
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
140
140
|
}
|
|
141
141
|
function buildGeminiCommandContent() {
|
|
142
|
-
return `description = "Discover and execute FRAIM jobs and skills"
|
|
143
|
-
prompt = """
|
|
144
|
-
# FRAIM
|
|
145
|
-
|
|
146
|
-
${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
|
|
142
|
+
return `description = "Discover and execute FRAIM jobs and skills"
|
|
143
|
+
prompt = """
|
|
144
|
+
# FRAIM
|
|
145
|
+
|
|
146
|
+
${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
|
|
147
147
|
"""`;
|
|
148
148
|
}
|
|
149
149
|
function describeInvocationSurface(ideName, invocationProfile) {
|
|
@@ -25,9 +25,9 @@ function adapterConfigTypes(file) {
|
|
|
25
25
|
return Array.isArray(file.configType) ? file.configType : [file.configType];
|
|
26
26
|
}
|
|
27
27
|
function buildManagedSection(body) {
|
|
28
|
-
return `${START_MARKER}
|
|
29
|
-
${body.trim()}
|
|
30
|
-
${END_MARKER}
|
|
28
|
+
return `${START_MARKER}
|
|
29
|
+
${body.trim()}
|
|
30
|
+
${END_MARKER}
|
|
31
31
|
`;
|
|
32
32
|
}
|
|
33
33
|
function mergeManagedSection(existingContent, managedSection) {
|
|
@@ -59,69 +59,69 @@ function getAdapterFiles(allowedConfigTypes = null) {
|
|
|
59
59
|
const employeeRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-employee/rules');
|
|
60
60
|
const personalizedRootPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee');
|
|
61
61
|
const projectRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/rules/project_rules.md');
|
|
62
|
-
const markdownBody = buildManagedSection(`
|
|
63
|
-
## FRAIM
|
|
64
|
-
|
|
65
|
-
This repository uses FRAIM.
|
|
66
|
-
|
|
67
|
-
- The FRAIM discovery catalog lives under \`${fraimRoot}/\`.
|
|
68
|
-
- Jobs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` are FRAIM's primary execution units. Treat them like first-class workflows when deciding how to execute work.
|
|
69
|
-
- Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
|
|
70
|
-
- Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
|
|
71
|
-
- Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
|
|
72
|
-
- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
|
|
73
|
-
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
74
|
-
- If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly.
|
|
75
|
-
- When FRAIM routing is active but the job is not exact, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections before using catalog tools.
|
|
76
|
-
- Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
|
|
77
|
-
- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
|
|
78
|
-
- For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
|
|
79
|
-
- Read \`${projectRulesPath}\` if it exists before doing work.
|
|
80
|
-
- When users ask for next step recommendations, use recommend-next-job skill under \`${employeeSkillsPath}/\` to gather context before suggesting jobs.
|
|
81
|
-
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
82
|
-
|
|
83
|
-
> [!IMPORTANT]
|
|
84
|
-
> **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.
|
|
62
|
+
const markdownBody = buildManagedSection(`
|
|
63
|
+
## FRAIM
|
|
64
|
+
|
|
65
|
+
This repository uses FRAIM.
|
|
66
|
+
|
|
67
|
+
- The FRAIM discovery catalog lives under \`${fraimRoot}/\`.
|
|
68
|
+
- Jobs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` are FRAIM's primary execution units. Treat them like first-class workflows when deciding how to execute work.
|
|
69
|
+
- Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
|
|
70
|
+
- Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
|
|
71
|
+
- Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
|
|
72
|
+
- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
|
|
73
|
+
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
74
|
+
- If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly.
|
|
75
|
+
- When FRAIM routing is active but the job is not exact, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections before using catalog tools.
|
|
76
|
+
- Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
|
|
77
|
+
- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
|
|
78
|
+
- For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
|
|
79
|
+
- Read \`${projectRulesPath}\` if it exists before doing work.
|
|
80
|
+
- When users ask for next step recommendations, use recommend-next-job skill under \`${employeeSkillsPath}/\` to gather context before suggesting jobs.
|
|
81
|
+
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
82
|
+
|
|
83
|
+
> [!IMPORTANT]
|
|
84
|
+
> **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.
|
|
85
85
|
`);
|
|
86
|
-
const cursorManagedBody = buildManagedSection(`
|
|
87
|
-
# FRAIM
|
|
88
|
-
|
|
89
|
-
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
|
|
86
|
+
const cursorManagedBody = buildManagedSection(`
|
|
87
|
+
# FRAIM
|
|
88
|
+
|
|
89
|
+
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
|
|
90
90
|
`);
|
|
91
|
-
const copilotBody = buildManagedSection(`
|
|
92
|
-
## FRAIM
|
|
93
|
-
|
|
94
|
-
- Use \`${fraimRoot}/\` as the repository's FRAIM catalog.
|
|
95
|
-
- FRAIM jobs are the primary execution units and should be treated like first-class workflows.
|
|
96
|
-
- FRAIM skills are reusable capabilities jobs compose.
|
|
97
|
-
- FRAIM rules are always-on constraints and conventions.
|
|
98
|
-
- Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
|
|
99
|
-
- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
|
|
100
|
-
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
101
|
-
- If the user names an exact FRAIM job, fetch that full job directly with FRAIM MCP tools.
|
|
102
|
-
- When FRAIM routing is active but the job is not exact, use local stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
|
|
103
|
-
- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
|
|
104
|
-
- **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
105
|
-
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
91
|
+
const copilotBody = buildManagedSection(`
|
|
92
|
+
## FRAIM
|
|
93
|
+
|
|
94
|
+
- Use \`${fraimRoot}/\` as the repository's FRAIM catalog.
|
|
95
|
+
- FRAIM jobs are the primary execution units and should be treated like first-class workflows.
|
|
96
|
+
- FRAIM skills are reusable capabilities jobs compose.
|
|
97
|
+
- FRAIM rules are always-on constraints and conventions.
|
|
98
|
+
- Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
|
|
99
|
+
- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
|
|
100
|
+
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
101
|
+
- If the user names an exact FRAIM job, fetch that full job directly with FRAIM MCP tools.
|
|
102
|
+
- When FRAIM routing is active but the job is not exact, use local stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
|
|
103
|
+
- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
|
|
104
|
+
- **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
105
|
+
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
106
106
|
`);
|
|
107
|
-
const fraimReadme = `# FRAIM Catalog
|
|
108
|
-
|
|
109
|
-
This directory is the repository-visible FRAIM surface.
|
|
110
|
-
|
|
111
|
-
- \`ai-employee/jobs/\`: employee job stubs
|
|
112
|
-
- \`ai-manager/jobs/\`: manager job stubs
|
|
113
|
-
- \`ai-employee/skills/\`: skill stubs
|
|
114
|
-
- \`ai-employee/rules/\`: rule stubs
|
|
115
|
-
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
116
|
-
|
|
117
|
-
When FRAIM routing is active and no exact FRAIM job is named, use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools. If an exact FRAIM job is named, load it directly. For ordinary requests, do not scan this catalog first.
|
|
107
|
+
const fraimReadme = `# FRAIM Catalog
|
|
108
|
+
|
|
109
|
+
This directory is the repository-visible FRAIM surface.
|
|
110
|
+
|
|
111
|
+
- \`ai-employee/jobs/\`: employee job stubs
|
|
112
|
+
- \`ai-manager/jobs/\`: manager job stubs
|
|
113
|
+
- \`ai-employee/skills/\`: skill stubs
|
|
114
|
+
- \`ai-employee/rules/\`: rule stubs
|
|
115
|
+
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
116
|
+
|
|
117
|
+
When FRAIM routing is active and no exact FRAIM job is named, use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools. If an exact FRAIM job is named, load it directly. For ordinary requests, do not scan this catalog first.
|
|
118
118
|
`;
|
|
119
|
-
const vscodePrompt = `# FRAIM
|
|
120
|
-
|
|
119
|
+
const vscodePrompt = `# FRAIM
|
|
120
|
+
|
|
121
121
|
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
|
|
122
|
-
const geminiProjectInstructions = `# Gemini Project Instructions
|
|
123
|
-
|
|
124
|
-
@../AGENTS.md
|
|
122
|
+
const geminiProjectInstructions = `# Gemini Project Instructions
|
|
123
|
+
|
|
124
|
+
@../AGENTS.md
|
|
125
125
|
`;
|
|
126
126
|
const all = [
|
|
127
127
|
{ path: 'AGENTS.md', content: markdownBody, configType: 'standard' },
|
|
@@ -230,6 +230,41 @@ class AIMentor {
|
|
|
230
230
|
status
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The job's declared phase graph (onSuccess/onFailure edges), or undefined
|
|
235
|
+
* for a simple/bootstrap-style job with no phases. Lets a caller derive
|
|
236
|
+
* graph facts (e.g. "which phase is this job's submission phase", issue
|
|
237
|
+
* #1276) without hardcoding phase names or duplicating job loading.
|
|
238
|
+
*/
|
|
239
|
+
async getJobPhaseMap(jobName) {
|
|
240
|
+
const job = await this.getOrLoadJob(jobName);
|
|
241
|
+
return job?.metadata.phases;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* True when a job's phase, with its includes resolved, itself instructs
|
|
245
|
+
* the agent to emit `evidence.reviewHandoff` (issue #1318 follow-up).
|
|
246
|
+
*
|
|
247
|
+
* The #1276 reviewHandoff gate fires for any job's graph-derived
|
|
248
|
+
* submission phase, but not every job family promises reviewHandoff at
|
|
249
|
+
* that phase — jobs built on `rich-review-artifact-contract.md`,
|
|
250
|
+
* `author-docx.md`, or `reporting-standards.md` (analysis reports, DOCX
|
|
251
|
+
* deliverables, operational setup jobs) use a different, legitimate
|
|
252
|
+
* completion contract and never mention the field, even resolved. Gating
|
|
253
|
+
* those unconditionally would reject a call the job's own instructions
|
|
254
|
+
* never told the agent to satisfy. This check reads the phase's actual
|
|
255
|
+
* resolved instructions rather than a hardcoded skill/job allowlist, so
|
|
256
|
+
* a new reviewHandoff-emitting skill (or a new non-reviewHandoff one)
|
|
257
|
+
* needs no update here — same "derive, don't hardcode" reasoning as
|
|
258
|
+
* `derivePredecessorPhase`.
|
|
259
|
+
*/
|
|
260
|
+
async phasePromisesReviewHandoff(jobName, phaseId) {
|
|
261
|
+
const job = await this.getOrLoadJob(jobName);
|
|
262
|
+
const raw = job?.phases.get(phaseId);
|
|
263
|
+
if (!raw)
|
|
264
|
+
return false;
|
|
265
|
+
const resolved = await this.resolveIncludes(raw, job.path);
|
|
266
|
+
return resolved.includes('evidence.reviewHandoff');
|
|
267
|
+
}
|
|
233
268
|
async getJobOverview(jobName) {
|
|
234
269
|
const job = await this.getOrLoadJob(jobName);
|
|
235
270
|
if (!job)
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Pure helpers for validating the three evidence fields that FRAIM jobs must
|
|
6
6
|
* emit on key seekMentoring calls so the Hub can render its review surfaces:
|
|
7
7
|
*
|
|
8
|
-
* - evidence.reviewHandoff (
|
|
8
|
+
* - evidence.reviewHandoff (job's submission phase, status === "complete";
|
|
9
|
+
* see isSubmitPhase for how that phase is found)
|
|
9
10
|
* - evidence.nextJobRecommendations (retrospective phase completion)
|
|
10
11
|
* - evidence.delegationLedger (create-delegation-graph phase)
|
|
11
12
|
*
|
|
@@ -26,17 +27,36 @@ exports.validateNextJobRecommendations = validateNextJobRecommendations;
|
|
|
26
27
|
exports.validateDelegationLedger = validateDelegationLedger;
|
|
27
28
|
exports.validateHandoffContracts = validateHandoffContracts;
|
|
28
29
|
exports.buildHandoffRejectionMessage = buildHandoffRejectionMessage;
|
|
30
|
+
const resolve_phase_edge_1 = require("./resolve-phase-edge");
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// Phase detection
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
/**
|
|
33
35
|
* Returns true when a seekMentoring call is a submit-phase handoff requiring
|
|
34
|
-
* a reviewHandoff evidence field.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* a reviewHandoff evidence field.
|
|
37
|
+
*
|
|
38
|
+
* Two triggers, either is sufficient:
|
|
39
|
+
* - `status === "awaiting_mentor"`: the original #916 signal. No published
|
|
40
|
+
* `seekMentoring` status permits this value today (confirmed by
|
|
41
|
+
* `spike/1157-approval-gate/s5-dead-submit-gate.js`), so this branch is
|
|
42
|
+
* provably unreachable in production — kept so a future status value, or a
|
|
43
|
+
* job that still emits it, is not silently unenforced.
|
|
44
|
+
* - `status === "complete"` at the job's own submission phase, derived from
|
|
45
|
+
* its real phase graph (issue #1276, gating-only rebuild of #1157 Change
|
|
46
|
+
* 1): the unique phase whose `onSuccess` targets `address-feedback`. This
|
|
47
|
+
* is the trigger that actually fires. No hardcoded phase-name list — a
|
|
48
|
+
* personalized job with a differently-named submission phase is still
|
|
49
|
+
* caught, and a job whose graph doesn't uniquely name one (`phases`
|
|
50
|
+
* omitted, or the derivation is ambiguous) is not gated at all rather than
|
|
51
|
+
* guessed at.
|
|
37
52
|
*/
|
|
38
|
-
function isSubmitPhase(currentPhase, status) {
|
|
39
|
-
|
|
53
|
+
function isSubmitPhase(currentPhase, status, phases) {
|
|
54
|
+
if (status === 'awaiting_mentor')
|
|
55
|
+
return true;
|
|
56
|
+
if (status === 'complete' && phases) {
|
|
57
|
+
return (0, resolve_phase_edge_1.derivePredecessorPhase)(phases, resolve_phase_edge_1.FEEDBACK_PHASE_ID) === currentPhase;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
42
62
|
* Returns true when a seekMentoring call is a retrospective completion.
|
|
@@ -221,7 +241,14 @@ function validateHandoffContracts(args) {
|
|
|
221
241
|
const phase = args.currentPhase ?? '';
|
|
222
242
|
const status = args.status ?? '';
|
|
223
243
|
const evidence = args.evidence ?? {};
|
|
224
|
-
|
|
244
|
+
const findings = args.findings ?? {};
|
|
245
|
+
if (isSubmitPhase(phase, status, args.phases) && !evidence.reviewHandoff && findings.reviewHandoff) {
|
|
246
|
+
return ['reviewHandoff was found in findings but must be nested under evidence. Move reviewHandoff out of findings and into the top-level evidence object.'];
|
|
247
|
+
}
|
|
248
|
+
if (isRetrospectivePhase(phase, status) && !evidence.nextJobRecommendations && findings.nextJobRecommendations) {
|
|
249
|
+
return ['nextJobRecommendations was found in findings but must be nested under evidence. Move nextJobRecommendations out of findings and into the top-level evidence object.'];
|
|
250
|
+
}
|
|
251
|
+
if (isSubmitPhase(phase, status, args.phases)) {
|
|
225
252
|
const errors = validateReviewHandoff(evidence.reviewHandoff);
|
|
226
253
|
if (errors)
|
|
227
254
|
return errors;
|
|
@@ -246,48 +273,48 @@ function validateHandoffContracts(args) {
|
|
|
246
273
|
// ---------------------------------------------------------------------------
|
|
247
274
|
// Rejection message builder
|
|
248
275
|
// ---------------------------------------------------------------------------
|
|
249
|
-
const ADDRESS_FEEDBACK_APPROVAL_SCHEMA = `\`\`\`javascript
|
|
250
|
-
evidence: {
|
|
251
|
-
approved: true // Set only after the manager explicitly approves via the Hub review action
|
|
252
|
-
}
|
|
276
|
+
const ADDRESS_FEEDBACK_APPROVAL_SCHEMA = `\`\`\`javascript
|
|
277
|
+
evidence: {
|
|
278
|
+
approved: true // Set only after the manager explicitly approves via the Hub review action
|
|
279
|
+
}
|
|
253
280
|
\`\`\``;
|
|
254
|
-
const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
|
|
255
|
-
evidence: {
|
|
256
|
-
reviewHandoff: {
|
|
257
|
-
reviewRequired: true,
|
|
258
|
-
reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
|
|
259
|
-
artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
|
|
260
|
-
summary: "<optional summary>",
|
|
261
|
-
reviewActions: [
|
|
262
|
-
{ kind: "approve", label: "Approve" },
|
|
263
|
-
{ kind: "request_changes", label: "Request Changes" }
|
|
264
|
-
]
|
|
265
|
-
}
|
|
266
|
-
}
|
|
281
|
+
const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
|
|
282
|
+
evidence: {
|
|
283
|
+
reviewHandoff: {
|
|
284
|
+
reviewRequired: true,
|
|
285
|
+
reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
|
|
286
|
+
artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
|
|
287
|
+
summary: "<optional summary>",
|
|
288
|
+
reviewActions: [
|
|
289
|
+
{ kind: "approve", label: "Approve" },
|
|
290
|
+
{ kind: "request_changes", label: "Request Changes" }
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
267
294
|
\`\`\``;
|
|
268
|
-
const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
|
|
269
|
-
evidence: {
|
|
270
|
-
nextJobRecommendations: [
|
|
271
|
-
{
|
|
272
|
-
jobId: "<job-slug>",
|
|
273
|
-
label: "<human-readable label>",
|
|
274
|
-
reason: "<optional: why this job is recommended>",
|
|
275
|
-
contextSummary: "<optional: one sentence context for the next agent>"
|
|
276
|
-
}
|
|
277
|
-
// 0–3 entries; an empty array [] is valid
|
|
278
|
-
]
|
|
279
|
-
}
|
|
295
|
+
const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
|
|
296
|
+
evidence: {
|
|
297
|
+
nextJobRecommendations: [
|
|
298
|
+
{
|
|
299
|
+
jobId: "<job-slug>",
|
|
300
|
+
label: "<human-readable label>",
|
|
301
|
+
reason: "<optional: why this job is recommended>",
|
|
302
|
+
contextSummary: "<optional: one sentence context for the next agent>"
|
|
303
|
+
}
|
|
304
|
+
// 0–3 entries; an empty array [] is valid
|
|
305
|
+
]
|
|
306
|
+
}
|
|
280
307
|
\`\`\``;
|
|
281
|
-
const DELEGATION_LEDGER_SCHEMA = `\`\`\`javascript
|
|
282
|
-
evidence: {
|
|
283
|
-
delegationLedger: {
|
|
284
|
-
delegationRequired: true,
|
|
285
|
-
objective: "<optional objective string>",
|
|
286
|
-
tasks: [
|
|
287
|
-
{ jobId: "<job-slug>", personaKey: "<persona key or null>", briefing: "<optional briefing>" }
|
|
288
|
-
]
|
|
289
|
-
}
|
|
290
|
-
}
|
|
308
|
+
const DELEGATION_LEDGER_SCHEMA = `\`\`\`javascript
|
|
309
|
+
evidence: {
|
|
310
|
+
delegationLedger: {
|
|
311
|
+
delegationRequired: true,
|
|
312
|
+
objective: "<optional objective string>",
|
|
313
|
+
tasks: [
|
|
314
|
+
{ jobId: "<job-slug>", personaKey: "<persona key or null>", briefing: "<optional briefing>" }
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
291
318
|
\`\`\``;
|
|
292
319
|
const FIELD_SCHEMAS = {
|
|
293
320
|
approved: ADDRESS_FEEDBACK_APPROVAL_SCHEMA,
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* change inert for every job that authors no map.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.FEEDBACK_PHASE_ID = void 0;
|
|
18
19
|
exports.resolvePhaseEdge = resolvePhaseEdge;
|
|
19
20
|
exports.resolveDiscriminant = resolveDiscriminant;
|
|
20
21
|
exports.discriminantKeys = discriminantKeys;
|
|
22
|
+
exports.derivePredecessorPhase = derivePredecessorPhase;
|
|
21
23
|
/** The default discriminant, and the mandatory key on every authored map. */
|
|
22
24
|
const DEFAULT_DISCRIMINANT = 'default';
|
|
23
25
|
/**
|
|
@@ -73,3 +75,34 @@ function discriminantKeys(edge) {
|
|
|
73
75
|
return [];
|
|
74
76
|
return Object.keys(edge).filter((key) => key !== DEFAULT_DISCRIMINANT);
|
|
75
77
|
}
|
|
78
|
+
/** The framework's one review/decision phase; every reviewable job routes into it. */
|
|
79
|
+
exports.FEEDBACK_PHASE_ID = 'address-feedback';
|
|
80
|
+
/**
|
|
81
|
+
* Finds the unique phase in a job's phase map whose `onSuccess` edge can
|
|
82
|
+
* resolve to `targetPhaseId` for some discriminant (including `default`).
|
|
83
|
+
*
|
|
84
|
+
* Gating-only rebuild of #1157 Change 1 ("graph-derived phase identity"),
|
|
85
|
+
* scoped per #1276: that change also added routing behavior and was reverted
|
|
86
|
+
* before merge because the two were bundled. Only the lookup survives here —
|
|
87
|
+
* it answers "which phase is this job's submission phase" without changing
|
|
88
|
+
* how any phase transition resolves. No hardcoded phase-name list: the
|
|
89
|
+
* derivation reads whatever the job's own graph declares, so it is correct
|
|
90
|
+
* for every current and future job without per-job edits.
|
|
91
|
+
*
|
|
92
|
+
* Returns `null` when zero or multiple phases match, so a caller can fail
|
|
93
|
+
* safe (treat as "unknown") rather than guess at an ambiguous graph.
|
|
94
|
+
*/
|
|
95
|
+
function derivePredecessorPhase(phases, targetPhaseId) {
|
|
96
|
+
const predecessors = [];
|
|
97
|
+
for (const [phaseId, edges] of Object.entries(phases ?? {})) {
|
|
98
|
+
if (phaseId === targetPhaseId || !edges)
|
|
99
|
+
continue;
|
|
100
|
+
const edge = edges.onSuccess;
|
|
101
|
+
const targets = typeof edge === 'string'
|
|
102
|
+
? [edge]
|
|
103
|
+
: (edge && typeof edge === 'object' ? Object.values(edge) : []);
|
|
104
|
+
if (targets.includes(targetPhaseId))
|
|
105
|
+
predecessors.push(phaseId);
|
|
106
|
+
}
|
|
107
|
+
return predecessors.length === 1 ? predecessors[0] : null;
|
|
108
|
+
}
|