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.
@@ -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)
@@ -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',
@@ -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.342",
3
+ "version": "2.0.343",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {