feat: add passive cross-service actions
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"test:onboarding": "tsx scripts/onboarding-ledger.test.ts",
|
"test:onboarding": "tsx scripts/onboarding-ledger.test.ts",
|
||||||
"test:missions": "tsx scripts/mission-lifecycle.test.ts",
|
"test:missions": "tsx scripts/mission-lifecycle.test.ts",
|
||||||
|
"test:passive-actions": "tsx scripts/passive-actions.test.ts",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"workflows:smoke": "tsx src/workflows/smoke-test.ts",
|
"workflows:smoke": "tsx src/workflows/smoke-test.ts",
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { careerTransitionReducer } from "../src/missions/career-transition/reducer.js";
|
||||||
|
import { interviewToOfferReducer } from "../src/missions/interview-to-offer/reducer.js";
|
||||||
|
import { personalBrandOpportunityReducer } from "../src/missions/personal-brand-opportunity-engine/reducer.js";
|
||||||
|
import { promotionReadinessReducer } from "../src/missions/promotion-readiness/reducer.js";
|
||||||
|
import { salaryNegotiationReducer } from "../src/missions/salary-negotiation-war-room/reducer.js";
|
||||||
|
import type { GrowActiveMission } from "../src/actors/missions/types.js";
|
||||||
|
import type { MissionReducer } from "../src/missions/reducer-types.js";
|
||||||
|
import type { MissionReducerContext } from "../src/missions/reducer-types.js";
|
||||||
|
|
||||||
|
function missionFor(missionId: GrowActiveMission["missionId"], actorType: GrowActiveMission["actorType"]): GrowActiveMission {
|
||||||
|
return {
|
||||||
|
instanceId: `mission-${missionId}-test`,
|
||||||
|
missionId,
|
||||||
|
workflowId: missionId,
|
||||||
|
title: missionId,
|
||||||
|
shortTitle: missionId,
|
||||||
|
status: "active",
|
||||||
|
progressPercent: 0,
|
||||||
|
currentStageId: "resume",
|
||||||
|
actorType,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mission = missionFor("interview-to-offer", "interviewToOfferMissionActor");
|
||||||
|
|
||||||
|
function ctxWithMission(activeMission: GrowActiveMission, event: Partial<MissionReducerContext["event"]> & { source: string; type: string; payload?: Record<string, unknown> }): MissionReducerContext {
|
||||||
|
return {
|
||||||
|
userId: "user_test",
|
||||||
|
activeMission,
|
||||||
|
event: {
|
||||||
|
id: `event-${activeMission.missionId}-${event.type}`,
|
||||||
|
userId: "user_test",
|
||||||
|
orgId: null,
|
||||||
|
source: event.source,
|
||||||
|
type: event.type,
|
||||||
|
category: "service",
|
||||||
|
occurredAt: new Date(),
|
||||||
|
receivedAt: new Date(),
|
||||||
|
mission: event.mission,
|
||||||
|
subject: null,
|
||||||
|
correlation: null,
|
||||||
|
payload: event.payload ?? {},
|
||||||
|
raw: {},
|
||||||
|
dedupeKey: null,
|
||||||
|
processingStatus: "pending",
|
||||||
|
processingError: null,
|
||||||
|
processedAt: null,
|
||||||
|
},
|
||||||
|
qscoreSignals: [],
|
||||||
|
insight: {
|
||||||
|
summary: "test insight",
|
||||||
|
confidence: "low",
|
||||||
|
recommendedActions: [],
|
||||||
|
missionStageHints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ctx(event: Partial<MissionReducerContext["event"]> & { source: string; type: string; payload?: Record<string, unknown> }): MissionReducerContext {
|
||||||
|
return ctxWithMission(mission, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const interviewFeedbackPayload = {
|
||||||
|
review: {
|
||||||
|
status: "completed",
|
||||||
|
overall_score: 72,
|
||||||
|
weak_areas: ["impact metrics", "ownership clarity"],
|
||||||
|
proof_gaps: ["no scale numbers"],
|
||||||
|
story_issues: ["STAR structure is loose"],
|
||||||
|
summary: "Good direction, but missing measurable proof.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleplayFeedbackPayload = {
|
||||||
|
review: {
|
||||||
|
status: "completed",
|
||||||
|
weak_areas: ["concision", "objection handling"],
|
||||||
|
story_gaps: ["needs clearer tradeoff story"],
|
||||||
|
summary: "Good empathy, but answers need tighter story framing.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducerCases: Array<{
|
||||||
|
name: string;
|
||||||
|
reducer: MissionReducer;
|
||||||
|
mission: GrowActiveMission;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: "interview to offer",
|
||||||
|
reducer: interviewToOfferReducer,
|
||||||
|
mission,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "career transition",
|
||||||
|
reducer: careerTransitionReducer,
|
||||||
|
mission: missionFor("career-transition", "careerTransitionMissionActor"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "promotion readiness",
|
||||||
|
reducer: promotionReadinessReducer,
|
||||||
|
mission: missionFor("promotion-readiness", "promotionReadinessMissionActor"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "salary negotiation",
|
||||||
|
reducer: salaryNegotiationReducer,
|
||||||
|
mission: missionFor("salary-negotiation-war-room", "salaryNegotiationWarRoomMissionActor"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "personal brand",
|
||||||
|
reducer: personalBrandOpportunityReducer,
|
||||||
|
mission: missionFor("personal-brand-opportunity-engine", "personalBrandOpportunityMissionActor"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const resumeResult = interviewToOfferReducer.reduce(ctx({
|
||||||
|
source: "resume-builder",
|
||||||
|
type: "resume.analysis.completed",
|
||||||
|
payload: {
|
||||||
|
analysis: {
|
||||||
|
summary: "Strong backend platform project.",
|
||||||
|
strengths: ["Built an event-driven backend"],
|
||||||
|
gaps: ["Add impact metrics"],
|
||||||
|
missing_keywords: ["Kafka", "AWS"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const proofPractice = resumeResult.actions.find((action) => action.payload?.passiveAction === "resume_analysis_to_interview_practice");
|
||||||
|
assert.ok(proofPractice, "resume analysis should create an interview practice passive action");
|
||||||
|
assert.equal(proofPractice?.serviceId, "interview-service");
|
||||||
|
assert.equal(proofPractice?.toolName, "interview.configure_practice");
|
||||||
|
assert.match(String(proofPractice?.payload?.prompt), /event-driven backend/i);
|
||||||
|
|
||||||
|
const interviewResult = interviewToOfferReducer.reduce(ctx({
|
||||||
|
source: "interview-service",
|
||||||
|
type: "interview.feedback.generated",
|
||||||
|
payload: interviewFeedbackPayload,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resumeUpgrade = interviewResult.actions.find((action) => action.payload?.passiveAction === "interview_feedback_to_resume_upgrade");
|
||||||
|
assert.ok(resumeUpgrade, "interview feedback should create a resume upgrade passive action");
|
||||||
|
assert.equal(resumeUpgrade?.mode, "approval_required");
|
||||||
|
assert.equal(resumeUpgrade?.serviceId, "resume-service");
|
||||||
|
assert.deepEqual(resumeUpgrade?.payload?.missingProof, ["no scale numbers"]);
|
||||||
|
assert.deepEqual(resumeUpgrade?.payload?.storyIssues, ["STAR structure is loose", "add measurable impact proof"]);
|
||||||
|
|
||||||
|
const roleplayResult = interviewToOfferReducer.reduce(ctx({
|
||||||
|
source: "roleplay-service",
|
||||||
|
type: "roleplay.feedback.generated",
|
||||||
|
payload: roleplayFeedbackPayload,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const storyArtifact = roleplayResult.artifacts.find((artifact) => artifact.type === "story_bank_update");
|
||||||
|
const communicationDrill = roleplayResult.actions.find((action) => action.payload?.passiveAction === "roleplay_feedback_to_communication_drill");
|
||||||
|
assert.ok(storyArtifact, "roleplay feedback should create a story bank artifact");
|
||||||
|
assert.ok(communicationDrill, "roleplay feedback should create a communication drill passive action");
|
||||||
|
assert.equal(communicationDrill?.serviceId, "interview-service");
|
||||||
|
assert.equal(communicationDrill?.toolName, "interview.configure_practice");
|
||||||
|
assert.deepEqual(communicationDrill?.payload?.storyIssues, ["needs clearer tradeoff story", "tighten STAR story structure"]);
|
||||||
|
|
||||||
|
for (const testCase of reducerCases) {
|
||||||
|
const reducerResumeResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
|
||||||
|
source: "resume-builder",
|
||||||
|
type: "resume.analysis.completed",
|
||||||
|
payload: {
|
||||||
|
analysis: {
|
||||||
|
summary: "Strong backend platform project.",
|
||||||
|
strengths: ["Built an event-driven backend"],
|
||||||
|
gaps: ["Add impact metrics"],
|
||||||
|
missing_keywords: ["Kafka", "AWS"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
assert.ok(
|
||||||
|
reducerResumeResult.actions.some((action) => action.payload?.passiveAction === "resume_analysis_to_interview_practice"),
|
||||||
|
`${testCase.name} resume analysis should create an interview practice passive action`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const reducerInterviewResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
|
||||||
|
source: "interview-service",
|
||||||
|
type: "interview.feedback.generated",
|
||||||
|
payload: interviewFeedbackPayload,
|
||||||
|
}));
|
||||||
|
assert.ok(
|
||||||
|
reducerInterviewResult.actions.some((action) => action.payload?.passiveAction === "interview_feedback_to_resume_upgrade"),
|
||||||
|
`${testCase.name} interview feedback should create a resume upgrade passive action`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const reducerRoleplayResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
|
||||||
|
source: "roleplay-service",
|
||||||
|
type: "roleplay.feedback.generated",
|
||||||
|
payload: roleplayFeedbackPayload,
|
||||||
|
}));
|
||||||
|
assert.ok(
|
||||||
|
reducerRoleplayResult.actions.some((action) => action.payload?.passiveAction === "roleplay_feedback_to_communication_drill"),
|
||||||
|
`${testCase.name} roleplay feedback should create a communication drill passive action`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
reducerRoleplayResult.artifacts.some((artifact) => artifact.type === "story_bank_update"),
|
||||||
|
`${testCase.name} roleplay feedback should create a story bank update artifact`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("passive-actions tests passed");
|
||||||
|
process.exit(0);
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
||||||
import { actionForAgent, extractResumeSignals, extractWeakAreas, isInterviewEvent, isRelevantServiceEvent, isResumeEvent, isRoleplayEvent, missionExplicitlyMatches, serviceHref } from "../reducer-helpers.js";
|
import {
|
||||||
|
actionForAgent,
|
||||||
|
extractResumeSignals,
|
||||||
|
extractWeakAreas,
|
||||||
|
isFeedbackEvent,
|
||||||
|
isInterviewEvent,
|
||||||
|
isRelevantServiceEvent,
|
||||||
|
isResumeEvent,
|
||||||
|
isRoleplayEvent,
|
||||||
|
missionExplicitlyMatches,
|
||||||
|
passiveInterviewFeedbackResumeUpgrade,
|
||||||
|
passiveResumeAnalysisInterviewPractice,
|
||||||
|
passiveRoleplayFeedbackStoryBank,
|
||||||
|
serviceHref,
|
||||||
|
} from "../reducer-helpers.js";
|
||||||
|
|
||||||
export const careerTransitionReducer: MissionReducer = {
|
export const careerTransitionReducer: MissionReducer = {
|
||||||
missionId: "career-transition",
|
missionId: "career-transition",
|
||||||
@@ -48,6 +62,14 @@ export const careerTransitionReducer: MissionReducer = {
|
|||||||
priority: 95,
|
priority: 95,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passiveResumeAnalysisInterviewPractice({
|
||||||
|
missionId: "career-transition",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "interview",
|
||||||
|
priority: 98,
|
||||||
|
}));
|
||||||
eventMessage = "Transferable skills map created; repositioned resume action is ready.";
|
eventMessage = "Transferable skills map created; repositioned resume action is ready.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +78,7 @@ export const careerTransitionReducer: MissionReducer = {
|
|||||||
eventMessage = "Adjacent-role interview practice started.";
|
eventMessage = "Adjacent-role interview practice started.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterviewEvent(event.source, type) && type.includes("review_completed")) {
|
if (isInterviewEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Adjacent-role credibility checked." });
|
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Adjacent-role credibility checked." });
|
||||||
stagePatches.push({ stageId: "roleplay", status: "ready", progressPercent: 0, outputSummary: "Practice the 'why I am switching' narrative next." });
|
stagePatches.push({ stageId: "roleplay", status: "ready", progressPercent: 0, outputSummary: "Practice the 'why I am switching' narrative next." });
|
||||||
@@ -74,12 +96,30 @@ export const careerTransitionReducer: MissionReducer = {
|
|||||||
priority: 92,
|
priority: 92,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passiveInterviewFeedbackResumeUpgrade({
|
||||||
|
missionId: "career-transition",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "resume",
|
||||||
|
priority: 104,
|
||||||
|
}));
|
||||||
eventMessage = "Career transition interview feedback produced the next pitch-practice action.";
|
eventMessage = "Career transition interview feedback produced the next pitch-practice action.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoleplayEvent(event.source, type) && type.includes("review_completed")) {
|
if (isRoleplayEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
|
const passive = passiveRoleplayFeedbackStoryBank({
|
||||||
|
missionId: "career-transition",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "roleplay",
|
||||||
|
priority: 94,
|
||||||
|
});
|
||||||
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Transition pitch practice reviewed." });
|
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Transition pitch practice reviewed." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Transition confidence signals updated." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Transition confidence signals updated." });
|
||||||
|
artifacts.push(passive.artifact);
|
||||||
|
actions.push(passive.action);
|
||||||
eventMessage = "Transition narrative practice completed.";
|
eventMessage = "Transition narrative practice completed.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { getString } from "../../events/envelope.js";
|
|||||||
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
||||||
import {
|
import {
|
||||||
actionForAgent,
|
actionForAgent,
|
||||||
|
extractMissingProof,
|
||||||
extractOverallScore,
|
extractOverallScore,
|
||||||
|
extractResumeProofPoints,
|
||||||
extractResumeSignals,
|
extractResumeSignals,
|
||||||
|
extractStoryIssues,
|
||||||
extractWeakAreas,
|
extractWeakAreas,
|
||||||
isInterviewEvent,
|
isInterviewEvent,
|
||||||
isQscoreEvent,
|
isQscoreEvent,
|
||||||
@@ -27,6 +30,10 @@ function reviewSummary(payload: Record<string, unknown>) {
|
|||||||
return summary ?? "Mock interview review completed.";
|
return summary ?? "Mock interview review completed.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFeedbackEvent(type: string) {
|
||||||
|
return type.includes("review_completed") || type.includes("review.completed") || type.includes("feedback.generated");
|
||||||
|
}
|
||||||
|
|
||||||
export const interviewToOfferReducer: MissionReducer = {
|
export const interviewToOfferReducer: MissionReducer = {
|
||||||
missionId: "interview-to-offer",
|
missionId: "interview-to-offer",
|
||||||
accepts: acceptsMission,
|
accepts: acceptsMission,
|
||||||
@@ -47,6 +54,7 @@ export const interviewToOfferReducer: MissionReducer = {
|
|||||||
|
|
||||||
if (isResumeEvent(event.source, type) && (type.includes("analysis_completed") || type.includes("analysis.complete") || type.includes("analyzed"))) {
|
if (isResumeEvent(event.source, type) && (type.includes("analysis_completed") || type.includes("analysis.complete") || type.includes("analyzed"))) {
|
||||||
const signals = extractResumeSignals(payload);
|
const signals = extractResumeSignals(payload);
|
||||||
|
const proofPoints = extractResumeProofPoints(payload);
|
||||||
stagePatches.push({ stageId: "resume", status: "done", progressPercent: 100, outputSummary: "Resume talking points and fit scan are ready." });
|
stagePatches.push({ stageId: "resume", status: "done", progressPercent: 100, outputSummary: "Resume talking points and fit scan are ready." });
|
||||||
stagePatches.push({ stageId: "interview", status: "ready", progressPercent: 0 });
|
stagePatches.push({ stageId: "interview", status: "ready", progressPercent: 0 });
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
@@ -69,6 +77,29 @@ export const interviewToOfferReducer: MissionReducer = {
|
|||||||
priority: 92,
|
priority: 92,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(actionForAgent("interview-to-offer", "interview", {
|
||||||
|
stageId: "interview",
|
||||||
|
serviceId: "interview-service",
|
||||||
|
toolName: "interview.configure_practice",
|
||||||
|
mode: "suggestion",
|
||||||
|
title: "Practice explaining your strongest resume proof",
|
||||||
|
body: proofPoints.strengths.length
|
||||||
|
? `Run a mock focused on ${proofPoints.strengths.slice(0, 2).join(" and ")} so your resume turns into interview-ready stories.`
|
||||||
|
: "Run a resume-led mock interview so your strongest proof turns into interview-ready stories.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "resume_analysis_to_interview_practice",
|
||||||
|
resumeSignals: signals,
|
||||||
|
proofPoints,
|
||||||
|
prompt: proofPoints.strengths[0]
|
||||||
|
? `Practice explaining ${proofPoints.strengths[0]} with clear ownership, impact, and tradeoffs.`
|
||||||
|
: "Practice explaining your strongest resume project with clear ownership, impact, and tradeoffs.",
|
||||||
|
href: serviceHref("interview", activeMission.instanceId, activeMission.missionId, "interview"),
|
||||||
|
},
|
||||||
|
sourceEventId: event.id,
|
||||||
|
idempotencyKey: `${activeMission.instanceId}:resume-analysis:proof-interview:${event.id}`,
|
||||||
|
priority: 98,
|
||||||
|
urgency: "today",
|
||||||
|
}));
|
||||||
eventMessage = "Resume fit scan completed; mock interview is ready to run.";
|
eventMessage = "Resume fit scan completed; mock interview is ready to run.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +113,10 @@ export const interviewToOfferReducer: MissionReducer = {
|
|||||||
eventMessage = "Mock interview completed; waiting for review.";
|
eventMessage = "Mock interview completed; waiting for review.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterviewEvent(event.source, type) && (type.includes("review_completed") || type.includes("review.completed"))) {
|
if (isInterviewEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
|
const missingProof = extractMissingProof(payload);
|
||||||
|
const storyIssues = extractStoryIssues(payload);
|
||||||
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: reviewSummary(payload) });
|
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: reviewSummary(payload) });
|
||||||
stagePatches.push({ stageId: "roleplay", status: "ready", progressPercent: 0, outputSummary: "Practice the communication gaps surfaced by interview feedback." });
|
stagePatches.push({ stageId: "roleplay", status: "ready", progressPercent: 0, outputSummary: "Practice the communication gaps surfaced by interview feedback." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 60, outputSummary: "Readiness signals updated from interview review." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 60, outputSummary: "Readiness signals updated from interview review." });
|
||||||
@@ -99,14 +132,27 @@ export const interviewToOfferReducer: MissionReducer = {
|
|||||||
serviceId: "resume-service",
|
serviceId: "resume-service",
|
||||||
toolName: "resume.create_version_prompt_draft",
|
toolName: "resume.create_version_prompt_draft",
|
||||||
mode: "approval_required",
|
mode: "approval_required",
|
||||||
title: "Create a tailored resume version from this interview feedback?",
|
title: "Draft stronger resume bullets from interview feedback?",
|
||||||
body: weakAreas.length
|
body: [...weakAreas, ...missingProof, ...storyIssues].length
|
||||||
? `The interview exposed ${weakAreas.slice(0, 3).join(", ")}. Approve a Resume Agent draft that turns this feedback into targeted bullets and talking points.`
|
? `Approve a Resume Agent draft that fixes ${[...weakAreas, ...missingProof, ...storyIssues].slice(0, 3).join(", ")} with stronger bullets and proof.`
|
||||||
: "Approve a Resume Agent draft that turns the interview feedback into targeted bullets and talking points.",
|
: "Approve a Resume Agent draft that turns the interview feedback into stronger bullets and talking points.",
|
||||||
payload: { weakAreas, sourceReviewEventId: event.id, href: serviceHref("resume", activeMission.instanceId, activeMission.missionId, "resume") },
|
prompt: "Create a resume upgrade draft from this interview feedback. Focus on measurable impact, ownership, missing proof, and reusable talking points.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "interview_feedback_to_resume_upgrade",
|
||||||
|
weakAreas,
|
||||||
|
missingProof,
|
||||||
|
storyIssues,
|
||||||
|
sourceReviewEventId: event.id,
|
||||||
|
draftInstructions: [
|
||||||
|
"Rewrite weak bullets with action, scope, metric, and result.",
|
||||||
|
"Add proof for any interview gaps that lacked evidence.",
|
||||||
|
"Create talking points that match the feedback themes.",
|
||||||
|
],
|
||||||
|
href: serviceHref("resume", activeMission.instanceId, activeMission.missionId, "resume"),
|
||||||
|
},
|
||||||
sourceEventId: event.id,
|
sourceEventId: event.id,
|
||||||
idempotencyKey: `${activeMission.instanceId}:interview-review:tailor-resume:${event.id}`,
|
idempotencyKey: `${activeMission.instanceId}:interview-review:tailor-resume:${event.id}`,
|
||||||
priority: 100,
|
priority: 108,
|
||||||
urgency: "now",
|
urgency: "now",
|
||||||
}));
|
}));
|
||||||
if (weakAreas.some((area) => /communication|story|clarity|confidence|concise/i.test(area))) {
|
if (weakAreas.some((area) => /communication|story|clarity|confidence|concise/i.test(area))) {
|
||||||
@@ -127,9 +173,45 @@ export const interviewToOfferReducer: MissionReducer = {
|
|||||||
eventMessage = "Interview review completed; resume and roleplay next actions were created.";
|
eventMessage = "Interview review completed; resume and roleplay next actions were created.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoleplayEvent(event.source, type) && type.includes("review_completed")) {
|
if (isRoleplayEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
|
const weakAreas = extractWeakAreas(payload);
|
||||||
|
const storyIssues = extractStoryIssues(payload);
|
||||||
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Communication drill reviewed." });
|
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Communication drill reviewed." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 75, outputSummary: "Communication readiness updated." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 75, outputSummary: "Communication readiness updated." });
|
||||||
|
artifacts.push({
|
||||||
|
type: "story_bank_update",
|
||||||
|
title: "Story bank updates from roleplay feedback",
|
||||||
|
stageId: "roleplay",
|
||||||
|
summary: [...weakAreas, ...storyIssues].length
|
||||||
|
? `Turn these into reusable STAR stories: ${[...weakAreas, ...storyIssues].slice(0, 5).join(", ")}`
|
||||||
|
: "Roleplay feedback captured story bank improvements for future interviews.",
|
||||||
|
metadata: { sourceEventId: event.id, weakAreas, storyIssues, payload },
|
||||||
|
});
|
||||||
|
actions.push(actionForAgent("interview-to-offer", "interview", {
|
||||||
|
stageId: "interview",
|
||||||
|
serviceId: "interview-service",
|
||||||
|
toolName: "interview.configure_practice",
|
||||||
|
mode: "suggestion",
|
||||||
|
title: "Run a story-bank recovery mock",
|
||||||
|
body: [...weakAreas, ...storyIssues].length
|
||||||
|
? `Practice the communication gaps from roleplay: ${[...weakAreas, ...storyIssues].slice(0, 3).join(", ")}.`
|
||||||
|
: "Run a targeted mock interview that converts roleplay feedback into reusable STAR stories.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "roleplay_feedback_to_communication_drill",
|
||||||
|
weakAreas,
|
||||||
|
storyIssues,
|
||||||
|
storyBankInstructions: [
|
||||||
|
"Convert each weak communication moment into a STAR story prompt.",
|
||||||
|
"Practice the answer in an interview setting.",
|
||||||
|
"Save the strongest version as reusable story-bank material.",
|
||||||
|
],
|
||||||
|
href: serviceHref("interview", activeMission.instanceId, activeMission.missionId, "interview"),
|
||||||
|
},
|
||||||
|
sourceEventId: event.id,
|
||||||
|
idempotencyKey: `${activeMission.instanceId}:roleplay-review:story-bank-interview:${event.id}`,
|
||||||
|
priority: 96,
|
||||||
|
urgency: "today",
|
||||||
|
}));
|
||||||
eventMessage = "Roleplay review improved interview communication readiness.";
|
eventMessage = "Roleplay review improved interview communication readiness.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
||||||
import { actionForAgent, extractResumeSignals, extractWeakAreas, isInterviewEvent, isRelevantServiceEvent, isResumeEvent, isRoleplayEvent, missionDetailHref, missionExplicitlyMatches, serviceHref } from "../reducer-helpers.js";
|
import {
|
||||||
|
actionForAgent,
|
||||||
|
extractResumeSignals,
|
||||||
|
extractWeakAreas,
|
||||||
|
isFeedbackEvent,
|
||||||
|
isInterviewEvent,
|
||||||
|
isRelevantServiceEvent,
|
||||||
|
isResumeEvent,
|
||||||
|
isRoleplayEvent,
|
||||||
|
missionDetailHref,
|
||||||
|
missionExplicitlyMatches,
|
||||||
|
passiveInterviewFeedbackResumeUpgrade,
|
||||||
|
passiveResumeAnalysisInterviewPractice,
|
||||||
|
passiveRoleplayFeedbackStoryBank,
|
||||||
|
serviceHref,
|
||||||
|
} from "../reducer-helpers.js";
|
||||||
|
|
||||||
export const personalBrandOpportunityReducer: MissionReducer = {
|
export const personalBrandOpportunityReducer: MissionReducer = {
|
||||||
missionId: "personal-brand-opportunity-engine",
|
missionId: "personal-brand-opportunity-engine",
|
||||||
@@ -48,14 +63,31 @@ export const personalBrandOpportunityReducer: MissionReducer = {
|
|||||||
priority: 92,
|
priority: 92,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passiveResumeAnalysisInterviewPractice({
|
||||||
|
missionId: "personal-brand-opportunity-engine",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "interview",
|
||||||
|
priority: 90,
|
||||||
|
}));
|
||||||
eventMessage = "Resume proof points created a profile positioning action.";
|
eventMessage = "Resume proof points created a profile positioning action.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoleplayEvent(event.source, type) && type.includes("review_completed")) {
|
if (isRoleplayEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
|
const passive = passiveRoleplayFeedbackStoryBank({
|
||||||
|
missionId: "personal-brand-opportunity-engine",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "roleplay",
|
||||||
|
priority: 92,
|
||||||
|
});
|
||||||
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Networking pitch reviewed." });
|
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Networking pitch reviewed." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Brand voice/readiness signals updated." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Brand voice/readiness signals updated." });
|
||||||
artifacts.push({ type: "networking_scripts", title: "Networking script improvements", stageId: "roleplay", summary: weakAreas.length ? `Improve: ${weakAreas.join(", ")}` : "Networking pitch practice completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
artifacts.push({ type: "networking_scripts", title: "Networking script improvements", stageId: "roleplay", summary: weakAreas.length ? `Improve: ${weakAreas.join(", ")}` : "Networking pitch practice completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
||||||
|
artifacts.push(passive.artifact);
|
||||||
actions.push(actionForAgent("personal-brand-opportunity-engine", "planner", {
|
actions.push(actionForAgent("personal-brand-opportunity-engine", "planner", {
|
||||||
stageId: "positioning",
|
stageId: "positioning",
|
||||||
mode: "suggestion",
|
mode: "suggestion",
|
||||||
@@ -67,13 +99,22 @@ export const personalBrandOpportunityReducer: MissionReducer = {
|
|||||||
priority: 82,
|
priority: 82,
|
||||||
urgency: "soon",
|
urgency: "soon",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passive.action);
|
||||||
eventMessage = "Networking pitch review created brand content next steps.";
|
eventMessage = "Networking pitch review created brand content next steps.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterviewEvent(event.source, type) && type.includes("review_completed")) {
|
if (isInterviewEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Credibility signals mined from interview review." });
|
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Credibility signals mined from interview review." });
|
||||||
artifacts.push({ type: "credibility_signal_map", title: "Credibility signal map", stageId: "interview", summary: weakAreas.length ? `Recurring gaps/themes: ${weakAreas.join(", ")}` : "Interview review mined for positioning signals.", metadata: { sourceEventId: event.id, weakAreas } });
|
artifacts.push({ type: "credibility_signal_map", title: "Credibility signal map", stageId: "interview", summary: weakAreas.length ? `Recurring gaps/themes: ${weakAreas.join(", ")}` : "Interview review mined for positioning signals.", metadata: { sourceEventId: event.id, weakAreas } });
|
||||||
|
actions.push(passiveInterviewFeedbackResumeUpgrade({
|
||||||
|
missionId: "personal-brand-opportunity-engine",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "resume",
|
||||||
|
priority: 98,
|
||||||
|
}));
|
||||||
eventMessage = "Interview feedback was mined for brand positioning signals.";
|
eventMessage = "Interview feedback was mined for brand positioning signals.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
||||||
import { actionForAgent, extractResumeSignals, extractWeakAreas, isInterviewEvent, isRelevantServiceEvent, isResumeEvent, isRoleplayEvent, missionExplicitlyMatches, serviceHref } from "../reducer-helpers.js";
|
import {
|
||||||
|
actionForAgent,
|
||||||
|
extractResumeSignals,
|
||||||
|
extractWeakAreas,
|
||||||
|
isFeedbackEvent,
|
||||||
|
isInterviewEvent,
|
||||||
|
isRelevantServiceEvent,
|
||||||
|
isResumeEvent,
|
||||||
|
isRoleplayEvent,
|
||||||
|
missionExplicitlyMatches,
|
||||||
|
passiveInterviewFeedbackResumeUpgrade,
|
||||||
|
passiveResumeAnalysisInterviewPractice,
|
||||||
|
passiveRoleplayFeedbackStoryBank,
|
||||||
|
serviceHref,
|
||||||
|
} from "../reducer-helpers.js";
|
||||||
|
|
||||||
export const promotionReadinessReducer: MissionReducer = {
|
export const promotionReadinessReducer: MissionReducer = {
|
||||||
missionId: "promotion-readiness",
|
missionId: "promotion-readiness",
|
||||||
@@ -48,14 +62,31 @@ export const promotionReadinessReducer: MissionReducer = {
|
|||||||
priority: 94,
|
priority: 94,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passiveResumeAnalysisInterviewPractice({
|
||||||
|
missionId: "promotion-readiness",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "interview",
|
||||||
|
priority: 91,
|
||||||
|
}));
|
||||||
eventMessage = "Promotion evidence packet is ready; manager conversation practice is next.";
|
eventMessage = "Promotion evidence packet is ready; manager conversation practice is next.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoleplayEvent(event.source, type) && type.includes("review_completed")) {
|
if (isRoleplayEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
|
const passive = passiveRoleplayFeedbackStoryBank({
|
||||||
|
missionId: "promotion-readiness",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "roleplay",
|
||||||
|
priority: 95,
|
||||||
|
});
|
||||||
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Manager conversation drill reviewed." });
|
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Manager conversation drill reviewed." });
|
||||||
stagePatches.push({ stageId: "interview", status: "ready", progressPercent: 0, outputSummary: "Practice leadership narratives next if gaps remain." });
|
stagePatches.push({ stageId: "interview", status: "ready", progressPercent: 0, outputSummary: "Practice leadership narratives next if gaps remain." });
|
||||||
artifacts.push({ type: "manager_conversation_script", title: "Manager conversation script", stageId: "roleplay", summary: weakAreas.length ? `Follow-up focus: ${weakAreas.join(", ")}` : "Manager conversation review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
artifacts.push({ type: "manager_conversation_script", title: "Manager conversation script", stageId: "roleplay", summary: weakAreas.length ? `Follow-up focus: ${weakAreas.join(", ")}` : "Manager conversation review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
||||||
|
artifacts.push(passive.artifact);
|
||||||
actions.push(actionForAgent("promotion-readiness", "interview", {
|
actions.push(actionForAgent("promotion-readiness", "interview", {
|
||||||
stageId: "interview",
|
stageId: "interview",
|
||||||
serviceId: "interview-service",
|
serviceId: "interview-service",
|
||||||
@@ -69,14 +100,23 @@ export const promotionReadinessReducer: MissionReducer = {
|
|||||||
priority: 86,
|
priority: 86,
|
||||||
urgency: "soon",
|
urgency: "soon",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passive.action);
|
||||||
eventMessage = "Manager conversation review updated promotion readiness.";
|
eventMessage = "Manager conversation review updated promotion readiness.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterviewEvent(event.source, type) && type.includes("review_completed")) {
|
if (isInterviewEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Leadership communication gap check completed." });
|
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Leadership communication gap check completed." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 75, outputSummary: "Leadership readiness signals updated." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 75, outputSummary: "Leadership readiness signals updated." });
|
||||||
artifacts.push({ type: "leadership_gap_map", title: "Leadership gap map", stageId: "interview", summary: weakAreas.length ? weakAreas.join(", ") : "Leadership practice review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
artifacts.push({ type: "leadership_gap_map", title: "Leadership gap map", stageId: "interview", summary: weakAreas.length ? weakAreas.join(", ") : "Leadership practice review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
||||||
|
actions.push(passiveInterviewFeedbackResumeUpgrade({
|
||||||
|
missionId: "promotion-readiness",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "resume",
|
||||||
|
priority: 102,
|
||||||
|
}));
|
||||||
eventMessage = "Leadership practice review updated the promotion gap map.";
|
eventMessage = "Leadership practice review updated the promotion gap map.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { asRecord, getNumber, getString } from "../events/envelope.js";
|
import { asRecord, getNumber, getString } from "../events/envelope.js";
|
||||||
import { buildServiceLink } from "../services/service-registry.js";
|
import { buildServiceLink } from "../services/service-registry.js";
|
||||||
import type { MissionActionPatch } from "./reducer-types.js";
|
import type { GrowActiveMission } from "../actors/missions/types.js";
|
||||||
|
import type { MissionActionPatch, MissionArtifactPatch } from "./reducer-types.js";
|
||||||
|
|
||||||
export function isResumeEvent(source: string, type: string) {
|
export function isResumeEvent(source: string, type: string) {
|
||||||
const value = source.toLowerCase();
|
const value = source.toLowerCase();
|
||||||
@@ -22,6 +23,10 @@ export function isQscoreEvent(source: string, type: string) {
|
|||||||
return value.includes("qscore") || type.startsWith("qscore.");
|
return value.includes("qscore") || type.startsWith("qscore.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFeedbackEvent(type: string) {
|
||||||
|
return type.includes("review_completed") || type.includes("review.completed") || type.includes("feedback.generated");
|
||||||
|
}
|
||||||
|
|
||||||
export function reviewRecord(payload: Record<string, unknown>) {
|
export function reviewRecord(payload: Record<string, unknown>) {
|
||||||
return asRecord(payload.review ?? payload.result ?? payload.data ?? payload);
|
return asRecord(payload.review ?? payload.result ?? payload.data ?? payload);
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,64 @@ export function extractWeakAreas(payload: Record<string, unknown>): string[] {
|
|||||||
return Array.from(new Set(areas)).slice(0, 5);
|
return Array.from(new Set(areas)).slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractStringListFromKeys(record: Record<string, unknown>, keys: string[]) {
|
||||||
|
const values: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
if (typeof item === "string" && item.trim()) values.push(item.trim());
|
||||||
|
else if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||||
|
const row = item as Record<string, unknown>;
|
||||||
|
const text = getString(row.title ?? row.name ?? row.label ?? row.summary ?? row.description ?? row.text);
|
||||||
|
if (text) values.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === "string" && value.trim()) {
|
||||||
|
values.push(...value.split(/[;\n]/).map((part) => part.trim()).filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(values)).slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractMissingProof(payload: Record<string, unknown>): string[] {
|
||||||
|
const review = reviewRecord(payload);
|
||||||
|
return extractStringListFromKeys(review, [
|
||||||
|
"missing_proof",
|
||||||
|
"missingProof",
|
||||||
|
"proof_gaps",
|
||||||
|
"proofGaps",
|
||||||
|
"evidence_gaps",
|
||||||
|
"evidenceGaps",
|
||||||
|
"missing_evidence",
|
||||||
|
"missingEvidence",
|
||||||
|
"gaps",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractStoryIssues(payload: Record<string, unknown>): string[] {
|
||||||
|
const review = reviewRecord(payload);
|
||||||
|
const values = extractStringListFromKeys(review, [
|
||||||
|
"story_issues",
|
||||||
|
"storyIssues",
|
||||||
|
"story_gaps",
|
||||||
|
"storyGaps",
|
||||||
|
"star_gaps",
|
||||||
|
"starGaps",
|
||||||
|
"communication_gaps",
|
||||||
|
"communicationGaps",
|
||||||
|
"recommendations",
|
||||||
|
]);
|
||||||
|
const summary = getString(review.summary ?? review.feedback_summary ?? review.overall_feedback);
|
||||||
|
if (summary) {
|
||||||
|
const lower = summary.toLowerCase();
|
||||||
|
if (lower.includes("star") || lower.includes("story")) values.push("tighten STAR story structure");
|
||||||
|
if (lower.includes("metric") || lower.includes("impact") || lower.includes("measurable")) values.push("add measurable impact proof");
|
||||||
|
if (lower.includes("ownership")) values.push("clarify ownership and scope");
|
||||||
|
}
|
||||||
|
return Array.from(new Set(values)).slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
export function extractResumeSignals(payload: Record<string, unknown>): string[] {
|
export function extractResumeSignals(payload: Record<string, unknown>): string[] {
|
||||||
const analysis = asRecord(payload.analysis ?? payload.result ?? payload.data ?? payload);
|
const analysis = asRecord(payload.analysis ?? payload.result ?? payload.data ?? payload);
|
||||||
const signals: string[] = [];
|
const signals: string[] = [];
|
||||||
@@ -80,6 +143,14 @@ export function extractResumeSignals(payload: Record<string, unknown>): string[]
|
|||||||
return signals.slice(0, 8);
|
return signals.slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractResumeProofPoints(payload: Record<string, unknown>) {
|
||||||
|
const analysis = asRecord(payload.analysis ?? payload.result ?? payload.data ?? payload);
|
||||||
|
const strengths = extractStringListFromKeys(analysis, ["strengths", "top_strengths", "strong_projects", "projects", "achievements"]);
|
||||||
|
const gaps = extractStringListFromKeys(analysis, ["gaps", "recommendations", "missing_keywords", "keyword_gaps", "weak_bullets"]);
|
||||||
|
const keywords = extractStringListFromKeys(analysis, ["keywords", "matched_keywords", "missing_keywords", "keyword_gaps"]);
|
||||||
|
return { strengths, gaps, keywords };
|
||||||
|
}
|
||||||
|
|
||||||
export function missionExplicitlyMatches(eventMission: unknown, missionId: string) {
|
export function missionExplicitlyMatches(eventMission: unknown, missionId: string) {
|
||||||
const mission = asRecord(eventMission);
|
const mission = asRecord(eventMission);
|
||||||
const explicit = getString(mission.missionId ?? mission.mission_id);
|
const explicit = getString(mission.missionId ?? mission.mission_id);
|
||||||
@@ -144,3 +215,127 @@ export function serviceHref(service: "resume" | "interview" | "roleplay" | "qsco
|
|||||||
export function missionDetailHref(missionInstanceId: string) {
|
export function missionDetailHref(missionInstanceId: string) {
|
||||||
return `/missions/${encodeURIComponent(missionInstanceId)}`;
|
return `/missions/${encodeURIComponent(missionInstanceId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function passiveResumeAnalysisInterviewPractice(input: {
|
||||||
|
missionId: string;
|
||||||
|
activeMission: GrowActiveMission;
|
||||||
|
eventId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
stageId?: string;
|
||||||
|
priority?: number;
|
||||||
|
}): MissionActionPatch {
|
||||||
|
const signals = extractResumeSignals(input.payload);
|
||||||
|
const proofPoints = extractResumeProofPoints(input.payload);
|
||||||
|
return actionForAgent(input.missionId, "interview", {
|
||||||
|
stageId: input.stageId ?? "interview",
|
||||||
|
serviceId: "interview-service",
|
||||||
|
toolName: "interview.configure_practice",
|
||||||
|
mode: "suggestion",
|
||||||
|
title: "Practice explaining your strongest resume proof",
|
||||||
|
body: proofPoints.strengths.length
|
||||||
|
? `Run a mock focused on ${proofPoints.strengths.slice(0, 2).join(" and ")} so your resume turns into interview-ready stories.`
|
||||||
|
: "Run a resume-led mock interview so your strongest proof turns into interview-ready stories.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "resume_analysis_to_interview_practice",
|
||||||
|
resumeSignals: signals,
|
||||||
|
proofPoints,
|
||||||
|
prompt: proofPoints.strengths[0]
|
||||||
|
? `Practice explaining ${proofPoints.strengths[0]} with clear ownership, impact, and tradeoffs.`
|
||||||
|
: "Practice explaining your strongest resume project with clear ownership, impact, and tradeoffs.",
|
||||||
|
href: serviceHref("interview", input.activeMission.instanceId, input.activeMission.missionId, input.stageId ?? "interview"),
|
||||||
|
},
|
||||||
|
sourceEventId: input.eventId,
|
||||||
|
idempotencyKey: `${input.activeMission.instanceId}:resume-analysis:proof-interview:${input.eventId}`,
|
||||||
|
priority: input.priority ?? 98,
|
||||||
|
urgency: "today",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passiveInterviewFeedbackResumeUpgrade(input: {
|
||||||
|
missionId: string;
|
||||||
|
activeMission: GrowActiveMission;
|
||||||
|
eventId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
stageId?: string;
|
||||||
|
priority?: number;
|
||||||
|
}): MissionActionPatch {
|
||||||
|
const weakAreas = extractWeakAreas(input.payload);
|
||||||
|
const missingProof = extractMissingProof(input.payload);
|
||||||
|
const storyIssues = extractStoryIssues(input.payload);
|
||||||
|
return actionForAgent(input.missionId, "resume", {
|
||||||
|
stageId: input.stageId ?? "resume",
|
||||||
|
serviceId: "resume-service",
|
||||||
|
toolName: "resume.create_version_prompt_draft",
|
||||||
|
mode: "approval_required",
|
||||||
|
title: "Draft stronger resume bullets from interview feedback?",
|
||||||
|
body: [...weakAreas, ...missingProof, ...storyIssues].length
|
||||||
|
? `Approve a Resume Agent draft that fixes ${[...weakAreas, ...missingProof, ...storyIssues].slice(0, 3).join(", ")} with stronger bullets and proof.`
|
||||||
|
: "Approve a Resume Agent draft that turns the interview feedback into stronger bullets and talking points.",
|
||||||
|
prompt: "Create a resume upgrade draft from this interview feedback. Focus on measurable impact, ownership, missing proof, and reusable talking points.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "interview_feedback_to_resume_upgrade",
|
||||||
|
weakAreas,
|
||||||
|
missingProof,
|
||||||
|
storyIssues,
|
||||||
|
sourceReviewEventId: input.eventId,
|
||||||
|
draftInstructions: [
|
||||||
|
"Rewrite weak bullets with action, scope, metric, and result.",
|
||||||
|
"Add proof for any interview gaps that lacked evidence.",
|
||||||
|
"Create talking points that match the feedback themes.",
|
||||||
|
],
|
||||||
|
href: serviceHref("resume", input.activeMission.instanceId, input.activeMission.missionId, input.stageId ?? "resume"),
|
||||||
|
},
|
||||||
|
sourceEventId: input.eventId,
|
||||||
|
idempotencyKey: `${input.activeMission.instanceId}:interview-review:tailor-resume:${input.eventId}`,
|
||||||
|
priority: input.priority ?? 108,
|
||||||
|
urgency: "now",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passiveRoleplayFeedbackStoryBank(input: {
|
||||||
|
missionId: string;
|
||||||
|
activeMission: GrowActiveMission;
|
||||||
|
eventId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
stageId?: string;
|
||||||
|
priority?: number;
|
||||||
|
}): { artifact: MissionArtifactPatch; action: MissionActionPatch } {
|
||||||
|
const weakAreas = extractWeakAreas(input.payload);
|
||||||
|
const storyIssues = extractStoryIssues(input.payload);
|
||||||
|
return {
|
||||||
|
artifact: {
|
||||||
|
type: "story_bank_update",
|
||||||
|
title: "Story bank updates from roleplay feedback",
|
||||||
|
stageId: input.stageId ?? "roleplay",
|
||||||
|
summary: [...weakAreas, ...storyIssues].length
|
||||||
|
? `Turn these into reusable STAR stories: ${[...weakAreas, ...storyIssues].slice(0, 5).join(", ")}`
|
||||||
|
: "Roleplay feedback captured story bank improvements for future interviews.",
|
||||||
|
metadata: { sourceEventId: input.eventId, weakAreas, storyIssues, payload: input.payload },
|
||||||
|
},
|
||||||
|
action: actionForAgent(input.missionId, "interview", {
|
||||||
|
stageId: "interview",
|
||||||
|
serviceId: "interview-service",
|
||||||
|
toolName: "interview.configure_practice",
|
||||||
|
mode: "suggestion",
|
||||||
|
title: "Run a story-bank recovery mock",
|
||||||
|
body: [...weakAreas, ...storyIssues].length
|
||||||
|
? `Practice the communication gaps from roleplay: ${[...weakAreas, ...storyIssues].slice(0, 3).join(", ")}.`
|
||||||
|
: "Run a targeted mock interview that converts roleplay feedback into reusable STAR stories.",
|
||||||
|
payload: {
|
||||||
|
passiveAction: "roleplay_feedback_to_communication_drill",
|
||||||
|
weakAreas,
|
||||||
|
storyIssues,
|
||||||
|
storyBankInstructions: [
|
||||||
|
"Convert each weak communication moment into a STAR story prompt.",
|
||||||
|
"Practice the answer in an interview setting.",
|
||||||
|
"Save the strongest version as reusable story-bank material.",
|
||||||
|
],
|
||||||
|
href: serviceHref("interview", input.activeMission.instanceId, input.activeMission.missionId, "interview"),
|
||||||
|
},
|
||||||
|
sourceEventId: input.eventId,
|
||||||
|
idempotencyKey: `${input.activeMission.instanceId}:roleplay-review:story-bank-interview:${input.eventId}`,
|
||||||
|
priority: input.priority ?? 96,
|
||||||
|
urgency: "today",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
import type { MissionReducer, MissionReduction, MissionStagePatch } from "../reducer-types.js";
|
||||||
import { actionForAgent, extractResumeSignals, extractWeakAreas, isInterviewEvent, isRelevantServiceEvent, isResumeEvent, isRoleplayEvent, missionExplicitlyMatches, serviceHref } from "../reducer-helpers.js";
|
import {
|
||||||
|
actionForAgent,
|
||||||
|
extractResumeSignals,
|
||||||
|
extractWeakAreas,
|
||||||
|
isFeedbackEvent,
|
||||||
|
isInterviewEvent,
|
||||||
|
isRelevantServiceEvent,
|
||||||
|
isResumeEvent,
|
||||||
|
isRoleplayEvent,
|
||||||
|
missionExplicitlyMatches,
|
||||||
|
passiveInterviewFeedbackResumeUpgrade,
|
||||||
|
passiveResumeAnalysisInterviewPractice,
|
||||||
|
passiveRoleplayFeedbackStoryBank,
|
||||||
|
serviceHref,
|
||||||
|
} from "../reducer-helpers.js";
|
||||||
|
|
||||||
export const salaryNegotiationReducer: MissionReducer = {
|
export const salaryNegotiationReducer: MissionReducer = {
|
||||||
missionId: "salary-negotiation-war-room",
|
missionId: "salary-negotiation-war-room",
|
||||||
@@ -48,6 +62,14 @@ export const salaryNegotiationReducer: MissionReducer = {
|
|||||||
priority: 96,
|
priority: 96,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passiveResumeAnalysisInterviewPractice({
|
||||||
|
missionId: "salary-negotiation-war-room",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "interview",
|
||||||
|
priority: 88,
|
||||||
|
}));
|
||||||
eventMessage = "Value evidence is ready for negotiation practice.";
|
eventMessage = "Value evidence is ready for negotiation practice.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +78,20 @@ export const salaryNegotiationReducer: MissionReducer = {
|
|||||||
eventMessage = "Negotiation drill started.";
|
eventMessage = "Negotiation drill started.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoleplayEvent(event.source, type) && type.includes("review_completed")) {
|
if (isRoleplayEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
const weakAreas = extractWeakAreas(payload);
|
const weakAreas = extractWeakAreas(payload);
|
||||||
|
const passive = passiveRoleplayFeedbackStoryBank({
|
||||||
|
missionId: "salary-negotiation-war-room",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "roleplay",
|
||||||
|
priority: 93,
|
||||||
|
});
|
||||||
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Negotiation drill reviewed." });
|
stagePatches.push({ stageId: "roleplay", status: "done", progressPercent: 100, outputSummary: "Negotiation drill reviewed." });
|
||||||
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Confidence signals updated." });
|
stagePatches.push({ stageId: "qscore", status: "in_progress", progressPercent: 70, outputSummary: "Confidence signals updated." });
|
||||||
artifacts.push({ type: "negotiation_objection_map", title: "Objection handling map", stageId: "roleplay", summary: weakAreas.length ? `Practice objections: ${weakAreas.join(", ")}` : "Negotiation practice review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
artifacts.push({ type: "negotiation_objection_map", title: "Objection handling map", stageId: "roleplay", summary: weakAreas.length ? `Practice objections: ${weakAreas.join(", ")}` : "Negotiation practice review completed.", metadata: { sourceEventId: event.id, weakAreas } });
|
||||||
|
artifacts.push(passive.artifact);
|
||||||
actions.push(actionForAgent("salary-negotiation-war-room", "roleplay", {
|
actions.push(actionForAgent("salary-negotiation-war-room", "roleplay", {
|
||||||
stageId: "roleplay",
|
stageId: "roleplay",
|
||||||
serviceId: "roleplay-service",
|
serviceId: "roleplay-service",
|
||||||
@@ -74,11 +105,20 @@ export const salaryNegotiationReducer: MissionReducer = {
|
|||||||
priority: 94,
|
priority: 94,
|
||||||
urgency: "today",
|
urgency: "today",
|
||||||
}));
|
}));
|
||||||
|
actions.push(passive.action);
|
||||||
eventMessage = "Negotiation drill review created the next objection-handling action.";
|
eventMessage = "Negotiation drill review created the next objection-handling action.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInterviewEvent(event.source, type) && type.includes("review_completed")) {
|
if (isInterviewEvent(event.source, type) && isFeedbackEvent(type)) {
|
||||||
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Communication confidence signal captured from interview review." });
|
stagePatches.push({ stageId: "interview", status: "done", progressPercent: 100, outputSummary: "Communication confidence signal captured from interview review." });
|
||||||
|
actions.push(passiveInterviewFeedbackResumeUpgrade({
|
||||||
|
missionId: "salary-negotiation-war-room",
|
||||||
|
activeMission,
|
||||||
|
eventId: event.id,
|
||||||
|
payload,
|
||||||
|
stageId: "resume",
|
||||||
|
priority: 99,
|
||||||
|
}));
|
||||||
eventMessage = "Interview feedback updated negotiation confidence signals.";
|
eventMessage = "Interview feedback updated negotiation confidence signals.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user