FN-5759: inject active-goal context across engine lanes

Enable always-on active-goal prompt injection and diagnostic wiring across triage/executor flows.

- Refactor goal-context injection so agent heartbeat, executor, and triage all apply the active-goal seam consistently.
- Extend goal injection diagnostics implementation and exports, plus anchoring audit touchpoints.
- Add and update tests for goal context injection behavior and diagnostics wiring.
- Update architecture and diagnostics docs to describe the new always-on seam.

Files changed:
 docs/architecture.md                               |  2 +-
 docs/diagnostics.md                                |  2 +-
 .../src/__tests__/goal-context-injection.test.ts   | 19 ++++-
 .../goal-injection-diagnostics-wiring.test.ts      | 98 +++++++++++++++++++++-
 packages/engine/src/agent-heartbeat.ts             | 31 ++-----
 packages/engine/src/executor.ts                    | 32 ++-----
 packages/engine/src/goal-anchoring-audit.ts        |  2 +-
 packages/engine/src/goal-injection-diagnostics.ts  | 42 +++++++++-
 packages/engine/src/index.ts                       |  2 +
 packages/engine/src/triage.ts                      | 28 +++++--
 10 files changed, 191 insertions(+), 67 deletions(-)

Fusion-Task-Id: FN-5759

Fusion-Task-Lineage: 1419de34-2e86-4459-bcd4-426f15c067fe
This commit is contained in:
gsxdsm
2026-05-30 21:54:06 -07:00
parent c9b159f4e5
commit d98ff8780c
10 changed files with 191 additions and 67 deletions

View File

@@ -436,7 +436,7 @@ Hybrid evaluator pipeline (FN-3389/FN-3391):
- `reviewer`
- `heartbeat`
- Integration points append the built plugin section to the role-specific system/task prompt only when contributions exist, preserving existing prompts when no plugins contribute.
- Executor and heartbeat system prompts also inject a shared `goalContext` dynamic layer via `buildGoalContextSection(...)`; when no active goals exist, no goal section is emitted.
- Executor, heartbeat, and planning (triage) system prompts inject a shared `goalContext` dynamic layer via the canonical `resolveAndEmitGoalContext(...)` seam (which uses `buildGoalContextSection(...)`); when no active goals exist, no goal section is emitted.
### Agent Permissions

View File

@@ -2,7 +2,7 @@
## Goal injection diagnostics (`[goal-injection]`)
Executor and heartbeat runs emit one goal-injection diagnostic with outcome `applied`, `no-goals`, or `disabled-or-failed`.
Executor, heartbeat, and planning runs emit one goal-injection diagnostic with outcome `applied`, `no-goals`, or `disabled-or-failed`.
- Run-audit event: `prompt:goal-injection` (`database` domain, target lane) with metadata `{ lane, outcome, goalCount, goalIds, truncated, reason?, errorClass?, runId?, agentId?, taskId? }`.
- Task log (executor lane with `taskId`): `[goal-injection] <outcome> count=<n> ids=<json-array> truncated=<bool> ...`.

View File

@@ -32,8 +32,17 @@ function buildHeartbeatPrompt(activeGoals: Goal[]): { goalContext: string; promp
return { goalContext, prompt: collapsePromptLayers(layers) };
}
function buildPlanningPrompt(activeGoals: Goal[]): { goalContext: string; prompt: string } {
const goalContext = buildGoalContextSection({ activeGoals }).text;
const layers = buildPromptLayers({
basePrompt: "PLANNING_BASE",
goalContext,
});
return { goalContext, prompt: collapsePromptLayers(layers) };
}
describe("goal context lane injection parity", () => {
it("injects byte-identical goal block across heartbeat and executor lanes", () => {
it("injects byte-identical goal block across heartbeat executor and planning lanes", () => {
const activeGoals = [
goal("G-001", "Ship CLI", "2026-01-01T00:00:00.000Z"),
goal("G-002", "Harden engine", "2026-01-02T00:00:00.000Z"),
@@ -42,21 +51,27 @@ describe("goal context lane injection parity", () => {
const expectedGoalBlock = buildGoalContextSection({ activeGoals }).text;
const executor = buildExecutorPrompt(activeGoals);
const heartbeat = buildHeartbeatPrompt(activeGoals);
const planning = buildPlanningPrompt(activeGoals);
expect(executor.goalContext).toBe(expectedGoalBlock);
expect(heartbeat.goalContext).toBe(expectedGoalBlock);
expect(planning.goalContext).toBe(expectedGoalBlock);
});
it("emits no goal header or blank-line artifact when active goals are empty", () => {
const executor = buildExecutorPrompt([]);
const heartbeat = buildHeartbeatPrompt([]);
const planning = buildPlanningPrompt([]);
expect(executor.goalContext).toBe("");
expect(heartbeat.goalContext).toBe("");
expect(planning.goalContext).toBe("");
expect(executor.prompt).toBe("EXECUTOR_BASE");
expect(heartbeat.prompt).toBe("HEARTBEAT_BASE");
expect(planning.prompt).toBe("PLANNING_BASE");
expect(executor.prompt).not.toContain("## Active Goals");
expect(heartbeat.prompt).not.toContain("## Active Goals");
expect(planning.prompt).not.toContain("## Active Goals");
});
it("uses shared formatter output without lane-local reformatting", () => {
@@ -65,8 +80,10 @@ describe("goal context lane injection parity", () => {
const helperOutput = buildGoalContextSection({ activeGoals }).text;
const executor = buildExecutorPrompt(activeGoals);
const heartbeat = buildHeartbeatPrompt(activeGoals);
const planning = buildPlanningPrompt(activeGoals);
expect(executor.goalContext).toEqual(helperOutput);
expect(heartbeat.goalContext).toEqual(helperOutput);
expect(planning.goalContext).toEqual(helperOutput);
});
});

View File

@@ -1,6 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import type { Goal } from "@fusion/core";
import { emitGoalInjectionDiagnostic, resolveGoalContextForDiagnostics } from "../goal-injection-diagnostics.js";
import {
emitGoalInjectionDiagnostic,
resolveAndEmitGoalContext,
resolveGoalContextForDiagnostics,
} from "../goal-injection-diagnostics.js";
function goal(id: string, title: string, createdAt: string): Goal {
return {
@@ -14,6 +18,98 @@ function goal(id: string, title: string, createdAt: string): Goal {
}
describe("goal injection diagnostics wiring seam", () => {
it("resolveAndEmitGoalContext emits applied diagnostics and audit for planning lane", async () => {
const goals = [goal("G-1", "one", "2026-01-01T00:00:00.000Z")];
const store = {
getGoalStore: () => ({ listGoals: () => goals }),
logEntry: vi.fn().mockResolvedValue(undefined),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
} as any;
const audit = { database: vi.fn().mockResolvedValue(undefined) } as any;
const resolution = await resolveAndEmitGoalContext({
lane: "planning",
store,
audit,
taskId: "FN-1",
runContext: { runId: "plan-run", agentId: "agent-1", taskId: "FN-1", phase: "plan" },
});
expect(resolution.classification).toMatchObject({ outcome: "applied", goalCount: 1, goalIds: ["G-1"] });
expect(audit.database).toHaveBeenCalledTimes(1);
expect(store.recordRunAuditEvent).toHaveBeenCalledTimes(1);
});
it("resolveAndEmitGoalContext emits no-goals semantics when goal store is empty", async () => {
const store = {
getGoalStore: () => ({ listGoals: () => [] }),
logEntry: vi.fn().mockResolvedValue(undefined),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
} as any;
const audit = { database: vi.fn().mockResolvedValue(undefined) } as any;
const resolution = await resolveAndEmitGoalContext({
lane: "executor",
store,
audit,
taskId: "FN-1",
runContext: { runId: "exec-run", agentId: "agent-1", taskId: "FN-1", phase: "execute" },
});
expect(resolution.goalContext).toBe("");
expect(resolution.classification).toMatchObject({ outcome: "no-goals", goalCount: 0, goalIds: [] });
expect(audit.database.mock.calls[0][0]).toMatchObject({
type: "goal:injection-skipped",
metadata: { lane: "executor", count: 0, reason: "no-active-goals" },
});
});
it("routes heartbeat executor and planning lanes through the same canonical helper", async () => {
const lanes = ["heartbeat", "executor", "planning"] as const;
for (const lane of lanes) {
const store = {
getGoalStore: () => ({ listGoals: () => [goal("G-1", "one", "2026-01-01T00:00:00.000Z")] }),
logEntry: vi.fn().mockResolvedValue(undefined),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
} as any;
const audit = { database: vi.fn().mockResolvedValue(undefined) } as any;
const resolution = await resolveAndEmitGoalContext({
lane,
store,
audit,
taskId: "FN-1",
runContext: { runId: `${lane}-run`, agentId: "agent-1", taskId: "FN-1", phase: lane },
});
expect(resolution.classification).toMatchObject({ outcome: "applied", goalCount: 1, goalIds: ["G-1"] });
expect(audit.database).toHaveBeenCalledTimes(1);
expect(audit.database.mock.calls[0][0].metadata.lane).toBe(lane);
expect(store.recordRunAuditEvent).toHaveBeenCalledTimes(1);
expect(store.recordRunAuditEvent.mock.calls[0][0].metadata.lane).toBe(lane);
}
});
it("resolveAndEmitGoalContext handles missing getGoalStore with disabled classification", async () => {
const store = {
logEntry: vi.fn().mockResolvedValue(undefined),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
} as any;
const audit = { database: vi.fn().mockResolvedValue(undefined) } as any;
const resolution = await resolveAndEmitGoalContext({
lane: "heartbeat",
store,
audit,
taskId: "FN-1",
runContext: { runId: "hb-run", agentId: "agent-1", taskId: "FN-1", phase: "heartbeat" },
});
expect(resolution.goalContext).toBe("");
expect(resolution.classification).toMatchObject({ outcome: "disabled-or-failed", reason: "store-unavailable" });
expect(audit.database).toHaveBeenCalledTimes(1);
expect(store.recordRunAuditEvent).toHaveBeenCalledTimes(1);
});
it("emits applied audit metadata for positive injection", async () => {
const goals = [goal("G-1", "one", "2026-01-01T00:00:00.000Z"), goal("G-2", "two", "2026-01-02T00:00:00.000Z")];
const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);

View File

@@ -32,11 +32,7 @@ import {
} from "./agent-instructions.js";
import { resolveHeartbeatPromptTemplate, resolveHeartbeatScopeDisciplineMode, selectHeartbeatProcedure } from "./heartbeat-procedure-resolver.js";
import { buildPromptLayers, collapsePromptLayers } from "./prompt-layers.js";
import {
emitGoalInjectionDiagnostic,
resolveGoalContextForDiagnostics,
} from "./goal-injection-diagnostics.js";
import { emitGoalAnchoringAudit } from "./goal-anchoring-audit.js";
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
import { createLogger, heartbeatLog, formatError } from "./logger.js";
import { acquireTaskWorktree } from "./worktree-acquisition.js";
import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
@@ -2387,31 +2383,14 @@ export class HeartbeatMonitor {
heartbeatLog.log(`applied plugin prompt contributions for heartbeat surface`);
}
const heartbeatGoalResolution = resolveGoalContextForDiagnostics({
listActiveGoals:
typeof taskStore.getGoalStore === "function"
? () => taskStore.getGoalStore().listGoals({ status: "active" })
: undefined,
});
const heartbeatGoalContext = heartbeatGoalResolution.goalContext;
const heartbeatGoalClassification = heartbeatGoalResolution.classification;
await emitGoalAnchoringAudit(audit, {
const heartbeatGoalResolution = await resolveAndEmitGoalContext({
lane: "heartbeat",
taskId,
goalsInjected: heartbeatGoalClassification.goalCount,
truncated: heartbeatGoalClassification.truncated,
reason: heartbeatGoalClassification.outcome === "no-goals" ? "no-active-goals" : undefined,
});
await emitGoalInjectionDiagnostic({
lane: "heartbeat",
...heartbeatGoalClassification,
runId: run.id,
agentId,
store: taskStore,
audit,
taskId,
runContext: engineRunContext,
});
const heartbeatGoalContext = heartbeatGoalResolution.goalContext;
const heartbeatLayers = buildPromptLayers({
basePrompt: baseHeartbeatSystemPrompt,

View File

@@ -100,11 +100,7 @@ import {
buildPluginPromptSection,
} from "./agent-instructions.js";
import { buildPromptLayers, collapsePromptLayers } from "./prompt-layers.js";
import {
emitGoalInjectionDiagnostic,
resolveGoalContextForDiagnostics,
} from "./goal-injection-diagnostics.js";
import { emitGoalAnchoringAudit } from "./goal-anchoring-audit.js";
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
import type { AgentReflectionService } from "./agent-reflection.js";
import { createRunAuditor, generateSyntheticRunId, type EngineRunContext, type RunAuditor } from "./run-audit.js";
import { AutoRecoveryDispatcher } from "./auto-recovery.js";
@@ -4170,32 +4166,14 @@ export class TaskExecutor {
executorLog.log(`${task.id}: applied plugin prompt contributions for executor-system surface`);
}
const executorGoalResolution = resolveGoalContextForDiagnostics({
listActiveGoals:
typeof this.store.getGoalStore === "function"
? () => this.store.getGoalStore().listGoals({ status: "active" })
: undefined,
});
const executorGoalContext = executorGoalResolution.goalContext;
const executorGoalClassification = executorGoalResolution.classification;
await emitGoalAnchoringAudit(audit, {
const executorGoalResolution = await resolveAndEmitGoalContext({
lane: "executor",
taskId: task.id,
goalsInjected: executorGoalClassification.goalCount,
truncated: executorGoalClassification.truncated,
reason: executorGoalClassification.outcome === "no-goals" ? "no-active-goals" : undefined,
});
await emitGoalInjectionDiagnostic({
lane: "executor",
...executorGoalClassification,
runId: engineRunContext.runId,
agentId: engineRunContext.agentId,
taskId: task.id,
store: this.store,
audit,
taskId: task.id,
runContext: engineRunContext,
});
const executorGoalContext = executorGoalResolution.goalContext;
const executorLayers = buildPromptLayers({
basePrompt: getExecutorSystemPrompt(settings),

View File

@@ -15,7 +15,7 @@ if (new Set(GOAL_AUDIT_TYPES).size !== GOAL_AUDIT_TYPES.length || GOAL_AUDIT_TYP
}
/** Engine lane that attempted goal anchoring; powers dashboard filtering for cite-rate observability. */
export type GoalAnchoringLane = "heartbeat" | "executor";
export type GoalAnchoringLane = "heartbeat" | "executor" | "planning";
/**
* Structured goal-injection audit payload.

View File

@@ -1,7 +1,10 @@
import type { Goal } from "@fusion/core";
import { buildGoalContextSection, type GoalInjectionResult } from "./goal-context-injector.js";
import type { TaskStore } from "@fusion/core";
import type { GoalAnchoringLane } from "./goal-anchoring-audit.js";
import { emitGoalAnchoringAudit } from "./goal-anchoring-audit.js";
import type { EngineRunContext } from "./run-audit.js";
import type { RunAuditor } from "./run-audit.js";
import { createLogger } from "./logger.js";
const diagnosticsLog = createLogger("goal-injection-diagnostics");
@@ -15,7 +18,7 @@ export type GoalInjectionDisabledReason =
| "injector-threw";
export interface GoalInjectionDiagnostic {
lane: "heartbeat" | "executor";
lane: GoalAnchoringLane;
outcome: GoalInjectionOutcome;
goalCount: number;
goalIds: string[];
@@ -84,6 +87,14 @@ export interface GoalContextResolution {
classification: GoalInjectionClassification;
}
export interface ResolveAndEmitGoalContextInput {
lane: GoalAnchoringLane;
store: TaskStore;
audit: RunAuditor;
taskId?: string;
runContext?: EngineRunContext | null;
}
export interface ResolveGoalContextInput {
listActiveGoals?: () => Goal[];
injector?: (activeGoals: Goal[]) => GoalInjectionResult;
@@ -121,6 +132,35 @@ export function resolveGoalContextForDiagnostics(input: ResolveGoalContextInput)
}
}
export async function resolveAndEmitGoalContext(input: ResolveAndEmitGoalContextInput): Promise<GoalContextResolution> {
const resolution = resolveGoalContextForDiagnostics({
listActiveGoals:
typeof input.store.getGoalStore === "function"
? () => input.store.getGoalStore().listGoals({ status: "active" })
: undefined,
});
await emitGoalAnchoringAudit(input.audit, {
lane: input.lane,
taskId: input.taskId,
goalsInjected: resolution.classification.goalCount,
truncated: resolution.classification.truncated,
reason: resolution.classification.outcome === "no-goals" ? "no-active-goals" : undefined,
});
await emitGoalInjectionDiagnostic({
lane: input.lane,
...resolution.classification,
runId: input.runContext?.runId,
agentId: input.runContext?.agentId,
taskId: input.taskId,
store: input.store,
runContext: input.runContext,
});
return resolution;
}
function formatAgentLogLine(input: GoalInjectionDiagnostic): string {
const ids = JSON.stringify(input.goalIds);
const reason = input.reason ? ` reason=${input.reason}` : "";

View File

@@ -186,11 +186,13 @@ export {
classifyGoalInjectionFailure,
classifyGoalInjectionResult,
emitGoalInjectionDiagnostic,
resolveAndEmitGoalContext,
type GoalInjectionClassification,
type GoalInjectionDiagnostic,
type GoalInjectionDiagnosticInput,
type GoalInjectionDisabledReason,
type GoalInjectionOutcome,
type ResolveAndEmitGoalContextInput,
} from "./goal-injection-diagnostics.js";
export {
emitGoalAnchoringAudit,

View File

@@ -81,6 +81,7 @@ import {
import { runGhostBugPreflight } from "./triage-preflight.js";
import { archiveAsGhostBug } from "./self-healing.js";
import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js";
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
export const TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board.
@@ -1171,9 +1172,28 @@ export class TriageProcessor {
planLog.log(`${task.id}: applied plugin prompt contributions for triage surface`);
}
const triageRunContext = {
runId: generateSyntheticRunId("triage", task.id),
agentId: assignedAgent?.id ?? "triage",
taskId: task.id,
taskLineageId: task.lineageId,
phase: "plan",
source: "triage",
} as const;
const runAuditor = createRunAuditor(this.store, triageRunContext);
const triageGoalResolution = await resolveAndEmitGoalContext({
lane: "planning",
store: this.store,
audit: runAuditor,
taskId: task.id,
runContext: triageRunContext,
});
const triageLayers = buildPromptLayers({
basePrompt: resolveAgentPrompt("triage", settings.agentPrompts)
|| (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT),
goalContext: triageGoalResolution.goalContext,
agentInstructions: [
triageIdentitySection,
triageInstructions,
@@ -1210,14 +1230,6 @@ export class TriageProcessor {
defaultProvider: planningModel.provider,
defaultModelId: planningModel.modelId,
};
const runAuditor = createRunAuditor(this.store, {
runId: generateSyntheticRunId("triage", task.id),
agentId: assignedAgent?.id ?? "triage",
taskId: task.id,
taskLineageId: task.lineageId,
phase: "plan",
source: "triage",
});
let { session } = await createResolvedAgentSession({
sessionPurpose: "triage",