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.
@@ -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, 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.
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, 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.
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, use local stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
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, 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.
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
- return value in exports.PERSONA_HIRE_CATALOG;
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
- function buildPhaseStartWarning(phaseId) {
302
- return (`⚠️ **Phase-start notice**: phase \`${phaseId}\` completed without an on-arrival ` +
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: managerLocalContent,
532
+ content: resolvedManagerLocal.content,
525
533
  source: 'local',
526
534
  personalized: true,
527
- inherited: false,
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: managerOverlayContent,
552
+ content: resolvedManagerOverlay.content,
536
553
  source: 'local',
537
554
  personalized: true,
538
- inherited: false,
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: orgOverlayContent,
572
+ content: resolvedOrgOverlay.content,
547
573
  source: 'local',
548
574
  personalized: true,
549
- inherited: false,
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, use "starting".'
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/`, inspect those first to choose the best job.',
552
- '- Also inspect `fraim/personalized-employee/jobs/` for local job overrides or repo-specific jobs when routing is active.',
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.341",
3
+ "version": "2.0.343",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {