FN-6611: clarify engineer backlog auto-claim routing
Clarifies why engineer agents see but do not auto-claim backlog tasks without opt-in. - Surface actionable project and per-agent opt-in guidance in no-task heartbeat prompts. - Include eligible backlog candidates in the blocked engineer guidance so routing remains visible. - Cover disabled engineer auto-claim scenarios with prompt assertions. - Update agent and settings docs with the visible configuration paths. Files changed: docs/agents.md | 2 +- docs/settings-reference.md | 2 +- .../engine/src/__tests__/heartbeat-executor.test.ts | 11 +++++++++-- packages/engine/src/agent-heartbeat.ts | 19 ++++++++++++++++--- 4 files changed, 27 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-6611 Fusion-Task-Lineage: ffcafc50-1664-4807-826c-cccf68d31012
This commit is contained in:
@@ -567,7 +567,7 @@ When an identity-bearing, non-ephemeral agent wakes with no assigned task and `r
|
||||
Guardrails:
|
||||
- Only unpaused, unassigned, unchecked-out todo tasks with satisfied dependencies are considered
|
||||
- Claims are rejected for terminal/paused/owned/conflicting tasks
|
||||
- Implementation-task backlog pickup is executor-only by default. Engineer-role agents may opt in through project setting `engineerBacklogAutoClaim` or per-agent `runtimeConfig.engineerBacklogAutoClaim`; the per-agent value overrides the project default in both directions.
|
||||
- Implementation-task backlog pickup is executor-only by default. Engineer-role agents may opt in through **Settings → Scheduling & Capacity → "Let engineer agents auto-claim backlog tasks"** (`settings.engineerBacklogAutoClaim`) or **Agents → Agent Detail → Settings → Heartbeat Settings → "Engineer Backlog Auto-Claim"** (`runtimeConfig.engineerBacklogAutoClaim`); the per-agent value overrides the project default in both directions. If a no-task engineer wake shows compatible backlog while this is disabled, delegate the work or create a coordination follow-up instead of treating the board as empty.
|
||||
- Explicit task routing/delegation is not affected by the backlog auto-claim opt-in gate.
|
||||
- Checkout safety is preserved (`checkout_conflict` paths are non-fatal skips)
|
||||
- On successful claim, the same heartbeat run switches into task-scoped execution (no nested run re-entry)
|
||||
|
||||
@@ -308,7 +308,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `heartbeatScopeDiscipline` | `"strict" \| "lite" \| "off"` | `"strict"` | Heartbeat prompt procedure mode. `strict` keeps coordination-heavy scope discipline, `lite` restores pre-2026-05-11 wording, and `off` uses a minimal procedure. Per-agent `runtimeConfig.heartbeatScopeDiscipline` can override this default. |
|
||||
| `heartbeatPromptTemplate` | `"default" \| "compact"` | `"default"` | Heartbeat execution-prompt trim template default. Per-agent `runtimeConfig.heartbeatPromptTemplate` overrides this value. Role fallback when unset everywhere is `executor`→`default`, non-executor coordination roles→`compact`. |
|
||||
| `autoClaimCandidatesInPrompt` | `number` | `5` | Default no-task heartbeat candidate list length. Integer range `0-10`; `0` suppresses candidate prompt injection. |
|
||||
| `engineerBacklogAutoClaim` | `boolean` | `false` | Opt engineer-role agents into no-task backlog auto-claim for implementation tasks. The default remains executor-only; per-agent `runtimeConfig.engineerBacklogAutoClaim` overrides this project default, and explicit routing/delegation is unchanged. Configure the project default in **Settings → Scheduling & Capacity → Let engineer agents auto-claim backlog tasks**; configure the per-agent override in **Agents → Agent Detail → Settings → Heartbeat Settings → Engineer Backlog Auto-Claim**. |
|
||||
| `engineerBacklogAutoClaim` | `boolean` | `false` | Opt engineer-role agents into no-task backlog auto-claim for implementation tasks. The default remains executor-only; per-agent `runtimeConfig.engineerBacklogAutoClaim` overrides this project default, and explicit routing/delegation is unchanged. Configure the project default in **Settings → Scheduling & Capacity → "Let engineer agents auto-claim backlog tasks"**; configure the per-agent override in **Agents → Agent Detail → Settings → Heartbeat Settings → "Engineer Backlog Auto-Claim"**. |
|
||||
| `defaultNodeId` | `string` | `undefined` | Optional project default execution node for task dispatch. When set, tasks without a per-task `nodeId` override resolve to this node (`routing source: project-default`). See [Task Management → Node Routing](./task-management.md#node-routing). |
|
||||
| `unavailableNodePolicy` | `"block" \| "fallback-local"` | `"block"` | Project routing policy used during scheduler dispatch when a task resolves to a remote node and node health is known. `"block"` keeps the task in `todo` if the node is unhealthy; `"fallback-local"` reroutes dispatch to local execution. See [Architecture → Task Routing Architecture](./architecture.md#task-routing-architecture). |
|
||||
| `secretsAccessPolicy` | `"auto" \| "prompt" \| "deny"` | `undefined` | Project-level default secret access policy (overrides global default when present). |
|
||||
|
||||
@@ -1063,10 +1063,10 @@ describe("executeHeartbeat", () => {
|
||||
columnMovedAt: oldEnoughForBaseScore,
|
||||
} as unknown as TaskDetail;
|
||||
const scenarios = [
|
||||
{ name: "engineer default", role: "engineer", settings: {}, runtimeConfig: {}, shouldClaim: false, promptText: "engineerBacklogAutoClaim disabled" },
|
||||
{ name: "engineer default", role: "engineer", settings: {}, runtimeConfig: {}, shouldClaim: false, promptText: "compatible backlog blocked; engineerBacklogAutoClaim disabled", assertEngineerGuidance: true },
|
||||
{ name: "engineer project opt-in", role: "engineer", settings: { engineerBacklogAutoClaim: true }, runtimeConfig: {}, shouldClaim: true },
|
||||
{ name: "engineer runtime opt-in overrides project off", role: "engineer", settings: { engineerBacklogAutoClaim: false }, runtimeConfig: { engineerBacklogAutoClaim: true }, shouldClaim: true },
|
||||
{ name: "engineer runtime opt-out overrides project on", role: "engineer", settings: { engineerBacklogAutoClaim: true }, runtimeConfig: { engineerBacklogAutoClaim: false }, shouldClaim: false, promptText: "engineerBacklogAutoClaim disabled" },
|
||||
{ name: "engineer runtime opt-out overrides project on", role: "engineer", settings: { engineerBacklogAutoClaim: true }, runtimeConfig: { engineerBacklogAutoClaim: false }, shouldClaim: false, promptText: "compatible backlog blocked; engineerBacklogAutoClaim disabled", assertEngineerGuidance: true },
|
||||
{ name: "executor unchanged", role: "executor", settings: { engineerBacklogAutoClaim: false }, runtimeConfig: {}, shouldClaim: true },
|
||||
{ name: "reviewer blocked with opt-in", role: "reviewer", settings: { engineerBacklogAutoClaim: true }, runtimeConfig: {}, shouldClaim: false, promptText: "executor or opted-in engineer role required" },
|
||||
{ name: "custom blocked with opt-in", role: "custom", settings: { engineerBacklogAutoClaim: true }, runtimeConfig: {}, shouldClaim: false, promptText: "executor or opted-in engineer role required" },
|
||||
@@ -1112,6 +1112,13 @@ describe("executeHeartbeat", () => {
|
||||
expect(store.claimTaskForAgent, scenario.name).not.toHaveBeenCalled();
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
|
||||
expect(executionPrompt, scenario.name).toContain(scenario.promptText);
|
||||
if ("assertEngineerGuidance" in scenario && scenario.assertEngineerGuidance) {
|
||||
expect(executionPrompt, scenario.name).toContain("Snapshot found 1 eligible Todo task(s), but this engineer-role agent is not opted into backlog auto-claim.");
|
||||
expect(executionPrompt, scenario.name).toContain("Settings → Scheduling & Capacity → \"Let engineer agents auto-claim backlog tasks\" (settings.engineerBacklogAutoClaim)");
|
||||
expect(executionPrompt, scenario.name).toContain("Agents → Agent Detail → Settings → Heartbeat Settings → \"Engineer Backlog Auto-Claim\" (runtimeConfig.engineerBacklogAutoClaim)");
|
||||
expect(executionPrompt, scenario.name).toContain("Next action: delegate one of the listed tasks to an executor/opted-in engineer or create a coordination follow-up");
|
||||
expect(executionPrompt, scenario.name).toContain("- FN-CANDIDATE: Implementation reliability");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -283,18 +283,24 @@ function formatBacklogAutoClaimRoleStatus(agent: Agent, allowEngineer: boolean):
|
||||
if (agent.role === "engineer") {
|
||||
return allowEngineer
|
||||
? "enabled"
|
||||
: "enabled (no role-compatible candidates; engineerBacklogAutoClaim disabled)";
|
||||
: "enabled (compatible backlog blocked; engineerBacklogAutoClaim disabled)";
|
||||
}
|
||||
return allowEngineer
|
||||
? "enabled (no role-compatible candidates; executor or opted-in engineer role required)"
|
||||
: "enabled (no role-compatible candidates; executor role required)";
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:AgentRouting 2026-06-17-18:56:
|
||||
* Engineer-role no-task wakes can see compatible backlog that remains unclaimable because backlog auto-claim is executor-only by default.
|
||||
* Preserve that safety boundary while surfacing an actionable opt-in or delegation path so the agent does not treat the board as empty.
|
||||
*/
|
||||
function formatBacklogAutoClaimRoleGuidance(agent: Agent, allowEngineer: boolean, candidateCount: number): string[] {
|
||||
if (agent.role === "engineer" && !allowEngineer) {
|
||||
return [
|
||||
`- Snapshot found ${candidateCount} eligible Todo task(s), but this engineer-role agent is not opted into backlog auto-claim.`,
|
||||
"- Backlog auto-claim is executor-only by default; set project settings.engineerBacklogAutoClaim or per-agent runtimeConfig.engineerBacklogAutoClaim to true to opt engineer agents in.",
|
||||
"- Backlog auto-claim is executor-only by default; opt in at Settings → Scheduling & Capacity → \"Let engineer agents auto-claim backlog tasks\" (settings.engineerBacklogAutoClaim) or per agent at Agents → Agent Detail → Settings → Heartbeat Settings → \"Engineer Backlog Auto-Claim\" (runtimeConfig.engineerBacklogAutoClaim).",
|
||||
"- Next action: delegate one of the listed tasks to an executor/opted-in engineer or create a coordination follow-up instead of treating the board as empty.",
|
||||
];
|
||||
}
|
||||
return [
|
||||
@@ -2083,6 +2089,7 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
let autoClaimCandidates: AutoClaimCandidate[] = [];
|
||||
let autoClaimPromptCandidates: readonly AutoClaimCandidate[] = [];
|
||||
let autoClaimSnapshotCandidateCount = 0;
|
||||
let autoClaimRoleFilteredCount = 0;
|
||||
const autoClaimEnabled = isAutoClaimRelevantTasksEnabled(agent);
|
||||
@@ -2091,6 +2098,7 @@ export class HeartbeatMonitor {
|
||||
try {
|
||||
const snapshot = await this.snapshotManager.getSnapshot();
|
||||
autoClaimSnapshotCandidateCount = snapshot.tasks.length;
|
||||
autoClaimPromptCandidates = snapshot.tasks;
|
||||
const roleCompatibleCandidates = snapshot.tasks.filter((candidate) => canAgentTakeImplementationTask(agent, candidate, { allowEngineer: engineerBacklogAutoClaim }));
|
||||
const skippedIncompatibleCount = snapshot.tasks.length - roleCompatibleCandidates.length;
|
||||
autoClaimRoleFilteredCount = skippedIncompatibleCount;
|
||||
@@ -2780,7 +2788,12 @@ export class HeartbeatMonitor {
|
||||
? autoClaimCandidates
|
||||
.slice(0, promptCandidateLimit)
|
||||
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`)
|
||||
: noRoleCompatibleCandidateLines
|
||||
: [
|
||||
...noRoleCompatibleCandidateLines,
|
||||
...autoClaimPromptCandidates
|
||||
.slice(0, promptCandidateLimit)
|
||||
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`),
|
||||
]
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
Reference in New Issue
Block a user