fraim 2.0.341 → 2.0.343
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/dist/src/cli/setup/ide-invocation-surfaces.js +1 -1
- package/dist/src/cli/utils/agent-adapters.js +6 -6
- package/dist/src/cli/utils/remote-sync.js +5 -0
- package/dist/src/config/persona-hiring.js +7 -1
- package/dist/src/core/ai-mentor.js +14 -0
- package/dist/src/core/handoff-contracts.js +8 -3
- package/dist/src/core/utils/capability-index.js +104 -0
- package/dist/src/core/utils/local-registry-resolver.js +33 -6
- package/dist/src/local-mcp-server/stdio-server.js +1 -1
- package/dist/src/mcp/tool-schemas.js +1 -1
- package/dist/src/services/mcp-service.js +3 -3
- package/package.json +1 -1
|
@@ -69,7 +69,7 @@ ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
|
|
|
69
69
|
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.
|
|
70
70
|
|
|
71
71
|
2. **If the user did not specify a FRAIM job or topic after activation**:
|
|
72
|
-
If local FRAIM job stubs are present in the workspace,
|
|
72
|
+
If local FRAIM job stubs are present in the workspace, read that jobs directory's \`INDEX.md\` first (it lists every job's name, one-line intent, and category) and match the request against it; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning the stubs directly if no \`INDEX.md\` is present. Also check \`fraim/personalized-employee/jobs/\` (and its own \`INDEX.md\`, if present) 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.
|
|
73
73
|
|
|
74
74
|
3. **Find the match**:
|
|
75
75
|
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, call \`get_fraim_job({ job: "adhoc-prompt" })\` directly and execute it with the user's instructions as the task input — no confirmation question, and do not pick the nearest catalog job.
|
|
@@ -72,7 +72,7 @@ This repository uses FRAIM.
|
|
|
72
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
73
|
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
74
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,
|
|
75
|
+
- When FRAIM routing is active but the job is not exact, read \`${employeeJobsPath}/INDEX.md\` and \`${managerJobsPath}/INDEX.md\` first (each lists every job's name, one-line intent, and category) to identify the most appropriate job; open an individual job stub only to confirm a shortlisted candidate. If an \`INDEX.md\` is missing or looks stale, fall back to scanning the job stubs under those directories directly. Also check \`${personalizedRootPath}/jobs/INDEX.md\` (or its stubs, if no index is present) for local overrides or repo-specific jobs.
|
|
76
76
|
- Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
|
|
77
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
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" })\`.
|
|
@@ -99,7 +99,7 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
|
|
|
99
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
100
|
- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
|
|
101
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,
|
|
102
|
+
- When FRAIM routing is active but the job is not exact, read the local jobs directory's \`INDEX.md\` first to identify which job to invoke (falling back to the individual stub files if no index is present) before fetching full content with FRAIM MCP tools.
|
|
103
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
104
|
- **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
105
105
|
- ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
|
|
@@ -108,13 +108,13 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
|
|
|
108
108
|
|
|
109
109
|
This directory is the repository-visible FRAIM surface.
|
|
110
110
|
|
|
111
|
-
- \`ai-employee/jobs/\`: employee job stubs
|
|
112
|
-
- \`ai-manager/jobs/\`: manager job stubs
|
|
113
|
-
- \`ai-employee/skills/\`: skill stubs
|
|
111
|
+
- \`ai-employee/jobs/\`: employee job stubs (+ \`INDEX.md\`)
|
|
112
|
+
- \`ai-manager/jobs/\`: manager job stubs (+ \`INDEX.md\`)
|
|
113
|
+
- \`ai-employee/skills/\`: skill stubs (+ \`INDEX.md\`)
|
|
114
114
|
- \`ai-employee/rules/\`: rule stubs
|
|
115
115
|
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
116
116
|
|
|
117
|
-
When FRAIM routing is active and no exact FRAIM job is named,
|
|
117
|
+
When FRAIM routing is active and no exact FRAIM job is named, read the \`INDEX.md\` in the relevant directory first to discover which FRAIM job, skill, or rule is relevant (falling back to scanning the individual stubs if no index is present), 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
119
|
const vscodePrompt = `# FRAIM
|
|
120
120
|
|
|
@@ -23,6 +23,7 @@ const path_1 = require("path");
|
|
|
23
23
|
const chalk_1 = __importDefault(require("chalk"));
|
|
24
24
|
const script_sync_utils_1 = require("./script-sync-utils");
|
|
25
25
|
const fraim_gitignore_1 = require("./fraim-gitignore");
|
|
26
|
+
const capability_index_1 = require("../../core/utils/capability-index");
|
|
26
27
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
27
28
|
const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
|
|
28
29
|
exports.SYNCED_CONTENT_BANNER_MARKER = '<!-- FRAIM_SYNC_MANAGED_CONTENT -->';
|
|
@@ -268,6 +269,7 @@ async function syncRoleJobsToUserDir(roleFiles, role) {
|
|
|
268
269
|
}
|
|
269
270
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
270
271
|
}
|
|
272
|
+
(0, capability_index_1.writeCapabilityIndex)(targetDir, 'job');
|
|
271
273
|
return roleFiles.length;
|
|
272
274
|
}
|
|
273
275
|
/**
|
|
@@ -353,6 +355,7 @@ async function syncFromRemote(options) {
|
|
|
353
355
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
354
356
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${relativePath}`)}`));
|
|
355
357
|
}
|
|
358
|
+
(0, capability_index_1.writeCapabilityIndex)(employeeJobsDir, 'job');
|
|
356
359
|
// Sync ai-manager job stubs to fraim/ai-manager/jobs/
|
|
357
360
|
const managerJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-manager', 'jobs');
|
|
358
361
|
cleanWorkspaceDirectory(options.projectRoot, managerJobsDir, 'manager job directory', assertWorkspacePath, 'ai-manager', 'jobs');
|
|
@@ -370,6 +373,7 @@ async function syncFromRemote(options) {
|
|
|
370
373
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
371
374
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${relativePath}`)}`));
|
|
372
375
|
}
|
|
376
|
+
(0, capability_index_1.writeCapabilityIndex)(managerJobsDir, 'job');
|
|
373
377
|
// Sync skill STUBS to fraim/ai-employee/skills/
|
|
374
378
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
375
379
|
const skillsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'skills');
|
|
@@ -387,6 +391,7 @@ async function syncFromRemote(options) {
|
|
|
387
391
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
388
392
|
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/skills/${relativePath}`)} (stub)`));
|
|
389
393
|
}
|
|
394
|
+
(0, capability_index_1.writeCapabilityIndex)(skillsDir, 'skill');
|
|
390
395
|
// Sync rule STUBS to fraim/ai-employee/rules/
|
|
391
396
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
392
397
|
const rulesDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'rules');
|
|
@@ -282,7 +282,13 @@ function buildPersonaAvatarUrl(personaKey) {
|
|
|
282
282
|
return `https://api.dicebear.com/9.x/${avatar.style}/svg?${params.toString()}`;
|
|
283
283
|
}
|
|
284
284
|
function isPersonaHireKey(value) {
|
|
285
|
-
|
|
285
|
+
// Own properties only. `in` also answers true for every Object.prototype member,
|
|
286
|
+
// so 'constructor', 'toString', '__proto__' and friends passed this guard and then
|
|
287
|
+
// indexed the catalog to a prototype member rather than a persona. The guard is the
|
|
288
|
+
// input validation in front of the hire-checkout and entitlement reads, where the
|
|
289
|
+
// value is caller-supplied, and (issue #1925) in front of the employee-record reader,
|
|
290
|
+
// where it is file-supplied.
|
|
291
|
+
return Object.prototype.hasOwnProperty.call(exports.PERSONA_HIRE_CATALOG, value);
|
|
286
292
|
}
|
|
287
293
|
function getPersonaHireAmountCents(personaKey, mode) {
|
|
288
294
|
const persona = exports.PERSONA_HIRE_CATALOG[personaKey];
|
|
@@ -65,6 +65,9 @@ class AIMentor {
|
|
|
65
65
|
const unique = Array.from(new Set(matches));
|
|
66
66
|
throw new Error(`Unresolved include directives in ${context}: ${unique.join(', ')}`);
|
|
67
67
|
}
|
|
68
|
+
buildPhaseArrivalInstruction(jobName, phaseId) {
|
|
69
|
+
return `## On Arrival\nBefore starting any work in this phase, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "starting" })\`.\n\n`;
|
|
70
|
+
}
|
|
68
71
|
buildReportBackFooter(jobName, phaseId, phaseFlow) {
|
|
69
72
|
if (!phaseFlow)
|
|
70
73
|
return '';
|
|
@@ -140,6 +143,9 @@ class AIMentor {
|
|
|
140
143
|
targetPhase = isVeryFirstCall ? targetDefinition.setup.phase : targetDefinition.recur.phase;
|
|
141
144
|
}
|
|
142
145
|
let message = `### Current Phase: ${targetPhase}\n\n`;
|
|
146
|
+
if (isVeryFirstCall) {
|
|
147
|
+
message += this.buildPhaseArrivalInstruction(workflow.metadata.name, targetPhase);
|
|
148
|
+
}
|
|
143
149
|
let instructions = workflow.phases.get(targetPhase);
|
|
144
150
|
if (instructions) {
|
|
145
151
|
instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
|
|
@@ -229,6 +235,7 @@ class AIMentor {
|
|
|
229
235
|
}
|
|
230
236
|
else if (nextPhaseId) {
|
|
231
237
|
message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
|
|
238
|
+
message += this.buildPhaseArrivalInstruction(workflow.metadata.name, nextPhaseId);
|
|
232
239
|
let nextInstructions = workflow.phases.get(nextPhaseId);
|
|
233
240
|
if (nextInstructions) {
|
|
234
241
|
nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
|
|
@@ -275,6 +282,13 @@ class AIMentor {
|
|
|
275
282
|
?? (!(0, resolve_phase_edge_1.isRecurrencePhase)(phaseMeta) ? phaseMeta?.onFailure : undefined), (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence)) || phaseId)
|
|
276
283
|
: phaseId;
|
|
277
284
|
let message = `### Current Phase: ${targetPhaseId}\n\n`;
|
|
285
|
+
// Issue #1935: a `failure` commits a completion for the phase being left, so
|
|
286
|
+
// the phase resolved above is entered with no open instance and needs its own
|
|
287
|
+
// `status: "starting"` call, for a back-edge and for the self-retry alike.
|
|
288
|
+
// `incomplete` commits nothing and never leaves the phase, so it gets none.
|
|
289
|
+
if (status === 'failure') {
|
|
290
|
+
message += this.buildPhaseArrivalInstruction(workflow.metadata.name, targetPhaseId);
|
|
291
|
+
}
|
|
278
292
|
let instructions = workflow.phases.get(targetPhaseId);
|
|
279
293
|
if (instructions) {
|
|
280
294
|
instructions = skipIncludes ? instructions : await this.resolveIncludes(instructions, workflow.path);
|
|
@@ -297,11 +297,16 @@ function upsertPhaseStarted(marker, phaseId, timestamp) {
|
|
|
297
297
|
startedPhases[phaseId] = timestamp;
|
|
298
298
|
return { startedPhases };
|
|
299
299
|
}
|
|
300
|
-
/** Builds the warn-only notice for any phase completed without an on-arrival start.
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
/** Builds the warn-only notice for any phase completed without an on-arrival start.
|
|
301
|
+
* When nextPhase is supplied, a forward-looking instruction is appended so the agent
|
|
302
|
+
* knows to call seekMentoring({status:"starting"}) before beginning the next phase. */
|
|
303
|
+
function buildPhaseStartWarning(phaseId, nextPhase) {
|
|
304
|
+
const base = (`⚠️ **Phase-start notice**: phase \`${phaseId}\` completed without an on-arrival ` +
|
|
303
305
|
`\`seekMentoring({ currentPhase: "${phaseId}", status: "starting" })\` call in this session. ` +
|
|
304
306
|
`The missing call was flagged, but this completion remains accepted.\n\n`);
|
|
307
|
+
if (!nextPhase)
|
|
308
|
+
return base;
|
|
309
|
+
return base + `Before starting any work in **${nextPhase}**, call \`seekMentoring({ currentPhase: "${nextPhase}", status: "starting" })\` immediately.\n\n`;
|
|
305
310
|
}
|
|
306
311
|
function isAbsoluteHttpUrl(url) {
|
|
307
312
|
if (typeof url !== 'string' || !url)
|
|
@@ -0,0 +1,104 @@
|
|
|
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.CAPABILITY_INDEX_FILENAME = void 0;
|
|
7
|
+
exports.buildCapabilityIndexContent = buildCapabilityIndexContent;
|
|
8
|
+
exports.writeCapabilityIndex = writeCapabilityIndex;
|
|
9
|
+
exports.refreshPersonalizedJobsIndex = refreshPersonalizedJobsIndex;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const stub_generator_1 = require("./stub-generator");
|
|
13
|
+
exports.CAPABILITY_INDEX_FILENAME = 'INDEX.md';
|
|
14
|
+
const CAPABILITY_INDEX_MARKER = '<!-- FRAIM_DISCOVERY_INDEX -->';
|
|
15
|
+
const MAX_SUMMARY_LENGTH = 140;
|
|
16
|
+
function oneLine(text, maxLength = MAX_SUMMARY_LENGTH) {
|
|
17
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
18
|
+
if (collapsed.length <= maxLength)
|
|
19
|
+
return collapsed;
|
|
20
|
+
return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
|
|
21
|
+
}
|
|
22
|
+
function fetchHintFor(kind, entry) {
|
|
23
|
+
return kind === 'job'
|
|
24
|
+
? `get_fraim_job({ job: "${entry.name}" })`
|
|
25
|
+
: `get_fraim_file({ path: "skills/${entry.category}/${entry.name}.md" })`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds the compact index markdown for one stub layer (all job stubs, or all
|
|
29
|
+
* skill stubs, under a single jobs/ or skills/ root). Pure function so the
|
|
30
|
+
* content can be unit-tested without touching the filesystem.
|
|
31
|
+
*/
|
|
32
|
+
function buildCapabilityIndexContent(kind, entries) {
|
|
33
|
+
const noun = kind === 'job' ? 'Job' : 'Skill';
|
|
34
|
+
const sorted = [...entries].sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
35
|
+
const categorized = new Map();
|
|
36
|
+
for (const entry of sorted) {
|
|
37
|
+
const list = categorized.get(entry.category) ?? [];
|
|
38
|
+
list.push(entry);
|
|
39
|
+
categorized.set(entry.category, list);
|
|
40
|
+
}
|
|
41
|
+
const lines = [
|
|
42
|
+
CAPABILITY_INDEX_MARKER,
|
|
43
|
+
`# FRAIM ${noun} Index`,
|
|
44
|
+
'',
|
|
45
|
+
`Compact discovery index for this layer's ${kind} stubs — read this file first instead of opening every individual stub.`,
|
|
46
|
+
];
|
|
47
|
+
for (const [category, categoryEntries] of categorized) {
|
|
48
|
+
lines.push('', `## ${category}`);
|
|
49
|
+
for (const entry of categoryEntries) {
|
|
50
|
+
lines.push(`- **${entry.name}**: ${entry.summary} (${fetchHintFor(kind, entry)})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push('', '---', '', '> [!IMPORTANT]', `> This index is for discovery only. Once you have a candidate ${kind}, fetch its full content before acting on it — never execute from the one-line summary above.`);
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
function readCategoryEntries(kind, rootDir) {
|
|
57
|
+
const entries = [];
|
|
58
|
+
const categoryDirents = fs_1.default.existsSync(rootDir)
|
|
59
|
+
? fs_1.default.readdirSync(rootDir, { withFileTypes: true }).filter((d) => d.isDirectory())
|
|
60
|
+
: [];
|
|
61
|
+
for (const categoryDirent of categoryDirents) {
|
|
62
|
+
const categoryDir = path_1.default.join(rootDir, categoryDirent.name);
|
|
63
|
+
const fileNames = fs_1.default
|
|
64
|
+
.readdirSync(categoryDir, { withFileTypes: true })
|
|
65
|
+
.filter((d) => d.isFile() && d.name.endsWith('.md'))
|
|
66
|
+
.map((d) => d.name);
|
|
67
|
+
for (const fileName of fileNames) {
|
|
68
|
+
const filePath = path_1.default.join(categoryDir, fileName);
|
|
69
|
+
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
70
|
+
const name = fileName.replace(/\.md$/, '');
|
|
71
|
+
const summary = kind === 'job'
|
|
72
|
+
? oneLine((0, stub_generator_1.parseRegistryJob)(content).intent)
|
|
73
|
+
: oneLine((0, stub_generator_1.parseRegistrySkill)(content).skillInput);
|
|
74
|
+
entries.push({ category: categoryDirent.name, name, summary });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Scans a jobs/ or skills/ root (one file per {category}/{name}.md, mirroring
|
|
81
|
+
* the layout every stub layer already uses) and (re)writes a compact INDEX.md
|
|
82
|
+
* at that root. Safe to call on a root with no category subfolders yet (writes
|
|
83
|
+
* nothing, since there is nothing to index).
|
|
84
|
+
*/
|
|
85
|
+
function writeCapabilityIndex(rootDir, kind) {
|
|
86
|
+
if (!fs_1.default.existsSync(rootDir))
|
|
87
|
+
return;
|
|
88
|
+
const entries = readCategoryEntries(kind, rootDir);
|
|
89
|
+
if (entries.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const indexPath = path_1.default.join(rootDir, exports.CAPABILITY_INDEX_FILENAME);
|
|
92
|
+
fs_1.default.writeFileSync(indexPath, buildCapabilityIndexContent(kind, entries), 'utf8');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Refreshes the job-stub index for the one layer `fraim sync` never writes:
|
|
96
|
+
* `fraim/personalized-employee/jobs/` and `.../manager-jobs/`. That layer is
|
|
97
|
+
* populated by agent writes (e.g. the evolve-employee job), not by sync, so
|
|
98
|
+
* there is no deterministic write path to hook — callers invoke this directly
|
|
99
|
+
* after writing or changing a personalized job overlay file.
|
|
100
|
+
*/
|
|
101
|
+
function refreshPersonalizedJobsIndex(projectRoot) {
|
|
102
|
+
writeCapabilityIndex(path_1.default.join(projectRoot, 'fraim', 'personalized-employee', 'jobs'), 'job');
|
|
103
|
+
writeCapabilityIndex(path_1.default.join(projectRoot, 'fraim', 'personalized-employee', 'manager-jobs'), 'job');
|
|
104
|
+
}
|
|
@@ -520,33 +520,60 @@ class LocalRegistryResolver {
|
|
|
520
520
|
// publish or sync and resolves at all under `single-machine`).
|
|
521
521
|
const managerLocalContent = this.readManagerLocalOverlayFile(path);
|
|
522
522
|
if (managerLocalContent !== null) {
|
|
523
|
+
// Issue #1867: apply inheritance so an extends-based override merges with its parent.
|
|
524
|
+
let resolvedManagerLocal;
|
|
525
|
+
try {
|
|
526
|
+
resolvedManagerLocal = await this.resolveInheritance(managerLocalContent, path);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
resolvedManagerLocal = { content: managerLocalContent, imports: [] };
|
|
530
|
+
}
|
|
523
531
|
return {
|
|
524
|
-
content:
|
|
532
|
+
content: resolvedManagerLocal.content,
|
|
525
533
|
source: 'local',
|
|
526
534
|
personalized: true,
|
|
527
|
-
inherited:
|
|
535
|
+
inherited: resolvedManagerLocal.imports.length > 0,
|
|
536
|
+
imports: resolvedManagerLocal.imports.length > 0 ? resolvedManagerLocal.imports : undefined,
|
|
528
537
|
scope: 'manager'
|
|
529
538
|
};
|
|
530
539
|
}
|
|
531
540
|
// Issue #869: manager overlay (precedence 3 — the synced cross-machine copy)
|
|
532
541
|
const managerOverlayContent = this.readManagerOverlayFile(path);
|
|
533
542
|
if (managerOverlayContent !== null) {
|
|
543
|
+
// Issue #1867: apply inheritance so an extends-based override merges with its parent.
|
|
544
|
+
let resolvedManagerOverlay;
|
|
545
|
+
try {
|
|
546
|
+
resolvedManagerOverlay = await this.resolveInheritance(managerOverlayContent, path);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
resolvedManagerOverlay = { content: managerOverlayContent, imports: [] };
|
|
550
|
+
}
|
|
534
551
|
return {
|
|
535
|
-
content:
|
|
552
|
+
content: resolvedManagerOverlay.content,
|
|
536
553
|
source: 'local',
|
|
537
554
|
personalized: true,
|
|
538
|
-
inherited:
|
|
555
|
+
inherited: resolvedManagerOverlay.imports.length > 0,
|
|
556
|
+
imports: resolvedManagerOverlay.imports.length > 0 ? resolvedManagerOverlay.imports : undefined,
|
|
539
557
|
scope: 'manager'
|
|
540
558
|
};
|
|
541
559
|
}
|
|
542
560
|
// Issue #869 Phase 2: org overlay (precedence 3 — after manager, before synced baseline)
|
|
543
561
|
const orgOverlayContent = this.readOrgOverlayFile(path);
|
|
544
562
|
if (orgOverlayContent !== null) {
|
|
563
|
+
// Issue #1867: apply inheritance so an extends-based override merges with its parent.
|
|
564
|
+
let resolvedOrgOverlay;
|
|
565
|
+
try {
|
|
566
|
+
resolvedOrgOverlay = await this.resolveInheritance(orgOverlayContent, path);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
resolvedOrgOverlay = { content: orgOverlayContent, imports: [] };
|
|
570
|
+
}
|
|
545
571
|
return {
|
|
546
|
-
content:
|
|
572
|
+
content: resolvedOrgOverlay.content,
|
|
547
573
|
source: 'local',
|
|
548
574
|
personalized: true,
|
|
549
|
-
inherited:
|
|
575
|
+
inherited: resolvedOrgOverlay.imports.length > 0,
|
|
576
|
+
imports: resolvedOrgOverlay.imports.length > 0 ? resolvedOrgOverlay.imports : undefined,
|
|
550
577
|
scope: 'org'
|
|
551
578
|
};
|
|
552
579
|
}
|
|
@@ -1371,7 +1371,7 @@ class FraimLocalMCPServer {
|
|
|
1371
1371
|
updated.phaseInstances = updated.phaseInstances?.filter((instance) => instance.jobId && instance.jobId !== args.jobId);
|
|
1372
1372
|
}
|
|
1373
1373
|
this.writeRunStateMarker(sessionId, args.jobName, updated);
|
|
1374
|
-
return (inspection.missingStart ? (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase) : '')
|
|
1374
|
+
return (inspection.missingStart ? (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase, nextPhase ?? undefined) : '')
|
|
1375
1375
|
+ (!args.jobId ? `Run-tracking notice: phase ${args.currentPhase} has no jobId; matching used the session, job name and phase.\n\n` : '');
|
|
1376
1376
|
}
|
|
1377
1377
|
readCachedTemplateFile(filename) {
|
|
@@ -343,7 +343,7 @@ Do not use this tool for other repositories or external project issue trackers.`
|
|
|
343
343
|
},
|
|
344
344
|
currentPhase: {
|
|
345
345
|
type: 'string',
|
|
346
|
-
description: 'The phase you are currently in or have just finished (e.g., "implement-scoping"). For initial workflow start,
|
|
346
|
+
description: 'The phase you are currently in or have just finished (e.g., "implement-scoping"). For initial workflow start, pass "starting" as a one-time bootstrap — this is distinct from the per-phase status:"starting" on-arrival call you make at the beginning of every phase.'
|
|
347
347
|
},
|
|
348
348
|
status: {
|
|
349
349
|
type: 'string',
|
|
@@ -548,8 +548,8 @@ class McpService {
|
|
|
548
548
|
'- 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.',
|
|
549
549
|
'- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.',
|
|
550
550
|
'- If the user already named a job, call `get_fraim_job({ job: "<job-name>" })` directly.',
|
|
551
|
-
'- If routing is active and local FRAIM job stubs are present under `fraim/ai-employee/jobs/` or `fraim/ai-manager/jobs/`,
|
|
552
|
-
'- Also
|
|
551
|
+
'- If routing is active and local FRAIM job stubs are present under `fraim/ai-employee/jobs/` or `fraim/ai-manager/jobs/`, read that directory\'s `INDEX.md` first (name, one-line intent, and category per job) to choose the best job; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning the stubs directly if no `INDEX.md` is present.',
|
|
552
|
+
'- Also check `fraim/personalized-employee/jobs/` (and its own `INDEX.md`, if present) for local job overrides or repo-specific jobs when routing is active.',
|
|
553
553
|
'- If local stubs are missing, you cannot inspect workspace files, or the user asks for available jobs, call `list_fraim_jobs()` to view the full catalog.',
|
|
554
554
|
'- 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.',
|
|
555
555
|
'- Use `get_fraim_file(...)` only for the specific skills or rules needed for the current task.',
|
|
@@ -824,7 +824,7 @@ class McpService {
|
|
|
824
824
|
else if (isPhaseCompletion) {
|
|
825
825
|
const inspection = (0, handoff_contracts_1.inspectPhaseCompletion)(marker, context);
|
|
826
826
|
if (inspection.missingStart)
|
|
827
|
-
phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase);
|
|
827
|
+
phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase, result.nextPhase ?? undefined);
|
|
828
828
|
const completed = (0, handoff_contracts_1.commitPhaseCompletion)(marker, inspection, { ...context, timestamp });
|
|
829
829
|
this.phaseStartMarkers.set(markerKey, completed);
|
|
830
830
|
if (args.status === 'complete' && result.nextPhase === null) {
|