Harden specialist backlog loop

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 22:49:19 -04:00
1 parent a900459d87
commit 770cdde4a7
10 files changed
+406 -28

No files matched your search

+27 -6
View File
@@ -1022,7 +1022,27 @@ cancelled
```
Items SHOULD identify owner, dependencies, affected domains, acceptance
criteria, evidence, and current commit when applicable.
criteria, evidence, the commit where the issue was observed, and current commit
when applicable.
The queue is singular across Program Director, domain lead, Session Manager,
and specialist views. Management layers sequence and route this queue; they do
not create parallel backlogs. For a specialist, next-work resolution MUST:
1. return that specialist's existing active assignment before any ready item;
2. otherwise return the first eligible ready item in deterministic queue order;
3. otherwise report the area idle without inventing work.
Assignment MUST fail closed when the specialist already owns another active
item. This invariant is serialized across different task files, not merely
protected by per-task revision locks. Blocked work is durable but not runnable;
it may be reconsidered explicitly when its dependency evidence changes.
Backlog health is read-only and reports exceptional conditions, including
multiple active assignments, stale assigned work, resolved dependencies on a
blocked item, missing or retired areas, old observation commits, ready work
without a live eligible specialist, and active work whose runtime session is
missing. Health never rewrites or reassigns work automatically.
Owner decisions affecting future work MUST be recorded durably. Chat messages
alone are insufficient. Project source, tests, approved configuration, and
@@ -1030,11 +1050,12 @@ accepted decision records outrank summaries and generated prompts.
Implementation checkpoint (2026-09-27): backlog items are revisioned JSON
records in the Session Manager's tracked worktree. The service and equivalent
CLI/MCP operations enforce transition, owner, dependency, and concurrent-write
rules; publish assignments and blockers through durable messaging; surface
active work in doctor; and require a landed integration record at the same
commit before completion. Keeping management records in the manager worktree
prevents backlog activity from dirtying canonical main.
CLI/MCP operations enforce transition, owner, dependency, one-active-item, and
concurrent-write rules; resolve assigned work before ready work; publish
assignments and blockers through durable messaging; surface active work and
health anomalies in doctor; and require a landed integration record at the
same commit before completion. Keeping management records in the manager
worktree prevents backlog activity from dirtying canonical main.
## 18. Checks and evidence
@@ -85,8 +85,8 @@ test_integrity_note:
then: each operation observes the same durable control-plane state
- id: SCN-cli-backlog
given: an installed fleet with Session Manager and worker worktrees
when: public CLI commands create, ready, assign, and list a tracked task
then: revisioned backlog state changes in the manager worktree and the assigned owner is visible through the same service
when: public CLI commands create, ready, assign, resolve next work, inspect health, and list a tracked task
then: revisioned backlog state changes in the manager worktree, next resumes the assignment before ready work, and health reports anomalies without mutation
- id: SCN-cli-self-host
given: two isolated candidate artifacts with complete named acceptance evidence
when: an owner plans, builds, activates, upgrades, and rolls back through the CLI
@@ -121,8 +121,12 @@ test_integrity_note:
then: provider-specific commands and runtime IDs remain separate, every create and resume launch receives identity-bound TORCH MCP access, the manager starts last, Codex completes idle, and only resident processes are stopped
- id: SCN-backlog-lifecycle
given: tracked tasks in the Session Manager worktree with domain owners, dependencies, and an integration lookup
when: tasks move through assignment, work, blocking, verification, readiness, landing, and completion
then: authority and optimistic revisions are enforced, concurrent locks fail closed, dependencies gate assignment, doctor observes active work, and completion requires the same landed commit
when: tasks move through assignment, restart-aware next-work resolution, work, blocking, verification, readiness, landing, and completion
then: authority and optimistic revisions are enforced, assignment locks prevent multiple active items per specialist, existing work outranks ready work, health is read-only, dependencies gate assignment, doctor observes active work, and completion requires the same landed commit
- id: SCN-backlog-health
given: a queue with duplicate active ownership, stale work, resolved blocked dependencies, retired areas, old observation commits, and missing specialist sessions
when: backlog health inspects the queue
then: every unusual condition is reported with no task mutation or automatic repair
- id: SCN-candidate-isolation
given: an inactive candidate in a namespace separate from project and stable runtime state
when: acceptance is incomplete, complete, or the accepted files are later modified
@@ -309,6 +313,9 @@ test_integrity_note:
- messages or handoffs silently granting authority
- workers creating tasks, changing owners, or completing work without a landed matching commit
- concurrent backlog writers silently overwriting a newer task revision
- restarted specialists skipping assigned work for a newer ready item
- different task files bypassing the one-active-assignment invariant
- backlog health silently rewriting or reassigning anomalous work
- management backlog files dirtying canonical main instead of the manager worktree
- repository content redirecting machine-local database writes
- MCP and CLI drifting onto separate business logic
+206 -2
View File
@@ -1,4 +1,5 @@
import { randomUUID } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import {
closeSync, existsSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, writeSync,
} from 'node:fs';
@@ -25,6 +26,7 @@ const TRANSITIONS = new Map([
const PRIORITIES = new Set(['urgent', 'high', 'normal', 'low']);
const WORKER_TRANSITIONS = new Set(['in_progress', 'blocked', 'verification', 'ready_to_integrate']);
const ACTIVE_ASSIGNMENT_STATES = new Set(['assigned', 'in_progress', 'verification', 'ready_to_integrate']);
function requiredText(value, name) {
if (typeof value !== 'string' || !value.trim()) {
@@ -52,6 +54,137 @@ function taskId(value) {
return id;
}
function optionalObservedAt(value) {
if (value === undefined || value === null || value === '') return null;
const observedAt = requiredText(value, 'observedAt');
if (!/^[0-9a-f]{7,64}$/i.test(observedAt)) {
throw new TorchError('observedAt must be a Git commit SHA', {
code: 'INVALID_BACKLOG_INPUT', details: { field: 'observedAt' },
});
}
return observedAt;
}
function headSha(repositoryRoot) {
try {
return execFileSync('git', ['-C', repositoryRoot, 'rev-parse', '--short=12', 'HEAD'], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
}).trim() || null;
} catch {
return null;
}
}
export function backlogObservedCommitDistance(repositoryRoot, observedAt) {
try {
const value = execFileSync('git', ['-C', repositoryRoot, 'rev-list', '--count', `${observedAt}..HEAD`], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return Number.isInteger(Number(value)) ? Number(value) : null;
} catch {
return null;
}
}
export function assessBacklogHealth({
tasks, agents = [], now = new Date(), staleAfterDays = 7, staleObservedCommits = 50,
commitDistance = () => null,
} = {}) {
const assessedAt = now instanceof Date ? now : new Date(now);
if (!Number.isFinite(assessedAt.getTime())) {
throw new TorchError('Backlog health requires a valid assessment time', { code: 'INVALID_BACKLOG_INPUT' });
}
if (!Number.isInteger(staleAfterDays) || staleAfterDays < 0
|| !Number.isInteger(staleObservedCommits) || staleObservedCommits < 0) {
throw new TorchError('Backlog health thresholds must be non-negative integers', {
code: 'INVALID_BACKLOG_INPUT',
});
}
const items = Array.isArray(tasks) ? tasks : [];
const agentById = new Map(agents.map((agent) => [agent.areaId ?? agent.area_id, agent]));
const taskById = new Map(items.map((task) => [task.id, task]));
const findings = [];
const activeByOwner = new Map();
for (const task of items) {
if (task.owner && ACTIVE_ASSIGNMENT_STATES.has(task.state)) {
const owned = activeByOwner.get(task.owner) ?? [];
owned.push(task);
activeByOwner.set(task.owner, owned);
}
}
for (const [owner, owned] of activeByOwner) {
if (owned.length > 1) findings.push({
severity: 'error', code: 'BACKLOG_MULTIPLE_ACTIVE_ASSIGNMENTS', owner,
tasks: owned.map((task) => ({ id: task.id, state: task.state })),
recommendation: 'Keep one active assignment for this specialist and deliberately requeue the others.',
});
}
for (const task of items) {
const referencedAreas = [...new Set([task.owner, ...(task.affectedDomains ?? [])].filter(Boolean))];
const unknownAreas = referencedAreas.filter((areaId) => !agentById.has(areaId));
if (unknownAreas.length) findings.push({
severity: 'error', code: 'BACKLOG_AREA_UNKNOWN', taskId: task.id, areas: unknownAreas,
recommendation: 'Route the task to a defined Fleet identity or restore the retired identity before assignment.',
});
if (task.state === 'assigned') {
const updatedAt = Date.parse(task.updatedAt);
const ageDays = Number.isFinite(updatedAt)
? Math.floor(Math.max(0, assessedAt.getTime() - updatedAt) / 86_400_000) : null;
if (ageDays === null || ageDays >= staleAfterDays) findings.push({
severity: 'warning', code: 'BACKLOG_ASSIGNED_STALE', taskId: task.id, owner: task.owner, ageDays,
recommendation: 'Confirm the specialist is still working this assignment before routing more work.',
});
}
if (task.owner && ACTIVE_ASSIGNMENT_STATES.has(task.state)) {
const agent = agentById.get(task.owner);
if (agent && (agent.state === 'offline' || !agent.runtimeSessionId && !agent.runtime_session_id)) findings.push({
severity: 'warning', code: 'BACKLOG_ASSIGNED_SESSION_MISSING', taskId: task.id, owner: task.owner,
recommendation: 'Resume the owning specialist or explicitly requeue the task; do not silently assign a second item.',
});
}
if (task.state === 'blocked' && (task.dependencies ?? []).length > 0
&& task.dependencies.every((dependency) => taskById.get(dependency)?.state === 'completed')) {
findings.push({
severity: 'warning', code: 'BACKLOG_BLOCKED_DEPENDENCIES_RESOLVED', taskId: task.id,
dependencies: task.dependencies,
recommendation: 'Re-check the blocker; all recorded dependencies are now complete.',
});
}
if (task.state === 'ready' && (task.affectedDomains ?? []).length > 0) {
const defined = task.affectedDomains.map((areaId) => agentById.get(areaId)).filter(Boolean);
if (defined.length > 0 && defined.every((agent) => agent.state === 'offline')) findings.push({
severity: 'warning', code: 'BACKLOG_READY_NO_LIVE_SPECIALIST', taskId: task.id,
areas: defined.map((agent) => agent.areaId ?? agent.area_id),
recommendation: 'Start or restore an eligible specialist when this ready work should be dispatched.',
});
}
if (task.observedAt) {
const distance = commitDistance(task.observedAt);
if (distance === null) findings.push({
severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_MISSING', taskId: task.id, observedAt: task.observedAt,
recommendation: 'Re-observe the task against a reachable commit before implementation.',
});
else if (distance >= staleObservedCommits) findings.push({
severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: task.id,
observedAt: task.observedAt, commitsBehind: distance,
recommendation: 'Verify the issue still reproduces before changing code.',
});
}
}
return {
schema: 'torch.dev/backlog-health/v1alpha1', assessedAt: assessedAt.toISOString(),
healthy: findings.length === 0, findings, mutationPerformed: false,
};
}
function parseTask(path) {
let task;
try { task = JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
@@ -131,6 +264,26 @@ export class BacklogService {
}
}
#withAssignmentLock(callback) {
const path = join(this.controlPlane.stateRoot, 'locks', 'backlog-assignment.lock');
let descriptor;
try {
descriptor = openSync(path, 'wx', 0o600);
writeSync(descriptor, `${JSON.stringify({ createdAt: this.clock().toISOString() })}\n`);
} catch (error) {
if (error.code === 'EEXIST') {
throw new TorchError('Another backlog assignment is already being evaluated', {
code: 'BACKLOG_ASSIGNMENT_LOCKED', details: { lockPath: path },
});
}
throw error;
}
try { return callback(); } finally {
closeSync(descriptor);
unlinkSync(path);
}
}
get(id) {
const path = this.#path(id);
if (!existsSync(path)) throw new TorchError(`Unknown backlog task: ${id}`, { code: 'BACKLOG_TASK_NOT_FOUND' });
@@ -149,9 +302,47 @@ export class BacklogService {
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
}
next({ areaId } = {}) {
const area = this.controlPlane.assertIdentity(areaId);
if (area === 'session-manager') {
throw new TorchError('Session Manager does not consume the specialist implementation queue', {
code: 'BACKLOG_OWNER_INVALID', details: { areaId: area },
});
}
const tasks = this.list();
const active = tasks.filter((task) => task.owner === area && ACTIVE_ASSIGNMENT_STATES.has(task.state));
if (active.length > 1) {
throw new TorchError(`${area} has multiple active backlog assignments`, {
code: 'BACKLOG_MULTIPLE_ACTIVE_ASSIGNMENTS',
details: { areaId: area, tasks: active.map((task) => ({ id: task.id, state: task.state })) },
});
}
if (active.length === 1) {
return {
schema: 'torch.dev/backlog-next/v1alpha1', areaId: area, disposition: 'resume',
task: active[0], mutationPerformed: false,
};
}
const ready = tasks.find((task) => task.state === 'ready'
&& (!task.affectedDomains.length || task.affectedDomains.includes(area))
&& task.dependencies.every((dependency) => tasks.find((item) => item.id === dependency)?.state === 'completed'));
return {
schema: 'torch.dev/backlog-next/v1alpha1', areaId: area,
disposition: ready ? 'ready' : 'idle', task: ready ?? null, mutationPerformed: false,
};
}
health({ staleAfterDays = 7, staleObservedCommits = 50, now = this.clock() } = {}) {
return assessBacklogHealth({
tasks: this.list(), agents: this.controlPlane.listAgents(), now,
staleAfterDays, staleObservedCommits,
commitDistance: (observedAt) => backlogObservedCommitDistance(this.repositoryRoot, observedAt),
});
}
create({
actorId, title, description, priority = 'normal', affectedDomains = [],
dependencies = [], acceptanceCriteria,
dependencies = [], acceptanceCriteria, observedAt,
} = {}) {
this.#assertMutable();
const actor = this.controlPlane.assertIdentity(actorId);
@@ -175,6 +366,7 @@ export class BacklogService {
state: 'proposed', owner: null, affectedDomains: domains, dependencies: dependencyIds,
acceptanceCriteria: textList(acceptanceCriteria, 'acceptanceCriteria', { required: true }),
evidence: [], commit: null, integrationRequest: null, blockedReason: null,
observedAt: optionalObservedAt(observedAt) ?? headSha(this.repositoryRoot),
createdAt: now, updatedAt: now, revision: 1,
history: [{ from: null, to: 'proposed', actorId: actor, at: now, note: 'Task created.' }],
};
@@ -190,7 +382,11 @@ export class BacklogService {
transition(input = {}) {
this.#assertMutable();
return this.#withTaskLock(input.taskId, () => this.#applyTransition(input));
return this.#withTaskLock(input.taskId, () => (
input.to === 'assigned'
? this.#withAssignmentLock(() => this.#applyTransition(input))
: this.#applyTransition(input)
));
}
#applyTransition({
@@ -243,6 +439,14 @@ export class BacklogService {
});
}
}
const existing = this.list().find((candidate) => candidate.id !== task.id
&& candidate.owner === assigned && ACTIVE_ASSIGNMENT_STATES.has(candidate.state));
if (existing) {
throw new TorchError(`${assigned} already has active backlog work`, {
code: 'BACKLOG_OWNER_BUSY',
details: { owner: assigned, taskId: existing.id, state: existing.state },
});
}
}
const nextEvidence = [...new Set([...task.evidence, ...textList(evidence, 'evidence')])];
const nextCommit = commit ?? task.commit;
+13 -1
View File
@@ -76,8 +76,10 @@ Usage:
torch coordinate --from <id> --body <text> [--with <id,id>] [--task <id>] [--path <path>] [--json]
torch handoff --from <id> [--to <id>] (--path <path> | --task <id>) --reason <text> [--json]
torch backlog list [--state <state>] [--owner <id>] [--json]
torch backlog next --area <id> [--json]
torch backlog health [--stale-days <n>] [--stale-observed-commits <n>] [--json]
torch backlog get --task <id> [--json]
torch backlog create --area <id> --title <text> --description <text> --accept <text,...> [--priority <priority>] [--domains <id,...>] [--depends <task,...>] [--json]
torch backlog create --area <id> --title <text> --description <text> --accept <text,...> [--priority <priority>] [--domains <id,...>] [--depends <task,...>] [--observed-at <sha>] [--json]
torch backlog transition --task <id> --area <id> --state <state> --revision <n> [--owner <id>] [--evidence <text,...>] [--commit <sha>] [--integration <id>] [--reason <text>] [--note <text>] [--json]
torch checks list [--json]
torch checks receipts [--commit <sha>] [--id <check>] [--area <id>] [--json]
@@ -813,6 +815,15 @@ export async function runCli(argv = process.argv.slice(2), {
if (operation === 'list') print({ tasks: backlog.list({
state: optionValue(argv, '--state'), owner: optionValue(argv, '--owner'),
}) }, { json });
else if (operation === 'next') print(backlog.next({
areaId: optionValue(argv, '--area'),
}), { json });
else if (operation === 'health') print(backlog.health({
staleAfterDays: optionValue(argv, '--stale-days') === undefined
? 7 : Number(optionValue(argv, '--stale-days')),
staleObservedCommits: optionValue(argv, '--stale-observed-commits') === undefined
? 50 : Number(optionValue(argv, '--stale-observed-commits')),
}), { json });
else if (operation === 'get') print(backlog.get(optionValue(argv, '--task')), { json });
else if (operation === 'create') print(backlog.create({
actorId: optionValue(argv, '--area'), title: optionValue(argv, '--title'),
@@ -820,6 +831,7 @@ export async function runCli(argv = process.argv.slice(2), {
acceptanceCriteria: commaList(optionValue(argv, '--accept')),
affectedDomains: commaList(optionValue(argv, '--domains')),
dependencies: commaList(optionValue(argv, '--depends')),
observedAt: optionValue(argv, '--observed-at'),
}), { json });
else if (operation === 'transition') print(backlog.transition({
taskId: optionValue(argv, '--task'), actorId: optionValue(argv, '--area'),
+25 -8
View File
@@ -9,6 +9,7 @@ import { forgeStatus } from '../forge/service.mjs';
import { classifyRecoverability } from '../canonical/local.mjs';
import { createClaudeAdapter } from '../adapters/claude.mjs';
import { createCodexAdapter } from '../adapters/codex.mjs';
import { assessBacklogHealth, backlogObservedCommitDistance } from '../backlog/service.mjs';
function parseJsonYaml(path) {
return JSON.parse(readFileSync(path, 'utf8'));
@@ -19,6 +20,7 @@ export function diagnoseProject({ repository, env = process.env, now = () => new
const configPath = join(trackedRoot, 'torch.yaml');
const manifestPath = join(trackedRoot, 'install-manifest.json');
const findings = [];
let identityRows = [];
if (!existsSync(configPath)) {
return { status: 'not-installed', healthy: false, findings: [{ severity: 'error', code: 'CONFIG_MISSING', path: configPath }] };
@@ -78,10 +80,11 @@ export function diagnoseProject({ repository, env = process.env, now = () => new
LEFT JOIN message_acks a ON a.message_id = m.id AND a.area_id = i.area_id
WHERE a.message_id IS NULL
`).get().count;
const present = database.prepare(`
identityRows = database.prepare(`
SELECT area_id, state, runtime, runtime_session_id, heartbeat_at
FROM identities WHERE state != 'offline' ORDER BY area_id
FROM identities ORDER BY area_id
`).all();
const present = identityRows.filter((identity) => identity.state !== 'offline');
if (unacknowledged > 0) {
findings.push({ severity: 'warning', code: 'MESSAGE_BACKLOG', unacknowledged });
}
@@ -167,19 +170,33 @@ export function diagnoseProject({ repository, env = process.env, now = () => new
try {
const task = parseJsonYaml(join(backlogRoot, name));
if (task.schema !== 'torch.dev/backlog-item/v1alpha1' || !task.id || !task.state) throw new Error('invalid task schema');
if (!['completed', 'cancelled'].includes(task.state)) {
tasks.push({ id: task.id, state: task.state, owner: task.owner, revision: task.revision });
}
tasks.push(task);
} catch (error) {
findings.push({
severity: 'error', code: 'BACKLOG_TASK_INVALID', path: join(backlogRoot, name), message: error.message,
});
}
}
if (tasks.length) findings.push({
severity: tasks.some((task) => task.state === 'blocked') ? 'warning' : 'info',
code: 'BACKLOG_ACTIVITY', tasks,
const activeTasks = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
if (activeTasks.length) findings.push({
severity: activeTasks.some((task) => task.state === 'blocked') ? 'warning' : 'info',
code: 'BACKLOG_ACTIVITY', tasks: activeTasks.map((task) => ({
id: task.id, state: task.state, owner: task.owner, revision: task.revision,
})),
});
try {
const roster = parseJsonYaml(join(trackedRoot, 'roster.yaml'));
const presenceByArea = new Map(identityRows.map((identity) => [identity.area_id, identity]));
const agents = (roster.areas ?? []).map((area) => ({
...area, areaId: area.id, ...(presenceByArea.get(area.id) ?? { state: 'offline' }),
}));
findings.push(...assessBacklogHealth({
tasks, agents, now: now(),
commitDistance: (observedAt) => backlogObservedCommitDistance(repository.root, observedAt),
}).findings);
} catch (error) {
findings.push({ severity: 'error', code: 'BACKLOG_HEALTH_INVALID', message: error.message });
}
}
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes })
const managerPrompt = `${promptContent(
proposal.session_manager?.prompt_source,
'# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n',
).trimEnd()}\n\n## Fleet evolution\n\nAt startup, after backlog intake, and when repeated handoffs or cross-domain work appear, call \`torch_assess_fleet_evolution\`. Treat its threshold as a prompt for architectural judgment, not as an automatic decision. When recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, inspect repository evidence and use \`torch_propose_domain\` to submit an evidence-backed Fleet change. When a specialist no longer earns its coordination cost, use \`torch_propose_domain_retirement\`; retirement preserves its branch and refuses active or unrecoverable work. Use \`torch_list_fleet_changes\` to follow change state. Never create, retire, approve, activate, or start a persistent identity yourself; owner approval, CLI activation, and quota-consuming runtime start are separate. Recommend a merge or split for owner review when boundaries should change but do not silently rewrite ownership.\n`;
).trimEnd()}\n\n## Backlog loop\n\nKeep one durable Fleet backlog. Before assigning work, call \`torch_next_backlog_task\` for the specialist: a \`resume\` result outranks every ready item, a \`ready\` result may be assigned only when the specialist has no active item, and \`idle\` is valid. After completion, repeat the same lookup. Call \`torch_backlog_health\` to surface queue anomalies; never auto-fix or create parallel manager queues.\n\n## Fleet evolution\n\nAt startup, after backlog intake, and when repeated handoffs or cross-domain work appear, call \`torch_assess_fleet_evolution\`. Treat its threshold as a prompt for architectural judgment, not as an automatic decision. When recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, inspect repository evidence and use \`torch_propose_domain\` to submit an evidence-backed Fleet change. When a specialist no longer earns its coordination cost, use \`torch_propose_domain_retirement\`; retirement preserves its branch and refuses active or unrecoverable work. Use \`torch_list_fleet_changes\` to follow change state. Never create, retire, approve, activate, or start a persistent identity yourself; owner approval, CLI activation, and quota-consuming runtime start are separate. Recommend a merge or split for owner review when boundaries should change but do not silently rewrite ownership.\n`;
const files = new Map([
['torch.yaml', jsonYaml(config)],
['roster.yaml', jsonYaml(roster)],
+5 -2
View File
@@ -52,11 +52,14 @@ export function renderDomainPrompt(domain) {
'',
'## Initial backlog',
'',
'- No task is implied by session creation. Read the tracked backlog and accept only an explicit assignment.',
'- No task is implied by session creation. Call `torch_next_backlog_task` for this area before choosing work.',
'- A `resume` result is the only current assignment. Continue it before considering any `ready` item.',
'- A `ready` result is a candidate, not an assignment; request Session Manager assignment before implementation.',
'- An `idle` result is valid. Do not invent filler work.',
'',
'## First move',
'',
'Query live identity and ownership, read the assigned backlog item and inbox, then report current repository evidence to the Session Manager.',
'Query live identity and ownership, call `torch_next_backlog_task`, read the inbox, then report current repository evidence to the Session Manager.',
'',
].join('\n');
}
+28 -2
View File
@@ -29,6 +29,8 @@ export const TORCH_CORE_MCP_TOOL_NAMES = Object.freeze([
export const TORCH_MCP_TOOL_NAMES = Object.freeze([
...TORCH_CORE_MCP_TOOL_NAMES,
'torch_list_backlog',
'torch_next_backlog_task',
'torch_backlog_health',
'torch_get_backlog_task',
'torch_create_backlog_task',
'torch_transition_backlog_task',
@@ -199,6 +201,29 @@ export function createTorchToolset(controlPlane, {
schema: { state: z.string().min(1).optional(), owner: z.string().min(1).optional() },
invoke: (input) => ({ tasks: backlogService.list(input) }),
});
tools.set('torch_next_backlog_task', {
description: 'Resume this specialist\'s active assignment before returning its first eligible ready task. Read-only.',
schema: { area_id: z.string().min(1).optional() },
invoke: ({ area_id: requestedArea }) => {
const bound = controlPlane.assertIdentity(actorId);
const areaId = requestedArea ?? bound;
if (bound !== 'session-manager' && areaId !== bound) {
throw new TorchError('A specialist may only inspect its own next backlog item', {
code: 'BACKLOG_AUTHORITY_REQUIRED', details: { actorId: bound, areaId },
});
}
return backlogService.next({ areaId });
},
});
tools.set('torch_backlog_health', {
description: 'Report unusual backlog conditions without changing any task.',
schema: {
stale_after_days: z.number().int().min(0).optional(),
stale_observed_commits: z.number().int().min(0).optional(),
},
invoke: ({ stale_after_days: staleAfterDays, stale_observed_commits: staleObservedCommits }) =>
backlogService.health({ staleAfterDays, staleObservedCommits }),
});
tools.set('torch_get_backlog_task', {
description: 'Read one durable tracked backlog task and its transition history.',
schema: { task_id: z.string().min(1) },
@@ -212,12 +237,13 @@ export function createTorchToolset(controlPlane, {
affected_domains: z.array(z.string().min(1)).optional(),
dependencies: z.array(z.string().min(1)).optional(),
acceptance_criteria: z.array(z.string().min(1)).min(1),
observed_at: z.string().min(1).optional(),
},
invoke: ({
actor_id: actorIdClaim, affected_domains: affectedDomains,
acceptance_criteria: acceptanceCriteria, ...input
acceptance_criteria: acceptanceCriteria, observed_at: observedAt, ...input
}) => backlogService.create({
...input, affectedDomains, acceptanceCriteria,
...input, affectedDomains, acceptanceCriteria, observedAt,
actorId: claimedIdentity(controlPlane, actorId, actorIdClaim, 'actor_id'),
}),
});
+79 -2
View File
@@ -4,7 +4,7 @@ import { mkdtempSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import test from 'node:test';
import { createBacklogService } from '../../src/backlog/service.mjs';
import { assessBacklogHealth, createBacklogService } from '../../src/backlog/service.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { diagnoseProject } from '../../src/kernel/doctor.mjs';
@@ -37,6 +37,44 @@ function fixture() {
return { root, env, worker: proposal.domains[0].id, mainStatus };
}
test('SCN-backlog-health: unusual queue conditions are reported without repair', () => {
const base = {
schema: 'torch.dev/backlog-item/v1alpha1', priority: 'normal', affectedDomains: ['runtime'],
dependencies: [], acceptanceCriteria: ['Observed.'], evidence: [], commit: null,
integrationRequest: null, blockedReason: null, createdAt: '2026-09-01T00:00:00Z',
updatedAt: '2026-09-01T00:00:00Z', revision: 1, history: [], observedAt: '1111111',
};
const tasks = [
{ ...base, id: 'TASK-active-1', title: 'One', description: 'One', state: 'assigned', owner: 'runtime' },
{ ...base, id: 'TASK-active-2', title: 'Two', description: 'Two', state: 'in_progress', owner: 'runtime' },
{ ...base, id: 'TASK-dependency', title: 'Done', description: 'Done', state: 'completed', owner: 'runtime' },
{
...base, id: 'TASK-blocked', title: 'Blocked', description: 'Blocked', state: 'blocked', owner: 'runtime',
dependencies: ['TASK-dependency'], blockedReason: 'Waiting on dependency.',
},
{ ...base, id: 'TASK-ready', title: 'Ready', description: 'Ready', state: 'ready', owner: null },
{
...base, id: 'TASK-retired', title: 'Retired', description: 'Retired', state: 'ready', owner: null,
affectedDomains: ['retired-area'],
},
];
const health = assessBacklogHealth({
tasks, now: new Date('2026-09-27T00:00:00Z'), staleAfterDays: 7, staleObservedCommits: 10,
agents: [{ areaId: 'runtime', state: 'offline', runtimeSessionId: null }],
commitDistance: () => 60,
});
const codes = new Set(health.findings.map((finding) => finding.code));
assert.deepEqual([...[
'BACKLOG_MULTIPLE_ACTIVE_ASSIGNMENTS', 'BACKLOG_ASSIGNED_STALE',
'BACKLOG_ASSIGNED_SESSION_MISSING', 'BACKLOG_BLOCKED_DEPENDENCIES_RESOLVED',
'BACKLOG_READY_NO_LIVE_SPECIALIST', 'BACKLOG_AREA_UNKNOWN',
'BACKLOG_OBSERVED_COMMIT_STALE',
]].filter((code) => !codes.has(code)), []);
assert.equal(health.healthy, false);
assert.equal(health.mutationPerformed, false);
assert.equal(tasks[0].state, 'assigned');
});
test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, dependencies, evidence, and landed completion', () => {
const context = fixture();
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
@@ -52,6 +90,10 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende
actorId: 'session-manager', title: 'Implement lifecycle', description: 'Build and verify it.',
affectedDomains: [context.worker], acceptanceCriteria: ['Exact test passes.'],
});
const alternate = backlog.create({
actorId: 'session-manager', title: 'Follow-up lifecycle', description: 'Wait behind active work.',
affectedDomains: [context.worker], acceptanceCriteria: ['Runs only after current work.'],
});
const dependent = backlog.create({
actorId: 'session-manager', title: 'Use lifecycle', description: 'Consume the completed work.',
affectedDomains: [context.worker], dependencies: [first.id], acceptanceCriteria: ['Dependency is complete.'],
@@ -63,6 +105,26 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende
taskId: first.id, actorId: 'session-manager', to: 'assigned', expectedRevision: task.revision,
owner: context.worker,
});
let alternateTask = backlog.transition({
taskId: alternate.id, actorId: 'session-manager', to: 'ready', expectedRevision: alternate.revision,
});
const nextWhileAssigned = backlog.next({ areaId: context.worker });
assert.equal(nextWhileAssigned.disposition, 'resume');
assert.equal(nextWhileAssigned.task.id, first.id, 'assigned work must outrank ready work after restart');
assert.equal(nextWhileAssigned.mutationPerformed, false);
assert.throws(
() => backlog.transition({
taskId: alternate.id, actorId: 'session-manager', to: 'assigned',
expectedRevision: alternateTask.revision, owner: context.worker,
}),
(error) => error.code === 'BACKLOG_OWNER_BUSY' && error.details.taskId === first.id,
);
const unhealthy = backlog.health({ staleAfterDays: 0 });
assert.equal(unhealthy.mutationPerformed, false);
assert.equal(unhealthy.findings.some((finding) =>
finding.code === 'BACKLOG_ASSIGNED_STALE' && finding.taskId === first.id), true);
assert.equal(unhealthy.findings.some((finding) =>
finding.code === 'BACKLOG_ASSIGNED_SESSION_MISSING' && finding.taskId === first.id), true);
assert.equal(control.readMessages({ recipient: context.worker }).at(-1).references.task, first.id);
assert.throws(
() => backlog.transition({
@@ -121,6 +183,12 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende
const diagnosis = diagnoseProject({ repository: inspectRepository(context.root), env: context.env });
assert.equal(diagnosis.findings.some((finding) =>
finding.code === 'BACKLOG_ACTIVITY' && finding.tasks.some((item) => item.id === dependent.id)), true);
assert.equal(
diagnosis.findings.some((finding) =>
finding.code === 'BACKLOG_ASSIGNED_SESSION_MISSING' && finding.taskId === first.id),
true,
'doctor must expose backlog health anomalies without a separate queue',
);
integrations.set('integration-1', {
state: 'landed', sourceCommit: '1111111111111111111111111111111111111111',
});
@@ -128,6 +196,9 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende
taskId: first.id, actorId: 'session-manager', to: 'completed', expectedRevision: task.revision,
});
assert.equal(task.state, 'completed');
const nextAfterCompletion = backlog.next({ areaId: context.worker });
assert.equal(nextAfterCompletion.disposition, 'ready');
assert.equal(nextAfterCompletion.task.id, alternate.id, 'queue order must remain deterministic');
dependentTask = backlog.transition({
taskId: dependent.id, actorId: 'session-manager', to: 'assigned', expectedRevision: dependentTask.revision,
@@ -137,7 +208,13 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende
assert.equal(backlog.list({ state: 'completed' })[0].id, first.id);
assert.equal(callTorchTool(control, 'torch_list_backlog', {}, {
actorId: context.worker, backlogService: backlog,
}).tasks.length, 2);
}).tasks.length, 3);
assert.equal(callTorchTool(control, 'torch_next_backlog_task', {}, {
actorId: context.worker, backlogService: backlog,
}).task.id, dependent.id);
assert.equal(callTorchTool(control, 'torch_backlog_health', {}, {
actorId: context.worker, backlogService: backlog,
}).mutationPerformed, false);
assert.throws(
() => callTorchTool(control, 'torch_create_backlog_task', {
title: 'Forged task', description: 'Worker cannot create it.', acceptance_criteria: ['No.'],
+11
View File
@@ -274,6 +274,17 @@ test('SCN-cli-backlog: public commands create, transition, assign, and list trac
}
assert.equal(task.state, 'assigned');
assert.equal(task.owner, proposal.domains[0].id);
const next = JSON.parse(run(root, [
'backlog', 'next', '--area', proposal.domains[0].id, '--json',
]).stdout);
assert.equal(next.disposition, 'resume');
assert.equal(next.task.id, task.id);
assert.equal(next.mutationPerformed, false);
const health = JSON.parse(run(root, [
'backlog', 'health', '--stale-days', '0', '--json',
]).stdout);
assert.equal(health.mutationPerformed, false);
assert.equal(health.findings.some((finding) => finding.code === 'BACKLOG_ASSIGNED_STALE'), true);
const listed = JSON.parse(run(root, ['backlog', 'list', '--state', 'assigned', '--json']).stdout);
assert.equal(listed.tasks[0].id, task.id);
});