fraim 2.0.342 → 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/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/local-mcp-server/stdio-server.js +1 -1
- package/dist/src/mcp/tool-schemas.js +1 -1
- package/dist/src/services/mcp-service.js +1 -1
- package/package.json +1 -1
|
@@ -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)
|
|
@@ -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',
|
|
@@ -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) {
|